Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/changes/RecentChange.php (4 issues)

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 );
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...
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 ) {
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...
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()
0 ignored issues
show
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...
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