Completed
Branch master (939199)
by
unknown
39:35
created

includes/changes/RecentChange.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
class RecentChange {
64
	// Constants for the rc_source field.  Extensions may also have
65
	// their own source constants.
66
	const SRC_EDIT = 'mw.edit';
67
	const SRC_NEW = 'mw.new';
68
	const SRC_LOG = 'mw.log';
69
	const SRC_EXTERNAL = 'mw.external'; // obsolete
70
	const SRC_CATEGORIZE = 'mw.categorize';
71
72
	public $mAttribs = [];
73
	public $mExtra = [];
74
75
	/**
76
	 * @var Title
77
	 */
78
	public $mTitle = false;
79
80
	/**
81
	 * @var User
82
	 */
83
	private $mPerformer = false;
84
85
	public $numberofWatchingusers = 0; # Dummy to prevent error message in SpecialRecentChangesLinked
86
	public $notificationtimestamp;
87
88
	/**
89
	 * @var int Line number of recent change. Default -1.
90
	 */
91
	public $counter = -1;
92
93
	/**
94
	 * @var array List of tags to apply
95
	 */
96
	private $tags = [];
97
98
	/**
99
	 * @var array Array of change types
100
	 */
101
	private static $changeTypes = [
102
		'edit' => RC_EDIT,
103
		'new' => RC_NEW,
104
		'log' => RC_LOG,
105
		'external' => RC_EXTERNAL,
106
		'categorize' => RC_CATEGORIZE,
107
	];
108
109
	# Factory methods
110
111
	/**
112
	 * @param mixed $row
113
	 * @return RecentChange
114
	 */
115
	public static function newFromRow( $row ) {
116
		$rc = new RecentChange;
117
		$rc->loadFromRow( $row );
118
119
		return $rc;
120
	}
121
122
	/**
123
	 * Parsing text to RC_* constants
124
	 * @since 1.24
125
	 * @param string|array $type
126
	 * @throws MWException
127
	 * @return int|array RC_TYPE
128
	 */
129
	public static function parseToRCType( $type ) {
130
		if ( is_array( $type ) ) {
131
			$retval = [];
132
			foreach ( $type as $t ) {
133
				$retval[] = RecentChange::parseToRCType( $t );
134
			}
135
136
			return $retval;
137
		}
138
139
		if ( !array_key_exists( $type, self::$changeTypes ) ) {
140
			throw new MWException( "Unknown type '$type'" );
141
		}
142
		return self::$changeTypes[$type];
143
	}
144
145
	/**
146
	 * Parsing RC_* constants to human-readable test
147
	 * @since 1.24
148
	 * @param int $rcType
149
	 * @return string $type
150
	 */
151
	public static function parseFromRCType( $rcType ) {
152
		return array_search( $rcType, self::$changeTypes, true ) ?: "$rcType";
153
	}
154
155
	/**
156
	 * Get an array of all change types
157
	 *
158
	 * @since 1.26
159
	 *
160
	 * @return array
161
	 */
162
	public static function getChangeTypes() {
163
		return array_keys( self::$changeTypes );
164
	}
165
166
	/**
167
	 * Obtain the recent change with a given rc_id value
168
	 *
169
	 * @param int $rcid The rc_id value to retrieve
170
	 * @return RecentChange|null
171
	 */
172
	public static function newFromId( $rcid ) {
173
		return self::newFromConds( [ 'rc_id' => $rcid ], __METHOD__ );
174
	}
175
176
	/**
177
	 * Find the first recent change matching some specific conditions
178
	 *
179
	 * @param array $conds Array of conditions
180
	 * @param mixed $fname Override the method name in profiling/logs
181
	 * @param int $dbType DB_* constant
182
	 *
183
	 * @return RecentChange|null
184
	 */
185
	public static function newFromConds(
186
		$conds,
187
		$fname = __METHOD__,
188
		$dbType = DB_REPLICA
189
	) {
190
		$db = wfGetDB( $dbType );
191
		$row = $db->selectRow( 'recentchanges', self::selectFields(), $conds, $fname );
192
		if ( $row !== false ) {
193
			return self::newFromRow( $row );
194
		} else {
195
			return null;
196
		}
197
	}
198
199
	/**
200
	 * Return the list of recentchanges fields that should be selected to create
201
	 * a new recentchanges object.
202
	 * @return array
203
	 */
204
	public static function selectFields() {
205
		return [
206
			'rc_id',
207
			'rc_timestamp',
208
			'rc_user',
209
			'rc_user_text',
210
			'rc_namespace',
211
			'rc_title',
212
			'rc_comment',
213
			'rc_minor',
214
			'rc_bot',
215
			'rc_new',
216
			'rc_cur_id',
217
			'rc_this_oldid',
218
			'rc_last_oldid',
219
			'rc_type',
220
			'rc_source',
221
			'rc_patrolled',
222
			'rc_ip',
223
			'rc_old_len',
224
			'rc_new_len',
225
			'rc_deleted',
226
			'rc_logid',
227
			'rc_log_type',
228
			'rc_log_action',
229
			'rc_params',
230
		];
231
	}
232
233
	# Accessors
234
235
	/**
236
	 * @param array $attribs
237
	 */
238
	public function setAttribs( $attribs ) {
239
		$this->mAttribs = $attribs;
240
	}
241
242
	/**
243
	 * @param array $extra
244
	 */
245
	public function setExtra( $extra ) {
246
		$this->mExtra = $extra;
247
	}
248
249
	/**
250
	 * @return Title
251
	 */
252
	public function &getTitle() {
253
		if ( $this->mTitle === false ) {
254
			$this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
255
		}
256
257
		return $this->mTitle;
258
	}
259
260
	/**
261
	 * Get the User object of the person who performed this change.
262
	 *
263
	 * @return User
264
	 */
265
	public function getPerformer() {
266
		if ( $this->mPerformer === false ) {
267
			if ( $this->mAttribs['rc_user'] ) {
268
				$this->mPerformer = User::newFromId( $this->mAttribs['rc_user'] );
269
			} else {
270
				$this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false );
271
			}
272
		}
273
274
		return $this->mPerformer;
275
	}
