Completed
Branch master (13ece3)
by
unknown
22:07
created

Title   F

Complexity

Total Complexity 703

Size/Duplication

Total Lines 4749
Duplicated Lines 3.12 %

Coupling/Cohesion

Components 1
Dependencies 40

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 148
loc 4749
rs 0.5217
wmc 703
lcom 1
cbo 40

189 Methods

Rating   Name   Duplication   Size   Complexity  
A newFromLinkTarget() 0 12 2
B newFromText() 0 15 6
B newFromTextThrow() 0 31 5
A newFromURL() 0 19 3
A getTitleCache() 0 6 2
A newFromID() 0 15 3
A newFromIDs() 0 19 3
A newFromRow() 0 5 1
D loadFromRow() 0 33 9
A makeTitle() 0 12 2
A makeTitleSafe() 0 15 3
A newMainPage() 0 8 2
A nameOf() 0 16 2
A legalChars() 0 4 1
A getTitleInvalidRegex() 0 4 1
F convertByteClassToUnicodeClass() 0 92 20
B makeName() 0 19 5
A escapeFragmentForURL() 0 7 1
A compare() 0 7 2
A isLocal() 0 9 3
A isExternal() 0 3 1
A getInterwiki() 0 3 1
A wasLocalInterwiki() 0 3 1
A isTrans() 0 7 2
A getTransWikiID() 0 7 2
A getTitleValue() 0 17 3
A getText() 0 3 1
A getPartialURL() 0 3 1
A getDBkey() 0 3 1
A getUserCaseDBKey() 0 8 2
A getNamespace() 0 3 1
A getSelectFields() 18 18 3
B getContentModel() 7 15 5
A hasContentModel() 0 3 1
A getNsText() 0 19 4
A getSubjectNsText() 0 4 1
A getTalkNsText() 0 4 1
A canTalk() 0 3 1
A canExist() 0 3 1
A isWatchable() 0 3 2
A isSpecialPage() 0 3 1
A isSpecial() 0 9 3
A fixSpecialName() 0 12 4
A inNamespace() 0 3 1
B inNamespaces() 0 14 5
A hasSubjectNamespace() 0 3 1
A isContentPage() 0 3 1
A isMovable() 0 10 3
A isMainPage() 0 3 1
A isSubpage() 0 5 2
A isConversionTable() 0 6 2
A isWikitextPage() 0 3 1
A isCssOrJsPage() 0 13 3
A isCssJsSubpage() 0 5 4
A getSkinFromCssJsSubpage() 0 9 2
A isCssSubpage() 0 4 3
A isJsSubpage() 0 4 3
A isTalkPage() 0 3 1
A getTalkPage() 0 3 1
A getSubjectPage() 0 8 2
A getOtherPage() 0 10 3
A getDefaultNamespace() 0 3 1
A getFragment() 0 3 1
A hasFragment() 0 3 1
A getFragmentForURL() 0 7 2
A setFragment() 0 3 1
A createFragmentTarget() 0 9 1
A prefix() 0 11 3
A getPrefixedDBkey() 0 5 1
A getPrefixedText() 0 8 2
A __toString() 0 3 1
A getFullText() 0 7 2
A getRootText() 0 7 2
A getRootTitle() 0 3 1
A getBaseText() 0 12 3
A getBaseTitle() 0 3 1
A getSubpageText() 0 7 2
A getSubpage() 0 3 1
A getSubpageUrlForm() 0 5 1
A getPrefixedURL() 0 5 1
B fixUrlQueryArgs() 0 26 6
A getFullURL() 0 17 1
C getLocalURL() 0 75 18
B getLinkURL() 0 10 5
A getInternalURL() 0 8 2
A getCanonicalURL() 0 6 1
A getEditURL() 0 8 2
A quickUserCan() 0 3 1
A userCan() 0 8 2
B getUserPermissionsErrors() 0 19 6
C checkQuickPermissions() 10 61 28
B resultToError() 0 19 10
B checkPermissionHooks() 0 21 8
B checkSpecialsAndNSPermissions() 0 17 6
C checkCSSandJSPermissions() 12 22 12
C checkPageRestrictions() 0 22 8
D checkCascadingSourcesRestrictions() 0 35 10
C checkActionPermissions() 0 54 20
D checkUserBlock() 0 26 10
C checkReadPermissions() 0 66 22
A missingPermissionError() 0 20 3
C getUserPermissionsErrorsInternal() 0 52 9
A getFilteredRestrictionTypes() 0 12 2
A getRestrictionTypes() 0 19 3
C getTitleProtection() 0 40 7
A deleteTitleProtection() 0 10 1
B isSemiProtected() 0 20 5
C isProtected() 0 24 8
B isNamespaceProtected() 0 12 5
A isCascadeProtected() 0 4 1
A areCascadeProtectionSourcesLoaded() 0 3 2
D getCascadeProtectionSources() 0 78 15
A areRestrictionsLoaded() 0 3 1
A getRestrictions() 0 8 3
A getAllRestrictions() 0 6 2
A getRestrictionExpiry() 0 6 3
A areRestrictionsCascading() 0 7 2
A loadRestrictionsFromResultWrapper() 0 9 2
C loadRestrictionsFromRows() 0 66 13
B loadRestrictions() 0 33 6
A flushRestrictions() 0 4 1
A getTitleFormatter() 0 3 1
A __construct() 0 2 1
A newFromDBkey() 0 11 2
A newFromTitleValue() 0 3 1
B purgeExpiredRestrictions() 0 35 3
A hasSubpages() 0 20 4
A getSubpages() 0 22 3
A isDeleted() 6 19 3
A isDeletedQuick() 6 17 4
A getArticleID() 0 18 4
B isRedirect() 0 27 4
A getLength() 0 21 4
B getLatestRevID() 0 21 5
A resetArticleID() 0 21 2
A clearCaches() 0 7 1
A capitalize() 0 9 2
B secureAndSplit() 0 35 3
B getLinksTo() 7 31 5
A getTemplateLinksTo() 0 3 1
B getLinksFrom() 0 42 4
A getTemplateLinksFrom() 0 3 1
B getBrokenLinksFrom() 0 29 3
B getCdnUrls() 0 24 5
A getSquidURLs() 0 3 1
A purgeSquid() 0 6 1
A moveNoAuth() 0 4 1
A isValidMoveOperation() 0 20 4
A validateFileMoveOperation() 0 15 4
B moveTo() 0 21 5
C moveSubpages() 0 60 10
D isSingleRevRedirect() 0 42 8
D isValidMoveTarget() 0 40 9
B getParentCategories() 0 28 4
B getParentCategoryTree() 0 20 5
A pageCond() 0 8 2
A getPreviousRevisionID() 17 17 3
A getNextRevisionID() 17 17 3
A getFirstRevision() 0 15 4
A getEarliestRevTime() 0 4 2
A isNewPage() 0 4 1
A isBigDeletion() 0 23 3
A estimateRevisionCount() 0 13 3
B countRevisionsBetween() 0 26 6
D getAuthorsBetween() 0 59 18
A countAuthorsBetween() 0 4 2
A equals() 0 6 3
A isSubpageOf() 0 5 3
A exists() 0 5 1
C isAlwaysKnown() 0 41 8
A isKnown() 0 3 2
A hasSourceText() 0 20 3
A getDefaultMessageText() 0 18 3
B invalidateCache() 0 25 4
A touchLinks() 0 6 2
A getTouched() 0 7 2
B getNotificationTimestamp() 0 31 6
B getNamespaceKey() 0 24 4
B getRedirectsHere() 0 30 4
B isValidRedirectTarget() 0 18 5
A getBacklinkCache() 0 3 1
A canUseNoindex() 0 10 2
A getCategorySortkey() 0 18 2
A getDbPageLanguageCode() 5 13 3
B getPageLanguage() 0 29 5
B getPageViewLanguage() 0 33 5
C getEditNotices() 43 67 9
A __sleep() 0 11 1
A __wakeup() 0 5 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Title often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Title, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Representation of a title within %MediaWiki.
4
 *
5
 * See title.txt
6
 *
7
 * This program is free software; you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation; either version 2 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License along
18
 * with this program; if not, write to the Free Software Foundation, Inc.,
19
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
 * http://www.gnu.org/copyleft/gpl.html
21
 *
22
 * @file
23
 */
24
use MediaWiki\Linker\LinkTarget;
25
use MediaWiki\MediaWikiServices;
26
27
/**
28
 * Represents a title within MediaWiki.
29
 * Optionally may contain an interwiki designation or namespace.
30
 * @note This class can fetch various kinds of data from the database;
31
 *       however, it does so inefficiently.
32
 * @note Consider using a TitleValue object instead. TitleValue is more lightweight
33
 *       and does not rely on global state or the database.
34
 */
35
class Title implements LinkTarget {
36
	/** @var HashBagOStuff */
37
	static private $titleCache = null;
38
39
	/**
40
	 * Title::newFromText maintains a cache to avoid expensive re-normalization of
41
	 * commonly used titles. On a batch operation this can become a memory leak
42
	 * if not bounded. After hitting this many titles reset the cache.
43
	 */
44
	const CACHE_MAX = 1000;
45
46
	/**
47
	 * Used to be GAID_FOR_UPDATE define. Used with getArticleID() and friends
48
	 * to use the master DB
49
	 */
50
	const GAID_FOR_UPDATE = 1;
51
52
	/**
53
	 * @name Private member variables
54
	 * Please use the accessor functions instead.
55
	 * @private
56
	 */
57
	// @{
58
59
	/** @var string Text form (spaces not underscores) of the main part */
60
	public $mTextform = '';
61
62
	/** @var string URL-encoded form of the main part */
63
	public $mUrlform = '';
64
65
	/** @var string Main part with underscores */
66
	public $mDbkeyform = '';
67
68
	/** @var string Database key with the initial letter in the case specified by the user */
69
	protected $mUserCaseDBKey;
70
71
	/** @var int Namespace index, i.e. one of the NS_xxxx constants */
72
	public $mNamespace = NS_MAIN;
73
74
	/** @var string Interwiki prefix */
75
	public $mInterwiki = '';
76
77
	/** @var bool Was this Title created from a string with a local interwiki prefix? */
78
	private $mLocalInterwiki = false;
79
80
	/** @var string Title fragment (i.e. the bit after the #) */
81
	public $mFragment = '';
82
83
	/** @var int Article ID, fetched from the link cache on demand */
84
	public $mArticleID = -1;
85
86
	/** @var bool|int ID of most recent revision */
87
	protected $mLatestID = false;
88
89
	/**
90
	 * @var bool|string ID of the page's content model, i.e. one of the
91
	 *   CONTENT_MODEL_XXX constants
92
	 */
93
	public $mContentModel = false;
94
95
	/** @var int Estimated number of revisions; null of not loaded */
96
	private $mEstimateRevisions;
97
98
	/** @var array Array of groups allowed to edit this article */
99
	public $mRestrictions = [];
100
101
	/** @var string|bool */
102
	protected $mOldRestrictions = false;
103
104
	/** @var bool Cascade restrictions on this page to included templates and images? */
105
	public $mCascadeRestriction;
106
107
	/** Caching the results of getCascadeProtectionSources */
108
	public $mCascadingRestrictions;
109
110
	/** @var array When do the restrictions on this page expire? */
111
	protected $mRestrictionsExpiry = [];
112
113
	/** @var bool Are cascading restrictions in effect on this page? */
114
	protected $mHasCascadingRestrictions;
115
116
	/** @var array Where are the cascading restrictions coming from on this page? */
117
	public $mCascadeSources;
118
119
	/** @var bool Boolean for initialisation on demand */
120
	public $mRestrictionsLoaded = false;
121
122
	/** @var string Text form including namespace/interwiki, initialised on demand */
123
	protected $mPrefixedText = null;
124
125
	/** @var mixed Cached value for getTitleProtection (create protection) */
126
	public $mTitleProtection;
127
128
	/**
129
	 * @var int Namespace index when there is no namespace. Don't change the
130
	 *   following default, NS_MAIN is hardcoded in several places. See bug 696.
131
	 *   Zero except in {{transclusion}} tags.
132
	 */
133
	public $mDefaultNamespace = NS_MAIN;
134
135
	/** @var int The page length, 0 for special pages */
136
	protected $mLength = -1;
137
138
	/** @var null Is the article at this title a redirect? */
139
	public $mRedirect = null;
140
141
	/** @var array Associative array of user ID -> timestamp/false */
142
	private $mNotificationTimestamp = [];
143
144
	/** @var bool Whether a page has any subpages */
145
	private $mHasSubpages;
146
147
	/** @var bool The (string) language code of the page's language and content code. */
148
	private $mPageLanguage = false;
149
150
	/** @var string|bool|null The page language code from the database, null if not saved in
151
	 * the database or false if not loaded, yet. */
152
	private $mDbPageLanguage = false;
153
154
	/** @var TitleValue A corresponding TitleValue object */
155
	private $mTitleValue = null;
156
157
	/** @var bool Would deleting this page be a big deletion? */
158
	private $mIsBigDeletion = null;
159
	// @}
160
161
	/**
162
	 * B/C kludge: provide a TitleParser for use by Title.
163
	 * Ideally, Title would have no methods that need this.
164
	 * Avoid usage of this singleton by using TitleValue
165
	 * and the associated services when possible.
166
	 *
167
	 * @return TitleFormatter
168
	 */
169
	private static function getTitleFormatter() {
170
		return MediaWikiServices::getInstance()->getTitleFormatter();
171
	}
172
173
	/**
174
	 * @access protected
175
	 */
176
	function __construct() {
177
	}
178
179
	/**
180
	 * Create a new Title from a prefixed DB key
181
	 *
182
	 * @param string $key The database key, which has underscores
183
	 *	instead of spaces, possibly including namespace and
184
	 *	interwiki prefixes
185
	 * @return Title|null Title, or null on an error
186
	 */
187
	public static function newFromDBkey( $key ) {
188
		$t = new Title();
189
		$t->mDbkeyform = $key;
190
191
		try {
192
			$t->secureAndSplit();
193
			return $t;
194
		} catch ( MalformedTitleException $ex ) {
195
			return null;
196
		}
197
	}
198
199
	/**
200
	 * Create a new Title from a TitleValue
201
	 *
202
	 * @param TitleValue $titleValue Assumed to be safe.
203
	 *
204
	 * @return Title
205
	 */
206
	public static function newFromTitleValue( TitleValue $titleValue ) {
207
		return self::newFromLinkTarget( $titleValue );
208
	}
209
210
	/**
211
	 * Create a new Title from a LinkTarget
212
	 *
213
	 * @param LinkTarget $linkTarget Assumed to be safe.
214
	 *
215
	 * @return Title
216
	 */
217
	public static function newFromLinkTarget( LinkTarget $linkTarget ) {
218
		if ( $linkTarget instanceof Title ) {
219
			// Special case if it's already a Title object
220
			return $linkTarget;
221
		}
222
		return self::makeTitle(
223
			$linkTarget->getNamespace(),
224
			$linkTarget->getText(),
225
			$linkTarget->getFragment(),
226
			$linkTarget->getInterwiki()
227
		);
228
	}
229
230
	/**
231
	 * Create a new Title from text, such as what one would find in a link. De-
232
	 * codes any HTML entities in the text.
233
	 *
234
	 * @param string|int|null $text The link text; spaces, prefixes, and an
235
	 *   initial ':' indicating the main namespace are accepted.
236
	 * @param int $defaultNamespace The namespace to use if none is specified
237
	 *   by a prefix.  If you want to force a specific namespace even if
238
	 *   $text might begin with a namespace prefix, use makeTitle() or
239
	 *   makeTitleSafe().
240
	 * @throws InvalidArgumentException
241
	 * @return Title|null Title or null on an error.
242
	 */
243
	public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
244
		// DWIM: Integers can be passed in here when page titles are used as array keys.
245
		if ( $text !== null && !is_string( $text ) && !is_int( $text ) ) {
246
			throw new InvalidArgumentException( '$text must be a string.' );
247
		}
248
		if ( $text === null ) {
249
			return null;
250
		}
251
252
		try {
253
			return Title::newFromTextThrow( strval( $text ), $defaultNamespace );
254
		} catch ( MalformedTitleException $ex ) {
255
			return null;
256
		}
257
	}
258
259
	/**
260
	 * Like Title::newFromText(), but throws MalformedTitleException when the title is invalid,
261
	 * rather than returning null.
262
	 *
263
	 * The exception subclasses encode detailed information about why the title is invalid.
264
	 *
265
	 * @see Title::newFromText
266
	 *
267
	 * @since 1.25
268
	 * @param string $text Title text to check
269
	 * @param int $defaultNamespace
270
	 * @throws MalformedTitleException If the title is invalid
271
	 * @return Title
272
	 */
273
	public static function newFromTextThrow( $text, $defaultNamespace = NS_MAIN ) {
274
		if ( is_object( $text ) ) {
275
			throw new MWException( '$text must be a string, given an object' );
276
		}
277
278
		$titleCache = self::getTitleCache();
279
280
		// Wiki pages often contain multiple links to the same page.
281
		// Title normalization and parsing can become expensive on pages with many
282
		// links, so we can save a little time by caching them.
283
		// In theory these are value objects and won't get changed...
284
		if ( $defaultNamespace == NS_MAIN ) {
285
			$t = $titleCache->get( $text );
286
			if ( $t ) {
287
				return $t;
288
			}
289
		}
290
291
		// Convert things like &eacute; &#257; or &#x3017; into normalized (bug 14952) text
292
		$filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
293
294
		$t = new Title();
295
		$t->mDbkeyform = strtr( $filteredText, ' ', '_' );
296
		$t->mDefaultNamespace = intval( $defaultNamespace );
297
298
		$t->secureAndSplit();
299
		if ( $defaultNamespace == NS_MAIN ) {
300
			$titleCache->set( $text, $t );
301
		}
302
		return $t;
303
	}
304
305
	/**
306
	 * THIS IS NOT THE FUNCTION YOU WANT. Use Title::newFromText().
307
	 *
308
	 * Example of wrong and broken code:
309
	 * $title = Title::newFromURL( $wgRequest->getVal( 'title' ) );
310
	 *
311
	 * Example of right code:
312
	 * $title = Title::newFromText( $wgRequest->getVal( 'title' ) );
313
	 *
314
	 * Create a new Title from URL-encoded text. Ensures that
315
	 * the given title's length does not exceed the maximum.
316
	 *
317
	 * @param string $url The title, as might be taken from a URL
318
	 * @return Title|null The new object, or null on an error
319
	 */
320
	public static function newFromURL( $url ) {
321
		$t = new Title();
322
323
		# For compatibility with old buggy URLs. "+" is usually not valid in titles,
324
		# but some URLs used it as a space replacement and they still come
325
		# from some external search tools.
326
		if ( strpos( self::legalChars(), '+' ) === false ) {
327
			$url = strtr( $url, '+', ' ' );
328
		}
329
330
		$t->mDbkeyform = strtr( $url, ' ', '_' );
331
332
		try {
333
			$t->secureAndSplit();
334
			return $t;
335
		} catch ( MalformedTitleException $ex ) {
336
			return null;
337
		}
338
	}
339
340
	/**
341
	 * @return HashBagOStuff
342
	 */
343
	private static function getTitleCache() {
344
		if ( self::$titleCache == null ) {
345
			self::$titleCache = new HashBagOStuff( [ 'maxKeys' => self::CACHE_MAX ] );
346
		}
347
		return self::$titleCache;
348
	}
349
350
	/**
351
	 * Returns a list of fields that are to be selected for initializing Title
352
	 * objects or LinkCache entries. Uses $wgContentHandlerUseDB to determine
353
	 * whether to include page_content_model.
354
	 *
355
	 * @return array
356
	 */
357 View Code Duplication
	protected static function getSelectFields() {
358
		global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
359
360
		$fields = [
361
			'page_namespace', 'page_title', 'page_id',
362
			'page_len', 'page_is_redirect', 'page_latest',
363
		];
364
365
		if ( $wgContentHandlerUseDB ) {
366
			$fields[] = 'page_content_model';
367
		}
368
369
		if ( $wgPageLanguageUseDB ) {
370
			$fields[] = 'page_lang';
371
		}
372
373
		return $fields;
374
	}
375
376
	/**
377
	 * Create a new Title from an article ID
378
	 *
379
	 * @param int $id The page_id corresponding to the Title to create
380
	 * @param int $flags Use Title::GAID_FOR_UPDATE to use master
381
	 * @return Title|null The new object, or null on an error
382
	 */
383
	public static function newFromID( $id, $flags = 0 ) {
384
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
385
		$row = $db->selectRow(
386
			'page',
387
			self::getSelectFields(),
388
			[ 'page_id' => $id ],
389
			__METHOD__
390
		);
391
		if ( $row !== false ) {
392
			$title = Title::newFromRow( $row );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $db->selectRow('page', s...d' => $id), __METHOD__) on line 385 can also be of type boolean; however, Title::newFromRow() does only seem to accept object<stdClass>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
393
		} else {
394
			$title = null;
395
		}
396
		return $title;
397
	}
398
399
	/**
400
	 * Make an array of titles from an array of IDs
401
	 *
402
	 * @param int[] $ids Array of IDs
403
	 * @return Title[] Array of Titles
404
	 */
405
	public static function newFromIDs( $ids ) {
406
		if ( !count( $ids ) ) {
407
			return [];
408
		}
409
		$dbr = wfGetDB( DB_SLAVE );
410
411
		$res = $dbr->select(
412
			'page',
413
			self::getSelectFields(),
414
			[ 'page_id' => $ids ],
415
			__METHOD__
416
		);
417
418
		$titles = [];
419
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
420
			$titles[] = Title::newFromRow( $row );
421
		}
422
		return $titles;
423
	}
424
425
	/**
426
	 * Make a Title object from a DB row
427
	 *
428
	 * @param stdClass $row Object database row (needs at least page_title,page_namespace)
429
	 * @return Title Corresponding Title
430
	 */
431
	public static function newFromRow( $row ) {
432
		$t = self::makeTitle( $row->page_namespace, $row->page_title );
433
		$t->loadFromRow( $row );
434
		return $t;
435
	}
436
437
	/**
438
	 * Load Title object fields from a DB row.
439
	 * If false is given, the title will be treated as non-existing.
440
	 *
441
	 * @param stdClass|bool $row Database row
442
	 */
