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 {
84 use DeprecationHelper;
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 // T403757: Don't send 'suppressed from creation' recent changes entries to the RCFeeds as they do not
555 // have systems to appropriately redact suppressed / deleted material
556 if ( $this->mAttribs['rc_deleted'] != 0 ) {
557 return;
558 }
559
560 $rcFeeds =
561 MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RCFeeds );
562 if ( $feeds === null ) {
563 $feeds = $rcFeeds;
564 }
565
566 $performer = $this->getPerformerIdentity();
567
568 foreach ( $feeds as $params ) {
569 $params += [
570 'omit_bots' => false,
571 'omit_anon' => false,
572 'omit_user' => false,
573 'omit_minor' => false,
574 'omit_patrolled' => false,
575 ];
576
577 if (
578 ( $params['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
579 ( $params['omit_anon'] && !$performer->isRegistered() ) ||
580 ( $params['omit_user'] && $performer->isRegistered() ) ||
581 ( $params['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
582 ( $params['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
583 $this->mAttribs['rc_type'] == RC_EXTERNAL
584 ) {
585 continue;
586 }
587
588 $actionComment = $this->mExtra['actionCommentIRC'] ?? null;
589
590 $feed = RCFeed::factory( $params );
591 $feed->notify( $this, $actionComment );
592 }
593 }
594
604 public static function getEngine( $uri, $params = [] ) {
605 wfDeprecated( __METHOD__, '1.29' );
606 $rcEngines =
607 MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RCEngines );
608 $scheme = parse_url( $uri, PHP_URL_SCHEME );
609 if ( !$scheme ) {
610 throw new MWException( "Invalid RCFeed uri: '$uri'" );
611 }
612 if ( !isset( $rcEngines[$scheme] ) ) {
613 throw new MWException( "Unknown RCFeed engine: '$scheme'" );
614 }
615 if ( defined( 'MW_PHPUNIT_TEST' ) && is_object( $rcEngines[$scheme] ) ) {
616 return $rcEngines[$scheme];
617 }
618 return new $rcEngines[$scheme]( $params );
619 }
620
632 public function doMarkPatrolled( Authority $performer, $auto = false, $tags = null ) {
633 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
634 $useRCPatrol = $mainConfig->get( MainConfigNames::UseRCPatrol );
635 $useNPPatrol = $mainConfig->get( MainConfigNames::UseNPPatrol );
636 $useFilePatrol = $mainConfig->get( MainConfigNames::UseFilePatrol );
637 // Fix up $tags so that the MarkPatrolled hook below always gets an array
638 if ( $tags === null ) {
639 $tags = [];
640 } elseif ( is_string( $tags ) ) {
641 $tags = [ $tags ];
642 }
643
644 $status = PermissionStatus::newEmpty();
645 // If recentchanges patrol is disabled, only new pages or new file versions
646 // can be patrolled, provided the appropriate config variable is set
647 if ( !$useRCPatrol && ( !$useNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) &&
648 ( !$useFilePatrol || !( $this->getAttribute( 'rc_type' ) == RC_LOG &&
649 $this->getAttribute( 'rc_log_type' ) == 'upload' ) ) ) {
650 $status->fatal( 'rcpatroldisabled' );
651 }
652 // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
653 $performer->authorizeWrite( $auto ? 'autopatrol' : 'patrol', $this->getTitle(), $status );
654 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
655 if ( !Hooks::runner()->onMarkPatrolled(
656 $this->getAttribute( 'rc_id' ), $user, false, $auto, $tags )
657 ) {
658 $status->fatal( 'hookaborted' );
659 }
660 // Users without the 'autopatrol' right can't patrol their own revisions
661 if ( $performer->getUser()->getName() === $this->getAttribute( 'rc_user_text' ) &&
662 !$performer->isAllowed( 'autopatrol' )
663 ) {
664 $status->fatal( 'markedaspatrollederror-noautopatrol' );
665 }
666 if ( !$status->isGood() ) {
667 return $status->toLegacyErrorArray();
668 }
669 // If the change was patrolled already, do nothing
670 if ( $this->getAttribute( 'rc_patrolled' ) ) {
671 return [];
672 }
673 // Actually set the 'patrolled' flag in RC
674 $this->reallyMarkPatrolled();
675 // Log this patrol event
676 PatrolLog::record( $this, $auto, $performer->getUser(), $tags );
677
678 Hooks::runner()->onMarkPatrolledComplete(
679 $this->getAttribute( 'rc_id' ), $user, false, $auto );
680
681 return [];
682 }
683
688 public function reallyMarkPatrolled() {
689 $dbw = wfGetDB( DB_PRIMARY );
690 $dbw->update(
691 'recentchanges',
692 [
693 'rc_patrolled' => self::PRC_PATROLLED
694 ],
695 [
696 'rc_id' => $this->getAttribute( 'rc_id' )
697 ],
698 __METHOD__
699 );
700 // Invalidate the page cache after the page has been patrolled
701 // to make sure that the Patrol link isn't visible any longer!
702 $this->getTitle()->invalidateCache();
703
704 // Enqueue a reverted tag update (in case the edit was a revert)
705 $revisionId = $this->getAttribute( 'rc_this_oldid' );
706 if ( $revisionId ) {
707 $revertedTagUpdateManager =
708 MediaWikiServices::getInstance()->getRevertedTagUpdateManager();
709 $revertedTagUpdateManager->approveRevertedTagForRevision( $revisionId );
710 }
711
712 return $dbw->affectedRows();
713 }
714
739 public static function notifyEdit(
740 $timestamp, $page, $minor, $user, $comment, $oldId, $lastTimestamp,
741 $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0,
742 $tags = [], EditResult $editResult = null
743 ) {
744 Assert::parameter( $page->exists(), '$page', 'must represent an existing page' );
745
746 $rc = new RecentChange;
747 $rc->mPage = $page;
748 $rc->mPerformer = $user;
749 $rc->mAttribs = [
750 'rc_timestamp' => $timestamp,
751 'rc_namespace' => $page->getNamespace(),
752 'rc_title' => $page->getDBkey(),
753 'rc_type' => RC_EDIT,
754 'rc_source' => self::SRC_EDIT,
755 'rc_minor' => $minor ? 1 : 0,
756 'rc_cur_id' => $page->getId(),
757 'rc_user' => $user->getId(),
758 'rc_user_text' => $user->getName(),
759 'rc_comment' => &$comment,
760 'rc_comment_text' => &$comment,
761 'rc_comment_data' => null,
762 'rc_this_oldid' => (int)$newId,
763 'rc_last_oldid' => $oldId,
764 'rc_bot' => $bot ? 1 : 0,
765 'rc_ip' => self::checkIPAddress( $ip ),
766 'rc_patrolled' => intval( $patrol ),
767 'rc_new' => 0, # obsolete
768 'rc_old_len' => $oldSize,
769 'rc_new_len' => $newSize,
770 'rc_deleted' => 0,
771 'rc_logid' => 0,
772 'rc_log_type' => null,
773 'rc_log_action' => '',
774 'rc_params' => ''
775 ];
776
777 // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
778 $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
779
780 $rc->mExtra = [
781 'prefixedDBkey' => $formatter->getPrefixedDBkey( $page ),
782 'lastTimestamp' => $lastTimestamp,
783 'oldSize' => $oldSize,
784 'newSize' => $newSize,
785 'pageStatus' => 'changed'
786 ];
787
788 DeferredUpdates::addCallableUpdate(
789 static function () use ( $rc, $tags, $editResult ) {
790 $rc->addTags( $tags );
791 $rc->setEditResult( $editResult );
792 $rc->save();
793 },
794 DeferredUpdates::POSTSEND,
796 );
797
798 return $rc;
799 }
800
820 public static function notifyNew(
821 $timestamp,
822 $page, $minor, $user, $comment, $bot,
823 $ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = []
824 ) {
825 Assert::parameter( $page->exists(), '$page', 'must represent an existing page' );
826
827 $rc = new RecentChange;
828 $rc->mPage = $page;
829 $rc->mPerformer = $user;
830 $rc->mAttribs = [
831 'rc_timestamp' => $timestamp,
832 'rc_namespace' => $page->getNamespace(),
833 'rc_title' => $page->getDBkey(),
834 'rc_type' => RC_NEW,
835 'rc_source' => self::SRC_NEW,
836 'rc_minor' => $minor ? 1 : 0,
837 'rc_cur_id' => $page->getId(),
838 'rc_user' => $user->getId(),
839 'rc_user_text' => $user->getName(),
840 'rc_comment' => &$comment,
841 'rc_comment_text' => &$comment,
842 'rc_comment_data' => null,
843 'rc_this_oldid' => (int)$newId,
844 'rc_last_oldid' => 0,
845 'rc_bot' => $bot ? 1 : 0,
846 'rc_ip' => self::checkIPAddress( $ip ),
847 'rc_patrolled' => intval( $patrol ),
848 'rc_new' => 1, # obsolete
849 'rc_old_len' => 0,
850 'rc_new_len' => $size,
851 'rc_deleted' => 0,
852 'rc_logid' => 0,
853 'rc_log_type' => null,
854 'rc_log_action' => '',
855 'rc_params' => ''
856 ];
857
858 // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
859 $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
860
861 $rc->mExtra = [
862 'prefixedDBkey' => $formatter->getPrefixedDBkey( $page ),
863 'lastTimestamp' => 0,
864 'oldSize' => 0,
865 'newSize' => $size,
866 'pageStatus' => 'created'
867 ];
868
869 DeferredUpdates::addCallableUpdate(
870 static function () use ( $rc, $tags ) {
871 $rc->addTags( $tags );
872 $rc->save();
873 },
874 DeferredUpdates::POSTSEND,
876 );
877
878 return $rc;
879 }
880
897 public static function notifyLog( $timestamp,
898 $logPage, $user, $actionComment, $ip, $type,
899 $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
900 ) {
901 $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()
902 ->get( MainConfigNames::LogRestrictions );
903
904 # Don't add private logs to RC!
905 if ( isset( $logRestrictions[$type] ) && $logRestrictions[$type] != '*' ) {
906 return false;
907 }
908 $rc = self::newLogEntry( $timestamp,
909 $logPage, $user, $actionComment, $ip, $type, $action,
910 $target, $logComment, $params, $newId, $actionCommentIRC );
911 $rc->save();
912
913 return true;
914 }
915
937 public static function newLogEntry( $timestamp,
938 $logPage, $user, $actionComment, $ip,
939 $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '',
940 $revId = 0, $isPatrollable = false, $forceBotFlag = null, $deleted = 0
941 ) {
942 global $wgRequest;
943 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
944
945 # # Get pageStatus for email notification
946 switch ( $type . '-' . $action ) {
947 case 'delete-delete':
948 case 'delete-delete_redir':
949 case 'delete-delete_redir2':
950 $pageStatus = 'deleted';
951 break;
952 case 'move-move':
953 case 'move-move_redir':
954 $pageStatus = 'moved';
955 break;
956 case 'delete-restore':
957 $pageStatus = 'restored';
958 break;
959 case 'upload-upload':
960 $pageStatus = 'created';
961 break;
962 case 'upload-overwrite':
963 default:
964 $pageStatus = 'changed';
965 break;
966 }
967
968 // Allow unpatrolled status for patrollable log entries
969 $canAutopatrol = $permissionManager->userHasRight( $user, 'autopatrol' );
970 $markPatrolled = $isPatrollable ? $canAutopatrol : true;
971
972 if ( $target instanceof PageIdentity && $target->canExist() ) {
973 $pageId = $target->getId();
974 } else {
975 $pageId = 0;
976 }
977
978 if ( $forceBotFlag !== null ) {
979 $bot = (int)$forceBotFlag;
980 } else {
981 $bot = $permissionManager->userHasRight( $user, 'bot' ) ?
982 (int)$wgRequest->getBool( 'bot', true ) : 0;
983 }
984
985 $rc = new RecentChange;
986 $rc->mPage = $target;
987 $rc->mPerformer = $user;
988 $rc->mAttribs = [
989 'rc_timestamp' => $timestamp,
990 'rc_namespace' => $target->getNamespace(),
991 'rc_title' => $target->getDBkey(),
992 'rc_type' => RC_LOG,
993 'rc_source' => self::SRC_LOG,
994 'rc_minor' => 0,
995 'rc_cur_id' => $pageId,
996 'rc_user' => $user->getId(),
997 'rc_user_text' => $user->getName(),
998 'rc_comment' => &$logComment,
999 'rc_comment_text' => &$logComment,
1000 'rc_comment_data' => null,
1001 'rc_this_oldid' => (int)$revId,
1002 'rc_last_oldid' => 0,
1003 'rc_bot' => $bot,
1004 'rc_ip' => self::checkIPAddress( $ip ),
1005 'rc_patrolled' => $markPatrolled ? self::PRC_AUTOPATROLLED : self::PRC_UNPATROLLED,
1006 'rc_new' => 0, # obsolete
1007 'rc_old_len' => null,
1008 'rc_new_len' => null,
1009 'rc_deleted' => $deleted,
1010 'rc_logid' => $newId,
1011 'rc_log_type' => $type,
1012 'rc_log_action' => $action,
1013 'rc_params' => $params
1014 ];
1015
1016 // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
1017 $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
1018
1019 $rc->mExtra = [
1020 // XXX: This does not correspond to rc_namespace/rc_title/rc_cur_id.
1021 // Is that intentional? For all other kinds of RC entries, prefixedDBkey
1022 // matches rc_namespace/rc_title. Do we even need $logPage?
1023 'prefixedDBkey' => $formatter->getPrefixedDBkey( $logPage ),
1024 'lastTimestamp' => 0,
1025 'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
1026 'pageStatus' => $pageStatus,
1027 'actionCommentIRC' => $actionCommentIRC
1028 ];
1029
1030 return $rc;
1031 }
1032
1054 public static function newForCategorization(
1055 $timestamp,
1056 PageIdentity $categoryTitle,
1057 ?UserIdentity $user,
1058 $comment,
1059 PageIdentity $pageTitle,
1060 $oldRevId,
1061 $newRevId,
1062 $lastTimestamp,
1063 $bot,
1064 $ip = '',
1065 $deleted = 0,
1066 $added = null
1067 ) {
1068 // Done in a backwards compatible way.
1069 $categoryWikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()
1070 ->newFromTitle( $categoryTitle );
1071
1072 '@phan-var WikiCategoryPage $categoryWikiPage';
1073 $params = [
1074 'hidden-cat' => $categoryWikiPage->isHidden()
1075 ];
1076 if ( $added !== null ) {
1077 $params['added'] = $added;
1078 }
1079
1080 if ( !$user ) {
1081 // XXX: when and why do we need this?
1082 $user = MediaWikiServices::getInstance()->getActorStore()->getUnknownActor();
1083 }
1084
1085 $rc = new RecentChange;
1086 $rc->mPage = $categoryTitle;
1087 $rc->mPerformer = $user;
1088 $rc->mAttribs = [
1089 'rc_timestamp' => MWTimestamp::convert( TS_MW, $timestamp ),
1090 'rc_namespace' => $categoryTitle->getNamespace(),
1091 'rc_title' => $categoryTitle->getDBkey(),
1092 'rc_type' => RC_CATEGORIZE,
1093 'rc_source' => self::SRC_CATEGORIZE,
1094 'rc_minor' => 0,
1095 // XXX: rc_cur_id does not correspond to rc_namespace/rc_title.
1096 // They refer to different pages. Is that intentional?
1097 'rc_cur_id' => $pageTitle->getId(),
1098 'rc_user' => $user->getId(),
1099 'rc_user_text' => $user->getName(),
1100 'rc_comment' => &$comment,
1101 'rc_comment_text' => &$comment,
1102 'rc_comment_data' => null,
1103 'rc_this_oldid' => (int)$newRevId,
1104 'rc_last_oldid' => $oldRevId,
1105 'rc_bot' => $bot ? 1 : 0,
1106 'rc_ip' => self::checkIPAddress( $ip ),
1107 'rc_patrolled' => self::PRC_AUTOPATROLLED, // Always patrolled, just like log entries
1108 'rc_new' => 0, # obsolete
1109 'rc_old_len' => null,
1110 'rc_new_len' => null,
1111 'rc_deleted' => $deleted,
1112 'rc_logid' => 0,
1113 'rc_log_type' => null,
1114 'rc_log_action' => '',
1115 'rc_params' => serialize( $params )
1116 ];
1117
1118 // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
1119 $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
1120
1121 $rc->mExtra = [
1122 'prefixedDBkey' => $formatter->getPrefixedDBkey( $categoryTitle ),
1123 'lastTimestamp' => $lastTimestamp,
1124 'oldSize' => 0,
1125 'newSize' => 0,
1126 'pageStatus' => 'changed'
1127 ];
1128
1129 return $rc;
1130 }
1131
1140 public function getParam( $name ) {
1141 $params = $this->parseParams();
1142 return $params[$name] ?? null;
1143 }
1144
1150 public function loadFromRow( $row ) {
1151 $this->mAttribs = get_object_vars( $row );
1152 $this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
1153 // rc_deleted MUST be set
1154 $this->mAttribs['rc_deleted'] = $row->rc_deleted;
1155
1156 $comment = CommentStore::getStore()
1157 // Legacy because $row may have come from self::selectFields()
1158 ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $row, true )
1159 ->text;
1160 $this->mAttribs['rc_comment'] = &$comment;
1161 $this->mAttribs['rc_comment_text'] = &$comment;
1162 $this->mAttribs['rc_comment_data'] = null;
1163
1164 $this->mPerformer = $this->getUserIdentityFromAnyId(
1165 $row->rc_user ?? null,
1166 $row->rc_user_text ?? null,
1167 $row->rc_actor ?? null
1168 );
1169 $this->mAttribs['rc_user'] = $this->mPerformer->getId();
1170 $this->mAttribs['rc_user_text'] = $this->mPerformer->getName();
1171
1172 // Watchlist expiry.
1173 if ( isset( $row->we_expiry ) && $row->we_expiry ) {
1174 $this->watchlistExpiry = wfTimestamp( TS_MW, $row->we_expiry );
1175 }
1176 }
1177
1184 public function getAttribute( $name ) {
1185 if ( $name === 'rc_comment' ) {
1186 return CommentStore::getStore()
1187 ->getComment( 'rc_comment', $this->mAttribs, true )->text;
1188 }
1189
1190 if ( $name === 'rc_user' || $name === 'rc_user_text' || $name === 'rc_actor' ) {
1191 $user = $this->getPerformerIdentity();
1192
1193 if ( $name === 'rc_user' ) {
1194 return $user->getId();
1195 }
1196 if ( $name === 'rc_user_text' ) {
1197 return $user->getName();
1198 }
1199 if ( $name === 'rc_actor' ) {
1200 // NOTE: rc_actor exists in the database, but application logic should not use it.
1201 wfDeprecatedMsg( 'Accessing deprecated field rc_actor', '1.36' );
1202 $actorStore = MediaWikiServices::getInstance()->getActorStore();
1203 $db = wfGetDB( DB_REPLICA );
1204 return $actorStore->findActorId( $user, $db );
1205 }
1206 }
1207
1208 return $this->mAttribs[$name] ?? null;
1209 }
1210
1214 public function getAttributes() {
1215 return $this->mAttribs;
1216 }
1217
1224 public function diffLinkTrail( $forceCur ) {
1225 if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
1226 $trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
1227 "&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
1228 if ( $forceCur ) {
1229 $trail .= '&diff=0';
1230 } else {
1231 $trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
1232 }
1233 } else {
1234 $trail = '';
1235 }
1236
1237 return $trail;
1238 }
1239
1247 public function getCharacterDifference( $old = 0, $new = 0 ) {
1248 if ( $old === 0 ) {
1249 $old = $this->mAttribs['rc_old_len'];
1250 }
1251 if ( $new === 0 ) {
1252 $new = $this->mAttribs['rc_new_len'];
1253 }
1254 if ( $old === null || $new === null ) {
1255 return '';
1256 }
1257
1258 return ChangesList::showCharacterDifference( $old, $new );
1259 }
1260
1261 private static function checkIPAddress( $ip ) {
1262 global $wgRequest;
1263 if ( $ip ) {
1264 if ( !IPUtils::isIPAddress( $ip ) ) {
1265 throw new MWException( "Attempt to write \"" . $ip .
1266 "\" as an IP address into recent changes" );
1267 }
1268 } else {
1269 $ip = $wgRequest->getIP();
1270 if ( !$ip ) {
1271 $ip = '';
1272 }
1273 }
1274
1275 return $ip;
1276 }
1277
1287 public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
1288 $rcMaxAge =
1289 MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RCMaxAge );
1290
1291 return (int)wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $rcMaxAge;
1292 }
1293
1301 public function parseParams() {
1302 $rcParams = $this->getAttribute( 'rc_params' );
1303
1304 AtEase::suppressWarnings();
1305 $unserializedParams = unserialize( $rcParams );
1306 AtEase::restoreWarnings();
1307
1308 return $unserializedParams;
1309 }
1310
1319 public function addTags( $tags ) {
1320 if ( is_string( $tags ) ) {
1321 $this->tags[] = $tags;
1322 } else {
1323 $this->tags = array_merge( $tags, $this->tags );
1324 }
1325 }
1326
1334 public function setEditResult( ?EditResult $editResult ) {
1335 $this->editResult = $editResult;
1336 }
1337
1345 private function getUserIdentityFromAnyId(
1346 $userId,
1347 $userName,
1348 $actorId = null
1349 ): UserIdentity {
1350 // XXX: Is this logic needed elsewhere? Should it be reusable?
1351
1352 $userId = isset( $userId ) ? (int)$userId : null;
1353 $actorId = isset( $actorId ) ? (int)$actorId : 0;
1354
1355 $actorStore = MediaWikiServices::getInstance()->getActorStore();
1356 if ( $userName && $actorId ) {
1357 // Likely the fields are coming from a join on actor table,
1358 // so can definitely build a UserIdentityValue.
1359 return $actorStore->newActorFromRowFields( $userId, $userName, $actorId );
1360 }
1361 if ( $userId !== null ) {
1362 if ( $userName !== null ) {
1363 // NOTE: For IPs and external users, $userId will be 0.
1364 $user = new UserIdentityValue( $userId, $userName );
1365 } else {
1366 $user = $actorStore->getUserIdentityByUserId( $userId );
1367
1368 if ( !$user ) {
1369 throw new RuntimeException( "User not found by ID: $userId" );
1370 }
1371 }
1372 } elseif ( $actorId > 0 ) {
1373 $db = wfGetDB( DB_REPLICA );
1374 $user = $actorStore->getActorById( $actorId, $db );
1375
1376 if ( !$user ) {
1377 throw new RuntimeException( "User not found by actor ID: $actorId" );
1378 }
1379 } elseif ( $userName !== null ) {
1380 $user = $actorStore->getUserIdentityByName( $userName );
1381
1382 if ( !$user ) {
1383 throw new RuntimeException( "User not found by name: $userName" );
1384 }
1385 } else {
1386 throw new RuntimeException( 'At least one of user ID, actor ID or user name must be given' );
1387 }
1388
1389 return $user;
1390 }
1391}
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.
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 newLogEntry( $timestamp, $logPage, $user, $actionComment, $ip, $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='', $revId=0, $isPatrollable=false, $forceBotFlag=null, $deleted=0)
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.
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:684
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