MediaWiki  master
RecentChange.php
Go to the documentation of this file.
1 <?php
24 use Wikimedia\IPUtils;
25 
72 class RecentChange implements Taggable {
73  // Constants for the rc_source field. Extensions may also have
74  // their own source constants.
75  public const SRC_EDIT = 'mw.edit';
76  public const SRC_NEW = 'mw.new';
77  public const SRC_LOG = 'mw.log';
78  public const SRC_EXTERNAL = 'mw.external'; // obsolete
79  public const SRC_CATEGORIZE = 'mw.categorize';
80 
81  public const PRC_UNPATROLLED = 0;
82  public const PRC_PATROLLED = 1;
83  public const PRC_AUTOPATROLLED = 2;
84 
88  public const SEND_NONE = true;
89 
93  public const SEND_FEED = false;
94 
96  public $mAttribs = [];
97  public $mExtra = [];
98 
102  public $mTitle = false;
103 
107  private $mPerformer = false;
108 
109  public $numberofWatchingusers = 0; # Dummy to prevent error message in SpecialRecentChangesLinked
111 
116 
120  public $counter = -1;
121 
125  private $tags = [];
126 
130  private const CHANGE_TYPES = [
131  'edit' => RC_EDIT,
132  'new' => RC_NEW,
133  'log' => RC_LOG,
134  'external' => RC_EXTERNAL,
135  'categorize' => RC_CATEGORIZE,
136  ];
137 
138  # Factory methods
139 
144  public static function newFromRow( $row ) {
145  $rc = new RecentChange;
146  $rc->loadFromRow( $row );
147 
148  return $rc;
149  }
150 
158  public static function parseToRCType( $type ) {
159  if ( is_array( $type ) ) {
160  $retval = [];
161  foreach ( $type as $t ) {
162  $retval[] = self::parseToRCType( $t );
163  }
164 
165  return $retval;
166  }
167 
168  if ( !array_key_exists( $type, self::CHANGE_TYPES ) ) {
169  throw new MWException( "Unknown type '$type'" );
170  }
171  return self::CHANGE_TYPES[$type];
172  }
173 
180  public static function parseFromRCType( $rcType ) {
181  return array_search( $rcType, self::CHANGE_TYPES, true ) ?: "$rcType";
182  }
183 
191  public static function getChangeTypes() {
192  return array_keys( self::CHANGE_TYPES );
193  }
194 
201  public static function newFromId( $rcid ) {
202  return self::newFromConds( [ 'rc_id' => $rcid ], __METHOD__ );
203  }
204 
214  public static function newFromConds(
215  $conds,
216  $fname = __METHOD__,
217  $dbType = DB_REPLICA
218  ) {
219  $db = wfGetDB( $dbType );
220  $rcQuery = self::getQueryInfo();
221  $row = $db->selectRow(
222  $rcQuery['tables'], $rcQuery['fields'], $conds, $fname, [], $rcQuery['joins']
223  );
224  if ( $row !== false ) {
225  return self::newFromRow( $row );
226  } else {
227  return null;
228  }
229  }
230 
240  public static function getQueryInfo() {
241  $commentQuery = CommentStore::getStore()->getJoin( 'rc_comment' );
242  $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
243  return [
244  'tables' => [ 'recentchanges' ] + $commentQuery['tables'] + $actorQuery['tables'],
245  'fields' => [
246  'rc_id',
247  'rc_timestamp',
248  'rc_namespace',
249  'rc_title',
250  'rc_minor',
251  'rc_bot',
252  'rc_new',
253  'rc_cur_id',
254  'rc_this_oldid',
255  'rc_last_oldid',
256  'rc_type',
257  'rc_source',
258  'rc_patrolled',
259  'rc_ip',
260  'rc_old_len',
261  'rc_new_len',
262  'rc_deleted',
263  'rc_logid',
264  'rc_log_type',
265  'rc_log_action',
266  'rc_params',
267  ] + $commentQuery['fields'] + $actorQuery['fields'],
268  'joins' => $commentQuery['joins'] + $actorQuery['joins'],
269  ];
270  }
271 
272  # Accessors
273 
277  public function setAttribs( $attribs ) {
278  $this->mAttribs = $attribs;
279  }
280 
284  public function setExtra( $extra ) {
285  $this->mExtra = $extra;
286  }
287 
291  public function &getTitle() {
292  if ( $this->mTitle === false ) {
293  $this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
294  }
295 
296  return $this->mTitle;
297  }
298 
304  public function getPerformer() {
305  if ( $this->mPerformer === false ) {
306  if ( !empty( $this->mAttribs['rc_actor'] ) ) {
307  $this->mPerformer = User::newFromActorId( $this->mAttribs['rc_actor'] );
308  } elseif ( !empty( $this->mAttribs['rc_user'] ) ) {
309  $this->mPerformer = User::newFromId( $this->mAttribs['rc_user'] );
310  } elseif ( !empty( $this->mAttribs['rc_user_text'] ) ) {
311  $this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false );
312  } else {
313  throw new MWException( 'RecentChange object lacks rc_actor, rc_user, and rc_user_text' );
314  }
315  }
316 
317  return $this->mPerformer;
318  }
319 
329  public function save( $send = self::SEND_FEED ) {
331 
332  $dbw = wfGetDB( DB_MASTER );
333  if ( !is_array( $this->mExtra ) ) {
334  $this->mExtra = [];
335  }
336 
337  if ( !$wgPutIPinRC ) {
338  $this->mAttribs['rc_ip'] = '';
339  }
340 
341  # Strict mode fixups (not-NULL fields)
342  foreach ( [ 'minor', 'bot', 'new', 'patrolled', 'deleted' ] as $field ) {
343  $this->mAttribs["rc_$field"] = (int)$this->mAttribs["rc_$field"];
344  }
345  # ...more fixups (NULL fields)
346  foreach ( [ 'old_len', 'new_len' ] as $field ) {
347  $this->mAttribs["rc_$field"] = isset( $this->mAttribs["rc_$field"] )
348  ? (int)$this->mAttribs["rc_$field"]
349  : null;
350  }
351 
352  # If our database is strict about IP addresses, use NULL instead of an empty string
353  $strictIPs = $dbw->getType() === 'postgres'; // legacy
354  if ( $strictIPs && $this->mAttribs['rc_ip'] == '' ) {
355  unset( $this->mAttribs['rc_ip'] );
356  }
357 
358  # Trim spaces on user supplied text
359  $this->mAttribs['rc_comment'] = trim( $this->mAttribs['rc_comment'] );
360 
361  # Fixup database timestamps
362  $this->mAttribs['rc_timestamp'] = $dbw->timestamp( $this->mAttribs['rc_timestamp'] );
363 
364  # # If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
365  if ( $this->mAttribs['rc_cur_id'] == 0 ) {
366  unset( $this->mAttribs['rc_cur_id'] );
367  }
368 
369  $row = $this->mAttribs;
370 
371  # Convert mAttribs['rc_comment'] for CommentStore
372  $comment = $row['rc_comment'];
373  unset( $row['rc_comment'], $row['rc_comment_text'], $row['rc_comment_data'] );
374  $row += CommentStore::getStore()->insert( $dbw, 'rc_comment', $comment );
375 
376  # Convert mAttribs['rc_user'] etc for ActorMigration
377  $user = User::newFromAnyId(
378  $row['rc_user'] ?? null,
379  $row['rc_user_text'] ?? null,
380  $row['rc_actor'] ?? null
381  );
382  unset( $row['rc_user'], $row['rc_user_text'], $row['rc_actor'] );
383  $row += ActorMigration::newMigration()->getInsertValues( $dbw, 'rc_user', $user );
384 
385  # Don't reuse an existing rc_id for the new row, if one happens to be
386  # set for some reason.
387  unset( $row['rc_id'] );
388 
389  # Insert new row
390  $dbw->insert( 'recentchanges', $row, __METHOD__ );
391 
392  # Set the ID
393  $this->mAttribs['rc_id'] = $dbw->insertId();
394 
395  # Notify extensions
396  Hooks::runner()->onRecentChange_save( $this );
397 
398  if ( count( $this->tags ) ) {
399  ChangeTags::addTags( $this->tags, $this->mAttribs['rc_id'],
400  $this->mAttribs['rc_this_oldid'], $this->mAttribs['rc_logid'], null, $this );
401  }
402 
403  if ( $send === self::SEND_FEED ) {
404  // Emit the change to external applications via RCFeeds.
405  $this->notifyRCFeeds();
406  }
407 
408  # E-mail notifications
409  if ( $wgUseEnotif || $wgShowUpdatedMarker ) {
410  $editor = $this->getPerformer();
411  $title = $this->getTitle();
412 
413  // Never send an RC notification email about categorization changes
414  if (
415  Hooks::runner()->onAbortEmailNotification( $editor, $title, $this ) &&
416  $this->mAttribs['rc_type'] != RC_CATEGORIZE
417  ) {
418  // @FIXME: This would be better as an extension hook
419  // Send emails or email jobs once this row is safely committed
420  $dbw->onTransactionCommitOrIdle(
421  function () use ( $editor, $title ) {
422  $enotif = new EmailNotification();
423  $enotif->notifyOnPageChange(
424  $editor,
425  $title,
426  $this->mAttribs['rc_timestamp'],
427  $this->mAttribs['rc_comment'],
428  $this->mAttribs['rc_minor'],
429  $this->mAttribs['rc_last_oldid'],
430  $this->mExtra['pageStatus']
431  );
432  },
433  __METHOD__
434  );
435  }
436  }
437 
438  // Update the cached list of active users
439  if ( $this->mAttribs['rc_user'] > 0 ) {
441  }
442  }
443 
448  public function notifyRCFeeds( array $feeds = null ) {
449  global $wgRCFeeds;
450  if ( $feeds === null ) {
451  $feeds = $wgRCFeeds;
452  }
453 
454  $performer = $this->getPerformer();
455 
456  foreach ( $feeds as $params ) {
457  $params += [
458  'omit_bots' => false,
459  'omit_anon' => false,
460  'omit_user' => false,
461  'omit_minor' => false,
462  'omit_patrolled' => false,
463  ];
464 
465  if (
466  ( $params['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
467  ( $params['omit_anon'] && $performer->isAnon() ) ||
468  ( $params['omit_user'] && !$performer->isAnon() ) ||
469  ( $params['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
470  ( $params['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
471  $this->mAttribs['rc_type'] == RC_EXTERNAL
472  ) {
473  continue;
474  }
475 
476  $actionComment = $this->mExtra['actionCommentIRC'] ?? null;
477 
478  $feed = RCFeed::factory( $params );
479  $feed->notify( $this, $actionComment );
480  }
481  }
482 
491  public static function getEngine( $uri, $params = [] ) {
492  // TODO: Merge into RCFeed::factory().
493  global $wgRCEngines;
494  $scheme = parse_url( $uri, PHP_URL_SCHEME );
495  if ( !$scheme ) {
496  throw new MWException( "Invalid RCFeed uri: '$uri'" );
497  }
498  if ( !isset( $wgRCEngines[$scheme] ) ) {
499  throw new MWException( "Unknown RCFeedEngine scheme: '$scheme'" );
500  }
501  if ( defined( 'MW_PHPUNIT_TEST' ) && is_object( $wgRCEngines[$scheme] ) ) {
502  return $wgRCEngines[$scheme];
503  }
504  return new $wgRCEngines[$scheme]( $params );
505  }
506 
518  public static function markPatrolled( $change, $auto = false, $tags = null ) {
519  wfDeprecated( __METHOD__, '1.35' );
520 
521  global $wgUser;
522 
523  $change = $change instanceof RecentChange
524  ? $change
525  : self::newFromId( $change );
526 
527  if ( !$change instanceof RecentChange ) {
528  return null;
529  }
530 
531  return $change->doMarkPatrolled( $wgUser, $auto, $tags );
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  return $dbw->affectedRows();
622  }
623 
643  public static function notifyEdit(
644  $timestamp, $title, $minor, $user, $comment, $oldId, $lastTimestamp,
645  $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0,
646  $tags = []
647  ) {
648  $rc = new RecentChange;
649  $rc->mTitle = $title;
650  $rc->mPerformer = $user;
651  $rc->mAttribs = [
652  'rc_timestamp' => $timestamp,
653  'rc_namespace' => $title->getNamespace(),
654  'rc_title' => $title->getDBkey(),
655  'rc_type' => RC_EDIT,
656  'rc_source' => self::SRC_EDIT,
657  'rc_minor' => $minor ? 1 : 0,
658  'rc_cur_id' => $title->getArticleID(),
659  'rc_user' => $user->getId(),
660  'rc_user_text' => $user->getName(),
661  'rc_actor' => $user->getActorId(),
662  'rc_comment' => &$comment,
663  'rc_comment_text' => &$comment,
664  'rc_comment_data' => null,
665  'rc_this_oldid' => $newId,
666  'rc_last_oldid' => $oldId,
667  'rc_bot' => $bot ? 1 : 0,
668  'rc_ip' => self::checkIPAddress( $ip ),
669  'rc_patrolled' => intval( $patrol ),
670  'rc_new' => 0, # obsolete
671  'rc_old_len' => $oldSize,
672  'rc_new_len' => $newSize,
673  'rc_deleted' => 0,
674  'rc_logid' => 0,
675  'rc_log_type' => null,
676  'rc_log_action' => '',
677  'rc_params' => ''
678  ];
679 
680  $rc->mExtra = [
681  'prefixedDBkey' => $title->getPrefixedDBkey(),
682  'lastTimestamp' => $lastTimestamp,
683  'oldSize' => $oldSize,
684  'newSize' => $newSize,
685  'pageStatus' => 'changed'
686  ];
687 
689  function () use ( $rc, $tags ) {
690  $rc->addTags( $tags );
691  $rc->save();
692  },
694  wfGetDB( DB_MASTER )
695  );
696 
697  return $rc;
698  }
699 
717  public static function notifyNew(
718  $timestamp, $title, $minor, $user, $comment, $bot,
719  $ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = []
720  ) {
721  $rc = new RecentChange;
722  $rc->mTitle = $title;
723  $rc->mPerformer = $user;
724  $rc->mAttribs = [
725  'rc_timestamp' => $timestamp,
726  'rc_namespace' => $title->getNamespace(),
727  'rc_title' => $title->getDBkey(),
728  'rc_type' => RC_NEW,
729  'rc_source' => self::SRC_NEW,
730  'rc_minor' => $minor ? 1 : 0,
731  'rc_cur_id' => $title->getArticleID(),
732  'rc_user' => $user->getId(),
733  'rc_user_text' => $user->getName(),
734  'rc_actor' => $user->getActorId(),
735  'rc_comment' => &$comment,
736  'rc_comment_text' => &$comment,
737  'rc_comment_data' => null,
738  'rc_this_oldid' => $newId,
739  'rc_last_oldid' => 0,
740  'rc_bot' => $bot ? 1 : 0,
741  'rc_ip' => self::checkIPAddress( $ip ),
742  'rc_patrolled' => intval( $patrol ),
743  'rc_new' => 1, # obsolete
744  'rc_old_len' => 0,
745  'rc_new_len' => $size,
746  'rc_deleted' => 0,
747  'rc_logid' => 0,
748  'rc_log_type' => null,
749  'rc_log_action' => '',
750  'rc_params' => ''
751  ];
752 
753  $rc->mExtra = [
754  'prefixedDBkey' => $title->getPrefixedDBkey(),
755  'lastTimestamp' => 0,
756  'oldSize' => 0,
757  'newSize' => $size,
758  'pageStatus' => 'created'
759  ];
760 
762  function () use ( $rc, $tags ) {
763  $rc->addTags( $tags );
764  $rc->save();
765  },
767  wfGetDB( DB_MASTER )
768  );
769 
770  return $rc;
771  }
772 
788  public static function notifyLog( $timestamp, $title, $user, $actionComment, $ip, $type,
789  $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
790  ) {
791  global $wgLogRestrictions;
792 
793  # Don't add private logs to RC!
794  if ( isset( $wgLogRestrictions[$type] ) && $wgLogRestrictions[$type] != '*' ) {
795  return false;
796  }
797  $rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action,
798  $target, $logComment, $params, $newId, $actionCommentIRC );
799  $rc->save();
800 
801  return true;
802  }
803 
821  public static function newLogEntry( $timestamp, $title, $user, $actionComment, $ip,
822  $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '',
823  $revId = 0, $isPatrollable = false ) {
824  global $wgRequest;
825  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
826 
827  # # Get pageStatus for email notification
828  switch ( $type . '-' . $action ) {
829  case 'delete-delete':
830  case 'delete-delete_redir':
831  $pageStatus = 'deleted';
832  break;
833  case 'move-move':
834  case 'move-move_redir':
835  $pageStatus = 'moved';
836  break;
837  case 'delete-restore':
838  $pageStatus = 'restored';
839  break;
840  case 'upload-upload':
841  $pageStatus = 'created';
842  break;
843  case 'upload-overwrite':
844  default:
845  $pageStatus = 'changed';
846  break;
847  }
848 
849  // Allow unpatrolled status for patrollable log entries
850  $canAutopatrol = $permissionManager->userHasRight( $user, 'autopatrol' );
851  $markPatrolled = $isPatrollable ? $canAutopatrol : true;
852 
853  $rc = new RecentChange;
854  $rc->mTitle = $target;
855  $rc->mPerformer = $user;
856  $rc->mAttribs = [
857  'rc_timestamp' => $timestamp,
858  'rc_namespace' => $target->getNamespace(),
859  'rc_title' => $target->getDBkey(),
860  'rc_type' => RC_LOG,
861  'rc_source' => self::SRC_LOG,
862  'rc_minor' => 0,
863  'rc_cur_id' => $target->getArticleID(),
864  'rc_user' => $user->getId(),
865  'rc_user_text' => $user->getName(),
866  'rc_actor' => $user->getActorId(),
867  'rc_comment' => &$logComment,
868  'rc_comment_text' => &$logComment,
869  'rc_comment_data' => null,
870  'rc_this_oldid' => $revId,
871  'rc_last_oldid' => 0,
872  'rc_bot' => $permissionManager->userHasRight( $user, 'bot' ) ?
873  (int)$wgRequest->getBool( 'bot', true ) : 0,
874  'rc_ip' => self::checkIPAddress( $ip ),
875  'rc_patrolled' => $markPatrolled ? self::PRC_AUTOPATROLLED : self::PRC_UNPATROLLED,
876  'rc_new' => 0, # obsolete
877  'rc_old_len' => null,
878  'rc_new_len' => null,
879  'rc_deleted' => 0,
880  'rc_logid' => $newId,
881  'rc_log_type' => $type,
882  'rc_log_action' => $action,
883  'rc_params' => $params
884  ];
885 
886  $rc->mExtra = [
887  'prefixedDBkey' => $title->getPrefixedDBkey(),
888  'lastTimestamp' => 0,
889  'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
890  'pageStatus' => $pageStatus,
891  'actionCommentIRC' => $actionCommentIRC
892  ];
893 
894  return $rc;
895  }
896 
918  public static function newForCategorization(
919  $timestamp,
920  Title $categoryTitle,
921  ?User $user,
922  $comment,
923  Title $pageTitle,
924  $oldRevId,
925  $newRevId,
926  $lastTimestamp,
927  $bot,
928  $ip = '',
929  $deleted = 0,
930  $added = null
931  ) {
932  // Done in a backwards compatible way.
933  $params = [
934  'hidden-cat' => WikiCategoryPage::factory( $categoryTitle )->isHidden()
935  ];
936  if ( $added !== null ) {
937  $params['added'] = $added;
938  }
939 
940  $rc = new RecentChange;
941  $rc->mTitle = $categoryTitle;
942  $rc->mPerformer = $user;
943  $rc->mAttribs = [
944  'rc_timestamp' => $timestamp,
945  'rc_namespace' => $categoryTitle->getNamespace(),
946  'rc_title' => $categoryTitle->getDBkey(),
947  'rc_type' => RC_CATEGORIZE,
948  'rc_source' => self::SRC_CATEGORIZE,
949  'rc_minor' => 0,
950  'rc_cur_id' => $pageTitle->getArticleID(),
951  'rc_user' => $user ? $user->getId() : 0,
952  'rc_user_text' => $user ? $user->getName() : '',
953  'rc_actor' => $user ? $user->getActorId() : null,
954  'rc_comment' => &$comment,
955  'rc_comment_text' => &$comment,
956  'rc_comment_data' => null,
957  'rc_this_oldid' => $newRevId,
958  'rc_last_oldid' => $oldRevId,
959  'rc_bot' => $bot ? 1 : 0,
960  'rc_ip' => self::checkIPAddress( $ip ),
961  'rc_patrolled' => self::PRC_AUTOPATROLLED, // Always patrolled, just like log entries
962  'rc_new' => 0, # obsolete
963  'rc_old_len' => null,
964  'rc_new_len' => null,
965  'rc_deleted' => $deleted,
966  'rc_logid' => 0,
967  'rc_log_type' => null,
968  'rc_log_action' => '',
969  'rc_params' => serialize( $params )
970  ];
971 
972  $rc->mExtra = [
973  'prefixedDBkey' => $categoryTitle->getPrefixedDBkey(),
974  'lastTimestamp' => $lastTimestamp,
975  'oldSize' => 0,
976  'newSize' => 0,
977  'pageStatus' => 'changed'
978  ];
979 
980  return $rc;
981  }
982 
991  public function getParam( $name ) {
992  $params = $this->parseParams();
993  return $params[$name] ?? null;
994  }
995 
1001  public function loadFromRow( $row ) {
1002  $this->mAttribs = get_object_vars( $row );
1003  $this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
1004  // rc_deleted MUST be set
1005  $this->mAttribs['rc_deleted'] = $row->rc_deleted;
1006 
1007  if ( isset( $this->mAttribs['rc_ip'] ) ) {
1008  // Clean up CIDRs for Postgres per T164898. ("127.0.0.1" casts to "127.0.0.1/32")
1009  $n = strpos( $this->mAttribs['rc_ip'], '/' );
1010  if ( $n !== false ) {
1011  $this->mAttribs['rc_ip'] = substr( $this->mAttribs['rc_ip'], 0, $n );
1012  }
1013  }
1014 
1015  $comment = CommentStore::getStore()
1016  // Legacy because $row may have come from self::selectFields()
1017  ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $row, true )
1018  ->text;
1019  $this->mAttribs['rc_comment'] = &$comment;
1020  $this->mAttribs['rc_comment_text'] = &$comment;
1021  $this->mAttribs['rc_comment_data'] = null;
1022 
1023  $user = User::newFromAnyId(
1024  $this->mAttribs['rc_user'] ?? null,
1025  $this->mAttribs['rc_user_text'] ?? null,
1026  $this->mAttribs['rc_actor'] ?? null
1027  );
1028  $this->mAttribs['rc_user'] = $user->getId();
1029  $this->mAttribs['rc_user_text'] = $user->getName();
1030  $this->mAttribs['rc_actor'] = $user->getActorId();
1031 
1032  // Watchlist expiry.
1033  if ( isset( $row->we_expiry ) && $row->we_expiry ) {
1034  $this->watchlistExpiry = wfTimestamp( TS_MW, $row->we_expiry );
1035  }
1036  }
1037 
1044  public function getAttribute( $name ) {
1045  if ( $name === 'rc_comment' ) {
1046  return CommentStore::getStore()
1047  ->getComment( 'rc_comment', $this->mAttribs, true )->text;
1048  }
1049 
1050  if ( $name === 'rc_user' || $name === 'rc_user_text' || $name === 'rc_actor' ) {
1051  $user = User::newFromAnyId(
1052  $this->mAttribs['rc_user'] ?? null,
1053  $this->mAttribs['rc_user_text'] ?? null,
1054  $this->mAttribs['rc_actor'] ?? null
1055  );
1056  if ( $name === 'rc_user' ) {
1057  return $user->getId();
1058  }
1059  if ( $name === 'rc_user_text' ) {
1060  return $user->getName();
1061  }
1062  if ( $name === 'rc_actor' ) {
1063  return $user->getActorId();
1064  }
1065  }
1066 
1067  return $this->mAttribs[$name] ?? null;
1068  }
1069 
1073  public function getAttributes() {
1074  return $this->mAttribs;
1075  }
1076 
1083  public function diffLinkTrail( $forceCur ) {
1084  if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
1085  $trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
1086  "&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
1087  if ( $forceCur ) {
1088  $trail .= '&diff=0';
1089  } else {
1090  $trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
1091  }
1092  } else {
1093  $trail = '';
1094  }
1095 
1096  return $trail;
1097  }
1098 
1106  public function getCharacterDifference( $old = 0, $new = 0 ) {
1107  if ( $old === 0 ) {
1108  $old = $this->mAttribs['rc_old_len'];
1109  }
1110  if ( $new === 0 ) {
1111  $new = $this->mAttribs['rc_new_len'];
1112  }
1113  if ( $old === null || $new === null ) {
1114  return '';
1115  }
1116 
1117  return ChangesList::showCharacterDifference( $old, $new );
1118  }
1119 
1120  private static function checkIPAddress( $ip ) {
1121  global $wgRequest;
1122  if ( $ip ) {
1123  if ( !IPUtils::isIPAddress( $ip ) ) {
1124  throw new MWException( "Attempt to write \"" . $ip .
1125  "\" as an IP address into recent changes" );
1126  }
1127  } else {
1128  $ip = $wgRequest->getIP();
1129  if ( !$ip ) {
1130  $ip = '';
1131  }
1132  }
1133 
1134  return $ip;
1135  }
1136 
1146  public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
1147  global $wgRCMaxAge;
1148 
1149  return wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $wgRCMaxAge;
1150  }
1151 
1159  public function parseParams() {
1160  $rcParams = $this->getAttribute( 'rc_params' );
1161 
1162  Wikimedia\suppressWarnings();
1163  $unserializedParams = unserialize( $rcParams );
1164  Wikimedia\restoreWarnings();
1165 
1166  return $unserializedParams;
1167  }
1168 
1177  public function addTags( $tags ) {
1178  if ( is_string( $tags ) ) {
1179  $this->tags[] = $tags;
1180  } else {
1181  $this->tags = array_merge( $tags, $this->tags );
1182  }
1183  }
1184 }
RecentChange\getCharacterDifference
getCharacterDifference( $old=0, $new=0)
Returns the change size (HTML).
Definition: RecentChange.php:1106
RecentChange\save
save( $send=self::SEND_FEED)
Writes the data in this object to the database.
Definition: RecentChange.php:329
RecentChange\getQueryInfo
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new recentchanges object.
Definition: RecentChange.php:240
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\$tags
array $tags
List of tags to apply.
Definition: RecentChange.php:125
User\getId
getId()
Get the user's ID.
Definition: User.php:2052
RecentChange\setExtra
setExtra( $extra)
Definition: RecentChange.php:284
User\getActorId
getActorId(IDatabase $dbw=null)
Get the user's actor ID.
Definition: User.php:2120
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:152
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:214
$wgShowUpdatedMarker
$wgShowUpdatedMarker
Show "Updated (since my last visit)" marker in RC view, watchlist and history view for watched pages ...
Definition: DefaultSettings.php:7461
Title\getPrefixedDBkey
getPrefixedDBkey()
Get the prefixed database key form.
Definition: Title.php:1838
RecentChange\getAttributes
getAttributes()
Definition: RecentChange.php:1073
PatrolLog\record
static record( $rc, $auto=false, User $user=null, $tags=null)
Record a log event for a change being patrolled.
Definition: PatrolLog.php:43
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:72
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1808
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:3216
RecentChange\notifyLog
static notifyLog( $timestamp, $title, $user, $actionComment, $ip, $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='')
Definition: RecentChange.php:788
RecentChange\getEngine
static getEngine( $uri, $params=[])
Definition: RecentChange.php:491
RecentChange\loadFromRow
loadFromRow( $row)
Initialises the members of this object from a mysql row object.
Definition: RecentChange.php:1001
RecentChange\getChangeTypes
static getChangeTypes()
Get an array of all change types.
Definition: RecentChange.php:191
RecentChange\reallyMarkPatrolled
reallyMarkPatrolled()
Mark this RecentChange patrolled, without error checking.
Definition: RecentChange.php:605
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:120
RecentChange\setAttribs
setAttribs( $attribs)
Definition: RecentChange.php:277
MediaWiki\ChangeTags\Taggable
Interface that defines how to tag objects.
Definition: Taggable.php:32
RecentChange\SRC_CATEGORIZE
const SRC_CATEGORIZE
Definition: RecentChange.php:79
RecentChange\parseToRCType
static parseToRCType( $type)
Parsing text to RC_* constants.
Definition: RecentChange.php:158
serialize
serialize()
Definition: ApiMessageTrait.php:138
$wgUseRCPatrol
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
Definition: DefaultSettings.php:7354
$wgUseNPPatrol
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
Definition: DefaultSettings.php:7370
RecentChange\SRC_LOG
const SRC_LOG
Definition: RecentChange.php:77
RecentChangesUpdateJob\newCacheUpdateJob
static newCacheUpdateJob()
Definition: RecentChangesUpdateJob.php:54
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:139
RecentChange\parseParams
parseParams()
Parses and returns the rc_params attribute.
Definition: RecentChange.php:1159
$wgPutIPinRC
$wgPutIPinRC
Log IP addresses in the recentchanges table; can be accessed only by extensions (e....
Definition: DefaultSettings.php:6191
RecentChange\$mTitle
Title false $mTitle
Definition: RecentChange.php:102
Title\getDBkey
getDBkey()
Get the main part with underscores.
Definition: Title.php:1023
MWException
MediaWiki exception.
Definition: MWException.php:28
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:154
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1026
Title\getNamespace
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1032
RecentChange\$notificationtimestamp
$notificationtimestamp
Definition: RecentChange.php:110
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2461
RecentChange\$watchlistExpiry
string null $watchlistExpiry
The expiry time, if this is a temporary watchlist item.
Definition: RecentChange.php:115
RecentChange\notifyEdit
static notifyEdit( $timestamp, $title, $minor, $user, $comment, $oldId, $lastTimestamp, $bot, $ip='', $oldSize=0, $newSize=0, $newId=0, $patrol=0, $tags=[])
Makes an entry in the database corresponding to an edit.
Definition: RecentChange.php:643
$wgRCFeeds
$wgRCFeeds
Configuration for feeds to which notifications about recent changes will be sent.
Definition: DefaultSettings.php:7321
RecentChange\newFromRow
static newFromRow( $row)
Definition: RecentChange.php:144
DeferredUpdates\POSTSEND
const POSTSEND
Definition: DeferredUpdates.php:85
$title
$title
Definition: testCompression.php:38
RecentChange\SRC_EDIT
const SRC_EDIT
Definition: RecentChange.php:75
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:592
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:1083
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:76
$wgUseFilePatrol
$wgUseFilePatrol
Use file patrolling to check new files on Special:Newfiles.
Definition: DefaultSettings.php:7381
RecentChange\PRC_PATROLLED
const PRC_PATROLLED
Definition: RecentChange.php:82
RecentChange\newFromId
static newFromId( $rcid)
Obtain the recent change with a given rc_id value.
Definition: RecentChange.php:201
RecentChange\doMarkPatrolled
doMarkPatrolled(User $user, $auto=false, $tags=null)
Mark this RecentChange as patrolled.
Definition: RecentChange.php:545
RecentChange\$mExtra
$mExtra
Definition: RecentChange.php:97
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:918
$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:8167
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:7221
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\markPatrolled
static markPatrolled( $change, $auto=false, $tags=null)
Mark a given change as patrolled.
Definition: RecentChange.php:518
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:83
RecentChange\addTags
addTags( $tags)
Tags to append to the recent change, and associated revision/log.
Definition: RecentChange.php:1177
RecentChange\getAttribute
getAttribute( $name)
Get an attribute value.
Definition: RecentChange.php:1044
$wgUseEnotif
$wgUseEnotif
Definition: Setup.php:441
RecentChange\checkIPAddress
static checkIPAddress( $ip)
Definition: RecentChange.php:1120
RecentChange\newLogEntry
static newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='', $revId=0, $isPatrollable=false)
Definition: RecentChange.php:821
unserialize
unserialize( $serialized)
Definition: ApiMessageTrait.php:146
Title
Represents a title within MediaWiki.
Definition: Title.php:42
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:717
RecentChange\parseFromRCType
static parseFromRCType( $rcType)
Parsing RC_* constants to human-readable test.
Definition: RecentChange.php:180
$wgRCEngines
$wgRCEngines
Used by RecentChange::getEngine to find the correct engine for a given URI scheme.
Definition: DefaultSettings.php:7328
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:81
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:1146
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:107
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:448
$t
$t
Definition: testCompression.php:74
RecentChange\$mAttribs
array $mAttribs
Definition: RecentChange.php:96
RecentChange\getPerformer
getPerformer()
Get the User object of the person who performed this change.
Definition: RecentChange.php:304
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:291
CommentStore\getStore
static getStore()
Definition: CommentStore.php:109
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:59
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:2081
RecentChange\getParam
getParam( $name)
Get a parameter value.
Definition: RecentChange.php:991
RecentChange\$numberofWatchingusers
$numberofWatchingusers
Definition: RecentChange.php:109
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:271
RecentChange\SRC_EXTERNAL
const SRC_EXTERNAL
Definition: RecentChange.php:78
$type
$type
Definition: testCompression.php:52