Completed
Branch master (86dc85)
by
unknown
23:45
created

RecentChange::newForCategorization()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 54
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 54
rs 9.0306
cc 4
eloc 47
nc 8
nop 11

How to fix   Long Method    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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_SLAVE
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
		# If our database is strict about IP addresses, use NULL instead of an empty string
289
		if ( $dbw->strictIPs() && $this->mAttribs['rc_ip'] == '' ) {
290
			unset( $this->mAttribs['rc_ip'] );
291
		}
292
293
		# Trim spaces on user supplied text
294
		$this->mAttribs['rc_comment'] = trim( $this->mAttribs['rc_comment'] );
295
296
		# Make sure summary is truncated (whole multibyte characters)
297
		$this->mAttribs['rc_comment'] = $wgContLang->truncate( $this->mAttribs['rc_comment'], 255 );
298
299
		# Fixup database timestamps
300
		$this->mAttribs['rc_timestamp'] = $dbw->timestamp( $this->mAttribs['rc_timestamp'] );
301
		$this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'recentchanges_rc_id_seq' );
302
303
		# # If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
304
		if ( $dbw->cascadingDeletes() && $this->mAttribs['rc_cur_id'] == 0 ) {
305
			unset( $this->mAttribs['rc_cur_id'] );
306
		}
307
308
		# Insert new row
309
		$dbw->insert( 'recentchanges', $this->mAttribs, __METHOD__ );
310
311
		# Set the ID
312
		$this->mAttribs['rc_id'] = $dbw->insertId();
313
314
		# Notify extensions
315
		Hooks::run( 'RecentChange_save', [ &$this ] );
316
317
		# Notify external application via UDP
318
		if ( !$noudp ) {
319
			$this->notifyRCFeeds();
320
		}
321
322
		# E-mail notifications
323
		if ( $wgUseEnotif || $wgShowUpdatedMarker ) {
324
			$editor = $this->getPerformer();
325
			$title = $this->getTitle();
326
327
			// Never send an RC notification email about categorization changes
328
			if ( $this->mAttribs['rc_type'] != RC_CATEGORIZE ) {
329
				if ( Hooks::run( 'AbortEmailNotification', [ $editor, $title, $this ] ) ) {
330
					# @todo FIXME: This would be better as an extension hook
331
					$enotif = new EmailNotification();
332
					$enotif->notifyOnPageChange(
333
						$editor,
334
						$title,
335
						$this->mAttribs['rc_timestamp'],
336
						$this->mAttribs['rc_comment'],
337
						$this->mAttribs['rc_minor'],
338
						$this->mAttribs['rc_last_oldid'],
339
						$this->mExtra['pageStatus']
340
					);
341
				}
342
			}
343
		}
344
345
		// Update the cached list of active users
346
		if ( $this->mAttribs['rc_user'] > 0 ) {
347
			JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newCacheUpdateJob() );
348
		}
349
	}
350
351
	/**
352
	 * Notify all the feeds about the change.
353
	 * @param array $feeds Optional feeds to send to, defaults to $wgRCFeeds
354
	 */
