Completed
Branch master (fc4983)
by
unknown
30:21
created

RecentChange::save()   F

Complexity

Conditions 16
Paths 1536

Size

Total Lines 85
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 16
eloc 42
nc 1536
nop 1
dl 0
loc 85
rs 2
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Array of change types
95
	 */
96
	private static $changeTypes = [
97
		'edit' => RC_EDIT,
98
		'new' => RC_NEW,
99
		'log' => RC_LOG,
100
		'external' => RC_EXTERNAL,
101
		'categorize' => RC_CATEGORIZE,
102
	];
103
104
	# Factory methods
105
106
	/**
107
	 * @param mixed $row
108
	 * @return RecentChange
109
	 */
110
	public static function newFromRow( $row ) {
111
		$rc = new RecentChange;
112
		$rc->loadFromRow( $row );
113
114
		return $rc;
115
	}
116
117
	/**
118
	 * Parsing text to RC_* constants
119
	 * @since 1.24
120
	 * @param string|array $type
121
	 * @throws MWException
122
	 * @return int|array RC_TYPE
123
	 */
124
	public static function parseToRCType( $type ) {
125
		if ( is_array( $type ) ) {
126
			$retval = [];
127
			foreach ( $type as $t ) {
128
				$retval[] = RecentChange::parseToRCType( $t );
129
			}
130
131
			return $retval;
132
		}
133
134
		if ( !array_key_exists( $type, self::$changeTypes ) ) {
135
			throw new MWException( "Unknown type '$type'" );
136
		}
137
		return self::$changeTypes[$type];
138
	}
139
140
	/**
141
	 * Parsing RC_* constants to human-readable test
142
	 * @since 1.24
143
	 * @param int $rcType
144
	 * @return string $type
145
	 */
146
	public static function parseFromRCType( $rcType ) {
147
		return array_search( $rcType, self::$changeTypes, true ) ?: "$rcType";
148
	}
149
150
	/**
151
	 * Get an array of all change types
152
	 *
153
	 * @since 1.26
154
	 *
155
	 * @return array
156
	 */
157
	public static function getChangeTypes() {
158
		return array_keys( self::$changeTypes );
159
	}
160
161
	/**
162
	 * Obtain the recent change with a given rc_id value
163
	 *
164
	 * @param int $rcid The rc_id value to retrieve
165
	 * @return RecentChange|null
166
	 */
167
	public static function newFromId( $rcid ) {
168
		return self::newFromConds( [ 'rc_id' => $rcid ], __METHOD__ );
169
	}
170
171
	/**
172
	 * Find the first recent change matching some specific conditions
173
	 *
174
	 * @param array $conds Array of conditions
175
	 * @param mixed $fname Override the method name in profiling/logs
176
	 * @param int $dbType DB_* constant
177
	 *
178
	 * @return RecentChange|null
179
	 */
180
	public static function newFromConds(
181
		$conds,
182
		$fname = __METHOD__,
183
		$dbType = DB_REPLICA
184
	) {
185
		$db = wfGetDB( $dbType );
186
		$row = $db->selectRow( 'recentchanges', self::selectFields(), $conds, $fname );
187
		if ( $row !== false ) {
188
			return self::newFromRow( $row );
189
		} else {
190
			return null;
191
		}
192
	}
193
194
	/**
195
	 * Return the list of recentchanges fields that should be selected to create
196
	 * a new recentchanges object.
197
	 * @return array
198
	 */
199
	public static function selectFields() {
200
		return [
201
			'rc_id',
202
			'rc_timestamp',
203
			'rc_user',
204
			'rc_user_text',
205
			'rc_namespace',
206
			'rc_title',
207
			'rc_comment',
208
			'rc_minor',
209
			'rc_bot',
210
			'rc_new',
211
			'rc_cur_id',
212
			'rc_this_oldid',
213
			'rc_last_oldid',
214
			'rc_type',
215
			'rc_source',
216
			'rc_patrolled',
217
			'rc_ip',
218
			'rc_old_len',
219
			'rc_new_len',
220
			'rc_deleted',
221
			'rc_logid',
222
			'rc_log_type',
223
			'rc_log_action',
224
			'rc_params',
225
		];
226
	}
