MediaWiki REL1_39
RecentChange.php
Go to the documentation of this file.
1<?php
34use Wikimedia\Assert\Assert;
35use Wikimedia\AtEase\AtEase;
36use Wikimedia\IPUtils;
37
83class 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
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();
509 $title = Title::castFromPageReference( $page );
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 ) {
540 $jobs[] = RecentChangesUpdateJob::newPurgeJob();
541 }
542 // Update the cached list of active users
543 if ( $this->mAttribs['rc_user'] > 0 ) {
544 $jobs[] = RecentChangesUpdateJob::newCacheUpdateJob();
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
782 DeferredUpdates::addCallableUpdate(
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
863 DeferredUpdates::addCallableUpdate(
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.
This module processes the email notifications when the current page is changed.
MediaWiki exception.
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.
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
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
internal since 1.36
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.
getNamespace()
Returns the page's namespace number.
getDBkey()
Get the page title in DB key form.
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