276
277
	/**
278
	 * Writes the data in this object to the database
279
	 * @param bool $noudp
280
	 */
281
	public function save( $noudp = false ) {
282
		global $wgPutIPinRC, $wgUseEnotif, $wgShowUpdatedMarker, $wgContLang;
283
284
		$dbw = wfGetDB( DB_MASTER );
285
		if ( !is_array( $this->mExtra ) ) {
286
			$this->mExtra = [];
287
		}
288
289
		if ( !$wgPutIPinRC ) {
290
			$this->mAttribs['rc_ip'] = '';
291
		}
292
293
		# Strict mode fixups (not-NULL fields)
294
		foreach ( [ 'minor', 'bot', 'new', 'patrolled', 'deleted' ] as $field ) {
295
			$this->mAttribs["rc_$field"] = (int)$this->mAttribs["rc_$field"];
296
		}
297
		# ...more fixups (NULL fields)
298
		foreach ( [ 'old_len', 'new_len' ] as $field ) {
299
			$this->mAttribs["rc_$field"] = isset( $this->mAttribs["rc_$field"] )
300
				? (int)$this->mAttribs["rc_$field"]
301
				: null;
302
		}
303
304
		# If our database is strict about IP addresses, use NULL instead of an empty string
305
		$strictIPs = in_array( $dbw->getType(), [ 'oracle', 'postgres' ] ); // legacy
306
		if ( $strictIPs && $this->mAttribs['rc_ip'] == '' ) {
307
			unset( $this->mAttribs['rc_ip'] );
308
		}
309
310
		# Trim spaces on user supplied text
311
		$this->mAttribs['rc_comment'] = trim( $this->mAttribs['rc_comment'] );
312
313
		# Make sure summary is truncated (whole multibyte characters)
314
		$this->mAttribs['rc_comment'] = $wgContLang->truncate( $this->mAttribs['rc_comment'], 255 );
315
316
		# Fixup database timestamps
317
		$this->mAttribs['rc_timestamp'] = $dbw->timestamp( $this->mAttribs['rc_timestamp'] );
318
		$this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'recentchanges_rc_id_seq' );
319
320
		# # If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
321
		if ( $this->mAttribs['rc_cur_id'] == 0 ) {
322
			unset( $this->mAttribs['rc_cur_id'] );
323
		}
324
325
		# Insert new row
326
		$dbw->insert( 'recentchanges', $this->mAttribs, __METHOD__ );
327
328
		# Set the ID
329
		$this->mAttribs['rc_id'] = $dbw->insertId();
