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 
260  public static function getQueryInfo() {
261  $commentQuery = CommentStore::getStore()->getJoin( 'rc_comment' );
262  return [
263  'tables' => [
264  'recentchanges',
265  'recentchanges_actor' => 'actor'
266  ] + $commentQuery['tables'],
267  'fields' => [
268  'rc_id',
269  'rc_timestamp',
270  'rc_namespace',
271  'rc_title',
272  'rc_minor',
273  'rc_bot',
274  'rc_new',
275  'rc_cur_id',
276  'rc_this_oldid',
277  'rc_last_oldid',
278  'rc_type',
279  'rc_source',
280  'rc_patrolled',
281  'rc_ip',
282  'rc_old_len',
283  'rc_new_len',
284  'rc_deleted',
285  'rc_logid',
286  'rc_log_type',
287  'rc_log_action',
288  'rc_params',
289  'rc_actor',
290  'rc_user' => 'recentchanges_actor.actor_user',
291  'rc_user_text' => 'recentchanges_actor.actor_name',
292  ] + $commentQuery['fields'],
293  'joins' => [
294  'recentchanges_actor' => [ 'JOIN', 'actor_id=rc_actor' ]
295  ] + $commentQuery['joins'],
296  ];
297  }
298 
299  public function __construct() {
301  'mTitle',
302  '1.37',
303  function () {
304  return Title::castFromPageReference( $this->mPage );
305  },
306  function ( ?Title $title ) {
307  $this->mPage = $title;
308  }
309  );
310  }
311 
312  # Accessors
313 
317  public function setAttribs( $attribs ) {
318  $this->mAttribs = $attribs;
319  }
320 
324  public function setExtra( $extra ) {
325  $this->mExtra = $extra;
326  }
327 
332  public function getTitle() {
333  $this->mPage = Title::castFromPageReference( $this->getPage() );
334  return $this->mPage ?: Title::makeTitle( NS_SPECIAL, 'BadTitle' );
335  }
336 
341  public function getPage(): ?PageReference {
342  if ( !$this->mPage ) {
343  // NOTE: As per the 1.36 release, we always provide rc_title,
344  // even in cases where it doesn't really make sense.
345  // In the future, rc_title may be nullable, or we may use
346  // empty strings in entries that do not refer to a page.
347  if ( ( $this->mAttribs['rc_title'] ?? '' ) === '' ) {
348  return null;
349  }
350 
351  // XXX: We could use rc_cur_id to create a PageIdentityValue,
352  // at least if it's not a special page.
353  // However, newForCategorization() puts the ID of the categorized page into
354  // rc_cur_id, but the title of the category page into rc_title.
355  $this->mPage = new PageReferenceValue(
356  (int)$this->mAttribs['rc_namespace'],
357  $this->mAttribs['rc_title'],
358  PageReference::LOCAL
359  );
360  }
361 
362  return $this->mPage;
363  }
364 
371  public function getPerformer(): User {
372  wfDeprecated( __METHOD__, '1.36' );
373  if ( !$this->mPerformer instanceof User ) {
374  $this->mPerformer = User::newFromIdentity( $this->getPerformerIdentity() );
375  }
376 
377  return $this->mPerformer;
378  }
379 
387  public function getPerformerIdentity(): UserIdentity {
388  if ( !$this->mPerformer ) {
389  $this->mPerformer = $this->getUserIdentityFromAnyId(
390  $this->mAttribs['rc_user'] ?? null,
391  $this->mAttribs['rc_user_text'] ?? null,
392  $this->mAttribs['rc_actor'] ?? null
393  );
394  }
395 
396  return $this->mPerformer;
397  }
398 
408  public function save( $send = self::SEND_FEED ) {
409  $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
410  $putIPinRC = $mainConfig->get( MainConfigNames::PutIPinRC );
411  $useEnotif = $mainConfig->get( 'UseEnotif' );
412  $showUpdatedMarker = $mainConfig->get( MainConfigNames::ShowUpdatedMarker );
413  $dbw = wfGetDB( DB_PRIMARY );
414  if ( !is_array( $this->mExtra ) ) {
415  $this->mExtra = [];
416  }
417 
418  if ( !$putIPinRC ) {
419  $this->mAttribs['rc_ip'] = '';
420  }
421 
422  # Strict mode fixups (not-NULL fields)
423  foreach ( [ 'minor', 'bot', 'new', 'patrolled', 'deleted' ] as $field ) {
424  $this->mAttribs["rc_$field"] = (int)$this->mAttribs["rc_$field"];
425  }
426  # ...more fixups (NULL fields)
427  foreach ( [ 'old_len', 'new_len' ] as $field ) {
428  $this->mAttribs["rc_$field"] = isset( $this->mAttribs["rc_$field"] )
429  ? (int)$this->mAttribs["rc_$field"]
430  : null;
431  }
432 
433  # If our database is strict about IP addresses, use NULL instead of an empty string
434  $strictIPs = $dbw->getType() === 'postgres'; // legacy
435  if ( $strictIPs && $this->mAttribs['rc_ip'] == '' ) {
436  unset( $this->mAttribs['rc_ip'] );
437  }
438 
439  $row = $this->mAttribs;
440 
441  # Trim spaces on user supplied text
442  $row['rc_comment'] = trim( $row['rc_comment'] );
443 
444  # Fixup database timestamps
445  $row['rc_timestamp'] = $dbw->timestamp( $row['rc_timestamp'] );
446 
447  # # If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
448  if ( $row['rc_cur_id'] == 0 ) {
449  unset( $row['rc_cur_id'] );
450  }
451 
452  # Convert mAttribs['rc_comment'] for CommentStore
453  $comment = $row['rc_comment'];
454  unset( $row['rc_comment'], $row['rc_comment_text'], $row['rc_comment_data'] );
455  $row += CommentStore::getStore()->insert( $dbw, 'rc_comment', $comment );
456 
457  # Normalize UserIdentity to actor ID
458  $user = $this->getPerformerIdentity();
459  $actorStore = MediaWikiServices::getInstance()->getActorStore();
460  $row['rc_actor'] = $actorStore->acquireActorId( $user, $dbw );
461  unset( $row['rc_user'], $row['rc_user_text'] );
462 
463  # Don't reuse an existing rc_id for the new row, if one happens to be
464  # set for some reason.
465  unset( $row['rc_id'] );
466 
467  # Insert new row
468  $dbw->insert( 'recentchanges', $row, __METHOD__ );
469 
470  # Set the ID
471  $this->mAttribs['rc_id'] = $dbw->insertId();
472 
473  # Notify extensions
474  Hooks::runner()->onRecentChange_save( $this );
475 
476  // Apply revert tags (if needed)
477  if ( $this->editResult !== null && count( $this->editResult->getRevertTags() ) ) {
479  $this->editResult->getRevertTags(),
480  $this->mAttribs['rc_id'],
481  $this->mAttribs['rc_this_oldid'],
482  $this->mAttribs['rc_logid'],
483  FormatJson::encode( $this->editResult ),
484  $this
485  );
486  }
487 
488  if ( count( $this->tags ) ) {
489  // $this->tags may contain revert tags we already applied above, they will
490  // just be ignored.
492  $this->tags,
493  $this->mAttribs['rc_id'],
494  $this->mAttribs['rc_this_oldid'],
495  $this->mAttribs['rc_logid'],
496  null,
497  $this
498  );
499  }
500 
501  if ( $send === self::SEND_FEED ) {
502  // Emit the change to external applications via RCFeeds.
503  $this->notifyRCFeeds();
504  }
505 
506  # E-mail notifications
507  if ( $useEnotif || $showUpdatedMarker ) {
508  $userFactory = MediaWikiServices::getInstance()->getUserFactory();
509  $editor = $userFactory->newFromUserIdentity( $this->getPerformerIdentity() );
510  $page = $this->getPage();
512 
513  // Never send an RC notification email about categorization changes
514  if (
515  $title &&
516  Hooks::runner()->onAbortEmailNotification( $editor, $title, $this ) &&
517  $this->mAttribs['rc_type'] != RC_CATEGORIZE
518  ) {
519  // @FIXME: This would be better as an extension hook
520  // Send emails or email jobs once this row is safely committed
521  $dbw->onTransactionCommitOrIdle(
522  function () use ( $editor, $title ) {
523  $enotif = new EmailNotification();
524  $enotif->notifyOnPageChange(
525  $editor,
526  $title,
527  $this->mAttribs['rc_timestamp'],
528  $this->mAttribs['rc_comment'],
529  $this->mAttribs['rc_minor'],
530  $this->mAttribs['rc_last_oldid'],
531  $this->mExtra['pageStatus']
532  );
533  },
534  __METHOD__
535  );
536  }
537  }
538 
539  $jobs = [];
540  // Flush old entries from the `recentchanges` table
541  if ( mt_rand( 0, 9 ) == 0 ) {
543  }
544  // Update the cached list of active users
545  if ( $this->mAttribs['rc_user'] > 0 ) {
547  }
548  MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $jobs );
549  }
550 
555  public function notifyRCFeeds( array $feeds = null ) {
556  $rcFeeds =
557  MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RCFeeds );
558  if ( $feeds === null ) {
559  $feeds = $rcFeeds;
560  }
561 
562  $performer = $this->getPerformerIdentity();
563 
564  foreach ( $feeds as $params ) {
565  $params += [
566  'omit_bots' => false,
567  'omit_anon' => false,
568  'omit_user' => false,
569  'omit_minor' => false,
570  'omit_patrolled' => false,
571  ];
572 
573  if (
574  ( $params['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
575  ( $params['omit_anon'] && !$performer->isRegistered() ) ||
576  ( $params['omit_user'] && $performer->isRegistered() ) ||
577  ( $params['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
578  ( $params['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
579  $this->mAttribs['rc_type'] == RC_EXTERNAL
580  ) {
581  continue;
582  }
583 
584  $actionComment = $this->mExtra['actionCommentIRC'] ?? null;
585 
586  $feed = RCFeed::factory( $params );
587  $feed->notify( $this, $actionComment );
588  }
589  }
590 
600  public static function getEngine( $uri, $params = [] ) {
601  wfDeprecated( __METHOD__, '1.29' );
602  $rcEngines =
603  MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RCEngines );
604  $scheme = parse_url( $uri, PHP_URL_SCHEME );
605  if ( !$scheme ) {
606  throw new MWException( "Invalid RCFeed uri: '$uri'" );
607  }
608  if ( !isset( $rcEngines[$scheme] ) ) {
609  throw new MWException( "Unknown RCFeed engine: '$scheme'" );
610  }
611  if ( defined( 'MW_PHPUNIT_TEST' ) && is_object( $rcEngines[$scheme] ) ) {
612  return $rcEngines[$scheme];
613  }
614  return new $rcEngines[$scheme]( $params );
615  }
616 
628  public function doMarkPatrolled( Authority $performer, $auto = false, $tags = null ) {
629  $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
630  $useRCPatrol = $mainConfig->get( MainConfigNames::UseRCPatrol );
631  $useNPPatrol = $mainConfig->get( MainConfigNames::UseNPPatrol );
632  $useFilePatrol = $mainConfig->get( MainConfigNames::UseFilePatrol );
633  // Fix up $tags so that the MarkPatrolled hook below always gets an array
634  if ( $tags === null ) {
635  $tags = [];
636  } elseif ( is_string( $tags ) ) {
637  $tags = [ $tags ];
638  }
639 
640  $status = PermissionStatus::newEmpty();
641  // If recentchanges patrol is disabled, only new pages or new file versions
642  // can be patrolled, provided the appropriate config variable is set
643  if ( !$useRCPatrol && ( !$useNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) &&
644  ( !$useFilePatrol || !( $this->getAttribute( 'rc_type' ) == RC_LOG &&
645  $this->getAttribute( 'rc_log_type' ) == 'upload' ) ) ) {
646  $status->fatal( 'rcpatroldisabled' );
647  }
648  // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
649  $performer->authorizeWrite( $auto ? 'autopatrol' : 'patrol', $this->getTitle(), $status );
650  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
651  if ( !Hooks::runner()->onMarkPatrolled(
652  $this->getAttribute( 'rc_id' ), $user, false, $auto, $tags )
653  ) {
654  $status->fatal( 'hookaborted' );
655  }
656  // Users without the 'autopatrol' right can't patrol their own revisions
657  if ( $performer->getUser()->getName() === $this->getAttribute( 'rc_user_text' ) &&
658  !$performer->isAllowed( 'autopatrol' )
659  ) {
660  $status->fatal( 'markedaspatrollederror-noautopatrol' );
661  }
662  if ( !$status->isGood() ) {
663  return $status->toLegacyErrorArray();
664  }
665  // If the change was patrolled already, do nothing
666  if ( $this->getAttribute( 'rc_patrolled' ) ) {
667  return [];
668  }
669  // Actually set the 'patrolled' flag in RC
670  $this->reallyMarkPatrolled();
671  // Log this patrol event
672  PatrolLog::record( $this, $auto, $performer->getUser(), $tags );
673 
674  Hooks::runner()->onMarkPatrolledComplete(
675  $this->getAttribute( 'rc_id' ), $user, false, $auto );
676 
677  return [];
678  }
679 
684  public function reallyMarkPatrolled() {
685  $dbw = wfGetDB( DB_PRIMARY );
686  $dbw->update(
687  'recentchanges',
688  [
689  'rc_patrolled' => self::PRC_PATROLLED
690  ],
691  [
692  'rc_id' => $this->getAttribute( 'rc_id' )
693  ],
694  __METHOD__
695  );
696  // Invalidate the page cache after the page has been patrolled
697  // to make sure that the Patrol link isn't visible any longer!
698  $this->getTitle()->invalidateCache();
699 
700  // Enqueue a reverted tag update (in case the edit was a revert)
701  $revisionId = $this->getAttribute( 'rc_this_oldid' );
702  if ( $revisionId ) {
703  $revertedTagUpdateManager =
704  MediaWikiServices::getInstance()->getRevertedTagUpdateManager();
705  $revertedTagUpdateManager->approveRevertedTagForRevision( $revisionId );
706  }
707 
708  return $dbw->affectedRows();
709  }
710 
735  public static function notifyEdit(
736  $timestamp, $page, $minor, $user, $comment, $oldId, $lastTimestamp,
737  $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0,
738  $tags = [], EditResult $editResult = null
739  ) {
740  Assert::parameter( $page->exists(), '$page', 'must represent an existing page' );
741 
742  $rc = new RecentChange;
743  $rc->mPage = $page;
744  $rc->mPerformer = $user;
745  $rc->mAttribs = [
746  'rc_timestamp' => $timestamp,
747  'rc_namespace' => $page->getNamespace(),
748  'rc_title' => $page->getDBkey(),
749  'rc_type' => RC_EDIT,
750  'rc_source' => self::SRC_EDIT,
751  'rc_minor' => $minor ? 1 : 0,
752  'rc_cur_id' => $page->getId(),
753  'rc_user' => $user->getId(),
754  'rc_user_text' => $user->getName(),
755  'rc_comment' => &$comment,
756  'rc_comment_text' => &$comment,
757  'rc_comment_data' => null,
758  'rc_this_oldid' => (int)$newId,
759  'rc_last_oldid' => $oldId,
760  'rc_bot' => $bot ? 1 : 0,
761  'rc_ip' => self::checkIPAddress( $ip ),
762  'rc_patrolled' => intval( $patrol ),
763  'rc_new' => 0, # obsolete
764  'rc_old_len' => $oldSize,
765  'rc_new_len' => $newSize,
766  'rc_deleted' => 0,
767  'rc_logid' => 0,
768  'rc_log_type' => null,
769  'rc_log_action' => '',
770  'rc_params' => ''
771  ];
772 
773  // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
774  $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
775 
776  $rc->mExtra = [
777  'prefixedDBkey' => $formatter->getPrefixedDBkey( $page ),
778  'lastTimestamp' => $lastTimestamp,
779  'oldSize' => $oldSize,
780  'newSize' => $newSize,
781  'pageStatus' => 'changed'
782  ];
783 
785  static function () use ( $rc, $tags, $editResult ) {
786  $rc->addTags( $tags );
787  $rc->setEditResult( $editResult );
788  $rc->save();
789  },
790  DeferredUpdates::POSTSEND,
792  );
793 
794  return $rc;
795  }
796 
816  public static function notifyNew(
817  $timestamp,
818  $page, $minor, $user, $comment, $bot,
819  $ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = []
820  ) {
821  Assert::parameter( $page->exists(), '$page', 'must represent an existing page' );
822 
823  $rc = new RecentChange;
824  $rc->mPage = $page;
825  $rc->mPerformer = $user;
826  $rc->mAttribs = [
827  'rc_timestamp' => $timestamp,
828  'rc_namespace' => $page->getNamespace(),
829  'rc_title' => $page->getDBkey(),
830  'rc_type' => RC_NEW,
831  'rc_source' => self::SRC_NEW,
832  'rc_minor' => $minor ? 1 : 0,
833  'rc_cur_id' => $page->getId(),
834  'rc_user' => $user->getId(),
835  'rc_user_text' => $user->getName(),
836  'rc_comment' => &$comment,
837  'rc_comment_text' => &$comment,
838  'rc_comment_data' => null,
839  'rc_this_oldid' => (int)$newId,
840  'rc_last_oldid' => 0,
841  'rc_bot' => $bot ? 1 : 0,
842  'rc_ip' => self::checkIPAddress( $ip ),
843  'rc_patrolled' => intval( $patrol ),
844  'rc_new' => 1, # obsolete
845  'rc_old_len' => 0,
846  'rc_new_len' => $size,
847  'rc_deleted' => 0,
848  'rc_logid' => 0,
849  'rc_log_type' => null,
850  'rc_log_action' => '',
851  'rc_params' => ''
852  ];
853 
854  // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
855  $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
856 
857  $rc->mExtra = [
858  'prefixedDBkey' => $formatter->getPrefixedDBkey( $page ),
859  'lastTimestamp' => 0,
860  'oldSize' => 0,
861  'newSize' => $size,
862  'pageStatus' => 'created'
863  ];
864 
866  static function () use ( $rc, $tags ) {
867  $rc->addTags( $tags );
868  $rc->save();
869  },
870  DeferredUpdates::POSTSEND,
872  );
873 
874  return $rc;
875  }
876 
893  public static function notifyLog( $timestamp,
894  $logPage, $user, $actionComment, $ip, $type,
895  $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
896  ) {
897  $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()
898  ->get( MainConfigNames::LogRestrictions );
899 
900  # Don't add private logs to RC!
901  if ( isset( $logRestrictions[$type] ) && $logRestrictions[$type] != '*' ) {
902  return false;
903  }
904  $rc = self::newLogEntry( $timestamp,
905  $logPage, $user, $actionComment, $ip, $type, $action,
906  $target, $logComment, $params, $newId, $actionCommentIRC );
907  $rc->save();
908 
909  return true;
910  }
911 
930  public static function newLogEntry( $timestamp,
931  $logPage, $user, $actionComment, $ip,
932  $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '',
933  $revId = 0, $isPatrollable = false ) {
934  global $wgRequest;
935  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
936 
937  # # Get pageStatus for email notification
938  switch ( $type . '-' . $action ) {
939  case 'delete-delete':
940  case 'delete-delete_redir':
941  case 'delete-delete_redir2':
942  $pageStatus = 'deleted';
943  break;
944  case 'move-move':
945  case 'move-move_redir':
946  $pageStatus = 'moved';
947  break;
948  case 'delete-restore':
949  $pageStatus = 'restored';
950  break;
951  case 'upload-upload':
952  $pageStatus = 'created';
953  break;
954  case 'upload-overwrite':
955  default:
956  $pageStatus = 'changed';
957  break;
958  }
959 
960  // Allow unpatrolled status for patrollable log entries
961  $canAutopatrol = $permissionManager->userHasRight( $user, 'autopatrol' );
962  $markPatrolled = $isPatrollable ? $canAutopatrol : true;
963 
964  if ( $target instanceof PageIdentity && $target->canExist() ) {
965  $pageId = $target->getId();
966  } else {
967  $pageId = 0;
968  }
969 
970  $rc = new RecentChange;
971  $rc->mPage = $target;
972  $rc->mPerformer = $user;
973  $rc->mAttribs = [
974  'rc_timestamp' => $timestamp,
975  'rc_namespace' => $target->getNamespace(),
976  'rc_title' => $target->getDBkey(),
977  'rc_type' => RC_LOG,
978  'rc_source' => self::SRC_LOG,
979  'rc_minor' => 0,
980  'rc_cur_id' => $pageId,
981  'rc_user' => $user->getId(),
982  'rc_user_text' => $user->getName(),
983  'rc_comment' => &$logComment,
984  'rc_comment_text' => &$logComment,
985  'rc_comment_data' => null,
986  'rc_this_oldid' => (int)$revId,
987  'rc_last_oldid' => 0,
988  'rc_bot' => $permissionManager->userHasRight( $user, 'bot' ) ?
989  (int)$wgRequest->getBool( 'bot', true ) : 0,
990  'rc_ip' => self::checkIPAddress( $ip ),
991  'rc_patrolled' => $markPatrolled ? self::PRC_AUTOPATROLLED : self::PRC_UNPATROLLED,
992  'rc_new' => 0, # obsolete
993  'rc_old_len' => null,
994  'rc_new_len' => null,
995  'rc_deleted' => 0,
996  'rc_logid' => $newId,
997  'rc_log_type' => $type,
998  'rc_log_action' => $action,
999  'rc_params' => $params
1000  ];
1001 
1002  // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
1003  $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
1004 
1005  $rc->mExtra = [
1006  // XXX: This does not correspond to rc_namespace/rc_title/rc_cur_id.
1007  // Is that intentional? For all other kinds of RC entries, prefixedDBkey
1008  // matches rc_namespace/rc_title. Do we even need $logPage?
1009  'prefixedDBkey' => $formatter->getPrefixedDBkey( $logPage ),
1010  'lastTimestamp' => 0,
1011  'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
1012  'pageStatus' => $pageStatus,
1013  'actionCommentIRC' => $actionCommentIRC
1014  ];
1015 
1016  return $rc;
1017  }
1018 
1040  public static function newForCategorization(
1041  $timestamp,
1042  PageIdentity $categoryTitle,
1043  ?UserIdentity $user,
1044  $comment,
1045  PageIdentity $pageTitle,
1046  $oldRevId,
1047  $newRevId,
1048  $lastTimestamp,
1049  $bot,
1050  $ip = '',
1051  $deleted = 0,
1052  $added = null
1053  ) {
1054  // Done in a backwards compatible way.
1055  $categoryWikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()
1056  ->newFromTitle( $categoryTitle );
1057 
1058  '@phan-var WikiCategoryPage $categoryWikiPage';
1059  $params = [
1060  'hidden-cat' => $categoryWikiPage->isHidden()
1061  ];
1062  if ( $added !== null ) {
1063  $params['added'] = $added;
1064  }
1065 
1066  if ( !$user ) {
1067  // XXX: when and why do we need this?
1068  $user = MediaWikiServices::getInstance()->getActorStore()->getUnknownActor();
1069  }
1070 
1071  $rc = new RecentChange;
1072  $rc->mPage = $categoryTitle;
1073  $rc->mPerformer = $user;
1074  $rc->mAttribs = [
1075  'rc_timestamp' => MWTimestamp::convert( TS_MW, $timestamp ),
1076  'rc_namespace' => $categoryTitle->getNamespace(),
1077  'rc_title' => $categoryTitle->getDBkey(),
1078  'rc_type' => RC_CATEGORIZE,
1079  'rc_source' => self::SRC_CATEGORIZE,
1080  'rc_minor' => 0,
1081  // XXX: rc_cur_id does not correspond to rc_namespace/rc_title.
1082  // They refer to different pages. Is that intentional?
1083  'rc_cur_id' => $pageTitle->getId(),
1084  'rc_user' => $user->getId(),
1085  'rc_user_text' => $user->getName(),
1086  'rc_comment' => &$comment,
1087  'rc_comment_text' => &$comment,
1088  'rc_comment_data' => null,
1089  'rc_this_oldid' => (int)$newRevId,
1090  'rc_last_oldid' => $oldRevId,
1091  'rc_bot' => $bot ? 1 : 0,
1092  'rc_ip' => self::checkIPAddress( $ip ),
1093  'rc_patrolled' => self::PRC_AUTOPATROLLED, // Always patrolled, just like log entries
1094  'rc_new' => 0, # obsolete
1095  'rc_old_len' => null,
1096  'rc_new_len' => null,
1097  'rc_deleted' => $deleted,
1098  'rc_logid' => 0,
1099  'rc_log_type' => null,
1100  'rc_log_action' => '',
1101  'rc_params' => serialize( $params )
1102  ];
1103 
1104  // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
1105  $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
1106 
1107  $rc->mExtra = [
1108  'prefixedDBkey' => $formatter->getPrefixedDBkey( $categoryTitle ),
1109  'lastTimestamp' => $lastTimestamp,
1110  'oldSize' => 0,
1111  'newSize' => 0,
1112  'pageStatus' => 'changed'
1113  ];
1114 
1115  return $rc;
1116  }
1117 
1126  public function getParam( $name ) {
1127  $params = $this->parseParams();
1128  return $params[$name] ?? null;
1129  }
1130 
1136  public function loadFromRow( $row ) {
1137  $this->mAttribs = get_object_vars( $row );
1138  $this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
1139  // rc_deleted MUST be set
1140  $this->mAttribs['rc_deleted'] = $row->rc_deleted;
1141 
1142  if ( isset( $this->mAttribs['rc_ip'] ) ) {
1143  // Clean up CIDRs for Postgres per T164898. ("127.0.0.1" casts to "127.0.0.1/32")
1144  $n = strpos( $this->mAttribs['rc_ip'], '/' );
1145  if ( $n !== false ) {
1146  $this->mAttribs['rc_ip'] = substr( $this->mAttribs['rc_ip'], 0, $n );
1147  }
1148  }
1149 
1150  $comment = CommentStore::getStore()
1151  // Legacy because $row may have come from self::selectFields()
1152  ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $row, true )
1153  ->text;
1154  $this->mAttribs['rc_comment'] = &$comment;
1155  $this->mAttribs['rc_comment_text'] = &$comment;
1156  $this->mAttribs['rc_comment_data'] = null;
1157 
1158  $this->mPerformer = $this->getUserIdentityFromAnyId(
1159  $row->rc_user ?? null,
1160  $row->rc_user_text ?? null,
1161  $row->rc_actor ?? null
1162  );
1163  $this->mAttribs['rc_user'] = $this->mPerformer->getId();
1164  $this->mAttribs['rc_user_text'] = $this->mPerformer->getName();
1165 
1166  // Watchlist expiry.
1167  if ( isset( $row->we_expiry ) && $row->we_expiry ) {
1168  $this->watchlistExpiry = wfTimestamp( TS_MW, $row->we_expiry );
1169  }
1170  }
1171 
1178  public function getAttribute( $name ) {
1179  if ( $name === 'rc_comment' ) {
1180  return CommentStore::getStore()
1181  ->getComment( 'rc_comment', $this->mAttribs, true )->text;
1182  }
1183 
1184  if ( $name === 'rc_user' || $name === 'rc_user_text' || $name === 'rc_actor' ) {
1185  $user = $this->getPerformerIdentity();
1186 
1187  if ( $name === 'rc_user' ) {
1188  return $user->getId();
1189  }
1190  if ( $name === 'rc_user_text' ) {
1191  return $user->getName();
1192  }
1193  if ( $name === 'rc_actor' ) {
1194  // NOTE: rc_actor exists in the database, but application logic should not use it.
1195  wfDeprecatedMsg( 'Accessing deprecated field rc_actor', '1.36' );
1196  $actorStore = MediaWikiServices::getInstance()->getActorStore();
1197  $db = wfGetDB( DB_REPLICA );
1198  return $actorStore->findActorId( $user, $db );
1199  }
1200  }
1201 
1202  return $this->mAttribs[$name] ?? null;
1203  }
1204 
1208  public function getAttributes() {
1209  return $this->mAttribs;
1210  }
1211 
1218  public function diffLinkTrail( $forceCur ) {
1219  if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
1220  $trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
1221  "&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
1222  if ( $forceCur ) {
1223  $trail .= '&diff=0';
1224  } else {
1225  $trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
1226  }
1227  } else {
1228  $trail = '';
1229  }
1230 
1231  return $trail;
1232  }
1233 
1241  public function getCharacterDifference( $old = 0, $new = 0 ) {
1242  if ( $old === 0 ) {
1243  $old = $this->mAttribs['rc_old_len'];
1244  }
1245  if ( $new === 0 ) {
1246  $new = $this->mAttribs['rc_new_len'];
1247  }
1248  if ( $old === null || $new === null ) {
1249  return '';
1250  }
1251 
1252  return ChangesList::showCharacterDifference( $old, $new );
1253  }
1254 
1255  private static function checkIPAddress( $ip ) {
1256  global $wgRequest;
1257  if ( $ip ) {
1258  if ( !IPUtils::isIPAddress( $ip ) ) {
1259  throw new MWException( "Attempt to write \"" . $ip .
1260  "\" as an IP address into recent changes" );
1261  }
1262  } else {
1263  $ip = $wgRequest->getIP();
1264  if ( !$ip ) {
1265  $ip = '';
1266  }
1267  }
1268 
1269  return $ip;
1270  }
1271 
1281  public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
1282  $rcMaxAge =
1283  MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RCMaxAge );
1284 
1285  return (int)wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $rcMaxAge;
1286  }
1287 
1295  public function parseParams() {
1296  $rcParams = $this->getAttribute( 'rc_params' );
1297 
1298  AtEase::suppressWarnings();
1299  $unserializedParams = unserialize( $rcParams );
1300  AtEase::restoreWarnings();
1301 
1302  return $unserializedParams;
1303  }
1304 
1313  public function addTags( $tags ) {
1314  if ( is_string( $tags ) ) {
1315  $this->tags[] = $tags;
1316  } else {
1317  $this->tags = array_merge( $tags, $this->tags );
1318  }
1319  }
1320 
1328  public function setEditResult( ?EditResult $editResult ) {
1329  $this->editResult = $editResult;
1330  }
1331 
1339  private function getUserIdentityFromAnyId(
1340  $userId,
1341  $userName,
1342  $actorId = null
1343  ): UserIdentity {
1344  // XXX: Is this logic needed elsewhere? Should it be reusable?
1345 
1346  $userId = isset( $userId ) ? (int)$userId : null;
1347  $actorId = isset( $actorId ) ? (int)$actorId : 0;
1348 
1349  $actorStore = MediaWikiServices::getInstance()->getActorStore();
1350  if ( $userName && $actorId ) {
1351  // Likely the fields are coming from a join on actor table,
1352  // so can definitely build a UserIdentityValue.
1353  return $actorStore->newActorFromRowFields( $userId, $userName, $actorId );
1354  }
1355  if ( $userId !== null ) {
1356  if ( $userName !== null ) {
1357  // NOTE: For IPs and external users, $userId will be 0.
1358  $user = new UserIdentityValue( $userId, $userName );
1359  } else {
1360  $user = $actorStore->getUserIdentityByUserId( $userId );
1361 
1362  if ( !$user ) {
1363  throw new RuntimeException( "User not found by ID: $userId" );
1364  }
1365  }
1366  } elseif ( $actorId > 0 ) {
1367  $db = wfGetDB( DB_REPLICA );
1368  $user = $actorStore->getActorById( $actorId, $db );
1369 
1370  if ( !$user ) {
1371  throw new RuntimeException( "User not found by actor ID: $actorId" );
1372  }
1373  } elseif ( $userName !== null ) {
1374  $user = $actorStore->getUserIdentityByName( $userName );
1375 
1376  if ( !$user ) {
1377  throw new RuntimeException( "User not found by name: $userName" );
1378  }
1379  } else {
1380  throw new RuntimeException( 'At least one of user ID, actor ID or user name must be given' );
1381  }
1382 
1383  return $user;
1384  }
1385 }
serialize()
unserialize( $serialized)
const RC_NEW
Definition: Defines.php:116
const NS_SPECIAL
Definition: Defines.php:53
const RC_LOG
Definition: Defines.php:117
const RC_EXTERNAL
Definition: Defines.php:118
const RC_EDIT
Definition: Defines.php:115
const RC_CATEGORIZE
Definition: Defines.php:119
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:366
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:317
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 checkIPAddress( $ip)
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.
const CHANGE_TYPES
UserIdentity null $mPerformer
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.
EditResult null $editResult
EditResult associated with the edit.
const PRC_PATROLLED
const SRC_EXTERNAL
notifyRCFeeds(array $feeds=null)
Notify all the feeds about the change.
PageReference null $mPage
array $tags
List of tags to apply.
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.
getUserIdentityFromAnyId( $userId, $userName, $actorId=null)
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:68
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:673
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