227
228
	# Accessors
229
230
	/**
231
	 * @param array $attribs
232
	 */
233
	public function setAttribs( $attribs ) {
234
		$this->mAttribs = $attribs;
235
	}
236
237
	/**
238
	 * @param array $extra
239
	 */
240
	public function setExtra( $extra ) {
241
		$this->mExtra = $extra;
242
	}
243
244
	/**
245
	 * @return Title
246
	 */
247
	public function &getTitle() {
248
		if ( $this->mTitle === false ) {
249
			$this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
250
		}
251
252
		return $this->mTitle;
253
	}
254
255
	/**
256
	 * Get the User object of the person who performed this change.
257
	 *
258
	 * @return User
259
	 */
260
	public function getPerformer() {
261
		if ( $this->mPerformer === false ) {
262
			if ( $this->mAttribs['rc_user'] ) {
263
				$this->mPerformer = User::newFromId( $this->mAttribs['rc_user'] );
264
			} else {
265
				$this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false );
0 ignored issues
show
Documentation Bug introduced by
It seems like \User::newFromName($this...'rc_user_text'], false) can also be of type false. However, the property $mPerformer is declared as type object<User>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
266
			}
267
		}
268
269
		return $this->mPerformer;
270
	}
271
272
	/**
273
	 * Writes the data in this object to the database
274
	 * @param bool $noudp
275
	 */
276
	public function save( $noudp = false ) {
277
		global $wgPutIPinRC, $wgUseEnotif, $wgShowUpdatedMarker, $wgContLang;
278
279
		$dbw = wfGetDB( DB_MASTER );
280
		if ( !is_array( $this->mExtra ) ) {
281
			$this->mExtra = [];
282
		}
283
284
		if ( !$wgPutIPinRC ) {
285
			$this->mAttribs['rc_ip'] = '';
286
		}
287
288
		# Strict mode fixups (not-NULL fields)
289
		foreach ( [ 'minor', 'bot', 'new', 'patrolled', 'deleted' ] as $field ) {
290
			$this->mAttribs["rc_$field"] = (int)$this->mAttribs["rc_$field"];
291
		}
292
		# ...more fixups (NULL fields)
293
		foreach ( [ 'old_len', 'new_len' ] as $field ) {
294
			$this->mAttribs["rc_$field"] = isset( $this->mAttribs["rc_$field"] )
295
				? (int)$this->mAttribs["rc_$field"]
296
				: null;
297
		}
298
299
		# If our database is strict about IP addresses, use NULL instead of an empty string
300
		if ( $dbw->strictIPs() && $this->mAttribs['rc_ip'] == '' ) {
301
			unset( $this->mAttribs['rc_ip'] );
302
		}
303
304
		# Trim spaces on user supplied text
305
		$this->mAttribs['rc_comment'] = trim( $this->mAttribs['rc_comment'] );
306
307
		# Make sure summary is truncated (whole multibyte characters)
308
		$this->mAttribs['rc_comment'] = $wgContLang->truncate( $this->mAttribs['rc_comment'], 255 );
309
310
		# Fixup database timestamps
311
		$this->mAttribs['rc_timestamp'] = $dbw->timestamp( $this->mAttribs['rc_timestamp'] );
312
		$this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'recentchanges_rc_id_seq' );
313
314
		# # If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
315
		if ( $dbw->cascadingDeletes() && $this->mAttribs['rc_cur_id'] == 0 ) {
316
			unset( $this->mAttribs['rc_cur_id'] );
317
		}
318
319
		# Insert new row
320
		$dbw->insert( 'recentchanges', $this->mAttribs, __METHOD__ );
321
322
		# Set the ID
323
		$this->mAttribs['rc_id'] = $dbw->insertId();
324
325
		# Notify extensions
326
		Hooks::run( 'RecentChange_save', [ &$this ] );
327
328
		# Notify external application via UDP
329
		if ( !$noudp ) {
330
			$this->notifyRCFeeds();
331
		}
332
333
		# E-mail notifications
