MediaWiki REL1_34
RecentChange.php
Go to the documentation of this file.
1<?php
24
70class RecentChange implements Taggable {
71 // Constants for the rc_source field. Extensions may also have
72 // their own source constants.
73 const SRC_EDIT = 'mw.edit';
74 const SRC_NEW = 'mw.new';
75 const SRC_LOG = 'mw.log';
76 const SRC_EXTERNAL = 'mw.external'; // obsolete
77 const SRC_CATEGORIZE = 'mw.categorize';
78
79 const PRC_UNPATROLLED = 0;
80 const PRC_PATROLLED = 1;
82
86 const SEND_NONE = true;
87
91 const SEND_FEED = false;
92
94 public $mAttribs = [];
95 public $mExtra = [];
96
100 public $mTitle = false;
101
105 private $mPerformer = false;
106
107 public $numberofWatchingusers = 0; # Dummy to prevent error message in SpecialRecentChangesLinked
109
113 public $counter = -1;
114
118 private $tags = [];
119
123 private static $changeTypes = [
124 'edit' => RC_EDIT,
125 'new' => RC_NEW,
126 'log' => RC_LOG,
127 'external' => RC_EXTERNAL,
128 'categorize' => RC_CATEGORIZE,
129 ];
130
131 # Factory methods
132
137 public static function newFromRow( $row ) {
138 $rc = new RecentChange;
139 $rc->loadFromRow( $row );
140
141 return $rc;
142 }
143
151 public static function parseToRCType( $type ) {
152 if ( is_array( $type ) ) {
153 $retval = [];
154 foreach ( $type as $t ) {
155 $retval[] = self::parseToRCType( $t );
156 }
157
158 return $retval;
159 }
160
161 if ( !array_key_exists( $type, self::$changeTypes ) ) {
162 throw new MWException( "Unknown type '$type'" );
163 }
164 return self::$changeTypes[$type];
165 }
166
173 public static function parseFromRCType( $rcType ) {
174 return array_search( $rcType, self::$changeTypes, true ) ?: "$rcType";
175 }
176
184 public static function getChangeTypes() {
185 return array_keys( self::$changeTypes );
186 }
187
194 public static function newFromId( $rcid ) {
195 return self::newFromConds( [ 'rc_id' => $rcid ], __METHOD__ );
196 }
197
207 public static function newFromConds(
208 $conds,
209 $fname = __METHOD__,
210 $dbType = DB_REPLICA
211 ) {
212 $db = wfGetDB( $dbType );
213 $rcQuery = self::getQueryInfo();
214 $row = $db->selectRow(
215 $rcQuery['tables'], $rcQuery['fields'], $conds, $fname, [], $rcQuery['joins']
216 );
217 if ( $row !== false ) {
218 return self::newFromRow( $row );
219 } else {
220 return null;
221 }
222 }
223
233 public static function getQueryInfo() {
234 $commentQuery = CommentStore::getStore()->getJoin( 'rc_comment' );
235 $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
236 return [
237 'tables' => [ 'recentchanges' ] + $commentQuery['tables'] + $actorQuery['tables'],
238 'fields' => [
239 'rc_id',
240 'rc_timestamp',
241 'rc_namespace',
242 'rc_title',
243 'rc_minor',
244 'rc_bot',
245 'rc_new',
246 'rc_cur_id',
247 'rc_this_oldid',
248 'rc_last_oldid',
249 'rc_type',
250 'rc_source',
251 'rc_patrolled',
252 'rc_ip',
253 'rc_old_len',
254 'rc_new_len',
255 'rc_deleted',
256 'rc_logid',
257 'rc_log_type',
258 'rc_log_action',
259 'rc_params',
260 ] + $commentQuery['fields'] + $actorQuery['fields'],
261 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
262 ];
263 }
264
265 # Accessors
266
270 public function setAttribs( $attribs ) {
271 $this->mAttribs = $attribs;
272 }
273
277 public function setExtra( $extra ) {
278 $this->mExtra = $extra;
279 }
280
284 public function &getTitle() {
285 if ( $this->mTitle === false ) {
286 $this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
287 }
288
289 return $this->mTitle;
290 }
291
297 public function getPerformer() {
298 if ( $this->mPerformer === false ) {
299 if ( !empty( $this->mAttribs['rc_actor'] ) ) {
300 $this->mPerformer = User::newFromActorId( $this->mAttribs['rc_actor'] );
301 } elseif ( !empty( $this->mAttribs['rc_user'] ) ) {
302 $this->mPerformer = User::newFromId( $this->mAttribs['rc_user'] );
303 } elseif ( !empty( $this->mAttribs['rc_user_text'] ) ) {
304 $this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false );
305 } else {
306 throw new MWException( 'RecentChange object lacks rc_actor, rc_user, and rc_user_text' );
307 }
308 }
309
310 return $this->mPerformer;
311 }
312
322 public function save( $send = self::SEND_FEED ) {
324
325 $dbw = wfGetDB( DB_MASTER );
326 if ( !is_array( $this->mExtra ) ) {
327 $this->mExtra = [];
328 }
329
330 if ( !$wgPutIPinRC ) {
331 $this->mAttribs['rc_ip'] = '';
332 }
333
334 # Strict mode fixups (not-NULL fields)
335 foreach ( [ 'minor', 'bot', 'new', 'patrolled', 'deleted' ] as $field ) {
336 $this->mAttribs["rc_$field"] = (int)$this->mAttribs["rc_$field"];
337 }
338 # ...more fixups (NULL fields)
339 foreach ( [ 'old_len', 'new_len' ] as $field ) {
340 $this->mAttribs["rc_$field"] = isset( $this->mAttribs["rc_$field"] )
341 ? (int)$this->mAttribs["rc_$field"]
342 : null;
343 }
344
345 # If our database is strict about IP addresses, use NULL instead of an empty string
346 $strictIPs = $dbw->getType() === 'postgres'; // legacy
347 if ( $strictIPs && $this->mAttribs['rc_ip'] == '' ) {
348 unset( $this->mAttribs['rc_ip'] );
349 }
350
351 # Trim spaces on user supplied text
352 $this->mAttribs['rc_comment'] = trim( $this->mAttribs['rc_comment'] );
353
354 # Fixup database timestamps
355 $this->mAttribs['rc_timestamp'] = $dbw->timestamp( $this->mAttribs['rc_timestamp'] );
356
357 # # If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
358 if ( $this->mAttribs['rc_cur_id'] == 0 ) {
359 unset( $this->mAttribs['rc_cur_id'] );
360 }
361
362 $row = $this->mAttribs;
363
364 # Convert mAttribs['rc_comment'] for CommentStore
365 $comment = $row['rc_comment'];
366 unset( $row['rc_comment'], $row['rc_comment_text'], $row['rc_comment_data'] );
367 $row += CommentStore::getStore()->insert( $dbw, 'rc_comment', $comment );
368
369 # Convert mAttribs['rc_user'] etc for ActorMigration
370 $user = User::newFromAnyId(
371 $row['rc_user'] ?? null,
372 $row['rc_user_text'] ?? null,
373 $row['rc_actor'] ?? null
374 );
375 unset( $row['rc_user'], $row['rc_user_text'], $row['rc_actor'] );
376 $row += ActorMigration::newMigration()->getInsertValues( $dbw, 'rc_user', $user );
377
378 # Don't reuse an existing rc_id for the new row, if one happens to be
379 # set for some reason.
380 unset( $row['rc_id'] );
381
382 # Insert new row
383 $dbw->insert( 'recentchanges', $row, __METHOD__ );
384
385 # Set the ID
386 $this->mAttribs['rc_id'] = $dbw->insertId();
387
388 # Notify extensions
389 // Avoid PHP 7.1 warning from passing $this by reference
390 $rc = $this;
391 Hooks::run( 'RecentChange_save', [ &$rc ] );
392
393 if ( count( $this->tags ) ) {
394 ChangeTags::addTags( $this->tags, $this->mAttribs['rc_id'],
395 $this->mAttribs['rc_this_oldid'], $this->mAttribs['rc_logid'], null, $this );
396 }
397
398 if ( $send === self::SEND_FEED ) {
399 // Emit the change to external applications via RCFeeds.
400 $this->notifyRCFeeds();
401 }
402
403 # E-mail notifications
405 $editor = $this->getPerformer();
406 $title = $this->getTitle();
407
408 // Never send an RC notification email about categorization changes
409 if (
410 Hooks::run( 'AbortEmailNotification', [ $editor, $title, $this ] ) &&
411 $this->mAttribs['rc_type'] != RC_CATEGORIZE
412 ) {
413 // @FIXME: This would be better as an extension hook
414 // Send emails or email jobs once this row is safely committed
415 $dbw->onTransactionCommitOrIdle(
416 function () use ( $editor, $title ) {
417 $enotif = new EmailNotification();
418 $enotif->notifyOnPageChange(
419 $editor,
420 $title,
421 $this->mAttribs['rc_timestamp'],
422 $this->mAttribs['rc_comment'],
423 $this->mAttribs['rc_minor'],
424 $this->mAttribs['rc_last_oldid'],
425 $this->mExtra['pageStatus']
426 );
427 },
428 __METHOD__
429 );
430 }
431 }
432
433 // Update the cached list of active users
434 if ( $this->mAttribs['rc_user'] > 0 ) {
435 JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newCacheUpdateJob() );
436 }
437 }
438
443 public function notifyRCFeeds( array $feeds = null ) {
444 global $wgRCFeeds;
445 if ( $feeds === null ) {
446 $feeds = $wgRCFeeds;
447 }
448
449 $performer = $this->getPerformer();
450
451 foreach ( $feeds as $params ) {
452 $params += [
453 'omit_bots' => false,
454 'omit_anon' => false,
455 'omit_user' => false,
456 'omit_minor' => false,
457 'omit_patrolled' => false,
458 ];
459
460 if (
461 ( $params['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
462 ( $params['omit_anon'] && $performer->isAnon() ) ||
463 ( $params['omit_user'] && !$performer->isAnon() ) ||
464 ( $params['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
465 ( $params['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
466 $this->mAttribs['rc_type'] == RC_EXTERNAL
467 ) {
468 continue;
469 }
470
471 $actionComment = $this->mExtra['actionCommentIRC'] ?? null;
472
473 $feed = RCFeed::factory( $params );
474 $feed->notify( $this, $actionComment );
475 }
476 }
477
486 public static function getEngine( $uri, $params = [] ) {
487 // TODO: Merge into RCFeed::factory().
488 global $wgRCEngines;
489 $scheme = parse_url( $uri, PHP_URL_SCHEME );
490 if ( !$scheme ) {
491 throw new MWException( "Invalid RCFeed uri: '$uri'" );
492 }
493 if ( !isset( $wgRCEngines[$scheme] ) ) {
494 throw new MWException( "Unknown RCFeedEngine scheme: '$scheme'" );
495 }
496 if ( defined( 'MW_PHPUNIT_TEST' ) && is_object( $wgRCEngines[$scheme] ) ) {
497 return $wgRCEngines[$scheme];
498 }
499 return new $wgRCEngines[$scheme]( $params );
500 }
501
511 public static function markPatrolled( $change, $auto = false, $tags = null ) {
512 global $wgUser;
513
514 $change = $change instanceof RecentChange
515 ? $change
516 : self::newFromId( $change );
517
518 if ( !$change instanceof RecentChange ) {
519 return null;
520 }
521
522 return $change->doMarkPatrolled( $wgUser, $auto, $tags );
523 }
524
536 public function doMarkPatrolled( User $user, $auto = false, $tags = null ) {
538
539 // Fix up $tags so that the MarkPatrolled hook below always gets an array
540 if ( $tags === null ) {
541 $tags = [];
542 } elseif ( is_string( $tags ) ) {
543 $tags = [ $tags ];
544 }
545
546 $errors = [];
547 // If recentchanges patrol is disabled, only new pages or new file versions
548 // can be patrolled, provided the appropriate config variable is set
549 if ( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) &&
550 ( !$wgUseFilePatrol || !( $this->getAttribute( 'rc_type' ) == RC_LOG &&
551 $this->getAttribute( 'rc_log_type' ) == 'upload' ) ) ) {
552 $errors[] = [ 'rcpatroldisabled' ];
553 }
554 // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
555 $right = $auto ? 'autopatrol' : 'patrol';
556 $errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $user ) );
557 if ( !Hooks::run( 'MarkPatrolled',
558 [ $this->getAttribute( 'rc_id' ), &$user, false, $auto, &$tags ] )
559 ) {
560 $errors[] = [ 'hookaborted' ];
561 }
562 // Users without the 'autopatrol' right can't patrol their
563 // own revisions
564 if ( $user->getName() === $this->getAttribute( 'rc_user_text' ) &&
565 !MediaWikiServices::getInstance()->getPermissionManager()
566 ->userHasRight( $user, 'autopatrol' )
567 ) {
568 $errors[] = [ 'markedaspatrollederror-noautopatrol' ];
569 }
570 if ( $errors ) {
571 return $errors;
572 }
573 // If the change was patrolled already, do nothing
574 if ( $this->getAttribute( 'rc_patrolled' ) ) {
575 return [];
576 }
577 // Actually set the 'patrolled' flag in RC
578 $this->reallyMarkPatrolled();
579 // Log this patrol event
580 PatrolLog::record( $this, $auto, $user, $tags );
581
582 Hooks::run(
583 'MarkPatrolledComplete',
584 [ $this->getAttribute( 'rc_id' ), &$user, false, $auto ]
585 );
586
587 return [];
588 }
589
594 public function reallyMarkPatrolled() {
595 $dbw = wfGetDB( DB_MASTER );
596 $dbw->update(
597 'recentchanges',
598 [
599 'rc_patrolled' => self::PRC_PATROLLED
600 ],
601 [
602 'rc_id' => $this->getAttribute( 'rc_id' )
603 ],
604 __METHOD__
605 );
606 // Invalidate the page cache after the page has been patrolled
607 // to make sure that the Patrol link isn't visible any longer!
608 $this->getTitle()->invalidateCache();
609
610 return $dbw->affectedRows();
611 }
612
632 public static function notifyEdit(
633 $timestamp, $title, $minor, $user, $comment, $oldId, $lastTimestamp,
634 $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0,
635 $tags = []
636 ) {
637 $rc = new RecentChange;
638 $rc->mTitle = $title;
639 $rc->mPerformer = $user;
640 $rc->mAttribs = [
641 'rc_timestamp' => $timestamp,
642 'rc_namespace' => $title->getNamespace(),
643 'rc_title' => $title->getDBkey(),
644 'rc_type' => RC_EDIT,
645 'rc_source' => self::SRC_EDIT,
646 'rc_minor' => $minor ? 1 : 0,
647 'rc_cur_id' => $title->getArticleID(),
648 'rc_user' => $user->getId(),
649 'rc_user_text' => $user->getName(),
650 'rc_actor' => $user->getActorId(),
651 'rc_comment' => &$comment,
652 'rc_comment_text' => &$comment,
653 'rc_comment_data' => null,
654 'rc_this_oldid' => $newId,
655 'rc_last_oldid' => $oldId,
656 'rc_bot' => $bot ? 1 : 0,
657 'rc_ip' => self::checkIPAddress( $ip ),
658 'rc_patrolled' => intval( $patrol ),
659 'rc_new' => 0, # obsolete
660 'rc_old_len' => $oldSize,
661 'rc_new_len' => $newSize,
662 'rc_deleted' => 0,
663 'rc_logid' => 0,
664 'rc_log_type' => null,
665 'rc_log_action' => '',
666 'rc_params' => ''
667 ];
668
669 $rc->mExtra = [
670 'prefixedDBkey' => $title->getPrefixedDBkey(),
671 'lastTimestamp' => $lastTimestamp,
672 'oldSize' => $oldSize,
673 'newSize' => $newSize,
674 'pageStatus' => 'changed'
675 ];
676
677 DeferredUpdates::addCallableUpdate(
678 function () use ( $rc, $tags ) {
679 $rc->addTags( $tags );
680 $rc->save();
681 },
682 DeferredUpdates::POSTSEND,
684 );
685
686 return $rc;
687 }
688
706 public static function notifyNew(
707 $timestamp, $title, $minor, $user, $comment, $bot,
708 $ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = []
709 ) {
710 $rc = new RecentChange;
711 $rc->mTitle = $title;
712 $rc->mPerformer = $user;
713 $rc->mAttribs = [
714 'rc_timestamp' => $timestamp,
715 'rc_namespace' => $title->getNamespace(),
716 'rc_title' => $title->getDBkey(),
717 'rc_type' => RC_NEW,
718 'rc_source' => self::SRC_NEW,
719 'rc_minor' => $minor ? 1 : 0,
720 'rc_cur_id' => $title->getArticleID(),
721 'rc_user' => $user->getId(),
722 'rc_user_text' => $user->getName(),
723 'rc_actor' => $user->getActorId(),
724 'rc_comment' => &$comment,
725 'rc_comment_text' => &$comment,
726 'rc_comment_data' => null,
727 'rc_this_oldid' => $newId,
728 'rc_last_oldid' => 0,
729 'rc_bot' => $bot ? 1 : 0,
730 'rc_ip' => self::checkIPAddress( $ip ),
731 'rc_patrolled' => intval( $patrol ),
732 'rc_new' => 1, # obsolete
733 'rc_old_len' => 0,
734 'rc_new_len' => $size,
735 'rc_deleted' => 0,
736 'rc_logid' => 0,
737 'rc_log_type' => null,
738 'rc_log_action' => '',
739 'rc_params' => ''
740 ];
741
742 $rc->mExtra = [
743 'prefixedDBkey' => $title->getPrefixedDBkey(),
744 'lastTimestamp' => 0,
745 'oldSize' => 0,
746 'newSize' => $size,
747 'pageStatus' => 'created'
748 ];
749
750 DeferredUpdates::addCallableUpdate(
751 function () use ( $rc, $tags ) {
752 $rc->addTags( $tags );
753 $rc->save();
754 },
755 DeferredUpdates::POSTSEND,
757 );
758
759 return $rc;
760 }
761
777 public static function notifyLog( $timestamp, $title, $user, $actionComment, $ip, $type,
778 $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
779 ) {
780 global $wgLogRestrictions;
781
782 # Don't add private logs to RC!
783 if ( isset( $wgLogRestrictions[$type] ) && $wgLogRestrictions[$type] != '*' ) {
784 return false;
785 }
786 $rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action,
787 $target, $logComment, $params, $newId, $actionCommentIRC );
788 $rc->save();
789
790 return true;
791 }
792
810 public static function newLogEntry( $timestamp, $title, $user, $actionComment, $ip,
811 $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '',
812 $revId = 0, $isPatrollable = false ) {
813 global $wgRequest;
814 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
815
816 # # Get pageStatus for email notification
817 switch ( $type . '-' . $action ) {
818 case 'delete-delete':
819 case 'delete-delete_redir':
820 $pageStatus = 'deleted';
821 break;
822 case 'move-move':
823 case 'move-move_redir':
824 $pageStatus = 'moved';
825 break;
826 case 'delete-restore':
827 $pageStatus = 'restored';
828 break;
829 case 'upload-upload':
830 $pageStatus = 'created';
831 break;
832 case 'upload-overwrite':
833 default:
834 $pageStatus = 'changed';
835 break;
836 }
837
838 // Allow unpatrolled status for patrollable log entries
839 $canAutopatrol = $permissionManager->userHasRight( $user, 'autopatrol' );
840 $markPatrolled = $isPatrollable ? $canAutopatrol : true;
841
842 $rc = new RecentChange;
843 $rc->mTitle = $target;
844 $rc->mPerformer = $user;
845 $rc->mAttribs = [
846 'rc_timestamp' => $timestamp,
847 'rc_namespace' => $target->getNamespace(),
848 'rc_title' => $target->getDBkey(),
849 'rc_type' => RC_LOG,
850 'rc_source' => self::SRC_LOG,
851 'rc_minor' => 0,
852 'rc_cur_id' => $target->getArticleID(),
853 'rc_user' => $user->getId(),
854 'rc_user_text' => $user->getName(),
855 'rc_actor' => $user->getActorId(),
856 'rc_comment' => &$logComment,
857 'rc_comment_text' => &$logComment,
858 'rc_comment_data' => null,
859 'rc_this_oldid' => $revId,
860 'rc_last_oldid' => 0,
861 'rc_bot' => $permissionManager->userHasRight( $user, 'bot' ) ?
862 (int)$wgRequest->getBool( 'bot', true ) : 0,
863 'rc_ip' => self::checkIPAddress( $ip ),
864 'rc_patrolled' => $markPatrolled ? self::PRC_AUTOPATROLLED : self::PRC_UNPATROLLED,
865 'rc_new' => 0, # obsolete
866 'rc_old_len' => null,
867 'rc_new_len' => null,
868 'rc_deleted' => 0,
869 'rc_logid' => $newId,
870 'rc_log_type' => $type,
871 'rc_log_action' => $action,
872 'rc_params' => $params
873 ];
874
875 $rc->mExtra = [
876 'prefixedDBkey' => $title->getPrefixedDBkey(),
877 'lastTimestamp' => 0,
878 'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
879 'pageStatus' => $pageStatus,
880 'actionCommentIRC' => $actionCommentIRC
881 ];
882
883 return $rc;
884 }
885
907 public static function newForCategorization(
908 $timestamp,
909 Title $categoryTitle,
910 User $user = null,
911 $comment,
912 Title $pageTitle,
913 $oldRevId,
914 $newRevId,
915 $lastTimestamp,
916 $bot,
917 $ip = '',
918 $deleted = 0,
919 $added = null
920 ) {
921 // Done in a backwards compatible way.
922 $params = [
923 'hidden-cat' => WikiCategoryPage::factory( $categoryTitle )->isHidden()
924 ];
925 if ( $added !== null ) {
926 $params['added'] = $added;
927 }
928
929 $rc = new RecentChange;
930 $rc->mTitle = $categoryTitle;
931 $rc->mPerformer = $user;
932 $rc->mAttribs = [
933 'rc_timestamp' => $timestamp,
934 'rc_namespace' => $categoryTitle->getNamespace(),
935 'rc_title' => $categoryTitle->getDBkey(),
936 'rc_type' => RC_CATEGORIZE,
937 'rc_source' => self::SRC_CATEGORIZE,
938 'rc_minor' => 0,
939 'rc_cur_id' => $pageTitle->getArticleID(),
940 'rc_user' => $user ? $user->getId() : 0,
941 'rc_user_text' => $user ? $user->getName() : '',
942 'rc_actor' => $user ? $user->getActorId() : null,
943 'rc_comment' => &$comment,
944 'rc_comment_text' => &$comment,
945 'rc_comment_data' => null,
946 'rc_this_oldid' => $newRevId,
947 'rc_last_oldid' => $oldRevId,
948 'rc_bot' => $bot ? 1 : 0,
949 'rc_ip' => self::checkIPAddress( $ip ),
950 'rc_patrolled' => self::PRC_AUTOPATROLLED, // Always patrolled, just like log entries
951 'rc_new' => 0, # obsolete
952 'rc_old_len' => null,
953 'rc_new_len' => null,
954 'rc_deleted' => $deleted,
955 'rc_logid' => 0,
956 'rc_log_type' => null,
957 'rc_log_action' => '',
958 'rc_params' => serialize( $params )
959 ];
960
961 $rc->mExtra = [
962 'prefixedDBkey' => $categoryTitle->getPrefixedDBkey(),
963 'lastTimestamp' => $lastTimestamp,
964 'oldSize' => 0,
965 'newSize' => 0,
966 'pageStatus' => 'changed'
967 ];
968
969 return $rc;
970 }
971
980 public function getParam( $name ) {
981 $params = $this->parseParams();
982 return $params[$name] ?? null;
983 }
984
990 public function loadFromRow( $row ) {
991 $this->mAttribs = get_object_vars( $row );
992 $this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
993 // rc_deleted MUST be set
994 $this->mAttribs['rc_deleted'] = $row->rc_deleted;
995
996 if ( isset( $this->mAttribs['rc_ip'] ) ) {
997 // Clean up CIDRs for Postgres per T164898. ("127.0.0.1" casts to "127.0.0.1/32")
998 $n = strpos( $this->mAttribs['rc_ip'], '/' );
999 if ( $n !== false ) {
1000 $this->mAttribs['rc_ip'] = substr( $this->mAttribs['rc_ip'], 0, $n );
1001 }
1002 }
1003
1004 $comment = CommentStore::getStore()
1005 // Legacy because $row may have come from self::selectFields()
1006 ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $row, true )
1007 ->text;
1008 $this->mAttribs['rc_comment'] = &$comment;
1009 $this->mAttribs['rc_comment_text'] = &$comment;
1010 $this->mAttribs['rc_comment_data'] = null;
1011
1012 $user = User::newFromAnyId(
1013 $this->mAttribs['rc_user'] ?? null,
1014 $this->mAttribs['rc_user_text'] ?? null,
1015 $this->mAttribs['rc_actor'] ?? null
1016 );
1017 $this->mAttribs['rc_user'] = $user->getId();
1018 $this->mAttribs['rc_user_text'] = $user->getName();
1019 $this->mAttribs['rc_actor'] = $user->getActorId();
1020 }
1021
1028 public function getAttribute( $name ) {
1029 if ( $name === 'rc_comment' ) {
1030 return CommentStore::getStore()
1031 ->getComment( 'rc_comment', $this->mAttribs, true )->text;
1032 }
1033
1034 if ( $name === 'rc_user' || $name === 'rc_user_text' || $name === 'rc_actor' ) {
1035 $user = User::newFromAnyId(
1036 $this->mAttribs['rc_user'] ?? null,
1037 $this->mAttribs['rc_user_text'] ?? null,
1038 $this->mAttribs['rc_actor'] ?? null
1039 );
1040 if ( $name === 'rc_user' ) {
1041 return $user->getId();
1042 }
1043 if ( $name === 'rc_user_text' ) {
1044 return $user->getName();
1045 }
1046 if ( $name === 'rc_actor' ) {
1047 return $user->getActorId();
1048 }
1049 }
1050
1051 return $this->mAttribs[$name] ?? null;
1052 }
1053
1057 public function getAttributes() {
1058 return $this->mAttribs;
1059 }
1060
1067 public function diffLinkTrail( $forceCur ) {
1068 if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
1069 $trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
1070 "&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
1071 if ( $forceCur ) {
1072 $trail .= '&diff=0';
1073 } else {
1074 $trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
1075 }
1076 } else {
1077 $trail = '';
1078 }
1079
1080 return $trail;
1081 }
1082
1090 public function getCharacterDifference( $old = 0, $new = 0 ) {
1091 if ( $old === 0 ) {
1092 $old = $this->mAttribs['rc_old_len'];
1093 }
1094 if ( $new === 0 ) {
1095 $new = $this->mAttribs['rc_new_len'];
1096 }
1097 if ( $old === null || $new === null ) {
1098 return '';
1099 }
1100
1101 return ChangesList::showCharacterDifference( $old, $new );
1102 }
1103
1104 private static function checkIPAddress( $ip ) {
1105 global $wgRequest;
1106 if ( $ip ) {
1107 if ( !IP::isIPAddress( $ip ) ) {
1108 throw new MWException( "Attempt to write \"" . $ip .
1109 "\" as an IP address into recent changes" );
1110 }
1111 } else {
1112 $ip = $wgRequest->getIP();
1113 if ( !$ip ) {
1114 $ip = '';
1115 }
1116 }
1117
1118 return $ip;
1119 }
1120
1130 public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
1131 global $wgRCMaxAge;
1132
1133 return wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $wgRCMaxAge;
1134 }
1135
1143 public function parseParams() {
1144 $rcParams = $this->getAttribute( 'rc_params' );
1145
1146 Wikimedia\suppressWarnings();
1147 $unserializedParams = unserialize( $rcParams );
1148 Wikimedia\restoreWarnings();
1149
1150 return $unserializedParams;
1151 }
1152
1161 public function addTags( $tags ) {
1162 if ( is_string( $tags ) ) {
1163 $this->tags[] = $tags;
1164 } else {
1165 $this->tags = array_merge( $tags, $this->tags );
1166 }
1167 }
1168}
serialize()
unserialize( $serialized)
$wgRCFeeds
Recentchanges items are periodically purged; entries older than this many seconds will go.
$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.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
$wgUseEnotif
Definition Setup.php:437
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:751
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.
static showCharacterDifference( $old, $new, IContextSource $context=null)
Show formatted char difference.
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.
static record( $rc, $auto=false, User $user=null, $tags=null)
Record a log event for a change being patrolled.
Definition PatrolLog.php:42
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.
reallyMarkPatrolled()
Mark this RecentChange patrolled, without error checking.
static newFromRow( $row)
parseParams()
Parses and returns the rc_params attribute.
User false $mPerformer
static array $changeTypes
Array of change types.
static notifyEdit( $timestamp, $title, $minor, $user, $comment, $oldId, $lastTimestamp, $bot, $ip='', $oldSize=0, $newSize=0, $newId=0, $patrol=0, $tags=[])
Makes an entry in the database corresponding to an edit.
const SRC_CATEGORIZE
getPerformer()
Get the User object of the person who performed this change.
static checkIPAddress( $ip)
static newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='', $revId=0, $isPatrollable=false)
static getChangeTypes()
Get an array of all change types.
static newForCategorization( $timestamp, Title $categoryTitle, User $user=null, $comment, Title $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 isInRCLifespan( $timestamp, $tolerance=0)
Check whether the given timestamp is new enough to have a RC row with a given tolerance as the recent...
static markPatrolled( $change, $auto=false, $tags=null)
Mark a given change as patrolled.
int $counter
Line number of recent change.
save( $send=self::SEND_FEED)
Writes the data in this object to the database.
doMarkPatrolled(User $user, $auto=false, $tags=null)
Mark this RecentChange as patrolled.
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.
array $tags
List of tags to apply.
getParam( $name)
Get a parameter value.
addTags( $tags)
Tags to append to the recent change, and associated revision/log.
static notifyNew( $timestamp, $title, $minor, $user, $comment, $bot, $ip='', $size=0, $newId=0, $patrol=0, $tags=[])
Makes an entry in the database corresponding to page creation Note: the title object must be loaded w...
loadFromRow( $row)
Initialises the members of this object from a mysql row object.
getAttribute( $name)
Get an attribute value.
diffLinkTrail( $forceCur)
Gets the end part of the diff URL associated with this object Blank if no diff link should be display...
static notifyLog( $timestamp, $title, $user, $actionComment, $ip, $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='')
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:42
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1037
getPrefixedDBkey()
Get the prefixed database key form.
Definition Title.php:1806
getDBkey()
Get the main part with underscores.
Definition Title.php:1013
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition Title.php:3126
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2364
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition WikiPage.php:142
const RC_NEW
Definition Defines.php:132
const RC_LOG
Definition Defines.php:133
const RC_EXTERNAL
Definition Defines.php:134
const RC_EDIT
Definition Defines.php:131
const RC_CATEGORIZE
Definition Defines.php:135
Interface that defines how to tag objects.
Definition Taggable.php:32
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26