LogPager.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. <?php
  2. /**
  3. * Contain classes to list log entries
  4. *
  5. * Copyright © 2004 Brion Vibber <brion@pobox.com>
  6. * https://www.mediawiki.org/
  7. *
  8. * This program is free software; you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation; either version 2 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License along
  19. * with this program; if not, write to the Free Software Foundation, Inc.,
  20. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  21. * http://www.gnu.org/copyleft/gpl.html
  22. *
  23. * @file
  24. */
  25. use MediaWiki\MediaWikiServices;
  26. /**
  27. * @ingroup Pager
  28. */
  29. class LogPager extends ReverseChronologicalPager {
  30. /** @var array Log types */
  31. private $types = [];
  32. /** @var string Events limited to those by performer when set */
  33. private $performer = '';
  34. /** @var string|Title Events limited to those about Title when set */
  35. private $title = '';
  36. /** @var bool */
  37. private $pattern = false;
  38. /** @var string */
  39. private $typeCGI = '';
  40. /** @var string */
  41. private $action = '';
  42. /** @var bool */
  43. private $performerRestrictionsEnforced = false;
  44. /** @var bool */
  45. private $actionRestrictionsEnforced = false;
  46. /** @var array */
  47. private $mConds;
  48. /** @var string */
  49. private $mTagFilter;
  50. /** @var LogEventsList */
  51. public $mLogEventsList;
  52. /**
  53. * @param LogEventsList $list
  54. * @param string|array $types Log types to show
  55. * @param string $performer The user who made the log entries
  56. * @param string|Title $title The page title the log entries are for
  57. * @param bool $pattern Do a prefix search rather than an exact title match
  58. * @param array $conds Extra conditions for the query
  59. * @param int|bool $year The year to start from. Default: false
  60. * @param int|bool $month The month to start from. Default: false
  61. * @param int|bool $day The day to start from. Default: false
  62. * @param string $tagFilter Tag
  63. * @param string $action Specific action (subtype) requested
  64. * @param int $logId Log entry ID, to limit to a single log entry.
  65. */
  66. public function __construct( $list, $types = [], $performer = '', $title = '',
  67. $pattern = false, $conds = [], $year = false, $month = false, $day = false,
  68. $tagFilter = '', $action = '', $logId = 0
  69. ) {
  70. parent::__construct( $list->getContext() );
  71. $this->mConds = $conds;
  72. $this->mLogEventsList = $list;
  73. $this->limitType( $types ); // also excludes hidden types
  74. $this->limitLogId( $logId );
  75. $this->limitFilterTypes();
  76. $this->limitPerformer( $performer );
  77. $this->limitTitle( $title, $pattern );
  78. $this->limitAction( $action );
  79. $this->getDateCond( $year, $month, $day );
  80. $this->mTagFilter = $tagFilter;
  81. $this->mDb = wfGetDB( DB_REPLICA, 'logpager' );
  82. }
  83. public function getDefaultQuery() {
  84. $query = parent::getDefaultQuery();
  85. $query['type'] = $this->typeCGI; // arrays won't work here
  86. $query['user'] = $this->performer;
  87. $query['day'] = $this->mDay;
  88. $query['month'] = $this->mMonth;
  89. $query['year'] = $this->mYear;
  90. return $query;
  91. }
  92. private function limitFilterTypes() {
  93. if ( $this->hasEqualsClause( 'log_id' ) ) { // T220834
  94. return;
  95. }
  96. $filterTypes = $this->getFilterParams();
  97. foreach ( $filterTypes as $type => $hide ) {
  98. if ( $hide ) {
  99. $this->mConds[] = 'log_type != ' . $this->mDb->addQuotes( $type );
  100. }
  101. }
  102. }
  103. public function getFilterParams() {
  104. global $wgFilterLogTypes;
  105. $filters = [];
  106. if ( count( $this->types ) ) {
  107. return $filters;
  108. }
  109. $wpfilters = $this->getRequest()->getArray( "wpfilters" );
  110. $request_filters = $wpfilters === null ? [] : $wpfilters;
  111. foreach ( $wgFilterLogTypes as $type => $default ) {
  112. $hide = !in_array( $type, $request_filters );
  113. // Back-compat: Check old URL params if the new param wasn't passed
  114. if ( $wpfilters === null ) {
  115. $hide = $this->getRequest()->getBool( "hide_{$type}_log", $default );
  116. }
  117. $filters[$type] = $hide;
  118. }
  119. return $filters;
  120. }
  121. /**
  122. * Set the log reader to return only entries of the given type.
  123. * Type restrictions enforced here
  124. *
  125. * @param string|array $types Log types ('upload', 'delete', etc);
  126. * empty string means no restriction
  127. */
  128. private function limitType( $types ) {
  129. global $wgLogRestrictions;
  130. $user = $this->getUser();
  131. // If $types is not an array, make it an array
  132. $types = ( $types === '' ) ? [] : (array)$types;
  133. // Don't even show header for private logs; don't recognize it...
  134. $needReindex = false;
  135. foreach ( $types as $type ) {
  136. if ( isset( $wgLogRestrictions[$type] )
  137. && !MediaWikiServices::getInstance()
  138. ->getPermissionManager()
  139. ->userHasRight( $user, $wgLogRestrictions[$type] )
  140. ) {
  141. $needReindex = true;
  142. $types = array_diff( $types, [ $type ] );
  143. }
  144. }
  145. if ( $needReindex ) {
  146. // Lots of this code makes assumptions that
  147. // the first entry in the array is $types[0].
  148. $types = array_values( $types );
  149. }
  150. $this->types = $types;
  151. // Don't show private logs to unprivileged users.
  152. // Also, only show them upon specific request to avoid suprises.
  153. $audience = $types ? 'user' : 'public';
  154. $hideLogs = LogEventsList::getExcludeClause( $this->mDb, $audience, $user );
  155. if ( $hideLogs !== false ) {
  156. $this->mConds[] = $hideLogs;
  157. }
  158. if ( count( $types ) ) {
  159. $this->mConds['log_type'] = $types;
  160. // Set typeCGI; used in url param for paging
  161. if ( count( $types ) == 1 ) {
  162. $this->typeCGI = $types[0];
  163. }
  164. }
  165. }
  166. /**
  167. * Set the log reader to return only entries by the given user.
  168. *
  169. * @param string $name (In)valid user name
  170. * @return void
  171. */
  172. private function limitPerformer( $name ) {
  173. if ( $name == '' ) {
  174. return;
  175. }
  176. $usertitle = Title::makeTitleSafe( NS_USER, $name );
  177. if ( is_null( $usertitle ) ) {
  178. return;
  179. }
  180. // Normalize username first so that non-existent users used
  181. // in maintenance scripts work
  182. $name = $usertitle->getText();
  183. // Assume no joins required for log_user
  184. $this->mConds[] = ActorMigration::newMigration()->getWhere(
  185. wfGetDB( DB_REPLICA ), 'log_user', User::newFromName( $name, false )
  186. )['conds'];
  187. $this->enforcePerformerRestrictions();
  188. $this->performer = $name;
  189. }
  190. /**
  191. * Set the log reader to return only entries affecting the given page.
  192. * (For the block and rights logs, this is a user page.)
  193. *
  194. * @param string|Title $page Title name
  195. * @param bool $pattern
  196. * @return void
  197. */
  198. private function limitTitle( $page, $pattern ) {
  199. global $wgMiserMode, $wgUserrightsInterwikiDelimiter;
  200. if ( $page instanceof Title ) {
  201. $title = $page;
  202. } else {
  203. $title = Title::newFromText( $page );
  204. if ( strlen( $page ) == 0 || !$title instanceof Title ) {
  205. return;
  206. }
  207. }
  208. $this->title = $title->getPrefixedText();
  209. $ns = $title->getNamespace();
  210. $db = $this->mDb;
  211. $doUserRightsLogLike = false;
  212. if ( $this->types == [ 'rights' ] ) {
  213. $parts = explode( $wgUserrightsInterwikiDelimiter, $title->getDBkey() );
  214. if ( count( $parts ) == 2 ) {
  215. list( $name, $database ) = array_map( 'trim', $parts );
  216. if ( strstr( $database, '*' ) ) { // Search for wildcard in database name
  217. $doUserRightsLogLike = true;
  218. }
  219. }
  220. }
  221. /**
  222. * Using the (log_namespace, log_title, log_timestamp) index with a
  223. * range scan (LIKE) on the first two parts, instead of simple equality,
  224. * makes it unusable for sorting. Sorted retrieval using another index
  225. * would be possible, but then we might have to scan arbitrarily many
  226. * nodes of that index. Therefore, we need to avoid this if $wgMiserMode
  227. * is on.
  228. *
  229. * This is not a problem with simple title matches, because then we can
  230. * use the page_time index. That should have no more than a few hundred
  231. * log entries for even the busiest pages, so it can be safely scanned
  232. * in full to satisfy an impossible condition on user or similar.
  233. */
  234. $this->mConds['log_namespace'] = $ns;
  235. if ( $doUserRightsLogLike ) {
  236. $params = [ $name . $wgUserrightsInterwikiDelimiter ];
  237. foreach ( explode( '*', $database ) as $databasepart ) {
  238. $params[] = $databasepart;
  239. $params[] = $db->anyString();
  240. }
  241. array_pop( $params ); // Get rid of the last % we added.
  242. $this->mConds[] = 'log_title' . $db->buildLike( ...$params );
  243. } elseif ( $pattern && !$wgMiserMode ) {
  244. $this->mConds[] = 'log_title' . $db->buildLike( $title->getDBkey(), $db->anyString() );
  245. $this->pattern = $pattern;
  246. } else {
  247. $this->mConds['log_title'] = $title->getDBkey();
  248. }
  249. $this->enforceActionRestrictions();
  250. }
  251. /**
  252. * Set the log_action field to a specified value (or values)
  253. *
  254. * @param string $action
  255. */
  256. private function limitAction( $action ) {
  257. global $wgActionFilteredLogs;
  258. // Allow to filter the log by actions
  259. $type = $this->typeCGI;
  260. if ( $type === '' ) {
  261. // nothing to do
  262. return;
  263. }
  264. $actions = $wgActionFilteredLogs;
  265. if ( isset( $actions[$type] ) ) {
  266. // log type can be filtered by actions
  267. $this->mLogEventsList->setAllowedActions( array_keys( $actions[$type] ) );
  268. if ( $action !== '' && isset( $actions[$type][$action] ) ) {
  269. // add condition to query
  270. $this->mConds['log_action'] = $actions[$type][$action];
  271. $this->action = $action;
  272. }
  273. }
  274. }
  275. /**
  276. * Limit to the (single) specified log ID.
  277. * @param int $logId The log entry ID.
  278. */
  279. protected function limitLogId( $logId ) {
  280. if ( !$logId ) {
  281. return;
  282. }
  283. $this->mConds['log_id'] = $logId;
  284. }
  285. /**
  286. * Constructs the most part of the query. Extra conditions are sprinkled in
  287. * all over this class.
  288. * @return array
  289. */
  290. public function getQueryInfo() {
  291. $basic = DatabaseLogEntry::getSelectQueryData();
  292. $tables = $basic['tables'];
  293. $fields = $basic['fields'];
  294. $conds = $basic['conds'];
  295. $options = $basic['options'];
  296. $joins = $basic['join_conds'];
  297. # Add log_search table if there are conditions on it.
  298. # This filters the results to only include log rows that have
  299. # log_search records with the specified ls_field and ls_value values.
  300. if ( array_key_exists( 'ls_field', $this->mConds ) ) {
  301. $tables[] = 'log_search';
  302. $options['IGNORE INDEX'] = [ 'log_search' => 'ls_log_id' ];
  303. $options['USE INDEX'] = [ 'logging' => 'PRIMARY' ];
  304. if ( !$this->hasEqualsClause( 'ls_field' )
  305. || !$this->hasEqualsClause( 'ls_value' )
  306. ) {
  307. # Since (ls_field,ls_value,ls_logid) is unique, if the condition is
  308. # to match a specific (ls_field,ls_value) tuple, then there will be
  309. # no duplicate log rows. Otherwise, we need to remove the duplicates.
  310. $options[] = 'DISTINCT';
  311. }
  312. }
  313. # Don't show duplicate rows when using log_search
  314. $joins['log_search'] = [ 'JOIN', 'ls_log_id=log_id' ];
  315. // T221458: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor` before
  316. // `logging` and filesorting is somehow better than querying $limit+1 rows from `logging`.
  317. // Tell it not to reorder the query. But not when tag filtering or log_search was used, as it
  318. // seems as likely to be harmed as helped in that case.
  319. if ( !$this->mTagFilter && !array_key_exists( 'ls_field', $this->mConds ) ) {
  320. $options[] = 'STRAIGHT_JOIN';
  321. }
  322. if ( $this->performer !== '' ) {
  323. // T223151: MariaDB's optimizer, at least 10.1, likes to choose a wildly bad plan for
  324. // some reason for this code path. Tell it not to use the wrong index it wants to pick.
  325. $options['IGNORE INDEX'] = [ 'logging' => [ 'times' ] ];
  326. }
  327. $info = [
  328. 'tables' => $tables,
  329. 'fields' => $fields,
  330. 'conds' => array_merge( $conds, $this->mConds ),
  331. 'options' => $options,
  332. 'join_conds' => $joins,
  333. ];
  334. # Add ChangeTags filter query
  335. ChangeTags::modifyDisplayQuery( $info['tables'], $info['fields'], $info['conds'],
  336. $info['join_conds'], $info['options'], $this->mTagFilter );
  337. return $info;
  338. }
  339. /**
  340. * Checks if $this->mConds has $field matched to a *single* value
  341. * @param string $field
  342. * @return bool
  343. */
  344. protected function hasEqualsClause( $field ) {
  345. return (
  346. array_key_exists( $field, $this->mConds ) &&
  347. ( !is_array( $this->mConds[$field] ) || count( $this->mConds[$field] ) == 1 )
  348. );
  349. }
  350. function getIndexField() {
  351. return 'log_timestamp';
  352. }
  353. protected function getStartBody() {
  354. # Do a link batch query
  355. if ( $this->getNumRows() > 0 ) {
  356. $lb = new LinkBatch;
  357. foreach ( $this->mResult as $row ) {
  358. $lb->add( $row->log_namespace, $row->log_title );
  359. $lb->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
  360. $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) );
  361. $formatter = LogFormatter::newFromRow( $row );
  362. foreach ( $formatter->getPreloadTitles() as $title ) {
  363. $lb->addObj( $title );
  364. }
  365. }
  366. $lb->execute();
  367. $this->mResult->seek( 0 );
  368. }
  369. return '';
  370. }
  371. public function formatRow( $row ) {
  372. return $this->mLogEventsList->logLine( $row );
  373. }
  374. public function getType() {
  375. return $this->types;
  376. }
  377. /**
  378. * Guaranteed to either return a valid title string or a Zero-Length String
  379. *
  380. * @return string
  381. */
  382. public function getPerformer() {
  383. return $this->performer;
  384. }
  385. /**
  386. * @return string
  387. */
  388. public function getPage() {
  389. return $this->title;
  390. }
  391. /**
  392. * @return bool
  393. */
  394. public function getPattern() {
  395. return $this->pattern;
  396. }
  397. public function getYear() {
  398. return $this->mYear;
  399. }
  400. public function getMonth() {
  401. return $this->mMonth;
  402. }
  403. public function getDay() {
  404. return $this->mDay;
  405. }
  406. public function getTagFilter() {
  407. return $this->mTagFilter;
  408. }
  409. public function getAction() {
  410. return $this->action;
  411. }
  412. public function doQuery() {
  413. // Workaround MySQL optimizer bug
  414. $this->mDb->setBigSelects();
  415. parent::doQuery();
  416. $this->mDb->setBigSelects( 'default' );
  417. }
  418. /**
  419. * Paranoia: avoid brute force searches (T19342)
  420. */
  421. private function enforceActionRestrictions() {
  422. if ( $this->actionRestrictionsEnforced ) {
  423. return;
  424. }
  425. $this->actionRestrictionsEnforced = true;
  426. $user = $this->getUser();
  427. $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
  428. if ( !$permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
  429. $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0';
  430. } elseif ( !$permissionManager->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' ) ) {
  431. $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_ACTION ) .
  432. ' != ' . LogPage::SUPPRESSED_USER;
  433. }
  434. }
  435. /**
  436. * Paranoia: avoid brute force searches (T19342)
  437. */
  438. private function enforcePerformerRestrictions() {
  439. // Same as enforceActionRestrictions(), except for _USER instead of _ACTION bits.
  440. if ( $this->performerRestrictionsEnforced ) {
  441. return;
  442. }
  443. $this->performerRestrictionsEnforced = true;
  444. $user = $this->getUser();
  445. $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
  446. if ( !$permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
  447. $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0';
  448. } elseif ( !$permissionManager->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' ) ) {
  449. $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_USER ) .
  450. ' != ' . LogPage::SUPPRESSED_ACTION;
  451. }
  452. }
  453. }