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 $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.
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