443
	public function loadFromRow( $row ) {
444
		if ( $row ) { // page found
445
			if ( isset( $row->page_id ) ) {
446
				$this->mArticleID = (int)$row->page_id;
447
			}
448
			if ( isset( $row->page_len ) ) {
449
				$this->mLength = (int)$row->page_len;
450
			}
451
			if ( isset( $row->page_is_redirect ) ) {
452
				$this->mRedirect = (bool)$row->page_is_redirect;
0 ignored issues
show
Documentation Bug introduced by
It seems like (bool) $row->page_is_redirect of type boolean is incompatible with the declared type null of property $mRedirect.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
453
			}
454
			if ( isset( $row->page_latest ) ) {
455
				$this->mLatestID = (int)$row->page_latest;
456
			}
457
			if ( isset( $row->page_content_model ) ) {
458
				$this->mContentModel = strval( $row->page_content_model );
459
			} else {
460
				$this->mContentModel = false; # initialized lazily in getContentModel()
461
			}
462
			if ( isset( $row->page_lang ) ) {
463
				$this->mDbPageLanguage = (string)$row->page_lang;
464
			}
465
			if ( isset( $row->page_restrictions ) ) {
466
				$this->mOldRestrictions = $row->page_restrictions;
467
			}
468
		} else { // page not found
469
			$this->mArticleID = 0;
470
			$this->mLength = 0;
471
			$this->mRedirect = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type null of property $mRedirect.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
472
			$this->mLatestID = 0;
473
			$this->mContentModel = false; # initialized lazily in getContentModel()
474
		}
475
	}
476
477
	/**
478
	 * Create a new Title from a namespace index and a DB key.
479
	 * It's assumed that $ns and $title are *valid*, for instance when
480
	 * they came directly from the database or a special page name.
481
	 * For convenience, spaces are converted to underscores so that
482
	 * eg user_text fields can be used directly.
483
	 *
484
	 * @param int $ns The namespace of the article
485
	 * @param string $title The unprefixed database key form
486
	 * @param string $fragment The link fragment (after the "#")
487
	 * @param string $interwiki The interwiki prefix
488
	 * @return Title The new object
489
	 */
490
	public static function &makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) {
491
		$t = new Title();
492
		$t->mInterwiki = $interwiki;
493
		$t->mFragment = $fragment;
494
		$t->mNamespace = $ns = intval( $ns );
495
		$t->mDbkeyform = strtr( $title, ' ', '_' );
496
		$t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
497
		$t->mUrlform = wfUrlencode( $t->mDbkeyform );
498
		$t->mTextform = strtr( $title, '_', ' ' );
499
		$t->mContentModel = false; # initialized lazily in getContentModel()
500
		return $t;
501
	}
502
503
	/**
504
	 * Create a new Title from a namespace index and a DB key.
505
	 * The parameters will be checked for validity, which is a bit slower
506
	 * than makeTitle() but safer for user-provided data.
507
	 *
508
	 * @param int $ns The namespace of the article
509
	 * @param string $title Database key form
510
	 * @param string $fragment The link fragment (after the "#")
511
	 * @param string $interwiki Interwiki prefix
512
	 * @return Title|null The new object, or null on an error
513
	 */
514
	public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
515
		if ( !MWNamespace::exists( $ns ) ) {
516
			return null;
517
		}
518
519
		$t = new Title();
520
		$t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki, true );
521
522
		try {
523
			$t->secureAndSplit();
524
			return $t;
525
		} catch ( MalformedTitleException $ex ) {
526
			return null;
527
		}
528
	}
529
530
	/**
531
	 * Create a new Title for the Main Page
532
	 *
533
	 * @return Title The new object
534
	 */
535
	public static function newMainPage() {
536
		$title = Title::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() );
537
		// Don't give fatal errors if the message is broken
538
		if ( !$title ) {
539
			$title = Title::newFromText( 'Main Page' );
540
		}
541
		return $title;
542
	}
543
544
	/**
545
	 * Get the prefixed DB key associated with an ID
546
	 *
547
	 * @param int $id The page_id of the article
548
	 * @return Title|null An object representing the article, or null if no such article was found
549
	 */
550
	public static function nameOf( $id ) {
551
		$dbr = wfGetDB( DB_SLAVE );
552
553
		$s = $dbr->selectRow(
554
			'page',
555
			[ 'page_namespace', 'page_title' ],
556
			[ 'page_id' => $id ],
557
			__METHOD__
558
		);
559
		if ( $s === false ) {
560
			return null;
561
		}
562
563
		$n = self::makeName( $s->page_namespace, $s->page_title );
564
		return $n;
565
	}
566
567
	/**
568
	 * Get a regex character class describing the legal characters in a link
569
	 *
570
	 * @return string The list of characters, not delimited
571
	 */
572
	public static function legalChars() {
573
		global $wgLegalTitleChars;
574
		return $wgLegalTitleChars;
575
	}
576
577
	/**
578
	 * Returns a simple regex that will match on characters and sequences invalid in titles.
579
	 * Note that this doesn't pick up many things that could be wrong with titles, but that
580
	 * replacing this regex with something valid will make many titles valid.
581
	 *
582
	 * @deprecated since 1.25, use MediaWikiTitleCodec::getTitleInvalidRegex() instead
583
	 *
584
	 * @return string Regex string
585
	 */
586
	static function getTitleInvalidRegex() {
587
		wfDeprecated( __METHOD__, '1.25' );
588
		return MediaWikiTitleCodec::getTitleInvalidRegex();
589
	}
590
591
	/**
592
	 * Utility method for converting a character sequence from bytes to Unicode.
593
	 *
594
	 * Primary usecase being converting $wgLegalTitleChars to a sequence usable in
595
	 * javascript, as PHP uses UTF-8 bytes where javascript uses Unicode code units.
596
	 *
597
	 * @param string $byteClass
598
	 * @return string
599
	 */
600
	public static function convertByteClassToUnicodeClass( $byteClass ) {
601
		$length = strlen( $byteClass );
602
		// Input token queue
603
		$x0 = $x1 = $x2 = '';
0 ignored issues
show
Unused Code introduced by
$x2 is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
604
		// Decoded queue
605
		$d0 = $d1 = $d2 = '';
0 ignored issues
show
Unused Code introduced by
$d2 is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
606
		// Decoded integer codepoints
607
		$ord0 = $ord1 = $ord2 = 0;
0 ignored issues
show
Unused Code introduced by
$ord2 is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
608
		// Re-encoded queue
609
		$r0 = $r1 = $r2 = '';
0 ignored issues
show
Unused Code introduced by
$r2 is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
610
		// Output
611
		$out = '';
612
		// Flags
613
		$allowUnicode = false;
614
		for ( $pos = 0; $pos < $length; $pos++ ) {
615
			// Shift the queues down
616
			$x2 = $x1;
617
			$x1 = $x0;
618
			$d2 = $d1;
0 ignored issues
show
Unused Code introduced by
$d2 is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
619
			$d1 = $d0;
620
			$ord2 = $ord1;
621
			$ord1 = $ord0;
622
			$r2 = $r1;
623
			$r1 = $r0;
624
			// Load the current input token and decoded values
625
			$inChar = $byteClass[$pos];
626
			if ( $inChar == '\\' ) {
627
				if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) {
628
					$x0 = $inChar . $m[0];
629
					$d0 = chr( hexdec( $m[1] ) );
630
					$pos += strlen( $m[0] );
631
				} elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) {
632
					$x0 = $inChar . $m[0];
633
					$d0 = chr( octdec( $m[0] ) );
634
					$pos += strlen( $m[0] );
635
				} elseif ( $pos + 1 >= $length ) {
636
					$x0 = $d0 = '\\';
637
				} else {
638
					$d0 = $byteClass[$pos + 1];
639
					$x0 = $inChar . $d0;
640
					$pos += 1;
641
				}
642
			} else {
643
				$x0 = $d0 = $inChar;
644
			}
645
			$ord0 = ord( $d0 );
646
			// Load the current re-encoded value
647
			if ( $ord0 < 32 || $ord0 == 0x7f ) {
648
				$r0 = sprintf( '\x%02x', $ord0 );
649
			} elseif ( $ord0 >= 0x80 ) {
650
				// Allow unicode if a single high-bit character appears
651
				$r0 = sprintf( '\x%02x', $ord0 );
652
				$allowUnicode = true;
653
			} elseif ( strpos( '-\\[]^', $d0 ) !== false ) {
654
				$r0 = '\\' . $d0;
655
			} else {
656
				$r0 = $d0;
657
			}
658
			// Do the output
659
			if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) {
660
				// Range
661
				if ( $ord2 > $ord0 ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
662
					// Empty range
663
				} elseif ( $ord0 >= 0x80 ) {
664
					// Unicode range
665
					$allowUnicode = true;
666
					if ( $ord2 < 0x80 ) {
667
						// Keep the non-unicode section of the range
668
						$out .= "$r2-\\x7F";
669
					}
670
				} else {
671
					// Normal range
672
					$out .= "$r2-$r0";
673
				}
674
				// Reset state to the initial value
675
				$x0 = $x1 = $d0 = $d1 = $r0 = $r1 = '';
676
			} elseif ( $ord2 < 0x80 ) {
677
				// ASCII character
678
				$out .= $r2;
679
			}
680
		}
681
		if ( $ord1 < 0x80 ) {
682
			$out .= $r1;
683
		}
684
		if ( $ord0 < 0x80 ) {
685
			$out .= $r0;
686
		}
687
		if ( $allowUnicode ) {
688
			$out .= '\u0080-\uFFFF';
689
		}
690
		return $out;
691
	}
692
693
	/**
694
	 * Make a prefixed DB key from a DB key and a namespace index
695
	 *
696
	 * @param int $ns Numerical representation of the namespace
697
	 * @param string $title The DB key form the title
698
	 * @param string $fragment The link fragment (after the "#")
699
	 * @param string $interwiki The interwiki prefix
700
	 * @param bool $canonicalNamespace If true, use the canonical name for
701
	 *   $ns instead of the localized version.
702
	 * @return string The prefixed form of the title
703
	 */
704
	public static function makeName( $ns, $title, $fragment = '', $interwiki = '',
705
		$canonicalNamespace = false
706
	) {
707
		global $wgContLang;
708
709
		if ( $canonicalNamespace ) {
710
			$namespace = MWNamespace::getCanonicalName( $ns );
711
		} else {
712
			$namespace = $wgContLang->getNsText( $ns );
713
		}
714
		$name = $namespace == '' ? $title : "$namespace:$title";
715
		if ( strval( $interwiki ) != '' ) {
716
			$name = "$interwiki:$name";
717
		}
718
		if ( strval( $fragment ) != '' ) {
719
			$name .= '#' . $fragment;
720
		}
721
		return $name;
722
	}
723
724
	/**
725
	 * Escape a text fragment, say from a link, for a URL
726
	 *
727
	 * @param string $fragment Containing a URL or link fragment (after the "#")
728
	 * @return string Escaped string
729
	 */
730
	static function escapeFragmentForURL( $fragment ) {
731
		# Note that we don't urlencode the fragment.  urlencoded Unicode
732
		# fragments appear not to work in IE (at least up to 7) or in at least
733
		# one version of Opera 9.x.  The W3C validator, for one, doesn't seem
734
		# to care if they aren't encoded.
735
		return Sanitizer::escapeId( $fragment, 'noninitial' );
736
	}
737
738
	/**
739
	 * Callback for usort() to do title sorts by (namespace, title)
740
	 *
741
	 * @param Title $a
742
	 * @param Title $b
743
	 *
744
	 * @return int Result of string comparison, or namespace comparison
745
	 */
746
	public static function compare( $a, $b ) {
747
		if ( $a->getNamespace() == $b->getNamespace() ) {
748
			return strcmp( $a->getText(), $b->getText() );
749
		} else {
750
			return $a->getNamespace() - $b->getNamespace();
751
		}
752
	}
753
754
	/**
755
	 * Determine whether the object refers to a page within
756
	 * this project (either this wiki or a wiki with a local
757
	 * interwiki, see https://www.mediawiki.org/wiki/Manual:Interwiki_table#iw_local )
758
	 *
759
	 * @return bool True if this is an in-project interwiki link or a wikilink, false otherwise
760
	 */
761
	public function isLocal() {
762
		if ( $this->isExternal() ) {
763
			$iw = Interwiki::fetch( $this->mInterwiki );
0 ignored issues
show
Deprecated Code introduced by
The method Interwiki::fetch() has been deprecated with message: since 1.28, use InterwikiLookup instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
764
			if ( $iw ) {
765
				return $iw->isLocal();
766
			}
767
		}
768
		return true;
769
	}
770
771
	/**
772
	 * Is this Title interwiki?
773
	 *
774
	 * @return bool
775
	 */
776
	public function isExternal() {
777
		return $this->mInterwiki !== '';
778
	}
779
780
	/**
781
	 * Get the interwiki prefix
782
	 *
783
	 * Use Title::isExternal to check if a interwiki is set
784
	 *
785
	 * @return string Interwiki prefix
786
	 */
787
	public function getInterwiki() {
788
		return $this->mInterwiki;
789
	}
790
791
	/**
792
	 * Was this a local interwiki link?
793
	 *
794
	 * @return bool
795
	 */
796
	public function wasLocalInterwiki() {
797
		return $this->mLocalInterwiki;
798
	}
799
800
	/**
801
	 * Determine whether the object refers to a page within
802
	 * this project and is transcludable.
803
	 *
804
	 * @return bool True if this is transcludable
805
	 */
806
	public function isTrans() {
807
		if ( !$this->isExternal() ) {
808
			return false;
809
		}
810
811
		return Interwiki::fetch( $this->mInterwiki )->isTranscludable();
0 ignored issues
show
Deprecated Code introduced by
The method Interwiki::fetch() has been deprecated with message: since 1.28, use InterwikiLookup instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
812
	}
813
814
	/**
815
	 * Returns the DB name of the distant wiki which owns the object.
816
	 *
817
	 * @return string The DB name
818
	 */
819
	public function getTransWikiID() {
820
		if ( !$this->isExternal() ) {
821
			return false;
822
		}
823
824
		return Interwiki::fetch( $this->mInterwiki )->getWikiID();
0 ignored issues
show
Deprecated Code introduced by
The method Interwiki::fetch() has been deprecated with message: since 1.28, use InterwikiLookup instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
825
	}
826
827
	/**
828
	 * Get a TitleValue object representing this Title.
829
	 *
830
	 * @note Not all valid Titles have a corresponding valid TitleValue
831
	 * (e.g. TitleValues cannot represent page-local links that have a
832
	 * fragment but no title text).
833
	 *
834
	 * @return TitleValue|null
835
	 */
836
	public function getTitleValue() {
837
		if ( $this->mTitleValue === null ) {
838
			try {
839
				$this->mTitleValue = new TitleValue(
840
					$this->getNamespace(),
841
					$this->getDBkey(),
842
					$this->getFragment(),
843
					$this->getInterwiki()
844
				);
845
			} catch ( InvalidArgumentException $ex ) {
846
				wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' .
847
					$this->getPrefixedText() . ']]: ' . $ex->getMessage() . "\n" );
848
			}
849
		}
850
851
		return $this->mTitleValue;
852
	}
853
854
	/**
855
	 * Get the text form (spaces not underscores) of the main part
856
	 *
857
	 * @return string Main part of the title
858
	 */
859
	public function getText() {
860
		return $this->mTextform;
861
	}
862
863
	/**
864
	 * Get the URL-encoded form of the main part
865
	 *
866
	 * @return string Main part of the title, URL-encoded
867
	 */
868
	public function getPartialURL() {
869
		return $this->mUrlform;
870
	}
871
872
	/**
873
	 * Get the main part with underscores
874
	 *
875
	 * @return string Main part of the title, with underscores
876
	 */
877
	public function getDBkey() {
878
		return $this->mDbkeyform;
879
	}
880
881
	/**
882
	 * Get the DB key with the initial letter case as specified by the user
883
	 *
884
	 * @return string DB key
885
	 */
886
	function getUserCaseDBKey() {
887
		if ( !is_null( $this->mUserCaseDBKey ) ) {
888
			return $this->mUserCaseDBKey;
889
		} else {
890
			// If created via makeTitle(), $this->mUserCaseDBKey is not set.
891
			return $this->mDbkeyform;
892
		}
893
	}
894
895
	/**
896
	 * Get the namespace index, i.e. one of the NS_xxxx constants.
897
	 *
898
	 * @return int Namespace index
899
	 */
900
	public function getNamespace() {
901
		return $this->mNamespace;
902
	}
903
904
	/**
905
	 * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
906
	 *
907
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
908
	 * @return string Content model id
909
	 */
910
	public function getContentModel( $flags = 0 ) {
911 View Code Duplication
		if ( ( !$this->mContentModel || $flags === Title::GAID_FOR_UPDATE ) &&
912
			$this->getArticleID( $flags )
913
		) {
914
			$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
915
			$linkCache->addLinkObj( $this ); # in case we already had an article ID
916
			$this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
0 ignored issues
show
Documentation Bug introduced by
It seems like $linkCache->getGoodLinkFieldObj($this, 'model') can also be of type integer. However, the property $mContentModel is declared as type boolean|string. 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...
917
		}
918
919
		if ( !$this->mContentModel ) {
920
			$this->mContentModel = ContentHandler::getDefaultModelFor( $this );
921
		}
922
923
		return $this->mContentModel;
924
	}
925
926
	/**
927
	 * Convenience method for checking a title's content model name
928
	 *
929
	 * @param string $id The content model ID (use the CONTENT_MODEL_XXX constants).
930
	 * @return bool True if $this->getContentModel() == $id
931
	 */
932
	public function hasContentModel( $id ) {
933
		return $this->getContentModel() == $id;
934
	}
935
936
	/**
937
	 * Get the namespace text
938
	 *
939
	 * @return string Namespace text
940
	 */
941
	public function getNsText() {
942
		if ( $this->isExternal() ) {
943
			// This probably shouldn't even happen,
944
			// but for interwiki transclusion it sometimes does.
945
			// Use the canonical namespaces if possible to try to
946
			// resolve a foreign namespace.
947
			if ( MWNamespace::exists( $this->mNamespace ) ) {
948
				return MWNamespace::getCanonicalName( $this->mNamespace );
949
			}
950
		}
951
952
		try {
953
			$formatter = self::getTitleFormatter();
954
			return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform );
955
		} catch ( InvalidArgumentException $ex ) {
956
			wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" );
957
			return false;
958
		}
959
	}
960
961
	/**
962
	 * Get the namespace text of the subject (rather than talk) page
963
	 *
964
	 * @return string Namespace text
965
	 */
966
	public function getSubjectNsText() {
967
		global $wgContLang;
968
		return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) );
969
	}
970
971
	/**
972
	 * Get the namespace text of the talk page
973
	 *
974
	 * @return string Namespace text
975
	 */
976
	public function getTalkNsText() {
977
		global $wgContLang;
978
		return $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) );
979
	}
980
981
	/**
982
	 * Could this title have a corresponding talk page?
983
	 *
984
	 * @return bool
985
	 */
986
	public function canTalk() {
987
		return MWNamespace::canTalk( $this->mNamespace );
988
	}
989
990
	/**
991
	 * Is this in a namespace that allows actual pages?
992
	 *
993
	 * @return bool
994
	 */
995
	public function canExist() {
996
		return $this->mNamespace >= NS_MAIN;
997
	}
998
999
	/**
1000
	 * Can this title be added to a user's watchlist?
1001
	 *
1002
	 * @return bool
1003
	 */
1004
	public function isWatchable() {
1005
		return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
1006
	}
1007
1008
	/**
1009
	 * Returns true if this is a special page.
1010
	 *
1011
	 * @return bool
1012
	 */
1013
	public function isSpecialPage() {
1014
		return $this->getNamespace() == NS_SPECIAL;
1015
	}
1016
1017
	/**
1018
	 * Returns true if this title resolves to the named special page
1019
	 *
1020
	 * @param string $name The special page name
1021
	 * @return bool
1022
	 */
1023
	public function isSpecial( $name ) {
1024
		if ( $this->isSpecialPage() ) {
1025
			list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() );
1026
			if ( $name == $thisName ) {
1027
				return true;
1028
			}
1029
		}
1030
		return false;
1031
	}
1032
1033
	/**
1034
	 * If the Title refers to a special page alias which is not the local default, resolve
1035
	 * the alias, and localise the name as necessary.  Otherwise, return $this
1036
	 *
1037
	 * @return Title
1038
	 */
1039
	public function fixSpecialName() {
1040
		if ( $this->isSpecialPage() ) {
1041
			list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform );
1042
			if ( $canonicalName ) {
1043
				$localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par );
1044
				if ( $localName != $this->mDbkeyform ) {
1045
					return Title::makeTitle( NS_SPECIAL, $localName );
1046
				}
1047
			}
1048
		}
1049
		return $this;
1050
	}
1051
1052
	/**
1053
	 * Returns true if the title is inside the specified namespace.
1054
	 *
1055
	 * Please make use of this instead of comparing to getNamespace()
1056
	 * This function is much more resistant to changes we may make
1057
	 * to namespaces than code that makes direct comparisons.
1058
	 * @param int $ns The namespace
1059
	 * @return bool
1060
	 * @since 1.19
1061
	 */
1062
	public function inNamespace( $ns ) {
1063
		return MWNamespace::equals( $this->getNamespace(), $ns );
1064
	}
1065
1066
	/**
1067
	 * Returns true if the title is inside one of the specified namespaces.
1068
	 *
1069
	 * @param int $namespaces,... The namespaces to check for
0 ignored issues
show
Bug introduced by
There is no parameter named $namespaces,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1070
	 * @return bool
1071
	 * @since 1.19
1072
	 */
1073
	public function inNamespaces( /* ... */ ) {
1074
		$namespaces = func_get_args();
1075
		if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
1076
			$namespaces = $namespaces[0];
1077
		}
1078
1079
		foreach ( $namespaces as $ns ) {
1080
			if ( $this->inNamespace( $ns ) ) {
1081
				return true;
1082
			}
1083
		}
1084
1085
		return false;
1086
	}
1087
1088
	/**
1089
	 * Returns true if the title has the same subject namespace as the
1090
	 * namespace specified.
1091
	 * For example this method will take NS_USER and return true if namespace
1092
	 * is either NS_USER or NS_USER_TALK since both of them have NS_USER
1093
	 * as their subject namespace.
1094
	 *
1095
	 * This is MUCH simpler than individually testing for equivalence
1096
	 * against both NS_USER and NS_USER_TALK, and is also forward compatible.
1097
	 * @since 1.19
1098
	 * @param int $ns
1099
	 * @return bool
1100
	 */
1101
	public function hasSubjectNamespace( $ns ) {
1102
		return MWNamespace::subjectEquals( $this->getNamespace(), $ns );
1103
	}
1104
1105
	/**
1106
	 * Is this Title in a namespace which contains content?
1107
	 * In other words, is this a content page, for the purposes of calculating
1108
	 * statistics, etc?
1109
	 *
1110
	 * @return bool
1111
	 */
