MediaWiki  master
RecentChange.php
Go to the documentation of this file.
1 <?php
24 
70 class RecentChange implements Taggable {
71  // Constants for the rc_source field. Extensions may also have
72  // their own source constants.
73  const SRC_EDIT = 'mw.edit';
74  const SRC_NEW = 'mw.new';
75  const SRC_LOG = 'mw.log';
76  const SRC_EXTERNAL = 'mw.external'; // obsolete
77  const SRC_CATEGORIZE = 'mw.categorize';
78 
79  const PRC_UNPATROLLED = 0;
80  const PRC_PATROLLED = 1;
81  const PRC_AUTOPATROLLED = 2;
82 
86  const SEND_NONE = true;
87 
91  const SEND_FEED = false;
92 
94  public $mAttribs = [];
95  public $mExtra = [];
96 
100  public $mTitle = false;
101 
105  private $mPerformer = false;
106 
107  public $numberofWatchingusers = 0; # Dummy to prevent error message in SpecialRecentChangesLinked
109 
113  public $counter = -1;
114 
118  private $tags = [];
119 
123  private const CHANGE_TYPES = [
124  'edit' => RC_EDIT,
125  'new' => RC_NEW,
126  'log' => RC_LOG,
127  'external' => RC_EXTERNAL,
128  'categorize' => RC_CATEGORIZE,
129  ];
130 
131  # Factory methods
132 
137  public static function newFromRow( $row ) {
138  $rc = new RecentChange;
139  $rc->loadFromRow( $row );
140 
141  return $rc;
142  }
143 
151  public static function parseToRCType( $type ) {
152  if ( is_array( $type ) ) {
153  $retval = [];
154  foreach ( $type as $t ) {
155  $retval[] = self::parseToRCType( $t );
156  }
157 
158  return $retval;
159  }
160 
161  if ( !array_key_exists( $type, self::CHANGE_TYPES ) ) {
162  throw new MWException( "Unknown type '$type'" );
163  }
164  return self::CHANGE_TYPES[$type];
165  }
166 
173  public static function parseFromRCType( $rcType ) {
174  return array_search( $rcType, self::CHANGE_TYPES, true ) ?: "$rcType";
175  }
176 
184  public static function getChangeTypes() {
185  return array_keys( self::CHANGE_TYPES );
186  }
187 
194  public static function newFromId( $rcid ) {
195  return self::newFromConds( [ 'rc_id' => $rcid ], __METHOD__ );
196  }
197 
207  public static function newFromConds(
208  $conds,
209  $fname = __METHOD__,
210  $dbType = DB_REPLICA
211  ) {
212  $db = wfGetDB( $dbType );
213  $rcQuery = self::getQueryInfo();
214  $row = $db->selectRow(
215  $rcQuery['tables'], $rcQuery['fields'], $conds, $fname, [], $rcQuery['joins']
216  );
217  if ( $row !== false ) {
218  return self::newFromRow( $row );
219  } else {
220  return null;
221  }
222  }
223 
233  public static function getQueryInfo() {
234  $commentQuery = CommentStore::getStore()->getJoin( 'rc_comment' );
235  $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
236  return [
237  'tables' => [ 'recentchanges' ] + $commentQuery['tables'] + $actorQuery['tables'],
238  'fields' => [
239  'rc_id',
240  'rc_timestamp',
241  'rc_namespace',
242  'rc_title',
243  'rc_minor',
244  'rc_bot',
245  'rc_new',
246  'rc_cur_id',
247  'rc_this_oldid',
248  'rc_last_oldid',
249  'rc_type',
250  'rc_source',
251  'rc_patrolled',
252  'rc_ip',
253  'rc_old_len',
254  'rc_new_len',
255  'rc_deleted',
256  'rc_logid',
257  'rc_log_type',
258  'rc_log_action',
259  'rc_params',
260  ] + $commentQuery['fields'] + $actorQuery['fields'],
261  'joins' => $commentQuery['joins'] + $actorQuery['joins'],
262  ];
263  }
264 
265  # Accessors
266 
270  public function setAttribs( $attribs ) {
271  $this->mAttribs = $attribs;
272  }
273 
277  public function setExtra( $extra ) {
278  $this->mExtra = $extra;
279  }
280 
284  public function &getTitle() {
285  if ( $this->mTitle === false ) {
286  $this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
287  }
288 
289  return $this->mTitle;
290  }
291 
297  public function getPerformer() {
298  if ( $this->mPerformer === false ) {
299  if ( !empty( $this->mAttribs['rc_actor'] ) ) {
300  $this->mPerformer = User::newFromActorId( $this->mAttribs['rc_actor'] );
301  } elseif ( !empty( $this->mAttribs['rc_user'] ) ) {
302  $this->mPerformer = User::newFromId( $this->mAttribs['rc_user'] );
303  } elseif ( !empty( $this->mAttribs['rc_user_text'] ) ) {
304  $this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false );
305  } else {
306  throw new MWException( 'RecentChange object lacks rc_actor, rc_user, and rc_user_text' );
307  }
308  }
309 
310  return $this->mPerformer;
311  }
312 
322  public function save( $send = self::SEND_FEED ) {
324 
325  $dbw = wfGetDB( DB_MASTER );
326  if ( !is_array( $this->mExtra ) ) {
327  $this->mExtra = [];
328  }
329 
330  if ( !$wgPutIPinRC ) {
331  $this->mAttribs['rc_ip'] = '';
332  }
333 
334  # Strict mode fixups (not-NULL fields)
335  foreach ( [ 'minor', 'bot', 'new', 'patrolled', 'deleted' ] as $field ) {
336  $this->mAttribs["rc_$field"] = (int)$this->mAttribs["rc_$field"];
337  }
338  # ...more fixups (NULL fields)
339  foreach ( [ 'old_len', 'new_len' ] as $field ) {
340  $this->mAttribs["rc_$field"] = isset( $this->mAttribs["rc_$field"] )
341  ? (int)$this->mAttribs["rc_$field"]
342  : null;
343  }
344 
345  # If our database is strict about IP addresses, use NULL instead of an empty string
346  $strictIPs = $dbw->getType() === 'postgres'; // legacy
347  if ( $strictIPs && $this->mAttribs['rc_ip'] == '' ) {
348  unset( $this->mAttribs['rc_ip'] );
349  }
350 
351  # Trim spaces on user supplied text
352  $this->mAttribs['rc_comment'] = trim( $this->mAttribs['rc_comment'] );
353 
354  # Fixup database timestamps
355  $this->mAttribs['rc_timestamp'] = $dbw->timestamp( $this->mAttribs['rc_timestamp'] );
356 
357  # # If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
358  if ( $this->mAttribs['rc_cur_id'] == 0 ) {
359  unset( $this->mAttribs['rc_cur_id'] );
360  }
361 
362  $row = $this->mAttribs;
363 
364  # Convert mAttribs['rc_comment'] for CommentStore
365  $comment = $row['rc_comment'];
366  unset( $row['rc_comment'], $row['rc_comment_text'], $row['rc_comment_data'] );
367  $row += CommentStore::getStore()->insert( $dbw, 'rc_comment', $comment );
368 
369  # Convert mAttribs['rc_user'] etc for ActorMigration
370  $user = User::newFromAnyId(
371  $row['rc_user'] ?? null,
372  $row['rc_user_text'] ?? null,
373  $row['rc_actor'] ?? null
374  );
375  unset( $row['rc_user'], $row['rc_user_text'], $row['rc_actor'] );
376  $row += ActorMigration::newMigration()->getInsertValues( $dbw, 'rc_user', $user );
377 
378  # Don't reuse an existing rc_id for the new row, if one happens to be
379  # set for some reason.
380  unset( $row['rc_id'] );
381 
382  # Insert new row
383  $dbw->insert( 'recentchanges', $row, __METHOD__ );
384 
385  # Set the ID
386  $this->mAttribs['rc_id'] = $dbw->insertId();
387 
388  # Notify extensions
389  // Avoid PHP 7.1 warning from passing $this by reference
390  $rc = $this;
391  Hooks::run( 'RecentChange_save', [ &$rc ] );
392 
393  if ( count( $this->tags ) ) {
394  ChangeTags::addTags( $this->tags, $this->mAttribs['rc_id'],
395  $this->mAttribs['rc_this_oldid'], $this->mAttribs['rc_logid'], null, $this );
396  }
397 
398  if ( $send === self::SEND_FEED ) {
399  // Emit the change to external applications via RCFeeds.
400  $this->notifyRCFeeds();
401  }
402 
403  # E-mail notifications
404  if ( $wgUseEnotif || $wgShowUpdatedMarker ) {
405  $editor = $this->getPerformer();
406  $title = $this->getTitle();
407 
408  // Never send an RC notification email about categorization changes
409  if (
410  Hooks::run( 'AbortEmailNotification', [ $editor, $title, $this ] ) &&
411  $this->mAttribs['rc_type'] != RC_CATEGORIZE
412  ) {
413  // @FIXME: This would be better as an extension hook
414  // Send emails or email jobs once this row is safely committed
415  $dbw->onTransactionCommitOrIdle(
416  function () use ( $editor, $title ) {
417  $enotif = new EmailNotification();
418  $enotif->notifyOnPageChange(
419  $editor,
420  $title,
421  $this->mAttribs['rc_timestamp'],
422  $this->mAttribs['rc_comment'],
423  $this->mAttribs['rc_minor'],
424  $this->mAttribs['rc_last_oldid'],
425  $this->mExtra['pageStatus']
426  );
427  },
428  __METHOD__
429  );
430  }
431  }
432 
433  // Update the cached list of active users
434  if ( $this->mAttribs['rc_user'] > 0 ) {
436  }
437  }
438 
443  public function notifyRCFeeds( array $feeds = null ) {
444  global $wgRCFeeds;
445  if ( $feeds === null ) {
446  $feeds = $wgRCFeeds;
447  }
448 
449  $performer = $this->getPerformer();
450 
451  foreach ( $feeds as $params ) {
452  $params += [
453  'omit_bots' => false,
454  'omit_anon' => false,
455  'omit_user' => false,
456  'omit_minor' => false,
457  'omit_patrolled' => false,
458  ];
459 
460  if (
461  ( $params['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
462  ( $params['omit_anon'] && $performer->isAnon() ) ||
463  ( $params['omit_user'] && !$performer->isAnon() ) ||
464  ( $params['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
465  ( $params['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
466  $this->mAttribs['rc_type'] == RC_EXTERNAL
467  ) {
468  continue;
469  }
470 
471  $actionComment = $this->mExtra['actionCommentIRC'] ?? null;
472 
473  $feed = RCFeed::factory( $params );
474  $feed->notify( $this, $actionComment );
475  }
476  }
477 
486  public static function getEngine( $uri, $params = [] ) {
487  // TODO: Merge into RCFeed::factory().
488  global $wgRCEngines;
489  $scheme = parse_url( $uri, PHP_URL_SCHEME );
490  if ( !$scheme ) {
491  throw new MWException( "Invalid RCFeed uri: '$uri'" );
492  }
493  if ( !isset( $wgRCEngines[$scheme] ) ) {
494  throw new MWException( "Unknown RCFeedEngine scheme: '$scheme'" );
495  }
496  if ( defined( 'MW_PHPUNIT_TEST' ) && is_object( $wgRCEngines[$scheme] ) ) {
497  return $wgRCEngines[$scheme];
498  }
499  return new $wgRCEngines[$scheme]( $params );
500  }
501 
511  public static function markPatrolled( $change, $auto = false, $tags = null ) {
512  global $wgUser;
513 
514  $change = $change instanceof RecentChange
515  ? $change
516  : self::newFromId( $change );
517 
518  if ( !$change instanceof RecentChange ) {
519  return null;
520  }
521 
522  return $change->doMarkPatrolled( $wgUser, $auto, $tags );
523  }
524 
536  public function doMarkPatrolled( User $user, $auto = false, $tags = null ) {
538 
539  // Fix up $tags so that the MarkPatrolled hook below always gets an array
540  if ( $tags === null ) {
541  $tags = [];
542  } elseif ( is_string( $tags ) ) {
543  $tags = [ $tags ];
544  }
545 
546  $errors = [];
547  // If recentchanges patrol is disabled, only new pages or new file versions
548  // can be patrolled, provided the appropriate config variable is set
549  if ( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) &&
550  ( !$wgUseFilePatrol || !( $this->getAttribute( 'rc_type' ) == RC_LOG &&
551  $this->getAttribute( 'rc_log_type' ) == 'upload' ) ) ) {
552  $errors[] = [ 'rcpatroldisabled' ];
553  }
554  // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
555  $right = $auto ? 'autopatrol' : 'patrol';
556  $errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $user ) );
557  if ( !Hooks::run( 'MarkPatrolled',
558  [ $this->getAttribute( 'rc_id' ), &$user, false, $auto, &$tags ] )
559  ) {
560  $errors[] = [ 'hookaborted' ];
561  }
562  // Users without the 'autopatrol' right can't patrol their
563  // own revisions
564  if ( $user->getName() === $this->getAttribute( 'rc_user_text' ) &&
565  !MediaWikiServices::getInstance()->getPermissionManager()
566  ->userHasRight( $user, 'autopatrol' )
567  ) {
568  $errors[] = [ 'markedaspatrollederror-noautopatrol' ];
569  }
570  if ( $errors ) {
571  return $errors;
572  }
573  // If the change was patrolled already, do nothing
574  if ( $this->getAttribute( 'rc_patrolled' ) ) {
575  return [];
576  }
577  // Actually set the 'patrolled' flag in RC
578  $this->reallyMarkPatrolled();
579  // Log this patrol event
580  PatrolLog::record( $this, $auto, $user, $tags );
581 
582  Hooks::run(
583  'MarkPatrolledComplete',
584  [ $this->getAttribute( 'rc_id' ), &$user, false, $auto ]
585  );
586 
587  return [];
588  }
589 
594  public function reallyMarkPatrolled() {
595  $dbw = wfGetDB( DB_MASTER );
596  $dbw->update(
597  'recentchanges',
598  [
599  'rc_patrolled' => self::PRC_PATROLLED
600  ],
601  [
602  'rc_id' => $this->getAttribute( 'rc_id' )
603  ],
604  __METHOD__
605  );
606  // Invalidate the page cache after the page has been patrolled
607  // to make sure that the Patrol link isn't visible any longer!
608  $this->getTitle()->invalidateCache();
609 
610  return $dbw->affectedRows();
611  }
612 
632  public static function notifyEdit(
633  $timestamp, $title, $minor, $user, $comment, $oldId, $lastTimestamp,
634  $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0,
635  $tags = []
636  ) {
637  $rc = new RecentChange;
638  $rc->mTitle = $title;
639  $rc->mPerformer = $user;
640  $rc->mAttribs = [
641  'rc_timestamp' => $timestamp,
642  'rc_namespace' => $title->getNamespace(),
643  'rc_title' => $title->getDBkey(),
644  'rc_type' => RC_EDIT,
645  'rc_source' => self::SRC_EDIT,
646  'rc_minor' => $minor ? 1 : 0,
647  'rc_cur_id' => $title->getArticleID(),
648  'rc_user' => $user->getId(),
649  'rc_user_text' => $user->getName(),
650  'rc_actor' => $user->getActorId(),
651  'rc_comment' => &$comment,
652  'rc_comment_text' => &$comment,
653  'rc_comment_data' => null,
654  'rc_this_oldid' => $newId,
655  'rc_last_oldid' => $oldId,
656  'rc_bot' => $bot ? 1 : 0,
657  'rc_ip' => self::checkIPAddress( $ip ),
658  'rc_patrolled' => intval( $patrol ),
659  'rc_new' => 0, # obsolete
660  'rc_old_len' => $oldSize,
661  'rc_new_len' => $newSize,
662  'rc_deleted' => 0,
663  'rc_logid' => 0,
664  'rc_log_type' => null,
665  'rc_log_action' => '',
666  'rc_params' => ''
667  ];
668 
669  $rc->mExtra = [
670  'prefixedDBkey' => $title->getPrefixedDBkey(),
671  'lastTimestamp' => $lastTimestamp,
672  'oldSize' => $oldSize,
673  'newSize' => $newSize,
674  'pageStatus' => 'changed'
675  ];
676 
678  function () use ( $rc, $tags ) {
679  $rc->addTags( $tags );
680  $rc->save();
681  },
683  wfGetDB( DB_MASTER )
684  );
685 
686  return $rc;
687  }
688 
706  public static function notifyNew(
707  $timestamp, $title, $minor, $user, $comment, $bot,
708  $ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = []
709  ) {
710  $rc = new RecentChange;
711  $rc->mTitle = $title;
712  $rc->mPerformer = $user;
713  $rc->mAttribs = [
714  'rc_timestamp' => $timestamp,
715  'rc_namespace' => $title->getNamespace(),
716  'rc_title' => $title->getDBkey(),
717  'rc_type' => RC_NEW,
718  'rc_source' => self::SRC_NEW,
719  'rc_minor' => $minor ? 1 : 0,
720  'rc_cur_id' => $title->getArticleID(),
721  'rc_user' => $user->getId(),
722  'rc_user_text' => $user->getName(),
723  'rc_actor' => $user->getActorId(),
724  'rc_comment' => &$comment,
725  'rc_comment_text' => &$comment,
726  'rc_comment_data' => null,
727  'rc_this_oldid' => $newId,
728  'rc_last_oldid' => 0,
729  'rc_bot' => $bot ? 1 : 0,
730  'rc_ip' => self::checkIPAddress( $ip ),
731  'rc_patrolled' => intval( $patrol ),
732  'rc_new' => 1, # obsolete
733  'rc_old_len' => 0,
734  'rc_new_len' => $size,
735  'rc_deleted' => 0,
736  'rc_logid' => 0,
737  'rc_log_type' => null,
738  'rc_log_action' => '',
739  'rc_params' => ''
740  ];
741 
742  $rc->mExtra = [
743  'prefixedDBkey' => $title->getPrefixedDBkey(),
744  'lastTimestamp' => 0,
745  'oldSize' => 0,
746  'newSize' => $size,
747  'pageStatus' => 'created'
748  ];
749 
751  function () use ( $rc, $tags ) {
752  $rc->addTags( $tags );
753  $rc->save();
754  },
756  wfGetDB( DB_MASTER )
757  );
758 
759  return $rc;
760  }
761 
777  public static function notifyLog( $timestamp, $title, $user, $actionComment, $ip, $type,
778  $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
779  ) {
780  global $wgLogRestrictions;
781 
782  # Don't add private logs to RC!
783  if ( isset( $wgLogRestrictions[$type] ) && $wgLogRestrictions[$type] != '*' ) {
784  return false;
785  }
786  $rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action,
787  $target, $logComment, $params, $newId, $actionCommentIRC );
788  $rc->save();
789 
790  return true;
791  }
792 
810  public static function newLogEntry( $timestamp, $title, $user, $actionComment, $ip,
811  $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '',
812  $revId = 0, $isPatrollable = false ) {
813  global $wgRequest;
814  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
815 
816  # # Get pageStatus for email notification
817  switch ( $type . '-' . $action ) {
818  case 'delete-delete':
819  case 'delete-delete_redir':
820  $pageStatus = 'deleted';
821  break;
822  case 'move-move':
823  case 'move-move_redir':
824  $pageStatus = 'moved';
825  break;
826  case 'delete-restore':
827  $pageStatus = 'restored';
828  break;
829  case 'upload-upload':
830  $pageStatus = 'created';
831  break;
832  case 'upload-overwrite':
833  default:
834  $pageStatus = 'changed';
835  break;
836  }
837 
838  // Allow unpatrolled status for patrollable log entries
839  $canAutopatrol = $permissionManager->userHasRight( $user, 'autopatrol' );
840  $markPatrolled = $isPatrollable ? $canAutopatrol : true;
841 
842  $rc = new RecentChange;
843  $rc->mTitle = $target;
844  $rc->mPerformer = $user;
845  $rc->mAttribs = [
846  'rc_timestamp' => $timestamp,
847  'rc_namespace' => $target->getNamespace(),
848  'rc_title' => $target->getDBkey(),
849  'rc_type' => RC_LOG,
850  'rc_source' => self::SRC_LOG,
851  'rc_minor' => 0,
852  'rc_cur_id' => $target->getArticleID(),
853  'rc_user' => $user->getId(),
854  'rc_user_text' => $user->getName(),
855  'rc_actor' => $user->getActorId(),
856  'rc_comment' => &$logComment,
857  'rc_comment_text' => &$logComment,
858  'rc_comment_data' => null,
859  'rc_this_oldid' => $revId,
860  'rc_last_oldid' => 0,
861  'rc_bot' => $permissionManager->userHasRight( $user, 'bot' ) ?
862  (int)$wgRequest->getBool( 'bot', true ) : 0,
863  'rc_ip' => self::checkIPAddress( $ip ),
864  'rc_patrolled' => $markPatrolled ? self::PRC_AUTOPATROLLED : self::PRC_UNPATROLLED,
865  'rc_new' => 0, # obsolete
866  'rc_old_len' => null,
867  'rc_new_len' => null,
868  'rc_deleted' => 0,
869  'rc_logid' => $newId,
870  'rc_log_type' => $type,
871  'rc_log_action' => $action,
872  'rc_params' => $params
873  ];
874 
875  $rc->mExtra = [
876  'prefixedDBkey' => $title->getPrefixedDBkey(),
877  'lastTimestamp' => 0,
878  'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
879  'pageStatus' => $pageStatus,
880  'actionCommentIRC' => $actionCommentIRC
881  ];
882 
883  return $rc;
884  }
885 
907  public static function newForCategorization(
908  $timestamp,
909  Title $categoryTitle,
910  ?User $user,
911  $comment,
912  Title $pageTitle,
913  $oldRevId,
914  $newRevId,
915  $lastTimestamp,
916  $bot,
917  $ip = '',
918  $deleted = 0,
919  $added = null
920  ) {
921  // Done in a backwards compatible way.
922  $params = [
923  'hidden-cat' => WikiCategoryPage::factory( $categoryTitle )->isHidden()
924  ];
925  if ( $added !== null ) {
926  $params['added'] = $added;
927  }
928 
929  $rc = new RecentChange;
930  $rc->mTitle = $categoryTitle;
931  $rc->mPerformer = $user;
932  $rc->mAttribs = [
933  'rc_timestamp' => $timestamp,
934  'rc_namespace' => $categoryTitle->getNamespace(),
935  'rc_title' => $categoryTitle->getDBkey(),
936  'rc_type' => RC_CATEGORIZE,
937  'rc_source' => self::SRC_CATEGORIZE,
938  'rc_minor' => 0,
939  'rc_cur_id' => $pageTitle->getArticleID(),
940  'rc_user' => $user ? $user->getId() : 0,
941  'rc_user_text' => $user ? $user->getName() : '',
942  'rc_actor' => $user ? $user->getActorId() : null,
943  'rc_comment' => &$comment,
944  'rc_comment_text' => &$comment,
945  'rc_comment_data' => null,
946  'rc_this_oldid' => $newRevId,
947  'rc_last_oldid' => $oldRevId,
948  'rc_bot' => $bot ? 1 : 0,
949  'rc_ip' => self::checkIPAddress( $ip ),
950  'rc_patrolled' => self::PRC_AUTOPATROLLED, // Always patrolled, just like log entries
951  'rc_new' => 0, # obsolete
952  'rc_old_len' => null,
953  'rc_new_len' => null,
954  'rc_deleted' => $deleted,
955  'rc_logid' => 0,
956  'rc_log_type' => null,
957  'rc_log_action' => '',
958  'rc_params' => serialize( $params )
959  ];
960 
961  $rc->mExtra = [
962  'prefixedDBkey' => $categoryTitle->getPrefixedDBkey(),
963  'lastTimestamp' => $lastTimestamp,
964  'oldSize' => 0,
965  'newSize' => 0,
966  'pageStatus' => 'changed'
967  ];
968 
969  return $rc;
970  }
971 
980  public function getParam( $name ) {
981  $params = $this->parseParams();
982  return $params[$name] ?? null;
983  }
984 
990  public function loadFromRow( $row ) {
991  $this->mAttribs = get_object_vars( $row );
992  $this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
993  // rc_deleted MUST be set
994  $this->mAttribs['rc_deleted'] = $row->rc_deleted;
995 
996  if ( isset( $this->mAttribs['rc_ip'] ) ) {
997  // Clean up CIDRs for Postgres per T164898. ("127.0.0.1" casts to "127.0.0.1/32")
998  $n = strpos( $this->mAttribs['rc_ip'], '/' );
999  if ( $n !== false ) {
1000  $this->mAttribs['rc_ip'] = substr( $this->mAttribs['rc_ip'], 0, $n );
1001  }
1002  }
1003 
1004  $comment = CommentStore::getStore()
1005  // Legacy because $row may have come from self::selectFields()
1006  ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $row, true )
1007  ->text;
1008  $this->mAttribs['rc_comment'] = &$comment;
1009  $this->mAttribs['rc_comment_text'] = &$comment;
1010  $this->mAttribs['rc_comment_data'] = null;
1011 
1012  $user = User::newFromAnyId(
1013  $this->mAttribs['rc_user'] ?? null,
1014  $this->mAttribs['rc_user_text'] ?? null,
1015  $this->mAttribs['rc_actor'] ?? null
1016  );
1017  $this->mAttribs['rc_user'] = $user->getId();
1018  $this->mAttribs['rc_user_text'] = $user->getName();
1019  $this->mAttribs['rc_actor'] = $user->getActorId();
1020  }
1021 
1028  public function getAttribute( $name ) {
1029  if ( $name === 'rc_comment' ) {
1030  return CommentStore::getStore()
1031  ->getComment( 'rc_comment', $this->mAttribs, true )->text;
1032  }
1033 
1034  if ( $name === 'rc_user' || $name === 'rc_user_text' || $name === 'rc_actor' ) {
1035  $user = User::newFromAnyId(
1036  $this->mAttribs['rc_user'] ?? null,
1037  $this->mAttribs['rc_user_text'] ?? null,
1038  $this->mAttribs['rc_actor'] ?? null
1039  );
1040  if ( $name === 'rc_user' ) {
1041  return $user->getId();
1042  }
1043  if ( $name === 'rc_user_text' ) {
1044  return $user->getName();
1045  }
1046  if ( $name === 'rc_actor' ) {
1047  return $user->getActorId();
1048  }
1049  }
1050 
1051  return $this->mAttribs[$name] ?? null;
1052  }
1053 
1057  public function getAttributes() {
1058  return $this->mAttribs;
1059  }
1060 
1067  public function diffLinkTrail( $forceCur ) {
1068  if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
1069  $trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
1070  "&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
1071  if ( $forceCur ) {
1072  $trail .= '&diff=0';
1073  } else {
1074  $trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
1075  }
1076  } else {
1077  $trail = '';
1078  }
1079 
1080  return $trail;
1081  }
1082 
1090  public function getCharacterDifference( $old = 0, $new = 0 ) {
1091  if ( $old === 0 ) {
1092  $old = $this->mAttribs['rc_old_len'];
1093  }
1094  if ( $new === 0 ) {
1095  $new = $this->mAttribs['rc_new_len'];
1096  }
1097  if ( $old === null || $new === null ) {
1098  return '';
1099  }
1100 
1101  return ChangesList::showCharacterDifference( $old, $new );
1102  }
1103 
1104  private static function checkIPAddress( $ip ) {
1105  global $wgRequest;
1106  if ( $ip ) {
1107  if ( !IP::isIPAddress( $ip ) ) {
1108  throw new MWException( "Attempt to write \"" . $ip .
1109  "\" as an IP address into recent changes" );
1110  }
1111  } else {
1112  $ip = $wgRequest->getIP();
1113  if ( !$ip ) {
1114  $ip = '';
1115  }
1116  }
1117 
1118  return $ip;
1119  }
1120 
1130  public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
1131  global $wgRCMaxAge;
1132 
1133  return wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $wgRCMaxAge;
1134  }
1135 
1143  public function parseParams() {
1144  $rcParams = $this->getAttribute( 'rc_params' );
1145 
1146  Wikimedia\suppressWarnings();
1147  $unserializedParams = unserialize( $rcParams );
1148  Wikimedia\restoreWarnings();
1149 
1150  return $unserializedParams;
1151  }
1152 
1161  public function addTags( $tags ) {
1162  if ( is_string( $tags ) ) {
1163  $this->tags[] = $tags;
1164  } else {
1165  $this->tags = array_merge( $tags, $this->tags );
1166  }
1167  }
1168 }
static getEngine( $uri, $params=[])
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:142
This module processes the email notifications when the current page is changed.
static isIPAddress( $ip)
Determine if a string is as valid IP address or network (CIDR prefix).
Definition: IP.php:77
const RC_CATEGORIZE
Definition: Defines.php:126
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.
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3161
const SRC_CATEGORIZE
static getChangeTypes()
Get an array of all change types.
serialize()
doMarkPatrolled(User $user, $auto=false, $tags=null)
Mark this RecentChange as patrolled.
static newFromId( $rcid)
Obtain the recent change with a given rc_id value.
parseParams()
Parses and returns the rc_params attribute.
getAttribute( $name)
Get an attribute value.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
static record( $rc, $auto=false, User $user=null, $tags=null)
Record a log event for a change being patrolled.
Definition: PatrolLog.php:42
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:617
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...
$wgLogRestrictions
This restricts log access to those who have a certain right Users without this will not see it in the...
static newFromActorId( $id)
Static factory method for creation from a given actor ID.
Definition: User.php:575
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
const DB_MASTER
Definition: defines.php:26
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...
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2283
$wgRCMaxAge
Recentchanges items are periodically purged; entries older than this many seconds will go...
getPerformer()
Get the User object of the person who performed this change.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
User false $mPerformer
static parseFromRCType( $rcType)
Parsing RC_* constants to human-readable test.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
loadFromRow( $row)
Initialises the members of this object from a mysql row object.
const PRC_PATROLLED
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
static newMigration()
Static constructor.
getDBkey()
Get the main part with underscores.
Definition: Title.php:1014
Interface that defines how to tag objects.
Definition: Taggable.php:32
$wgUseFilePatrol
Use file patrolling to check new files on Special:Newfiles.
static markPatrolled( $change, $auto=false, $tags=null)
Mark a given change as patrolled.
$wgShowUpdatedMarker
Show "Updated (since my last visit)" marker in RC view, watchlist and history view for watched pages ...
save( $send=self::SEND_FEED)
Writes the data in this object to the database.
$wgPutIPinRC
Log IP addresses in the recentchanges table; can be accessed only by extensions (e.g.
setExtra( $extra)
unserialize( $serialized)
static newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='', $revId=0, $isPatrollable=false)
$wgRCEngines
Used by RecentChange::getEngine to find the correct engine for a given URI scheme.
const SRC_EXTERNAL
Title false $mTitle
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1035
array $tags
List of tags to apply.
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
static showCharacterDifference( $old, $new, IContextSource $context=null)
Show formatted char difference.
const PRC_UNPATROLLED
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:584
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...
setAttribs( $attribs)
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:560
$wgUseEnotif
Definition: Setup.php:435
static factory(array $params)
Definition: RCFeed.php:46
reallyMarkPatrolled()
Mark this RecentChange patrolled, without error checking.
static getStore()
getId()
Get the user&#39;s ID.
Definition: User.php:2254
notifyRCFeeds(array $feeds=null)
Notify all the feeds about the change.
static notifyLog( $timestamp, $title, $user, $actionComment, $ip, $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='')
getActorId(IDatabase $dbw=null)
Get the user&#39;s actor ID.
Definition: User.php:2322
getParam( $name)
Get a parameter value.
int $counter
Line number of recent change.
static parseToRCType( $type)
Parsing text to RC_* constants.
getCharacterDifference( $old=0, $new=0)
Returns the change size (HTML).
static singleton( $domain=false)
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new recentchanges object...
$wgRCFeeds
Recentchanges items are periodically purged; entries older than this many seconds will go...
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:729
const RC_EXTERNAL
Definition: Defines.php:125
const RC_NEW
Definition: Defines.php:123
const DB_REPLICA
Definition: defines.php:25
This is to display changes made to all articles linked in an article.
addTags( $tags)
Tags to append to the recent change, and associated revision/log.
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:536
static checkIPAddress( $ip)
const PRC_AUTOPATROLLED
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:256
const RC_EDIT
Definition: Defines.php:122
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
const RC_LOG
Definition: Defines.php:124
getPrefixedDBkey()
Get the prefixed database key form.
Definition: Title.php:1841
static newFromRow( $row)
diffLinkTrail( $forceCur)
Gets the end part of the diff URL associated with this object Blank if no diff link should be display...