334
		if ( $wgUseEnotif || $wgShowUpdatedMarker ) {
335
			$editor = $this->getPerformer();
336
			$title = $this->getTitle();
337
338
			// Never send an RC notification email about categorization changes
339
			if ( $this->mAttribs['rc_type'] != RC_CATEGORIZE ) {
340
				if ( Hooks::run( 'AbortEmailNotification', [ $editor, $title, $this ] ) ) {
341
					# @todo FIXME: This would be better as an extension hook
342
					$enotif = new EmailNotification();
343
					$enotif->notifyOnPageChange(
344
						$editor,
345
						$title,
346
						$this->mAttribs['rc_timestamp'],
347
						$this->mAttribs['rc_comment'],
348
						$this->mAttribs['rc_minor'],
349
						$this->mAttribs['rc_last_oldid'],
350
						$this->mExtra['pageStatus']
351
					);
352
				}
353
			}
354
		}
355
356
		// Update the cached list of active users
357
		if ( $this->mAttribs['rc_user'] > 0 ) {
358
			JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newCacheUpdateJob() );
359
		}
360
	}
361
362
	/**
363
	 * Notify all the feeds about the change.
364
	 * @param array $feeds Optional feeds to send to, defaults to $wgRCFeeds
365
	 */
366
	public function notifyRCFeeds( array $feeds = null ) {
367
		global $wgRCFeeds;
368
		if ( $feeds === null ) {
369
			$feeds = $wgRCFeeds;
370
		}
371
372
		$performer = $this->getPerformer();
373
374
		foreach ( $feeds as $feed ) {
375
			$feed += [
376
				'omit_bots' => false,
377
				'omit_anon' => false,
378
				'omit_user' => false,
379
				'omit_minor' => false,
380
				'omit_patrolled' => false,
381
			];
382
383
			if (
384
				( $feed['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
385
				( $feed['omit_anon'] && $performer->isAnon() ) ||
386
				( $feed['omit_user'] && !$performer->isAnon() ) ||
387
				( $feed['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
388
				( $feed['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
389
				$this->mAttribs['rc_type'] == RC_EXTERNAL
390
			) {
391
				continue;
392
			}
393
394
			$engine = self::getEngine( $feed['uri'] );
395
396
			if ( isset( $this->mExtra['actionCommentIRC'] ) ) {
397
				$actionComment = $this->mExtra['actionCommentIRC'];
398
			} else {
399
				$actionComment = null;
400
			}
401
402
			/** @var $formatter RCFeedFormatter */
403
			$formatter = is_object( $feed['formatter'] ) ? $feed['formatter'] : new $feed['formatter']();
404
			$line = $formatter->getLine( $feed, $this, $actionComment );
405
			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...
406
				// T109544
407
				// If a feed formatter returns null, this will otherwise cause an
408
				// error in at least RedisPubSubFeedEngine.
409
				// Not sure where/how this should best be handled.
410
				continue;
411
			}
412
413
			$engine->send( $feed, $line );
414
		}
415
	}
416
417
	/**
418
	 * Gets the stream engine object for a given URI from $wgRCEngines
419
	 *
420
	 * @param string $uri URI to get the engine object for
421
	 * @throws MWException
422
	 * @return RCFeedEngine The engine object
423
	 */
424
	public static function getEngine( $uri ) {
425
		global $wgRCEngines;
426
427
		$scheme = parse_url( $uri, PHP_URL_SCHEME );
428
		if ( !$scheme ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $scheme of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false 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...
429
			throw new MWException( __FUNCTION__ . ": Invalid stream logger URI: '$uri'" );
430
		}
431
432
		if ( !isset( $wgRCEngines[$scheme] ) ) {
433
			throw new MWException( __FUNCTION__ . ": Unknown stream logger URI scheme: $scheme" );
434
		}
435
436
		return new $wgRCEngines[$scheme];
437
	}
438
439
	/**
440
	 * Mark a given change as patrolled
441
	 *
442
	 * @param RecentChange|int $change RecentChange or corresponding rc_id
443
	 * @param bool $auto For automatic patrol
444
	 * @param string|string[] $tags Change tags to add to the patrol log entry
445
	 *   ($user should be able to add the specified tags before this is called)
446
	 * @return array See doMarkPatrolled(), or null if $change is not an existing rc_id
447
	 */
448
	public static function markPatrolled( $change, $auto = false, $tags = null ) {
449
		global $wgUser;
450
451
		$change = $change instanceof RecentChange
452
			? $change
453
			: RecentChange::newFromId( $change );
454
455
		if ( !$change instanceof RecentChange ) {
456
			return null;
457
		}
458
459
		return $change->doMarkPatrolled( $wgUser, $auto, $tags );
460
	}
461
462
	/**
463
	 * Mark this RecentChange as patrolled
464
	 *
465
	 * NOTE: Can also return 'rcpatroldisabled', 'hookaborted' and
466
	 * 'markedaspatrollederror-noautopatrol' as errors
467
	 * @param User $user User object doing the action
468
	 * @param bool $auto For automatic patrol
469
	 * @param string|string[] $tags Change tags to add to the patrol log entry
470
	 *   ($user should be able to add the specified tags before this is called)
471
	 * @return array Array of permissions errors, see Title::getUserPermissionsErrors()
472
	 */
473
	public function doMarkPatrolled( User $user, $auto = false, $tags = null ) {
474
		global $wgUseRCPatrol, $wgUseNPPatrol, $wgUseFilePatrol;
475
476
		$errors = [];
477
		// If recentchanges patrol is disabled, only new pages or new file versions
478
		// can be patrolled, provided the appropriate config variable is set
479
		if ( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) &&
480
			( !$wgUseFilePatrol || !( $this->getAttribute( 'rc_type' ) == RC_LOG &&
481
			$this->getAttribute( 'rc_log_type' ) == 'upload' ) ) ) {
482
			$errors[] = [ 'rcpatroldisabled' ];
483
		}
484
		// Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
485
		$right = $auto ? 'autopatrol' : 'patrol';
486
		$errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $user ) );
487
		if ( !Hooks::run( 'MarkPatrolled',
488
					[ $this->getAttribute( 'rc_id' ), &$user, false, $auto ] )
489
		) {
490
			$errors[] = [ 'hookaborted' ];
491
		}
492
		// Users without the 'autopatrol' right can't patrol their
493
		// own revisions
494
		if ( $user->getName() === $this->getAttribute( 'rc_user_text' )
495
			&& !$user->isAllowed( 'autopatrol' )
496
		) {
497
			$errors[] = [ 'markedaspatrollederror-noautopatrol' ];
498
		}
499
		if ( $errors ) {
500
			return $errors;
501
		}
502
		// If the change was patrolled already, do nothing
503
		if ( $this->getAttribute( 'rc_patrolled' ) ) {
504
			return [];
505
		}
506
		// Actually set the 'patrolled' flag in RC
507
		$this->reallyMarkPatrolled();
508
		// Log this patrol event
509
		PatrolLog::record( $this, $auto, $user, $tags );
510
511
		Hooks::run(
512
			'MarkPatrolledComplete',
513
			[ $this->getAttribute( 'rc_id' ), &$user, false, $auto ]
514
		);
515
516
		return [];
517
	}
518
519
	/**
520
	 * Mark this RecentChange patrolled, without error checking
521
	 * @return int Number of affected rows
522
	 */
523
	public function reallyMarkPatrolled() {
524
		$dbw = wfGetDB( DB_MASTER );
525
		$dbw->update(
526
			'recentchanges',
527
			[
528
				'rc_patrolled' => 1
529
			],
530
			[
531
				'rc_id' => $this->getAttribute( 'rc_id' )
532
			],
533
			__METHOD__
534
		);
535
		// Invalidate the page cache after the page has been patrolled
536
		// to make sure that the Patrol link isn't visible any longer!
537
		$this->getTitle()->invalidateCache();
538
539
		return $dbw->affectedRows();
540
	}
541
542
	/**
543
	 * Makes an entry in the database corresponding to an edit
544
	 *
545
	 * @param string $timestamp
546
	 * @param Title $title
547
	 * @param bool $minor
548
	 * @param User $user
549
	 * @param string $comment
550
	 * @param int $oldId
551
	 * @param string $lastTimestamp
552
	 * @param bool $bot
553
	 * @param string $ip
554
	 * @param int $oldSize
555
	 * @param int $newSize
556
	 * @param int $newId
557
	 * @param int $patrol
558
	 * @param array $tags
559
	 * @return RecentChange
560
	 */
561
	public static function notifyEdit(
562
		$timestamp, &$title, $minor, &$user, $comment, $oldId, $lastTimestamp,
563
		$bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0,
564
		$tags = []
565
	) {
566
		$rc = new RecentChange;
567
		$rc->mTitle = $title;
568
		$rc->mPerformer = $user;
569
		$rc->mAttribs = [
570
			'rc_timestamp' => $timestamp,
571
			'rc_namespace' => $title->getNamespace(),
572
			'rc_title' => $title->getDBkey(),
573
			'rc_type' => RC_EDIT,
574
			'rc_source' => self::SRC_EDIT,
575
			'rc_minor' => $minor ? 1 : 0,
576
			'rc_cur_id' => $title->getArticleID(),
577
			'rc_user' => $user->getId(),
578
			'rc_user_text' => $user->getName(),
579
			'rc_comment' => $comment,
580
			'rc_this_oldid' => $newId,
581
			'rc_last_oldid' => $oldId,
582
			'rc_bot' => $bot ? 1 : 0,
583
			'rc_ip' => self::checkIPAddress( $ip ),
584
			'rc_patrolled' => intval( $patrol ),
585
			'rc_new' => 0, # obsolete
586
			'rc_old_len' => $oldSize,
587
			'rc_new_len' => $newSize,
588
			'rc_deleted' => 0,
589
			'rc_logid' => 0,
590
			'rc_log_type' => null,
591
			'rc_log_action' => '',
592
			'rc_params' => ''
593
		];
594
595
		$rc->mExtra = [
596
			'prefixedDBkey' => $title->getPrefixedDBkey(),
597
			'lastTimestamp' => $lastTimestamp,
598
			'oldSize' => $oldSize,
599
			'newSize' => $newSize,
600
			'pageStatus' => 'changed'
601
		];
602
603
		DeferredUpdates::addCallableUpdate(
604 View Code Duplication
			function () use ( $rc, $tags ) {
605
				$rc->save();
606
				if ( $rc->mAttribs['rc_patrolled'] ) {
607
					PatrolLog::record( $rc, true, $rc->getPerformer() );
608
				}
609
				if ( count( $tags ) ) {
610
					ChangeTags::addTags( $tags, $rc->mAttribs['rc_id'],
611
						$rc->mAttribs['rc_this_oldid'], null, null );
612
				}
613
			},
614
			DeferredUpdates::POSTSEND,
615
			wfGetDB( DB_MASTER )
616
		);
617
618
		return $rc;
619
	}
620
621
	/**
622
	 * Makes an entry in the database corresponding to page creation
623
	 * Note: the title object must be loaded with the new id using resetArticleID()
624
	 *
625
	 * @param string $timestamp
626
	 * @param Title $title
627
	 * @param bool $minor
628
	 * @param User $user
629
	 * @param string $comment
630
	 * @param bool $bot
631
	 * @param string $ip
632
	 * @param int $size
633
	 * @param int $newId
634
	 * @param int $patrol
635
	 * @param array $tags
636
	 * @return RecentChange
637
	 */
638
	public static function notifyNew(
639
		$timestamp, &$title, $minor, &$user, $comment, $bot,
640
		$ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = []
641
	) {
642
		$rc = new RecentChange;
643
		$rc->mTitle = $title;
644
		$rc->mPerformer = $user;
645
		$rc->mAttribs = [
646
			'rc_timestamp' => $timestamp,
647
			'rc_namespace' => $title->getNamespace(),
648
			'rc_title' => $title->getDBkey(),
649
			'rc_type' => RC_NEW,
650
			'rc_source' => self::SRC_NEW,
651
			'rc_minor' => $minor ? 1 : 0,
652
			'rc_cur_id' => $title->getArticleID(),
653
			'rc_user' => $user->getId(),
654
			'rc_user_text' => $user->getName(),
655
			'rc_comment' => $comment,
656
			'rc_this_oldid' => $newId,
657
			'rc_last_oldid' => 0,
658
			'rc_bot' => $bot ? 1 : 0,
659
			'rc_ip' => self::checkIPAddress( $ip ),
660
			'rc_patrolled' => intval( $patrol ),
661
			'rc_new' => 1, # obsolete
662
			'rc_old_len' => 0,
663
			'rc_new_len' => $size,
664
			'rc_deleted' => 0,
665
			'rc_logid' => 0,
666
			'rc_log_type' => null,
667
			'rc_log_action' => '',
668
			'rc_params' => ''
669
		];
670
671
		$rc->mExtra = [
672
			'prefixedDBkey' => $title->getPrefixedDBkey(),
673
			'lastTimestamp' => 0,
674
			'oldSize' => 0,
675
			'newSize' => $size,
676
			'pageStatus' => 'created'
677
		];
678
679
		DeferredUpdates::addCallableUpdate(
680 View Code Duplication
			function () use ( $rc, $tags ) {
681
				$rc->save();
682
				if ( $rc->mAttribs['rc_patrolled'] ) {
683
					PatrolLog::record( $rc, true, $rc->getPerformer() );
684
				}
685
				if ( count( $tags ) ) {
686
					ChangeTags::addTags( $tags, $rc->mAttribs['rc_id'],
687
						$rc->mAttribs['rc_this_oldid'], null, null );
688
				}
689
			},
690
			DeferredUpdates::POSTSEND,
691
			wfGetDB( DB_MASTER )
692
		);
693
694
		return $rc;
695
	}
696
697
	/**
698
	 * @param string $timestamp
699
	 * @param Title $title
700
	 * @param User $user
701
	 * @param string $actionComment
702
	 * @param string $ip
703
	 * @param string $type
704
	 * @param string $action
705
	 * @param Title $target
706
	 * @param string $logComment
707
	 * @param string $params
708
	 * @param int $newId
709
	 * @param string $actionCommentIRC
710
	 * @return bool
711
	 */
712
	public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip, $type,
713
		$action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
714
	) {
715
		global $wgLogRestrictions;
716
717
		# Don't add private logs to RC!
718
		if ( isset( $wgLogRestrictions[$type] ) && $wgLogRestrictions[$type] != '*' ) {
719
			return false;
720
		}
721
		$rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action,
722
			$target, $logComment, $params, $newId, $actionCommentIRC );
723
		$rc->save();
724
725
		return true;
726
	}