1112
	public function isContentPage() {
1113
		return MWNamespace::isContent( $this->getNamespace() );
1114
	}
1115
1116
	/**
1117
	 * Would anybody with sufficient privileges be able to move this page?
1118
	 * Some pages just aren't movable.
1119
	 *
1120
	 * @return bool
1121
	 */
1122
	public function isMovable() {
1123
		if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->isExternal() ) {
1124
			// Interwiki title or immovable namespace. Hooks don't get to override here
1125
			return false;
1126
		}
1127
1128
		$result = true;
1129
		Hooks::run( 'TitleIsMovable', [ $this, &$result ] );
1130
		return $result;
1131
	}
1132
1133
	/**
1134
	 * Is this the mainpage?
1135
	 * @note Title::newFromText seems to be sufficiently optimized by the title
1136
	 * cache that we don't need to over-optimize by doing direct comparisons and
1137
	 * accidentally creating new bugs where $title->equals( Title::newFromText() )
1138
	 * ends up reporting something differently than $title->isMainPage();
1139
	 *
1140
	 * @since 1.18
1141
	 * @return bool
1142
	 */
1143
	public function isMainPage() {
1144
		return $this->equals( Title::newMainPage() );
0 ignored issues
show
Bug introduced by
It seems like \Title::newMainPage() can be null; however, equals() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1145
	}
1146
1147
	/**
1148
	 * Is this a subpage?
1149
	 *
1150
	 * @return bool
1151
	 */
1152
	public function isSubpage() {
1153
		return MWNamespace::hasSubpages( $this->mNamespace )
1154
			? strpos( $this->getText(), '/' ) !== false
1155
			: false;
1156
	}
1157
1158
	/**
1159
	 * Is this a conversion table for the LanguageConverter?
1160
	 *
1161
	 * @return bool
1162
	 */
1163
	public function isConversionTable() {
1164
		// @todo ConversionTable should become a separate content model.
1165
1166
		return $this->getNamespace() == NS_MEDIAWIKI &&
1167
			strpos( $this->getText(), 'Conversiontable/' ) === 0;
1168
	}
1169
1170
	/**
1171
	 * Does that page contain wikitext, or it is JS, CSS or whatever?
1172
	 *
1173
	 * @return bool
1174
	 */
1175
	public function isWikitextPage() {
1176
		return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
1177
	}
1178
1179
	/**
1180
	 * Could this page contain custom CSS or JavaScript for the global UI.
1181
	 * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
1182
	 * or CONTENT_MODEL_JAVASCRIPT.
1183
	 *
1184
	 * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage()
1185
	 * for that!
1186
	 *
1187
	 * Note that this method should not return true for pages that contain and
1188
	 * show "inactive" CSS or JS.
1189
	 *
1190
	 * @return bool
1191
	 * @todo FIXME: Rename to isSiteConfigPage() and remove deprecated hook
1192
	 */
1193
	public function isCssOrJsPage() {
1194
		$isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
1195
			&& ( $this->hasContentModel( CONTENT_MODEL_CSS )
1196
				|| $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
1197
1198
		# @note This hook is also called in ContentHandler::getDefaultModel.
1199
		#   It's called here again to make sure hook functions can force this
1200
		#   method to return true even outside the MediaWiki namespace.
1201
1202
		Hooks::run( 'TitleIsCssOrJsPage', [ $this, &$isCssOrJsPage ], '1.25' );
1203
1204
		return $isCssOrJsPage;
1205
	}
1206
1207
	/**
1208
	 * Is this a .css or .js subpage of a user page?
1209
	 * @return bool
1210
	 * @todo FIXME: Rename to isUserConfigPage()
1211
	 */
1212
	public function isCssJsSubpage() {
1213
		return ( NS_USER == $this->mNamespace && $this->isSubpage()
1214
				&& ( $this->hasContentModel( CONTENT_MODEL_CSS )
1215
					|| $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
1216
	}
1217
1218
	/**
1219
	 * Trim down a .css or .js subpage title to get the corresponding skin name
1220
	 *
1221
	 * @return string Containing skin name from .css or .js subpage title
1222
	 */
1223
	public function getSkinFromCssJsSubpage() {
1224
		$subpage = explode( '/', $this->mTextform );
1225
		$subpage = $subpage[count( $subpage ) - 1];
1226
		$lastdot = strrpos( $subpage, '.' );
1227
		if ( $lastdot === false ) {
1228
			return $subpage; # Never happens: only called for names ending in '.css' or '.js'
1229
		}
1230
		return substr( $subpage, 0, $lastdot );
1231
	}
1232
1233
	/**
1234
	 * Is this a .css subpage of a user page?
1235
	 *
1236
	 * @return bool
1237
	 */
1238
	public function isCssSubpage() {
1239
		return ( NS_USER == $this->mNamespace && $this->isSubpage()
1240
			&& $this->hasContentModel( CONTENT_MODEL_CSS ) );
1241
	}
1242
1243
	/**
1244
	 * Is this a .js subpage of a user page?
1245
	 *
1246
	 * @return bool
1247
	 */
1248
	public function isJsSubpage() {
1249
		return ( NS_USER == $this->mNamespace && $this->isSubpage()
1250
			&& $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
1251
	}
1252
1253
	/**
1254
	 * Is this a talk page of some sort?
1255
	 *
1256
	 * @return bool
1257
	 */
1258
	public function isTalkPage() {
1259
		return MWNamespace::isTalk( $this->getNamespace() );
1260
	}
1261
1262
	/**
1263
	 * Get a Title object associated with the talk page of this article
1264
	 *
1265
	 * @return Title The object for the talk page
1266
	 */
1267
	public function getTalkPage() {
1268
		return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
1269
	}
1270
1271
	/**
1272
	 * Get a title object associated with the subject page of this
1273
	 * talk page
1274
	 *
1275
	 * @return Title The object for the subject page
1276
	 */
1277
	public function getSubjectPage() {
1278
		// Is this the same title?
1279
		$subjectNS = MWNamespace::getSubject( $this->getNamespace() );
1280
		if ( $this->getNamespace() == $subjectNS ) {
1281
			return $this;
1282
		}
1283
		return Title::makeTitle( $subjectNS, $this->getDBkey() );
1284
	}
1285
1286
	/**
1287
	 * Get the other title for this page, if this is a subject page
1288
	 * get the talk page, if it is a subject page get the talk page
1289
	 *
1290
	 * @since 1.25
1291
	 * @throws MWException
1292
	 * @return Title
1293
	 */
1294
	public function getOtherPage() {
1295
		if ( $this->isSpecialPage() ) {
1296
			throw new MWException( 'Special pages cannot have other pages' );
1297
		}
1298
		if ( $this->isTalkPage() ) {
1299
			return $this->getSubjectPage();
1300
		} else {
1301
			return $this->getTalkPage();
1302
		}
1303
	}
1304
1305
	/**
1306
	 * Get the default namespace index, for when there is no namespace
1307
	 *
1308
	 * @return int Default namespace index
1309
	 */
1310
	public function getDefaultNamespace() {
1311
		return $this->mDefaultNamespace;
1312
	}
1313
1314
	/**
1315
	 * Get the Title fragment (i.e.\ the bit after the #) in text form
1316
	 *
1317
	 * Use Title::hasFragment to check for a fragment
1318
	 *
1319
	 * @return string Title fragment
1320
	 */
1321
	public function getFragment() {
1322
		return $this->mFragment;
1323
	}
1324
1325
	/**
1326
	 * Check if a Title fragment is set
1327
	 *
1328
	 * @return bool
1329
	 * @since 1.23
1330
	 */
1331
	public function hasFragment() {
1332
		return $this->mFragment !== '';
1333
	}
1334
1335
	/**
1336
	 * Get the fragment in URL form, including the "#" character if there is one
1337
	 * @return string Fragment in URL form
1338
	 */
1339
	public function getFragmentForURL() {
1340
		if ( !$this->hasFragment() ) {
1341
			return '';
1342
		} else {
1343
			return '#' . Title::escapeFragmentForURL( $this->getFragment() );
1344
		}
1345
	}
1346
1347
	/**
1348
	 * Set the fragment for this title. Removes the first character from the
1349
	 * specified fragment before setting, so it assumes you're passing it with
1350
	 * an initial "#".
1351
	 *
1352
	 * Deprecated for public use, use Title::makeTitle() with fragment parameter,
1353
	 * or Title::createFragmentTarget().
1354
	 * Still in active use privately.
1355
	 *
1356
	 * @private
1357
	 * @param string $fragment Text
1358
	 */
1359
	public function setFragment( $fragment ) {
1360
		$this->mFragment = strtr( substr( $fragment, 1 ), '_', ' ' );
1361
	}
1362
1363
	/**
1364
	 * Creates a new Title for a different fragment of the same page.
1365
	 *
1366
	 * @since 1.27
1367
	 * @param string $fragment
1368
	 * @return Title
1369
	 */
1370
	public function createFragmentTarget( $fragment ) {
1371
		return self::makeTitle(
1372
			$this->getNamespace(),
1373
			$this->getText(),
1374
			$fragment,
1375
			$this->getInterwiki()
1376
		);
1377
1378
	}
1379
1380
	/**
1381
	 * Prefix some arbitrary text with the namespace or interwiki prefix
1382
	 * of this object
1383
	 *
1384
	 * @param string $name The text
1385
	 * @return string The prefixed text
1386
	 */
1387
	private function prefix( $name ) {
1388
		$p = '';
1389
		if ( $this->isExternal() ) {
1390
			$p = $this->mInterwiki . ':';
1391
		}
1392
1393
		if ( 0 != $this->mNamespace ) {
1394
			$p .= $this->getNsText() . ':';
1395
		}
1396
		return $p . $name;
1397
	}
1398
1399
	/**
1400
	 * Get the prefixed database key form
1401
	 *
1402
	 * @return string The prefixed title, with underscores and
1403
	 *  any interwiki and namespace prefixes
1404
	 */
1405
	public function getPrefixedDBkey() {
1406
		$s = $this->prefix( $this->mDbkeyform );
1407
		$s = strtr( $s, ' ', '_' );
1408
		return $s;
1409
	}
1410
1411
	/**
1412
	 * Get the prefixed title with spaces.
1413
	 * This is the form usually used for display
1414
	 *
1415
	 * @return string The prefixed title, with spaces
1416
	 */
1417
	public function getPrefixedText() {
1418
		if ( $this->mPrefixedText === null ) {
1419
			$s = $this->prefix( $this->mTextform );
1420
			$s = strtr( $s, '_', ' ' );
1421
			$this->mPrefixedText = $s;
1422
		}
1423
		return $this->mPrefixedText;
1424
	}
1425
1426
	/**
1427
	 * Return a string representation of this title
1428
	 *
1429
	 * @return string Representation of this title
1430
	 */
1431
	public function __toString() {
1432
		return $this->getPrefixedText();
1433
	}
1434
1435
	/**
1436
	 * Get the prefixed title with spaces, plus any fragment
1437
	 * (part beginning with '#')
1438
	 *
1439
	 * @return string The prefixed title, with spaces and the fragment, including '#'
1440
	 */
1441
	public function getFullText() {
1442
		$text = $this->getPrefixedText();
1443
		if ( $this->hasFragment() ) {
1444
			$text .= '#' . $this->getFragment();
1445
		}
1446
		return $text;
1447
	}
1448
1449
	/**
1450
	 * Get the root page name text without a namespace, i.e. the leftmost part before any slashes
1451
	 *
1452
	 * @par Example:
1453
	 * @code
1454
	 * Title::newFromText('User:Foo/Bar/Baz')->getRootText();
1455
	 * # returns: 'Foo'
1456
	 * @endcode
1457
	 *
1458
	 * @return string Root name
1459
	 * @since 1.20
1460
	 */
1461
	public function getRootText() {
1462
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1463
			return $this->getText();
1464
		}
1465
1466
		return strtok( $this->getText(), '/' );
1467
	}
1468
1469
	/**
1470
	 * Get the root page name title, i.e. the leftmost part before any slashes
1471
	 *
1472
	 * @par Example:
1473
	 * @code
1474
	 * Title::newFromText('User:Foo/Bar/Baz')->getRootTitle();
1475
	 * # returns: Title{User:Foo}
1476
	 * @endcode
1477
	 *
1478
	 * @return Title Root title
1479
	 * @since 1.20
1480
	 */
1481
	public function getRootTitle() {
1482
		return Title::makeTitle( $this->getNamespace(), $this->getRootText() );
1483
	}
1484
1485
	/**
1486
	 * Get the base page name without a namespace, i.e. the part before the subpage name
1487
	 *
1488
	 * @par Example:
1489
	 * @code
1490
	 * Title::newFromText('User:Foo/Bar/Baz')->getBaseText();
1491
	 * # returns: 'Foo/Bar'
1492
	 * @endcode
1493
	 *
1494
	 * @return string Base name
1495
	 */
1496
	public function getBaseText() {
1497
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1498
			return $this->getText();
1499
		}
1500
1501
		$parts = explode( '/', $this->getText() );
1502
		# Don't discard the real title if there's no subpage involved
1503
		if ( count( $parts ) > 1 ) {
1504
			unset( $parts[count( $parts ) - 1] );
1505
		}
1506
		return implode( '/', $parts );
1507
	}
1508
1509
	/**
1510
	 * Get the base page name title, i.e. the part before the subpage name
1511
	 *
1512
	 * @par Example:
1513
	 * @code
1514
	 * Title::newFromText('User:Foo/Bar/Baz')->getBaseTitle();
1515
	 * # returns: Title{User:Foo/Bar}
1516
	 * @endcode
1517
	 *
1518
	 * @return Title Base title
1519
	 * @since 1.20
1520
	 */
1521
	public function getBaseTitle() {
1522
		return Title::makeTitle( $this->getNamespace(), $this->getBaseText() );
1523
	}
1524
1525
	/**
1526
	 * Get the lowest-level subpage name, i.e. the rightmost part after any slashes
1527
	 *
1528
	 * @par Example:
1529
	 * @code
1530
	 * Title::newFromText('User:Foo/Bar/Baz')->getSubpageText();
1531
	 * # returns: "Baz"
1532
	 * @endcode
1533
	 *
1534
	 * @return string Subpage name
1535
	 */
1536
	public function getSubpageText() {
1537
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1538
			return $this->mTextform;
1539
		}
1540
		$parts = explode( '/', $this->mTextform );
1541
		return $parts[count( $parts ) - 1];
1542
	}
1543
1544
	/**
1545
	 * Get the title for a subpage of the current page
1546
	 *
1547
	 * @par Example:
1548
	 * @code
1549
	 * Title::newFromText('User:Foo/Bar/Baz')->getSubpage("Asdf");
1550
	 * # returns: Title{User:Foo/Bar/Baz/Asdf}
1551
	 * @endcode
1552
	 *
1553
	 * @param string $text The subpage name to add to the title
1554
	 * @return Title Subpage title
1555
	 * @since 1.20
1556
	 */
1557
	public function getSubpage( $text ) {
1558
		return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text );
1559
	}
1560
1561
	/**
1562
	 * Get a URL-encoded form of the subpage text
1563
	 *
1564
	 * @return string URL-encoded subpage name
1565
	 */
1566
	public function getSubpageUrlForm() {
1567
		$text = $this->getSubpageText();
1568
		$text = wfUrlencode( strtr( $text, ' ', '_' ) );
1569
		return $text;
1570
	}
1571
1572
	/**
1573
	 * Get a URL-encoded title (not an actual URL) including interwiki
1574
	 *
1575
	 * @return string The URL-encoded form
1576
	 */
1577
	public function getPrefixedURL() {
1578
		$s = $this->prefix( $this->mDbkeyform );
1579
		$s = wfUrlencode( strtr( $s, ' ', '_' ) );
1580
		return $s;
1581
	}
1582
1583
	/**
1584
	 * Helper to fix up the get{Canonical,Full,Link,Local,Internal}URL args
1585
	 * get{Canonical,Full,Link,Local,Internal}URL methods accepted an optional
1586
	 * second argument named variant. This was deprecated in favor
1587
	 * of passing an array of option with a "variant" key
1588
	 * Once $query2 is removed for good, this helper can be dropped
1589
	 * and the wfArrayToCgi moved to getLocalURL();
1590
	 *
1591
	 * @since 1.19 (r105919)
1592
	 * @param array|string $query
1593
	 * @param bool $query2
1594
	 * @return string
1595
	 */
1596
	private static function fixUrlQueryArgs( $query, $query2 = false ) {
1597
		if ( $query2 !== false ) {
1598
			wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " .
1599
				"method called with a second parameter is deprecated. Add your " .
1600
				"parameter to an array passed as the first parameter.", "1.19" );
1601
		}
1602
		if ( is_array( $query ) ) {
1603
			$query = wfArrayToCgi( $query );
1604
		}
1605
		if ( $query2 ) {
1606
			if ( is_string( $query2 ) ) {
1607
				// $query2 is a string, we will consider this to be
1608
				// a deprecated $variant argument and add it to the query
1609
				$query2 = wfArrayToCgi( [ 'variant' => $query2 ] );
1610
			} else {
1611
				$query2 = wfArrayToCgi( $query2 );
1612
			}
1613
			// If we have $query content add a & to it first
1614
			if ( $query ) {
1615
				$query .= '&';
1616
			}
1617
			// Now append the queries together
1618
			$query .= $query2;
1619
		}
1620
		return $query;
1621
	}
1622
1623
	/**
1624
	 * Get a real URL referring to this title, with interwiki link and
1625
	 * fragment
1626
	 *
1627
	 * @see self::getLocalURL for the arguments.
1628
	 * @see wfExpandUrl
1629
	 * @param array|string $query
1630
	 * @param bool $query2
1631
	 * @param string $proto Protocol type to use in URL
1632
	 * @return string The URL
1633
	 */
1634
	public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
1635
		$query = self::fixUrlQueryArgs( $query, $query2 );
1636
1637
		# Hand off all the decisions on urls to getLocalURL
1638
		$url = $this->getLocalURL( $query );
1639
1640
		# Expand the url to make it a full url. Note that getLocalURL has the
1641
		# potential to output full urls for a variety of reasons, so we use
1642
		# wfExpandUrl instead of simply prepending $wgServer
1643
		$url = wfExpandUrl( $url, $proto );
1644
1645
		# Finally, add the fragment.
1646
		$url .= $this->getFragmentForURL();
1647
1648
		Hooks::run( 'GetFullURL', [ &$this, &$url, $query ] );
1649
		return $url;
1650
	}
1651
1652
	/**
1653
	 * Get a URL with no fragment or server name (relative URL) from a Title object.
1654
	 * If this page is generated with action=render, however,
1655
	 * $wgServer is prepended to make an absolute URL.
1656
	 *
1657
	 * @see self::getFullURL to always get an absolute URL.
1658
	 * @see self::getLinkURL to always get a URL that's the simplest URL that will be
1659
	 *  valid to link, locally, to the current Title.
1660
	 * @see self::newFromText to produce a Title object.
1661
	 *
1662
	 * @param string|array $query An optional query string,
1663
	 *   not used for interwiki links. Can be specified as an associative array as well,
1664
	 *   e.g., array( 'action' => 'edit' ) (keys and values will be URL-escaped).
1665
	 *   Some query patterns will trigger various shorturl path replacements.
1666
	 * @param array $query2 An optional secondary query array. This one MUST
1667
	 *   be an array. If a string is passed it will be interpreted as a deprecated
1668
	 *   variant argument and urlencoded into a variant= argument.
1669
	 *   This second query argument will be added to the $query
1670
	 *   The second parameter is deprecated since 1.19. Pass it as a key,value
1671
	 *   pair in the first parameter array instead.
1672
	 *
1673
	 * @return string String of the URL.
1674
	 */
1675
	public function getLocalURL( $query = '', $query2 = false ) {
1676
		global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
1677
1678
		$query = self::fixUrlQueryArgs( $query, $query2 );
0 ignored issues
show
Bug introduced by
It seems like $query2 defined by parameter $query2 on line 1675 can also be of type array; however, Title::fixUrlQueryArgs() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1679
1680
		$interwiki = Interwiki::fetch( $this->mInterwiki );
0 ignored issues
show
Deprecated Code introduced by
The method Interwiki::fetch() has been deprecated with message: since 1.28, use InterwikiLookup instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1681
		if ( $interwiki ) {
1682
			$namespace = $this->getNsText();
1683
			if ( $namespace != '' ) {
1684
				# Can this actually happen? Interwikis shouldn't be parsed.
1685
				# Yes! It can in interwiki transclusion. But... it probably shouldn't.
1686
				$namespace .= ':';
1687
			}
1688
			$url = $interwiki->getURL( $namespace . $this->getDBkey() );
1689
			$url = wfAppendQuery( $url, $query );
1690
		} else {
1691
			$dbkey = wfUrlencode( $this->getPrefixedDBkey() );
1692
			if ( $query == '' ) {
1693
				$url = str_replace( '$1', $dbkey, $wgArticlePath );
1694
				Hooks::run( 'GetLocalURL::Article', [ &$this, &$url ] );
1695
			} else {
1696
				global $wgVariantArticlePath, $wgActionPaths, $wgContLang;
1697
				$url = false;
1698
				$matches = [];
1699
1700
				if ( !empty( $wgActionPaths )
1701
					&& preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches )
1702
				) {
1703
					$action = urldecode( $matches[2] );
1704
					if ( isset( $wgActionPaths[$action] ) ) {
1705
						$query = $matches[1];
1706
						if ( isset( $matches[4] ) ) {
1707
							$query .= $matches[4];
1708
						}
1709
						$url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
1710
						if ( $query != '' ) {
1711
							$url = wfAppendQuery( $url, $query );
1712
						}
1713
					}
1714
				}
1715
1716
				if ( $url === false
1717
					&& $wgVariantArticlePath
1718
					&& preg_match( '/^variant=([^&]*)$/', $query, $matches )
1719
					&& $wgContLang->getCode() === $this->getPageLanguage()->getCode()
1720
					&& $this->getPageLanguage()->hasVariants()
1721
				) {
1722
					$variant = urldecode( $matches[1] );
1723
					if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
1724
						// Only do the variant replacement if the given variant is a valid
1725
						// variant for the page's language.
1726
						$url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath );
1727
						$url = str_replace( '$1', $dbkey, $url );
1728
					}
1729
				}
1730
1731
				if ( $url === false ) {
1732
					if ( $query == '-' ) {
1733
						$query = '';
1734
					}
1735
					$url = "{$wgScript}?title={$dbkey}&{$query}";
1736
				}
1737
			}
1738
1739
			Hooks::run( 'GetLocalURL::Internal', [ &$this, &$url, $query ] );