355
	public function notifyRCFeeds( array $feeds = null ) {
356
		global $wgRCFeeds;
357
		if ( $feeds === null ) {
358
			$feeds = $wgRCFeeds;
359
		}
360
361
		$performer = $this->getPerformer();
362
363
		foreach ( $feeds as $feed ) {
364
			$feed += [
365
				'omit_bots' => false,
366
				'omit_anon' => false,
367
				'omit_user' => false,
368
				'omit_minor' => false,
369
				'omit_patrolled' => false,
370
			];
371
372
			if (
373
				( $feed['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
374
				( $feed['omit_anon'] && $performer->isAnon() ) ||
375
				( $feed['omit_user'] && !$performer->isAnon() ) ||
376
				( $feed['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
377
				( $feed['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
378
				$this->mAttribs['rc_type'] == RC_EXTERNAL
379
			) {
380
				continue;
381
			}
382
383
			$engine = self::getEngine( $feed['uri'] );
384
385
			if ( isset( $this->mExtra['actionCommentIRC'] ) ) {
386
				$actionComment = $this->mExtra['actionCommentIRC'];
387
			} else {
388
				$actionComment = null;
389
			}
390
391
			/** @var $formatter RCFeedFormatter */
392
			$formatter = is_object( $feed['formatter'] ) ? $feed['formatter'] : new $feed['formatter']();
393
			$line = $formatter->getLine( $feed, $this, $actionComment );
394
			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...
395
				// T109544
396
				// If a feed formatter returns null, this will otherwise cause an
397
				// error in at least RedisPubSubFeedEngine.
398
				// Not sure where/how this should best be handled.
399
				continue;
400
			}
401
402
			$engine->send( $feed, $line );
403
		}
404
	}
405
406
	/**
407
	 * Gets the stream engine object for a given URI from $wgRCEngines
408
	 *
409
	 * @param string $uri URI to get the engine object for
410
	 * @throws MWException
411
	 * @return RCFeedEngine The engine object
412
	 */
413
	public static function getEngine( $uri ) {
414
		global $wgRCEngines;
415
416
		$scheme = parse_url( $uri, PHP_URL_SCHEME );
417
		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...
418
			throw new MWException( __FUNCTION__ . ": Invalid stream logger URI: '$uri'" );
419
		}
420
421
		if ( !isset( $wgRCEngines[$scheme] ) ) {
422
			throw new MWException( __FUNCTION__ . ": Unknown stream logger URI scheme: $scheme" );
423
		}
424
425
		return new $wgRCEngines[$scheme];
426
	}
427
428
	/**
429
	 * Mark a given change as patrolled
430
	 *
431
	 * @param RecentChange|int $change RecentChange or corresponding rc_id
432
	 * @param bool $auto For automatic patrol
433
	 * @param string|string[] $tags Change tags to add to the patrol log entry
434
	 *   ($user should be able to add the specified tags before this is called)
435
	 * @return array See doMarkPatrolled(), or null if $change is not an existing rc_id
436
	 */
437
	public static function markPatrolled( $change, $auto = false, $tags = null ) {
438
		global $wgUser;
439
440
		$change = $change instanceof RecentChange
441
			? $change
442
			: RecentChange::newFromId( $change );
443
444
		if ( !$change instanceof RecentChange ) {
445
			return null;
446
		}
447
448
		return $change->doMarkPatrolled( $wgUser, $auto, $tags );
449
	}
450
451
	/**
452
	 * Mark this RecentChange as patrolled
453
	 *
454
	 * NOTE: Can also return 'rcpatroldisabled', 'hookaborted' and
455
	 * 'markedaspatrollederror-noautopatrol' as errors
456
	 * @param User $user User object doing the action
457
	 * @param bool $auto For automatic patrol
458
	 * @param string|string[] $tags Change tags to add to the patrol log entry
459
	 *   ($user should be able to add the specified tags before this is called)
460
	 * @return array Array of permissions errors, see Title::getUserPermissionsErrors()
461
	 */
462
	public function doMarkPatrolled( User $user, $auto = false, $tags = null ) {
463
		global $wgUseRCPatrol, $wgUseNPPatrol, $wgUseFilePatrol;
464
465
		$errors = [];
466
		// If recentchanges patrol is disabled, only new pages or new file versions
467
		// can be patrolled, provided the appropriate config variable is set
468
		if ( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) &&
469
			( !$wgUseFilePatrol || !( $this->getAttribute( 'rc_type' ) == RC_LOG &&
470
			$this->getAttribute( 'rc_log_type' ) == 'upload' ) ) ) {
471
			$errors[] = [ 'rcpatroldisabled' ];
472
		}
473
		// Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
474
		$right = $auto ? 'autopatrol' : 'patrol';
475
		$errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $user ) );
476
		if ( !Hooks::run( 'MarkPatrolled',
477
					[ $this->getAttribute( 'rc_id' ), &$user, false, $auto ] )
478
		) {
479
			$errors[] = [ 'hookaborted' ];
480
		}
481
		// Users without the 'autopatrol' right can't patrol their
482
		// own revisions
483
		if ( $user->getName() === $this->getAttribute( 'rc_user_text' )
484
			&& !$user->isAllowed( 'autopatrol' )
485
		) {
486
			$errors[] = [ 'markedaspatrollederror-noautopatrol' ];
487
		}
488
		if ( $errors ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $errors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
489
			return $errors;
490
		}
491
		// If the change was patrolled already, do nothing
492
		if ( $this->getAttribute( 'rc_patrolled' ) ) {
493
			return [];
494
		}
495
		// Actually set the 'patrolled' flag in RC
496
		$this->reallyMarkPatrolled();
497
		// Log this patrol event
498
		PatrolLog::record( $this, $auto, $user, $tags );
499
500
		Hooks::run(
501
			'MarkPatrolledComplete',
502
			[ $this->getAttribute( 'rc_id' ), &$user, false, $auto ]
503
		);
504
505
		return [];
506
	}
507
508
	/**
509
	 * Mark this RecentChange patrolled, without error checking
510
	 * @return int Number of affected rows
511
	 */
512
	public function reallyMarkPatrolled() {
513
		$dbw = wfGetDB( DB_MASTER );
514
		$dbw->update(
515
			'recentchanges',
516
			[
517
				'rc_patrolled' => 1
518
			],
519
			[
520
				'rc_id' => $this->getAttribute( 'rc_id' )
521
			],
522
			__METHOD__
523
		);
524
		// Invalidate the page cache after the page has been patrolled
525
		// to make sure that the Patrol link isn't visible any longer!
526
		$this->getTitle()->invalidateCache();
527
528
		return $dbw->affectedRows();
529
	}
530
531
	/**
532
	 * Makes an entry in the database corresponding to an edit
533
	 *
534
	 * @param string $timestamp
535
	 * @param Title $title
536
	 * @param bool $minor
537
	 * @param User $user
538
	 * @param string $comment
539
	 * @param int $oldId
540
	 * @param string $lastTimestamp
541
	 * @param bool $bot
542
	 * @param string $ip
543
	 * @param int $oldSize
544
	 * @param int $newSize
545
	 * @param int $newId
546
	 * @param int $patrol
547
	 * @param array $tags
548
	 * @return RecentChange
549
	 */
550
	public static function notifyEdit(
551
		$timestamp, &$title, $minor, &$user, $comment, $oldId, $lastTimestamp,
552
		$bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0,
553
		$tags = []
554
	) {
555
		$rc = new RecentChange;
556
		$rc->mTitle = $title;
557
		$rc->mPerformer = $user;
558
		$rc->mAttribs = [
559
			'rc_timestamp' => $timestamp,
560
			'rc_namespace' => $title->getNamespace(),
561
			'rc_title' => $title->getDBkey(),
562
			'rc_type' => RC_EDIT,
563
			'rc_source' => self::SRC_EDIT,
564
			'rc_minor' => $minor ? 1 : 0,
565
			'rc_cur_id' => $title->getArticleID(),
566
			'rc_user' => $user->getId(),
567
			'rc_user_text' => $user->getName(),
568
			'rc_comment' => $comment,
569
			'rc_this_oldid' => $newId,
570
			'rc_last_oldid' => $oldId,
571
			'rc_bot' => $bot ? 1 : 0,
572
			'rc_ip' => self::checkIPAddress( $ip ),
573
			'rc_patrolled' => intval( $patrol ),
574
			'rc_new' => 0, # obsolete
575
			'rc_old_len' => $oldSize,
576
			'rc_new_len' => $newSize,
577
			'rc_deleted' => 0,
578
			'rc_logid' => 0,
579
			'rc_log_type' => null,
580
			'rc_log_action' => '',
581
			'rc_params' => ''
582
		];
583
584
		$rc->mExtra = [
585
			'prefixedDBkey' => $title->getPrefixedDBkey(),
586
			'lastTimestamp' => $lastTimestamp,
587
			'oldSize' => $oldSize,
588
			'newSize' => $newSize,
589
			'pageStatus' => 'changed'
590
		];
591
592 View Code Duplication
		DeferredUpdates::addCallableUpdate( function() use ( $rc, $tags ) {
593
			$rc->save();
594
			if ( $rc->mAttribs['rc_patrolled'] ) {
595
				PatrolLog::record( $rc, true, $rc->getPerformer() );
596
			}
597
			if ( count( $tags ) ) {
598
				ChangeTags::addTags( $tags, $rc->mAttribs['rc_id'],
599
					$rc->mAttribs['rc_this_oldid'], null, null );
600
			}
601
		} );
602
603
		return $rc;
604
	}
605
606
	/**
607
	 * Makes an entry in the database corresponding to page creation
608
	 * Note: the title object must be loaded with the new id using resetArticleID()
609
	 *
610
	 * @param string $timestamp
611
	 * @param Title $title
612
	 * @param bool $minor
613
	 * @param User $user
614
	 * @param string $comment
615
	 * @param bool $bot
616
	 * @param string $ip
617
	 * @param int $size
618
	 * @param int $newId
619
	 * @param int $patrol
620
	 * @param array $tags
621
	 * @return RecentChange
622
	 */
623
	public static function notifyNew(
624
		$timestamp, &$title, $minor, &$user, $comment, $bot,
625
		$ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = []
626
	) {
627
		$rc = new RecentChange;
628
		$rc->mTitle = $title;
629
		$rc->mPerformer = $user;
630
		$rc->mAttribs = [
631
			'rc_timestamp' => $timestamp,
632
			'rc_namespace' => $title->getNamespace(),
633
			'rc_title' => $title->getDBkey(),
634
			'rc_type' => RC_NEW,
635
			'rc_source' => self::SRC_NEW,
636
			'rc_minor' => $minor ? 1 : 0,
637
			'rc_cur_id' => $title->getArticleID(),
638
			'rc_user' => $user->getId(),
639
			'rc_user_text' => $user->getName(),
640
			'rc_comment' => $comment,
641
			'rc_this_oldid' => $newId,
642
			'rc_last_oldid' => 0,
643
			'rc_bot' => $bot ? 1 : 0,
644
			'rc_ip' => self::checkIPAddress( $ip ),
645
			'rc_patrolled' => intval( $patrol ),
646
			'rc_new' => 1, # obsolete
647
			'rc_old_len' => 0,
648
			'rc_new_len' => $size,
649
			'rc_deleted' => 0,
650
			'rc_logid' => 0,
651
			'rc_log_type' => null,
652
			'rc_log_action' => '',
653
			'rc_params' => ''
654
		];
655
656
		$rc->mExtra = [
657
			'prefixedDBkey' => $title->getPrefixedDBkey(),
658
			'lastTimestamp' => 0,
659
			'oldSize' => 0,
660
			'newSize' => $size,
661
			'pageStatus' => 'created'
662
		];
663
664 View Code Duplication
		DeferredUpdates::addCallableUpdate( function() use ( $rc, $tags ) {
665
			$rc->save();
666
			if ( $rc->mAttribs['rc_patrolled'] ) {
667
				PatrolLog::record( $rc, true, $rc->getPerformer() );
668
			}
669
			if ( count( $tags ) ) {
670
				ChangeTags::addTags( $tags, $rc->mAttribs['rc_id'],
671
					$rc->mAttribs['rc_this_oldid'], null, null );
672
			}
673
		} );
674
675
		return $rc;
676
	}
677
678
	/**
679
	 * @param string $timestamp
680
	 * @param Title $title
681
	 * @param User $user
682
	 * @param string $actionComment
683
	 * @param string $ip
684
	 * @param string $type
685
	 * @param string $action
686
	 * @param Title $target
687
	 * @param string $logComment
688
	 * @param string $params
689
	 * @param int $newId
690
	 * @param string $actionCommentIRC
691
	 * @return bool
692
	 */
693
	public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip, $type,
694
		$action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
695
	) {
696
		global $wgLogRestrictions;
697
698
		# Don't add private logs to RC!
699
		if ( isset( $wgLogRestrictions[$type] ) && $wgLogRestrictions[$type] != '*' ) {
700
			return false;
701
		}
702
		$rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action,
703
			$target, $logComment, $params, $newId, $actionCommentIRC );