330
331
		# Notify extensions
332
		Hooks::run( 'RecentChange_save', [ &$this ] );
333
334
		if ( count( $this->tags ) ) {
335
			ChangeTags::addTags( $this->tags, $this->mAttribs['rc_id'],
336
				$this->mAttribs['rc_this_oldid'], $this->mAttribs['rc_logid'], null, $this );
337
		}
338
339
		# Notify external application via UDP
340
		if ( !$noudp ) {
341
			$this->notifyRCFeeds();
342
		}
343
344
		# E-mail notifications
345
		if ( $wgUseEnotif || $wgShowUpdatedMarker ) {
346
			$editor = $this->getPerformer();
347
			$title = $this->getTitle();
348
349
			// Never send an RC notification email about categorization changes
350
			if (
351
				$this->mAttribs['rc_type'] != RC_CATEGORIZE &&
352
				Hooks::run( 'AbortEmailNotification', [ $editor, $title, $this ] )
353
			) {
354
				// @FIXME: This would be better as an extension hook
355
				// Send emails or email jobs once this row is safely committed
356
				$dbw->onTransactionIdle(
357
					function () use ( $editor, $title ) {
358
						$enotif = new EmailNotification();
359
						$enotif->notifyOnPageChange(
360
							$editor,
361
							$title,
362
							$this->mAttribs['rc_timestamp'],
363
							$this->mAttribs['rc_comment'],
364
							$this->mAttribs['rc_minor'],
365
							$this->mAttribs['rc_last_oldid'],
366
							$this->mExtra['pageStatus']
367
						);
368
					},
369
					__METHOD__
370
				);
371
			}
372
		}
373
374
		// Update the cached list of active users
375
		if ( $this->mAttribs['rc_user'] > 0 ) {
376
			JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newCacheUpdateJob() );
377
		}
378
	}
379
380
	/**
381
	 * Notify all the feeds about the change.
382
	 * @param array $feeds Optional feeds to send to, defaults to $wgRCFeeds
383
	 */