727
728
	/**
729
	 * @param string $timestamp
730
	 * @param Title $title
731
	 * @param User $user
732
	 * @param string $actionComment
733
	 * @param string $ip
734
	 * @param string $type
735
	 * @param string $action
736
	 * @param Title $target
737
	 * @param string $logComment
738
	 * @param string $params
739
	 * @param int $newId
740
	 * @param string $actionCommentIRC
741
	 * @param int $revId Id of associated revision, if any
742
	 * @param bool $isPatrollable Whether this log entry is patrollable
743
	 * @return RecentChange
744
	 */
745
	public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip,
746
		$type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '',
747
		$revId = 0, $isPatrollable = false ) {
748
		global $wgRequest;
749
750
		# # Get pageStatus for email notification
751
		switch ( $type . '-' . $action ) {
752
			case 'delete-delete':
753
				$pageStatus = 'deleted';
754
				break;
755
			case 'move-move':
756
			case 'move-move_redir':
757
				$pageStatus = 'moved';
758
				break;
759
			case 'delete-restore':
760
				$pageStatus = 'restored';
761
				break;
762
			case 'upload-upload':
763
				$pageStatus = 'created';
764
				break;
765
			case 'upload-overwrite':
766
			default:
767
				$pageStatus = 'changed';
768
				break;
769
		}
770
771
		// Allow unpatrolled status for patrollable log entries
772
		$markPatrolled = $isPatrollable ? $user->isAllowed( 'autopatrol' ) : true;
773
774
		$rc = new RecentChange;
775
		$rc->mTitle = $target;
776
		$rc->mPerformer = $user;
777
		$rc->mAttribs = [
778
			'rc_timestamp' => $timestamp,
779
			'rc_namespace' => $target->getNamespace(),
780
			'rc_title' => $target->getDBkey(),
781
			'rc_type' => RC_LOG,
782
			'rc_source' => self::SRC_LOG,
783
			'rc_minor' => 0,
784
			'rc_cur_id' => $target->getArticleID(),
785
			'rc_user' => $user->getId(),
786
			'rc_user_text' => $user->getName(),
787
			'rc_comment' => $logComment,
788
			'rc_this_oldid' => $revId,
789
			'rc_last_oldid' => 0,
790
			'rc_bot' => $user->isAllowed( 'bot' ) ? (int)$wgRequest->getBool( 'bot', true ) : 0,
791
			'rc_ip' => self::checkIPAddress( $ip ),
792
			'rc_patrolled' => $markPatrolled ? 1 : 0,
793
			'rc_new' => 0, # obsolete
794
			'rc_old_len' => null,
795
			'rc_new_len' => null,
796
			'rc_deleted' => 0,
797
			'rc_logid' => $newId,
798
			'rc_log_type' => $type,
799
			'rc_log_action' => $action,
800
			'rc_params' => $params
801
		];
802
803
		$rc->mExtra = [
804
			'prefixedDBkey' => $title->getPrefixedDBkey(),
805
			'lastTimestamp' => 0,
806
			'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
807
			'pageStatus' => $pageStatus,
808
			'actionCommentIRC' => $actionCommentIRC
809
		];
810
811
		return $rc;
812
	}
