MediaWiki REL1_37
RecentChange.php
Go to the documentation of this file.
1<?php
32use Wikimedia\Assert\Assert;
33use Wikimedia\IPUtils;
34
80class RecentChange implements Taggable {
82
83 // Constants for the rc_source field. Extensions may also have
84 // their own source constants.
85 public const SRC_EDIT = 'mw.edit';
86 public const SRC_NEW = 'mw.new';
87 public const SRC_LOG = 'mw.log';
88 public const SRC_EXTERNAL = 'mw.external'; // obsolete
89 public const SRC_CATEGORIZE = 'mw.categorize';
90
91 public const PRC_UNPATROLLED = 0;
92 public const PRC_PATROLLED = 1;
93 public const PRC_AUTOPATROLLED = 2;
94
98 public const SEND_NONE = true;
99
103 public const SEND_FEED = false;
104
106 public $mAttribs = [];
107 public $mExtra = [];
108
112 private $mPage = null;
113
117 private $mPerformer = null;
118
119 public $numberofWatchingusers = 0; # Dummy to prevent error message in SpecialRecentChangesLinked
121
126
130 public $counter = -1;
131
135 private $tags = [];
136
140 private $editResult = null;
141
142 private const CHANGE_TYPES = [
143 'edit' => RC_EDIT,
144 'new' => RC_NEW,
145 'log' => RC_LOG,
146 'external' => RC_EXTERNAL,
147 'categorize' => RC_CATEGORIZE,
148 ];
149
150 # Factory methods
151
156 public static function newFromRow( $row ) {
157 $rc = new RecentChange;
158 $rc->loadFromRow( $row );
159
160 return $rc;
161 }
162
170 public static function parseToRCType( $type ) {
171 if ( is_array( $type ) ) {
172 $retval = [];
173 foreach ( $type as $t ) {
174 $retval[] = self::parseToRCType( $t );
175 }
176
177 return $retval;
178 }
179
180 if ( !array_key_exists( $type, self::CHANGE_TYPES ) ) {
181 throw new MWException( "Unknown type '$type'" );
182 }
183 return self::CHANGE_TYPES[$type];
184 }
185
192 public static function parseFromRCType( $rcType ) {
193 return array_search( $rcType, self::CHANGE_TYPES, true ) ?: "$rcType";
194 }
195
203 public static function getChangeTypes() {
204 return array_keys( self::CHANGE_TYPES );
205 }
206
213 public static function newFromId( $rcid ) {
214 return self::newFromConds( [ 'rc_id' => $rcid ], __METHOD__ );
215 }
216
226 public static function newFromConds(
227 $conds,
228 $fname = __METHOD__,
229 $dbType = DB_REPLICA
230 ) {
231 $db = wfGetDB( $dbType );
232 $rcQuery = self::getQueryInfo();
233 $row = $db->selectRow(
234 $rcQuery['tables'], $rcQuery['fields'], $conds, $fname, [], $rcQuery['joins']
235 );
236 if ( $row !== false ) {
237 return self::newFromRow( $row );
238 } else {
239 return null;
240 }
241 }
242
257 public static function getQueryInfo() {
258 $commentQuery = CommentStore::getStore()->getJoin( 'rc_comment' );
259 return [
260 'tables' => [
261 'recentchanges',
262 'recentchanges_actor' => 'actor'
263 ] + $commentQuery['tables'],
264 'fields' => [
265 'rc_id',
266 'rc_timestamp',
267 'rc_namespace',
268 'rc_title',
269 'rc_minor',
270 'rc_bot',
271 'rc_new',
272 'rc_cur_id',
273 'rc_this_oldid',
274 'rc_last_oldid',
275 'rc_type',
276 'rc_source',
277 'rc_patrolled',
278 'rc_ip',
279 'rc_old_len',
280 'rc_new_len',
281 'rc_deleted',
282 'rc_logid',
283 'rc_log_type',
284 'rc_log_action',
285 'rc_params',
286 'rc_actor',
287 'rc_user' => 'recentchanges_actor.actor_user',
288 'rc_user_text' => 'recentchanges_actor.actor_name',
289 ] + $commentQuery['fields'],
290 'joins' => [
291 'recentchanges_actor' => [ 'JOIN', 'actor_id=rc_actor' ]
292 ] + $commentQuery['joins'],
293 ];
294 }
295
296 public function __construct() {
298 'mTitle',
299 '1.37',
300 function () {
301 return Title::castFromPageReference( $this->mPage );
302 },
303 function ( ?Title $title ) {
304 $this->mPage = $title;
305 }
306 );
307 }
308
309 # Accessors
310
314 public function setAttribs( $attribs ) {
315 $this->mAttribs = $attribs;
316 }
317
321 public function setExtra( $extra ) {
322 $this->mExtra = $extra;
323 }
324
329 public function getTitle() {
330 $this->mPage = Title::castFromPageReference( $this->getPage() );
331 return $this->mPage ?: Title::makeTitle( NS_SPECIAL, 'BadTitle' );
332 }
333
338 public function getPage(): ?PageReference {
339 if ( !$this->mPage ) {
340 // NOTE: As per the 1.36 release, we always provide rc_title,
341 // even in cases where it doesn't really make sense.
342 // In the future, rc_title may be nullable, or we may use
343 // empty strings in entries that do not refer to a page.
344 if ( ( $this->mAttribs['rc_title'] ?? '' ) === '' ) {
345 return null;
346 }
347
348 // XXX: We could use rc_cur_id to create a PageIdentityValue,
349 // at least if it's not a special page.
350 // However, newForCategorization() puts the ID of the categorized page into
351 // rc_cur_id, but the title of the category page into rc_title.
352 $this->mPage = new PageReferenceValue(
353 (int)$this->mAttribs['rc_namespace'],
354 $this->mAttribs['rc_title'],
355 PageReference::LOCAL
356 );
357 }
358
359 return $this->mPage;
360 }
361
368 public function getPerformer(): User {
369 wfDeprecated( __METHOD__, '1.36' );
370 if ( !$this->mPerformer instanceof User ) {
371 $this->mPerformer = User::newFromIdentity( $this->getPerformerIdentity() );
372 }
373
374 return $this->mPerformer;
375 }
376
385 if ( !$this->mPerformer ) {
386 $this->mPerformer = $this->getUserIdentityFromAnyId(
387 $this->mAttribs['rc_user'] ?? null,
388 $this->mAttribs['rc_user_text'] ?? null,
389 $this->mAttribs['rc_actor'] ?? null
390 );
391 }
392
393 return $this->mPerformer;
394 }
395
405 public function save( $send = self::SEND_FEED ) {
407
408 $dbw = wfGetDB( DB_PRIMARY );
409 if ( !is_array( $this->mExtra ) ) {
410 $this->mExtra = [];
411 }
412
413 if ( !$wgPutIPinRC ) {
414 $this->mAttribs['rc_ip'] = '';
415 }
416
417 # Strict mode fixups (not-NULL fields)
418 foreach ( [ 'minor', 'bot', 'new', 'patrolled', 'deleted' ] as $field ) {
419 $this->mAttribs["rc_$field"] = (int)$this->mAttribs["rc_$field"];
420 }
421 # ...more fixups (NULL fields)
422 foreach ( [ 'old_len', 'new_len' ] as $field ) {
423 $this->mAttribs["rc_$field"] = isset( $this->mAttribs["rc_$field"] )
424 ? (int)$this->mAttribs["rc_$field"]
425 : null;
426 }
427
428 # If our database is strict about IP addresses, use NULL instead of an empty string
429 $strictIPs = $dbw->getType() === 'postgres'; // legacy
430 if ( $strictIPs && $this->mAttribs['rc_ip'] == '' ) {
431 unset( $this->mAttribs['rc_ip'] );
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
503 $userFactory = MediaWikiServices::getInstance()->getUserFactory();
504 $editor = $userFactory->newFromUserIdentity( $this->getPerformerIdentity() );
505 $page = $this->getPage();
506 $title = Title::castFromPageReference( $page );
507
508 // Never send an RC notification email about categorization changes
509 if (
510 $title &&
511 Hooks::runner()->onAbortEmailNotification( $editor, $title, $this ) &&
512 $this->mAttribs['rc_type'] != RC_CATEGORIZE
513 ) {
514 // @FIXME: This would be better as an extension hook
515 // Send emails or email jobs once this row is safely committed
516 $dbw->onTransactionCommitOrIdle(
517 function () use ( $editor, $title ) {
518 $enotif = new EmailNotification();
519 $enotif->notifyOnPageChange(
520 $editor,
521 $title,
522 $this->mAttribs['rc_timestamp'],
523 $this->mAttribs['rc_comment'],
524 $this->mAttribs['rc_minor'],
525 $this->mAttribs['rc_last_oldid'],
526 $this->mExtra['pageStatus']
527 );
528 },
529 __METHOD__
530 );
531 }
532 }
533
534 $jobs = [];
535 // Flush old entries from the `recentchanges` table
536 if ( mt_rand( 0, 9 ) == 0 ) {
538 }
539 // Update the cached list of active users
540 if ( $this->mAttribs['rc_user'] > 0 ) {
542 }
543 JobQueueGroup::singleton()->lazyPush( $jobs );
544 }
545
550 public function notifyRCFeeds( array $feeds = null ) {
551 global $wgRCFeeds;
552 if ( $feeds === null ) {
553 $feeds = $wgRCFeeds;
554 }
555
556 $performer = $this->getPerformerIdentity();
557
558 foreach ( $feeds as $params ) {
559 $params += [
560 'omit_bots' => false,
561 'omit_anon' => false,
562 'omit_user' => false,
563 'omit_minor' => false,
564 'omit_patrolled' => false,
565 ];
566
567 if (
568 ( $params['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
569 ( $params['omit_anon'] && !$performer->isRegistered() ) ||
570 ( $params['omit_user'] && $performer->isRegistered() ) ||
571 ( $params['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
572 ( $params['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
573 $this->mAttribs['rc_type'] == RC_EXTERNAL
574 ) {
575 continue;
576 }
577
578 $actionComment = $this->mExtra['actionCommentIRC'] ?? null;
579
580 $feed = RCFeed::factory( $params );
581 $feed->notify( $this, $actionComment );
582 }
583 }
584
593 public static function getEngine( $uri, $params = [] ) {
594 // TODO: Merge into RCFeed::factory().
595 global $wgRCEngines;
596 $scheme = parse_url( $uri, PHP_URL_SCHEME );
597 if ( !$scheme ) {
598 throw new MWException( "Invalid RCFeed uri: '$uri'" );
599 }
600 if ( !isset( $wgRCEngines[$scheme] ) ) {
601 throw new MWException( "Unknown RCFeedEngine scheme: '$scheme'" );
602 }
603 if ( defined( 'MW_PHPUNIT_TEST' ) && is_object( $wgRCEngines[$scheme] ) ) {
604 return $wgRCEngines[$scheme];
605 }
606 // TODO For non test a object could be here?
607 // @phan-suppress-next-line PhanTypeExpectedObjectOrClassName
608 return new $wgRCEngines[$scheme]( $params );
609 }
610
622 public function doMarkPatrolled( Authority $performer, $auto = false, $tags = null ) {
624
625 // Fix up $tags so that the MarkPatrolled hook below always gets an array
626 if ( $tags === null ) {
627 $tags = [];
628 } elseif ( is_string( $tags ) ) {
629 $tags = [ $tags ];
630 }
631
632 $status = PermissionStatus::newEmpty();
633 // If recentchanges patrol is disabled, only new pages or new file versions
634 // can be patrolled, provided the appropriate config variable is set
635 if ( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) &&
636 ( !$wgUseFilePatrol || !( $this->getAttribute( 'rc_type' ) == RC_LOG &&
637 $this->getAttribute( 'rc_log_type' ) == 'upload' ) ) ) {
638 $status->fatal( 'rcpatroldisabled' );
639 }
640 // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
641 $performer->authorizeWrite( $auto ? 'autopatrol' : 'patrol', $this->getTitle(), $status );
642 $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
643 if ( !Hooks::runner()->onMarkPatrolled(
644 $this->getAttribute( 'rc_id' ), $user, false, $auto, $tags )
645 ) {
646 $status->fatal( 'hookaborted' );
647 }
648 // Users without the 'autopatrol' right can't patrol their own revisions
649 if ( $performer->getUser()->getName() === $this->getAttribute( 'rc_user_text' ) &&
650 !$performer->isAllowed( 'autopatrol' )
651 ) {
652 $status->fatal( 'markedaspatrollederror-noautopatrol' );
653 }
654 if ( !$status->isGood() ) {
655 return $status->toLegacyErrorArray();
656 }
657 // If the change was patrolled already, do nothing
658 if ( $this->getAttribute( 'rc_patrolled' ) ) {
659 return [];
660 }
661 // Actually set the 'patrolled' flag in RC
662 $this->reallyMarkPatrolled();
663 // Log this patrol event
664 PatrolLog::record( $this, $auto, $performer->getUser(), $tags );
665
666 Hooks::runner()->onMarkPatrolledComplete(
667 $this->getAttribute( 'rc_id' ), $user, false, $auto );
668
669 return [];
670 }
671
676 public function reallyMarkPatrolled() {
677 $dbw = wfGetDB( DB_PRIMARY );
678 $dbw->update(
679 'recentchanges',
680 [
681 'rc_patrolled' => self::PRC_PATROLLED
682 ],
683 [
684 'rc_id' => $this->getAttribute( 'rc_id' )
685 ],
686 __METHOD__
687 );
688 // Invalidate the page cache after the page has been patrolled
689 // to make sure that the Patrol link isn't visible any longer!
690 $this->getTitle()->invalidateCache();
691
692 // Enqueue a reverted tag update (in case the edit was a revert)
693 $revisionId = $this->getAttribute( 'rc_this_oldid' );
694 if ( $revisionId ) {
695 $revertedTagUpdateManager =
696 MediaWikiServices::getInstance()->getRevertedTagUpdateManager();
697 $revertedTagUpdateManager->approveRevertedTagForRevision( $revisionId );
698 }
699
700 return $dbw->affectedRows();
701 }
702
727 public static function notifyEdit(
728 $timestamp, $page, $minor, $user, $comment, $oldId, $lastTimestamp,
729 $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0,
730 $tags = [], EditResult $editResult = null
731 ) {
732 Assert::parameter( $page->exists(), '$page', 'must represent an existing page' );
733
734 $rc = new RecentChange;
735 $rc->mPage = $page;
736 $rc->mPerformer = $user;
737 $rc->mAttribs = [
738 'rc_timestamp' => $timestamp,
739 'rc_namespace' => $page->getNamespace(),
740 'rc_title' => $page->getDBkey(),
741 'rc_type' => RC_EDIT,
742 'rc_source' => self::SRC_EDIT,
743 'rc_minor' => $minor ? 1 : 0,
744 'rc_cur_id' => $page->getId(),
745 'rc_user' => $user->getId(),
746 'rc_user_text' => $user->getName(),
747 'rc_comment' => &$comment,
748 'rc_comment_text' => &$comment,
749 'rc_comment_data' => null,
750 'rc_this_oldid' => (int)$newId,
751 'rc_last_oldid' => $oldId,
752 'rc_bot' => $bot ? 1 : 0,
753 'rc_ip' => self::checkIPAddress( $ip ),
754 'rc_patrolled' => intval( $patrol ),
755 'rc_new' => 0, # obsolete
756 'rc_old_len' => $oldSize,
757 'rc_new_len' => $newSize,
758 'rc_deleted' => 0,
759 'rc_logid' => 0,
760 'rc_log_type' => null,
761 'rc_log_action' => '',
762 'rc_params' => ''
763 ];
764
765 // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
766 $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
767
768 $rc->mExtra = [
769 'prefixedDBkey' => $formatter->getPrefixedDBkey( $page ),
770 'lastTimestamp' => $lastTimestamp,
771 'oldSize' => $oldSize,
772 'newSize' => $newSize,
773 'pageStatus' => 'changed'
774 ];
775
776 DeferredUpdates::addCallableUpdate(
777 static function () use ( $rc, $tags, $editResult ) {
778 $rc->addTags( $tags );
779 $rc->setEditResult( $editResult );
780 $rc->save();
781 },
782 DeferredUpdates::POSTSEND,
784 );
785
786 return $rc;
787 }
788
808 public static function notifyNew(
809 $timestamp,
810 $page, $minor, $user, $comment, $bot,
811 $ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = []
812 ) {
813 Assert::parameter( $page->exists(), '$page', 'must represent an existing page' );
814
815 $rc = new RecentChange;
816 $rc->mPage = $page;
817 $rc->mPerformer = $user;
818 $rc->mAttribs = [
819 'rc_timestamp' => $timestamp,
820 'rc_namespace' => $page->getNamespace(),
821 'rc_title' => $page->getDBkey(),
822 'rc_type' => RC_NEW,
823 'rc_source' => self::SRC_NEW,
824 'rc_minor' => $minor ? 1 : 0,
825 'rc_cur_id' => $page->getId(),
826 'rc_user' => $user->getId(),
827 'rc_user_text' => $user->getName(),
828 'rc_comment' => &$comment,
829 'rc_comment_text' => &$comment,
830 'rc_comment_data' => null,
831 'rc_this_oldid' => (int)$newId,
832 'rc_last_oldid' => 0,
833 'rc_bot' => $bot ? 1 : 0,
834 'rc_ip' => self::checkIPAddress( $ip ),
835 'rc_patrolled' => intval( $patrol ),
836 'rc_new' => 1, # obsolete
837 'rc_old_len' => 0,
838 'rc_new_len' => $size,
839 'rc_deleted' => 0,
840 'rc_logid' => 0,
841 'rc_log_type' => null,
842 'rc_log_action' => '',
843 'rc_params' => ''
844 ];
845
846 // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
847 $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
848
849 $rc->mExtra = [
850 'prefixedDBkey' => $formatter->getPrefixedDBkey( $page ),
851 'lastTimestamp' => 0,
852 'oldSize' => 0,
853 'newSize' => $size,
854 'pageStatus' => 'created'
855 ];
856
857 DeferredUpdates::addCallableUpdate(
858 static function () use ( $rc, $tags ) {
859 $rc->addTags( $tags );
860 $rc->save();
861 },
862 DeferredUpdates::POSTSEND,
864 );
865
866 return $rc;
867 }
868
885 public static function notifyLog( $timestamp,
886 $logPage, $user, $actionComment, $ip, $type,
887 $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
888 ) {
889 global $wgLogRestrictions;
890
891 # Don't add private logs to RC!
892 if ( isset( $wgLogRestrictions[$type] ) && $wgLogRestrictions[$type] != '*' ) {
893 return false;
894 }
895 $rc = self::newLogEntry( $timestamp,
896 $logPage, $user, $actionComment, $ip, $type, $action,
897 $target, $logComment, $params, $newId, $actionCommentIRC );
898 $rc->save();
899
900 return true;
901 }
902
921 public static function newLogEntry( $timestamp,
922 $logPage, $user, $actionComment, $ip,
923 $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '',
924 $revId = 0, $isPatrollable = false ) {
925 global $wgRequest;
926 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
927
928 # # Get pageStatus for email notification
929 switch ( $type . '-' . $action ) {
930 case 'delete-delete':
931 case 'delete-delete_redir':
932 case 'delete-delete_redir2':
933 $pageStatus = 'deleted';
934 break;
935 case 'move-move':
936 case 'move-move_redir':
937 $pageStatus = 'moved';
938 break;
939 case 'delete-restore':
940 $pageStatus = 'restored';
941 break;
942 case 'upload-upload':
943 $pageStatus = 'created';
944 break;
945 case 'upload-overwrite':
946 default:
947 $pageStatus = 'changed';
948 break;
949 }
950
951 // Allow unpatrolled status for patrollable log entries
952 $canAutopatrol = $permissionManager->userHasRight( $user, 'autopatrol' );
953 $markPatrolled = $isPatrollable ? $canAutopatrol : true;
954
955 if ( $target instanceof PageIdentity && $target->canExist() ) {
956 $pageId = $target->getId();
957 } else {
958 $pageId = 0;
959 }
960
961 $rc = new RecentChange;
962 $rc->mPage = $target;
963 $rc->mPerformer = $user;
964 $rc->mAttribs = [
965 'rc_timestamp' => $timestamp,
966 'rc_namespace' => $target->getNamespace(),
967 'rc_title' => $target->getDBkey(),
968 'rc_type' => RC_LOG,
969 'rc_source' => self::SRC_LOG,
970 'rc_minor' => 0,
971 'rc_cur_id' => $pageId,
972 'rc_user' => $user->getId(),
973 'rc_user_text' => $user->getName(),
974 'rc_comment' => &$logComment,
975 'rc_comment_text' => &$logComment,
976 'rc_comment_data' => null,
977 'rc_this_oldid' => (int)$revId,
978 'rc_last_oldid' => 0,
979 'rc_bot' => $permissionManager->userHasRight( $user, 'bot' ) ?
980 (int)$wgRequest->getBool( 'bot', true ) : 0,
981 'rc_ip' => self::checkIPAddress( $ip ),
982 'rc_patrolled' => $markPatrolled ? self::PRC_AUTOPATROLLED : self::PRC_UNPATROLLED,
983 'rc_new' => 0, # obsolete
984 'rc_old_len' => null,
985 'rc_new_len' => null,
986 'rc_deleted' => 0,
987 'rc_logid' => $newId,
988 'rc_log_type' => $type,
989 'rc_log_action' => $action,
990 'rc_params' => $params
991 ];
992
993 // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
994 $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
995
996 $rc->mExtra = [
997 // XXX: This does not correspond to rc_namespace/rc_title/rc_cur_id.
998 // Is that intentional? For all other kinds of RC entries, prefixedDBkey
999 // matches rc_namespace/rc_title. Do we even need $logPage?
1000 'prefixedDBkey' => $formatter->getPrefixedDBkey( $logPage ),
1001 'lastTimestamp' => 0,
1002 'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
1003 'pageStatus' => $pageStatus,
1004 'actionCommentIRC' => $actionCommentIRC
1005 ];
1006
1007 return $rc;
1008 }
1009
1031 public static function newForCategorization(
1032 $timestamp,
1033 PageIdentity $categoryTitle,
1034 ?UserIdentity $user,
1035 $comment,
1036 PageIdentity $pageTitle,
1037 $oldRevId,
1038 $newRevId,
1039 $lastTimestamp,
1040 $bot,
1041 $ip = '',
1042 $deleted = 0,
1043 $added = null
1044 ) {
1045 // Done in a backwards compatible way.
1046 $categoryWikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()
1047 ->newFromTitle( $categoryTitle );
1048
1049 '@phan-var WikiCategoryPage $categoryWikiPage';
1050 $params = [
1051 'hidden-cat' => $categoryWikiPage->isHidden()
1052 ];
1053 if ( $added !== null ) {
1054 $params['added'] = $added;
1055 }
1056
1057 if ( !$user ) {
1058 // XXX: when and why do we need this?
1059 $user = MediaWikiServices::getInstance()->getActorStore()->getUnknownActor();
1060 }
1061
1062 $rc = new RecentChange;
1063 $rc->mPage = $categoryTitle;
1064 $rc->mPerformer = $user;
1065 $rc->mAttribs = [
1066 'rc_timestamp' => MWTimestamp::convert( TS_MW, $timestamp ),
1067 'rc_namespace' => $categoryTitle->getNamespace(),
1068 'rc_title' => $categoryTitle->getDBkey(),
1069 'rc_type' => RC_CATEGORIZE,
1070 'rc_source' => self::SRC_CATEGORIZE,
1071 'rc_minor' => 0,
1072 // XXX: rc_cur_id does not correspond to rc_namespace/rc_title.
1073 // They refer to different pages. Is that intentional?
1074 'rc_cur_id' => $pageTitle->getId(),
1075 'rc_user' => $user->getId(),
1076 'rc_user_text' => $user->getName(),
1077 'rc_comment' => &$comment,
1078 'rc_comment_text' => &$comment,
1079 'rc_comment_data' => null,
1080 'rc_this_oldid' => (int)$newRevId,
1081 'rc_last_oldid' => $oldRevId,
1082 'rc_bot' => $bot ? 1 : 0,
1083 'rc_ip' => self::checkIPAddress( $ip ),
1084 'rc_patrolled' => self::PRC_AUTOPATROLLED, // Always patrolled, just like log entries
1085 'rc_new' => 0, # obsolete
1086 'rc_old_len' => null,
1087 'rc_new_len' => null,
1088 'rc_deleted' => $deleted,
1089 'rc_logid' => 0,
1090 'rc_log_type' => null,
1091 'rc_log_action' => '',
1092 'rc_params' => serialize( $params )
1093 ];
1094
1095 // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting.
1096 $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
1097
1098 $rc->mExtra = [
1099 'prefixedDBkey' => $formatter->getPrefixedDBkey( $categoryTitle ),
1100 'lastTimestamp' => $lastTimestamp,
1101 'oldSize' => 0,
1102 'newSize' => 0,
1103 'pageStatus' => 'changed'
1104 ];
1105
1106 return $rc;
1107 }
1108
1117 public function getParam( $name ) {
1118 $params = $this->parseParams();
1119 return $params[$name] ?? null;
1120 }
1121
1127 public function loadFromRow( $row ) {
1128 $this->mAttribs = get_object_vars( $row );
1129 $this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
1130 // rc_deleted MUST be set
1131 $this->mAttribs['rc_deleted'] = $row->rc_deleted;
1132
1133 if ( isset( $this->mAttribs['rc_ip'] ) ) {
1134 // Clean up CIDRs for Postgres per T164898. ("127.0.0.1" casts to "127.0.0.1/32")
1135 $n = strpos( $this->mAttribs['rc_ip'], '/' );
1136 if ( $n !== false ) {
1137 $this->mAttribs['rc_ip'] = substr( $this->mAttribs['rc_ip'], 0, $n );
1138 }
1139 }
1140
1141 $comment = CommentStore::getStore()
1142 // Legacy because $row may have come from self::selectFields()
1143 ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $row, true )
1144 ->text;
1145 $this->mAttribs['rc_comment'] = &$comment;
1146 $this->mAttribs['rc_comment_text'] = &$comment;
1147 $this->mAttribs['rc_comment_data'] = null;
1148
1149 $this->mPerformer = $this->getUserIdentityFromAnyId(
1150 $row->rc_user ?? null,
1151 $row->rc_user_text ?? null,
1152 $row->rc_actor ?? null
1153 );
1154 $this->mAttribs['rc_user'] = $this->mPerformer->getId();
1155 $this->mAttribs['rc_user_text'] = $this->mPerformer->getName();
1156
1157 // Watchlist expiry.
1158 if ( isset( $row->we_expiry ) && $row->we_expiry ) {
1159 $this->watchlistExpiry = wfTimestamp( TS_MW, $row->we_expiry );
1160 }
1161 }
1162
1169 public function getAttribute( $name ) {
1170 if ( $name === 'rc_comment' ) {
1171 return CommentStore::getStore()
1172 ->getComment( 'rc_comment', $this->mAttribs, true )->text;
1173 }
1174
1175 if ( $name === 'rc_user' || $name === 'rc_user_text' || $name === 'rc_actor' ) {
1176 $user = $this->getPerformerIdentity();
1177
1178 if ( $name === 'rc_user' ) {
1179 return $user->getId();
1180 }
1181 if ( $name === 'rc_user_text' ) {
1182 return $user->getName();
1183 }
1184 if ( $name === 'rc_actor' ) {
1185 // NOTE: rc_actor exists in the database, but application logic should not use it.
1186 wfDeprecatedMsg( 'Accessing deprecated field rc_actor', '1.36' );
1187 $actorStore = MediaWikiServices::getInstance()->getActorStore();
1188 $db = wfGetDB( DB_REPLICA );
1189 return $actorStore->findActorId( $user, $db );
1190 }
1191 }
1192
1193 return $this->mAttribs[$name] ?? null;
1194 }
1195
1199 public function getAttributes() {
1200 return $this->mAttribs;
1201 }
1202
1209 public function diffLinkTrail( $forceCur ) {
1210 if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
1211 $trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
1212 "&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
1213 if ( $forceCur ) {
1214 $trail .= '&diff=0';
1215 } else {
1216 $trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
1217 }
1218 } else {
1219 $trail = '';
1220 }
1221
1222 return $trail;
1223 }
1224
1232 public function getCharacterDifference( $old = 0, $new = 0 ) {
1233 if ( $old === 0 ) {
1234 $old = $this->mAttribs['rc_old_len'];
1235 }
1236 if ( $new === 0 ) {
1237 $new = $this->mAttribs['rc_new_len'];
1238 }
1239 if ( $old === null || $new === null ) {
1240 return '';
1241 }
1242
1243 return ChangesList::showCharacterDifference( $old, $new );
1244 }
1245
1246 private static function checkIPAddress( $ip ) {
1247 global $wgRequest;
1248 if ( $ip ) {
1249 if ( !IPUtils::isIPAddress( $ip ) ) {
1250 throw new MWException( "Attempt to write \"" . $ip .
1251 "\" as an IP address into recent changes" );
1252 }
1253 } else {
1254 $ip = $wgRequest->getIP();
1255 if ( !$ip ) {
1256 $ip = '';
1257 }
1258 }
1259
1260 return $ip;
1261 }
1262
1272 public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
1273 global $wgRCMaxAge;
1274
1275 return wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $wgRCMaxAge;
1276 }
1277
1285 public function parseParams() {
1286 $rcParams = $this->getAttribute( 'rc_params' );
1287
1288 Wikimedia\suppressWarnings();
1289 $unserializedParams = unserialize( $rcParams );
1290 Wikimedia\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
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)
$wgRCFeeds
Configuration for feeds to which notifications about recent changes will be sent.
$wgPutIPinRC
Log IP addresses in the recentchanges table; can be accessed only by extensions (e....
$wgUseFilePatrol
Use file patrolling to check new files on Special:Newfiles.
$wgLogRestrictions
This restricts log access to those who have a certain right Users without this will not see it in the...
$wgShowUpdatedMarker
Show "Updated (since my last visit)" marker in RC view, watchlist and history view for watched pages ...
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
$wgRCMaxAge
Recentchanges items are periodically purged; entries older than this many seconds will go.
$wgRCEngines
Used by RecentChange::getEngine to find the correct engine for a given URI scheme.
const RC_NEW
Definition Defines.php:116
const NS_SPECIAL
Definition Defines.php:53
const RC_LOG
Definition Defines.php:117
const RC_EXTERNAL
Definition Defines.php:118
const RC_EDIT
Definition Defines.php:115
const RC_CATEGORIZE
Definition Defines.php:119
deprecatePublicPropertyFallback(string $property, string $version, callable $getter, ?callable $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.
$wgRequest
Definition Setup.php:702
$wgUseEnotif
Definition Setup.php:481
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:88
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.
MediaWikiServices is the service locator for the application scope of MediaWiki.
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:46
Utility class for creating new RC entries.
setExtra( $extra)
const PRC_UNPATROLLED
static getEngine( $uri, $params=[])
static parseToRCType( $type)
Parsing text to RC_* constants.
setEditResult(?EditResult $editResult)
Sets the EditResult associated with the edit.
static notifyEdit( $timestamp, $page, $minor, $user, $comment, $oldId, $lastTimestamp, $bot, $ip='', $oldSize=0, $newSize=0, $newId=0, $patrol=0, $tags=[], EditResult $editResult=null)
Makes an entry in the database corresponding to an edit.
reallyMarkPatrolled()
Mark this RecentChange patrolled, without error checking.
static newForCategorization( $timestamp, PageIdentity $categoryTitle, ?UserIdentity $user, $comment, PageIdentity $pageTitle, $oldRevId, $newRevId, $lastTimestamp, $bot, $ip='', $deleted=0, $added=null)
Constructs a RecentChange object for the given categorization This does not call save() on the object...
static newFromRow( $row)
doMarkPatrolled(Authority $performer, $auto=false, $tags=null)
Mark this RecentChange as patrolled.
parseParams()
Parses and returns the rc_params attribute.
const SRC_CATEGORIZE
getPerformer()
Get the User object of the person who performed this change.
static checkIPAddress( $ip)
static notifyNew( $timestamp, $page, $minor, $user, $comment, $bot, $ip='', $size=0, $newId=0, $patrol=0, $tags=[])
Makes an entry in the database corresponding to page creation.
static getChangeTypes()
Get an array of all change types.
static isInRCLifespan( $timestamp, $tolerance=0)
Check whether the given timestamp is new enough to have a RC row with a given tolerance as the recent...
int $counter
Line number of recent change.
UserIdentity null $mPerformer
save( $send=self::SEND_FEED)
Writes the data in this object to the database.
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new recentchanges object.
setAttribs( $attribs)
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
getCharacterDifference( $old=0, $new=0)
Returns the change size (HTML).
static parseFromRCType( $rcType)
Parsing RC_* constants to human-readable test.
EditResult null $editResult
EditResult associated with the edit.
const PRC_PATROLLED
notifyRCFeeds(array $feeds=null)
Notify all the feeds about the change.
PageReference null $mPage
array $tags
List of tags to apply.
getPerformerIdentity()
Get the UserIdentity of the client that performed this change.
getParam( $name)
Get a parameter value.
static newLogEntry( $timestamp, $logPage, $user, $actionComment, $ip, $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='', $revId=0, $isPatrollable=false)
addTags( $tags)
Tags to append to the recent change, and associated revision/log.
getUserIdentityFromAnyId( $userId, $userName, $actorId=null)
loadFromRow( $row)
Initialises the members of this object from a mysql row object.
static notifyLog( $timestamp, $logPage, $user, $actionComment, $ip, $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='')
getAttribute( $name)
Get an attribute value.
string null $watchlistExpiry
The expiry time, if this is a temporary watchlist item.
diffLinkTrail( $forceCur)
Gets the end part of the diff URL associated with this object Blank if no diff link should be display...
static newFromId( $rcid)
Obtain the recent change with a given rc_id value.
const PRC_AUTOPATROLLED
This is to display changes made to all articles linked in an article.
Represents a title within MediaWiki.
Definition Title.php:48
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:69
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:25
const DB_PRIMARY
Definition defines.php:27