1740
1741
			// @todo FIXME: This causes breakage in various places when we
1742
			// actually expected a local URL and end up with dupe prefixes.
1743
			if ( $wgRequest->getVal( 'action' ) == 'render' ) {
1744
				$url = $wgServer . $url;
1745
			}
1746
		}
1747
		Hooks::run( 'GetLocalURL', [ &$this, &$url, $query ] );
1748
		return $url;
1749
	}
1750
1751
	/**
1752
	 * Get a URL that's the simplest URL that will be valid to link, locally,
1753
	 * to the current Title.  It includes the fragment, but does not include
1754
	 * the server unless action=render is used (or the link is external).  If
1755
	 * there's a fragment but the prefixed text is empty, we just return a link
1756
	 * to the fragment.
1757
	 *
1758
	 * The result obviously should not be URL-escaped, but does need to be
1759
	 * HTML-escaped if it's being output in HTML.
1760
	 *
1761
	 * @param array $query
1762
	 * @param bool $query2
1763
	 * @param string $proto Protocol to use; setting this will cause a full URL to be used
1764
	 * @see self::getLocalURL for the arguments.
1765
	 * @return string The URL
1766
	 */
1767
	public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
1768
		if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) {
1769
			$ret = $this->getFullURL( $query, $query2, $proto );
1770
		} elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) {
1771
			$ret = $this->getFragmentForURL();
1772
		} else {
1773
			$ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
1774
		}
1775
		return $ret;
1776
	}
1777
1778
	/**
1779
	 * Get the URL form for an internal link.
1780
	 * - Used in various CDN-related code, in case we have a different
1781
	 * internal hostname for the server from the exposed one.
1782
	 *
1783
	 * This uses $wgInternalServer to qualify the path, or $wgServer
1784
	 * if $wgInternalServer is not set. If the server variable used is
1785
	 * protocol-relative, the URL will be expanded to http://
1786
	 *
1787
	 * @see self::getLocalURL for the arguments.
1788
	 * @return string The URL
1789
	 */
1790
	public function getInternalURL( $query = '', $query2 = false ) {
1791
		global $wgInternalServer, $wgServer;
1792
		$query = self::fixUrlQueryArgs( $query, $query2 );
1793
		$server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
1794
		$url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
1795
		Hooks::run( 'GetInternalURL', [ &$this, &$url, $query ] );
1796
		return $url;
1797
	}
1798
1799
	/**
1800
	 * Get the URL for a canonical link, for use in things like IRC and
1801
	 * e-mail notifications. Uses $wgCanonicalServer and the
1802
	 * GetCanonicalURL hook.
1803
	 *
1804
	 * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment
1805
	 *
1806
	 * @see self::getLocalURL for the arguments.
1807
	 * @return string The URL
1808
	 * @since 1.18
1809
	 */
1810
	public function getCanonicalURL( $query = '', $query2 = false ) {
1811
		$query = self::fixUrlQueryArgs( $query, $query2 );
1812
		$url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
1813
		Hooks::run( 'GetCanonicalURL', [ &$this, &$url, $query ] );
1814
		return $url;
1815
	}
1816
1817
	/**
1818
	 * Get the edit URL for this Title
1819
	 *
1820
	 * @return string The URL, or a null string if this is an interwiki link
1821
	 */
1822
	public function getEditURL() {
1823
		if ( $this->isExternal() ) {
1824
			return '';
1825
		}
1826
		$s = $this->getLocalURL( 'action=edit' );
1827
1828
		return $s;
1829
	}
1830
1831
	/**
1832
	 * Can $user perform $action on this page?
1833
	 * This skips potentially expensive cascading permission checks
1834
	 * as well as avoids expensive error formatting
1835
	 *
1836
	 * Suitable for use for nonessential UI controls in common cases, but
1837
	 * _not_ for functional access control.
1838
	 *
1839
	 * May provide false positives, but should never provide a false negative.
1840
	 *
1841
	 * @param string $action Action that permission needs to be checked for
1842
	 * @param User $user User to check (since 1.19); $wgUser will be used if not provided.
1843
	 * @return bool
1844
	 */
1845
	public function quickUserCan( $action, $user = null ) {
1846
		return $this->userCan( $action, $user, false );
1847
	}
1848
1849
	/**
1850
	 * Can $user perform $action on this page?
1851
	 *
1852
	 * @param string $action Action that permission needs to be checked for
1853
	 * @param User $user User to check (since 1.19); $wgUser will be used if not
1854
	 *   provided.
1855
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
1856
	 * @return bool
1857
	 */
1858
	public function userCan( $action, $user = null, $rigor = 'secure' ) {
1859
		if ( !$user instanceof User ) {
1860
			global $wgUser;
1861
			$user = $wgUser;
1862
		}
1863
1864
		return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) );
1865
	}
1866
1867
	/**
1868
	 * Can $user perform $action on this page?
1869
	 *
1870
	 * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
1871
	 *
1872
	 * @param string $action Action that permission needs to be checked for
1873
	 * @param User $user User to check
1874
	 * @param string $rigor One of (quick,full,secure)
1875
	 *   - quick  : does cheap permission checks from slaves (usable for GUI creation)
1876
	 *   - full   : does cheap and expensive checks possibly from a slave
1877
	 *   - secure : does cheap and expensive checks, using the master as needed
1878
	 * @param array $ignoreErrors Array of Strings Set this to a list of message keys
1879
	 *   whose corresponding errors may be ignored.
1880
	 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
1881
	 */
1882
	public function getUserPermissionsErrors(
1883
		$action, $user, $rigor = 'secure', $ignoreErrors = []
1884
	) {
1885
		$errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor );
1886
1887
		// Remove the errors being ignored.
1888
		foreach ( $errors as $index => $error ) {
1889
			$errKey = is_array( $error ) ? $error[0] : $error;
1890
1891
			if ( in_array( $errKey, $ignoreErrors ) ) {
1892
				unset( $errors[$index] );
1893
			}
1894
			if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
1895
				unset( $errors[$index] );
1896
			}
1897
		}
1898
1899
		return $errors;
1900
	}
1901
1902
	/**
1903
	 * Permissions checks that fail most often, and which are easiest to test.
1904
	 *
1905
	 * @param string $action The action to check
1906
	 * @param User $user User to check
1907
	 * @param array $errors List of current errors
1908
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
1909
	 * @param bool $short Short circuit on first error
1910
	 *
1911
	 * @return array List of errors
1912
	 */
1913
	private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) {
1914
		if ( !Hooks::run( 'TitleQuickPermissions',
1915
			[ $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ] )
1916
		) {
1917
			return $errors;
1918
		}
1919
1920
		if ( $action == 'create' ) {
1921
			if (
1922
				( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
1923
				( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
1924
			) {
1925
				$errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
1926
			}
1927
		} elseif ( $action == 'move' ) {
1928 View Code Duplication
			if ( !$user->isAllowed( 'move-rootuserpages' )
1929
					&& $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1930
				// Show user page-specific message only if the user can move other pages
1931
				$errors[] = [ 'cant-move-user-page' ];
1932
			}
1933
1934
			// Check if user is allowed to move files if it's a file
1935
			if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
1936
				$errors[] = [ 'movenotallowedfile' ];
1937
			}
1938
1939
			// Check if user is allowed to move category pages if it's a category page
1940
			if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
1941
				$errors[] = [ 'cant-move-category-page' ];
1942
			}
1943
1944
			if ( !$user->isAllowed( 'move' ) ) {
1945
				// User can't move anything
1946
				$userCanMove = User::groupHasPermission( 'user', 'move' );
1947
				$autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
1948
				if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
1949
					// custom message if logged-in users without any special rights can move
1950
					$errors[] = [ 'movenologintext' ];
1951
				} else {
1952
					$errors[] = [ 'movenotallowed' ];
1953
				}
1954
			}
1955
		} elseif ( $action == 'move-target' ) {
1956
			if ( !$user->isAllowed( 'move' ) ) {
1957
				// User can't move anything
1958
				$errors[] = [ 'movenotallowed' ];
1959 View Code Duplication
			} elseif ( !$user->isAllowed( 'move-rootuserpages' )
1960
					&& $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1961
				// Show user page-specific message only if the user can move other pages
1962
				$errors[] = [ 'cant-move-to-user-page' ];
1963
			} elseif ( !$user->isAllowed( 'move-categorypages' )
1964
					&& $this->mNamespace == NS_CATEGORY ) {
1965
				// Show category page-specific message only if the user can move other pages
1966
				$errors[] = [ 'cant-move-to-category-page' ];
1967
			}
1968
		} elseif ( !$user->isAllowed( $action ) ) {
1969
			$errors[] = $this->missingPermissionError( $action, $short );
1970
		}
1971
1972
		return $errors;
1973
	}
1974
1975
	/**
1976
	 * Add the resulting error code to the errors array
1977
	 *
1978
	 * @param array $errors List of current errors
1979
	 * @param array $result Result of errors
1980
	 *
1981
	 * @return array List of errors
1982
	 */
1983
	private function resultToError( $errors, $result ) {
1984
		if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
1985
			// A single array representing an error
1986
			$errors[] = $result;
1987
		} elseif ( is_array( $result ) && is_array( $result[0] ) ) {
1988
			// A nested array representing multiple errors
1989
			$errors = array_merge( $errors, $result );
1990
		} elseif ( $result !== '' && is_string( $result ) ) {
1991
			// A string representing a message-id
1992
			$errors[] = [ $result ];
1993
		} elseif ( $result instanceof MessageSpecifier ) {
1994
			// A message specifier representing an error
1995
			$errors[] = [ $result ];
1996
		} elseif ( $result === false ) {
1997
			// a generic "We don't want them to do that"
1998
			$errors[] = [ 'badaccess-group0' ];
1999
		}
2000
		return $errors;
2001
	}
2002
2003
	/**
2004
	 * Check various permission hooks
2005
	 *
2006
	 * @param string $action The action to check
2007
	 * @param User $user User to check
2008
	 * @param array $errors List of current errors
2009
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2010
	 * @param bool $short Short circuit on first error
2011
	 *
2012
	 * @return array List of errors
2013
	 */
2014
	private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) {
2015
		// Use getUserPermissionsErrors instead
2016
		$result = '';
2017
		if ( !Hooks::run( 'userCan', [ &$this, &$user, $action, &$result ] ) ) {
2018
			return $result ? [] : [ [ 'badaccess-group0' ] ];
2019
		}
2020
		// Check getUserPermissionsErrors hook
2021
		if ( !Hooks::run( 'getUserPermissionsErrors', [ &$this, &$user, $action, &$result ] ) ) {
2022
			$errors = $this->resultToError( $errors, $result );
2023
		}
2024
		// Check getUserPermissionsErrorsExpensive hook
2025
		if (
2026
			$rigor !== 'quick'
2027
			&& !( $short && count( $errors ) > 0 )
2028
			&& !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$this, &$user, $action, &$result ] )
2029
		) {
2030
			$errors = $this->resultToError( $errors, $result );
2031
		}
2032
2033
		return $errors;
2034
	}
2035
2036
	/**
2037
	 * Check permissions on special pages & namespaces
2038
	 *
2039
	 * @param string $action The action to check
2040
	 * @param User $user User to check
2041
	 * @param array $errors List of current errors
2042
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2043
	 * @param bool $short Short circuit on first error
2044
	 *
2045
	 * @return array List of errors
2046
	 */
2047
	private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) {
2048
		# Only 'createaccount' can be performed on special pages,
2049
		# which don't actually exist in the DB.
2050
		if ( NS_SPECIAL == $this->mNamespace && $action !== 'createaccount' ) {
2051
			$errors[] = [ 'ns-specialprotected' ];
2052
		}
2053
2054
		# Check $wgNamespaceProtection for restricted namespaces
2055
		if ( $this->isNamespaceProtected( $user ) ) {
2056
			$ns = $this->mNamespace == NS_MAIN ?
2057
				wfMessage( 'nstab-main' )->text() : $this->getNsText();
2058
			$errors[] = $this->mNamespace == NS_MEDIAWIKI ?
2059
				[ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
2060
		}
2061
2062
		return $errors;
2063
	}
2064
2065
	/**
2066
	 * Check CSS/JS sub-page permissions
2067
	 *
2068
	 * @param string $action The action to check
2069
	 * @param User $user User to check
2070
	 * @param array $errors List of current errors
2071
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2072
	 * @param bool $short Short circuit on first error
2073
	 *
2074
	 * @return array List of errors
2075
	 */
2076
	private function checkCSSandJSPermissions( $action, $user, $errors, $rigor, $short ) {
2077
		# Protect css/js subpages of user pages
2078
		# XXX: this might be better using restrictions
2079
		# XXX: right 'editusercssjs' is deprecated, for backward compatibility only
2080
		if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) {
2081
			if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
2082 View Code Duplication
				if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) {
2083
					$errors[] = [ 'mycustomcssprotected', $action ];
2084
				} elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) {
2085
					$errors[] = [ 'mycustomjsprotected', $action ];
2086
				}
2087 View Code Duplication
			} else {
2088
				if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) {
2089
					$errors[] = [ 'customcssprotected', $action ];
2090
				} elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
2091
					$errors[] = [ 'customjsprotected', $action ];
2092
				}
2093
			}
2094
		}
2095
2096
		return $errors;
2097
	}
2098
2099
	/**
2100
	 * Check against page_restrictions table requirements on this
2101
	 * page. The user must possess all required rights for this
2102
	 * action.
2103
	 *
2104
	 * @param string $action The action to check
2105
	 * @param User $user User to check
2106
	 * @param array $errors List of current errors
2107
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2108
	 * @param bool $short Short circuit on first error
2109
	 *
2110
	 * @return array List of errors
2111
	 */
2112
	private function checkPageRestrictions( $action, $user, $errors, $rigor, $short ) {
2113
		foreach ( $this->getRestrictions( $action ) as $right ) {
2114
			// Backwards compatibility, rewrite sysop -> editprotected
2115
			if ( $right == 'sysop' ) {
2116
				$right = 'editprotected';
2117
			}
2118
			// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2119
			if ( $right == 'autoconfirmed' ) {
2120
				$right = 'editsemiprotected';
2121
			}
2122
			if ( $right == '' ) {
2123
				continue;
2124
			}
2125
			if ( !$user->isAllowed( $right ) ) {
2126
				$errors[] = [ 'protectedpagetext', $right, $action ];
2127
			} elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
2128
				$errors[] = [ 'protectedpagetext', 'protect', $action ];
2129
			}
2130
		}
2131
2132
		return $errors;
2133
	}
2134
2135
	/**
2136
	 * Check restrictions on cascading pages.
2137
	 *
2138
	 * @param string $action The action to check
2139
	 * @param User $user User to check
2140
	 * @param array $errors List of current errors
2141
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2142
	 * @param bool $short Short circuit on first error
2143
	 *
2144
	 * @return array List of errors
2145
	 */
2146
	private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
2147
		if ( $rigor !== 'quick' && !$this->isCssJsSubpage() ) {
2148
			# We /could/ use the protection level on the source page, but it's
2149
			# fairly ugly as we have to establish a precedence hierarchy for pages
2150
			# included by multiple cascade-protected pages. So just restrict
2151
			# it to people with 'protect' permission, as they could remove the
2152
			# protection anyway.
2153
			list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
2154
			# Cascading protection depends on more than this page...
2155
			# Several cascading protected pages may include this page...
2156
			# Check each cascading level
2157
			# This is only for protection restrictions, not for all actions
2158
			if ( isset( $restrictions[$action] ) ) {
2159
				foreach ( $restrictions[$action] as $right ) {
2160
					// Backwards compatibility, rewrite sysop -> editprotected
2161
					if ( $right == 'sysop' ) {
2162
						$right = 'editprotected';
2163
					}
2164
					// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2165
					if ( $right == 'autoconfirmed' ) {
2166
						$right = 'editsemiprotected';
2167
					}
2168
					if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
2169
						$pages = '';
2170
						foreach ( $cascadingSources as $page ) {
0 ignored issues
show
Bug introduced by
The expression $cascadingSources of type array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
2171
							$pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
2172
						}
2173
						$errors[] = [ 'cascadeprotected', count( $cascadingSources ), $pages, $action ];
2174
					}
2175
				}
2176
			}
2177
		}
2178
2179
		return $errors;
2180
	}
2181
2182
	/**
2183
	 * Check action permissions not already checked in checkQuickPermissions
2184
	 *
2185
	 * @param string $action The action to check
2186
	 * @param User $user User to check
2187
	 * @param array $errors List of current errors
2188
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2189
	 * @param bool $short Short circuit on first error
2190
	 *
2191
	 * @return array List of errors
2192
	 */
2193
	private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) {
2194
		global $wgDeleteRevisionsLimit, $wgLang;
2195
2196
		if ( $action == 'protect' ) {
2197
			if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
2198
				// If they can't edit, they shouldn't protect.
2199
				$errors[] = [ 'protect-cantedit' ];
2200
			}
2201
		} elseif ( $action == 'create' ) {
2202
			$title_protection = $this->getTitleProtection();
2203
			if ( $title_protection ) {
2204
				if ( $title_protection['permission'] == ''
2205
					|| !$user->isAllowed( $title_protection['permission'] )
2206
				) {
2207
					$errors[] = [
2208
						'titleprotected',
2209
						User::whoIs( $title_protection['user'] ),
2210
						$title_protection['reason']
2211
					];
2212
				}
2213
			}
2214
		} elseif ( $action == 'move' ) {
2215
			// Check for immobile pages
2216
			if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2217
				// Specific message for this case
2218
				$errors[] = [ 'immobile-source-namespace', $this->getNsText() ];
2219
			} elseif ( !$this->isMovable() ) {
2220
				// Less specific message for rarer cases
2221
				$errors[] = [ 'immobile-source-page' ];
2222
			}
2223
		} elseif ( $action == 'move-target' ) {
2224
			if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2225
				$errors[] = [ 'immobile-target-namespace', $this->getNsText() ];
2226
			} elseif ( !$this->isMovable() ) {
2227
				$errors[] = [ 'immobile-target-page' ];
2228
			}
2229
		} elseif ( $action == 'delete' ) {
2230
			$tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true );
2231
			if ( !$tempErrors ) {
2232
				$tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
2233
					$user, $tempErrors, $rigor, true );
2234
			}
2235
			if ( $tempErrors ) {
2236
				// If protection keeps them from editing, they shouldn't be able to delete.
2237
				$errors[] = [ 'deleteprotected' ];
2238
			}
2239
			if ( $rigor !== 'quick' && $wgDeleteRevisionsLimit
2240
				&& !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion()
2241
			) {
2242
				$errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
2243
			}
2244
		}
2245
		return $errors;
2246
	}
2247
2248
	/**
2249
	 * Check that the user isn't blocked from editing.
2250
	 *
2251
	 * @param string $action The action to check
2252
	 * @param User $user User to check
2253
	 * @param array $errors List of current errors
2254
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2255
	 * @param bool $short Short circuit on first error
2256
	 *
2257
	 * @return array List of errors
2258
	 */
2259
	private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
2260
		// Account creation blocks handled at userlogin.
2261
		// Unblocking handled in SpecialUnblock
2262
		if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
2263
			return $errors;
2264
		}
2265
2266
		global $wgEmailConfirmToEdit;
2267
2268
		if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
2269
			$errors[] = [ 'confirmedittext' ];
2270
		}
2271
2272
		$useSlave = ( $rigor !== 'secure' );
2273
		if ( ( $action == 'edit' || $action == 'create' )
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
2274
			&& !$user->isBlockedFrom( $this, $useSlave )
2275
		) {
2276
			// Don't block the user from editing their own talk page unless they've been
2277
			// explicitly blocked from that too.
2278
		} elseif ( $user->isBlocked() && $user->getBlock()->prevents( $action ) !== false ) {
2279
			// @todo FIXME: Pass the relevant context into this function.
2280
			$errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() );
2281
		}
2282
2283
		return $errors;
2284
	}
2285
2286
	/**
2287
	 * Check that the user is allowed to read this page.
2288
	 *
2289
	 * @param string $action The action to check
2290
	 * @param User $user User to check
2291
	 * @param array $errors List of current errors
2292
	 * @param string $rigor Same format as Title::getUserPermissionsErrors()
2293
	 * @param bool $short Short circuit on first error
2294
	 *
2295
	 * @return array List of errors
2296
	 */
2297
	private function checkReadPermissions( $action, $user, $errors, $rigor, $short ) {
2298
		global $wgWhitelistRead, $wgWhitelistReadRegexp;
2299
2300
		$whitelisted = false;
2301
		if ( User::isEveryoneAllowed( 'read' ) ) {
2302
			# Shortcut for public wikis, allows skipping quite a bit of code
2303
			$whitelisted = true;
2304
		} elseif ( $user->isAllowed( 'read' ) ) {
2305
			# If the user is allowed to read pages, he is allowed to read all pages
2306
			$whitelisted = true;
2307
		} elseif ( $this->isSpecial( 'Userlogin' )
2308
			|| $this->isSpecial( 'ChangePassword' )
2309
			|| $this->isSpecial( 'PasswordReset' )
2310
		) {
2311
			# Always grant access to the login page.
2312
			# Even anons need to be able to log in.
2313
			$whitelisted = true;
2314
		} elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
2315
			# Time to check the whitelist
2316
			# Only do these checks is there's something to check against
2317
			$name = $this->getPrefixedText();
2318
			$dbName = $this->getPrefixedDBkey();
2319
2320
			// Check for explicit whitelisting with and without underscores
2321
			if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
2322
				$whitelisted = true;
2323
			} elseif ( $this->getNamespace() == NS_MAIN ) {
2324
				# Old settings might have the title prefixed with
2325
				# a colon for main-namespace pages
2326
				if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
2327
					$whitelisted = true;
2328
				}
2329
			} elseif ( $this->isSpecialPage() ) {
2330
				# If it's a special page, ditch the subpage bit and check again
2331
				$name = $this->getDBkey();
2332
				list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name );
2333
				if ( $name ) {
2334
					$pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
2335
					if ( in_array( $pure, $wgWhitelistRead, true ) ) {
2336
						$whitelisted = true;
2337
					}
2338
				}
2339
			}
2340
		}
2341
2342
		if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) {
2343
			$name = $this->getPrefixedText();
2344
			// Check for regex whitelisting
2345
			foreach ( $wgWhitelistReadRegexp as $listItem ) {
2346
				if ( preg_match( $listItem, $name ) ) {
2347
					$whitelisted = true;
2348
					break;
2349
				}
2350
			}
2351
		}