704
		$rc->save();
705
706
		return true;
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
	 * @param int $revId Id of associated revision, if any
723
	 * @param bool $isPatrollable Whether this log entry is patrollable
724
	 * @return RecentChange
725
	 */
726
	public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip,
727
		$type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '',
728
		$revId = 0, $isPatrollable = false ) {
729
		global $wgRequest;
730
731
		# # Get pageStatus for email notification
732
		switch ( $type . '-' . $action ) {
733
			case 'delete-delete':
734
				$pageStatus = 'deleted';
735
				break;
736
			case 'move-move':
737
			case 'move-move_redir':
738
				$pageStatus = 'moved';
739
				break;
740
			case 'delete-restore':
741
				$pageStatus = 'restored';
742
				break;
743
			case 'upload-upload':
744
				$pageStatus = 'created';
745
				break;
746
			case 'upload-overwrite':
747
			default:
748
				$pageStatus = 'changed';
749
				break;
750
		}
751
752
		// Allow unpatrolled status for patrollable log entries
753
		$markPatrolled = $isPatrollable ? $user->isAllowed( 'autopatrol' ) : true;
754
755
		$rc = new RecentChange;
756
		$rc->mTitle = $target;
757
		$rc->mPerformer = $user;
758
		$rc->mAttribs = [
759
			'rc_timestamp' => $timestamp,
760
			'rc_namespace' => $target->getNamespace(),
761
			'rc_title' => $target->getDBkey(),
762
			'rc_type' => RC_LOG,
763
			'rc_source' => self::SRC_LOG,
764
			'rc_minor' => 0,
765
			'rc_cur_id' => $target->getArticleID(),
766
			'rc_user' => $user->getId(),
767
			'rc_user_text' => $user->getName(),
768
			'rc_comment' => $logComment,
769
			'rc_this_oldid' => $revId,
770
			'rc_last_oldid' => 0,
771
			'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot', true ) : 0,
772
			'rc_ip' => self::checkIPAddress( $ip ),
773
			'rc_patrolled' => $markPatrolled ? 1 : 0,
774
			'rc_new' => 0, # obsolete
775
			'rc_old_len' => null,
776
			'rc_new_len' => null,
777
			'rc_deleted' => 0,
778
			'rc_logid' => $newId,
779
			'rc_log_type' => $type,
780
			'rc_log_action' => $action,
781
			'rc_params' => $params
782
		];