384
	public function notifyRCFeeds( array $feeds = null ) {
385
		global $wgRCFeeds;
386
		if ( $feeds === null ) {
387
			$feeds = $wgRCFeeds;
388
		}
389
390
		$performer = $this->getPerformer();
391
392
		foreach ( $feeds as $feed ) {
393
			$feed += [
394
				'omit_bots' => false,
395
				'omit_anon' => false,
396
				'omit_user' => false,
397
				'omit_minor' => false,
398
				'omit_patrolled' => false,
399
			];
400
401
			if (
402
				( $feed['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
403
				( $feed['omit_anon'] && $performer->isAnon() ) ||
404
				( $feed['omit_user'] && !$performer->isAnon() ) ||
405
				( $feed['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
406
				( $feed['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
407
				$this->mAttribs['rc_type'] == RC_EXTERNAL
408
			) {
409
				continue;
410
			}
411
412
			$engine = self::getEngine( $feed['uri'] );
413
414
			if ( isset( $this->mExtra['actionCommentIRC'] ) ) {
415
				$actionComment = $this->mExtra['actionCommentIRC'];
416
			} else {
417
				$actionComment = null;
418
			}
419
420
			/** @var $formatter RCFeedFormatter */
421
			$formatter = is_object( $feed['formatter'] ) ? $feed['formatter'] : new $feed['formatter']();
422
			$line = $formatter->getLine( $feed, $this, $actionComment );
423
			if ( !$line ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $line of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
424
				// T109544
425
				// If a feed formatter returns null, this will otherwise cause an
426
				// error in at least RedisPubSubFeedEngine.
427
				// Not sure where/how this should best be handled.
428
				continue;
429
			}
430
431
			$engine->send( $feed, $line );
432
		}
433
	}
434
435
	/**
436
	 * Gets the stream engine object for a given URI from $wgRCEngines
437
	 *
438
	 * @param string $uri URI to get the engine object for
439
	 * @throws MWException
440
	 * @return RCFeedEngine The engine object
441
	 */
442
	public static function getEngine( $uri ) {
443
		global $wgRCEngines;
444
445
		$scheme = parse_url( $uri, PHP_URL_SCHEME );
446
		if ( !$scheme ) {
447
			throw new MWException( __FUNCTION__ . ": Invalid stream logger URI: '$uri'" );
448
		}
449
450
		if ( !isset( $wgRCEngines[$scheme] ) ) {
451
			throw new MWException( __FUNCTION__ . ": Unknown stream logger URI scheme: $scheme" );
452
		}
453
454
		return new $wgRCEngines[$scheme];
455
	}
456
457
	/**
458
	 * Mark a given change as patrolled
459
	 *
460
	 * @param RecentChange|int $change RecentChange or corresponding rc_id
461
	 * @param bool $auto For automatic patrol
462
	 * @param string|string[] $tags Change tags to add to the patrol log entry
463
	 *   ($user should be able to add the specified tags before this is called)
464
	 * @return array See doMarkPatrolled(), or null if $change is not an existing rc_id
465
	 */
466
	public static function markPatrolled( $change, $auto = false, $tags = null ) {
467
		global $wgUser;
468
469
		$change = $change instanceof RecentChange
470
			? $change
471
			: RecentChange::newFromId( $change );
472
473
		if ( !$change instanceof RecentChange ) {
474
			return null;
475
		}
476
477
		return $change->doMarkPatrolled( $wgUser, $auto, $tags );
478
	}
479
480
	/**
481
	 * Mark this RecentChange as patrolled
482
	 *
483
	 * NOTE: Can also return 'rcpatroldisabled', 'hookaborted' and
484
	 * 'markedaspatrollederror-noautopatrol' as errors
485
	 * @param User $user User object doing the action
486
	 * @param bool $auto For automatic patrol
487
	 * @param string|string[] $tags Change tags to add to the patrol log entry
488
	 *   ($user should be able to add the specified tags before this is called)
489
	 * @return array Array of permissions errors, see Title::getUserPermissionsErrors()
490
	 */
491
	public function doMarkPatrolled( User $user, $auto = false, $tags = null ) {
492
		global $wgUseRCPatrol, $wgUseNPPatrol, $wgUseFilePatrol;
493
494
		$errors = [];
495
		// If recentchanges patrol is disabled, only new pages or new file versions
496
		// can be patrolled, provided the appropriate config variable is set
497
		if ( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) &&
498
			( !$wgUseFilePatrol || !( $this->getAttribute( 'rc_type' ) == RC_LOG &&
499
			$this->getAttribute( 'rc_log_type' ) == 'upload' ) ) ) {
500
			$errors[] = [ 'rcpatroldisabled' ];
501
		}
502
		// Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
503
		$right = $auto ? 'autopatrol' : 'patrol';
504
		$errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $user ) );
505
		if ( !Hooks::run( 'MarkPatrolled',
506
					[ $this->getAttribute( 'rc_id' ), &$user, false, $auto ] )
507
		) {
508
			$errors[] = [ 'hookaborted' ];
509
		}
510
		// Users without the 'autopatrol' right can't patrol their
511
		// own revisions
512
		if ( $user->getName() === $this->getAttribute( 'rc_user_text' )
513
			&& !$user->isAllowed( 'autopatrol' )
514
		) {
515
			$errors[] = [ 'markedaspatrollederror-noautopatrol' ];
516
		}
517
		if ( $errors ) {
518
			return $errors;
519
		}
520
		// If the change was patrolled already, do nothing
521
		if ( $this->getAttribute( 'rc_patrolled' ) ) {
522
			return [];
523
		}
524
		// Actually set the 'patrolled' flag in RC
525
		$this->reallyMarkPatrolled();
526
		// Log this patrol event
527
		PatrolLog::record( $this, $auto, $user, $tags );
528
529
		Hooks::run(
530
			'MarkPatrolledComplete',
531
			[ $this->getAttribute( 'rc_id' ), &$user, false, $auto ]
532
		);
533
534
		return [];
535
	}
536
537
	/**
538
	 * Mark this RecentChange patrolled, without error checking
539
	 * @return int Number of affected rows
540
	 */
541
	public function reallyMarkPatrolled() {
542
		$dbw = wfGetDB( DB_MASTER );
543
		$dbw->update(
544
			'recentchanges',
545
			[
546
				'rc_patrolled' => 1
547
			],
548
			[
549
				'rc_id' => $this->getAttribute( 'rc_id' )
550
			],
551
			__METHOD__
552
		);
553
		// Invalidate the page cache after the page has been patrolled
554
		// to make sure that the Patrol link isn't visible any longer!
555
		$this->getTitle()->invalidateCache();
556
557
		return $dbw->affectedRows();
558
	}
559
560
	/**
561
	 * Makes an entry in the database corresponding to an edit
562
	 *
563
	 * @param string $timestamp
564
	 * @param Title $title
565
	 * @param bool $minor
566
	 * @param User $user
567
	 * @param string $comment
568
	 * @param int $oldId
569
	 * @param string $lastTimestamp
570
	 * @param bool $bot
571
	 * @param string $ip
572
	 * @param int $oldSize
573
	 * @param int $newSize
574
	 * @param int $newId
575
	 * @param int $patrol
576
	 * @param array $tags
577
	 * @return RecentChange
578
	 */
579
	public static function notifyEdit(
580
		$timestamp, &$title, $minor, &$user, $comment, $oldId, $lastTimestamp,
581
		$bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0,
582
		$tags = []
583
	) {
584
		$rc = new RecentChange;
585
		$rc->mTitle = $title;
586
		$rc->mPerformer = $user;
587
		$rc->mAttribs = [
588
			'rc_timestamp' => $timestamp,
589
			'rc_namespace' => $title->getNamespace(),
590
			'rc_title' => $title->getDBkey(),
591
			'rc_type' => RC_EDIT,
592
			'rc_source' => self::SRC_EDIT,
593
			'rc_minor' => $minor ? 1 : 0,
594
			'rc_cur_id' => $title->getArticleID(),
595
			'rc_user' => $user->getId(),
596
			'rc_user_text' => $user->getName(),
597
			'rc_comment' => $comment,
598
			'rc_this_oldid' => $newId,
599
			'rc_last_oldid' => $oldId,
600
			'rc_bot' => $bot ? 1 : 0,
601
			'rc_ip' => self::checkIPAddress( $ip ),
602
			'rc_patrolled' => intval( $patrol ),
603
			'rc_new' => 0, # obsolete
604
			'rc_old_len' => $oldSize,
605
			'rc_new_len' => $newSize,
606
			'rc_deleted' => 0,
607
			'rc_logid' => 0,
608
			'rc_log_type' => null,
609
			'rc_log_action' => '',
610
			'rc_params' => ''
611
		];
612
613
		$rc->mExtra = [
614
			'prefixedDBkey' => $title->getPrefixedDBkey(),
615
			'lastTimestamp' => $lastTimestamp,
616
			'oldSize' => $oldSize,
617
			'newSize' => $newSize,
618
			'pageStatus' => 'changed'
619
		];
620
621
		DeferredUpdates::addCallableUpdate(
622 View Code Duplication
			function () use ( $rc, $tags ) {
623
				$rc->addTags( $tags );
624
				$rc->save();
625
				if ( $rc->mAttribs['rc_patrolled'] ) {
626
					PatrolLog::record( $rc, true, $rc->getPerformer() );
627
				}
628
			},
629
			DeferredUpdates::POSTSEND,
630
			wfGetDB( DB_MASTER )
631
		);
632
633
		return $rc;
634
	}
635
636
	/**
637
	 * Makes an entry in the database corresponding to page creation
638
	 * Note: the title object must be loaded with the new id using resetArticleID()
639
	 *
640
	 * @param string $timestamp
641
	 * @param Title $title
642
	 * @param bool $minor
643
	 * @param User $user
644
	 * @param string $comment
645
	 * @param bool $bot
646
	 * @param string $ip
647
	 * @param int $size
648
	 * @param int $newId
649
	 * @param int $patrol
650
	 * @param array $tags
651
	 * @return RecentChange
652
	 */
653
	public static function notifyNew(
654
		$timestamp, &$title, $minor, &$user, $comment, $bot,
655
		$ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = []
656
	) {
657
		$rc = new RecentChange;
658
		$rc->mTitle = $title;
659
		$rc->mPerformer = $user;
660
		$rc->mAttribs = [
661
			'rc_timestamp' => $timestamp,
662
			'rc_namespace' => $title->getNamespace(),
663
			'rc_title' => $title->getDBkey(),
664
			'rc_type' => RC_NEW,
665
			'rc_source' => self::SRC_NEW,
666
			'rc_minor' => $minor ? 1 : 0,
667
			'rc_cur_id' => $title->getArticleID(),
668
			'rc_user' => $user->getId(),
669
			'rc_user_text' => $user->getName(),
670
			'rc_comment' => $comment,
671
			'rc_this_oldid' => $newId,
672
			'rc_last_oldid' => 0,
673
			'rc_bot' => $bot ? 1 : 0,
674
			'rc_ip' => self::checkIPAddress( $ip ),
675
			'rc_patrolled' => intval( $patrol ),
676
			'rc_new' => 1, # obsolete
677
			'rc_old_len' => 0,
678
			'rc_new_len' => $size,
679
			'rc_deleted' => 0,
680
			'rc_logid' => 0,
681
			'rc_log_type' => null,
682
			'rc_log_action' => '',
683
			'rc_params' => ''
684
		];
685
686
		$rc->mExtra = [
687
			'prefixedDBkey' => $title->getPrefixedDBkey(),
688
			'lastTimestamp' => 0,
689
			'oldSize' => 0,
690
			'newSize' => $size,
691
			'pageStatus' => 'created'
692
		];
693
694
		DeferredUpdates::addCallableUpdate(
695 View Code Duplication
			function () use ( $rc, $tags ) {
696
				$rc->addTags( $tags );
697
				$rc->save();
698
				if ( $rc->mAttribs['rc_patrolled'] ) {
699
					PatrolLog::record( $rc, true, $rc->getPerformer() );
700
				}
701
			},
702
			DeferredUpdates::POSTSEND,
703
			wfGetDB( DB_MASTER )
704
		);
705
706
		return $rc;
707
	}
708
709
	/**
710
	 * @param string $timestamp
711
	 * @param Title $title
712
	 * @param User $user
713
	 * @param string $actionComment
714
	 * @param string $ip
715
	 * @param string $type
716
	 * @param string $action
717
	 * @param Title $target
718
	 * @param string $logComment
719
	 * @param string $params
720
	 * @param int $newId
721
	 * @param string $actionCommentIRC
722
	 * @return bool
723
	 */
724
	public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip, $type,
725
		$action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
726
	) {
727
		global $wgLogRestrictions;
728
729
		# Don't add private logs to RC!
730
		if ( isset( $wgLogRestrictions[$type] ) && $wgLogRestrictions[$type] != '*' ) {
731
			return false;
732
		}
733
		$rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action,
734
			$target, $logComment, $params, $newId, $actionCommentIRC );
735
		$rc->save();
736
737
		return true;
738
	}
739
740
	/**
741
	 * @param string $timestamp
742
	 * @param Title $title
743
	 * @param User $user
744
	 * @param string $actionComment
745
	 * @param string $ip
746
	 * @param string $type
747
	 * @param string $action
748
	 * @param Title $target
749
	 * @param string $logComment
750
	 * @param string $params
751
	 * @param int $newId
752
	 * @param string $actionCommentIRC
753
	 * @param int $revId Id of associated revision, if any
754
	 * @param bool $isPatrollable Whether this log entry is patrollable
755
	 * @return RecentChange
756
	 */
757
	public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip,
758
		$type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '',
759
		$revId = 0, $isPatrollable = false ) {
760
		global $wgRequest;
761
762
		# # Get pageStatus for email notification
763
		switch ( $type . '-' . $action ) {
764
			case 'delete-delete':
765
				$pageStatus = 'deleted';
766
				break;
767
			case 'move-move':
768
			case 'move-move_redir':
769
				$pageStatus = 'moved';
770
				break;
771
			case 'delete-restore':
772
				$pageStatus = 'restored';
773
				break;
774
			case 'upload-upload':
775
				$pageStatus = 'created';
776
				break;
777
			case 'upload-overwrite':
778
			default:
779
				$pageStatus = 'changed';
780
				break;
781
		}
782
783
		// Allow unpatrolled status for patrollable log entries
784
		$markPatrolled = $isPatrollable ? $user->isAllowed( 'autopatrol' ) : true;
785
786
		$rc = new RecentChange;
787
		$rc->mTitle = $target;
788
		$rc->mPerformer = $user;
789
		$rc->mAttribs = [
790
			'rc_timestamp' => $timestamp,
791
			'rc_namespace' => $target->getNamespace(),
792
			'rc_title' => $target->getDBkey(),
793
			'rc_type' => RC_LOG,
794
			'rc_source' => self::SRC_LOG,
795
			'rc_minor' => 0,
796
			'rc_cur_id' => $target->getArticleID(),
797
			'rc_user' => $user->getId(),
798
			'rc_user_text' => $user->getName(),
799
			'rc_comment' => $logComment,
800
			'rc_this_oldid' => $revId,
801
			'rc_last_oldid' => 0,
802
			'rc_bot' => $user->isAllowed( 'bot' ) ? (int)$wgRequest->getBool( 'bot', true ) : 0,
803
			'rc_ip' => self::checkIPAddress( $ip ),
804
			'rc_patrolled' => $markPatrolled ? 1 : 0,
805
			'rc_new' => 0, # obsolete
806
			'rc_old_len' => null,
807
			'rc_new_len' => null,
808
			'rc_deleted' => 0,
809
			'rc_logid' => $newId,
810
			'rc_log_type' => $type,
811
			'rc_log_action' => $action,
812
			'rc_params' => $params
813
		];
814
815
		$rc->mExtra = [
816
			'prefixedDBkey' => $title->getPrefixedDBkey(),
817
			'lastTimestamp' => 0,
818
			'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
819
			'pageStatus' => $pageStatus,
820
			'actionCommentIRC' => $actionCommentIRC
821
		];
822
823
		return $rc;
824
	}