2352
2353
		if ( !$whitelisted ) {
2354
			# If the title is not whitelisted, give extensions a chance to do so...
2355
			Hooks::run( 'TitleReadWhitelist', [ $this, $user, &$whitelisted ] );
2356
			if ( !$whitelisted ) {
2357
				$errors[] = $this->missingPermissionError( $action, $short );
2358
			}
2359
		}
2360
2361
		return $errors;
2362
	}
2363
2364
	/**
2365
	 * Get a description array when the user doesn't have the right to perform
2366
	 * $action (i.e. when User::isAllowed() returns false)
2367
	 *
2368
	 * @param string $action The action to check
2369
	 * @param bool $short Short circuit on first error
2370
	 * @return array List of errors
2371
	 */
2372
	private function missingPermissionError( $action, $short ) {
2373
		// We avoid expensive display logic for quickUserCan's and such
2374
		if ( $short ) {
2375
			return [ 'badaccess-group0' ];
2376
		}
2377
2378
		$groups = array_map( [ 'User', 'makeGroupLinkWiki' ],
2379
			User::getGroupsWithPermission( $action ) );
2380
2381
		if ( count( $groups ) ) {
2382
			global $wgLang;
2383
			return [
2384
				'badaccess-groups',
2385
				$wgLang->commaList( $groups ),
2386
				count( $groups )
2387
			];
2388
		} else {
2389
			return [ 'badaccess-group0' ];
2390
		}
2391
	}
2392
2393
	/**
2394
	 * Can $user perform $action on this page? This is an internal function,
2395
	 * with multiple levels of checks depending on performance needs; see $rigor below.
2396
	 * It does not check wfReadOnly().
2397
	 *
2398
	 * @param string $action Action that permission needs to be checked for
2399
	 * @param User $user User to check
2400
	 * @param string $rigor One of (quick,full,secure)
2401
	 *   - quick  : does cheap permission checks from slaves (usable for GUI creation)
2402
	 *   - full   : does cheap and expensive checks possibly from a slave
2403
	 *   - secure : does cheap and expensive checks, using the master as needed
2404
	 * @param bool $short Set this to true to stop after the first permission error.
2405
	 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
2406
	 */
2407
	protected function getUserPermissionsErrorsInternal(
2408
		$action, $user, $rigor = 'secure', $short = false
2409
	) {
2410
		if ( $rigor === true ) {
2411
			$rigor = 'secure'; // b/c
2412
		} elseif ( $rigor === false ) {
2413
			$rigor = 'quick'; // b/c
2414
		} elseif ( !in_array( $rigor, [ 'quick', 'full', 'secure' ] ) ) {
2415
			throw new Exception( "Invalid rigor parameter '$rigor'." );
2416
		}
2417
2418
		# Read has special handling
2419
		if ( $action == 'read' ) {
2420
			$checks = [
2421
				'checkPermissionHooks',
2422
				'checkReadPermissions',
2423
			];
2424
		# Don't call checkSpecialsAndNSPermissions or checkCSSandJSPermissions
2425
		# here as it will lead to duplicate error messages. This is okay to do
2426
		# since anywhere that checks for create will also check for edit, and
2427
		# those checks are called for edit.
2428
		} elseif ( $action == 'create' ) {
2429
			$checks = [
2430
				'checkQuickPermissions',
2431
				'checkPermissionHooks',
2432
				'checkPageRestrictions',
2433
				'checkCascadingSourcesRestrictions',
2434
				'checkActionPermissions',
2435
				'checkUserBlock'
2436
			];
2437
		} else {
2438
			$checks = [
2439
				'checkQuickPermissions',
2440
				'checkPermissionHooks',
2441
				'checkSpecialsAndNSPermissions',
2442
				'checkCSSandJSPermissions',
2443
				'checkPageRestrictions',
2444
				'checkCascadingSourcesRestrictions',
2445
				'checkActionPermissions',
2446
				'checkUserBlock'
2447
			];
2448
		}
2449
2450
		$errors = [];
2451
		while ( count( $checks ) > 0 &&
2452
				!( $short && count( $errors ) > 0 ) ) {
2453
			$method = array_shift( $checks );
2454
			$errors = $this->$method( $action, $user, $errors, $rigor, $short );
2455
		}
2456
2457
		return $errors;
2458
	}
2459
2460
	/**
2461
	 * Get a filtered list of all restriction types supported by this wiki.
2462
	 * @param bool $exists True to get all restriction types that apply to
2463
	 * titles that do exist, False for all restriction types that apply to
2464
	 * titles that do not exist
2465
	 * @return array
2466
	 */
2467
	public static function getFilteredRestrictionTypes( $exists = true ) {
2468
		global $wgRestrictionTypes;
2469
		$types = $wgRestrictionTypes;
2470
		if ( $exists ) {
2471
			# Remove the create restriction for existing titles
2472
			$types = array_diff( $types, [ 'create' ] );
2473
		} else {
2474
			# Only the create and upload restrictions apply to non-existing titles
2475
			$types = array_intersect( $types, [ 'create', 'upload' ] );
2476
		}
2477
		return $types;
2478
	}
2479
2480
	/**
2481
	 * Returns restriction types for the current Title
2482
	 *
2483
	 * @return array Applicable restriction types
2484
	 */
2485
	public function getRestrictionTypes() {
2486
		if ( $this->isSpecialPage() ) {
2487
			return [];
2488
		}
2489
2490
		$types = self::getFilteredRestrictionTypes( $this->exists() );
2491
2492
		if ( $this->getNamespace() != NS_FILE ) {
2493
			# Remove the upload restriction for non-file titles
2494
			$types = array_diff( $types, [ 'upload' ] );
2495
		}
2496
2497
		Hooks::run( 'TitleGetRestrictionTypes', [ $this, &$types ] );
2498
2499
		wfDebug( __METHOD__ . ': applicable restrictions to [[' .
2500
			$this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
2501
2502
		return $types;
2503
	}
2504
2505
	/**
2506
	 * Is this title subject to title protection?
2507
	 * Title protection is the one applied against creation of such title.
2508
	 *
2509
	 * @return array|bool An associative array representing any existent title
2510
	 *   protection, or false if there's none.
2511
	 */
2512
	public function getTitleProtection() {
2513
		// Can't protect pages in special namespaces
2514
		if ( $this->getNamespace() < 0 ) {
2515
			return false;
2516
		}
2517
2518
		// Can't protect pages that exist.
2519
		if ( $this->exists() ) {
2520
			return false;
2521
		}
2522
2523
		if ( $this->mTitleProtection === null ) {
2524
			$dbr = wfGetDB( DB_SLAVE );
2525
			$res = $dbr->select(
2526
				'protected_titles',
2527
				[
2528
					'user' => 'pt_user',
2529
					'reason' => 'pt_reason',
2530
					'expiry' => 'pt_expiry',
2531
					'permission' => 'pt_create_perm'
2532
				],
2533
				[ 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ],
2534
				__METHOD__
2535
			);
2536
2537
			// fetchRow returns false if there are no rows.
2538
			$row = $dbr->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $dbr->select('protected_...etDBkey()), __METHOD__) on line 2525 can also be of type boolean; however, IDatabase::fetchRow() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2539
			if ( $row ) {
2540
				if ( $row['permission'] == 'sysop' ) {
2541
					$row['permission'] = 'editprotected'; // B/C
2542
				}
2543
				if ( $row['permission'] == 'autoconfirmed' ) {
2544
					$row['permission'] = 'editsemiprotected'; // B/C
2545
				}
2546
				$row['expiry'] = $dbr->decodeExpiry( $row['expiry'] );
2547
			}
2548
			$this->mTitleProtection = $row;
2549
		}
2550
		return $this->mTitleProtection;
2551
	}
2552
2553
	/**
2554
	 * Remove any title protection due to page existing
2555
	 */
2556
	public function deleteTitleProtection() {
2557
		$dbw = wfGetDB( DB_MASTER );
2558
2559
		$dbw->delete(
2560
			'protected_titles',
2561
			[ 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ],
2562
			__METHOD__
2563
		);
2564
		$this->mTitleProtection = false;
2565
	}
2566
2567
	/**
2568
	 * Is this page "semi-protected" - the *only* protection levels are listed
2569
	 * in $wgSemiprotectedRestrictionLevels?
2570
	 *
2571
	 * @param string $action Action to check (default: edit)
2572
	 * @return bool
2573
	 */
2574
	public function isSemiProtected( $action = 'edit' ) {
2575
		global $wgSemiprotectedRestrictionLevels;
2576
2577
		$restrictions = $this->getRestrictions( $action );
2578
		$semi = $wgSemiprotectedRestrictionLevels;
2579
		if ( !$restrictions || !$semi ) {
2580
			// Not protected, or all protection is full protection
2581
			return false;
2582
		}
2583
2584
		// Remap autoconfirmed to editsemiprotected for BC
2585
		foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) {
2586
			$semi[$key] = 'editsemiprotected';
2587
		}
2588
		foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) {
2589
			$restrictions[$key] = 'editsemiprotected';
2590
		}
2591
2592
		return !array_diff( $restrictions, $semi );
2593
	}
2594
2595
	/**
2596
	 * Does the title correspond to a protected article?
2597
	 *
2598
	 * @param string $action The action the page is protected from,
2599
	 * by default checks all actions.
2600
	 * @return bool
2601
	 */
2602
	public function isProtected( $action = '' ) {
2603
		global $wgRestrictionLevels;
2604
2605
		$restrictionTypes = $this->getRestrictionTypes();
2606
2607
		# Special pages have inherent protection
2608
		if ( $this->isSpecialPage() ) {
2609
			return true;
2610
		}
2611
2612
		# Check regular protection levels
2613
		foreach ( $restrictionTypes as $type ) {
2614
			if ( $action == $type || $action == '' ) {
2615
				$r = $this->getRestrictions( $type );
2616
				foreach ( $wgRestrictionLevels as $level ) {
2617
					if ( in_array( $level, $r ) && $level != '' ) {
2618
						return true;
2619
					}
2620
				}
2621
			}
2622
		}
2623
2624
		return false;
2625
	}
2626
2627
	/**
2628
	 * Determines if $user is unable to edit this page because it has been protected
2629
	 * by $wgNamespaceProtection.
2630
	 *
2631
	 * @param User $user User object to check permissions
2632
	 * @return bool
2633
	 */
2634
	public function isNamespaceProtected( User $user ) {
2635
		global $wgNamespaceProtection;
2636
2637
		if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
2638
			foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
2639
				if ( $right != '' && !$user->isAllowed( $right ) ) {
2640
					return true;
2641
				}
2642
			}
2643
		}
2644
		return false;
2645
	}
2646
2647
	/**
2648
	 * Cascading protection: Return true if cascading restrictions apply to this page, false if not.
2649
	 *
2650
	 * @return bool If the page is subject to cascading restrictions.
2651
	 */
2652
	public function isCascadeProtected() {
2653
		list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
2654
		return ( $sources > 0 );
2655
	}
2656
2657
	/**
2658
	 * Determines whether cascading protection sources have already been loaded from
2659
	 * the database.
2660
	 *
2661
	 * @param bool $getPages True to check if the pages are loaded, or false to check
2662
	 * if the status is loaded.
2663
	 * @return bool Whether or not the specified information has been loaded
2664
	 * @since 1.23
2665
	 */
2666
	public function areCascadeProtectionSourcesLoaded( $getPages = true ) {
2667
		return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null;
2668
	}
2669
2670
	/**
2671
	 * Cascading protection: Get the source of any cascading restrictions on this page.
2672
	 *
2673
	 * @param bool $getPages Whether or not to retrieve the actual pages
2674
	 *        that the restrictions have come from and the actual restrictions
2675
	 *        themselves.
2676
	 * @return array Two elements: First is an array of Title objects of the
2677
	 *        pages from which cascading restrictions have come, false for
2678
	 *        none, or true if such restrictions exist but $getPages was not
2679
	 *        set. Second is an array like that returned by
2680
	 *        Title::getAllRestrictions(), or an empty array if $getPages is
2681
	 *        false.
2682
	 */
2683
	public function getCascadeProtectionSources( $getPages = true ) {
2684
		$pagerestrictions = [];
2685
2686
		if ( $this->mCascadeSources !== null && $getPages ) {
2687
			return [ $this->mCascadeSources, $this->mCascadingRestrictions ];
2688
		} elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) {
2689
			return [ $this->mHasCascadingRestrictions, $pagerestrictions ];
2690
		}
2691
2692
		$dbr = wfGetDB( DB_SLAVE );
2693
2694
		if ( $this->getNamespace() == NS_FILE ) {
2695
			$tables = [ 'imagelinks', 'page_restrictions' ];
2696
			$where_clauses = [
2697
				'il_to' => $this->getDBkey(),
2698
				'il_from=pr_page',
2699
				'pr_cascade' => 1
2700
			];
2701
		} else {
2702
			$tables = [ 'templatelinks', 'page_restrictions' ];
2703
			$where_clauses = [
2704
				'tl_namespace' => $this->getNamespace(),
2705
				'tl_title' => $this->getDBkey(),
2706
				'tl_from=pr_page',
2707
				'pr_cascade' => 1
2708
			];
2709
		}
2710
2711
		if ( $getPages ) {
2712
			$cols = [ 'pr_page', 'page_namespace', 'page_title',
2713
				'pr_expiry', 'pr_type', 'pr_level' ];
2714
			$where_clauses[] = 'page_id=pr_page';
2715
			$tables[] = 'page';
2716
		} else {
2717
			$cols = [ 'pr_expiry' ];
2718
		}
2719
2720
		$res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
2721
2722
		$sources = $getPages ? [] : false;
2723
		$now = wfTimestampNow();
2724
2725
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
2726
			$expiry = $dbr->decodeExpiry( $row->pr_expiry );
2727
			if ( $expiry > $now ) {
2728
				if ( $getPages ) {
2729
					$page_id = $row->pr_page;
2730
					$page_ns = $row->page_namespace;
2731
					$page_title = $row->page_title;
2732
					$sources[$page_id] = Title::makeTitle( $page_ns, $page_title );
2733
					# Add groups needed for each restriction type if its not already there
2734
					# Make sure this restriction type still exists
2735
2736
					if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
2737
						$pagerestrictions[$row->pr_type] = [];
2738
					}
2739
2740
					if (
2741
						isset( $pagerestrictions[$row->pr_type] )
2742
						&& !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] )
2743
					) {
2744
						$pagerestrictions[$row->pr_type][] = $row->pr_level;
2745
					}
2746
				} else {
2747
					$sources = true;
2748
				}
2749
			}
2750
		}
2751
2752
		if ( $getPages ) {
2753
			$this->mCascadeSources = $sources;
0 ignored issues
show
Documentation Bug introduced by
It seems like $sources can also be of type boolean. However, the property $mCascadeSources is declared as type array. 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...
2754
			$this->mCascadingRestrictions = $pagerestrictions;
2755
		} else {
2756
			$this->mHasCascadingRestrictions = $sources;
0 ignored issues
show
Documentation Bug introduced by
It seems like $sources can also be of type array. However, the property $mHasCascadingRestrictions is declared as type boolean. 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...
2757
		}
2758
2759
		return [ $sources, $pagerestrictions ];
2760
	}
2761
2762
	/**
2763
	 * Accessor for mRestrictionsLoaded
2764
	 *
2765
	 * @return bool Whether or not the page's restrictions have already been
2766
	 * loaded from the database
2767
	 * @since 1.23
2768
	 */
2769
	public function areRestrictionsLoaded() {
2770
		return $this->mRestrictionsLoaded;
2771
	}
2772
2773
	/**
2774
	 * Accessor/initialisation for mRestrictions
2775
	 *
2776
	 * @param string $action Action that permission needs to be checked for
2777
	 * @return array Restriction levels needed to take the action. All levels are
2778
	 *     required. Note that restriction levels are normally user rights, but 'sysop'
2779
	 *     and 'autoconfirmed' are also allowed for backwards compatibility. These should
2780
	 *     be mapped to 'editprotected' and 'editsemiprotected' respectively.
2781
	 */
2782
	public function getRestrictions( $action ) {
2783
		if ( !$this->mRestrictionsLoaded ) {
2784
			$this->loadRestrictions();
2785
		}
2786
		return isset( $this->mRestrictions[$action] )
2787
				? $this->mRestrictions[$action]
2788
				: [];
2789
	}
2790
2791
	/**
2792
	 * Accessor/initialisation for mRestrictions
2793
	 *
2794
	 * @return array Keys are actions, values are arrays as returned by
2795
	 *     Title::getRestrictions()
2796
	 * @since 1.23
2797
	 */
2798
	public function getAllRestrictions() {
2799
		if ( !$this->mRestrictionsLoaded ) {
2800
			$this->loadRestrictions();
2801
		}
2802
		return $this->mRestrictions;
2803
	}
2804
2805
	/**
2806
	 * Get the expiry time for the restriction against a given action
2807
	 *
2808
	 * @param string $action
2809
	 * @return string|bool 14-char timestamp, or 'infinity' if the page is protected forever
2810
	 *     or not protected at all, or false if the action is not recognised.
2811
	 */
2812
	public function getRestrictionExpiry( $action ) {
2813
		if ( !$this->mRestrictionsLoaded ) {
2814
			$this->loadRestrictions();
2815
		}
2816
		return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
2817
	}
2818
2819
	/**
2820
	 * Returns cascading restrictions for the current article
2821
	 *
2822
	 * @return bool
2823
	 */
2824
	function areRestrictionsCascading() {
2825
		if ( !$this->mRestrictionsLoaded ) {
2826
			$this->loadRestrictions();
2827
		}
2828
2829
		return $this->mCascadeRestriction;
2830
	}
2831
2832
	/**
2833
	 * Loads a string into mRestrictions array
2834
	 *
2835
	 * @param ResultWrapper $res Resource restrictions as an SQL result.
2836
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2837
	 *        restrictions from page table (pre 1.10)
2838
	 */
2839
	private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) {
2840
		$rows = [];
2841
2842
		foreach ( $res as $row ) {
2843
			$rows[] = $row;
2844
		}
2845
2846
		$this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
2847
	}
2848
2849
	/**
2850
	 * Compiles list of active page restrictions from both page table (pre 1.10)
2851
	 * and page_restrictions table for this existing page.
2852
	 * Public for usage by LiquidThreads.
2853
	 *
2854
	 * @param array $rows Array of db result objects
2855
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2856
	 *   restrictions from page table (pre 1.10)
2857
	 */
2858
	public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
2859
		$dbr = wfGetDB( DB_SLAVE );
2860
2861
		$restrictionTypes = $this->getRestrictionTypes();
2862
2863
		foreach ( $restrictionTypes as $type ) {
2864
			$this->mRestrictions[$type] = [];
2865
			$this->mRestrictionsExpiry[$type] = 'infinity';
2866
		}
2867
2868
		$this->mCascadeRestriction = false;
2869
2870
		# Backwards-compatibility: also load the restrictions from the page record (old format).
2871
		if ( $oldFashionedRestrictions !== null ) {
2872
			$this->mOldRestrictions = $oldFashionedRestrictions;
2873
		}
2874
2875
		if ( $this->mOldRestrictions === false ) {
2876
			$this->mOldRestrictions = $dbr->selectField( 'page', 'page_restrictions',
2877
				[ 'page_id' => $this->getArticleID() ], __METHOD__ );
2878
		}
2879
2880
		if ( $this->mOldRestrictions != '' ) {
2881
			foreach ( explode( ':', trim( $this->mOldRestrictions ) ) as $restrict ) {
2882
				$temp = explode( '=', trim( $restrict ) );
2883
				if ( count( $temp ) == 1 ) {
2884
					// old old format should be treated as edit/move restriction
2885
					$this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
2886
					$this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
2887
				} else {
2888
					$restriction = trim( $temp[1] );
2889
					if ( $restriction != '' ) { // some old entries are empty
2890
						$this->mRestrictions[$temp[0]] = explode( ',', $restriction );
2891
					}
2892
				}
2893
			}
2894
		}
2895
2896
		if ( count( $rows ) ) {
2897
			# Current system - load second to make them override.
2898
			$now = wfTimestampNow();
2899
2900
			# Cycle through all the restrictions.
2901
			foreach ( $rows as $row ) {
2902
2903
				// Don't take care of restrictions types that aren't allowed
2904
				if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
2905
					continue;
2906
				}
2907
2908
				// This code should be refactored, now that it's being used more generally,
2909
				// But I don't really see any harm in leaving it in Block for now -werdna
2910
				$expiry = $dbr->decodeExpiry( $row->pr_expiry );
2911
2912
				// Only apply the restrictions if they haven't expired!
2913
				if ( !$expiry || $expiry > $now ) {
2914
					$this->mRestrictionsExpiry[$row->pr_type] = $expiry;
2915
					$this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
2916
2917
					$this->mCascadeRestriction |= $row->pr_cascade;
2918
				}
2919
			}
2920
		}
2921
2922
		$this->mRestrictionsLoaded = true;
2923
	}
2924
2925
	/**
2926
	 * Load restrictions from the page_restrictions table
2927
	 *
2928
	 * @param string $oldFashionedRestrictions Comma-separated list of page
2929
	 *   restrictions from page table (pre 1.10)
2930
	 */
