]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/changes/RecentChange.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / changes / RecentChange.php
1 <?php
2 /**
3  * Utility class for creating and accessing recent change entries.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  */
22
23 /**
24  * Utility class for creating new RC entries
25  *
26  * mAttribs:
27  *  rc_id           id of the row in the recentchanges table
28  *  rc_timestamp    time the entry was made
29  *  rc_namespace    namespace #
30  *  rc_title        non-prefixed db key
31  *  rc_type         is new entry, used to determine whether updating is necessary
32  *  rc_source       string representation of change source
33  *  rc_minor        is minor
34  *  rc_cur_id       page_id of associated page entry
35  *  rc_user         user id who made the entry
36  *  rc_user_text    user name who made the entry
37  *  rc_comment      edit summary
38  *  rc_this_oldid   rev_id associated with this entry (or zero)
39  *  rc_last_oldid   rev_id associated with the entry before this one (or zero)
40  *  rc_bot          is bot, hidden
41  *  rc_ip           IP address of the user in dotted quad notation
42  *  rc_new          obsolete, use rc_type==RC_NEW
43  *  rc_patrolled    boolean whether or not someone has marked this edit as patrolled
44  *  rc_old_len      integer byte length of the text before the edit
45  *  rc_new_len      the same after the edit
46  *  rc_deleted      partial deletion
47  *  rc_logid        the log_id value for this log entry (or zero)
48  *  rc_log_type     the log type (or null)
49  *  rc_log_action   the log action (or null)
50  *  rc_params       log params
51  *
52  * mExtra:
53  *  prefixedDBkey   prefixed db key, used by external app via msg queue
54  *  lastTimestamp   timestamp of previous entry, used in WHERE clause during update
55  *  oldSize         text size before the change
56  *  newSize         text size after the change
57  *  pageStatus      status of the page: created, deleted, moved, restored, changed
58  *
59  * temporary:       not stored in the database
60  *      notificationtimestamp
61  *      numberofWatchingusers
62  *
63  * @todo Deprecate access to mAttribs (direct or via getAttributes). Right now
64  *  we're having to include both rc_comment and rc_comment_text/rc_comment_data
65  *  so random crap works right.
66  */
67 class RecentChange {
68         // Constants for the rc_source field.  Extensions may also have
69         // their own source constants.
70         const SRC_EDIT = 'mw.edit';
71         const SRC_NEW = 'mw.new';
72         const SRC_LOG = 'mw.log';
73         const SRC_EXTERNAL = 'mw.external'; // obsolete
74         const SRC_CATEGORIZE = 'mw.categorize';
75
76         public $mAttribs = [];
77         public $mExtra = [];
78
79         /**
80          * @var Title
81          */
82         public $mTitle = false;
83
84         /**
85          * @var User
86          */
87         private $mPerformer = false;
88
89         public $numberofWatchingusers = 0; # Dummy to prevent error message in SpecialRecentChangesLinked
90         public $notificationtimestamp;
91
92         /**
93          * @var int Line number of recent change. Default -1.
94          */
95         public $counter = -1;
96
97         /**
98          * @var array List of tags to apply
99          */
100         private $tags = [];
101
102         /**
103          * @var array Array of change types
104          */
105         private static $changeTypes = [
106                 'edit' => RC_EDIT,
107                 'new' => RC_NEW,
108                 'log' => RC_LOG,
109                 'external' => RC_EXTERNAL,
110                 'categorize' => RC_CATEGORIZE,
111         ];
112
113         # Factory methods
114
115         /**
116          * @param mixed $row
117          * @return RecentChange
118          */
119         public static function newFromRow( $row ) {
120                 $rc = new RecentChange;
121                 $rc->loadFromRow( $row );
122
123                 return $rc;
124         }
125
126         /**
127          * Parsing text to RC_* constants
128          * @since 1.24
129          * @param string|array $type
130          * @throws MWException
131          * @return int|array RC_TYPE
132          */
133         public static function parseToRCType( $type ) {
134                 if ( is_array( $type ) ) {
135                         $retval = [];
136                         foreach ( $type as $t ) {
137                                 $retval[] = self::parseToRCType( $t );
138                         }
139
140                         return $retval;
141                 }
142
143                 if ( !array_key_exists( $type, self::$changeTypes ) ) {
144                         throw new MWException( "Unknown type '$type'" );
145                 }
146                 return self::$changeTypes[$type];
147         }
148
149         /**
150          * Parsing RC_* constants to human-readable test
151          * @since 1.24
152          * @param int $rcType
153          * @return string $type
154          */
155         public static function parseFromRCType( $rcType ) {
156                 return array_search( $rcType, self::$changeTypes, true ) ?: "$rcType";
157         }
158
159         /**
160          * Get an array of all change types
161          *
162          * @since 1.26
163          *
164          * @return array
165          */
166         public static function getChangeTypes() {
167                 return array_keys( self::$changeTypes );
168         }
169
170         /**
171          * Obtain the recent change with a given rc_id value
172          *
173          * @param int $rcid The rc_id value to retrieve
174          * @return RecentChange|null
175          */
176         public static function newFromId( $rcid ) {
177                 return self::newFromConds( [ 'rc_id' => $rcid ], __METHOD__ );
178         }
179
180         /**
181          * Find the first recent change matching some specific conditions
182          *
183          * @param array $conds Array of conditions
184          * @param mixed $fname Override the method name in profiling/logs
185          * @param int $dbType DB_* constant
186          *
187          * @return RecentChange|null
188          */
189         public static function newFromConds(
190                 $conds,
191                 $fname = __METHOD__,
192                 $dbType = DB_REPLICA
193         ) {
194                 $db = wfGetDB( $dbType );
195                 $row = $db->selectRow( 'recentchanges', self::selectFields(), $conds, $fname );
196                 if ( $row !== false ) {
197                         return self::newFromRow( $row );
198                 } else {
199                         return null;
200                 }
201         }
202
203         /**
204          * Return the list of recentchanges fields that should be selected to create
205          * a new recentchanges object.
206          * @todo Deprecate this in favor of a method that returns tables and joins
207          *  as well, and use CommentStore::getJoin().
208          * @return array
209          */
210         public static function selectFields() {
211                 return [
212                         'rc_id',
213                         'rc_timestamp',
214                         'rc_user',
215                         'rc_user_text',
216                         'rc_namespace',
217                         'rc_title',
218                         'rc_minor',
219                         'rc_bot',
220                         'rc_new',
221                         'rc_cur_id',
222                         'rc_this_oldid',
223                         'rc_last_oldid',
224                         'rc_type',
225                         'rc_source',
226                         'rc_patrolled',
227                         'rc_ip',
228                         'rc_old_len',
229                         'rc_new_len',
230                         'rc_deleted',
231                         'rc_logid',
232                         'rc_log_type',
233                         'rc_log_action',
234                         'rc_params',
235                 ] + CommentStore::newKey( 'rc_comment' )->getFields();
236         }
237
238         # Accessors
239
240         /**
241          * @param array $attribs
242          */
243         public function setAttribs( $attribs ) {
244                 $this->mAttribs = $attribs;
245         }
246
247         /**
248          * @param array $extra
249          */
250         public function setExtra( $extra ) {
251                 $this->mExtra = $extra;
252         }
253
254         /**
255          * @return Title
256          */
257         public function &getTitle() {
258                 if ( $this->mTitle === false ) {
259                         $this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
260                 }
261
262                 return $this->mTitle;
263         }
264
265         /**
266          * Get the User object of the person who performed this change.
267          *
268          * @return User
269          */
270         public function getPerformer() {
271                 if ( $this->mPerformer === false ) {
272                         if ( $this->mAttribs['rc_user'] ) {
273                                 $this->mPerformer = User::newFromId( $this->mAttribs['rc_user'] );
274                         } else {
275                                 $this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false );
276                         }
277                 }
278
279                 return $this->mPerformer;
280         }
281
282         /**
283          * Writes the data in this object to the database
284          * @param bool $noudp
285          */
286         public function save( $noudp = false ) {
287                 global $wgPutIPinRC, $wgUseEnotif, $wgShowUpdatedMarker;
288
289                 $dbw = wfGetDB( DB_MASTER );
290                 if ( !is_array( $this->mExtra ) ) {
291                         $this->mExtra = [];
292                 }
293
294                 if ( !$wgPutIPinRC ) {
295                         $this->mAttribs['rc_ip'] = '';
296                 }
297
298                 # Strict mode fixups (not-NULL fields)
299                 foreach ( [ 'minor', 'bot', 'new', 'patrolled', 'deleted' ] as $field ) {
300                         $this->mAttribs["rc_$field"] = (int)$this->mAttribs["rc_$field"];
301                 }
302                 # ...more fixups (NULL fields)
303                 foreach ( [ 'old_len', 'new_len' ] as $field ) {
304                         $this->mAttribs["rc_$field"] = isset( $this->mAttribs["rc_$field"] )
305                                 ? (int)$this->mAttribs["rc_$field"]
306                                 : null;
307                 }
308
309                 # If our database is strict about IP addresses, use NULL instead of an empty string
310                 $strictIPs = in_array( $dbw->getType(), [ 'oracle', 'postgres' ] ); // legacy
311                 if ( $strictIPs && $this->mAttribs['rc_ip'] == '' ) {
312                         unset( $this->mAttribs['rc_ip'] );
313                 }
314
315                 # Trim spaces on user supplied text
316                 $this->mAttribs['rc_comment'] = trim( $this->mAttribs['rc_comment'] );
317
318                 # Fixup database timestamps
319                 $this->mAttribs['rc_timestamp'] = $dbw->timestamp( $this->mAttribs['rc_timestamp'] );
320
321                 # # If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
322                 if ( $this->mAttribs['rc_cur_id'] == 0 ) {
323                         unset( $this->mAttribs['rc_cur_id'] );
324                 }
325
326                 # Convert mAttribs['rc_comment'] for CommentStore
327                 $row = $this->mAttribs;
328                 $comment = $row['rc_comment'];
329                 unset( $row['rc_comment'], $row['rc_comment_text'], $row['rc_comment_data'] );
330                 $row += CommentStore::newKey( 'rc_comment' )->insert( $dbw, $comment );
331
332                 # Don't reuse an existing rc_id for the new row, if one happens to be
333                 # set for some reason.
334                 unset( $row['rc_id'] );
335
336                 # Insert new row
337                 $dbw->insert( 'recentchanges', $row, __METHOD__ );
338
339                 # Set the ID
340                 $this->mAttribs['rc_id'] = $dbw->insertId();
341
342                 # Notify extensions
343                 // Avoid PHP 7.1 warning from passing $this by reference
344                 $rc = $this;
345                 Hooks::run( 'RecentChange_save', [ &$rc ] );
346
347                 if ( count( $this->tags ) ) {
348                         ChangeTags::addTags( $this->tags, $this->mAttribs['rc_id'],
349                                 $this->mAttribs['rc_this_oldid'], $this->mAttribs['rc_logid'], null, $this );
350                 }
351
352                 # Notify external application via UDP
353                 if ( !$noudp ) {
354                         $this->notifyRCFeeds();
355                 }
356
357                 # E-mail notifications
358                 if ( $wgUseEnotif || $wgShowUpdatedMarker ) {
359                         $editor = $this->getPerformer();
360                         $title = $this->getTitle();
361
362                         // Never send an RC notification email about categorization changes
363                         if (
364                                 Hooks::run( 'AbortEmailNotification', [ $editor, $title, $this ] ) &&
365                                 $this->mAttribs['rc_type'] != RC_CATEGORIZE
366                         ) {
367                                 // @FIXME: This would be better as an extension hook
368                                 // Send emails or email jobs once this row is safely committed
369                                 $dbw->onTransactionIdle(
370                                         function () use ( $editor, $title ) {
371                                                 $enotif = new EmailNotification();
372                                                 $enotif->notifyOnPageChange(
373                                                         $editor,
374                                                         $title,
375                                                         $this->mAttribs['rc_timestamp'],
376                                                         $this->mAttribs['rc_comment'],
377                                                         $this->mAttribs['rc_minor'],
378                                                         $this->mAttribs['rc_last_oldid'],
379                                                         $this->mExtra['pageStatus']
380                                                 );
381                                         },
382                                         __METHOD__
383                                 );
384                         }
385                 }
386
387                 // Update the cached list of active users
388                 if ( $this->mAttribs['rc_user'] > 0 ) {
389                         JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newCacheUpdateJob() );
390                 }
391         }
392
393         /**
394          * Notify all the feeds about the change.
395          * @param array $feeds Optional feeds to send to, defaults to $wgRCFeeds
396          */
397         public function notifyRCFeeds( array $feeds = null ) {
398                 global $wgRCFeeds;
399                 if ( $feeds === null ) {
400                         $feeds = $wgRCFeeds;
401                 }
402
403                 $performer = $this->getPerformer();
404
405                 foreach ( $feeds as $params ) {
406                         $params += [
407                                 'omit_bots' => false,
408                                 'omit_anon' => false,
409                                 'omit_user' => false,
410                                 'omit_minor' => false,
411                                 'omit_patrolled' => false,
412                         ];
413
414                         if (
415                                 ( $params['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
416                                 ( $params['omit_anon'] && $performer->isAnon() ) ||
417                                 ( $params['omit_user'] && !$performer->isAnon() ) ||
418                                 ( $params['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
419                                 ( $params['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
420                                 $this->mAttribs['rc_type'] == RC_EXTERNAL
421                         ) {
422                                 continue;
423                         }
424
425                         if ( isset( $this->mExtra['actionCommentIRC'] ) ) {
426                                 $actionComment = $this->mExtra['actionCommentIRC'];
427                         } else {
428                                 $actionComment = null;
429                         }
430
431                         $feed = RCFeed::factory( $params );
432                         $feed->notify( $this, $actionComment );
433                 }
434         }
435
436         /**
437          * @since 1.22
438          * @deprecated since 1.29 Use RCFeed::factory() instead
439          * @param string $uri URI to get the engine object for
440          * @param array $params
441          * @return RCFeedEngine The engine object
442          * @throws MWException
443          */
444         public static function getEngine( $uri, $params = [] ) {
445                 // TODO: Merge into RCFeed::factory().
446                 global $wgRCEngines;
447                 $scheme = parse_url( $uri, PHP_URL_SCHEME );
448                 if ( !$scheme ) {
449                         throw new MWException( "Invalid RCFeed uri: '$uri'" );
450                 }
451                 if ( !isset( $wgRCEngines[$scheme] ) ) {
452                         throw new MWException( "Unknown RCFeedEngine scheme: '$scheme'" );
453                 }
454                 if ( defined( 'MW_PHPUNIT_TEST' ) && is_object( $wgRCEngines[$scheme] ) ) {
455                         return $wgRCEngines[$scheme];
456                 }
457                 return new $wgRCEngines[$scheme]( $params );
458         }
459
460         /**
461          * Mark a given change as patrolled
462          *
463          * @param RecentChange|int $change RecentChange or corresponding rc_id
464          * @param bool $auto For automatic patrol
465          * @param string|string[] $tags Change tags to add to the patrol log entry
466          *   ($user should be able to add the specified tags before this is called)
467          * @return array See doMarkPatrolled(), or null if $change is not an existing rc_id
468          */
469         public static function markPatrolled( $change, $auto = false, $tags = null ) {
470                 global $wgUser;
471
472                 $change = $change instanceof RecentChange
473                         ? $change
474                         : self::newFromId( $change );
475
476                 if ( !$change instanceof RecentChange ) {
477                         return null;
478                 }
479
480                 return $change->doMarkPatrolled( $wgUser, $auto, $tags );
481         }
482
483         /**
484          * Mark this RecentChange as patrolled
485          *
486          * NOTE: Can also return 'rcpatroldisabled', 'hookaborted' and
487          * 'markedaspatrollederror-noautopatrol' as errors
488          * @param User $user User object doing the action
489          * @param bool $auto For automatic patrol
490          * @param string|string[] $tags Change tags to add to the patrol log entry
491          *   ($user should be able to add the specified tags before this is called)
492          * @return array Array of permissions errors, see Title::getUserPermissionsErrors()
493          */
494         public function doMarkPatrolled( User $user, $auto = false, $tags = null ) {
495                 global $wgUseRCPatrol, $wgUseNPPatrol, $wgUseFilePatrol;
496
497                 $errors = [];
498                 // If recentchanges patrol is disabled, only new pages or new file versions
499                 // can be patrolled, provided the appropriate config variable is set
500                 if ( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) &&
501                         ( !$wgUseFilePatrol || !( $this->getAttribute( 'rc_type' ) == RC_LOG &&
502                         $this->getAttribute( 'rc_log_type' ) == 'upload' ) ) ) {
503                         $errors[] = [ 'rcpatroldisabled' ];
504                 }
505                 // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
506                 $right = $auto ? 'autopatrol' : 'patrol';
507                 $errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $user ) );
508                 if ( !Hooks::run( 'MarkPatrolled',
509                                         [ $this->getAttribute( 'rc_id' ), &$user, false, $auto ] )
510                 ) {
511                         $errors[] = [ 'hookaborted' ];
512                 }
513                 // Users without the 'autopatrol' right can't patrol their
514                 // own revisions
515                 if ( $user->getName() === $this->getAttribute( 'rc_user_text' )
516                         && !$user->isAllowed( 'autopatrol' )
517                 ) {
518                         $errors[] = [ 'markedaspatrollederror-noautopatrol' ];
519                 }
520                 if ( $errors ) {
521                         return $errors;
522                 }
523                 // If the change was patrolled already, do nothing
524                 if ( $this->getAttribute( 'rc_patrolled' ) ) {
525                         return [];
526                 }
527                 // Actually set the 'patrolled' flag in RC
528                 $this->reallyMarkPatrolled();
529                 // Log this patrol event
530                 PatrolLog::record( $this, $auto, $user, $tags );
531
532                 Hooks::run(
533                         'MarkPatrolledComplete',
534                         [ $this->getAttribute( 'rc_id' ), &$user, false, $auto ]
535                 );
536
537                 return [];
538         }
539
540         /**
541          * Mark this RecentChange patrolled, without error checking
542          * @return int Number of affected rows
543          */
544         public function reallyMarkPatrolled() {
545                 $dbw = wfGetDB( DB_MASTER );
546                 $dbw->update(
547                         'recentchanges',
548                         [
549                                 'rc_patrolled' => 1
550                         ],
551                         [
552                                 'rc_id' => $this->getAttribute( 'rc_id' )
553                         ],
554                         __METHOD__
555                 );
556                 // Invalidate the page cache after the page has been patrolled
557                 // to make sure that the Patrol link isn't visible any longer!
558                 $this->getTitle()->invalidateCache();
559
560                 return $dbw->affectedRows();
561         }
562
563         /**
564          * Makes an entry in the database corresponding to an edit
565          *
566          * @param string $timestamp
567          * @param Title &$title
568          * @param bool $minor
569          * @param User &$user
570          * @param string $comment
571          * @param int $oldId
572          * @param string $lastTimestamp
573          * @param bool $bot
574          * @param string $ip
575          * @param int $oldSize
576          * @param int $newSize
577          * @param int $newId
578          * @param int $patrol
579          * @param array $tags
580          * @return RecentChange
581          */
582         public static function notifyEdit(
583                 $timestamp, &$title, $minor, &$user, $comment, $oldId, $lastTimestamp,
584                 $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0,
585                 $tags = []
586         ) {
587                 $rc = new RecentChange;
588                 $rc->mTitle = $title;
589                 $rc->mPerformer = $user;
590                 $rc->mAttribs = [
591                         'rc_timestamp' => $timestamp,
592                         'rc_namespace' => $title->getNamespace(),
593                         'rc_title' => $title->getDBkey(),
594                         'rc_type' => RC_EDIT,
595                         'rc_source' => self::SRC_EDIT,
596                         'rc_minor' => $minor ? 1 : 0,
597                         'rc_cur_id' => $title->getArticleID(),
598                         'rc_user' => $user->getId(),
599                         'rc_user_text' => $user->getName(),
600                         'rc_comment' => &$comment,
601                         'rc_comment_text' => &$comment,
602                         'rc_comment_data' => null,
603                         'rc_this_oldid' => $newId,
604                         'rc_last_oldid' => $oldId,
605                         'rc_bot' => $bot ? 1 : 0,
606                         'rc_ip' => self::checkIPAddress( $ip ),
607                         'rc_patrolled' => intval( $patrol ),
608                         'rc_new' => 0, # obsolete
609                         'rc_old_len' => $oldSize,
610                         'rc_new_len' => $newSize,
611                         'rc_deleted' => 0,
612                         'rc_logid' => 0,
613                         'rc_log_type' => null,
614                         'rc_log_action' => '',
615                         'rc_params' => ''
616                 ];
617
618                 $rc->mExtra = [
619                         'prefixedDBkey' => $title->getPrefixedDBkey(),
620                         'lastTimestamp' => $lastTimestamp,
621                         'oldSize' => $oldSize,
622                         'newSize' => $newSize,
623                         'pageStatus' => 'changed'
624                 ];
625
626                 DeferredUpdates::addCallableUpdate(
627                         function () use ( $rc, $tags ) {
628                                 $rc->addTags( $tags );
629                                 $rc->save();
630                                 if ( $rc->mAttribs['rc_patrolled'] ) {
631                                         PatrolLog::record( $rc, true, $rc->getPerformer() );
632                                 }
633                         },
634                         DeferredUpdates::POSTSEND,
635                         wfGetDB( DB_MASTER )
636                 );
637
638                 return $rc;
639         }
640
641         /**
642          * Makes an entry in the database corresponding to page creation
643          * Note: the title object must be loaded with the new id using resetArticleID()
644          *
645          * @param string $timestamp
646          * @param Title &$title
647          * @param bool $minor
648          * @param User &$user
649          * @param string $comment
650          * @param bool $bot
651          * @param string $ip
652          * @param int $size
653          * @param int $newId
654          * @param int $patrol
655          * @param array $tags
656          * @return RecentChange
657          */
658         public static function notifyNew(
659                 $timestamp, &$title, $minor, &$user, $comment, $bot,
660                 $ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = []
661         ) {
662                 $rc = new RecentChange;
663                 $rc->mTitle = $title;
664                 $rc->mPerformer = $user;
665                 $rc->mAttribs = [
666                         'rc_timestamp' => $timestamp,
667                         'rc_namespace' => $title->getNamespace(),
668                         'rc_title' => $title->getDBkey(),
669                         'rc_type' => RC_NEW,
670                         'rc_source' => self::SRC_NEW,
671                         'rc_minor' => $minor ? 1 : 0,
672                         'rc_cur_id' => $title->getArticleID(),
673                         'rc_user' => $user->getId(),
674                         'rc_user_text' => $user->getName(),
675                         'rc_comment' => &$comment,
676                         'rc_comment_text' => &$comment,
677                         'rc_comment_data' => null,
678                         'rc_this_oldid' => $newId,
679                         'rc_last_oldid' => 0,
680                         'rc_bot' => $bot ? 1 : 0,
681                         'rc_ip' => self::checkIPAddress( $ip ),
682                         'rc_patrolled' => intval( $patrol ),
683                         'rc_new' => 1, # obsolete
684                         'rc_old_len' => 0,
685                         'rc_new_len' => $size,
686                         'rc_deleted' => 0,
687                         'rc_logid' => 0,
688                         'rc_log_type' => null,
689                         'rc_log_action' => '',
690                         'rc_params' => ''
691                 ];
692
693                 $rc->mExtra = [
694                         'prefixedDBkey' => $title->getPrefixedDBkey(),
695                         'lastTimestamp' => 0,
696                         'oldSize' => 0,
697                         'newSize' => $size,
698                         'pageStatus' => 'created'
699                 ];
700
701                 DeferredUpdates::addCallableUpdate(
702                         function () use ( $rc, $tags ) {
703                                 $rc->addTags( $tags );
704                                 $rc->save();
705                                 if ( $rc->mAttribs['rc_patrolled'] ) {
706                                         PatrolLog::record( $rc, true, $rc->getPerformer() );
707                                 }
708                         },
709                         DeferredUpdates::POSTSEND,
710                         wfGetDB( DB_MASTER )
711                 );
712
713                 return $rc;
714         }
715
716         /**
717          * @param string $timestamp
718          * @param Title &$title
719          * @param User &$user
720          * @param string $actionComment
721          * @param string $ip
722          * @param string $type
723          * @param string $action
724          * @param Title $target
725          * @param string $logComment
726          * @param string $params
727          * @param int $newId
728          * @param string $actionCommentIRC
729          * @return bool
730          */
731         public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip, $type,
732                 $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
733         ) {
734                 global $wgLogRestrictions;
735
736                 # Don't add private logs to RC!
737                 if ( isset( $wgLogRestrictions[$type] ) && $wgLogRestrictions[$type] != '*' ) {
738                         return false;
739                 }
740                 $rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action,
741                         $target, $logComment, $params, $newId, $actionCommentIRC );
742                 $rc->save();
743
744                 return true;
745         }
746
747         /**
748          * @param string $timestamp
749          * @param Title &$title
750          * @param User &$user
751          * @param string $actionComment
752          * @param string $ip
753          * @param string $type
754          * @param string $action
755          * @param Title $target
756          * @param string $logComment
757          * @param string $params
758          * @param int $newId
759          * @param string $actionCommentIRC
760          * @param int $revId Id of associated revision, if any
761          * @param bool $isPatrollable Whether this log entry is patrollable
762          * @return RecentChange
763          */
764         public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip,
765                 $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '',
766                 $revId = 0, $isPatrollable = false ) {
767                 global $wgRequest;
768
769                 # # Get pageStatus for email notification
770                 switch ( $type . '-' . $action ) {
771                         case 'delete-delete':
772                         case 'delete-delete_redir':
773                                 $pageStatus = 'deleted';
774                                 break;
775                         case 'move-move':
776                         case 'move-move_redir':
777                                 $pageStatus = 'moved';
778                                 break;
779                         case 'delete-restore':
780                                 $pageStatus = 'restored';
781                                 break;
782                         case 'upload-upload':
783                                 $pageStatus = 'created';
784                                 break;
785                         case 'upload-overwrite':
786                         default:
787                                 $pageStatus = 'changed';
788                                 break;
789                 }
790
791                 // Allow unpatrolled status for patrollable log entries
792                 $markPatrolled = $isPatrollable ? $user->isAllowed( 'autopatrol' ) : true;
793
794                 $rc = new RecentChange;
795                 $rc->mTitle = $target;
796                 $rc->mPerformer = $user;
797                 $rc->mAttribs = [
798                         'rc_timestamp' => $timestamp,
799                         'rc_namespace' => $target->getNamespace(),
800                         'rc_title' => $target->getDBkey(),
801                         'rc_type' => RC_LOG,
802                         'rc_source' => self::SRC_LOG,
803                         'rc_minor' => 0,
804                         'rc_cur_id' => $target->getArticleID(),
805                         'rc_user' => $user->getId(),
806                         'rc_user_text' => $user->getName(),
807                         'rc_comment' => &$logComment,
808                         'rc_comment_text' => &$logComment,
809                         'rc_comment_data' => null,
810                         'rc_this_oldid' => $revId,
811                         'rc_last_oldid' => 0,
812                         'rc_bot' => $user->isAllowed( 'bot' ) ? (int)$wgRequest->getBool( 'bot', true ) : 0,
813                         'rc_ip' => self::checkIPAddress( $ip ),
814                         'rc_patrolled' => $markPatrolled ? 1 : 0,
815                         'rc_new' => 0, # obsolete
816                         'rc_old_len' => null,
817                         'rc_new_len' => null,
818                         'rc_deleted' => 0,
819                         'rc_logid' => $newId,
820                         'rc_log_type' => $type,
821                         'rc_log_action' => $action,
822                         'rc_params' => $params
823                 ];
824
825                 $rc->mExtra = [
826                         'prefixedDBkey' => $title->getPrefixedDBkey(),
827                         'lastTimestamp' => 0,
828                         'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
829                         'pageStatus' => $pageStatus,
830                         'actionCommentIRC' => $actionCommentIRC
831                 ];
832
833                 return $rc;
834         }
835
836         /**
837          * Constructs a RecentChange object for the given categorization
838          * This does not call save() on the object and thus does not write to the db
839          *
840          * @since 1.27
841          *
842          * @param string $timestamp Timestamp of the recent change to occur
843          * @param Title $categoryTitle Title of the category a page is being added to or removed from
844          * @param User $user User object of the user that made the change
845          * @param string $comment Change summary
846          * @param Title $pageTitle Title of the page that is being added or removed
847          * @param int $oldRevId Parent revision ID of this change
848          * @param int $newRevId Revision ID of this change
849          * @param string $lastTimestamp Parent revision timestamp of this change
850          * @param bool $bot true, if the change was made by a bot
851          * @param string $ip IP address of the user, if the change was made anonymously
852          * @param int $deleted Indicates whether the change has been deleted
853          * @param bool $added true, if the category was added, false for removed
854          *
855          * @return RecentChange
856          */
857         public static function newForCategorization(
858                 $timestamp,
859                 Title $categoryTitle,
860                 User $user = null,
861                 $comment,
862                 Title $pageTitle,
863                 $oldRevId,
864                 $newRevId,
865                 $lastTimestamp,
866                 $bot,
867                 $ip = '',
868                 $deleted = 0,
869                 $added = null
870         ) {
871                 // Done in a backwards compatible way.
872                 $params = [
873                         'hidden-cat' => WikiCategoryPage::factory( $categoryTitle )->isHidden()
874                 ];
875                 if ( $added !== null ) {
876                         $params['added'] = $added;
877                 }
878
879                 $rc = new RecentChange;
880                 $rc->mTitle = $categoryTitle;
881                 $rc->mPerformer = $user;
882                 $rc->mAttribs = [
883                         'rc_timestamp' => $timestamp,
884                         'rc_namespace' => $categoryTitle->getNamespace(),
885                         'rc_title' => $categoryTitle->getDBkey(),
886                         'rc_type' => RC_CATEGORIZE,
887                         'rc_source' => self::SRC_CATEGORIZE,
888                         'rc_minor' => 0,
889                         'rc_cur_id' => $pageTitle->getArticleID(),
890                         'rc_user' => $user ? $user->getId() : 0,
891                         'rc_user_text' => $user ? $user->getName() : '',
892                         'rc_comment' => &$comment,
893                         'rc_comment_text' => &$comment,
894                         'rc_comment_data' => null,
895                         'rc_this_oldid' => $newRevId,
896                         'rc_last_oldid' => $oldRevId,
897                         'rc_bot' => $bot ? 1 : 0,
898                         'rc_ip' => self::checkIPAddress( $ip ),
899                         'rc_patrolled' => 1, // Always patrolled, just like log entries
900                         'rc_new' => 0, # obsolete
901                         'rc_old_len' => null,
902                         'rc_new_len' => null,
903                         'rc_deleted' => $deleted,
904                         'rc_logid' => 0,
905                         'rc_log_type' => null,
906                         'rc_log_action' => '',
907                         'rc_params' => serialize( $params )
908                 ];
909
910                 $rc->mExtra = [
911                         'prefixedDBkey' => $categoryTitle->getPrefixedDBkey(),
912                         'lastTimestamp' => $lastTimestamp,
913                         'oldSize' => 0,
914                         'newSize' => 0,
915                         'pageStatus' => 'changed'
916                 ];
917
918                 return $rc;
919         }
920
921         /**
922          * Get a parameter value
923          *
924          * @since 1.27
925          *
926          * @param string $name parameter name
927          * @return mixed
928          */
929         public function getParam( $name ) {
930                 $params = $this->parseParams();
931                 return isset( $params[$name] ) ? $params[$name] : null;
932         }
933
934         /**
935          * Initialises the members of this object from a mysql row object
936          *
937          * @param mixed $row
938          */
939         public function loadFromRow( $row ) {
940                 $this->mAttribs = get_object_vars( $row );
941                 $this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
942                 // rc_deleted MUST be set
943                 $this->mAttribs['rc_deleted'] = $row->rc_deleted;
944
945                 if ( isset( $this->mAttribs['rc_ip'] ) ) {
946                         // Clean up CIDRs for Postgres per T164898. ("127.0.0.1" casts to "127.0.0.1/32")
947                         $n = strpos( $this->mAttribs['rc_ip'], '/' );
948                         if ( $n !== false ) {
949                                 $this->mAttribs['rc_ip'] = substr( $this->mAttribs['rc_ip'], 0, $n );
950                         }
951                 }
952
953                 $comment = CommentStore::newKey( 'rc_comment' )
954                         // Legacy because $row probably came from self::selectFields()
955                         ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text;
956                 $this->mAttribs['rc_comment'] = &$comment;
957                 $this->mAttribs['rc_comment_text'] = &$comment;
958                 $this->mAttribs['rc_comment_data'] = null;
959         }
960
961         /**
962          * Get an attribute value
963          *
964          * @param string $name Attribute name
965          * @return mixed
966          */
967         public function getAttribute( $name ) {
968                 if ( $name === 'rc_comment' ) {
969                         return CommentStore::newKey( 'rc_comment' )->getComment( $this->mAttribs, true )->text;
970                 }
971                 return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : null;
972         }
973
974         /**
975          * @return array
976          */
977         public function getAttributes() {
978                 return $this->mAttribs;
979         }
980
981         /**
982          * Gets the end part of the diff URL associated with this object
983          * Blank if no diff link should be displayed
984          * @param bool $forceCur
985          * @return string
986          */
987         public function diffLinkTrail( $forceCur ) {
988                 if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
989                         $trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
990                                 "&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
991                         if ( $forceCur ) {
992                                 $trail .= '&diff=0';
993                         } else {
994                                 $trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
995                         }
996                 } else {
997                         $trail = '';
998                 }
999
1000                 return $trail;
1001         }
1002
1003         /**
1004          * Returns the change size (HTML).
1005          * The lengths can be given optionally.
1006          * @param int $old
1007          * @param int $new
1008          * @return string
1009          */
1010         public function getCharacterDifference( $old = 0, $new = 0 ) {
1011                 if ( $old === 0 ) {
1012                         $old = $this->mAttribs['rc_old_len'];
1013                 }
1014                 if ( $new === 0 ) {
1015                         $new = $this->mAttribs['rc_new_len'];
1016                 }
1017                 if ( $old === null || $new === null ) {
1018                         return '';
1019                 }
1020
1021                 return ChangesList::showCharacterDifference( $old, $new );
1022         }
1023
1024         private static function checkIPAddress( $ip ) {
1025                 global $wgRequest;
1026                 if ( $ip ) {
1027                         if ( !IP::isIPAddress( $ip ) ) {
1028                                 throw new MWException( "Attempt to write \"" . $ip .
1029                                         "\" as an IP address into recent changes" );
1030                         }
1031                 } else {
1032                         $ip = $wgRequest->getIP();
1033                         if ( !$ip ) {
1034                                 $ip = '';
1035                         }
1036                 }
1037
1038                 return $ip;
1039         }
1040
1041         /**
1042          * Check whether the given timestamp is new enough to have a RC row with a given tolerance
1043          * as the recentchanges table might not be cleared out regularly (so older entries might exist)
1044          * or rows which will be deleted soon shouldn't be included.
1045          *
1046          * @param mixed $timestamp MWTimestamp compatible timestamp
1047          * @param int $tolerance Tolerance in seconds
1048          * @return bool
1049          */
1050         public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
1051                 global $wgRCMaxAge;
1052
1053                 return wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $wgRCMaxAge;
1054         }
1055
1056         /**
1057          * Parses and returns the rc_params attribute
1058          *
1059          * @since 1.26
1060          *
1061          * @return mixed|bool false on failed unserialization
1062          */
1063         public function parseParams() {
1064                 $rcParams = $this->getAttribute( 'rc_params' );
1065
1066                 MediaWiki\suppressWarnings();
1067                 $unserializedParams = unserialize( $rcParams );
1068                 MediaWiki\restoreWarnings();
1069
1070                 return $unserializedParams;
1071         }
1072
1073         /**
1074          * Tags to append to the recent change,
1075          * and associated revision/log
1076          *
1077          * @since 1.28
1078          *
1079          * @param string|array $tags
1080          */
1081         public function addTags( $tags ) {
1082                 if ( is_string( $tags ) ) {
1083                         $this->tags[] = $tags;
1084                 } else {
1085                         $this->tags = array_merge( $tags, $this->tags );
1086                 }
1087         }
1088 }