813
814
	/**
815
	 * Constructs a RecentChange object for the given categorization
816
	 * This does not call save() on the object and thus does not write to the db
817
	 *
818
	 * @since 1.27
819
	 *
820
	 * @param string $timestamp Timestamp of the recent change to occur
821
	 * @param Title $categoryTitle Title of the category a page is being added to or removed from
822
	 * @param User $user User object of the user that made the change
823
	 * @param string $comment Change summary
824
	 * @param Title $pageTitle Title of the page that is being added or removed
825
	 * @param int $oldRevId Parent revision ID of this change
826
	 * @param int $newRevId Revision ID of this change
827
	 * @param string $lastTimestamp Parent revision timestamp of this change
828
	 * @param bool $bot true, if the change was made by a bot
829
	 * @param string $ip IP address of the user, if the change was made anonymously
830
	 * @param int $deleted Indicates whether the change has been deleted
831
	 *
832
	 * @return RecentChange
833
	 */
834
	public static function newForCategorization(
835
		$timestamp,
836
		Title $categoryTitle,
837
		User $user = null,
838
		$comment,
839
		Title $pageTitle,
840
		$oldRevId,
841
		$newRevId,
842
		$lastTimestamp,
843
		$bot,
844
		$ip = '',
845
		$deleted = 0
846
	) {
847
		$rc = new RecentChange;
848
		$rc->mTitle = $categoryTitle;
849
		$rc->mPerformer = $user;
850
		$rc->mAttribs = [
851
			'rc_timestamp' => $timestamp,
852
			'rc_namespace' => $categoryTitle->getNamespace(),
853
			'rc_title' => $categoryTitle->getDBkey(),
854
			'rc_type' => RC_CATEGORIZE,
855
			'rc_source' => self::SRC_CATEGORIZE,
856
			'rc_minor' => 0,
857
			'rc_cur_id' => $pageTitle->getArticleID(),
858
			'rc_user' => $user ? $user->getId() : 0,
859
			'rc_user_text' => $user ? $user->getName() : '',
860
			'rc_comment' => $comment,
861
			'rc_this_oldid' => $newRevId,
862
			'rc_last_oldid' => $oldRevId,
863
			'rc_bot' => $bot ? 1 : 0,
864
			'rc_ip' => self::checkIPAddress( $ip ),
865
			'rc_patrolled' => 1, // Always patrolled, just like log entries
866
			'rc_new' => 0, # obsolete
867
			'rc_old_len' => null,
868
			'rc_new_len' => null,
869
			'rc_deleted' => $deleted,
870
			'rc_logid' => 0,
871
			'rc_log_type' => null,
872
			'rc_log_action' => '',
873
			'rc_params' =>  serialize( [
874
				'hidden-cat' => WikiCategoryPage::factory( $categoryTitle )->isHidden()
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class WikiPage as the method isHidden() does only exist in the following sub-classes of WikiPage: WikiCategoryPage. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
875
			] )
876
		];
877
878
		$rc->mExtra = [
879
			'prefixedDBkey' => $categoryTitle->getPrefixedDBkey(),
880
			'lastTimestamp' => $lastTimestamp,
881
			'oldSize' => 0,
882
			'newSize' => 0,
883
			'pageStatus' => 'changed'
884
		];
885
886
		return $rc;
887
	}