825
826
	/**
827
	 * Constructs a RecentChange object for the given categorization
828
	 * This does not call save() on the object and thus does not write to the db
829
	 *
830
	 * @since 1.27
831
	 *
832
	 * @param string $timestamp Timestamp of the recent change to occur
833
	 * @param Title $categoryTitle Title of the category a page is being added to or removed from
834
	 * @param User $user User object of the user that made the change
835
	 * @param string $comment Change summary
836
	 * @param Title $pageTitle Title of the page that is being added or removed
837
	 * @param int $oldRevId Parent revision ID of this change
838
	 * @param int $newRevId Revision ID of this change
839
	 * @param string $lastTimestamp Parent revision timestamp of this change
840
	 * @param bool $bot true, if the change was made by a bot
841
	 * @param string $ip IP address of the user, if the change was made anonymously
842
	 * @param int $deleted Indicates whether the change has been deleted
843
	 *
844
	 * @return RecentChange
845
	 */
846
	public static function newForCategorization(
847
		$timestamp,
848
		Title $categoryTitle,
849
		User $user = null,
850
		$comment,
851
		Title $pageTitle,
852
		$oldRevId,
853
		$newRevId,
854
		$lastTimestamp,
855
		$bot,
856
		$ip = '',
857
		$deleted = 0
858
	) {
859
		$rc = new RecentChange;
860
		$rc->mTitle = $categoryTitle;
861
		$rc->mPerformer = $user;
862
		$rc->mAttribs = [
863
			'rc_timestamp' => $timestamp,
864
			'rc_namespace' => $categoryTitle->getNamespace(),
865
			'rc_title' => $categoryTitle->getDBkey(),
866
			'rc_type' => RC_CATEGORIZE,
867
			'rc_source' => self::SRC_CATEGORIZE,
868
			'rc_minor' => 0,
869
			'rc_cur_id' => $pageTitle->getArticleID(),
870
			'rc_user' => $user ? $user->getId() : 0,
871
			'rc_user_text' => $user ? $user->getName() : '',
872
			'rc_comment' => $comment,
873
			'rc_this_oldid' => $newRevId,
874
			'rc_last_oldid' => $oldRevId,
875
			'rc_bot' => $bot ? 1 : 0,
876
			'rc_ip' => self::checkIPAddress( $ip ),
877
			'rc_patrolled' => 1, // Always patrolled, just like log entries
878
			'rc_new' => 0, # obsolete
879
			'rc_old_len' => null,
880
			'rc_new_len' => null,
881
			'rc_deleted' => $deleted,
882
			'rc_logid' => 0,
883
			'rc_log_type' => null,
884
			'rc_log_action' => '',
885
			'rc_params' =>  serialize( [
886
				'hidden-cat' => WikiCategoryPage::factory( $categoryTitle )->isHidden()
887
			] )
888
		];
889
890
		$rc->mExtra = [
891
			'prefixedDBkey' => $categoryTitle->getPrefixedDBkey(),
892
			'lastTimestamp' => $lastTimestamp,
893
			'oldSize' => 0,
894
			'newSize' => 0,
895
			'pageStatus' => 'changed'
896
		];
897
898
		return $rc;
899
	}