2931
	public function loadRestrictions( $oldFashionedRestrictions = null ) {
2932
		if ( !$this->mRestrictionsLoaded ) {
2933
			$dbr = wfGetDB( DB_SLAVE );
2934
			if ( $this->exists() ) {
2935
				$res = $dbr->select(
2936
					'page_restrictions',
2937
					[ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ],
2938
					[ 'pr_page' => $this->getArticleID() ],
2939
					__METHOD__
2940
				);
2941
2942
				$this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $dbr->select('page_restr...ticleID()), __METHOD__) on line 2935 can also be of type boolean; however, Title::loadRestrictionsFromResultWrapper() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2943
			} else {
2944
				$title_protection = $this->getTitleProtection();
2945
2946
				if ( $title_protection ) {
2947
					$now = wfTimestampNow();
2948
					$expiry = $dbr->decodeExpiry( $title_protection['expiry'] );
2949
2950
					if ( !$expiry || $expiry > $now ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $expiry 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...
2951
						// Apply the restrictions
2952
						$this->mRestrictionsExpiry['create'] = $expiry;
2953
						$this->mRestrictions['create'] = explode( ',', trim( $title_protection['permission'] ) );
2954
					} else { // Get rid of the old restrictions
2955
						$this->mTitleProtection = false;
2956
					}
2957
				} else {
2958
					$this->mRestrictionsExpiry['create'] = 'infinity';
2959
				}
2960
				$this->mRestrictionsLoaded = true;
2961
			}
2962
		}
2963
	}
2964
2965
	/**
2966
	 * Flush the protection cache in this object and force reload from the database.
2967
	 * This is used when updating protection from WikiPage::doUpdateRestrictions().
2968
	 */
2969
	public function flushRestrictions() {
2970
		$this->mRestrictionsLoaded = false;
2971
		$this->mTitleProtection = null;
2972
	}
2973
2974
	/**
2975
	 * Purge expired restrictions from the page_restrictions table
2976
	 *
2977
	 * This will purge no more than $wgUpdateRowsPerQuery page_restrictions rows
2978
	 */
2979
	static function purgeExpiredRestrictions() {
2980
		if ( wfReadOnly() ) {
2981
			return;
2982
		}
2983
2984
		DeferredUpdates::addUpdate( new AtomicSectionUpdate(
2985
			wfGetDB( DB_MASTER ),
2986
			__METHOD__,
2987
			function ( IDatabase $dbw, $fname ) {
2988
				$config = MediaWikiServices::getInstance()->getMainConfig();
2989
				$ids = $dbw->selectFieldValues(
2990
					'page_restrictions',
2991
					'pr_id',
2992
					[ 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
2993
					$fname,
2994
					[ 'LIMIT' => $config->get( 'UpdateRowsPerQuery' ) ] // T135470
2995
				);
2996
				if ( $ids ) {
2997
					$dbw->delete( 'page_restrictions', [ 'pr_id' => $ids ], $fname );
2998
				}
2999
			}
3000
		) );
3001
3002
		DeferredUpdates::addUpdate( new AtomicSectionUpdate(
3003
			wfGetDB( DB_MASTER ),
3004
			__METHOD__,
3005
			function ( IDatabase $dbw, $fname ) {
3006
				$dbw->delete(
3007
					'protected_titles',
3008
					[ 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3009
					$fname
3010
				);
3011
			}
3012
		) );
3013
	}
3014
3015
	/**
3016
	 * Does this have subpages?  (Warning, usually requires an extra DB query.)
3017
	 *
3018
	 * @return bool
3019
	 */
3020
	public function hasSubpages() {
3021
		if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
3022
			# Duh
3023
			return false;
3024
		}
3025
3026
		# We dynamically add a member variable for the purpose of this method
3027
		# alone to cache the result.  There's no point in having it hanging
3028
		# around uninitialized in every Title object; therefore we only add it
3029
		# if needed and don't declare it statically.
3030
		if ( $this->mHasSubpages === null ) {
3031
			$this->mHasSubpages = false;
3032
			$subpages = $this->getSubpages( 1 );
3033
			if ( $subpages instanceof TitleArray ) {
3034
				$this->mHasSubpages = (bool)$subpages->count();
3035
			}
3036
		}
3037
3038
		return $this->mHasSubpages;
3039
	}
3040
3041
	/**
3042
	 * Get all subpages of this page.
3043
	 *
3044
	 * @param int $limit Maximum number of subpages to fetch; -1 for no limit
3045
	 * @return TitleArray|array TitleArray, or empty array if this page's namespace
3046
	 *  doesn't allow subpages
3047
	 */
3048
	public function getSubpages( $limit = -1 ) {
3049
		if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3050
			return [];
3051
		}
3052
3053
		$dbr = wfGetDB( DB_SLAVE );
3054
		$conds['page_namespace'] = $this->getNamespace();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$conds was never initialized. Although not strictly required by PHP, it is generally a good practice to add $conds = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
3055
		$conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
3056
		$options = [];
3057
		if ( $limit > -1 ) {
3058
			$options['LIMIT'] = $limit;
3059
		}
3060
		$this->mSubpages = TitleArray::newFromResult(
0 ignored issues
show
Bug introduced by
The property mSubpages does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
3061
			$dbr->select( 'page',
0 ignored issues
show
Bug introduced by
It seems like $dbr->select('page', arr..., __METHOD__, $options) targeting DatabaseBase::select() can also be of type boolean; however, TitleArray::newFromResult() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
3062
				[ 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ],
3063
				$conds,
3064
				__METHOD__,
3065
				$options
3066
			)
3067
		);
3068
		return $this->mSubpages;
3069
	}
3070
3071
	/**
3072
	 * Is there a version of this page in the deletion archive?
3073
	 *
3074
	 * @return int The number of archived revisions
3075
	 */
3076
	public function isDeleted() {
3077
		if ( $this->getNamespace() < 0 ) {
3078
			$n = 0;
3079
		} else {
3080
			$dbr = wfGetDB( DB_SLAVE );
3081
3082
			$n = $dbr->selectField( 'archive', 'COUNT(*)',
3083
				[ 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ],
3084
				__METHOD__
3085
			);
3086 View Code Duplication
			if ( $this->getNamespace() == NS_FILE ) {
3087
				$n += $dbr->selectField( 'filearchive', 'COUNT(*)',
3088
					[ 'fa_name' => $this->getDBkey() ],
3089
					__METHOD__
3090
				);
3091
			}
3092
		}
3093
		return (int)$n;
3094
	}
3095
3096
	/**
3097
	 * Is there a version of this page in the deletion archive?
3098
	 *
3099
	 * @return bool
3100
	 */
3101
	public function isDeletedQuick() {
3102
		if ( $this->getNamespace() < 0 ) {
3103
			return false;
3104
		}
3105
		$dbr = wfGetDB( DB_SLAVE );
3106
		$deleted = (bool)$dbr->selectField( 'archive', '1',
3107
			[ 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ],
3108
			__METHOD__
3109
		);
3110 View Code Duplication
		if ( !$deleted && $this->getNamespace() == NS_FILE ) {
3111
			$deleted = (bool)$dbr->selectField( 'filearchive', '1',
3112
				[ 'fa_name' => $this->getDBkey() ],
3113
				__METHOD__
3114
			);
3115
		}
3116
		return $deleted;
3117
	}
3118
3119
	/**
3120
	 * Get the article ID for this Title from the link cache,
3121
	 * adding it if necessary
3122
	 *
3123
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select
3124
	 *  for update
3125
	 * @return int The ID
3126
	 */
3127
	public function getArticleID( $flags = 0 ) {
3128
		if ( $this->getNamespace() < 0 ) {
3129
			$this->mArticleID = 0;
3130
			return $this->mArticleID;
3131
		}
3132
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3133
		if ( $flags & self::GAID_FOR_UPDATE ) {
3134
			$oldUpdate = $linkCache->forUpdate( true );
3135
			$linkCache->clearLink( $this );
3136
			$this->mArticleID = $linkCache->addLinkObj( $this );
3137
			$linkCache->forUpdate( $oldUpdate );
3138
		} else {
3139
			if ( -1 == $this->mArticleID ) {
3140
				$this->mArticleID = $linkCache->addLinkObj( $this );
3141
			}
3142
		}
3143
		return $this->mArticleID;
3144
	}
3145
3146
	/**
3147
	 * Is this an article that is a redirect page?
3148
	 * Uses link cache, adding it if necessary
3149
	 *
3150
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3151
	 * @return bool
3152
	 */
3153
	public function isRedirect( $flags = 0 ) {
3154
		if ( !is_null( $this->mRedirect ) ) {
3155
			return $this->mRedirect;
3156
		}
3157
		if ( !$this->getArticleID( $flags ) ) {
3158
			$this->mRedirect = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type null of property $mRedirect.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
3159
			return $this->mRedirect;
3160
		}
3161
3162
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3163
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3164
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
3165
		if ( $cached === null ) {
3166
			# Trust LinkCache's state over our own
3167
			# LinkCache is telling us that the page doesn't exist, despite there being cached
3168
			# data relating to an existing page in $this->mArticleID. Updaters should clear
3169
			# LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is
3170
			# set, then LinkCache will definitely be up to date here, since getArticleID() forces
3171
			# LinkCache to refresh its data from the master.
3172
			$this->mRedirect = false;
3173
			return $this->mRedirect;
3174
		}
3175
3176
		$this->mRedirect = (bool)$cached;
0 ignored issues
show
Documentation Bug introduced by
It seems like (bool) $cached of type boolean is incompatible with the declared type null of property $mRedirect.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
3177
3178
		return $this->mRedirect;
3179
	}
3180
3181
	/**
3182
	 * What is the length of this page?
3183
	 * Uses link cache, adding it if necessary
3184
	 *
3185
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3186
	 * @return int
3187
	 */
3188
	public function getLength( $flags = 0 ) {
3189
		if ( $this->mLength != -1 ) {
3190
			return $this->mLength;
3191
		}
3192
		if ( !$this->getArticleID( $flags ) ) {
3193
			$this->mLength = 0;
3194
			return $this->mLength;
3195
		}
3196
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3197
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3198
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
3199
		if ( $cached === null ) {
3200
			# Trust LinkCache's state over our own, as for isRedirect()
3201
			$this->mLength = 0;
3202
			return $this->mLength;
3203
		}
3204
3205
		$this->mLength = intval( $cached );
3206
3207
		return $this->mLength;
3208
	}
3209
3210
	/**
3211
	 * What is the page_latest field for this page?
3212
	 *
3213
	 * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3214
	 * @return int Int or 0 if the page doesn't exist
3215
	 */
3216
	public function getLatestRevID( $flags = 0 ) {
3217
		if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
3218
			return intval( $this->mLatestID );
3219
		}
3220
		if ( !$this->getArticleID( $flags ) ) {
3221
			$this->mLatestID = 0;
3222
			return $this->mLatestID;
3223
		}
3224
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3225
		$linkCache->addLinkObj( $this ); # in case we already had an article ID
3226
		$cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
3227
		if ( $cached === null ) {
3228
			# Trust LinkCache's state over our own, as for isRedirect()
3229
			$this->mLatestID = 0;
3230
			return $this->mLatestID;
3231
		}
3232
3233
		$this->mLatestID = intval( $cached );
3234
3235
		return $this->mLatestID;
3236
	}
3237
3238
	/**
3239
	 * This clears some fields in this object, and clears any associated
3240
	 * keys in the "bad links" section of the link cache.
3241
	 *
3242
	 * - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow
3243
	 * loading of the new page_id. It's also called from
3244
	 * WikiPage::doDeleteArticleReal()
3245
	 *
3246
	 * @param int $newid The new Article ID
3247
	 */
3248
	public function resetArticleID( $newid ) {
3249
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3250
		$linkCache->clearLink( $this );
3251
3252
		if ( $newid === false ) {
3253
			$this->mArticleID = -1;
3254
		} else {
3255
			$this->mArticleID = intval( $newid );
3256
		}
3257
		$this->mRestrictionsLoaded = false;
3258
		$this->mRestrictions = [];
3259
		$this->mOldRestrictions = false;
3260
		$this->mRedirect = null;
3261
		$this->mLength = -1;
3262
		$this->mLatestID = false;
3263
		$this->mContentModel = false;
3264
		$this->mEstimateRevisions = null;
3265
		$this->mPageLanguage = false;
3266
		$this->mDbPageLanguage = false;
3267
		$this->mIsBigDeletion = null;
3268
	}
3269
3270
	public static function clearCaches() {
3271
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3272
		$linkCache->clear();
3273
3274
		$titleCache = self::getTitleCache();
3275
		$titleCache->clear();
3276
	}
3277
3278
	/**
3279
	 * Capitalize a text string for a title if it belongs to a namespace that capitalizes
3280
	 *
3281
	 * @param string $text Containing title to capitalize
3282
	 * @param int $ns Namespace index, defaults to NS_MAIN
3283
	 * @return string Containing capitalized title
3284
	 */
3285
	public static function capitalize( $text, $ns = NS_MAIN ) {
3286
		global $wgContLang;
3287
3288
		if ( MWNamespace::isCapitalized( $ns ) ) {
3289
			return $wgContLang->ucfirst( $text );
3290
		} else {
3291
			return $text;
3292
		}
3293
	}
3294
3295
	/**
3296
	 * Secure and split - main initialisation function for this object
3297
	 *
3298
	 * Assumes that mDbkeyform has been set, and is urldecoded
3299
	 * and uses underscores, but not otherwise munged.  This function
3300
	 * removes illegal characters, splits off the interwiki and
3301
	 * namespace prefixes, sets the other forms, and canonicalizes
3302
	 * everything.
3303
	 *
3304
	 * @throws MalformedTitleException On invalid titles
3305
	 * @return bool True on success
3306
	 */
3307
	private function secureAndSplit() {
3308
		# Initialisation
3309
		$this->mInterwiki = '';
3310
		$this->mFragment = '';
3311
		$this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
3312
3313
		$dbkey = $this->mDbkeyform;
3314
3315
		// @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
3316
		//        the parsing code with Title, while avoiding massive refactoring.
3317
		// @todo: get rid of secureAndSplit, refactor parsing code.
3318
		// @note: getTitleParser() returns a TitleParser implementation which does not have a
3319
		//        splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
3320
		$titleCodec = MediaWikiServices::getInstance()->getTitleParser();
3321
		// MalformedTitleException can be thrown here
3322
		$parts = $titleCodec->splitTitleString( $dbkey, $this->getDefaultNamespace() );
3323
3324
		# Fill fields
3325
		$this->setFragment( '#' . $parts['fragment'] );
3326
		$this->mInterwiki = $parts['interwiki'];
3327
		$this->mLocalInterwiki = $parts['local_interwiki'];
3328
		$this->mNamespace = $parts['namespace'];
3329
		$this->mUserCaseDBKey = $parts['user_case_dbkey'];
3330
3331
		$this->mDbkeyform = $parts['dbkey'];
3332
		$this->mUrlform = wfUrlencode( $this->mDbkeyform );
3333
		$this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
3334
3335
		# We already know that some pages won't be in the database!
3336
		if ( $this->isExternal() || $this->mNamespace == NS_SPECIAL ) {
3337
			$this->mArticleID = 0;
3338
		}
3339
3340
		return true;
3341
	}
3342
3343
	/**
3344
	 * Get an array of Title objects linking to this Title
3345
	 * Also stores the IDs in the link cache.
3346
	 *
3347
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3348
	 * On heavily-used templates it will max out the memory.
3349
	 *
3350
	 * @param array $options May be FOR UPDATE
3351
	 * @param string $table Table name
3352
	 * @param string $prefix Fields prefix
3353
	 * @return Title[] Array of Title objects linking here
3354
	 */
3355
	public function getLinksTo( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3356
		if ( count( $options ) > 0 ) {
3357
			$db = wfGetDB( DB_MASTER );
3358
		} else {
3359
			$db = wfGetDB( DB_SLAVE );
3360
		}
3361
3362
		$res = $db->select(
3363
			[ 'page', $table ],
3364
			self::getSelectFields(),
3365
			[
3366
				"{$prefix}_from=page_id",
3367
				"{$prefix}_namespace" => $this->getNamespace(),
3368
				"{$prefix}_title" => $this->getDBkey() ],
3369
			__METHOD__,
3370
			$options
3371
		);
3372
3373
		$retVal = [];
3374
		if ( $res->numRows() ) {
3375
			$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3376 View Code Duplication
			foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
3377
				$titleObj = Title::makeTitle( $row->page_namespace, $row->page_title );
3378
				if ( $titleObj ) {
3379
					$linkCache->addGoodLinkObjFromRow( $titleObj, $row );
3380
					$retVal[] = $titleObj;
3381
				}
3382
			}
3383
		}
3384
		return $retVal;
3385
	}
3386
3387
	/**
3388
	 * Get an array of Title objects using this Title as a template
3389
	 * Also stores the IDs in the link cache.
3390
	 *
3391
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3392
	 * On heavily-used templates it will max out the memory.
3393
	 *
3394
	 * @param array $options Query option to Database::select()
3395
	 * @return Title[] Array of Title the Title objects linking here
3396
	 */
3397
	public function getTemplateLinksTo( $options = [] ) {
3398
		return $this->getLinksTo( $options, 'templatelinks', 'tl' );
3399
	}
3400
3401
	/**
3402
	 * Get an array of Title objects linked from this Title
3403
	 * Also stores the IDs in the link cache.
3404
	 *
3405
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3406
	 * On heavily-used templates it will max out the memory.
3407
	 *
3408
	 * @param array $options Query option to Database::select()
3409
	 * @param string $table Table name
3410
	 * @param string $prefix Fields prefix
3411
	 * @return array Array of Title objects linking here
3412
	 */
3413
	public function getLinksFrom( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3414
		$id = $this->getArticleID();
3415
3416
		# If the page doesn't exist; there can't be any link from this page
3417
		if ( !$id ) {
3418
			return [];
3419
		}
3420
3421
		$db = wfGetDB( DB_SLAVE );
3422
3423
		$blNamespace = "{$prefix}_namespace";
3424
		$blTitle = "{$prefix}_title";
3425
3426
		$res = $db->select(
3427
			[ $table, 'page' ],
3428
			array_merge(
3429
				[ $blNamespace, $blTitle ],
3430
				WikiPage::selectFields()
3431
			),
3432
			[ "{$prefix}_from" => $id ],
3433
			__METHOD__,
3434
			$options,
3435
			[ 'page' => [
3436
				'LEFT JOIN',
3437
				[ "page_namespace=$blNamespace", "page_title=$blTitle" ]
3438
			] ]
3439
		);
3440
3441
		$retVal = [];
3442
		$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3443
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
3444
			if ( $row->page_id ) {
3445
				$titleObj = Title::newFromRow( $row );
3446
			} else {
3447
				$titleObj = Title::makeTitle( $row->$blNamespace, $row->$blTitle );
3448
				$linkCache->addBadLinkObj( $titleObj );
3449
			}
3450
			$retVal[] = $titleObj;
3451
		}
3452
3453
		return $retVal;
3454
	}
3455
3456
	/**
3457
	 * Get an array of Title objects used on this Title as a template
3458
	 * Also stores the IDs in the link cache.
3459
	 *
3460
	 * WARNING: do not use this function on arbitrary user-supplied titles!
3461
	 * On heavily-used templates it will max out the memory.
3462
	 *
3463
	 * @param array $options May be FOR UPDATE
3464
	 * @return Title[] Array of Title the Title objects used here
3465
	 */
3466
	public function getTemplateLinksFrom( $options = [] ) {
3467
		return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
3468
	}
3469
3470
	/**
3471
	 * Get an array of Title objects referring to non-existent articles linked
3472
	 * from this page.
3473
	 *
3474
	 * @todo check if needed (used only in SpecialBrokenRedirects.php, and
3475
	 *   should use redirect table in this case).
3476
	 * @return Title[] Array of Title the Title objects
3477
	 */
3478
	public function getBrokenLinksFrom() {
3479
		if ( $this->getArticleID() == 0 ) {
3480
			# All links from article ID 0 are false positives
3481
			return [];
3482
		}
3483
3484
		$dbr = wfGetDB( DB_SLAVE );
3485
		$res = $dbr->select(
3486
			[ 'page', 'pagelinks' ],
3487
			[ 'pl_namespace', 'pl_title' ],
3488
			[
3489
				'pl_from' => $this->getArticleID(),
3490
				'page_namespace IS NULL'
3491
			],
3492
			__METHOD__, [],
3493
			[
3494
				'page' => [
3495
					'LEFT JOIN',
3496
					[ 'pl_namespace=page_namespace', 'pl_title=page_title' ]
3497
				]
3498
			]
3499
		);
3500
3501
		$retVal = [];
3502
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
3503
			$retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
3504
		}
3505
		return $retVal;
3506
	}
3507
3508
	/**
3509
	 * Get a list of URLs to purge from the CDN cache when this
3510
	 * page changes
3511
	 *
3512
	 * @return string[] Array of String the URLs
3513
	 */
3514
	public function getCdnUrls() {
3515
		$urls = [
3516
			$this->getInternalURL(),
3517
			$this->getInternalURL( 'action=history' )
3518
		];
3519
3520
		$pageLang = $this->getPageLanguage();
3521
		if ( $pageLang->hasVariants() ) {
3522
			$variants = $pageLang->getVariants();
3523
			foreach ( $variants as $vCode ) {
3524
				$urls[] = $this->getInternalURL( $vCode );
3525
			}
3526
		}
3527
3528
		// If we are looking at a css/js user subpage, purge the action=raw.
3529
		if ( $this->isJsSubpage() ) {
3530
			$urls[] = $this->getInternalURL( 'action=raw&ctype=text/javascript' );
3531
		} elseif ( $this->isCssSubpage() ) {
3532
			$urls[] = $this->getInternalURL( 'action=raw&ctype=text/css' );
3533
		}
3534
3535
		Hooks::run( 'TitleSquidURLs', [ $this, &$urls ] );
3536
		return $urls;
3537
	}
3538
3539
	/**
3540
	 * @deprecated since 1.27 use getCdnUrls()
3541
	 */
3542
	public function getSquidURLs() {
3543
		return $this->getCdnUrls();
3544
	}
3545
3546
	/**
3547
	 * Purge all applicable CDN URLs
3548
	 */
3549
	public function purgeSquid() {
3550
		DeferredUpdates::addUpdate(
3551
			new CdnCacheUpdate( $this->getCdnUrls() ),
3552
			DeferredUpdates::PRESEND
3553
		);
3554
	}
3555
3556
	/**
3557
	 * Move this page without authentication
3558
	 *
3559
	 * @deprecated since 1.25 use MovePage class instead
3560
	 * @param Title $nt The new page Title
3561
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3562
	 */
3563
	public function moveNoAuth( &$nt ) {
3564
		wfDeprecated( __METHOD__, '1.25' );
3565
		return $this->moveTo( $nt, false );
0 ignored issues
show
Deprecated Code introduced by
The method Title::moveTo() has been deprecated with message: since 1.25, use the MovePage class instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3566
	}
3567
3568
	/**
3569
	 * Check whether a given move operation would be valid.
3570
	 * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
3571
	 *
3572
	 * @deprecated since 1.25, use MovePage's methods instead
3573
	 * @param Title $nt The new title
3574
	 * @param bool $auth Whether to check user permissions (uses $wgUser)
3575
	 * @param string $reason Is the log summary of the move, used for spam checking
3576
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3577
	 */
3578
	public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
3579
		global $wgUser;
3580
3581
		if ( !( $nt instanceof Title ) ) {
3582
			// Normally we'd add this to $errors, but we'll get
3583
			// lots of syntax errors if $nt is not an object
3584
			return [ [ 'badtitletext' ] ];
3585
		}
3586
3587
		$mp = new MovePage( $this, $nt );
3588
		$errors = $mp->isValidMove()->getErrorsArray();
0 ignored issues
show
Deprecated Code introduced by
The method Status::getErrorsArray() has been deprecated with message: 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3589
		if ( $auth ) {
3590
			$errors = wfMergeErrorArrays(
3591
				$errors,
3592
				$mp->checkPermissions( $wgUser, $reason )->getErrorsArray()
0 ignored issues
show
Deprecated Code introduced by
The method Status::getErrorsArray() has been deprecated with message: 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3593
			);
3594
		}