783
784
		$rc->mExtra = [
785
			'prefixedDBkey' => $title->getPrefixedDBkey(),
786
			'lastTimestamp' => 0,
787
			'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
788
			'pageStatus' => $pageStatus,
789
			'actionCommentIRC' => $actionCommentIRC
790
		];
791
792
		return $rc;
793
	}
794
795
	/**
796
	 * Constructs a RecentChange object for the given categorization
797
	 * This does not call save() on the object and thus does not write to the db
798
	 *
799
	 * @since 1.27
800
	 *
801
	 * @param string $timestamp Timestamp of the recent change to occur
802
	 * @param Title $categoryTitle Title of the category a page is being added to or removed from
803
	 * @param User $user User object of the user that made the change
804
	 * @param string $comment Change summary
805
	 * @param Title $pageTitle Title of the page that is being added or removed
806
	 * @param int $oldRevId Parent revision ID of this change
807
	 * @param int $newRevId Revision ID of this change
808
	 * @param string $lastTimestamp Parent revision timestamp of this change
809
	 * @param bool $bot true, if the change was made by a bot
810
	 * @param string $ip IP address of the user, if the change was made anonymously
811
	 * @param int $deleted Indicates whether the change has been deleted
812
	 *
813
	 * @return RecentChange
814
	 */