900
901
	/**
902
	 * Get a parameter value
903
	 *
904
	 * @since 1.27
905
	 *
906
	 * @param string $name parameter name
907
	 * @return mixed
908
	 */
909
	public function getParam( $name ) {
910
		$params = $this->parseParams();
911
		return isset( $params[$name] ) ? $params[$name] : null;
912
	}
913
914
	/**
915
	 * Initialises the members of this object from a mysql row object
916
	 *
917
	 * @param mixed $row
918
	 */
919
	public function loadFromRow( $row ) {
920
		$this->mAttribs = get_object_vars( $row );
921
		$this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
922
		$this->mAttribs['rc_deleted'] = $row->rc_deleted; // MUST be set
923
	}
924
925
	/**
926
	 * Get an attribute value
927
	 *
928
	 * @param string $name Attribute name
929
	 * @return mixed
930
	 */
931
	public function getAttribute( $name ) {
932
		return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : null;
933
	}
934
935
	/**
936
	 * @return array
937
	 */
938
	public function getAttributes() {
939
		return $this->mAttribs;
940
	}
941
942
	/**
943
	 * Gets the end part of the diff URL associated with this object
944
	 * Blank if no diff link should be displayed
945
	 * @param bool $forceCur
946
	 * @return string
947
	 */
948
	public function diffLinkTrail( $forceCur ) {
949
		if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
950
			$trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
951
				"&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
952
			if ( $forceCur ) {
953
				$trail .= '&diff=0';
954
			} else {
955
				$trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
956
			}
957
		} else {
958
			$trail = '';
959
		}