888
889
	/**
890
	 * Get a parameter value
891
	 *
892
	 * @since 1.27
893
	 *
894
	 * @param string $name parameter name
895
	 * @return mixed
896
	 */
897
	public function getParam( $name ) {
898
		$params = $this->parseParams();
899
		return isset( $params[$name] ) ? $params[$name] : null;
900
	}
901
902
	/**
903
	 * Initialises the members of this object from a mysql row object
904
	 *
905
	 * @param mixed $row
906
	 */
907
	public function loadFromRow( $row ) {
908
		$this->mAttribs = get_object_vars( $row );
909
		$this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
910
		$this->mAttribs['rc_deleted'] = $row->rc_deleted; // MUST be set
911
	}
912
913
	/**
914
	 * Get an attribute value
915
	 *
916
	 * @param string $name Attribute name
917
	 * @return mixed
918
	 */
919
	public function getAttribute( $name ) {
920
		return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : null;
921
	}
922
923
	/**
924
	 * @return array
925
	 */
926
	public function getAttributes() {
927
		return $this->mAttribs;
928
	}
929
930
	/**
931
	 * Gets the end part of the diff URL associated with this object
932
	 * Blank if no diff link should be displayed
933
	 * @param bool $forceCur
934
	 * @return string
935
	 */
936
	public function diffLinkTrail( $forceCur ) {
937
		if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
938
			$trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
939
				"&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
940
			if ( $forceCur ) {
941
				$trail .= '&diff=0';
942
			} else {
943
				$trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
944
			}
945
		} else {
946
			$trail = '';
947
		}
