LogPager.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  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. /**
  26. * @ingroup Pager
  27. */
  28. class LogPager extends ReverseChronologicalPager {
  29. /** @var array Log types */
  30. private $types = [];
  31. /** @var string Events limited to those by performer when set */
  32. private $performer = '';
  33. /** @var string|Title Events limited to those about Title when set */
  34. private $title = '';
  35. /** @var string */
  36. private $pattern = '';
  37. /** @var string */
  38. private $typeCGI = '';
  39. /** @var string */
  40. private $action = '';
  41. /** @var LogEventsList */
  42. public $mLogEventsList;
  43. /**
  44. * @param LogEventsList $list
  45. * @param string|array $types Log types to show
  46. * @param string $performer The user who made the log entries
  47. * @param string|Title $title The page title the log entries are for
  48. * @param string $pattern Do a prefix search rather than an exact title match
  49. * @param array $conds Extra conditions for the query
  50. * @param int|bool $year The year to start from. Default: false
  51. * @param int|bool $month The month to start from. Default: false
  52. * @param string $tagFilter Tag
  53. * @param string $action Specific action (subtype) requested
  54. * @param int $logId Log entry ID, to limit to a single log entry.
  55. */
  56. public function __construct( $list, $types = [], $performer = '', $title = '',
  57. $pattern = '', $conds = [], $year = false, $month = false, $tagFilter = '',
  58. $action = '', $logId = false
  59. ) {
  60. parent::__construct( $list->getContext() );
  61. $this->mConds = $conds;
  62. $this->mLogEventsList = $list;
  63. $this->limitType( $types ); // also excludes hidden types
  64. $this->limitPerformer( $performer );
  65. $this->limitTitle( $title, $pattern );
  66. $this->limitAction( $action );
  67. $this->getDateCond( $year, $month );
  68. $this->mTagFilter = $tagFilter;
  69. $this->limitLogId( $logId );
  70. $this->mDb = wfGetDB( DB_REPLICA, 'logpager' );
  71. }
  72. public function getDefaultQuery() {
  73. $query = parent::getDefaultQuery();
  74. $query['type'] = $this->typeCGI; // arrays won't work here
  75. $query['user'] = $this->performer;
  76. $query['month'] = $this->mMonth;
  77. $query['year'] = $this->mYear;
  78. return $query;
  79. }
  80. // Call ONLY after calling $this->limitType() already!
  81. public function getFilterParams() {
  82. global $wgFilterLogTypes;
  83. $filters = [];
  84. if ( count( $this->types ) ) {
  85. return $filters;
  86. }
  87. foreach ( $wgFilterLogTypes as $type => $default ) {
  88. // Avoid silly filtering
  89. if ( $type !== 'patrol' || $this->getUser()->useNPPatrol() ) {
  90. $hide = $this->getRequest()->getInt( "hide_{$type}_log", $default );
  91. $filters[$type] = $hide;
  92. if ( $hide ) {
  93. $this->mConds[] = 'log_type != ' . $this->mDb->addQuotes( $type );
  94. }
  95. }
  96. }
  97. return $filters;
  98. }
  99. /**
  100. * Set the log reader to return only entries of the given type.
  101. * Type restrictions enforced here
  102. *
  103. * @param string|array $types Log types ('upload', 'delete', etc);
  104. * empty string means no restriction
  105. */
  106. private function limitType( $types ) {
  107. global $wgLogRestrictions;
  108. $user = $this->getUser();
  109. // If $types is not an array, make it an array
  110. $types = ( $types === '' ) ? [] : (array)$types;
  111. // Don't even show header for private logs; don't recognize it...
  112. $needReindex = false;
  113. foreach ( $types as $type ) {
  114. if ( isset( $wgLogRestrictions[$type] )
  115. && !$user->isAllowed( $wgLogRestrictions[$type] )
  116. ) {
  117. $needReindex = true;
  118. $types = array_diff( $types, [ $type ] );
  119. }
  120. }
  121. if ( $needReindex ) {
  122. // Lots of this code makes assumptions that
  123. // the first entry in the array is $types[0].
  124. $types = array_values( $types );
  125. }
  126. $this->types = $types;
  127. // Don't show private logs to unprivileged users.
  128. // Also, only show them upon specific request to avoid suprises.
  129. $audience = $types ? 'user' : 'public';
  130. $hideLogs = LogEventsList::getExcludeClause( $this->mDb, $audience, $user );
  131. if ( $hideLogs !== false ) {
  132. $this->mConds[] = $hideLogs;
  133. }
  134. if ( count( $types ) ) {
  135. $this->mConds['log_type'] = $types;
  136. // Set typeCGI; used in url param for paging
  137. if ( count( $types ) == 1 ) {
  138. $this->typeCGI = $types[0];
  139. }
  140. }
  141. }
  142. /**
  143. * Set the log reader to return only entries by the given user.
  144. *
  145. * @param string $name (In)valid user name
  146. * @return void
  147. */
  148. private function limitPerformer( $name ) {
  149. if ( $name == '' ) {
  150. return;
  151. }
  152. $usertitle = Title::makeTitleSafe( NS_USER, $name );
  153. if ( is_null( $usertitle ) ) {
  154. return;
  155. }
  156. // Normalize username first so that non-existent users used
  157. // in maintenance scripts work
  158. $name = $usertitle->getText();
  159. /* Fetch userid at first, if known, provides awesome query plan afterwards */
  160. $userid = User::idFromName( $name );
  161. if ( !$userid ) {
  162. $this->mConds['log_user_text'] = IP::sanitizeIP( $name );
  163. } else {
  164. $this->mConds['log_user'] = $userid;
  165. }
  166. // Paranoia: avoid brute force searches (T19342)
  167. $user = $this->getUser();
  168. if ( !$user->isAllowed( 'deletedhistory' ) ) {
  169. $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0';
  170. } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
  171. $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_USER ) .
  172. ' != ' . LogPage::SUPPRESSED_USER;
  173. }
  174. $this->performer = $name;
  175. }
  176. /**
  177. * Set the log reader to return only entries affecting the given page.
  178. * (For the block and rights logs, this is a user page.)
  179. *
  180. * @param string|Title $page Title name
  181. * @param string $pattern
  182. * @return void
  183. */
  184. private function limitTitle( $page, $pattern ) {
  185. global $wgMiserMode, $wgUserrightsInterwikiDelimiter;
  186. if ( $page instanceof Title ) {
  187. $title = $page;
  188. } else {
  189. $title = Title::newFromText( $page );
  190. if ( strlen( $page ) == 0 || !$title instanceof Title ) {
  191. return;
  192. }
  193. }
  194. $this->title = $title->getPrefixedText();
  195. $ns = $title->getNamespace();
  196. $db = $this->mDb;
  197. $doUserRightsLogLike = false;
  198. if ( $this->types == [ 'rights' ] ) {
  199. $parts = explode( $wgUserrightsInterwikiDelimiter, $title->getDBkey() );
  200. if ( count( $parts ) == 2 ) {
  201. list( $name, $database ) = array_map( 'trim', $parts );
  202. if ( strstr( $database, '*' ) ) { // Search for wildcard in database name
  203. $doUserRightsLogLike = true;
  204. }
  205. }
  206. }
  207. /**
  208. * Using the (log_namespace, log_title, log_timestamp) index with a
  209. * range scan (LIKE) on the first two parts, instead of simple equality,
  210. * makes it unusable for sorting. Sorted retrieval using another index
  211. * would be possible, but then we might have to scan arbitrarily many
  212. * nodes of that index. Therefore, we need to avoid this if $wgMiserMode
  213. * is on.
  214. *
  215. * This is not a problem with simple title matches, because then we can
  216. * use the page_time index. That should have no more than a few hundred
  217. * log entries for even the busiest pages, so it can be safely scanned
  218. * in full to satisfy an impossible condition on user or similar.
  219. */
  220. $this->mConds['log_namespace'] = $ns;
  221. if ( $doUserRightsLogLike ) {
  222. $params = [ $name . $wgUserrightsInterwikiDelimiter ];
  223. foreach ( explode( '*', $database ) as $databasepart ) {
  224. $params[] = $databasepart;
  225. $params[] = $db->anyString();
  226. }
  227. array_pop( $params ); // Get rid of the last % we added.
  228. $this->mConds[] = 'log_title' . $db->buildLike( $params );
  229. } elseif ( $pattern && !$wgMiserMode ) {
  230. $this->mConds[] = 'log_title' . $db->buildLike( $title->getDBkey(), $db->anyString() );
  231. $this->pattern = $pattern;
  232. } else {
  233. $this->mConds['log_title'] = $title->getDBkey();
  234. }
  235. // Paranoia: avoid brute force searches (T19342)
  236. $user = $this->getUser();
  237. if ( !$user->isAllowed( 'deletedhistory' ) ) {
  238. $this->mConds[] = $db->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0';
  239. } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
  240. $this->mConds[] = $db->bitAnd( 'log_deleted', LogPage::SUPPRESSED_ACTION ) .
  241. ' != ' . LogPage::SUPPRESSED_ACTION;
  242. }
  243. }
  244. /**
  245. * Set the log_action field to a specified value (or values)
  246. *
  247. * @param string $action
  248. */
  249. private function limitAction( $action ) {
  250. global $wgActionFilteredLogs;
  251. // Allow to filter the log by actions
  252. $type = $this->typeCGI;
  253. if ( $type === '' ) {
  254. // nothing to do
  255. return;
  256. }
  257. $actions = $wgActionFilteredLogs;
  258. if ( isset( $actions[$type] ) ) {
  259. // log type can be filtered by actions
  260. $this->mLogEventsList->setAllowedActions( array_keys( $actions[$type] ) );
  261. if ( $action !== '' && isset( $actions[$type][$action] ) ) {
  262. // add condition to query
  263. $this->mConds['log_action'] = $actions[$type][$action];
  264. $this->action = $action;
  265. }
  266. }
  267. }
  268. /**
  269. * Limit to the (single) specified log ID.
  270. * @param int $logId The log entry ID.
  271. */
  272. protected function limitLogId( $logId ) {
  273. if ( !$logId ) {
  274. return;
  275. }
  276. $this->mConds['log_id'] = $logId;
  277. }
  278. /**
  279. * Constructs the most part of the query. Extra conditions are sprinkled in
  280. * all over this class.
  281. * @return array
  282. */
  283. public function getQueryInfo() {
  284. $basic = DatabaseLogEntry::getSelectQueryData();
  285. $tables = $basic['tables'];
  286. $fields = $basic['fields'];
  287. $conds = $basic['conds'];
  288. $options = $basic['options'];
  289. $joins = $basic['join_conds'];
  290. # Add log_search table if there are conditions on it.
  291. # This filters the results to only include log rows that have
  292. # log_search records with the specified ls_field and ls_value values.
  293. if ( array_key_exists( 'ls_field', $this->mConds ) ) {
  294. $tables[] = 'log_search';
  295. $options['IGNORE INDEX'] = [ 'log_search' => 'ls_log_id' ];
  296. $options['USE INDEX'] = [ 'logging' => 'PRIMARY' ];
  297. if ( !$this->hasEqualsClause( 'ls_field' )
  298. || !$this->hasEqualsClause( 'ls_value' )
  299. ) {
  300. # Since (ls_field,ls_value,ls_logid) is unique, if the condition is
  301. # to match a specific (ls_field,ls_value) tuple, then there will be
  302. # no duplicate log rows. Otherwise, we need to remove the duplicates.
  303. $options[] = 'DISTINCT';
  304. }
  305. }
  306. # Don't show duplicate rows when using log_search
  307. $joins['log_search'] = [ 'INNER JOIN', 'ls_log_id=log_id' ];
  308. $info = [
  309. 'tables' => $tables,
  310. 'fields' => $fields,
  311. 'conds' => array_merge( $conds, $this->mConds ),
  312. 'options' => $options,
  313. 'join_conds' => $joins,
  314. ];
  315. # Add ChangeTags filter query
  316. ChangeTags::modifyDisplayQuery( $info['tables'], $info['fields'], $info['conds'],
  317. $info['join_conds'], $info['options'], $this->mTagFilter );
  318. return $info;
  319. }
  320. /**
  321. * Checks if $this->mConds has $field matched to a *single* value
  322. * @param string $field
  323. * @return bool
  324. */
  325. protected function hasEqualsClause( $field ) {
  326. return (
  327. array_key_exists( $field, $this->mConds ) &&
  328. ( !is_array( $this->mConds[$field] ) || count( $this->mConds[$field] ) == 1 )
  329. );
  330. }
  331. function getIndexField() {
  332. return 'log_timestamp';
  333. }
  334. public function getStartBody() {
  335. # Do a link batch query
  336. if ( $this->getNumRows() > 0 ) {
  337. $lb = new LinkBatch;
  338. foreach ( $this->mResult as $row ) {
  339. $lb->add( $row->log_namespace, $row->log_title );
  340. $lb->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
  341. $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) );
  342. $formatter = LogFormatter::newFromRow( $row );
  343. foreach ( $formatter->getPreloadTitles() as $title ) {
  344. $lb->addObj( $title );
  345. }
  346. }
  347. $lb->execute();
  348. $this->mResult->seek( 0 );
  349. }
  350. return '';
  351. }
  352. public function formatRow( $row ) {
  353. return $this->mLogEventsList->logLine( $row );
  354. }
  355. public function getType() {
  356. return $this->types;
  357. }
  358. /**
  359. * Guaranteed to either return a valid title string or a Zero-Length String
  360. *
  361. * @return string
  362. */
  363. public function getPerformer() {
  364. return $this->performer;
  365. }
  366. /**
  367. * @return string
  368. */
  369. public function getPage() {
  370. return $this->title;
  371. }
  372. public function getPattern() {
  373. return $this->pattern;
  374. }
  375. public function getYear() {
  376. return $this->mYear;
  377. }
  378. public function getMonth() {
  379. return $this->mMonth;
  380. }
  381. public function getTagFilter() {
  382. return $this->mTagFilter;
  383. }
  384. public function getAction() {
  385. return $this->action;
  386. }
  387. public function doQuery() {
  388. // Workaround MySQL optimizer bug
  389. $this->mDb->setBigSelects();
  390. parent::doQuery();
  391. $this->mDb->setBigSelects( 'default' );
  392. }
  393. }