LogEntry.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814
  1. <?php
  2. /**
  3. * Contain classes for dealing with individual log entries
  4. *
  5. * This is how I see the log system history:
  6. * - appending to plain wiki pages
  7. * - formatting log entries based on database fields
  8. * - user is now part of the action message
  9. *
  10. * This program is free software; you can redistribute it and/or modify
  11. * it under the terms of the GNU General Public License as published by
  12. * the Free Software Foundation; either version 2 of the License, or
  13. * (at your option) any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU General Public License along
  21. * with this program; if not, write to the Free Software Foundation, Inc.,
  22. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  23. * http://www.gnu.org/copyleft/gpl.html
  24. *
  25. * @file
  26. * @author Niklas Laxström
  27. * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
  28. * @since 1.19
  29. */
  30. use Wikimedia\Rdbms\IDatabase;
  31. /**
  32. * Interface for log entries. Every log entry has these methods.
  33. *
  34. * @since 1.19
  35. */
  36. interface LogEntry {
  37. /**
  38. * The main log type.
  39. *
  40. * @return string
  41. */
  42. public function getType();
  43. /**
  44. * The log subtype.
  45. *
  46. * @return string
  47. */
  48. public function getSubtype();
  49. /**
  50. * The full logtype in format maintype/subtype.
  51. *
  52. * @return string
  53. */
  54. public function getFullType();
  55. /**
  56. * Get the extra parameters stored for this message.
  57. *
  58. * @return array
  59. */
  60. public function getParameters();
  61. /**
  62. * Get the user for performed this action.
  63. *
  64. * @return User
  65. */
  66. public function getPerformer();
  67. /**
  68. * Get the target page of this action.
  69. *
  70. * @return Title
  71. */
  72. public function getTarget();
  73. /**
  74. * Get the timestamp when the action was executed.
  75. *
  76. * @return string
  77. */
  78. public function getTimestamp();
  79. /**
  80. * Get the user provided comment.
  81. *
  82. * @return string
  83. */
  84. public function getComment();
  85. /**
  86. * Get the access restriction.
  87. *
  88. * @return string
  89. */
  90. public function getDeleted();
  91. /**
  92. * @param int $field One of LogPage::DELETED_* bitfield constants
  93. * @return bool
  94. */
  95. public function isDeleted( $field );
  96. }
  97. /**
  98. * Extends the LogEntryInterface with some basic functionality
  99. *
  100. * @since 1.19
  101. */
  102. abstract class LogEntryBase implements LogEntry {
  103. public function getFullType() {
  104. return $this->getType() . '/' . $this->getSubtype();
  105. }
  106. public function isDeleted( $field ) {
  107. return ( $this->getDeleted() & $field ) === $field;
  108. }
  109. /**
  110. * Whether the parameters for this log are stored in new or
  111. * old format.
  112. *
  113. * @return bool
  114. */
  115. public function isLegacy() {
  116. return false;
  117. }
  118. /**
  119. * Create a blob from a parameter array
  120. *
  121. * @since 1.26
  122. * @param array $params
  123. * @return string
  124. */
  125. public static function makeParamBlob( $params ) {
  126. return serialize( (array)$params );
  127. }
  128. /**
  129. * Extract a parameter array from a blob
  130. *
  131. * @since 1.26
  132. * @param string $blob
  133. * @return array
  134. */
  135. public static function extractParams( $blob ) {
  136. return unserialize( $blob );
  137. }
  138. }
  139. /**
  140. * This class wraps around database result row.
  141. *
  142. * @since 1.19
  143. */
  144. class DatabaseLogEntry extends LogEntryBase {
  145. /**
  146. * Returns array of information that is needed for querying
  147. * log entries. Array contains the following keys:
  148. * tables, fields, conds, options and join_conds
  149. *
  150. * @return array
  151. */
  152. public static function getSelectQueryData() {
  153. $commentQuery = CommentStore::newKey( 'log_comment' )->getJoin();
  154. $tables = [ 'logging', 'user' ] + $commentQuery['tables'];
  155. $fields = [
  156. 'log_id', 'log_type', 'log_action', 'log_timestamp',
  157. 'log_user', 'log_user_text',
  158. 'log_namespace', 'log_title', // unused log_page
  159. 'log_params', 'log_deleted',
  160. 'user_id', 'user_name', 'user_editcount',
  161. ] + $commentQuery['fields'];
  162. $joins = [
  163. // IPs don't have an entry in user table
  164. 'user' => [ 'LEFT JOIN', 'log_user=user_id' ],
  165. ] + $commentQuery['joins'];
  166. return [
  167. 'tables' => $tables,
  168. 'fields' => $fields,
  169. 'conds' => [],
  170. 'options' => [],
  171. 'join_conds' => $joins,
  172. ];
  173. }
  174. /**
  175. * Constructs new LogEntry from database result row.
  176. * Supports rows from both logging and recentchanges table.
  177. *
  178. * @param stdClass|array $row
  179. * @return DatabaseLogEntry
  180. */
  181. public static function newFromRow( $row ) {
  182. $row = (object)$row;
  183. if ( isset( $row->rc_logid ) ) {
  184. return new RCDatabaseLogEntry( $row );
  185. } else {
  186. return new self( $row );
  187. }
  188. }
  189. /** @var stdClass Database result row. */
  190. protected $row;
  191. /** @var User */
  192. protected $performer;
  193. /** @var array Parameters for log entry */
  194. protected $params;
  195. /** @var int A rev id associated to the log entry */
  196. protected $revId = null;
  197. /** @var bool Whether the parameters for this log entry are stored in new or old format. */
  198. protected $legacy;
  199. protected function __construct( $row ) {
  200. $this->row = $row;
  201. }
  202. /**
  203. * Returns the unique database id.
  204. *
  205. * @return int
  206. */
  207. public function getId() {
  208. return (int)$this->row->log_id;
  209. }
  210. /**
  211. * Returns whatever is stored in the database field.
  212. *
  213. * @return string
  214. */
  215. protected function getRawParameters() {
  216. return $this->row->log_params;
  217. }
  218. public function isLegacy() {
  219. // This extracts the property
  220. $this->getParameters();
  221. return $this->legacy;
  222. }
  223. public function getType() {
  224. return $this->row->log_type;
  225. }
  226. public function getSubtype() {
  227. return $this->row->log_action;
  228. }
  229. public function getParameters() {
  230. if ( !isset( $this->params ) ) {
  231. $blob = $this->getRawParameters();
  232. MediaWiki\suppressWarnings();
  233. $params = LogEntryBase::extractParams( $blob );
  234. MediaWiki\restoreWarnings();
  235. if ( $params !== false ) {
  236. $this->params = $params;
  237. $this->legacy = false;
  238. } else {
  239. $this->params = LogPage::extractParams( $blob );
  240. $this->legacy = true;
  241. }
  242. if ( isset( $this->params['associated_rev_id'] ) ) {
  243. $this->revId = $this->params['associated_rev_id'];
  244. unset( $this->params['associated_rev_id'] );
  245. }
  246. }
  247. return $this->params;
  248. }
  249. public function getAssociatedRevId() {
  250. // This extracts the property
  251. $this->getParameters();
  252. return $this->revId;
  253. }
  254. public function getPerformer() {
  255. if ( !$this->performer ) {
  256. $userId = (int)$this->row->log_user;
  257. if ( $userId !== 0 ) {
  258. // logged-in users
  259. if ( isset( $this->row->user_name ) ) {
  260. $this->performer = User::newFromRow( $this->row );
  261. } else {
  262. $this->performer = User::newFromId( $userId );
  263. }
  264. } else {
  265. // IP users
  266. $userText = $this->row->log_user_text;
  267. $this->performer = User::newFromName( $userText, false );
  268. }
  269. }
  270. return $this->performer;
  271. }
  272. public function getTarget() {
  273. $namespace = $this->row->log_namespace;
  274. $page = $this->row->log_title;
  275. $title = Title::makeTitle( $namespace, $page );
  276. return $title;
  277. }
  278. public function getTimestamp() {
  279. return wfTimestamp( TS_MW, $this->row->log_timestamp );
  280. }
  281. public function getComment() {
  282. return CommentStore::newKey( 'log_comment' )->getComment( $this->row )->text;
  283. }
  284. public function getDeleted() {
  285. return $this->row->log_deleted;
  286. }
  287. }
  288. class RCDatabaseLogEntry extends DatabaseLogEntry {
  289. public function getId() {
  290. return $this->row->rc_logid;
  291. }
  292. protected function getRawParameters() {
  293. return $this->row->rc_params;
  294. }
  295. public function getAssociatedRevId() {
  296. return $this->row->rc_this_oldid;
  297. }
  298. public function getType() {
  299. return $this->row->rc_log_type;
  300. }
  301. public function getSubtype() {
  302. return $this->row->rc_log_action;
  303. }
  304. public function getPerformer() {
  305. if ( !$this->performer ) {
  306. $userId = (int)$this->row->rc_user;
  307. if ( $userId !== 0 ) {
  308. $this->performer = User::newFromId( $userId );
  309. } else {
  310. $userText = $this->row->rc_user_text;
  311. // Might be an IP, don't validate the username
  312. $this->performer = User::newFromName( $userText, false );
  313. }
  314. }
  315. return $this->performer;
  316. }
  317. public function getTarget() {
  318. $namespace = $this->row->rc_namespace;
  319. $page = $this->row->rc_title;
  320. $title = Title::makeTitle( $namespace, $page );
  321. return $title;
  322. }
  323. public function getTimestamp() {
  324. return wfTimestamp( TS_MW, $this->row->rc_timestamp );
  325. }
  326. public function getComment() {
  327. return CommentStore::newKey( 'rc_comment' )
  328. // Legacy because the row probably used RecentChange::selectFields()
  329. ->getCommentLegacy( wfGetDB( DB_REPLICA ), $this->row )->text;
  330. }
  331. public function getDeleted() {
  332. return $this->row->rc_deleted;
  333. }
  334. }
  335. /**
  336. * Class for creating log entries manually, to inject them into the database.
  337. *
  338. * @since 1.19
  339. */
  340. class ManualLogEntry extends LogEntryBase {
  341. /** @var string Type of log entry */
  342. protected $type;
  343. /** @var string Sub type of log entry */
  344. protected $subtype;
  345. /** @var array Parameters for log entry */
  346. protected $parameters = [];
  347. /** @var array */
  348. protected $relations = [];
  349. /** @var User Performer of the action for the log entry */
  350. protected $performer;
  351. /** @var Title Target title for the log entry */
  352. protected $target;
  353. /** @var string Timestamp of creation of the log entry */
  354. protected $timestamp;
  355. /** @var string Comment for the log entry */
  356. protected $comment = '';
  357. /** @var int A rev id associated to the log entry */
  358. protected $revId = 0;
  359. /** @var array Change tags add to the log entry */
  360. protected $tags = null;
  361. /** @var int Deletion state of the log entry */
  362. protected $deleted;
  363. /** @var int ID of the log entry */
  364. protected $id;
  365. /** @var bool Can this log entry be patrolled? */
  366. protected $isPatrollable = false;
  367. /** @var bool Whether this is a legacy log entry */
  368. protected $legacy = false;
  369. /**
  370. * @since 1.19
  371. * @param string $type
  372. * @param string $subtype
  373. */
  374. public function __construct( $type, $subtype ) {
  375. $this->type = $type;
  376. $this->subtype = $subtype;
  377. }
  378. /**
  379. * Set extra log parameters.
  380. *
  381. * You can pass params to the log action message by prefixing the keys with
  382. * a number and optional type, using colons to separate the fields. The
  383. * numbering should start with number 4, the first three parameters are
  384. * hardcoded for every message.
  385. *
  386. * If you want to store stuff that should not be available in messages, don't
  387. * prefix the array key with a number and don't use the colons.
  388. *
  389. * Example:
  390. * $entry->setParameters(
  391. * '4::color' => 'blue',
  392. * '5:number:count' => 3000,
  393. * 'animal' => 'dog'
  394. * );
  395. *
  396. * @since 1.19
  397. * @param array $parameters Associative array
  398. */
  399. public function setParameters( $parameters ) {
  400. $this->parameters = $parameters;
  401. }
  402. /**
  403. * Declare arbitrary tag/value relations to this log entry.
  404. * These can be used to filter log entries later on.
  405. *
  406. * @param array $relations Map of (tag => (list of values|value))
  407. * @since 1.22
  408. */
  409. public function setRelations( array $relations ) {
  410. $this->relations = $relations;
  411. }
  412. /**
  413. * Set the user that performed the action being logged.
  414. *
  415. * @since 1.19
  416. * @param User $performer
  417. */
  418. public function setPerformer( User $performer ) {
  419. $this->performer = $performer;
  420. }
  421. /**
  422. * Set the title of the object changed.
  423. *
  424. * @since 1.19
  425. * @param Title $target
  426. */
  427. public function setTarget( Title $target ) {
  428. $this->target = $target;
  429. }
  430. /**
  431. * Set the timestamp of when the logged action took place.
  432. *
  433. * @since 1.19
  434. * @param string $timestamp
  435. */
  436. public function setTimestamp( $timestamp ) {
  437. $this->timestamp = $timestamp;
  438. }
  439. /**
  440. * Set a comment associated with the action being logged.
  441. *
  442. * @since 1.19
  443. * @param string $comment
  444. */
  445. public function setComment( $comment ) {
  446. $this->comment = $comment;
  447. }
  448. /**
  449. * Set an associated revision id.
  450. *
  451. * For example, the ID of the revision that was inserted to mark a page move
  452. * or protection, file upload, etc.
  453. *
  454. * @since 1.27
  455. * @param int $revId
  456. */
  457. public function setAssociatedRevId( $revId ) {
  458. $this->revId = $revId;
  459. }
  460. /**
  461. * Set change tags for the log entry.
  462. *
  463. * @since 1.27
  464. * @param string|string[] $tags
  465. */
  466. public function setTags( $tags ) {
  467. if ( is_string( $tags ) ) {
  468. $tags = [ $tags ];
  469. }
  470. $this->tags = $tags;
  471. }
  472. /**
  473. * Set whether this log entry should be made patrollable
  474. * This shouldn't depend on config, only on whether there is full support
  475. * in the software for patrolling this log entry.
  476. * False by default
  477. *
  478. * @since 1.27
  479. * @param bool $patrollable
  480. */
  481. public function setIsPatrollable( $patrollable ) {
  482. $this->isPatrollable = (bool)$patrollable;
  483. }
  484. /**
  485. * Set the 'legacy' flag
  486. *
  487. * @since 1.25
  488. * @param bool $legacy
  489. */
  490. public function setLegacy( $legacy ) {
  491. $this->legacy = $legacy;
  492. }
  493. /**
  494. * Set the 'deleted' flag.
  495. *
  496. * @since 1.19
  497. * @param int $deleted One of LogPage::DELETED_* bitfield constants
  498. */
  499. public function setDeleted( $deleted ) {
  500. $this->deleted = $deleted;
  501. }
  502. /**
  503. * Insert the entry into the `logging` table.
  504. *
  505. * @param IDatabase $dbw
  506. * @return int ID of the log entry
  507. * @throws MWException
  508. */
  509. public function insert( IDatabase $dbw = null ) {
  510. $dbw = $dbw ?: wfGetDB( DB_MASTER );
  511. if ( $this->timestamp === null ) {
  512. $this->timestamp = wfTimestampNow();
  513. }
  514. // Trim spaces on user supplied text
  515. $comment = trim( $this->getComment() );
  516. $params = $this->getParameters();
  517. $relations = $this->relations;
  518. // Additional fields for which there's no space in the database table schema
  519. $revId = $this->getAssociatedRevId();
  520. if ( $revId ) {
  521. $params['associated_rev_id'] = $revId;
  522. $relations['associated_rev_id'] = $revId;
  523. }
  524. $data = [
  525. 'log_type' => $this->getType(),
  526. 'log_action' => $this->getSubtype(),
  527. 'log_timestamp' => $dbw->timestamp( $this->getTimestamp() ),
  528. 'log_user' => $this->getPerformer()->getId(),
  529. 'log_user_text' => $this->getPerformer()->getName(),
  530. 'log_namespace' => $this->getTarget()->getNamespace(),
  531. 'log_title' => $this->getTarget()->getDBkey(),
  532. 'log_page' => $this->getTarget()->getArticleID(),
  533. 'log_params' => LogEntryBase::makeParamBlob( $params ),
  534. ];
  535. if ( isset( $this->deleted ) ) {
  536. $data['log_deleted'] = $this->deleted;
  537. }
  538. $data += CommentStore::newKey( 'log_comment' )->insert( $dbw, $comment );
  539. $dbw->insert( 'logging', $data, __METHOD__ );
  540. $this->id = $dbw->insertId();
  541. $rows = [];
  542. foreach ( $relations as $tag => $values ) {
  543. if ( !strlen( $tag ) ) {
  544. throw new MWException( "Got empty log search tag." );
  545. }
  546. if ( !is_array( $values ) ) {
  547. $values = [ $values ];
  548. }
  549. foreach ( $values as $value ) {
  550. $rows[] = [
  551. 'ls_field' => $tag,
  552. 'ls_value' => $value,
  553. 'ls_log_id' => $this->id
  554. ];
  555. }
  556. }
  557. if ( count( $rows ) ) {
  558. $dbw->insert( 'log_search', $rows, __METHOD__, 'IGNORE' );
  559. }
  560. return $this->id;
  561. }
  562. /**
  563. * Get a RecentChanges object for the log entry
  564. *
  565. * @param int $newId
  566. * @return RecentChange
  567. * @since 1.23
  568. */
  569. public function getRecentChange( $newId = 0 ) {
  570. $formatter = LogFormatter::newFromEntry( $this );
  571. $context = RequestContext::newExtraneousContext( $this->getTarget() );
  572. $formatter->setContext( $context );
  573. $logpage = SpecialPage::getTitleFor( 'Log', $this->getType() );
  574. $user = $this->getPerformer();
  575. $ip = "";
  576. if ( $user->isAnon() ) {
  577. // "MediaWiki default" and friends may have
  578. // no IP address in their name
  579. if ( IP::isIPAddress( $user->getName() ) ) {
  580. $ip = $user->getName();
  581. }
  582. }
  583. return RecentChange::newLogEntry(
  584. $this->getTimestamp(),
  585. $logpage,
  586. $user,
  587. $formatter->getPlainActionText(),
  588. $ip,
  589. $this->getType(),
  590. $this->getSubtype(),
  591. $this->getTarget(),
  592. $this->getComment(),
  593. LogEntryBase::makeParamBlob( $this->getParameters() ),
  594. $newId,
  595. $formatter->getIRCActionComment(), // Used for IRC feeds
  596. $this->getAssociatedRevId(), // Used for e.g. moves and uploads
  597. $this->getIsPatrollable()
  598. );
  599. }
  600. /**
  601. * Publish the log entry.
  602. *
  603. * @param int $newId Id of the log entry.
  604. * @param string $to One of: rcandudp (default), rc, udp
  605. */
  606. public function publish( $newId, $to = 'rcandudp' ) {
  607. DeferredUpdates::addCallableUpdate(
  608. function () use ( $newId, $to ) {
  609. $log = new LogPage( $this->getType() );
  610. if ( !$log->isRestricted() ) {
  611. $rc = $this->getRecentChange( $newId );
  612. if ( $to === 'rc' || $to === 'rcandudp' ) {
  613. // save RC, passing tags so they are applied there
  614. $tags = $this->getTags();
  615. if ( is_null( $tags ) ) {
  616. $tags = [];
  617. }
  618. $rc->addTags( $tags );
  619. $rc->save( 'pleasedontudp' );
  620. }
  621. if ( $to === 'udp' || $to === 'rcandudp' ) {
  622. $rc->notifyRCFeeds();
  623. }
  624. // Log the autopatrol if the log entry is patrollable
  625. if ( $this->getIsPatrollable() &&
  626. $rc->getAttribute( 'rc_patrolled' ) === 1
  627. ) {
  628. PatrolLog::record( $rc, true, $this->getPerformer() );
  629. }
  630. }
  631. },
  632. DeferredUpdates::POSTSEND,
  633. wfGetDB( DB_MASTER )
  634. );
  635. }
  636. public function getType() {
  637. return $this->type;
  638. }
  639. public function getSubtype() {
  640. return $this->subtype;
  641. }
  642. public function getParameters() {
  643. return $this->parameters;
  644. }
  645. /**
  646. * @return User
  647. */
  648. public function getPerformer() {
  649. return $this->performer;
  650. }
  651. /**
  652. * @return Title
  653. */
  654. public function getTarget() {
  655. return $this->target;
  656. }
  657. public function getTimestamp() {
  658. $ts = $this->timestamp !== null ? $this->timestamp : wfTimestampNow();
  659. return wfTimestamp( TS_MW, $ts );
  660. }
  661. public function getComment() {
  662. return $this->comment;
  663. }
  664. /**
  665. * @since 1.27
  666. * @return int
  667. */
  668. public function getAssociatedRevId() {
  669. return $this->revId;
  670. }
  671. /**
  672. * @since 1.27
  673. * @return array
  674. */
  675. public function getTags() {
  676. return $this->tags;
  677. }
  678. /**
  679. * Whether this log entry is patrollable
  680. *
  681. * @since 1.27
  682. * @return bool
  683. */
  684. public function getIsPatrollable() {
  685. return $this->isPatrollable;
  686. }
  687. /**
  688. * @since 1.25
  689. * @return bool
  690. */
  691. public function isLegacy() {
  692. return $this->legacy;
  693. }
  694. public function getDeleted() {
  695. return (int)$this->deleted;
  696. }
  697. }