3595
3596
		return $errors ?: true;
3597
	}
3598
3599
	/**
3600
	 * Check if the requested move target is a valid file move target
3601
	 * @todo move this to MovePage
3602
	 * @param Title $nt Target title
3603
	 * @return array List of errors
3604
	 */
3605
	protected function validateFileMoveOperation( $nt ) {
3606
		global $wgUser;
3607
3608
		$errors = [];
3609
3610
		$destFile = wfLocalFile( $nt );
3611
		$destFile->load( File::READ_LATEST );
3612
		if ( !$wgUser->isAllowed( 'reupload-shared' )
3613
			&& !$destFile->exists() && wfFindFile( $nt )
3614
		) {
3615
			$errors[] = [ 'file-exists-sharedrepo' ];
3616
		}
3617
3618
		return $errors;
3619
	}
3620
3621
	/**
3622
	 * Move a title to a new location
3623
	 *
3624
	 * @deprecated since 1.25, use the MovePage class instead
3625
	 * @param Title $nt The new title
3626
	 * @param bool $auth Indicates whether $wgUser's permissions
3627
	 *  should be checked
3628
	 * @param string $reason The reason for the move
3629
	 * @param bool $createRedirect Whether to create a redirect from the old title to the new title.
3630
	 *  Ignored if the user doesn't have the suppressredirect right.
3631
	 * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3632
	 */
3633
	public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
3634
		global $wgUser;
3635
		$err = $this->isValidMoveOperation( $nt, $auth, $reason );
0 ignored issues
show
Deprecated Code introduced by
The method Title::isValidMoveOperation() has been deprecated with message: since 1.25, use MovePage's methods instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3636
		if ( is_array( $err ) ) {
3637
			// Auto-block user's IP if the account was "hard" blocked
3638
			$wgUser->spreadAnyEditBlock();
3639
			return $err;
3640
		}
3641
		// Check suppressredirect permission
3642
		if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
3643
			$createRedirect = true;
3644
		}
3645
3646
		$mp = new MovePage( $this, $nt );
3647
		$status = $mp->move( $wgUser, $reason, $createRedirect );
3648
		if ( $status->isOK() ) {
3649
			return true;
3650
		} else {
3651
			return $status->getErrorsArray();
0 ignored issues
show
Deprecated Code introduced by
The method Status::getErrorsArray() has been deprecated with message: 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3652
		}
3653
	}
3654
3655
	/**
3656
	 * Move this page's subpages to be subpages of $nt
3657
	 *
3658
	 * @param Title $nt Move target
3659
	 * @param bool $auth Whether $wgUser's permissions should be checked
3660
	 * @param string $reason The reason for the move
3661
	 * @param bool $createRedirect Whether to create redirects from the old subpages to
3662
	 *     the new ones Ignored if the user doesn't have the 'suppressredirect' right
3663
	 * @return array Array with old page titles as keys, and strings (new page titles) or
3664
	 *     arrays (errors) as values, or an error array with numeric indices if no pages
3665
	 *     were moved
3666
	 */
3667
	public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) {
3668
		global $wgMaximumMovedPages;
3669
		// Check permissions
3670
		if ( !$this->userCan( 'move-subpages' ) ) {
3671
			return [ 'cant-move-subpages' ];
3672
		}
3673
		// Do the source and target namespaces support subpages?
3674
		if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3675
			return [ 'namespace-nosubpages',
3676
				MWNamespace::getCanonicalName( $this->getNamespace() ) ];
3677
		}
3678
		if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
3679
			return [ 'namespace-nosubpages',
3680
				MWNamespace::getCanonicalName( $nt->getNamespace() ) ];
3681
		}
3682
3683
		$subpages = $this->getSubpages( $wgMaximumMovedPages + 1 );
3684
		$retval = [];
3685
		$count = 0;
3686
		foreach ( $subpages as $oldSubpage ) {
0 ignored issues
show
Bug introduced by
The expression $subpages of type array|object<TitleArrayFromResult>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
3687
			$count++;
3688
			if ( $count > $wgMaximumMovedPages ) {
3689
				$retval[$oldSubpage->getPrefixedText()] =
3690
						[ 'movepage-max-pages',
3691
							$wgMaximumMovedPages ];
3692
				break;
3693
			}
3694
3695
			// We don't know whether this function was called before
3696
			// or after moving the root page, so check both
3697
			// $this and $nt
3698
			if ( $oldSubpage->getArticleID() == $this->getArticleID()
3699
				|| $oldSubpage->getArticleID() == $nt->getArticleID()
3700
			) {
3701
				// When moving a page to a subpage of itself,
3702
				// don't move it twice
3703
				continue;
3704
			}
3705
			$newPageName = preg_replace(
3706
					'#^' . preg_quote( $this->getDBkey(), '#' ) . '#',
3707
					StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
3708
					$oldSubpage->getDBkey() );
3709
			if ( $oldSubpage->isTalkPage() ) {
3710
				$newNs = $nt->getTalkPage()->getNamespace();
3711
			} else {
3712
				$newNs = $nt->getSubjectPage()->getNamespace();
3713
			}
3714
			# Bug 14385: we need makeTitleSafe because the new page names may
3715
			# be longer than 255 characters.
3716
			$newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
3717
3718
			$success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect );
3719
			if ( $success === true ) {
3720
				$retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
3721
			} else {
3722
				$retval[$oldSubpage->getPrefixedText()] = $success;
3723
			}
3724
		}
3725
		return $retval;
3726
	}
3727
3728
	/**
3729
	 * Checks if this page is just a one-rev redirect.
3730
	 * Adds lock, so don't use just for light purposes.
3731
	 *
3732
	 * @return bool
3733
	 */
3734
	public function isSingleRevRedirect() {
3735
		global $wgContentHandlerUseDB;
3736
3737
		$dbw = wfGetDB( DB_MASTER );
3738
3739
		# Is it a redirect?
3740
		$fields = [ 'page_is_redirect', 'page_latest', 'page_id' ];
3741
		if ( $wgContentHandlerUseDB ) {
3742
			$fields[] = 'page_content_model';
3743
		}
3744
3745
		$row = $dbw->selectRow( 'page',
3746
			$fields,
3747
			$this->pageCond(),
3748
			__METHOD__,
3749
			[ 'FOR UPDATE' ]
3750
		);
3751
		# Cache some fields we may want
3752
		$this->mArticleID = $row ? intval( $row->page_id ) : 0;
3753
		$this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
0 ignored issues
show
Documentation Bug introduced by
It seems like $row ? (bool) $row->page_is_redirect : false of type boolean is incompatible with the declared type null of property $mRedirect.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
3754
		$this->mLatestID = $row ? intval( $row->page_latest ) : false;
3755
		$this->mContentModel = $row && isset( $row->page_content_model )
3756
			? strval( $row->page_content_model )
3757
			: false;
3758
3759
		if ( !$this->mRedirect ) {
3760
			return false;
3761
		}
3762
		# Does the article have a history?
3763
		$row = $dbw->selectField( [ 'page', 'revision' ],
3764
			'rev_id',
3765
			[ 'page_namespace' => $this->getNamespace(),
3766
				'page_title' => $this->getDBkey(),
3767
				'page_id=rev_page',
3768
				'page_latest != rev_id'
3769
			],
3770
			__METHOD__,
3771
			[ 'FOR UPDATE' ]
3772
		);
3773
		# Return true if there was no history
3774
		return ( $row === false );
3775
	}
3776
3777
	/**
3778
	 * Checks if $this can be moved to a given Title
3779
	 * - Selects for update, so don't call it unless you mean business
3780
	 *
3781
	 * @deprecated since 1.25, use MovePage's methods instead
3782
	 * @param Title $nt The new title to check
3783
	 * @return bool
3784
	 */
3785
	public function isValidMoveTarget( $nt ) {
3786
		# Is it an existing file?
3787
		if ( $nt->getNamespace() == NS_FILE ) {
3788
			$file = wfLocalFile( $nt );
3789
			$file->load( File::READ_LATEST );
3790
			if ( $file->exists() ) {
3791
				wfDebug( __METHOD__ . ": file exists\n" );
3792
				return false;
3793
			}
3794
		}
3795
		# Is it a redirect with no history?
3796
		if ( !$nt->isSingleRevRedirect() ) {
3797
			wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
3798
			return false;
3799
		}
3800
		# Get the article text
3801
		$rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST );
3802
		if ( !is_object( $rev ) ) {
3803
			return false;
3804
		}
3805
		$content = $rev->getContent();
3806
		# Does the redirect point to the source?
3807
		# Or is it a broken self-redirect, usually caused by namespace collisions?
3808
		$redirTitle = $content ? $content->getRedirectTarget() : null;
3809
3810
		if ( $redirTitle ) {
3811
			if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
3812
				$redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
3813
				wfDebug( __METHOD__ . ": redirect points to other page\n" );
3814
				return false;
3815
			} else {
3816
				return true;
3817
			}
3818
		} else {
3819
			# Fail safe (not a redirect after all. strange.)
3820
			wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
3821
						" is a redirect, but it doesn't contain a valid redirect.\n" );
3822
			return false;
3823
		}
3824
	}
3825
3826
	/**
3827
	 * Get categories to which this Title belongs and return an array of
3828
	 * categories' names.
3829
	 *
3830
	 * @return array Array of parents in the form:
3831
	 *	  $parent => $currentarticle
3832
	 */
3833
	public function getParentCategories() {
3834
		global $wgContLang;
3835
3836
		$data = [];
3837
3838
		$titleKey = $this->getArticleID();
3839
3840
		if ( $titleKey === 0 ) {
3841
			return $data;
3842
		}
3843
3844
		$dbr = wfGetDB( DB_SLAVE );
3845
3846
		$res = $dbr->select(
3847
			'categorylinks',
3848
			'cl_to',
3849
			[ 'cl_from' => $titleKey ],
3850
			__METHOD__
3851
		);
3852
3853
		if ( $res->numRows() > 0 ) {
3854
			foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
3855
				// $data[] = Title::newFromText($wgContLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to);
3856
				$data[$wgContLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText();
3857
			}
3858
		}
3859
		return $data;
3860
	}
3861
3862
	/**
3863
	 * Get a tree of parent categories
3864
	 *
3865
	 * @param array $children Array with the children in the keys, to check for circular refs
3866
	 * @return array Tree of parent categories
3867
	 */
3868
	public function getParentCategoryTree( $children = [] ) {
3869
		$stack = [];
3870
		$parents = $this->getParentCategories();
3871
3872
		if ( $parents ) {
3873
			foreach ( $parents as $parent => $current ) {
3874
				if ( array_key_exists( $parent, $children ) ) {
3875
					# Circular reference
3876
					$stack[$parent] = [];
3877
				} else {
3878
					$nt = Title::newFromText( $parent );
3879
					if ( $nt ) {
3880
						$stack[$parent] = $nt->getParentCategoryTree( $children + [ $parent => 1 ] );
3881
					}
3882
				}
3883
			}
3884
		}
3885
3886
		return $stack;
3887
	}
3888
3889
	/**
3890
	 * Get an associative array for selecting this title from
3891
	 * the "page" table
3892
	 *
3893
	 * @return array Array suitable for the $where parameter of DB::select()
3894
	 */
3895
	public function pageCond() {
3896
		if ( $this->mArticleID > 0 ) {
3897
			// PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
3898
			return [ 'page_id' => $this->mArticleID ];
3899
		} else {
3900
			return [ 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ];
3901
		}
3902
	}
3903
3904
	/**
3905
	 * Get the revision ID of the previous revision
3906
	 *
3907
	 * @param int $revId Revision ID. Get the revision that was before this one.
3908
	 * @param int $flags Title::GAID_FOR_UPDATE
3909
	 * @return int|bool Old revision ID, or false if none exists
3910
	 */
3911 View Code Duplication
	public function getPreviousRevisionID( $revId, $flags = 0 ) {
3912
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3913
		$revId = $db->selectField( 'revision', 'rev_id',
3914
			[
3915
				'rev_page' => $this->getArticleID( $flags ),
3916
				'rev_id < ' . intval( $revId )
3917
			],
3918
			__METHOD__,
3919
			[ 'ORDER BY' => 'rev_id DESC' ]
3920
		);
3921
3922
		if ( $revId === false ) {
3923
			return false;
3924
		} else {
3925
			return intval( $revId );
3926
		}
3927
	}
3928
3929
	/**
3930
	 * Get the revision ID of the next revision
3931
	 *
3932
	 * @param int $revId Revision ID. Get the revision that was after this one.
3933
	 * @param int $flags Title::GAID_FOR_UPDATE
3934
	 * @return int|bool Next revision ID, or false if none exists
3935
	 */
3936 View Code Duplication
	public function getNextRevisionID( $revId, $flags = 0 ) {
3937
		$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3938
		$revId = $db->selectField( 'revision', 'rev_id',
3939
			[
3940
				'rev_page' => $this->getArticleID( $flags ),
3941
				'rev_id > ' . intval( $revId )
3942
			],
3943
			__METHOD__,
3944
			[ 'ORDER BY' => 'rev_id' ]
3945
		);
3946
3947
		if ( $revId === false ) {
3948
			return false;
3949
		} else {
3950
			return intval( $revId );
3951
		}
3952
	}
3953
3954
	/**
3955
	 * Get the first revision of the page
3956
	 *
3957
	 * @param int $flags Title::GAID_FOR_UPDATE
3958
	 * @return Revision|null If page doesn't exist
3959
	 */
3960
	public function getFirstRevision( $flags = 0 ) {
3961
		$pageId = $this->getArticleID( $flags );
3962
		if ( $pageId ) {
3963
			$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
3964
			$row = $db->selectRow( 'revision', Revision::selectFields(),
3965
				[ 'rev_page' => $pageId ],
3966
				__METHOD__,
3967
				[ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ]
3968
			);
3969
			if ( $row ) {
3970
				return new Revision( $row );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $db->selectRow('revision...mp ASC', 'LIMIT' => 1)) on line 3964 can also be of type boolean; however, Revision::__construct() does only seem to accept object|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
3971
			}
3972
		}
3973
		return null;
3974
	}
3975
3976
	/**
3977
	 * Get the oldest revision timestamp of this page
3978
	 *
3979
	 * @param int $flags Title::GAID_FOR_UPDATE
3980
	 * @return string MW timestamp
3981
	 */
3982
	public function getEarliestRevTime( $flags = 0 ) {
3983
		$rev = $this->getFirstRevision( $flags );
3984
		return $rev ? $rev->getTimestamp() : null;
3985
	}
3986
3987
	/**
3988
	 * Check if this is a new page
3989
	 *
3990
	 * @return bool
3991
	 */
3992
	public function isNewPage() {
3993
		$dbr = wfGetDB( DB_SLAVE );
3994
		return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
3995
	}
3996
3997
	/**
3998
	 * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit
3999
	 *
4000
	 * @return bool
4001
	 */
4002
	public function isBigDeletion() {
4003
		global $wgDeleteRevisionsLimit;
4004
4005
		if ( !$wgDeleteRevisionsLimit ) {
4006
			return false;
4007
		}
4008
4009
		if ( $this->mIsBigDeletion === null ) {
4010
			$dbr = wfGetDB( DB_SLAVE );
4011
4012
			$revCount = $dbr->selectRowCount(
4013
				'revision',
4014
				'1',
4015
				[ 'rev_page' => $this->getArticleID() ],
4016
				__METHOD__,
4017
				[ 'LIMIT' => $wgDeleteRevisionsLimit + 1 ]
4018
			);
4019
4020
			$this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit;
4021
		}
4022
4023
		return $this->mIsBigDeletion;
4024
	}
4025
4026
	/**
4027
	 * Get the approximate revision count of this page.
4028
	 *
4029
	 * @return int
4030
	 */
4031
	public function estimateRevisionCount() {
4032
		if ( !$this->exists() ) {
4033
			return 0;
4034
		}
4035
4036
		if ( $this->mEstimateRevisions === null ) {
4037
			$dbr = wfGetDB( DB_SLAVE );
4038
			$this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
4039
				[ 'rev_page' => $this->getArticleID() ], __METHOD__ );
4040
		}
4041
4042
		return $this->mEstimateRevisions;
4043
	}
4044
4045
	/**
4046
	 * Get the number of revisions between the given revision.
4047
	 * Used for diffs and other things that really need it.
4048
	 *
4049
	 * @param int|Revision $old Old revision or rev ID (first before range)
4050
	 * @param int|Revision $new New revision or rev ID (first after range)
4051
	 * @param int|null $max Limit of Revisions to count, will be incremented to detect truncations
4052
	 * @return int Number of revisions between these revisions.
4053
	 */
4054
	public function countRevisionsBetween( $old, $new, $max = null ) {
4055
		if ( !( $old instanceof Revision ) ) {
4056
			$old = Revision::newFromTitle( $this, (int)$old );
4057
		}
4058
		if ( !( $new instanceof Revision ) ) {
4059
			$new = Revision::newFromTitle( $this, (int)$new );
4060
		}
4061
		if ( !$old || !$new ) {
4062
			return 0; // nothing to compare
4063
		}
4064
		$dbr = wfGetDB( DB_SLAVE );
4065
		$conds = [
4066
			'rev_page' => $this->getArticleID(),
4067
			'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
0 ignored issues
show
Security Bug introduced by
It seems like $dbr->timestamp($old->getTimestamp()) targeting DatabaseBase::timestamp() can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?
Loading history...
4068
			'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
0 ignored issues
show
Security Bug introduced by
It seems like $dbr->timestamp($new->getTimestamp()) targeting DatabaseBase::timestamp() can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?
Loading history...
4069
		];
4070
		if ( $max !== null ) {
4071
			return $dbr->selectRowCount( 'revision', '1',
4072
				$conds,
4073
				__METHOD__,
4074
				[ 'LIMIT' => $max + 1 ] // extra to detect truncation
4075
			);
4076
		} else {
4077
			return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
4078
		}
4079
	}
4080
4081
	/**
4082
	 * Get the authors between the given revisions or revision IDs.
4083
	 * Used for diffs and other things that really need it.
4084
	 *
4085
	 * @since 1.23
4086
	 *
4087
	 * @param int|Revision $old Old revision or rev ID (first before range by default)
4088
	 * @param int|Revision $new New revision or rev ID (first after range by default)
4089
	 * @param int $limit Maximum number of authors
4090
	 * @param string|array $options (Optional): Single option, or an array of options:
4091
	 *     'include_old' Include $old in the range; $new is excluded.
4092
	 *     'include_new' Include $new in the range; $old is excluded.
4093
	 *     'include_both' Include both $old and $new in the range.
4094
	 *     Unknown option values are ignored.
4095
	 * @return array|null Names of revision authors in the range; null if not both revisions exist
4096
	 */
4097
	public function getAuthorsBetween( $old, $new, $limit, $options = [] ) {
4098
		if ( !( $old instanceof Revision ) ) {
4099
			$old = Revision::newFromTitle( $this, (int)$old );
4100
		}
4101
		if ( !( $new instanceof Revision ) ) {
4102
			$new = Revision::newFromTitle( $this, (int)$new );
4103
		}
4104
		// XXX: what if Revision objects are passed in, but they don't refer to this title?
4105
		// Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID()
4106
		// in the sanity check below?
4107
		if ( !$old || !$new ) {
4108
			return null; // nothing to compare
4109
		}
4110
		$authors = [];
4111
		$old_cmp = '>';
4112
		$new_cmp = '<';
4113
		$options = (array)$options;
4114
		if ( in_array( 'include_old', $options ) ) {
4115
			$old_cmp = '>=';
4116
		}
4117
		if ( in_array( 'include_new', $options ) ) {
4118
			$new_cmp = '<=';
4119
		}
4120
		if ( in_array( 'include_both', $options ) ) {
4121
			$old_cmp = '>=';
4122
			$new_cmp = '<=';
4123
		}
4124
		// No DB query needed if $old and $new are the same or successive revisions:
4125
		if ( $old->getId() === $new->getId() ) {
4126
			return ( $old_cmp === '>' && $new_cmp === '<' ) ?
4127
				[] :
4128
				[ $old->getUserText( Revision::RAW ) ];
4129
		} elseif ( $old->getId() === $new->getParentId() ) {
4130
			if ( $old_cmp === '>=' && $new_cmp === '<=' ) {
4131
				$authors[] = $old->getUserText( Revision::RAW );
4132
				if ( $old->getUserText( Revision::RAW ) != $new->getUserText( Revision::RAW ) ) {
4133
					$authors[] = $new->getUserText( Revision::RAW );
4134
				}
4135
			} elseif ( $old_cmp === '>=' ) {
4136
				$authors[] = $old->getUserText( Revision::RAW );
4137
			} elseif ( $new_cmp === '<=' ) {
4138
				$authors[] = $new->getUserText( Revision::RAW );
4139
			}
4140
			return $authors;
4141
		}
4142
		$dbr = wfGetDB( DB_SLAVE );
4143
		$res = $dbr->select( 'revision', 'DISTINCT rev_user_text',
4144
			[
4145
				'rev_page' => $this->getArticleID(),
4146
				"rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
0 ignored issues
show
Security Bug introduced by
It seems like $dbr->timestamp($old->getTimestamp()) targeting DatabaseBase::timestamp() can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?
Loading history...
4147
				"rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
0 ignored issues
show
Security Bug introduced by
It seems like $dbr->timestamp($new->getTimestamp()) targeting DatabaseBase::timestamp() can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?
Loading history...
4148
			], __METHOD__,
4149
			[ 'LIMIT' => $limit + 1 ] // add one so caller knows it was truncated
4150
		);
4151
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
4152
			$authors[] = $row->rev_user_text;
4153
		}
4154
		return $authors;
4155
	}
4156
4157
	/**
4158
	 * Get the number of authors between the given revisions or revision IDs.
4159
	 * Used for diffs and other things that really need it.
4160
	 *
4161
	 * @param int|Revision $old Old revision or rev ID (first before range by default)
4162
	 * @param int|Revision $new New revision or rev ID (first after range by default)
4163
	 * @param int $limit Maximum number of authors
4164
	 * @param string|array $options (Optional): Single option, or an array of options:
4165
	 *     'include_old' Include $old in the range; $new is excluded.
4166
	 *     'include_new' Include $new in the range; $old is excluded.
4167
	 *     'include_both' Include both $old and $new in the range.
4168
	 *     Unknown option values are ignored.
4169
	 * @return int Number of revision authors in the range; zero if not both revisions exist
4170
	 */
4171
	public function countAuthorsBetween( $old, $new, $limit, $options = [] ) {
4172
		$authors = $this->getAuthorsBetween( $old, $new, $limit, $options );
4173
		return $authors ? count( $authors ) : 0;
4174
	}