948
949
		return $trail;
950
	}
951
952
	/**
953
	 * Returns the change size (HTML).
954
	 * The lengths can be given optionally.
955
	 * @param int $old
956
	 * @param int $new
957
	 * @return string
958
	 */
959
	public function getCharacterDifference( $old = 0, $new = 0 ) {
960
		if ( $old === 0 ) {
961
			$old = $this->mAttribs['rc_old_len'];
962
		}
963
		if ( $new === 0 ) {
964
			$new = $this->mAttribs['rc_new_len'];
965
		}
966
		if ( $old === null || $new === null ) {
967
			return '';
968
		}
969
970
		return ChangesList::showCharacterDifference( $old, $new );
971
	}
972
973
	private static function checkIPAddress( $ip ) {
974
		global $wgRequest;
975
		if ( $ip ) {
976
			if ( !IP::isIPAddress( $ip ) ) {
977
				throw new MWException( "Attempt to write \"" . $ip .
978
					"\" as an IP address into recent changes" );
979
			}
980
		} else {
981
			$ip = $wgRequest->getIP();
982
			if ( !$ip ) {
983
				$ip = '';
984
			}
985
		}
986
987
		return $ip;
988
	}
989
990
	/**
991
	 * Check whether the given timestamp is new enough to have a RC row with a given tolerance
992
	 * as the recentchanges table might not be cleared out regularly (so older entries might exist)
993
	 * or rows which will be deleted soon shouldn't be included.
994
	 *
995
	 * @param mixed $timestamp MWTimestamp compatible timestamp
996
	 * @param int $tolerance Tolerance in seconds
997
	 * @return bool
998
	 */
999
	public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
1000
		global $wgRCMaxAge;
1001
1002
		return wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $wgRCMaxAge;
1003
	}
1004
1005
	/**
1006
	 * Parses and returns the rc_params attribute
1007
	 *
1008
	 * @since 1.26
1009
	 *
1010
	 * @return mixed|bool false on failed unserialization
1011
	 */
1012
	public function parseParams() {
1013
		$rcParams = $this->getAttribute( 'rc_params' );
1014
1015
		MediaWiki\suppressWarnings();
1016
		$unserializedParams = unserialize( $rcParams );
1017
		MediaWiki\restoreWarnings();
1018
1019
		return $unserializedParams;
1020
	}
1021
}
1022