MediaWiki  master
RecentChange.php
Go to the documentation of this file.
1 <?php
34 use Wikimedia\Assert\Assert;
35 use Wikimedia\AtEase\AtEase;
36 use Wikimedia\IPUtils;
37 
83 class RecentChange implements Taggable {
85 
86  // Constants for the rc_source field. Extensions may also have
87  // their own source constants.
88  public const SRC_EDIT = 'mw.edit';
89  public const SRC_NEW = 'mw.new';
90  public const SRC_LOG = 'mw.log';
91  public const SRC_EXTERNAL = 'mw.external'; // obsolete
92  public const SRC_CATEGORIZE = 'mw.categorize';
93 
94  public const PRC_UNPATROLLED = 0;
95  public const PRC_PATROLLED = 1;
96  public const PRC_AUTOPATROLLED = 2;
97 
101  public const SEND_NONE = true;
102 
106  public const SEND_FEED = false;
107 
109  public $mAttribs = [];
110  public $mExtra = [];
111 
115  private $mPage = null;
116 
120  private $mPerformer = null;
121 
122  public $numberofWatchingusers = 0; # Dummy to prevent error message in SpecialRecentChangesLinked
124 
129 
133  public $counter = -1;
134 
138  private $tags = [];
139 
143  private $editResult = null;
144 
145  private const CHANGE_TYPES = [
146  'edit' => RC_EDIT,
147  'new' => RC_NEW,
148  'log' => RC_LOG,
149  'external' => RC_EXTERNAL,
150  'categorize' => RC_CATEGORIZE,
151  ];
152 
153  # Factory methods
154 
159  public static function newFromRow( $row ) {
160  $rc = new RecentChange;
161  $rc->loadFromRow( $row );
162 
163  return $rc;
164  }
165 
173  public static function parseToRCType( $type ) {
174  if ( is_array( $type ) ) {
175  $retval = [];
176  foreach ( $type as $t ) {
177  $retval[] = self::parseToRCType( $t );
178  }
179 
180  return $retval;
181  }
182 
183  if ( !array_key_exists( $type, self::CHANGE_TYPES ) ) {
184  throw new MWException( "Unknown type '$type'" );
185  }
186  return self::CHANGE_TYPES[$type];
187  }
188 
195  public static function parseFromRCType( $rcType ) {
196  return array_search( $rcType, self::CHANGE_TYPES, true ) ?: "$rcType";
197  }
198 
206  public static function getChangeTypes() {
207  return array_keys( self::CHANGE_TYPES );
208  }
209 
216  public static function newFromId( $rcid ) {
217  return self::newFromConds( [ 'rc_id' => $rcid ], __METHOD__ );
218  }
219 
229  public static function newFromConds(
230  $conds,
231  $fname = __METHOD__,
232  $dbType = DB_REPLICA
233  ) {
234  $db = wfGetDB( $dbType );
235  $rcQuery = self::getQueryInfo();
236  $row = $db->selectRow(
237  $rcQuery['tables'], $rcQuery['fields'], $conds, $fname, [], $rcQuery['joins']
238  );
239  if ( $row !== false ) {
240  return self::newFromRow( $row );
241  } else {
242  return null;
243  }
244  }
245 
261  public static function getQueryInfo() {
262  $commentQuery = CommentStore::getStore()->getJoin( 'rc_comment' );
263  // Optimizer sometimes refuses to pick up the correct join order (T311360)
264  $commentQuery['joins']['comment_rc_comment'][0] = 'STRAIGHT_JOIN';
265  return [
266  'tables' => [
267  'recentchanges',
268  'recentchanges_actor' => 'actor'
269  ] + $commentQuery['tables'],
270  'fields' => [
271  'rc_id',
272  'rc_timestamp',
273  'rc_namespace',
274  'rc_title',
275  'rc_minor',
276  'rc_bot',
277  'rc_new',
278  'rc_cur_id',
279  'rc_this_oldid',
280  'rc_last_oldid',
281  'rc_type',
282  'rc_source',
283  'rc_patrolled',
284  'rc_ip',
285  'rc_old_len',
286  'rc_new_len',
287  'rc_deleted',
288  'rc_logid',
289  'rc_log_type',
290  'rc_log_action',
291  'rc_params',
292  'rc_actor',
293  'rc_user' => 'recentchanges_actor.actor_user',
294  'rc_user_text' => 'recentchanges_actor.actor_name',
295  ] + $commentQuery['fields'],
296  'joins' => [
297  'recentchanges_actor' => [ 'STRAIGHT_JOIN', 'actor_id=rc_actor' ]
298  ] + $commentQuery['joins'],
299  ];
300  }
301 
302  public function __construct() {
304  'mTitle',
305  '1.37',
306  function () {
307  return Title::castFromPageReference( $this->mPage );
308  },
309  function ( ?Title $title ) {
310  $this->mPage = $title;
311  }
312  );
313  }
314 
315  # Accessors
316 
320  public function setAttribs( $attribs ) {
321  $this->mAttribs = $attribs;
322  }
323 
327  public function setExtra( $extra ) {
328  $this->mExtra = $extra;
329  }
330 
335  public function getTitle() {
336  $this->mPage = Title::castFromPageReference( $this->getPage() );
337  return $this->mPage ?: Title::makeTitle( NS_SPECIAL, 'BadTitle' );
338  }
339 
344  public function getPage(): ?PageReference {
345  if ( !$this->mPage ) {
346  // NOTE: As per the 1.36 release, we always provide rc_title,
347  // even in cases where it doesn't really make sense.
348  // In the future, rc_title may be nullable, or we may use
349  // empty strings in entries that do not refer to a page.
350  if ( ( $this->mAttribs['rc_title'] ?? '' ) === '' ) {
351  return null;
352  }
353 
354  // XXX: We could use rc_cur_id to create a PageIdentityValue,
355  // at least if it's not a special page.
356  // However, newForCategorization() puts the ID of the categorized page into
357  // rc_cur_id, but the title of the category page into rc_title.
358  $this->mPage = new PageReferenceValue(
359  (int)$this->mAttribs['rc_namespace'],
360  $this->mAttribs['rc_title'],
361  PageReference::LOCAL
362  );
363  }
364 
365  return $this->mPage;
366  }
367 
374  public function getPerformer(): User {
375  wfDeprecated( __METHOD__, '1.36' );
376  if ( !$this->mPerformer instanceof User ) {
377  $this->mPerformer = User::newFromIdentity( $this->getPerformerIdentity() );
378  }
379 
380  return $this->mPerformer;
381  }
382 
390  public function getPerformerIdentity(): UserIdentity {
391  if ( !$this->mPerformer ) {
392  $this->mPerformer = $this->getUserIdentityFromAnyId(
393  $this->mAttribs['rc_user'] ?? null,
394  $this->mAttribs['rc_user_text'] ?? null,
395  $this->mAttribs['rc_actor'] ?? null
396  );
397  }
398 
399  return $this->mPerformer;
400  }
401 
411  public function save( $send = self::SEND_FEED ) {
412  $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
413  $putIPinRC = $mainConfig->get( MainConfigNames::PutIPinRC );
414  $dbw = wfGetDB( DB_PRIMARY );
415  if ( !is_array( $this->mExtra ) ) {
416  $this->mExtra = [];
417  }
418 
419  if ( !$putIPinRC ) {
420  $this->mAttribs['rc_ip'] = '';
421  }
422 
423  # Strict mode fixups (not-NULL fields)
424  foreach ( [ 'minor', 'bot', 'new', 'patrolled', 'deleted' ] as $field ) {
425  $this->mAttribs["rc_$field"] = (int)$this->mAttribs["rc_$field"];
426  }
427  # ...more fixups (NULL fields)
428  foreach ( [ 'old_len', 'new_len' ] as $field ) {
429  $this->mAttribs["rc_$field"] = isset( $this->mAttribs["rc_$field"] )
430  ? (int)$this->mAttribs["rc_$field"]
431  : null;
432  }
433 
434  $row = $this->mAttribs;
435 
436  # Trim spaces on user supplied text
437  $row['rc_comment'] = trim( $row['rc_comment'] ?? '' );
438 
439  # Fixup database timestamps
440  $row['rc_timestamp'] = $dbw->timestamp( $row['rc_timestamp'] );
441 
442  # # If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
443  if ( $row['rc_cur_id'] == 0 ) {
444  unset( $row['rc_cur_id'] );
445  }
446 
447  # Convert mAttribs['rc_comment'] for CommentStore
448  $comment = $row['rc_comment'];
449  unset( $row['rc_comment'], $row['rc_comment_text'], $row['rc_comment_data'] );
450  $row += CommentStore::getStore()->insert( $dbw, 'rc_comment', $comment );
451 
452  # Normalize UserIdentity to actor ID
453  $user = $this->getPerformerIdentity();
454  $actorStore = MediaWikiServices::getInstance()->getActorStore();
455  $row['rc_actor'] = $actorStore->acquireActorId( $user, $dbw );
456  unset( $row['rc_user'], $row['rc_user_text'] );
457 
458  # Don't reuse an existing rc_id for the new row, if one happens to be
459  # set for some reason.
460  unset( $row['rc_id'] );
461 
462  # Insert new row
463  $dbw->insert( 'recentchanges', $row, __METHOD__ );
464 
465  # Set the ID
466  $this->mAttribs['rc_id'] = $dbw->insertId();
467 
468  # Notify extensions
469  Hooks::runner()->onRecentChange_save( $this );
470 
471  // Apply revert tags (if needed)
472  if ( $this->editResult !== null && count( $this->editResult->getRevertTags() ) ) {
474  $this->editResult->getRevertTags(),
475  $this->mAttribs['rc_id'],
476  $this->mAttribs['rc_this_oldid'],
477  $this->mAttribs['rc_logid'],
478  FormatJson::encode( $this->editResult ),
479  $this
480  );
481  }
482 
483  if ( count( $this->tags ) ) {
484  // $this->tags may contain revert tags we already applied above, they will
485  // just be ignored.
487  $this->tags,
488  $this->mAttribs['rc_id'],
489  $this->mAttribs['rc_this_oldid'],
490  $this->mAttribs['rc_logid'],
491  null,
492  $this
493  );
494  }
495 
496  if ( $send === self::SEND_FEED ) {
497  // Emit the change to external applications via RCFeeds.
498  $this->notifyRCFeeds();
499  }
500 
501  # E-mail notifications
502  if ( $mainConfig->get( MainConfigNames::EnotifUserTalk ) ||
503  $mainConfig->get( MainConfigNames::EnotifWatchlist ) ||
504  $mainConfig->get( MainConfigNames::ShowUpdatedMarker )
505  ) {
506  $userFactory = MediaWikiServices::getInstance()->getUserFactory();
507  $editor = $userFactory->newFromUserIdentity( $this->getPerformerIdentity() );
508  $page = $this->getPage();
510 
511  // Never send an RC notification email about categorization changes
512  if (
513  $title &&
514  Hooks::runner()->onAbortEmailNotification( $editor, $title, $this ) &&
515  $this->mAttribs['rc_type'] != RC_CATEGORIZE
516  ) {
517  // @FIXME: This would be better as an extension hook
518  // Send emails or email jobs once this row is safely committed
519  $dbw->onTransactionCommitOrIdle(
520  function () use ( $editor, $title ) {
521  $enotif = new EmailNotification();
522  $enotif->notifyOnPageChange(
523  $editor,
524  $title,
525  $this->mAttribs['rc_timestamp'],
526  $this->mAttribs['rc_comment'],
527  $this->mAttribs['rc_minor'],
528  $this->mAttribs['rc_last_oldid'],
529  $this->mExtra['pageStatus']
530  );
531  },
532  __METHOD__
533  );
534  }
535  }
536 
537  $jobs = [];
538  // Flush old entries from the `recentchanges` table
539  if ( mt_rand( 0, 9 ) == 0 ) {
541  }
542  // Update the cached list of active users
543  if ( $this->mAttribs['rc_user'] > 0 ) {
545  }
546  MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $jobs );
547  }
548 
553  public function notifyRCFeeds( array $feeds = null ) {
554  $rcFeeds =
555  MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RCFeeds );
556  if ( $feeds === null ) {
557  $feeds = $rcFeeds;
558  }
559 
560  $performer = $this->getPerformerIdentity();
561 
562  foreach ( $feeds as $params ) {
563  $params += [
564  'omit_bots' => false,
565  'omit_anon' => false,
566  'omit_user' => false,
567  'omit_minor' => false,
568  'omit_patrolled' => false,
569  ];
570 
571  if (
572  ( $params['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
573  ( $params['omit_anon'] && !$performer->isRegistered() ) ||
574  ( $params['omit_user'] && $performer->isRegistered() ) ||
575  ( $params['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
576  ( $params['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
577  $this->mAttribs['rc_type'] == RC_EXTERNAL
578  ) {
579  continue;
580  }
581 
582  $actionComment = $this->mExtra['actionCommentIRC'] ?? null;
583 
584  $feed = RCFeed::factory( $params );
585  $feed->notify( $this, $actionComment );
586  }
587  }
588 
598  public static function getEngine( $uri, $params = [] ) {
599  wfDeprecated( __METHOD__, '1.29' );
600  $rcEngines =
601  MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RCEngines );
602  $scheme = parse_url( $uri, PHP_URL_SCHEME );
603  if ( !$scheme ) {
604  throw new MWException( "Invalid RCFeed uri: '$uri'" );
605  }
606  if ( !isset( $rcEngines[$scheme] ) ) {
607  throw new MWException( "Unknown RCFeed engine: '$scheme'" );
608  }
609  if ( defined( 'MW_PHPUNIT_TEST' ) && is_object( $rcEngines[$scheme] ) ) {
610  return $rcEngines[$scheme];
611  }
612  return new $rcEngines[$scheme]( $params );
613  }
614 
626  public function doMarkPatrolled( Authority $performer, $auto = false, $tags = null ) {
627  $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
628  $useRCPatrol = $mainConfig->get( MainConfigNames::UseRCPatrol );
629  $useNPPatrol = $mainConfig->get( MainConfigNames::UseNPPatrol );
630  $useFilePatrol = $mainConfig->get( MainConfigNames::UseFilePatrol );
631  // Fix up $tags so that the MarkPatrolled hook below always gets an array
632  if ( $tags === null ) {
633  $tags = [];
634  } elseif ( is_string( $tags ) ) {
635  $tags = [ $tags ];
636  }
637 
638  $status = PermissionStatus::newEmpty();
639  // If recentchanges patrol is disabled, only new pages or new file versions
640  // can be patrolled, provided the appropriate config variable is set
641  if ( !$useRCPatrol && ( !$useNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) &&
642  ( !$useFilePatrol || !( $this->getAttribute( 'rc_type' ) == RC_LOG &&
643  $this->getAttribute( 'rc_log_type' ) == 'upload' ) ) ) {
644  $status->fatal( 'rcpatroldisabled' );
645  }
646  // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
647  $performer->authorizeWrite( $auto ? 'autopatrol' : 'patrol', $this->getTitle(), $status );
648  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
649  if ( !Hooks::runner()->onMarkPatrolled(
650  $this->getAttribute( 'rc_id' ), $user, false, $auto, $tags )
651  ) {
652  $status->fatal( 'hookaborted' );
653  }
654  // Users without the 'autopatrol' right can't patrol their own revisions
655  if ( $performer->getUser()->getName() === $this->getAttribute( 'rc_user_text' ) &&
656  !$performer->isAllowed( 'autopatrol' )
657  ) {
658  $status->fatal( 'markedaspatrollederror-noautopatrol' );
659  }
660  if ( !$status->isGood() ) {
661  return $status->toLegacyErrorArray();
662  }
663  // If the change was patrolled already, do nothing
664  if ( $this->getAttribute( 'rc_patrolled' ) ) {
665  return [];
666  }
667  // Actually set the 'patrolled' flag in RC
668  $this->reallyMarkPatrolled();
669  // Log this patrol event
670  PatrolLog::record( $this, $auto, $performer->getUser(), $tags );
671 
672  Hooks::runner()->onMarkPatrolledComplete(
673  $this->getAttribute( 'rc_id' ), $user, false, $auto );
674 
675  return [];
676  }
677 
682  public function reallyMarkPatrolled() {
683  $dbw = wfGetDB( DB_PRIMARY );
684  $dbw->update(
685  'recentchanges',
686  [
687  'rc_patrolled' => self::PRC_PATROLLED
688  ],
689  [
690  'rc_id' => $this->getAttribute( 'rc_id' )
691  ],
692  __METHOD__
693  );
694  // Invalidate the page cache after the page has been patrolled
695  // to make sure that the Patrol link isn't visible any longer!
696  $this->getTitle()->invalidateCache();
697 
698  // Enqueue a reverted tag update (in case the edit was a revert)
699  $revisionId = $this->getAttribute( 'rc_this_oldid' );
700  if ( $revisionId ) {
701  $revertedTagUpdateManager =
702  MediaWikiServices::getInstance()->getRevertedTagUpdateManager();
703  $revertedTagUpdateManager->approveRevertedTagForRevision( $revisionId );
704  }
705 
706  return $dbw->affectedRows();
707  }
708 
733  public static function notifyEdit(
734  $timestamp, $page, $minor, $user, $comment, $oldId, $lastTimestamp,
735  $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0,
736  $tags = [], EditResult $editResult = null
737  ) {
738  Assert::parameter( $page->exists(), '$page', 'must represent an existing page' );
739 
740  $rc = new RecentChange;
741  $rc->mPage = $page;
742  $rc->mPerformer = $user;
743  $rc->mAttribs = [
744  'rc_timestamp' => $timestamp,
745  'rc_namespace' => $page->getNamespace(),
746  'rc_title' => $page->getDBkey(),
747  'rc_type' => RC_EDIT,
748  'rc_source' => self::SRC_EDIT,
749  'rc_minor' => $minor ? 1 : 0,
750  'rc_cur_id' => $page->getId(),
751  'rc_user' => $user->getId(),
752  'rc_user_text' => $user->getName(),
753  'rc_comment' => &$comment,
754  'rc_comment_text' => &$comment,
755  'rc_comment_data' => null,
756  'rc_this_oldid' => (int)$newId,
757  'rc_last_oldid' => $oldId,
758  'rc_bot' => $bot ? 1 : 0,
759  'rc_ip' => self::checkIPAddress( $ip ),
760  'rc_patrolled' => intval( $patrol ),
761  'rc_new' => 0, # obsolete
762  'rc_old_len' => $oldSize,
763  'rc_new_len' => $newSize,
764  'rc_deleted' => 0,
765  'rc_logid' => 0,
766  'rc_log_type' => null,
767  'rc_log_action' => '',
768  'rc_params' => ''
769  ];
770 
771  // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
772  $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
773 
774  $rc->mExtra = [
775  'prefixedDBkey' => $formatter->getPrefixedDBkey( $page ),
776  'lastTimestamp' => $lastTimestamp,
777  'oldSize' => $oldSize,
778  'newSize' => $newSize,
779  'pageStatus' => 'changed'
780  ];
781 
783  static function () use ( $rc, $tags, $editResult ) {
784  $rc->addTags( $tags );
785  $rc->setEditResult( $editResult );
786  $rc->save();
787  },
788  DeferredUpdates::POSTSEND,
790  );
791 
792  return $rc;
793  }
794 
814  public static function notifyNew(
815  $timestamp,
816  $page, $minor, $user, $comment, $bot,
817  $ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = []
818  ) {
819  Assert::parameter( $page->exists(), '$page', 'must represent an existing page' );
820 
821  $rc = new RecentChange;
822  $rc->mPage = $page;
823  $rc->mPerformer = $user;
824  $rc->mAttribs = [
825  'rc_timestamp' => $timestamp,
826  'rc_namespace' => $page->getNamespace(),
827  'rc_title' => $page->getDBkey(),
828  'rc_type' => RC_NEW,
829  'rc_source' => self::SRC_NEW,
830  'rc_minor' => $minor ? 1 : 0,
831  'rc_cur_id' => $page->getId(),
832  'rc_user' => $user->getId(),
833  'rc_user_text' => $user->getName(),
834  'rc_comment' => &$comment,
835  'rc_comment_text' => &$comment,
836  'rc_comment_data' => null,
837  'rc_this_oldid' => (int)$newId,
838  'rc_last_oldid' => 0,
839  'rc_bot' => $bot ? 1 : 0,
840  'rc_ip' => self::checkIPAddress( $ip ),
841  'rc_patrolled' => intval( $patrol ),
842  'rc_new' => 1, # obsolete
843  'rc_old_len' => 0,
844  'rc_new_len' => $size,
845  'rc_deleted' => 0,
846  'rc_logid' => 0,
847  'rc_log_type' => null,
848  'rc_log_action' => '',
849  'rc_params' => ''
850  ];
851 
852  // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
853  $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
854 
855  $rc->mExtra = [
856  'prefixedDBkey' => $formatter->getPrefixedDBkey( $page ),
857  'lastTimestamp' => 0,
858  'oldSize' => 0,
859  'newSize' => $size,
860  'pageStatus' => 'created'
861  ];
862 
864  static function () use ( $rc, $tags ) {
865  $rc->addTags( $tags );
866  $rc->save();
867  },
868  DeferredUpdates::POSTSEND,
870  );
871 
872  return $rc;
873  }
874 
891  public static function notifyLog( $timestamp,
892  $logPage, $user, $actionComment, $ip, $type,
893  $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
894  ) {
895  $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()
896  ->get( MainConfigNames::LogRestrictions );
897 
898  # Don't add private logs to RC!
899  if ( isset( $logRestrictions[$type] ) && $logRestrictions[$type] != '*' ) {
900  return false;
901  }
902  $rc = self::newLogEntry( $timestamp,
903  $logPage, $user, $actionComment, $ip, $type, $action,
904  $target, $logComment, $params, $newId, $actionCommentIRC );
905  $rc->save();
906 
907  return true;
908  }
909 
928  public static function newLogEntry( $timestamp,
929  $logPage, $user, $actionComment, $ip,
930  $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '',
931  $revId = 0, $isPatrollable = false ) {
932  global $wgRequest;
933  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
934 
935  # # Get pageStatus for email notification
936  switch ( $type . '-' . $action ) {
937  case 'delete-delete':
938  case 'delete-delete_redir':
939  case 'delete-delete_redir2':
940  $pageStatus = 'deleted';
941  break;
942  case 'move-move':
943  case 'move-move_redir':
944  $pageStatus = 'moved';
945  break;
946  case 'delete-restore':
947  $pageStatus = 'restored';
948  break;
949  case 'upload-upload':
950  $pageStatus = 'created';
951  break;
952  case 'upload-overwrite':
953  default:
954  $pageStatus = 'changed';
955  break;
956  }
957 
958  // Allow unpatrolled status for patrollable log entries
959  $canAutopatrol = $permissionManager->userHasRight( $user, 'autopatrol' );
960  $markPatrolled = $isPatrollable ? $canAutopatrol : true;
961 
962  if ( $target instanceof PageIdentity && $target->canExist() ) {
963  $pageId = $target->getId();
964  } else {
965  $pageId = 0;
966  }
967 
968  $rc = new RecentChange;
969  $rc->mPage = $target;
970  $rc->mPerformer = $user;
971  $rc->mAttribs = [
972  'rc_timestamp' => $timestamp,
973  'rc_namespace' => $target->getNamespace(),
974  'rc_title' => $target->getDBkey(),
975  'rc_type' => RC_LOG,
976  'rc_source' => self::SRC_LOG,
977  'rc_minor' => 0,
978  'rc_cur_id' => $pageId,
979  'rc_user' => $user->getId(),
980  'rc_user_text' => $user->getName(),
981  'rc_comment' => &$logComment,
982  'rc_comment_text' => &$logComment,
983  'rc_comment_data' => null,
984  'rc_this_oldid' => (int)$revId,
985  'rc_last_oldid' => 0,
986  'rc_bot' => $permissionManager->userHasRight( $user, 'bot' ) ?
987  (int)$wgRequest->getBool( 'bot', true ) : 0,
988  'rc_ip' => self::checkIPAddress( $ip ),
989  'rc_patrolled' => $markPatrolled ? self::PRC_AUTOPATROLLED : self::PRC_UNPATROLLED,
990  'rc_new' => 0, # obsolete
991  'rc_old_len' => null,
992  'rc_new_len' => null,
993  'rc_deleted' => 0,
994  'rc_logid' => $newId,
995  'rc_log_type' => $type,
996  'rc_log_action' => $action,
997  'rc_params' => $params
998  ];
999 
1000  // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
1001  $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
1002 
1003  $rc->mExtra = [
1004  // XXX: This does not correspond to rc_namespace/rc_title/rc_cur_id.
1005  // Is that intentional? For all other kinds of RC entries, prefixedDBkey
1006  // matches rc_namespace/rc_title. Do we even need $logPage?
1007  'prefixedDBkey' => $formatter->getPrefixedDBkey( $logPage ),
1008  'lastTimestamp' => 0,
1009  'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
1010  'pageStatus' => $pageStatus,
1011  'actionCommentIRC' => $actionCommentIRC
1012  ];
1013 
1014  return $rc;
1015  }
1016 
1038  public static function newForCategorization(
1039  $timestamp,
1040  PageIdentity $categoryTitle,
1041  ?UserIdentity $user,
1042  $comment,
1043  PageIdentity $pageTitle,
1044  $oldRevId,
1045  $newRevId,
1046  $lastTimestamp,
1047  $bot,
1048  $ip = '',
1049  $deleted = 0,
1050  $added = null
1051  ) {
1052  // Done in a backwards compatible way.
1053  $categoryWikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()
1054  ->newFromTitle( $categoryTitle );
1055 
1056  '@phan-var WikiCategoryPage $categoryWikiPage';
1057  $params = [
1058  'hidden-cat' => $categoryWikiPage->isHidden()
1059  ];
1060  if ( $added !== null ) {
1061  $params['added'] = $added;
1062  }
1063 
1064  if ( !$user ) {
1065  // XXX: when and why do we need this?
1066  $user = MediaWikiServices::getInstance()->getActorStore()->getUnknownActor();
1067  }
1068 
1069  $rc = new RecentChange;
1070  $rc->mPage = $categoryTitle;
1071  $rc->mPerformer = $user;
1072  $rc->mAttribs = [
1073  'rc_timestamp' => MWTimestamp::convert( TS_MW, $timestamp ),
1074  'rc_namespace' => $categoryTitle->getNamespace(),
1075  'rc_title' => $categoryTitle->getDBkey(),
1076  'rc_type' => RC_CATEGORIZE,
1077  'rc_source' => self::SRC_CATEGORIZE,
1078  'rc_minor' => 0,
1079  // XXX: rc_cur_id does not correspond to rc_namespace/rc_title.
1080  // They refer to different pages. Is that intentional?
1081  'rc_cur_id' => $pageTitle->getId(),
1082  'rc_user' => $user->getId(),
1083  'rc_user_text' => $user->getName(),
1084  'rc_comment' => &$comment,
1085  'rc_comment_text' => &$comment,
1086  'rc_comment_data' => null,
1087  'rc_this_oldid' => (int)$newRevId,
1088  'rc_last_oldid' => $oldRevId,
1089  'rc_bot' => $bot ? 1 : 0,
1090  'rc_ip' => self::checkIPAddress( $ip ),
1091  'rc_patrolled' => self::PRC_AUTOPATROLLED, // Always patrolled, just like log entries
1092  'rc_new' => 0, # obsolete
1093  'rc_old_len' => null,
1094  'rc_new_len' => null,
1095  'rc_deleted' => $deleted,
1096  'rc_logid' => 0,
1097  'rc_log_type' => null,
1098  'rc_log_action' => '',
1099  'rc_params' => serialize( $params )
1100  ];
1101 
1102  // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
1103  $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
1104 
1105  $rc->mExtra = [
1106  'prefixedDBkey' => $formatter->getPrefixedDBkey( $categoryTitle ),
1107  'lastTimestamp' => $lastTimestamp,
1108  'oldSize' => 0,
1109  'newSize' => 0,
1110  'pageStatus' => 'changed'
1111  ];
1112 
1113  return $rc;
1114  }
1115 
1124  public function getParam( $name ) {
1125  $params = $this->parseParams();
1126  return $params[$name] ?? null;
1127  }
1128 
1134  public function loadFromRow( $row ) {
1135  $this->mAttribs = get_object_vars( $row );
1136  $this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
1137  // rc_deleted MUST be set
1138  $this->mAttribs['rc_deleted'] = $row->rc_deleted;
1139 
1140  $comment = CommentStore::getStore()
1141  // Legacy because $row may have come from self::selectFields()
1142  ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $row, true )
1143  ->text;
1144  $this->mAttribs['rc_comment'] = &$comment;
1145  $this->mAttribs['rc_comment_text'] = &$comment;
1146  $this->mAttribs['rc_comment_data'] = null;
1147 
1148  $this->mPerformer = $this->getUserIdentityFromAnyId(
1149  $row->rc_user ?? null,
1150  $row->rc_user_text ?? null,
1151  $row->rc_actor ?? null
1152  );
1153  $this->mAttribs['rc_user'] = $this->mPerformer->getId();
1154  $this->mAttribs['rc_user_text'] = $this->mPerformer->getName();
1155 
1156  // Watchlist expiry.
1157  if ( isset( $row->we_expiry ) && $row->we_expiry ) {
1158  $this->watchlistExpiry = wfTimestamp( TS_MW, $row->we_expiry );
1159  }
1160  }
1161 
1168  public function getAttribute( $name ) {
1169  if ( $name === 'rc_comment' ) {
1170  return CommentStore::getStore()
1171  ->getComment( 'rc_comment', $this->mAttribs, true )->text;
1172  }
1173 
1174  if ( $name === 'rc_user' || $name === 'rc_user_text' || $name === 'rc_actor' ) {
1175  $user = $this->getPerformerIdentity();
1176 
1177  if ( $name === 'rc_user' ) {
1178  return $user->getId();
1179  }
1180  if ( $name === 'rc_user_text' ) {
1181  return $user->getName();
1182  }
1183  if ( $name === 'rc_actor' ) {
1184  // NOTE: rc_actor exists in the database, but application logic should not use it.
1185  wfDeprecatedMsg( 'Accessing deprecated field rc_actor', '1.36' );
1186  $actorStore = MediaWikiServices::getInstance()->getActorStore();
1187  $db = wfGetDB( DB_REPLICA );
1188  return $actorStore->findActorId( $user, $db );
1189  }
1190  }
1191 
1192  return $this->mAttribs[$name] ?? null;
1193  }
1194 
1198  public function getAttributes() {
1199  return $this->mAttribs;
1200  }
1201 
1208  public function diffLinkTrail( $forceCur ) {
1209  if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
1210  $trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
1211  "&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
1212  if ( $forceCur ) {
1213  $trail .= '&diff=0';
1214  } else {
1215  $trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
1216  }
1217  } else {
1218  $trail = '';
1219  }
1220 
1221  return $trail;
1222  }
1223 
1231  public function getCharacterDifference( $old = 0, $new = 0 ) {
1232  if ( $old === 0 ) {
1233  $old = $this->mAttribs['rc_old_len'];
1234  }
1235  if ( $new === 0 ) {
1236  $new = $this->mAttribs['rc_new_len'];
1237  }
1238  if ( $old === null || $new === null ) {
1239  return '';
1240  }
1241 
1242  return ChangesList::showCharacterDifference( $old, $new );
1243  }
1244 
1245  private static function checkIPAddress( $ip ) {
1246  global $wgRequest;
1247  if ( $ip ) {
1248  if ( !IPUtils::isIPAddress( $ip ) ) {
1249  throw new MWException( "Attempt to write \"" . $ip .
1250  "\" as an IP address into recent changes" );
1251  }
1252  } else {
1253  $ip = $wgRequest->getIP();
1254  if ( !$ip ) {
1255  $ip = '';
1256  }
1257  }
1258 
1259  return $ip;
1260  }
1261 
1271  public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
1272  $rcMaxAge =
1273  MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RCMaxAge );
1274 
1275  return (int)wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $rcMaxAge;
1276  }
1277 
1285  public function parseParams() {
1286  $rcParams = $this->getAttribute( 'rc_params' );
1287 
1288  AtEase::suppressWarnings();
1289  $unserializedParams = unserialize( $rcParams );
1290  AtEase::restoreWarnings();
1291 
1292  return $unserializedParams;
1293  }
1294 
1303  public function addTags( $tags ) {
1304  if ( is_string( $tags ) ) {
1305  $this->tags[] = $tags;
1306  } else {
1307  $this->tags = array_merge( $tags, $this->tags );
1308  }
1309  }
1310 
1318  public function setEditResult( ?EditResult $editResult ) {
1319  $this->editResult = $editResult;
1320  }
1321 
1329  private function getUserIdentityFromAnyId(
1330  $userId,
1331  $userName,
1332  $actorId = null
1333  ): UserIdentity {
1334  // XXX: Is this logic needed elsewhere? Should it be reusable?
1335 
1336  $userId = isset( $userId ) ? (int)$userId : null;
1337  $actorId = isset( $actorId ) ? (int)$actorId : 0;
1338 
1339  $actorStore = MediaWikiServices::getInstance()->getActorStore();
1340  if ( $userName && $actorId ) {
1341  // Likely the fields are coming from a join on actor table,
1342  // so can definitely build a UserIdentityValue.
1343  return $actorStore->newActorFromRowFields( $userId, $userName, $actorId );
1344  }
1345  if ( $userId !== null ) {
1346  if ( $userName !== null ) {
1347  // NOTE: For IPs and external users, $userId will be 0.
1348  $user = new UserIdentityValue( $userId, $userName );
1349  } else {
1350  $user = $actorStore->getUserIdentityByUserId( $userId );
1351 
1352  if ( !$user ) {
1353  throw new RuntimeException( "User not found by ID: $userId" );
1354  }
1355  }
1356  } elseif ( $actorId > 0 ) {
1357  $db = wfGetDB( DB_REPLICA );
1358  $user = $actorStore->getActorById( $actorId, $db );
1359 
1360  if ( !$user ) {
1361  throw new RuntimeException( "User not found by actor ID: $actorId" );
1362  }
1363  } elseif ( $userName !== null ) {
1364  $user = $actorStore->getUserIdentityByName( $userName );
1365 
1366  if ( !$user ) {
1367  throw new RuntimeException( "User not found by name: $userName" );
1368  }
1369  } else {
1370  throw new RuntimeException( 'At least one of user ID, actor ID or user name must be given' );
1371  }
1372 
1373  return $user;
1374  }
1375 }
serialize()
unserialize( $serialized)
const RC_NEW
Definition: Defines.php:117
const NS_SPECIAL
Definition: Defines.php:53
const RC_LOG
Definition: Defines.php:118
const RC_EXTERNAL
Definition: Defines.php:119
const RC_EDIT
Definition: Defines.php:116
const RC_CATEGORIZE
Definition: Defines.php:120
deprecatePublicPropertyFallback(string $property, string $version, $getter, $setter=null, $class=null, $component=null)
Mark a removed public property as deprecated and provide fallback getter and setter callables.
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated or implementati...
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
global $wgRequest
Definition: Setup.php:377
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition: WebStart.php:82
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:328
static showCharacterDifference( $old, $new, IContextSource $context=null)
Show formatted char difference.
static getStore()
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
This module processes the email notifications when the current page is changed.
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:96
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
MediaWiki exception.
Definition: MWException.php:29
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Immutable value object representing a page reference.
A StatusValue for permission errors.
Object for storing information about the effects of an edit.
Definition: EditResult.php:38
Value object representing a user's identity.
static record( $rc, $auto, UserIdentity $user, $tags=null)
Record a log event for a change being patrolled.
Definition: PatrolLog.php:44
static factory(array $params)
Definition: RCFeed.php:45
Utility class for creating new RC entries.
setExtra( $extra)
const PRC_UNPATROLLED
static getEngine( $uri, $params=[])
static parseToRCType( $type)
Parsing text to RC_* constants.
setEditResult(?EditResult $editResult)
Sets the EditResult associated with the edit.
static notifyEdit( $timestamp, $page, $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.
reallyMarkPatrolled()
Mark this RecentChange patrolled, without error checking.
static newForCategorization( $timestamp, PageIdentity $categoryTitle, ?UserIdentity $user, $comment, PageIdentity $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...
static newFromRow( $row)
doMarkPatrolled(Authority $performer, $auto=false, $tags=null)
Mark this RecentChange as patrolled.
parseParams()
Parses and returns the rc_params attribute.
const SRC_CATEGORIZE
getPerformer()
Get the User object of the person who performed this change.
static notifyNew( $timestamp, $page, $minor, $user, $comment, $bot, $ip='', $size=0, $newId=0, $patrol=0, $tags=[])
Makes an entry in the database corresponding to page creation.
static getChangeTypes()
Get an array of all change types.
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...
int $counter
Line number of recent change.
save( $send=self::SEND_FEED)
Writes the data in this object to the database.
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new recentchanges object.
setAttribs( $attribs)
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
getCharacterDifference( $old=0, $new=0)
Returns the change size (HTML).
static parseFromRCType( $rcType)
Parsing RC_* constants to human-readable test.
const PRC_PATROLLED
const SRC_EXTERNAL
notifyRCFeeds(array $feeds=null)
Notify all the feeds about the change.
getPerformerIdentity()
Get the UserIdentity of the client that performed this change.
getParam( $name)
Get a parameter value.
static newLogEntry( $timestamp, $logPage, $user, $actionComment, $ip, $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='', $revId=0, $isPatrollable=false)
addTags( $tags)
Tags to append to the recent change, and associated revision/log.
loadFromRow( $row)
Initialises the members of this object from a mysql row object.
static notifyLog( $timestamp, $logPage, $user, $actionComment, $ip, $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='')
getAttribute( $name)
Get an attribute value.
string null $watchlistExpiry
The expiry time, if this is a temporary watchlist item.
diffLinkTrail( $forceCur)
Gets the end part of the diff URL associated with this object Blank if no diff link should be display...
static newFromId( $rcid)
Obtain the recent change with a given rc_id value.
const PRC_AUTOPATROLLED
This is to display changes made to all articles linked in an article.
Represents a title within MediaWiki.
Definition: Title.php:49
static castFromPageReference(?PageReference $pageReference)
Return a Title for a given Reference.
Definition: Title.php:332
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:638
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:70
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:675
Interface that defines how to tag objects.
Definition: Taggable.php:32
Interface for objects (potentially) representing an editable wiki page.
getId( $wikiId=self::LOCAL)
Returns the page ID.
canExist()
Checks whether this PageIdentity represents a "proper" page, meaning that it could exist as an editab...
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
getDBkey()
Get the page title in DB key form.
getNamespace()
Returns the page's namespace number.
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
authorizeWrite(string $action, PageIdentity $target, PermissionStatus $status=null)
Authorize write access.
getUser()
Returns the performer of the actions associated with this authority.
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)
const DB_REPLICA
Definition: defines.php:26
const DB_PRIMARY
Definition: defines.php:28