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