MediaWiki  master
RecentChange.php
Go to the documentation of this file.
1 <?php
26 use Wikimedia\IPUtils;
27 
74 class RecentChange implements Taggable {
75  // Constants for the rc_source field. Extensions may also have
76  // their own source constants.
77  public const SRC_EDIT = 'mw.edit';
78  public const SRC_NEW = 'mw.new';
79  public const SRC_LOG = 'mw.log';
80  public const SRC_EXTERNAL = 'mw.external'; // obsolete
81  public const SRC_CATEGORIZE = 'mw.categorize';
82 
83  public const PRC_UNPATROLLED = 0;
84  public const PRC_PATROLLED = 1;
85  public const PRC_AUTOPATROLLED = 2;
86 
90  public const SEND_NONE = true;
91 
95  public const SEND_FEED = false;
96 
98  public $mAttribs = [];
99  public $mExtra = [];
100 
104  public $mTitle = false;
105 
109  private $mPerformer = false;
110 
111  public $numberofWatchingusers = 0; # Dummy to prevent error message in SpecialRecentChangesLinked
113 
118 
122  public $counter = -1;
123 
127  private $tags = [];
128 
132  private $editResult = null;
133 
137  private const CHANGE_TYPES = [
138  'edit' => RC_EDIT,
139  'new' => RC_NEW,
140  'log' => RC_LOG,
141  'external' => RC_EXTERNAL,
142  'categorize' => RC_CATEGORIZE,
143  ];
144 
145  # Factory methods
146 
151  public static function newFromRow( $row ) {
152  $rc = new RecentChange;
153  $rc->loadFromRow( $row );
154 
155  return $rc;
156  }
157 
165  public static function parseToRCType( $type ) {
166  if ( is_array( $type ) ) {
167  $retval = [];
168  foreach ( $type as $t ) {
169  $retval[] = self::parseToRCType( $t );
170  }
171 
172  return $retval;
173  }
174 
175  if ( !array_key_exists( $type, self::CHANGE_TYPES ) ) {
176  throw new MWException( "Unknown type '$type'" );
177  }
178  return self::CHANGE_TYPES[$type];
179  }
180 
187  public static function parseFromRCType( $rcType ) {
188  return array_search( $rcType, self::CHANGE_TYPES, true ) ?: "$rcType";
189  }
190 
198  public static function getChangeTypes() {
199  return array_keys( self::CHANGE_TYPES );
200  }
201 
208  public static function newFromId( $rcid ) {
209  return self::newFromConds( [ 'rc_id' => $rcid ], __METHOD__ );
210  }
211 
221  public static function newFromConds(
222  $conds,
223  $fname = __METHOD__,
224  $dbType = DB_REPLICA
225  ) {
226  $db = wfGetDB( $dbType );
227  $rcQuery = self::getQueryInfo();
228  $row = $db->selectRow(
229  $rcQuery['tables'], $rcQuery['fields'], $conds, $fname, [], $rcQuery['joins']
230  );
231  if ( $row !== false ) {
232  return self::newFromRow( $row );
233  } else {
234  return null;
235  }
236  }
237 
247  public static function getQueryInfo() {
248  $commentQuery = CommentStore::getStore()->getJoin( 'rc_comment' );
249  $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
250  return [
251  'tables' => [ 'recentchanges' ] + $commentQuery['tables'] + $actorQuery['tables'],
252  'fields' => [
253  'rc_id',
254  'rc_timestamp',
255  'rc_namespace',
256  'rc_title',
257  'rc_minor',
258  'rc_bot',
259  'rc_new',
260  'rc_cur_id',
261  'rc_this_oldid',
262  'rc_last_oldid',
263  'rc_type',
264  'rc_source',
265  'rc_patrolled',
266  'rc_ip',
267  'rc_old_len',
268  'rc_new_len',
269  'rc_deleted',
270  'rc_logid',
271  'rc_log_type',
272  'rc_log_action',
273  'rc_params',
274  ] + $commentQuery['fields'] + $actorQuery['fields'],
275  'joins' => $commentQuery['joins'] + $actorQuery['joins'],
276  ];
277  }
278 
279  # Accessors
280 
284  public function setAttribs( $attribs ) {
285  $this->mAttribs = $attribs;
286  }
287 
291  public function setExtra( $extra ) {
292  $this->mExtra = $extra;
293  }
294 
298  public function &getTitle() {
299  if ( $this->mTitle === false ) {
300  $this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
301  }
302 
303  return $this->mTitle;
304  }
305 
311  public function getPerformer() {
312  if ( $this->mPerformer === false ) {
313  if ( !empty( $this->mAttribs['rc_actor'] ) ) {
314  $this->mPerformer = User::newFromActorId( $this->mAttribs['rc_actor'] );
315  } elseif ( !empty( $this->mAttribs['rc_user'] ) ) {
316  $this->mPerformer = User::newFromId( $this->mAttribs['rc_user'] );
317  } elseif ( !empty( $this->mAttribs['rc_user_text'] ) ) {
318  $this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false );
319  } else {
320  throw new MWException( 'RecentChange object lacks rc_actor, rc_user, and rc_user_text' );
321  }
322  }
323 
324  return $this->mPerformer;
325  }
326 
336  public function save( $send = self::SEND_FEED ) {
338 
339  $dbw = wfGetDB( DB_MASTER );
340  if ( !is_array( $this->mExtra ) ) {
341  $this->mExtra = [];
342  }
343 
344  if ( !$wgPutIPinRC ) {
345  $this->mAttribs['rc_ip'] = '';
346  }
347 
348  # Strict mode fixups (not-NULL fields)
349  foreach ( [ 'minor', 'bot', 'new', 'patrolled', 'deleted' ] as $field ) {
350  $this->mAttribs["rc_$field"] = (int)$this->mAttribs["rc_$field"];
351  }
352  # ...more fixups (NULL fields)
353  foreach ( [ 'old_len', 'new_len' ] as $field ) {
354  $this->mAttribs["rc_$field"] = isset( $this->mAttribs["rc_$field"] )
355  ? (int)$this->mAttribs["rc_$field"]
356  : null;
357  }
358 
359  # If our database is strict about IP addresses, use NULL instead of an empty string
360  $strictIPs = $dbw->getType() === 'postgres'; // legacy
361  if ( $strictIPs && $this->mAttribs['rc_ip'] == '' ) {
362  unset( $this->mAttribs['rc_ip'] );
363  }
364 
365  # Trim spaces on user supplied text
366  $this->mAttribs['rc_comment'] = trim( $this->mAttribs['rc_comment'] );
367 
368  # Fixup database timestamps
369  $this->mAttribs['rc_timestamp'] = $dbw->timestamp( $this->mAttribs['rc_timestamp'] );
370 
371  # # If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
372  if ( $this->mAttribs['rc_cur_id'] == 0 ) {
373  unset( $this->mAttribs['rc_cur_id'] );
374  }
375 
376  $row = $this->mAttribs;
377 
378  # Convert mAttribs['rc_comment'] for CommentStore
379  $comment = $row['rc_comment'];
380  unset( $row['rc_comment'], $row['rc_comment_text'], $row['rc_comment_data'] );
381  $row += CommentStore::getStore()->insert( $dbw, 'rc_comment', $comment );
382 
383  # Convert mAttribs['rc_user'] etc for ActorMigration
384  $user = User::newFromAnyId(
385  $row['rc_user'] ?? null,
386  $row['rc_user_text'] ?? null,
387  $row['rc_actor'] ?? null
388  );
389  unset( $row['rc_user'], $row['rc_user_text'], $row['rc_actor'] );
390  $row += ActorMigration::newMigration()->getInsertValues( $dbw, 'rc_user', $user );
391 
392  # Don't reuse an existing rc_id for the new row, if one happens to be
393  # set for some reason.
394  unset( $row['rc_id'] );
395 
396  # Insert new row
397  $dbw->insert( 'recentchanges', $row, __METHOD__ );
398 
399  # Set the ID
400  $this->mAttribs['rc_id'] = $dbw->insertId();
401 
402  # Notify extensions
403  Hooks::runner()->onRecentChange_save( $this );
404 
405  // Apply revert tags (if needed)
406  if ( $this->editResult !== null && count( $this->editResult->getRevertTags() ) ) {
408  $this->editResult->getRevertTags(),
409  $this->mAttribs['rc_id'],
410  $this->mAttribs['rc_this_oldid'],
411  $this->mAttribs['rc_logid'],
412  FormatJson::encode( $this->editResult ),
413  $this
414  );
415  }
416 
417  if ( count( $this->tags ) ) {
418  // $this->tags may contain revert tags we already applied above, they will
419  // just be ignored.
421  $this->tags,
422  $this->mAttribs['rc_id'],
423  $this->mAttribs['rc_this_oldid'],
424  $this->mAttribs['rc_logid'],
425  null,
426  $this
427  );
428  }
429 
430  if ( $send === self::SEND_FEED ) {
431  // Emit the change to external applications via RCFeeds.
432  $this->notifyRCFeeds();
433  }
434 
435  # E-mail notifications
436  if ( $wgUseEnotif || $wgShowUpdatedMarker ) {
437  $editor = $this->getPerformer();
438  $title = $this->getTitle();
439 
440  // Never send an RC notification email about categorization changes
441  if (
442  Hooks::runner()->onAbortEmailNotification( $editor, $title, $this ) &&
443  $this->mAttribs['rc_type'] != RC_CATEGORIZE
444  ) {
445  // @FIXME: This would be better as an extension hook
446  // Send emails or email jobs once this row is safely committed
447  $dbw->onTransactionCommitOrIdle(
448  function () use ( $editor, $title ) {
449  $enotif = new EmailNotification();
450  $enotif->notifyOnPageChange(
451  $editor,
452  $title,
453  $this->mAttribs['rc_timestamp'],
454  $this->mAttribs['rc_comment'],
455  $this->mAttribs['rc_minor'],
456  $this->mAttribs['rc_last_oldid'],
457  $this->mExtra['pageStatus']
458  );
459  },
460  __METHOD__
461  );
462  }
463  }
464 
465  // Update the cached list of active users
466  if ( $this->mAttribs['rc_user'] > 0 ) {
468  }
469  }
470 
475  public function notifyRCFeeds( array $feeds = null ) {
476  global $wgRCFeeds;
477  if ( $feeds === null ) {
478  $feeds = $wgRCFeeds;
479  }
480 
481  $performer = $this->getPerformer();
482 
483  foreach ( $feeds as $params ) {
484  $params += [
485  'omit_bots' => false,
486  'omit_anon' => false,
487  'omit_user' => false,
488  'omit_minor' => false,
489  'omit_patrolled' => false,
490  ];
491 
492  if (
493  ( $params['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
494  ( $params['omit_anon'] && $performer->isAnon() ) ||
495  ( $params['omit_user'] && !$performer->isAnon() ) ||
496  ( $params['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
497  ( $params['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
498  $this->mAttribs['rc_type'] == RC_EXTERNAL
499  ) {
500  continue;
501  }
502 
503  $actionComment = $this->mExtra['actionCommentIRC'] ?? null;
504 
505  $feed = RCFeed::factory( $params );
506  $feed->notify( $this, $actionComment );
507  }
508  }
509 
518  public static function getEngine( $uri, $params = [] ) {
519  // TODO: Merge into RCFeed::factory().
520  global $wgRCEngines;
521  $scheme = parse_url( $uri, PHP_URL_SCHEME );
522  if ( !$scheme ) {
523  throw new MWException( "Invalid RCFeed uri: '$uri'" );
524  }
525  if ( !isset( $wgRCEngines[$scheme] ) ) {
526  throw new MWException( "Unknown RCFeedEngine scheme: '$scheme'" );
527  }
528  if ( defined( 'MW_PHPUNIT_TEST' ) && is_object( $wgRCEngines[$scheme] ) ) {
529  return $wgRCEngines[$scheme];
530  }
531  return new $wgRCEngines[$scheme]( $params );
532  }
533 
545  public function doMarkPatrolled( User $user, $auto = false, $tags = null ) {
547 
548  $permManager = MediaWikiServices::getInstance()->getPermissionManager();
549 
550  // Fix up $tags so that the MarkPatrolled hook below always gets an array
551  if ( $tags === null ) {
552  $tags = [];
553  } elseif ( is_string( $tags ) ) {
554  $tags = [ $tags ];
555  }
556 
557  $errors = [];
558  // If recentchanges patrol is disabled, only new pages or new file versions
559  // can be patrolled, provided the appropriate config variable is set
560  if ( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) &&
561  ( !$wgUseFilePatrol || !( $this->getAttribute( 'rc_type' ) == RC_LOG &&
562  $this->getAttribute( 'rc_log_type' ) == 'upload' ) ) ) {
563  $errors[] = [ 'rcpatroldisabled' ];
564  }
565  // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
566  $right = $auto ? 'autopatrol' : 'patrol';
567  $errors = array_merge(
568  $errors,
569  $permManager->getPermissionErrors( $right, $user, $this->getTitle() )
570  );
571  if ( !Hooks::runner()->onMarkPatrolled(
572  $this->getAttribute( 'rc_id' ), $user, false, $auto, $tags )
573  ) {
574  $errors[] = [ 'hookaborted' ];
575  }
576  // Users without the 'autopatrol' right can't patrol their
577  // own revisions
578  if ( $user->getName() === $this->getAttribute( 'rc_user_text' ) &&
579  !$permManager->userHasRight( $user, 'autopatrol' )
580  ) {
581  $errors[] = [ 'markedaspatrollederror-noautopatrol' ];
582  }
583  if ( $errors ) {
584  return $errors;
585  }
586  // If the change was patrolled already, do nothing
587  if ( $this->getAttribute( 'rc_patrolled' ) ) {
588  return [];
589  }
590  // Actually set the 'patrolled' flag in RC
591  $this->reallyMarkPatrolled();
592  // Log this patrol event
593  PatrolLog::record( $this, $auto, $user, $tags );
594 
595  Hooks::runner()->onMarkPatrolledComplete(
596  $this->getAttribute( 'rc_id' ), $user, false, $auto );
597 
598  return [];
599  }
600 
605  public function reallyMarkPatrolled() {
606  $dbw = wfGetDB( DB_MASTER );
607  $dbw->update(
608  'recentchanges',
609  [
610  'rc_patrolled' => self::PRC_PATROLLED
611  ],
612  [
613  'rc_id' => $this->getAttribute( 'rc_id' )
614  ],
615  __METHOD__
616  );
617  // Invalidate the page cache after the page has been patrolled
618  // to make sure that the Patrol link isn't visible any longer!
619  $this->getTitle()->invalidateCache();
620 
621  // Enqueue a reverted tag update (in case the edit was a revert)
622  $revisionId = $this->getAttribute( 'rc_this_oldid' );
623  if ( $revisionId ) {
624  $revertedTagUpdateManager =
625  MediaWikiServices::getInstance()->getRevertedTagUpdateManager();
626  $revertedTagUpdateManager->approveRevertedTagForRevision( $revisionId );
627  }
628 
629  return $dbw->affectedRows();
630  }
631 
656  public static function notifyEdit(
657  $timestamp, $title, $minor, $user, $comment, $oldId, $lastTimestamp,
658  $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0,
659  $tags = [], EditResult $editResult = null
660  ) {
661  $rc = new RecentChange;
662  $rc->mTitle = $title;
663  $rc->mPerformer = $user;
664  $rc->mAttribs = [
665  'rc_timestamp' => $timestamp,
666  'rc_namespace' => $title->getNamespace(),
667  'rc_title' => $title->getDBkey(),
668  'rc_type' => RC_EDIT,
669  'rc_source' => self::SRC_EDIT,
670  'rc_minor' => $minor ? 1 : 0,
671  'rc_cur_id' => $title->getArticleID(),
672  'rc_user' => $user->getId(),
673  'rc_user_text' => $user->getName(),
674  'rc_actor' => $user->getActorId(),
675  'rc_comment' => &$comment,
676  'rc_comment_text' => &$comment,
677  'rc_comment_data' => null,
678  'rc_this_oldid' => (int)$newId,
679  'rc_last_oldid' => $oldId,
680  'rc_bot' => $bot ? 1 : 0,
681  'rc_ip' => self::checkIPAddress( $ip ),
682  'rc_patrolled' => intval( $patrol ),
683  'rc_new' => 0, # obsolete
684  'rc_old_len' => $oldSize,
685  'rc_new_len' => $newSize,
686  'rc_deleted' => 0,
687  'rc_logid' => 0,
688  'rc_log_type' => null,
689  'rc_log_action' => '',
690  'rc_params' => ''
691  ];
692 
693  $rc->mExtra = [
694  'prefixedDBkey' => $title->getPrefixedDBkey(),
695  'lastTimestamp' => $lastTimestamp,
696  'oldSize' => $oldSize,
697  'newSize' => $newSize,
698  'pageStatus' => 'changed'
699  ];
700 
702  static function () use ( $rc, $tags, $editResult ) {
703  $rc->addTags( $tags );
704  $rc->setEditResult( $editResult );
705  $rc->save();
706  },
707  DeferredUpdates::POSTSEND,
708  wfGetDB( DB_MASTER )
709  );
710 
711  return $rc;
712  }
713 
731  public static function notifyNew(
732  $timestamp, $title, $minor, $user, $comment, $bot,
733  $ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = []
734  ) {
735  $rc = new RecentChange;
736  $rc->mTitle = $title;
737  $rc->mPerformer = $user;
738  $rc->mAttribs = [
739  'rc_timestamp' => $timestamp,
740  'rc_namespace' => $title->getNamespace(),
741  'rc_title' => $title->getDBkey(),
742  'rc_type' => RC_NEW,
743  'rc_source' => self::SRC_NEW,
744  'rc_minor' => $minor ? 1 : 0,
745  'rc_cur_id' => $title->getArticleID(),
746  'rc_user' => $user->getId(),
747  'rc_user_text' => $user->getName(),
748  'rc_actor' => $user->getActorId(),
749  'rc_comment' => &$comment,
750  'rc_comment_text' => &$comment,
751  'rc_comment_data' => null,
752  'rc_this_oldid' => (int)$newId,
753  'rc_last_oldid' => 0,
754  'rc_bot' => $bot ? 1 : 0,
755  'rc_ip' => self::checkIPAddress( $ip ),
756  'rc_patrolled' => intval( $patrol ),
757  'rc_new' => 1, # obsolete
758  'rc_old_len' => 0,
759  'rc_new_len' => $size,
760  'rc_deleted' => 0,
761  'rc_logid' => 0,
762  'rc_log_type' => null,
763  'rc_log_action' => '',
764  'rc_params' => ''
765  ];
766 
767  $rc->mExtra = [
768  'prefixedDBkey' => $title->getPrefixedDBkey(),
769  'lastTimestamp' => 0,
770  'oldSize' => 0,
771  'newSize' => $size,
772  'pageStatus' => 'created'
773  ];
774 
776  static function () use ( $rc, $tags ) {
777  $rc->addTags( $tags );
778  $rc->save();
779  },
780  DeferredUpdates::POSTSEND,
781  wfGetDB( DB_MASTER )
782  );
783 
784  return $rc;
785  }
786 
802  public static function notifyLog( $timestamp, $title, $user, $actionComment, $ip, $type,
803  $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
804  ) {
805  global $wgLogRestrictions;
806 
807  # Don't add private logs to RC!
808  if ( isset( $wgLogRestrictions[$type] ) && $wgLogRestrictions[$type] != '*' ) {
809  return false;
810  }
811  $rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action,
812  $target, $logComment, $params, $newId, $actionCommentIRC );
813  $rc->save();
814 
815  return true;
816  }
817 
835  public static function newLogEntry( $timestamp, $title, $user, $actionComment, $ip,
836  $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '',
837  $revId = 0, $isPatrollable = false ) {
838  global $wgRequest;
839  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
840 
841  # # Get pageStatus for email notification
842  switch ( $type . '-' . $action ) {
843  case 'delete-delete':
844  case 'delete-delete_redir':
845  case 'delete-delete_redir2':
846  $pageStatus = 'deleted';
847  break;
848  case 'move-move':
849  case 'move-move_redir':
850  $pageStatus = 'moved';
851  break;
852  case 'delete-restore':
853  $pageStatus = 'restored';
854  break;
855  case 'upload-upload':
856  $pageStatus = 'created';
857  break;
858  case 'upload-overwrite':
859  default:
860  $pageStatus = 'changed';
861  break;
862  }
863 
864  // Allow unpatrolled status for patrollable log entries
865  $canAutopatrol = $permissionManager->userHasRight( $user, 'autopatrol' );
866  $markPatrolled = $isPatrollable ? $canAutopatrol : true;
867 
868  $rc = new RecentChange;
869  $rc->mTitle = $target;
870  $rc->mPerformer = User::newFromIdentity( $user );
871  $rc->mAttribs = [
872  'rc_timestamp' => $timestamp,
873  'rc_namespace' => $target->getNamespace(),
874  'rc_title' => $target->getDBkey(),
875  'rc_type' => RC_LOG,
876  'rc_source' => self::SRC_LOG,
877  'rc_minor' => 0,
878  'rc_cur_id' => $target->getArticleID(),
879  'rc_user' => $user->getId(),
880  'rc_user_text' => $user->getName(),
881  'rc_actor' => $user->getActorId(),
882  'rc_comment' => &$logComment,
883  'rc_comment_text' => &$logComment,
884  'rc_comment_data' => null,
885  'rc_this_oldid' => (int)$revId,
886  'rc_last_oldid' => 0,
887  'rc_bot' => $permissionManager->userHasRight( $user, 'bot' ) ?
888  (int)$wgRequest->getBool( 'bot', true ) : 0,
889  'rc_ip' => self::checkIPAddress( $ip ),
890  'rc_patrolled' => $markPatrolled ? self::PRC_AUTOPATROLLED : self::PRC_UNPATROLLED,
891  'rc_new' => 0, # obsolete
892  'rc_old_len' => null,
893  'rc_new_len' => null,
894  'rc_deleted' => 0,
895  'rc_logid' => $newId,
896  'rc_log_type' => $type,
897  'rc_log_action' => $action,
898  'rc_params' => $params
899  ];
900 
901  $rc->mExtra = [
902  'prefixedDBkey' => $title->getPrefixedDBkey(),
903  'lastTimestamp' => 0,
904  'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
905  'pageStatus' => $pageStatus,
906  'actionCommentIRC' => $actionCommentIRC
907  ];
908 
909  return $rc;
910  }
911 
933  public static function newForCategorization(
934  $timestamp,
935  Title $categoryTitle,
936  ?User $user,
937  $comment,
938  Title $pageTitle,
939  $oldRevId,
940  $newRevId,
941  $lastTimestamp,
942  $bot,
943  $ip = '',
944  $deleted = 0,
945  $added = null
946  ) {
947  // Done in a backwards compatible way.
948  $categoryWikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $categoryTitle );
949  '@phan-var WikiCategoryPage $categoryWikiPage';
950  $params = [
951  'hidden-cat' => $categoryWikiPage->isHidden()
952  ];
953  if ( $added !== null ) {
954  $params['added'] = $added;
955  }
956 
957  $rc = new RecentChange;
958  $rc->mTitle = $categoryTitle;
959  $rc->mPerformer = $user;
960  $rc->mAttribs = [
961  'rc_timestamp' => $timestamp,
962  'rc_namespace' => $categoryTitle->getNamespace(),
963  'rc_title' => $categoryTitle->getDBkey(),
964  'rc_type' => RC_CATEGORIZE,
965  'rc_source' => self::SRC_CATEGORIZE,
966  'rc_minor' => 0,
967  'rc_cur_id' => $pageTitle->getArticleID(),
968  'rc_user' => $user ? $user->getId() : 0,
969  'rc_user_text' => $user ? $user->getName() : '',
970  'rc_actor' => $user ? $user->getActorId() : null,
971  'rc_comment' => &$comment,
972  'rc_comment_text' => &$comment,
973  'rc_comment_data' => null,
974  'rc_this_oldid' => (int)$newRevId,
975  'rc_last_oldid' => $oldRevId,
976  'rc_bot' => $bot ? 1 : 0,
977  'rc_ip' => self::checkIPAddress( $ip ),
978  'rc_patrolled' => self::PRC_AUTOPATROLLED, // Always patrolled, just like log entries
979  'rc_new' => 0, # obsolete
980  'rc_old_len' => null,
981  'rc_new_len' => null,
982  'rc_deleted' => $deleted,
983  'rc_logid' => 0,
984  'rc_log_type' => null,
985  'rc_log_action' => '',
986  'rc_params' => serialize( $params )
987  ];
988 
989  $rc->mExtra = [
990  'prefixedDBkey' => $categoryTitle->getPrefixedDBkey(),
991  'lastTimestamp' => $lastTimestamp,
992  'oldSize' => 0,
993  'newSize' => 0,
994  'pageStatus' => 'changed'
995  ];
996 
997  return $rc;
998  }
999 
1008  public function getParam( $name ) {
1009  $params = $this->parseParams();
1010  return $params[$name] ?? null;
1011  }
1012 
1018  public function loadFromRow( $row ) {
1019  $this->mAttribs = get_object_vars( $row );
1020  $this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
1021  // rc_deleted MUST be set
1022  $this->mAttribs['rc_deleted'] = $row->rc_deleted;
1023 
1024  if ( isset( $this->mAttribs['rc_ip'] ) ) {
1025  // Clean up CIDRs for Postgres per T164898. ("127.0.0.1" casts to "127.0.0.1/32")
1026  $n = strpos( $this->mAttribs['rc_ip'], '/' );
1027  if ( $n !== false ) {
1028  $this->mAttribs['rc_ip'] = substr( $this->mAttribs['rc_ip'], 0, $n );
1029  }
1030  }
1031 
1032  $comment = CommentStore::getStore()
1033  // Legacy because $row may have come from self::selectFields()
1034  ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $row, true )
1035  ->text;
1036  $this->mAttribs['rc_comment'] = &$comment;
1037  $this->mAttribs['rc_comment_text'] = &$comment;
1038  $this->mAttribs['rc_comment_data'] = null;
1039 
1040  $user = User::newFromAnyId(
1041  $this->mAttribs['rc_user'] ?? null,
1042  $this->mAttribs['rc_user_text'] ?? null,
1043  $this->mAttribs['rc_actor'] ?? null
1044  );
1045  $this->mAttribs['rc_user'] = $user->getId();
1046  $this->mAttribs['rc_user_text'] = $user->getName();
1047  $this->mAttribs['rc_actor'] = $user->getActorId();
1048 
1049  // Watchlist expiry.
1050  if ( isset( $row->we_expiry ) && $row->we_expiry ) {
1051  $this->watchlistExpiry = wfTimestamp( TS_MW, $row->we_expiry );
1052  }
1053  }
1054 
1061  public function getAttribute( $name ) {
1062  if ( $name === 'rc_comment' ) {
1063  return CommentStore::getStore()
1064  ->getComment( 'rc_comment', $this->mAttribs, true )->text;
1065  }
1066 
1067  if ( $name === 'rc_user' || $name === 'rc_user_text' || $name === 'rc_actor' ) {
1068  $user = User::newFromAnyId(
1069  $this->mAttribs['rc_user'] ?? null,
1070  $this->mAttribs['rc_user_text'] ?? null,
1071  $this->mAttribs['rc_actor'] ?? null
1072  );
1073  if ( $name === 'rc_user' ) {
1074  return $user->getId();
1075  }
1076  if ( $name === 'rc_user_text' ) {
1077  return $user->getName();
1078  }
1079  if ( $name === 'rc_actor' ) {
1080  return $user->getActorId();
1081  }
1082  }
1083 
1084  return $this->mAttribs[$name] ?? null;
1085  }
1086 
1090  public function getAttributes() {
1091  return $this->mAttribs;
1092  }
1093 
1100  public function diffLinkTrail( $forceCur ) {
1101  if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
1102  $trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
1103  "&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
1104  if ( $forceCur ) {
1105  $trail .= '&diff=0';
1106  } else {
1107  $trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
1108  }
1109  } else {
1110  $trail = '';
1111  }
1112 
1113  return $trail;
1114  }
1115 
1123  public function getCharacterDifference( $old = 0, $new = 0 ) {
1124  if ( $old === 0 ) {
1125  $old = $this->mAttribs['rc_old_len'];
1126  }
1127  if ( $new === 0 ) {
1128  $new = $this->mAttribs['rc_new_len'];
1129  }
1130  if ( $old === null || $new === null ) {
1131  return '';
1132  }
1133 
1134  return ChangesList::showCharacterDifference( $old, $new );
1135  }
1136 
1137  private static function checkIPAddress( $ip ) {
1138  global $wgRequest;
1139  if ( $ip ) {
1140  if ( !IPUtils::isIPAddress( $ip ) ) {
1141  throw new MWException( "Attempt to write \"" . $ip .
1142  "\" as an IP address into recent changes" );
1143  }
1144  } else {
1145  $ip = $wgRequest->getIP();
1146  if ( !$ip ) {
1147  $ip = '';
1148  }
1149  }
1150 
1151  return $ip;
1152  }
1153 
1163  public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
1164  global $wgRCMaxAge;
1165 
1166  return wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $wgRCMaxAge;
1167  }
1168 
1176  public function parseParams() {
1177  $rcParams = $this->getAttribute( 'rc_params' );
1178 
1179  Wikimedia\suppressWarnings();
1180  $unserializedParams = unserialize( $rcParams );
1181  Wikimedia\restoreWarnings();
1182 
1183  return $unserializedParams;
1184  }
1185 
1194  public function addTags( $tags ) {
1195  if ( is_string( $tags ) ) {
1196  $this->tags[] = $tags;
1197  } else {
1198  $this->tags = array_merge( $tags, $this->tags );
1199  }
1200  }
1201 
1209  public function setEditResult( ?EditResult $editResult ) {
1210  $this->editResult = $editResult;
1211  }
1212 }
RecentChange\getCharacterDifference
getCharacterDifference( $old=0, $new=0)
Returns the change size (HTML).
Definition: RecentChange.php:1123
RecentChange\save
save( $send=self::SEND_FEED)
Writes the data in this object to the database.
Definition: RecentChange.php:336
RecentChange\getQueryInfo
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new recentchanges object.
Definition: RecentChange.php:247
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:623
RecentChange\$editResult
EditResult null $editResult
EditResult associated with the edit.
Definition: RecentChange.php:132
RecentChange\$tags
array $tags
List of tags to apply.
Definition: RecentChange.php:127
RC_EDIT
const RC_EDIT
Definition: Defines.php:126
User\getId
getId()
Get the user's ID.
Definition: User.php:2067
RecentChange\setEditResult
setEditResult(?EditResult $editResult)
Sets the EditResult associated with the edit.
Definition: RecentChange.php:1209
RecentChange\setExtra
setExtra( $extra)
Definition: RecentChange.php:291
SpecialRecentChangesLinked
This is to display changes made to all articles linked in an article.
Definition: SpecialRecentChangesLinked.php:33
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:172
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:221
$wgShowUpdatedMarker
$wgShowUpdatedMarker
Show "Updated (since my last visit)" marker in RC view, watchlist and history view for watched pages ...
Definition: DefaultSettings.php:7540
Title\getPrefixedDBkey
getPrefixedDBkey()
Get the prefixed database key form.
Definition: Title.php:1874
RecentChange\getAttributes
getAttributes()
Definition: RecentChange.php:1090
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:74
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1832
Title\getArticleID
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3182
RecentChange\notifyLog
static notifyLog( $timestamp, $title, $user, $actionComment, $ip, $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='')
Definition: RecentChange.php:802
RecentChange\getEngine
static getEngine( $uri, $params=[])
Definition: RecentChange.php:518
RecentChange\loadFromRow
loadFromRow( $row)
Initialises the members of this object from a mysql row object.
Definition: RecentChange.php:1018
RecentChange\getChangeTypes
static getChangeTypes()
Get an array of all change types.
Definition: RecentChange.php:198
RecentChange\reallyMarkPatrolled
reallyMarkPatrolled()
Mark this RecentChange patrolled, without error checking.
Definition: RecentChange.php:605
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:584
RecentChange\$counter
int $counter
Line number of recent change.
Definition: RecentChange.php:122
RecentChange\setAttribs
setAttribs( $attribs)
Definition: RecentChange.php:284
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:655
MediaWiki\ChangeTags\Taggable
Interface that defines how to tag objects.
Definition: Taggable.php:32
RecentChange\SRC_CATEGORIZE
const SRC_CATEGORIZE
Definition: RecentChange.php:81
User\getActorId
getActorId( $dbwOrWikiId=self::LOCAL)
Get the user's actor ID.
Definition: User.php:2156
RecentChange\parseToRCType
static parseToRCType( $type)
Parsing text to RC_* constants.
Definition: RecentChange.php:165
RC_NEW
const RC_NEW
Definition: Defines.php:127
serialize
serialize()
Definition: ApiMessageTrait.php:138
RecentChange\notifyEdit
static notifyEdit( $timestamp, $title, $minor, $user, $comment, $oldId, $lastTimestamp, $bot, $ip='', $oldSize=0, $newSize=0, $newId=0, $patrol=0, $tags=[], EditResult $editResult=null)
Makes an entry in the database corresponding to an edit.
Definition: RecentChange.php:656
$wgUseRCPatrol
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
Definition: DefaultSettings.php:7433
$wgUseNPPatrol
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
Definition: DefaultSettings.php:7449
RecentChange\SRC_LOG
const SRC_LOG
Definition: RecentChange.php:79
RecentChangesUpdateJob\newCacheUpdateJob
static newCacheUpdateJob()
Definition: RecentChangesUpdateJob.php:54
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:35
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:157
RecentChange\parseParams
parseParams()
Parses and returns the rc_params attribute.
Definition: RecentChange.php:1176
$wgPutIPinRC
$wgPutIPinRC
Log IP addresses in the recentchanges table; can be accessed only by extensions (e....
Definition: DefaultSettings.php:6299
RecentChange\$mTitle
Title false $mTitle
Definition: RecentChange.php:104
RC_LOG
const RC_LOG
Definition: Defines.php:128
FormatJson\encode
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:115
Title\getDBkey
getDBkey()
Get the main part with underscores.
Definition: Title.php:1059
MWException
MediaWiki exception.
Definition: MWException.php:29
Title\getNamespace
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1068
RecentChange\$notificationtimestamp
$notificationtimestamp
Definition: RecentChange.php:112
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2467
RecentChange\$watchlistExpiry
string null $watchlistExpiry
The expiry time, if this is a temporary watchlist item.
Definition: RecentChange.php:117
$wgRCFeeds
$wgRCFeeds
Configuration for feeds to which notifications about recent changes will be sent.
Definition: DefaultSettings.php:7400
RecentChange\newFromRow
static newFromRow( $row)
Definition: RecentChange.php:151
$title
$title
Definition: testCompression.php:38
RecentChange\SRC_EDIT
const SRC_EDIT
Definition: RecentChange.php:77
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:626
RecentChange\diffLinkTrail
diffLinkTrail( $forceCur)
Gets the end part of the diff URL associated with this object Blank if no diff link should be display...
Definition: RecentChange.php:1100
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
User\newFromAnyId
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:682
DB_MASTER
const DB_MASTER
Definition: defines.php:26
RecentChange\SRC_NEW
const SRC_NEW
Definition: RecentChange.php:78
$wgUseFilePatrol
$wgUseFilePatrol
Use file patrolling to check new files on Special:Newfiles.
Definition: DefaultSettings.php:7460
MediaWiki\Storage\EditResult
Object for storing information about the effects of an edit.
Definition: EditResult.php:38
RecentChange\PRC_PATROLLED
const PRC_PATROLLED
Definition: RecentChange.php:84
RecentChange\newFromId
static newFromId( $rcid)
Obtain the recent change with a given rc_id value.
Definition: RecentChange.php:208
RecentChange\doMarkPatrolled
doMarkPatrolled(User $user, $auto=false, $tags=null)
Mark this RecentChange as patrolled.
Definition: RecentChange.php:545
RecentChange\$mExtra
$mExtra
Definition: RecentChange.php:99
RC_EXTERNAL
const RC_EXTERNAL
Definition: Defines.php:129
RecentChange\newForCategorization
static newForCategorization( $timestamp, Title $categoryTitle, ?User $user, $comment, Title $pageTitle, $oldRevId, $newRevId, $lastTimestamp, $bot, $ip='', $deleted=0, $added=null)
Constructs a RecentChange object for the given categorization This does not call save() on the object...
Definition: RecentChange.php:933
$wgLogRestrictions
$wgLogRestrictions
This restricts log access to those who have a certain right Users without this will not see it in the...
Definition: DefaultSettings.php:8248
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
$wgRCMaxAge
$wgRCMaxAge
Recentchanges items are periodically purged; entries older than this many seconds will go.
Definition: DefaultSettings.php:7300
ChangesList\showCharacterDifference
static showCharacterDifference( $old, $new, IContextSource $context=null)
Show formatted char difference.
Definition: ChangesList.php:333
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:85
RecentChange\addTags
addTags( $tags)
Tags to append to the recent change, and associated revision/log.
Definition: RecentChange.php:1194
RecentChange\getAttribute
getAttribute( $name)
Get an attribute value.
Definition: RecentChange.php:1061
$wgUseEnotif
$wgUseEnotif
Definition: Setup.php:448
RecentChange\checkIPAddress
static checkIPAddress( $ip)
Definition: RecentChange.php:1137
RecentChange\newLogEntry
static newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='', $revId=0, $isPatrollable=false)
Definition: RecentChange.php:835
unserialize
unserialize( $serialized)
Definition: ApiMessageTrait.php:146
Title
Represents a title within MediaWiki.
Definition: Title.php:46
JobQueueGroup\singleton
static singleton( $domain=false)
Definition: JobQueueGroup.php:70
RC_CATEGORIZE
const RC_CATEGORIZE
Definition: Defines.php:130
RecentChange\notifyNew
static notifyNew( $timestamp, $title, $minor, $user, $comment, $bot, $ip='', $size=0, $newId=0, $patrol=0, $tags=[])
Makes an entry in the database corresponding to page creation Note: the title object must be loaded w...
Definition: RecentChange.php:731
RecentChange\parseFromRCType
static parseFromRCType( $rcType)
Parsing RC_* constants to human-readable test.
Definition: RecentChange.php:187
$wgRCEngines
$wgRCEngines
Used by RecentChange::getEngine to find the correct engine for a given URI scheme.
Definition: DefaultSettings.php:7407
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:83
RCFeed\factory
static factory(array $params)
Definition: RCFeed.php:46
RecentChange\isInRCLifespan
static isInRCLifespan( $timestamp, $tolerance=0)
Check whether the given timestamp is new enough to have a RC row with a given tolerance as the recent...
Definition: RecentChange.php:1163
User\newFromActorId
static newFromActorId( $id)
Static factory method for creation from a given actor ID.
Definition: User.php:638
RecentChange\$mPerformer
User false $mPerformer
Definition: RecentChange.php:109
RecentChange\notifyRCFeeds
notifyRCFeeds(array $feeds=null)
Notify all the feeds about the change.
Definition: RecentChange.php:475
$t
$t
Definition: testCompression.php:74
RecentChange\$mAttribs
array $mAttribs
Definition: RecentChange.php:98
RecentChange\getPerformer
getPerformer()
Get the User object of the person who performed this change.
Definition: RecentChange.php:311
EmailNotification
This module processes the email notifications when the current page is changed.
Definition: EmailNotification.php:48
$wgRequest
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:651
RecentChange\getTitle
& getTitle()
Definition: RecentChange.php:298
CommentStore\getStore
static getStore()
Definition: CommentStore.php:120
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:66
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
Definition: DeferredUpdates.php:145
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2113
RecentChange\getParam
getParam( $name)
Get a parameter value.
Definition: RecentChange.php:1008
RecentChange\$numberofWatchingusers
$numberofWatchingusers
Definition: RecentChange.php:111
ChangeTags\addTags
static addTags( $tags, $rc_id=null, $rev_id=null, $log_id=null, $params=null, RecentChange $rc=null)
Add tags to a change given its rc_id, rev_id and/or log_id.
Definition: ChangeTags.php:310
PatrolLog\record
static record( $rc, $auto, User $user, $tags=null)
Record a log event for a change being patrolled.
Definition: PatrolLog.php:42
RecentChange\SRC_EXTERNAL
const SRC_EXTERNAL
Definition: RecentChange.php:80
$type
$type
Definition: testCompression.php:52