815
	public static function newForCategorization(
816
		$timestamp,
817
		Title $categoryTitle,
818
		User $user = null,
819
		$comment,
820
		Title $pageTitle,
821
		$oldRevId,
822
		$newRevId,
823
		$lastTimestamp,
824
		$bot,
825
		$ip = '',
826
		$deleted = 0
827
	) {
828
		$rc = new RecentChange;
829
		$rc->mTitle = $categoryTitle;
830
		$rc->mPerformer = $user;
831
		$rc->mAttribs = [
832
			'rc_timestamp' => $timestamp,
833
			'rc_namespace' => $categoryTitle->getNamespace(),
834
			'rc_title' => $categoryTitle->getDBkey(),
835
			'rc_type' => RC_CATEGORIZE,
836
			'rc_source' => self::SRC_CATEGORIZE,
837
			'rc_minor' => 0,
838
			'rc_cur_id' => $pageTitle->getArticleID(),
839
			'rc_user' => $user ? $user->getId() : 0,
840
			'rc_user_text' => $user ? $user->getName() : '',
841
			'rc_comment' => $comment,
842
			'rc_this_oldid' => $newRevId,
843
			'rc_last_oldid' => $oldRevId,
844
			'rc_bot' => $bot ? 1 : 0,
845
			'rc_ip' => self::checkIPAddress( $ip ),
846
			'rc_patrolled' => 1, // Always patrolled, just like log entries
847
			'rc_new' => 0, # obsolete
848
			'rc_old_len' => null,
849
			'rc_new_len' => null,
850
			'rc_deleted' => $deleted,
851
			'rc_logid' => 0,
852
			'rc_log_type' => null,
853
			'rc_log_action' => '',
854
			'rc_params' =>  serialize( [
855
				'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...
856
			] )
857
		];
858
859
		$rc->mExtra = [
860
			'prefixedDBkey' => $categoryTitle->getPrefixedDBkey(),
861
			'lastTimestamp' => $lastTimestamp,
862
			'oldSize' => 0,
863
			'newSize' => 0,
864
			'pageStatus' => 'changed'
865
		];
866
867
		return $rc;
868
	}