960
961
		return $trail;
962
	}
963
964
	/**
965
	 * Returns the change size (HTML).
966
	 * The lengths can be given optionally.
967
	 * @param int $old
968
	 * @param int $new
969
	 * @return string
970
	 */
971
	public function getCharacterDifference( $old = 0, $new = 0 ) {
972
		if ( $old === 0 ) {
973
			$old = $this->mAttribs['rc_old_len'];
974
		}
975
		if ( $new === 0 ) {
976
			$new = $this->mAttribs['rc_new_len'];
977
		}
978
		if ( $old === null || $new === null ) {
979
			return '';
980
		}
981
982
		return ChangesList::showCharacterDifference( $old, $new );
983
	}
984
985
	private static function checkIPAddress( $ip ) {
986
		global $wgRequest;
987
		if ( $ip ) {
988
			if ( !IP::isIPAddress( $ip ) ) {
989
				throw new MWException( "Attempt to write \"" . $ip .
990
					"\" as an IP address into recent changes" );
991
			}
992
		} else {
993
			$ip = $wgRequest->getIP();
994
			if ( !$ip ) {
995
				$ip = '';
996
			}
997
		}
998
999
		return $ip;
1000
	}
1001
1002
	/**
1003
	 * Check whether the given timestamp is new enough to have a RC row with a given tolerance
1004
	 * as the recentchanges table might not be cleared out regularly (so older entries might exist)
1005
	 * or rows which will be deleted soon shouldn't be included.
1006
	 *
1007
	 * @param mixed $timestamp MWTimestamp compatible timestamp
1008
	 * @param int $tolerance Tolerance in seconds
1009
	 * @return bool
1010
	 */
1011
	public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
1012
		global $wgRCMaxAge;
1013
1014
		return wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $wgRCMaxAge;
1015
	}
1016
1017
	/**
1018
	 * Parses and returns the rc_params attribute
1019
	 *
1020
	 * @since 1.26
1021
	 *
1022
	 * @return mixed|bool false on failed unserialization
1023
	 */
1024
	public function parseParams() {
1025
		$rcParams = $this->getAttribute( 'rc_params' );
1026
1027
		MediaWiki\suppressWarnings();
1028
		$unserializedParams = unserialize( $rcParams );
1029
		MediaWiki\restoreWarnings();
1030
1031
		return $unserializedParams;
1032
	}
1033
1034
	/**
1035
	 * Tags to append to the recent change,
1036
	 * and associated revision/log
1037
	 *
1038
	 * @since 1.28
1039
	 *
1040
	 * @param string|array $tags
1041
	 */
1042
	public function addTags( $tags ) {
1043
		if ( is_string( $tags ) ) {
1044
			$this->tags[] = $tags;
1045
		} else {
1046
			$this->tags = array_merge( $tags, $this->tags );
1047
		}
1048
	}
1049
}
1050