4175
4176
	/**
4177
	 * Compare with another title.
4178
	 *
4179
	 * @param Title $title
4180
	 * @return bool
4181
	 */
4182
	public function equals( Title $title ) {
4183
		// Note: === is necessary for proper matching of number-like titles.
4184
		return $this->getInterwiki() === $title->getInterwiki()
4185
			&& $this->getNamespace() == $title->getNamespace()
4186
			&& $this->getDBkey() === $title->getDBkey();
4187
	}
4188
4189
	/**
4190
	 * Check if this title is a subpage of another title
4191
	 *
4192
	 * @param Title $title
4193
	 * @return bool
4194
	 */
4195
	public function isSubpageOf( Title $title ) {
4196
		return $this->getInterwiki() === $title->getInterwiki()
4197
			&& $this->getNamespace() == $title->getNamespace()
4198
			&& strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0;
4199
	}
4200
4201
	/**
4202
	 * Check if page exists.  For historical reasons, this function simply
4203
	 * checks for the existence of the title in the page table, and will
4204
	 * thus return false for interwiki links, special pages and the like.
4205
	 * If you want to know if a title can be meaningfully viewed, you should
4206
	 * probably call the isKnown() method instead.
4207
	 *
4208
	 * @param int $flags An optional bit field; may be Title::GAID_FOR_UPDATE to check
4209
	 *   from master/for update
4210
	 * @return bool
4211
	 */
4212
	public function exists( $flags = 0 ) {
4213
		$exists = $this->getArticleID( $flags ) != 0;
4214
		Hooks::run( 'TitleExists', [ $this, &$exists ] );
4215
		return $exists;
4216
	}
4217
4218
	/**
4219
	 * Should links to this title be shown as potentially viewable (i.e. as
4220
	 * "bluelinks"), even if there's no record by this title in the page
4221
	 * table?
4222
	 *
4223
	 * This function is semi-deprecated for public use, as well as somewhat
4224
	 * misleadingly named.  You probably just want to call isKnown(), which
4225
	 * calls this function internally.
4226
	 *
4227
	 * (ISSUE: Most of these checks are cheap, but the file existence check
4228
	 * can potentially be quite expensive.  Including it here fixes a lot of
4229
	 * existing code, but we might want to add an optional parameter to skip
4230
	 * it and any other expensive checks.)
4231
	 *
4232
	 * @return bool
4233
	 */
4234
	public function isAlwaysKnown() {
4235
		$isKnown = null;
4236
4237
		/**
4238
		 * Allows overriding default behavior for determining if a page exists.
4239
		 * If $isKnown is kept as null, regular checks happen. If it's
4240
		 * a boolean, this value is returned by the isKnown method.
4241
		 *
4242
		 * @since 1.20
4243
		 *
4244
		 * @param Title $title
4245
		 * @param bool|null $isKnown
4246
		 */
4247
		Hooks::run( 'TitleIsAlwaysKnown', [ $this, &$isKnown ] );
4248
4249
		if ( !is_null( $isKnown ) ) {
4250
			return $isKnown;
4251
		}
4252
4253
		if ( $this->isExternal() ) {
4254
			return true;  // any interwiki link might be viewable, for all we know
4255
		}
4256
4257
		switch ( $this->mNamespace ) {
4258
			case NS_MEDIA:
4259
			case NS_FILE:
4260
				// file exists, possibly in a foreign repo
4261
				return (bool)wfFindFile( $this );
4262
			case NS_SPECIAL:
4263
				// valid special page
4264
				return SpecialPageFactory::exists( $this->getDBkey() );
4265
			case NS_MAIN:
4266
				// selflink, possibly with fragment
4267
				return $this->mDbkeyform == '';
4268
			case NS_MEDIAWIKI:
4269
				// known system message
4270
				return $this->hasSourceText() !== false;
4271
			default:
4272
				return false;
4273
		}
4274
	}
4275
4276
	/**
4277
	 * Does this title refer to a page that can (or might) be meaningfully
4278
	 * viewed?  In particular, this function may be used to determine if
4279
	 * links to the title should be rendered as "bluelinks" (as opposed to
4280
	 * "redlinks" to non-existent pages).
4281
	 * Adding something else to this function will cause inconsistency
4282
	 * since LinkHolderArray calls isAlwaysKnown() and does its own
4283
	 * page existence check.
4284
	 *
4285
	 * @return bool
4286
	 */
4287
	public function isKnown() {
4288
		return $this->isAlwaysKnown() || $this->exists();
4289
	}
4290
4291
	/**
4292
	 * Does this page have source text?
4293
	 *
4294
	 * @return bool
4295
	 */
4296
	public function hasSourceText() {
4297
		if ( $this->exists() ) {
4298
			return true;
4299
		}
4300
4301
		if ( $this->mNamespace == NS_MEDIAWIKI ) {
4302
			// If the page doesn't exist but is a known system message, default
4303
			// message content will be displayed, same for language subpages-
4304
			// Use always content language to avoid loading hundreds of languages
4305
			// to get the link color.
4306
			global $wgContLang;
4307
			list( $name, ) = MessageCache::singleton()->figureMessage(
4308
				$wgContLang->lcfirst( $this->getText() )
4309
			);
4310
			$message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false );
4311
			return $message->exists();
4312
		}
4313
4314
		return false;
4315
	}
4316
4317
	/**
4318
	 * Get the default message text or false if the message doesn't exist
4319
	 *
4320
	 * @return string|bool
4321
	 */
4322
	public function getDefaultMessageText() {
4323
		global $wgContLang;
4324
4325
		if ( $this->getNamespace() != NS_MEDIAWIKI ) { // Just in case
4326
			return false;
4327
		}
4328
4329
		list( $name, $lang ) = MessageCache::singleton()->figureMessage(
4330
			$wgContLang->lcfirst( $this->getText() )
4331
		);
4332
		$message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false );
4333
4334
		if ( $message->exists() ) {
4335
			return $message->plain();
4336
		} else {
4337
			return false;
4338
		}
4339
	}
4340
4341
	/**
4342
	 * Updates page_touched for this page; called from LinksUpdate.php
4343
	 *
4344
	 * @param string $purgeTime [optional] TS_MW timestamp
4345
	 * @return bool True if the update succeeded
4346
	 */
4347
	public function invalidateCache( $purgeTime = null ) {
4348
		if ( wfReadOnly() ) {
4349
			return false;
4350
		}
4351
4352
		if ( $this->mArticleID === 0 ) {
4353
			return true; // avoid gap locking if we know it's not there
4354
		}
4355
4356
		$method = __METHOD__;
4357
		$dbw = wfGetDB( DB_MASTER );
4358
		$conds = $this->pageCond();
4359
		$dbw->onTransactionIdle( function () use ( $dbw, $conds, $method, $purgeTime ) {
4360
			$dbTimestamp = $dbw->timestamp( $purgeTime ?: time() );
4361
4362
			$dbw->update(
4363
				'page',
4364
				[ 'page_touched' => $dbTimestamp ],
4365
				$conds + [ 'page_touched < ' . $dbw->addQuotes( $dbTimestamp ) ],
0 ignored issues
show
Security Bug introduced by
It seems like $dbTimestamp defined by $dbw->timestamp($purgeTime ?: time()) on line 4360 can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
4366
				$method
4367
			);
4368
		} );
4369
4370
		return true;
4371
	}
4372
4373
	/**
4374
	 * Update page_touched timestamps and send CDN purge messages for
4375
	 * pages linking to this title. May be sent to the job queue depending
4376
	 * on the number of links. Typically called on create and delete.
4377
	 */
4378
	public function touchLinks() {
4379
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks' ) );
4380
		if ( $this->getNamespace() == NS_CATEGORY ) {
4381
			DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'categorylinks' ) );
4382
		}
4383
	}
4384
4385
	/**
4386
	 * Get the last touched timestamp
4387
	 *
4388
	 * @param IDatabase $db Optional db
4389
	 * @return string Last-touched timestamp
4390
	 */
4391
	public function getTouched( $db = null ) {
4392
		if ( $db === null ) {
4393
			$db = wfGetDB( DB_SLAVE );
4394
		}
4395
		$touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
4396
		return $touched;
4397
	}
4398
4399
	/**
4400
	 * Get the timestamp when this page was updated since the user last saw it.
4401
	 *
4402
	 * @param User $user
4403
	 * @return string|null
4404
	 */
4405
	public function getNotificationTimestamp( $user = null ) {
4406
		global $wgUser;
4407
4408
		// Assume current user if none given
4409
		if ( !$user ) {
4410
			$user = $wgUser;
4411
		}
4412
		// Check cache first
4413
		$uid = $user->getId();
4414
		if ( !$uid ) {
4415
			return false;
4416
		}
4417
		// avoid isset here, as it'll return false for null entries
4418
		if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
4419
			return $this->mNotificationTimestamp[$uid];
4420
		}
4421
		// Don't cache too much!
4422
		if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
4423
			$this->mNotificationTimestamp = [];
4424
		}
4425
4426
		$store = MediaWikiServices::getInstance()->getWatchedItemStore();
4427
		$watchedItem = $store->getWatchedItem( $user, $this );
4428
		if ( $watchedItem ) {
4429
			$this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp();
4430
		} else {
4431
			$this->mNotificationTimestamp[$uid] = false;
4432
		}
4433
4434
		return $this->mNotificationTimestamp[$uid];
4435
	}
4436
4437
	/**
4438
	 * Generate strings used for xml 'id' names in monobook tabs
4439
	 *
4440
	 * @param string $prepend Defaults to 'nstab-'
4441
	 * @return string XML 'id' name
4442
	 */
4443
	public function getNamespaceKey( $prepend = 'nstab-' ) {
4444
		global $wgContLang;
4445
		// Gets the subject namespace if this title
4446
		$namespace = MWNamespace::getSubject( $this->getNamespace() );
4447
		// Checks if canonical namespace name exists for namespace
4448
		if ( MWNamespace::exists( $this->getNamespace() ) ) {
4449
			// Uses canonical namespace name
4450
			$namespaceKey = MWNamespace::getCanonicalName( $namespace );
4451
		} else {
4452
			// Uses text of namespace
4453
			$namespaceKey = $this->getSubjectNsText();
4454
		}
4455
		// Makes namespace key lowercase
4456
		$namespaceKey = $wgContLang->lc( $namespaceKey );
4457
		// Uses main
4458
		if ( $namespaceKey == '' ) {
4459
			$namespaceKey = 'main';
4460
		}
4461
		// Changes file to image for backwards compatibility
4462
		if ( $namespaceKey == 'file' ) {
4463
			$namespaceKey = 'image';
4464
		}
4465
		return $prepend . $namespaceKey;
4466
	}
4467
4468
	/**
4469
	 * Get all extant redirects to this Title
4470
	 *
4471
	 * @param int|null $ns Single namespace to consider; null to consider all namespaces
4472
	 * @return Title[] Array of Title redirects to this title
4473
	 */
4474
	public function getRedirectsHere( $ns = null ) {
4475
		$redirs = [];
4476
4477
		$dbr = wfGetDB( DB_SLAVE );
4478
		$where = [
4479
			'rd_namespace' => $this->getNamespace(),
4480
			'rd_title' => $this->getDBkey(),
4481
			'rd_from = page_id'
4482
		];
4483
		if ( $this->isExternal() ) {
4484
			$where['rd_interwiki'] = $this->getInterwiki();
4485
		} else {
4486
			$where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL';
4487
		}
4488
		if ( !is_null( $ns ) ) {
4489
			$where['page_namespace'] = $ns;
4490
		}
4491
4492
		$res = $dbr->select(
4493
			[ 'redirect', 'page' ],
4494
			[ 'page_namespace', 'page_title' ],
4495
			$where,
4496
			__METHOD__
4497
		);
4498
4499
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
4500
			$redirs[] = self::newFromRow( $row );
4501
		}
4502
		return $redirs;
4503
	}
4504
4505
	/**
4506
	 * Check if this Title is a valid redirect target
4507
	 *
4508
	 * @return bool
4509
	 */
4510
	public function isValidRedirectTarget() {
4511
		global $wgInvalidRedirectTargets;
4512
4513
		if ( $this->isSpecialPage() ) {
4514
			// invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
4515
			if ( $this->isSpecial( 'Userlogout' ) ) {
4516
				return false;
4517
			}
4518
4519
			foreach ( $wgInvalidRedirectTargets as $target ) {
4520
				if ( $this->isSpecial( $target ) ) {
4521
					return false;
4522
				}
4523
			}
4524
		}
4525
4526
		return true;
4527
	}
4528
4529
	/**
4530
	 * Get a backlink cache object
4531
	 *
4532
	 * @return BacklinkCache
4533
	 */
4534
	public function getBacklinkCache() {
4535
		return BacklinkCache::get( $this );
4536
	}
4537
4538
	/**
4539
	 * Whether the magic words __INDEX__ and __NOINDEX__ function for  this page.
4540
	 *
4541
	 * @return bool
4542
	 */
4543
	public function canUseNoindex() {
4544
		global $wgContentNamespaces, $wgExemptFromUserRobotsControl;
4545
4546
		$bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
4547
			? $wgContentNamespaces
4548
			: $wgExemptFromUserRobotsControl;
4549
4550
		return !in_array( $this->mNamespace, $bannedNamespaces );
4551
4552
	}
4553
4554
	/**
4555
	 * Returns the raw sort key to be used for categories, with the specified
4556
	 * prefix.  This will be fed to Collation::getSortKey() to get a
4557
	 * binary sortkey that can be used for actual sorting.
4558
	 *
4559
	 * @param string $prefix The prefix to be used, specified using
4560
	 *   {{defaultsort:}} or like [[Category:Foo|prefix]].  Empty for no
4561
	 *   prefix.
4562
	 * @return string
4563
	 */
4564
	public function getCategorySortkey( $prefix = '' ) {
4565
		$unprefixed = $this->getText();
4566
4567
		// Anything that uses this hook should only depend
4568
		// on the Title object passed in, and should probably
4569
		// tell the users to run updateCollations.php --force
4570
		// in order to re-sort existing category relations.
4571
		Hooks::run( 'GetDefaultSortkey', [ $this, &$unprefixed ] );
4572
		if ( $prefix !== '' ) {
4573
			# Separate with a line feed, so the unprefixed part is only used as
4574
			# a tiebreaker when two pages have the exact same prefix.
4575
			# In UCA, tab is the only character that can sort above LF
4576
			# so we strip both of them from the original prefix.
4577
			$prefix = strtr( $prefix, "\n\t", '  ' );
4578
			return "$prefix\n$unprefixed";
4579
		}
4580
		return $unprefixed;
4581
	}
4582
4583
	/**
4584
	 * Returns the page language code saved in the database, if $wgPageLanguageUseDB is set
4585
	 * to true in LocalSettings.php, otherwise returns false. If there is no language saved in
4586
	 * the db, it will return NULL.
4587
	 *
4588
	 * @return string|null|bool
4589
	 */
4590
	private function getDbPageLanguageCode() {
4591
		global $wgPageLanguageUseDB;
4592
4593
		// check, if the page language could be saved in the database, and if so and
4594
		// the value is not requested already, lookup the page language using LinkCache
4595 View Code Duplication
		if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) {
4596
			$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
4597
			$linkCache->addLinkObj( $this );
4598
			$this->mDbPageLanguage = $linkCache->getGoodLinkFieldObj( $this, 'lang' );
0 ignored issues
show
Documentation Bug introduced by
It seems like $linkCache->getGoodLinkFieldObj($this, 'lang') can also be of type integer. However, the property $mDbPageLanguage is declared as type string|boolean|null. 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...
4599
		}
4600
4601
		return $this->mDbPageLanguage;
4602
	}
4603
4604
	/**
4605
	 * Get the language in which the content of this page is written in
4606
	 * wikitext. Defaults to $wgContLang, but in certain cases it can be
4607
	 * e.g. $wgLang (such as special pages, which are in the user language).
4608
	 *
4609
	 * @since 1.18
4610
	 * @return Language
4611
	 */
4612
	public function getPageLanguage() {
4613
		global $wgLang, $wgLanguageCode;
4614
		if ( $this->isSpecialPage() ) {
4615
			// special pages are in the user language
4616
			return $wgLang;
4617
		}
4618
4619
		// Checking if DB language is set
4620
		$dbPageLanguage = $this->getDbPageLanguageCode();
4621
		if ( $dbPageLanguage ) {
4622
			return wfGetLangObj( $dbPageLanguage );
4623
		}
4624
4625
		if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) {
4626
			// Note that this may depend on user settings, so the cache should
4627
			// be only per-request.
4628
			// NOTE: ContentHandler::getPageLanguage() may need to load the
4629
			// content to determine the page language!
4630
			// Checking $wgLanguageCode hasn't changed for the benefit of unit
4631
			// tests.
4632
			$contentHandler = ContentHandler::getForTitle( $this );
4633
			$langObj = $contentHandler->getPageLanguage( $this );
4634
			$this->mPageLanguage = [ $langObj->getCode(), $wgLanguageCode ];
0 ignored issues
show
Documentation Bug introduced by
It seems like array($langObj->getCode(), $wgLanguageCode) of type array<integer,string,{"0":"string"}> is incompatible with the declared type boolean of property $mPageLanguage.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
4635
		} else {
4636
			$langObj = wfGetLangObj( $this->mPageLanguage[0] );
4637
		}
4638
4639
		return $langObj;
4640
	}
4641
4642
	/**
4643
	 * Get the language in which the content of this page is written when
4644
	 * viewed by user. Defaults to $wgContLang, but in certain cases it can be
4645
	 * e.g. $wgLang (such as special pages, which are in the user language).
4646
	 *
4647
	 * @since 1.20
4648
	 * @return Language
4649
	 */
4650
	public function getPageViewLanguage() {
4651
		global $wgLang;
4652
4653
		if ( $this->isSpecialPage() ) {
4654
			// If the user chooses a variant, the content is actually
4655
			// in a language whose code is the variant code.
4656
			$variant = $wgLang->getPreferredVariant();
4657
			if ( $wgLang->getCode() !== $variant ) {
4658
				return Language::factory( $variant );
4659
			}
4660
4661
			return $wgLang;
4662
		}
4663
4664
		// Checking if DB language is set
4665
		$dbPageLanguage = $this->getDbPageLanguageCode();
4666
		if ( $dbPageLanguage ) {
4667
			$pageLang = wfGetLangObj( $dbPageLanguage );
4668
			$variant = $pageLang->getPreferredVariant();
4669
			if ( $pageLang->getCode() !== $variant ) {
4670
				$pageLang = Language::factory( $variant );
4671
			}
4672
4673
			return $pageLang;
4674
		}
4675
4676
		// @note Can't be cached persistently, depends on user settings.
4677
		// @note ContentHandler::getPageViewLanguage() may need to load the
4678
		//   content to determine the page language!
4679
		$contentHandler = ContentHandler::getForTitle( $this );
4680
		$pageLang = $contentHandler->getPageViewLanguage( $this );
4681
		return $pageLang;
4682
	}
4683
4684
	/**
4685
	 * Get a list of rendered edit notices for this page.
4686
	 *
4687
	 * Array is keyed by the original message key, and values are rendered using parseAsBlock, so
4688
	 * they will already be wrapped in paragraphs.
4689
	 *
4690
	 * @since 1.21
4691
	 * @param int $oldid Revision ID that's being edited
4692
	 * @return array
4693
	 */
4694
	public function getEditNotices( $oldid = 0 ) {
4695
		$notices = [];
4696
4697
		// Optional notice for the entire namespace
4698
		$editnotice_ns = 'editnotice-' . $this->getNamespace();
4699
		$msg = wfMessage( $editnotice_ns );
4700 View Code Duplication
		if ( $msg->exists() ) {
4701
			$html = $msg->parseAsBlock();
4702
			// Edit notices may have complex logic, but output nothing (T91715)
4703
			if ( trim( $html ) !== '' ) {
4704
				$notices[$editnotice_ns] = Html::rawElement(
4705
					'div',
4706
					[ 'class' => [
4707
						'mw-editnotice',
4708
						'mw-editnotice-namespace',
4709
						Sanitizer::escapeClass( "mw-$editnotice_ns" )
4710
					] ],
4711
					$html
4712
				);
4713
			}
4714
		}
4715
4716
		if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) {
4717
			// Optional notice for page itself and any parent page
4718
			$parts = explode( '/', $this->getDBkey() );
4719
			$editnotice_base = $editnotice_ns;
4720
			while ( count( $parts ) > 0 ) {
4721
				$editnotice_base .= '-' . array_shift( $parts );
4722
				$msg = wfMessage( $editnotice_base );
4723 View Code Duplication
				if ( $msg->exists() ) {
4724
					$html = $msg->parseAsBlock();
4725
					if ( trim( $html ) !== '' ) {
4726
						$notices[$editnotice_base] = Html::rawElement(
4727
							'div',
4728
							[ 'class' => [
4729
								'mw-editnotice',
4730
								'mw-editnotice-base',
4731
								Sanitizer::escapeClass( "mw-$editnotice_base" )
4732
							] ],
4733
							$html
4734
						);
4735
					}
4736
				}
4737
			}
4738
		} else {
4739
			// Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys
4740
			$editnoticeText = $editnotice_ns . '-' . strtr( $this->getDBkey(), '/', '-' );
4741
			$msg = wfMessage( $editnoticeText );
4742 View Code Duplication
			if ( $msg->exists() ) {
4743
				$html = $msg->parseAsBlock();
4744
				if ( trim( $html ) !== '' ) {
4745
					$notices[$editnoticeText] = Html::rawElement(
4746
						'div',
4747
						[ 'class' => [
4748
							'mw-editnotice',
4749
							'mw-editnotice-page',
4750
							Sanitizer::escapeClass( "mw-$editnoticeText" )
4751
						] ],
4752
						$html
4753
					);
4754
				}
4755
			}
4756
		}
4757
4758
		Hooks::run( 'TitleGetEditNotices', [ $this, $oldid, &$notices ] );
4759
		return $notices;
4760
	}
4761
4762
	/**
4763
	 * @return array
4764
	 */
4765
	public function __sleep() {
4766
		return [
4767
			'mNamespace',
4768
			'mDbkeyform',
4769
			'mFragment',
4770
			'mInterwiki',
4771
			'mLocalInterwiki',
4772
			'mUserCaseDBKey',
4773
			'mDefaultNamespace',
4774
		];
4775
	}
4776
4777
	public function __wakeup() {
4778
		$this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0;
4779
		$this->mUrlform = wfUrlencode( $this->mDbkeyform );
4780
		$this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
4781
	}
4782
4783
}
4784