869
870
	/**
871
	 * Get a parameter value
872
	 *
873
	 * @since 1.27
874
	 *
875
	 * @param string $name parameter name
876
	 * @return mixed
877
	 */
878
	public function getParam( $name ) {
879
		$params = $this->parseParams();
880
		return isset( $params[$name] ) ? $params[$name] : null;
881
	}
882
883
	/**
884
	 * Initialises the members of this object from a mysql row object
885
	 *
886
	 * @param mixed $row
887
	 */
888
	public function loadFromRow( $row ) {
889
		$this->mAttribs = get_object_vars( $row );
890
		$this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
891
		$this->mAttribs['rc_deleted'] = $row->rc_deleted; // MUST be set
892
	}
893
894
	/**
895
	 * Get an attribute value
896
	 *
897
	 * @param string $name Attribute name
898
	 * @return mixed
899
	 */
900
	public function getAttribute( $name ) {
901
		return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : null;
902
	}
903
904
	/**
905
	 * @return array
906
	 */
907
	public function getAttributes() {
908
		return $this->mAttribs;
909
	}
910
911
	/**
912
	 * Gets the end part of the diff URL associated with this object
913
	 * Blank if no diff link should be displayed
914
	 * @param bool $forceCur
915
	 * @return string
916
	 */
917
	public function diffLinkTrail( $forceCur ) {
918
		if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
919
			$trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
920
				"&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
921
			if ( $forceCur ) {
922
				$trail .= '&diff=0';
923
			} else {
924
				$trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
925
			}
926
		} else {
927
			$trail = '';
928
		}
929
930
		return $trail;
931
	}
932
933
	/**
934
	 * Returns the change size (HTML).
935
	 * The lengths can be given optionally.
936
	 * @param int $old
937
	 * @param int $new
938
	 * @return string
939
	 */
940
	public function getCharacterDifference( $old = 0, $new = 0 ) {
941
		if ( $old === 0 ) {
942
			$old = $this->mAttribs['rc_old_len'];
943
		}
944
		if ( $new === 0 ) {
945
			$new = $this->mAttribs['rc_new_len'];
946
		}
947
		if ( $old === null || $new === null ) {
948
			return '';
949
		}
950
951
		return ChangesList::showCharacterDifference( $old, $new );
952
	}
953
954
	private static function checkIPAddress( $ip ) {
955
		global $wgRequest;
956
		if ( $ip ) {
957
			if ( !IP::isIPAddress( $ip ) ) {
958
				throw new MWException( "Attempt to write \"" . $ip .
959
					"\" as an IP address into recent changes" );
960
			}
961
		} else {
962
			$ip = $wgRequest->getIP();
963
			if ( !$ip ) {
964
				$ip = '';
965
			}
966
		}
967
968
		return $ip;
969
	}
970
971
	/**
972
	 * Check whether the given timestamp is new enough to have a RC row with a given tolerance
973
	 * as the recentchanges table might not be cleared out regularly (so older entries might exist)
974
	 * or rows which will be deleted soon shouldn't be included.
975
	 *
976
	 * @param mixed $timestamp MWTimestamp compatible timestamp
977
	 * @param int $tolerance Tolerance in seconds
978
	 * @return bool
979
	 */
980
	public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
981
		global $wgRCMaxAge;
982
983
		return wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $wgRCMaxAge;
984
	}
985
986
	/**
987
	 * Parses and returns the rc_params attribute
988
	 *
989
	 * @since 1.26
990
	 *
991
	 * @return mixed|bool false on failed unserialization
992
	 */
993
	public function parseParams() {
994
		$rcParams = $this->getAttribute( 'rc_params' );
995
996
		MediaWiki\suppressWarnings();
997
		$unserializedParams = unserialize( $rcParams );
998
		MediaWiki\restoreWarnings();
999
1000
		return $unserializedParams;
1001
	}
1002
}
1003