Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
57.14% |
688 / 1204 |
|
52.98% |
89 / 168 |
CRAP | |
0.00% |
0 / 1 |
Title | |
57.19% |
688 / 1203 |
|
52.98% |
89 / 168 |
17726.21 | |
0.00% |
0 / 1 |
getLanguageConverter | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getPageLanguageConverter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDbProvider | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTitleFormatter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getInterwikiLookup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newFromDBkey | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
newFromLinkTarget | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
castFromLinkTarget | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
newFromPageIdentity | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
castFromPageIdentity | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newFromPageReference | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
castFromPageReference | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
newFromText | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
56 | |||
newFromTextThrow | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
uncache | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
newFromURL | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getTitleCache | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
newFromID | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
newFromRow | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
loadFromRow | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
72 | |||
makeTitle | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
makeTitleSafe | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
newMainPage | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
legalChars | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
convertByteClassToUnicodeClass | |
91.23% |
52 / 57 |
|
0.00% |
0 / 1 |
20.27 | |||
makeName | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
6 | |||
compare | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
isValid | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
8 | |||
isLocal | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getInterwiki | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
wasLocalInterwiki | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isTrans | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTransWikiID | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTitleValue | |
72.73% |
8 / 11 |
|
0.00% |
0 / 1 |
3.18 | |||
getText | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPartialURL | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDBkey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNamespace | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
shouldReadLatest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getContentModel | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
6.03 | |||
hasContentModel | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setContentModel | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
lazyFillContentModel | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
12 | |||
getNsText | |
66.67% |
8 / 12 |
|
0.00% |
0 / 1 |
5.93 | |||
getSubjectNsText | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getTalkNsText | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
canHaveTalkPage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
canExist | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
6.22 | |||
isSpecialPage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isSpecial | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
fixSpecialName | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
inNamespace | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
inNamespaces | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
hasSubjectNamespace | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isContentPage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
isMovable | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
isMainPage | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
isSubpage | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
isConversionTable | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
isWikitextPage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isSiteConfigPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
isUserConfigPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getSkinFromConfigSubpage | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
isUserCssConfigPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
isUserJsonConfigPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
isUserJsConfigPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
isSiteCssConfigPage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
isSiteJsonConfigPage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
isSiteJsConfigPage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
isRawHtmlMessage | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
isTalkPage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getTalkPage | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getTalkPageIfDefined | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getSubjectPage | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
warnIfPageCannotExist | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
getOtherPage | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
getFragment | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFragmentForURL | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
5 | |||
setFragment | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
createFragmentTarget | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
normalizeFragment | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
prefix | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
4.13 | |||
getPrefixedDBkey | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getPrefixedText | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
__toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFullText | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
findSubpageDivider | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
hasSubpagesEnabled | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getRootText | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
getRootTitle | |
50.00% |
5 / 10 |
|
0.00% |
0 / 1 |
4.12 | |||
getBaseText | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
getBaseTitle | |
50.00% |
5 / 10 |
|
0.00% |
0 / 1 |
4.12 | |||
getSubpageText | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getSubpage | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getSubpageUrlForm | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getPrefixedURL | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getFullURL | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getFullUrlForRedirect | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getLocalURL | |
95.45% |
42 / 44 |
|
0.00% |
0 / 1 |
20 | |||
getLinkURL | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
getInternalURL | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getCanonicalURL | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
getEditURL | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getTitleProtection | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
deleteTitleProtection | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
loadRestrictions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
flushRestrictions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
purgeExpiredRestrictions | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
12 | |||
hasSubpages | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
getSubpages | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
isDeleted | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDeletedEditsCount | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
isDeletedQuick | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
hasDeletedEdits | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
getArticleID | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
isRedirect | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
getLength | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
getLatestRevID | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
resetArticleID | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
clearCaches | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
capitalize | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
secureAndSplit | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 | |||
getLinksTo | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
42 | |||
getTemplateLinksTo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLinksFrom | |
82.50% |
33 / 40 |
|
0.00% |
0 / 1 |
5.13 | |||
getTemplateLinksFrom | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getBrokenLinksFrom | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
getCdnUrls | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
purgeSquid | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
isSingleRevRedirect | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
getParentCategories | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
4.00 | |||
getParentCategoryTree | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
pageCond | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isNewPage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isBigDeletion | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
estimateRevisionCount | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
equals | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
isSamePageAs | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
isSubpageOf | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
exists | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
isAlwaysKnown | |
83.33% |
15 / 18 |
|
0.00% |
0 / 1 |
9.37 | |||
isKnown | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
hasSourceText | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getDefaultMessageText | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getDefaultSystemMessage | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 | |||
invalidateCache | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
30 | |||
touchLinks | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
getTouched | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getNamespaceKey | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
getRedirectsHere | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
isValidRedirectTarget | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
canUseNoindex | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getCategorySortkey | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getDbPageLanguageCode | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
30 | |||
getDbPageLanguage | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getPageLanguage | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
getPageViewLanguage | |
63.16% |
12 / 19 |
|
0.00% |
0 / 1 |
6.25 | |||
getEditNotices | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
42 | |||
getFieldFromPageStore | |
83.33% |
10 / 12 |
|
0.00% |
0 / 1 |
6.17 | |||
__sleep | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
__wakeup | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
__clone | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getWikiId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getId | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
assertProperPage | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
toPageIdentity | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
toPageRecord | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * Representation of a title within MediaWiki. |
4 | * |
5 | * See Title.md |
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 | |
25 | namespace MediaWiki\Title; |
26 | |
27 | use DBAccessObjectUtils; |
28 | use HTMLCacheUpdateJob; |
29 | use IDBAccessObject; |
30 | use ILanguageConverter; |
31 | use InvalidArgumentException; |
32 | use Language; |
33 | use MapCacheLRU; |
34 | use MediaWiki\Cache\LinkCache; |
35 | use MediaWiki\Context\RequestContext; |
36 | use MediaWiki\DAO\WikiAwareEntityTrait; |
37 | use MediaWiki\Deferred\AtomicSectionUpdate; |
38 | use MediaWiki\Deferred\AutoCommitUpdate; |
39 | use MediaWiki\Deferred\DeferredUpdates; |
40 | use MediaWiki\HookContainer\HookRunner; |
41 | use MediaWiki\Html\Html; |
42 | use MediaWiki\Interwiki\InterwikiLookup; |
43 | use MediaWiki\Linker\LinkTarget; |
44 | use MediaWiki\MainConfigNames; |
45 | use MediaWiki\MediaWikiServices; |
46 | use MediaWiki\Message\Message; |
47 | use MediaWiki\Page\ExistingPageRecord; |
48 | use MediaWiki\Page\PageIdentity; |
49 | use MediaWiki\Page\PageIdentityValue; |
50 | use MediaWiki\Page\PageReference; |
51 | use MediaWiki\Page\PageStoreRecord; |
52 | use MediaWiki\Page\ProperPageIdentity; |
53 | use MediaWiki\Parser\Sanitizer; |
54 | use MediaWiki\Request\PathRouter; |
55 | use MediaWiki\ResourceLoader\WikiModule; |
56 | use MediaWiki\SpecialPage\SpecialPage; |
57 | use MediaWiki\Utils\MWTimestamp; |
58 | use MessageLocalizer; |
59 | use MWException; |
60 | use RuntimeException; |
61 | use stdClass; |
62 | use Wikimedia\Assert\Assert; |
63 | use Wikimedia\Assert\PreconditionException; |
64 | use Wikimedia\Parsoid\Core\LinkTarget as ParsoidLinkTarget; |
65 | use Wikimedia\Parsoid\Core\LinkTargetTrait; |
66 | use Wikimedia\Rdbms\IConnectionProvider; |
67 | use Wikimedia\Rdbms\IDatabase; |
68 | use WikiPage; |
69 | |
70 | /** |
71 | * Represents a title within MediaWiki. |
72 | * Optionally may contain an interwiki designation or namespace. |
73 | * @note This class can fetch various kinds of data from the database; |
74 | * however, it does so inefficiently. |
75 | * @note Consider using a TitleValue object instead. TitleValue is more lightweight |
76 | * and does not rely on global state or the database. |
77 | */ |
78 | class Title implements LinkTarget, PageIdentity { |
79 | use WikiAwareEntityTrait; |
80 | use LinkTargetTrait; |
81 | |
82 | /** @var MapCacheLRU|null */ |
83 | private static $titleCache = null; |
84 | |
85 | /** |
86 | * Title::newFromText maintains a cache to avoid expensive re-normalization of |
87 | * commonly used titles. On a batch operation this can become a memory leak |
88 | * if not bounded. |
89 | */ |
90 | private const CACHE_MAX = 1000; |
91 | |
92 | /** |
93 | * Flag for use with factory methods like newFromLinkTarget() that have |
94 | * a $forceClone parameter. If set, the method must return a new instance. |
95 | * Without this flag, some factory methods may return existing instances.as |
96 | * |
97 | * @since 1.33 |
98 | */ |
99 | public const NEW_CLONE = 'clone'; |
100 | |
101 | /** @var string Text form (spaces not underscores) of the main part */ |
102 | private $mTextform = ''; |
103 | |
104 | /** @var string URL-encoded form of the main part */ |
105 | private $mUrlform = ''; |
106 | |
107 | /** @var string Main part with underscores */ |
108 | private $mDbkeyform = ''; |
109 | |
110 | /** @var int Namespace index, i.e. one of the NS_xxxx constants */ |
111 | private $mNamespace = NS_MAIN; |
112 | |
113 | /** @var string Interwiki prefix */ |
114 | private $mInterwiki = ''; |
115 | |
116 | /** @var bool Was this Title created from a string with a local interwiki prefix? */ |
117 | private $mLocalInterwiki = false; |
118 | |
119 | /** @var string Title fragment (i.e. the bit after the #) */ |
120 | private $mFragment = ''; |
121 | |
122 | /***************************************************************************/ |
123 | // region Private member variables |
124 | /** @name Private member variables |
125 | * Please use the accessor functions instead. |
126 | * @internal |
127 | * @{ |
128 | */ |
129 | |
130 | /** @var int Article ID, fetched from the link cache on demand */ |
131 | public $mArticleID = -1; |
132 | |
133 | /** @var int|false ID of most recent revision */ |
134 | protected $mLatestID = false; |
135 | |
136 | /** |
137 | * @var string|false ID of the page's content model, i.e. one of the |
138 | * CONTENT_MODEL_XXX constants |
139 | */ |
140 | private $mContentModel = false; |
141 | |
142 | /** |
143 | * @var bool If a content model was forced via setContentModel() |
144 | * this will be true to avoid having other code paths reset it |
145 | */ |
146 | private $mForcedContentModel = false; |
147 | |
148 | /** @var int|null Estimated number of revisions; null of not loaded */ |
149 | private $mEstimateRevisions; |
150 | |
151 | /** |
152 | * Text form including namespace/interwiki, initialised on demand |
153 | * |
154 | * Only public to share cache with TitleFormatter |
155 | * |
156 | * @internal |
157 | * @var string|null |
158 | */ |
159 | public $prefixedText = null; |
160 | |
161 | /** |
162 | * Namespace to assume when no namespace was passed to factory methods. |
163 | * This must be NS_MAIN, as it's hardcoded in several places. See T2696. |
164 | * Used primarily for {{transclusion}} tags. |
165 | */ |
166 | private const DEFAULT_NAMESPACE = NS_MAIN; |
167 | |
168 | /** @var int The page length, 0 for special pages */ |
169 | protected $mLength = -1; |
170 | |
171 | /** @var null|bool Is the article at this title a redirect? */ |
172 | public $mRedirect = null; |
173 | |
174 | /** @var bool Whether a page has any subpages */ |
175 | private $mHasSubpages; |
176 | |
177 | /** @var array|null The (string) language code of the page's language and content code. */ |
178 | private $mPageLanguage; |
179 | |
180 | /** @var string|false|null The page language code from the database, null if not saved in |
181 | * the database or false if not loaded, yet. |
182 | */ |
183 | private $mDbPageLanguage = false; |
184 | |
185 | /** @var TitleValue|null */ |
186 | private $mTitleValue = null; |
187 | |
188 | /** @var bool|null Would deleting this page be a big deletion? */ |
189 | private $mIsBigDeletion = null; |
190 | |
191 | /** @var bool|null Is the title known to be valid? */ |
192 | private $mIsValid = null; |
193 | |
194 | /** @var string|null The key of this instance in the internal Title instance cache */ |
195 | private $mInstanceCacheKey = null; |
196 | |
197 | // endregion -- end of private member variables |
198 | /** @} */ |
199 | /***************************************************************************/ |
200 | |
201 | /** |
202 | * Shorthand for getting a Language Converter for specific language |
203 | * @param Language $language Language of converter |
204 | * @return ILanguageConverter |
205 | */ |
206 | private function getLanguageConverter( $language ): ILanguageConverter { |
207 | return MediaWikiServices::getInstance()->getLanguageConverterFactory() |
208 | ->getLanguageConverter( $language ); |
209 | } |
210 | |
211 | /** |
212 | * Shorthand for getting a Language Converter for page's language |
213 | * @return ILanguageConverter |
214 | */ |
215 | private function getPageLanguageConverter(): ILanguageConverter { |
216 | return $this->getLanguageConverter( $this->getPageLanguage() ); |
217 | } |
218 | |
219 | /** |
220 | * Shorthand for getting a database connection provider |
221 | * @return IConnectionProvider |
222 | */ |
223 | private function getDbProvider(): IConnectionProvider { |
224 | return MediaWikiServices::getInstance()->getConnectionProvider(); |
225 | } |
226 | |
227 | /** |
228 | * B/C kludge: provide a TitleParser for use by Title. |
229 | * Ideally, Title would have no methods that need this. |
230 | * Avoid usage of this singleton by using TitleValue |
231 | * and the associated services when possible. |
232 | * |
233 | * @return TitleFormatter |
234 | */ |
235 | private static function getTitleFormatter() { |
236 | return MediaWikiServices::getInstance()->getTitleFormatter(); |
237 | } |
238 | |
239 | /** |
240 | * B/C kludge: provide an InterwikiLookup for use by Title. |
241 | * Ideally, Title would have no methods that need this. |
242 | * Avoid usage of this singleton by using TitleValue |
243 | * and the associated services when possible. |
244 | * |
245 | * @return InterwikiLookup |
246 | */ |
247 | private static function getInterwikiLookup() { |
248 | return MediaWikiServices::getInstance()->getInterwikiLookup(); |
249 | } |
250 | |
251 | private function __construct() { |
252 | } |
253 | |
254 | /** |
255 | * Create a new Title from a prefixed DB key |
256 | * |
257 | * @param string $key The database key, which has underscores |
258 | * instead of spaces, possibly including namespace and |
259 | * interwiki prefixes |
260 | * @return Title|null Title, or null on an error |
261 | */ |
262 | public static function newFromDBkey( $key ) { |
263 | $t = new self(); |
264 | |
265 | try { |
266 | $t->secureAndSplit( $key ); |
267 | return $t; |
268 | } catch ( MalformedTitleException $ex ) { |
269 | return null; |
270 | } |
271 | } |
272 | |
273 | /** |
274 | * Returns a Title given a LinkTarget. |
275 | * If the given LinkTarget is already a Title instance, that instance is returned, |
276 | * unless $forceClone is "clone". If $forceClone is "clone" and the given LinkTarget |
277 | * is already a Title instance, that instance is copied using the clone operator. |
278 | * |
279 | * @since 1.27 |
280 | * @param ParsoidLinkTarget $linkTarget Assumed to be safe. |
281 | * @param string $forceClone set to NEW_CLONE to ensure a fresh instance is returned. |
282 | * @return Title |
283 | */ |
284 | public static function newFromLinkTarget( ParsoidLinkTarget $linkTarget, $forceClone = '' ) { |
285 | if ( $linkTarget instanceof Title ) { |
286 | // Special case if it's already a Title object |
287 | if ( $forceClone === self::NEW_CLONE ) { |
288 | return clone $linkTarget; |
289 | } else { |
290 | return $linkTarget; |
291 | } |
292 | } |
293 | return self::makeTitle( |
294 | $linkTarget->getNamespace(), |
295 | $linkTarget->getText(), |
296 | $linkTarget->getFragment(), |
297 | $linkTarget->getInterwiki() |
298 | ); |
299 | } |
300 | |
301 | /** |
302 | * Same as newFromLinkTarget(), but if passed null, returns null. |
303 | * |
304 | * @since 1.34 |
305 | * @param ParsoidLinkTarget|null $linkTarget Assumed to be safe (if not null). |
306 | * @return Title|null |
307 | */ |
308 | public static function castFromLinkTarget( ?ParsoidLinkTarget $linkTarget ) { |
309 | if ( !$linkTarget ) { |
310 | return null; |
311 | } |
312 | return self::newFromLinkTarget( $linkTarget ); |
313 | } |
314 | |
315 | /** |
316 | * Return a Title for a given PageIdentity. If $pageIdentity is a Title, |
317 | * that Title is returned unchanged. |
318 | * |
319 | * @since 1.41 |
320 | * @param PageIdentity $pageIdentity |
321 | * @return Title |
322 | */ |
323 | public static function newFromPageIdentity( PageIdentity $pageIdentity ): Title { |
324 | return self::newFromPageReference( $pageIdentity ); |
325 | } |
326 | |
327 | /** |
328 | * Same as newFromPageIdentity(), but if passed null, returns null. |
329 | * |
330 | * @since 1.36 |
331 | * @param PageIdentity|null $pageIdentity |
332 | * @return Title|null |
333 | */ |
334 | public static function castFromPageIdentity( ?PageIdentity $pageIdentity ): ?Title { |
335 | return self::castFromPageReference( $pageIdentity ); |
336 | } |
337 | |
338 | /** |
339 | * Return a Title for a given Reference. If $pageReference is a Title, |
340 | * that Title is returned unchanged. |
341 | * |
342 | * @since 1.41 |
343 | * @param PageReference $pageReference |
344 | * @return Title |
345 | */ |
346 | public static function newFromPageReference( PageReference $pageReference ): Title { |
347 | if ( $pageReference instanceof Title ) { |
348 | return $pageReference; |
349 | } |
350 | |
351 | $pageReference->assertWiki( self::LOCAL ); |
352 | $title = self::makeTitle( $pageReference->getNamespace(), $pageReference->getDBkey() ); |
353 | |
354 | if ( $pageReference instanceof PageIdentity ) { |
355 | $title->mArticleID = $pageReference->getId(); |
356 | } |
357 | return $title; |
358 | } |
359 | |
360 | /** |
361 | * Same as newFromPageReference(), but if passed null, returns null. |
362 | * |
363 | * @since 1.37 |
364 | * @param PageReference|null $pageReference |
365 | * @return Title|null |
366 | */ |
367 | public static function castFromPageReference( ?PageReference $pageReference ): ?Title { |
368 | if ( !$pageReference ) { |
369 | return null; |
370 | } |
371 | return self::newFromPageReference( $pageReference ); |
372 | } |
373 | |
374 | /** |
375 | * Create a new Title from text, such as what one would find in a link. |
376 | * Decodes any HTML entities in the text. |
377 | * Titles returned by this method are guaranteed to be valid. |
378 | * Call canExist() to check if the Title represents an editable page. |
379 | * |
380 | * @note The Title instance returned by this method is not guaranteed to be a fresh instance. |
381 | * It may instead be a cached instance created previously, with references to it remaining |
382 | * elsewhere. |
383 | * |
384 | * @param string|int|null $text The link text; spaces, prefixes, and an |
385 | * initial ':' indicating the main namespace are accepted. |
386 | * @param int $defaultNamespace The namespace to use if none is specified |
387 | * by a prefix. If you want to force a specific namespace even if |
388 | * $text might begin with a namespace prefix, use makeTitle() or |
389 | * makeTitleSafe(). |
390 | * @throws InvalidArgumentException |
391 | * @return Title|null Title or null if the Title could not be parsed because |
392 | * it is invalid. |
393 | */ |
394 | public static function newFromText( $text, $defaultNamespace = NS_MAIN ) { |
395 | // DWIM: Integers can be passed in here when page titles are used as array keys. |
396 | if ( $text !== null && !is_string( $text ) && !is_int( $text ) ) { |
397 | throw new InvalidArgumentException( '$text must be a string.' ); |
398 | } |
399 | if ( $text === null || $text === '' ) { |
400 | return null; |
401 | } |
402 | |
403 | try { |
404 | return self::newFromTextThrow( (string)$text, (int)$defaultNamespace ); |
405 | } catch ( MalformedTitleException $ex ) { |
406 | return null; |
407 | } |
408 | } |
409 | |
410 | /** |
411 | * Like Title::newFromText(), but throws MalformedTitleException when the title is invalid, |
412 | * rather than returning null. |
413 | * |
414 | * Titles returned by this method are guaranteed to be valid. |
415 | * Call canExist() to check if the Title represents an editable page. |
416 | * |
417 | * @note The Title instance returned by this method is not guaranteed to be a fresh instance. |
418 | * It may instead be a cached instance created previously, with references to it remaining |
419 | * elsewhere. |
420 | * |
421 | * @see Title::newFromText |
422 | * |
423 | * @since 1.25 |
424 | * @param string $text Title text to check |
425 | * @param int $defaultNamespace |
426 | * @throws MalformedTitleException If the title is invalid. |
427 | * @return Title |
428 | */ |
429 | public static function newFromTextThrow( $text, $defaultNamespace = NS_MAIN ) { |
430 | if ( is_object( $text ) ) { |
431 | throw new InvalidArgumentException( '$text must be a string, given an object' ); |
432 | } elseif ( $text === null ) { |
433 | // Legacy code relies on MalformedTitleException being thrown in this case |
434 | // TODO: fix(happens when URL with no title in it is parsed). |
435 | throw new MalformedTitleException( 'title-invalid-empty' ); |
436 | } |
437 | |
438 | $titleCache = self::getTitleCache(); |
439 | |
440 | // Wiki pages often contain multiple links to the same page. |
441 | // Title normalization and parsing can become expensive on pages with many |
442 | // links, so we can save a little time by caching them. |
443 | if ( $defaultNamespace === NS_MAIN ) { |
444 | $t = $titleCache->get( $text ); |
445 | if ( $t ) { |
446 | return $t; |
447 | } |
448 | } |
449 | |
450 | // Convert things like é ā or 〗 into normalized (T16952) text |
451 | $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text ); |
452 | |
453 | $t = new Title(); |
454 | $dbKeyForm = strtr( $filteredText, ' ', '_' ); |
455 | |
456 | $t->secureAndSplit( $dbKeyForm, (int)$defaultNamespace ); |
457 | if ( $defaultNamespace === NS_MAIN ) { |
458 | $t->mInstanceCacheKey = $text; |
459 | $titleCache->set( $text, $t ); |
460 | } |
461 | return $t; |
462 | } |
463 | |
464 | /** |
465 | * Removes this instance from the internal title cache, so it can be modified in-place |
466 | * without polluting the cache (see T281337). |
467 | */ |
468 | private function uncache() { |
469 | if ( $this->mInstanceCacheKey !== null ) { |
470 | $titleCache = self::getTitleCache(); |
471 | $titleCache->clear( $this->mInstanceCacheKey ); |
472 | $this->mInstanceCacheKey = null; |
473 | } |
474 | } |
475 | |
476 | /** |
477 | * THIS IS NOT THE FUNCTION YOU WANT. Use Title::newFromText(). |
478 | * |
479 | * Example of wrong and broken code: |
480 | * $title = Title::newFromURL( $request->getText( 'title' ) ); |
481 | * |
482 | * Example of right code: |
483 | * $title = Title::newFromText( $request->getText( 'title' ) ); |
484 | * |
485 | * Create a new Title from URL-encoded text. Ensures that |
486 | * the given title's length does not exceed the maximum. |
487 | * |
488 | * @param string $url The title, as might be taken from a URL |
489 | * @return Title|null The new object, or null on an error |
490 | */ |
491 | public static function newFromURL( $url ) { |
492 | $t = new Title(); |
493 | |
494 | # For compatibility with old buggy URLs. "+" is usually not valid in titles, |
495 | # but some URLs used it as a space replacement and they still come |
496 | # from some external search tools. |
497 | if ( !str_contains( self::legalChars(), '+' ) ) { |
498 | $url = strtr( $url, '+', ' ' ); |
499 | } |
500 | |
501 | $dbKeyForm = strtr( $url, ' ', '_' ); |
502 | |
503 | try { |
504 | $t->secureAndSplit( $dbKeyForm ); |
505 | return $t; |
506 | } catch ( MalformedTitleException $ex ) { |
507 | return null; |
508 | } |
509 | } |
510 | |
511 | /** |
512 | * @return MapCacheLRU |
513 | */ |
514 | private static function getTitleCache() { |
515 | if ( self::$titleCache === null ) { |
516 | self::$titleCache = new MapCacheLRU( self::CACHE_MAX ); |
517 | } |
518 | return self::$titleCache; |
519 | } |
520 | |
521 | /** |
522 | * Create a new Title from an article ID |
523 | * |
524 | * @param int $id The page_id corresponding to the Title to create |
525 | * @param int $flags Bitfield of IDBAccessObject::READ_* constants |
526 | * @return Title|null The new object, or null on an error |
527 | */ |
528 | public static function newFromID( $id, $flags = 0 ) { |
529 | $pageStore = MediaWikiServices::getInstance()->getPageStore(); |
530 | $dbr = DBAccessObjectUtils::getDBFromRecency( |
531 | MediaWikiServices::getInstance()->getConnectionProvider(), |
532 | $flags |
533 | ); |
534 | $row = $dbr->newSelectQueryBuilder() |
535 | ->select( $pageStore->getSelectFields() ) |
536 | ->from( 'page' ) |
537 | ->where( [ 'page_id' => $id ] ) |
538 | ->recency( $flags ) |
539 | ->caller( __METHOD__ )->fetchRow(); |
540 | if ( $row !== false ) { |
541 | $title = self::newFromRow( $row ); |
542 | } else { |
543 | $title = null; |
544 | } |
545 | |
546 | return $title; |
547 | } |
548 | |
549 | /** |
550 | * Make a Title object from a DB row |
551 | * |
552 | * @param stdClass $row Object database row (needs at least page_title,page_namespace) |
553 | * @return Title |
554 | */ |
555 | public static function newFromRow( $row ) { |
556 | $t = self::makeTitle( $row->page_namespace, $row->page_title ); |
557 | $t->loadFromRow( $row ); |
558 | return $t; |
559 | } |
560 | |
561 | /** |
562 | * Load Title object fields from a DB row. |
563 | * If false is given, the title will be treated as non-existing. |
564 | * |
565 | * @param stdClass|false $row Database row |
566 | */ |
567 | public function loadFromRow( $row ) { |
568 | if ( $row ) { // page found |
569 | if ( isset( $row->page_id ) ) { |
570 | $this->mArticleID = (int)$row->page_id; |
571 | } |
572 | if ( isset( $row->page_len ) ) { |
573 | $this->mLength = (int)$row->page_len; |
574 | } |
575 | if ( isset( $row->page_is_redirect ) ) { |
576 | $this->mRedirect = (bool)$row->page_is_redirect; |
577 | } |
578 | if ( isset( $row->page_latest ) ) { |
579 | $this->mLatestID = (int)$row->page_latest; |
580 | } |
581 | if ( isset( $row->page_content_model ) ) { |
582 | $this->lazyFillContentModel( $row->page_content_model ); |
583 | } else { |
584 | $this->lazyFillContentModel( false ); // lazily-load getContentModel() |
585 | } |
586 | if ( isset( $row->page_lang ) ) { |
587 | $this->mDbPageLanguage = (string)$row->page_lang; |
588 | } |
589 | } else { // page not found |
590 | $this->mArticleID = 0; |
591 | $this->mLength = 0; |
592 | $this->mRedirect = false; |
593 | $this->mLatestID = 0; |
594 | $this->lazyFillContentModel( false ); // lazily-load getContentModel() |
595 | } |
596 | } |
597 | |
598 | /** |
599 | * Create a new Title from a namespace index and a DB key. |
600 | * |
601 | * It's assumed that $ns and $title are safe, for instance when |
602 | * they came directly from the database or a special page name, |
603 | * not from user input. |
604 | * |
605 | * No validation is applied. For convenience, spaces are normalized |
606 | * to underscores, so that e.g. user_text fields can be used directly. |
607 | * |
608 | * @note This method may return Title objects that are "invalid" |
609 | * according to the isValid() method. This is usually caused by |
610 | * configuration changes: e.g. a namespace that was once defined is |
611 | * no longer configured, or a character that was once allowed in |
612 | * titles is now forbidden. |
613 | * |
614 | * @param int $ns The namespace of the article |
615 | * @param string $title The unprefixed database key form |
616 | * @param string $fragment The link fragment (after the "#") |
617 | * @param string $interwiki The interwiki prefix |
618 | * @return Title The new object |
619 | */ |
620 | public static function makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) { |
621 | $t = new Title(); |
622 | $t->mInterwiki = $interwiki; |
623 | $t->mFragment = self::normalizeFragment( $fragment ); |
624 | $t->mNamespace = $ns = (int)$ns; |
625 | $t->mDbkeyform = strtr( $title, ' ', '_' ); |
626 | $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; |
627 | $t->mUrlform = wfUrlencode( $t->mDbkeyform ); |
628 | $t->mTextform = strtr( $title, '_', ' ' ); |
629 | return $t; |
630 | } |
631 | |
632 | /** |
633 | * Create a new Title from a namespace index and a DB key. |
634 | * The parameters will be checked for validity, which is a bit slower |
635 | * than makeTitle() but safer for user-provided data. |
636 | * |
637 | * The Title object returned by this method is guaranteed to be valid. |
638 | * Call canExist() to check if the Title represents an editable page. |
639 | * |
640 | * @param int $ns The namespace of the article |
641 | * @param string $title Database key form |
642 | * @param string $fragment The link fragment (after the "#") |
643 | * @param string $interwiki Interwiki prefix |
644 | * @return Title|null The new object, or null on an error |
645 | */ |
646 | public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) { |
647 | // NOTE: ideally, this would just call makeTitle() and then isValid(), |
648 | // but presently, that means more overhead on a potential performance hotspot. |
649 | |
650 | if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $ns ) ) { |
651 | return null; |
652 | } |
653 | |
654 | $t = new Title(); |
655 | $dbKeyForm = self::makeName( $ns, $title, $fragment, $interwiki, true ); |
656 | |
657 | try { |
658 | $t->secureAndSplit( $dbKeyForm ); |
659 | return $t; |
660 | } catch ( MalformedTitleException $ex ) { |
661 | return null; |
662 | } |
663 | } |
664 | |
665 | /** |
666 | * Create a new Title for the Main Page |
667 | * |
668 | * This uses the 'mainpage' interface message, which could be specified in |
669 | * `$wgForceUIMsgAsContentMsg`. If that is the case, then calling this method |
670 | * will use the user language, which would involve initialising the session |
671 | * via `RequestContext::getMain()->getLanguage()`. For session-less endpoints, |
672 | * be sure to pass in a MessageLocalizer (such as your own RequestContext or |
673 | * ResourceLoader Context) to prevent an error. |
674 | * |
675 | * @note The Title instance returned by this method is not guaranteed to be a fresh instance. |
676 | * It may instead be a cached instance created previously, with references to it remaining |
677 | * elsewhere. |
678 | * |
679 | * @param MessageLocalizer|null $localizer An optional context to use (since 1.34) |
680 | * @return Title |
681 | */ |
682 | public static function newMainPage( MessageLocalizer $localizer = null ) { |
683 | static $recursionGuard = false; |
684 | |
685 | $title = null; |
686 | |
687 | if ( !$recursionGuard ) { |
688 | $msg = $localizer ? $localizer->msg( 'mainpage' ) : wfMessage( 'mainpage' ); |
689 | |
690 | $recursionGuard = true; |
691 | $title = self::newFromText( $msg->inContentLanguage()->text() ); |
692 | $recursionGuard = false; |
693 | } |
694 | |
695 | // Every page renders at least one link to the Main Page (e.g. sidebar). |
696 | // Don't produce fatal errors that would make the wiki inaccessible, and hard to fix the |
697 | // invalid message. |
698 | // |
699 | // Fallback scenarios: |
700 | // * Recursion guard |
701 | // If the message contains a bare local interwiki (T297571), then |
702 | // Title::newFromText via MediaWikiTitleCodec::splitTitleString can get back here. |
703 | // * Invalid title |
704 | // If the 'mainpage' message contains something that is invalid, Title::newFromText |
705 | // will return null. |
706 | |
707 | return $title ?? self::makeTitle( NS_MAIN, 'Main Page' ); |
708 | } |
709 | |
710 | /** |
711 | * Get a regex character class describing the legal characters in a link |
712 | * |
713 | * @return string The list of characters, not delimited |
714 | */ |
715 | public static function legalChars() { |
716 | global $wgLegalTitleChars; |
717 | return $wgLegalTitleChars; |
718 | } |
719 | |
720 | /** |
721 | * Utility method for converting a character sequence from bytes to Unicode. |
722 | * |
723 | * Primary usecase being converting $wgLegalTitleChars to a sequence usable in |
724 | * javascript, as PHP uses UTF-8 bytes where javascript uses Unicode code units. |
725 | * |
726 | * @param string $byteClass |
727 | * @return string |
728 | */ |
729 | public static function convertByteClassToUnicodeClass( $byteClass ) { |
730 | $length = strlen( $byteClass ); |
731 | // Input token queue |
732 | $x0 = $x1 = $x2 = ''; |
733 | // Decoded queue |
734 | $d0 = $d1 = ''; |
735 | // Decoded integer codepoints |
736 | $ord0 = $ord1 = $ord2 = 0; |
737 | // Re-encoded queue |
738 | $r0 = $r1 = $r2 = ''; |
739 | // Output |
740 | $out = ''; |
741 | // Flags |
742 | $allowUnicode = false; |
743 | for ( $pos = 0; $pos < $length; $pos++ ) { |
744 | // Shift the queues down |
745 | $x2 = $x1; |
746 | $x1 = $x0; |
747 | $d1 = $d0; |
748 | $ord2 = $ord1; |
749 | $ord1 = $ord0; |
750 | $r2 = $r1; |
751 | $r1 = $r0; |
752 | // Load the current input token and decoded values |
753 | $inChar = $byteClass[$pos]; |
754 | if ( $inChar === '\\' ) { |
755 | if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) { |
756 | $x0 = $inChar . $m[0]; |
757 | $d0 = chr( hexdec( $m[1] ) ); |
758 | $pos += strlen( $m[0] ); |
759 | } elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) { |
760 | $x0 = $inChar . $m[0]; |
761 | $d0 = chr( octdec( $m[0] ) ); |
762 | $pos += strlen( $m[0] ); |
763 | } elseif ( $pos + 1 >= $length ) { |
764 | $x0 = $d0 = '\\'; |
765 | } else { |
766 | $d0 = $byteClass[$pos + 1]; |
767 | $x0 = $inChar . $d0; |
768 | $pos++; |
769 | } |
770 | } else { |
771 | $x0 = $d0 = $inChar; |
772 | } |
773 | $ord0 = ord( $d0 ); |
774 | // Load the current re-encoded value |
775 | if ( $ord0 < 32 || $ord0 == 0x7f ) { |
776 | $r0 = sprintf( '\x%02x', $ord0 ); |
777 | } elseif ( $ord0 >= 0x80 ) { |
778 | // Allow unicode if a single high-bit character appears |
779 | $r0 = sprintf( '\x%02x', $ord0 ); |
780 | $allowUnicode = true; |
781 | // @phan-suppress-next-line PhanParamSuspiciousOrder false positive |
782 | } elseif ( strpos( '-\\[]^', $d0 ) !== false ) { |
783 | $r0 = '\\' . $d0; |
784 | } else { |
785 | $r0 = $d0; |
786 | } |
787 | // Do the output |
788 | if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) { |
789 | // Range |
790 | if ( $ord2 > $ord0 ) { |
791 | // Empty range |
792 | } elseif ( $ord0 >= 0x80 ) { |
793 | // Unicode range |
794 | $allowUnicode = true; |
795 | if ( $ord2 < 0x80 ) { |
796 | // Keep the non-unicode section of the range |
797 | $out .= "$r2-\\x7F"; |
798 | } |
799 | } else { |
800 | // Normal range |
801 | $out .= "$r2-$r0"; |
802 | } |
803 | // Reset state to the initial value |
804 | // @phan-suppress-next-line PhanPluginRedundantAssignmentInLoop |
805 | $x0 = $x1 = $d0 = $d1 = $r0 = $r1 = ''; |
806 | } elseif ( $ord2 < 0x80 ) { |
807 | // ASCII character |
808 | $out .= $r2; |
809 | } |
810 | } |
811 | // @phan-suppress-next-line PhanSuspiciousValueComparison |
812 | if ( $ord1 < 0x80 ) { |
813 | $out .= $r1; |
814 | } |
815 | if ( $ord0 < 0x80 ) { |
816 | $out .= $r0; |
817 | } |
818 | if ( $allowUnicode ) { |
819 | $out .= '\u0080-\uFFFF'; |
820 | } |
821 | return $out; |
822 | } |
823 | |
824 | /** |
825 | * Make a prefixed DB key from a DB key and a namespace index |
826 | * |
827 | * @param int $ns Numerical representation of the namespace |
828 | * @param string $title The DB key form the title |
829 | * @param string $fragment The link fragment (after the "#") |
830 | * @param string $interwiki The interwiki prefix |
831 | * @param bool $canonicalNamespace If true, use the canonical name for |
832 | * $ns instead of the localized version. |
833 | * @return string The prefixed form of the title |
834 | */ |
835 | public static function makeName( $ns, $title, $fragment = '', $interwiki = '', |
836 | $canonicalNamespace = false |
837 | ) { |
838 | if ( $canonicalNamespace ) { |
839 | $namespace = MediaWikiServices::getInstance()->getNamespaceInfo()-> |
840 | getCanonicalName( $ns ); |
841 | } else { |
842 | $namespace = MediaWikiServices::getInstance()->getContentLanguage()->getNsText( $ns ); |
843 | } |
844 | if ( $namespace === false ) { |
845 | // See T165149. Awkward, but better than erroneously linking to the main namespace. |
846 | $namespace = self::makeName( NS_SPECIAL, "Badtitle/NS$ns", '', '', $canonicalNamespace ); |
847 | } |
848 | $name = $namespace === '' ? $title : "$namespace:$title"; |
849 | if ( strval( $interwiki ) != '' ) { |
850 | $name = "$interwiki:$name"; |
851 | } |
852 | if ( strval( $fragment ) != '' ) { |
853 | $name .= '#' . $fragment; |
854 | } |
855 | return $name; |
856 | } |
857 | |
858 | /** |
859 | * Callback for usort() to do title sorts by (namespace, title) |
860 | * |
861 | * @param LinkTarget|PageReference $a |
862 | * @param LinkTarget|PageReference $b |
863 | * |
864 | * @return int Result of string comparison, or namespace comparison |
865 | */ |
866 | public static function compare( $a, $b ) { |
867 | return $a->getNamespace() <=> $b->getNamespace() |
868 | ?: strcmp( $a->getDBkey(), $b->getDBkey() ); |
869 | } |
870 | |
871 | /** |
872 | * Returns true if the title is a valid link target, and that it has been |
873 | * properly normalized. This method checks that the title is syntactically valid, |
874 | * and that the namespace it refers to exists. |
875 | * |
876 | * Titles constructed using newFromText() or makeTitleSafe() are always valid. |
877 | * |
878 | * @note Code that wants to check whether the title can represent a page that can |
879 | * be created and edited should use canExist() instead. Examples of valid titles |
880 | * that cannot "exist" are Special pages, interwiki links, and on-page section links |
881 | * that only have the fragment part set. |
882 | * |
883 | * @see canExist() |
884 | * |
885 | * @return bool |
886 | */ |
887 | public function isValid() { |
888 | if ( $this->mIsValid !== null ) { |
889 | return $this->mIsValid; |
890 | } |
891 | |
892 | try { |
893 | // Optimization: Avoid Title::getFullText because that involves GenderCache |
894 | // and (unbatched) database queries. For validation, canonical namespace suffices. |
895 | $text = self::makeName( $this->mNamespace, $this->mDbkeyform, $this->mFragment, $this->mInterwiki, true ); |
896 | $titleCodec = MediaWikiServices::getInstance()->getTitleParser(); |
897 | |
898 | '@phan-var MediaWikiTitleCodec $titleCodec'; |
899 | $parts = $titleCodec->splitTitleString( $text, $this->mNamespace ); |
900 | |
901 | // Check that nothing changed! |
902 | // This ensures that $text was already properly normalized. |
903 | if ( $parts['fragment'] !== $this->mFragment |
904 | || $parts['interwiki'] !== $this->mInterwiki |
905 | || $parts['local_interwiki'] !== $this->mLocalInterwiki |
906 | || $parts['namespace'] !== $this->mNamespace |
907 | || $parts['dbkey'] !== $this->mDbkeyform |
908 | ) { |
909 | $this->mIsValid = false; |
910 | return $this->mIsValid; |
911 | } |
912 | } catch ( MalformedTitleException $ex ) { |
913 | $this->mIsValid = false; |
914 | return $this->mIsValid; |
915 | } |
916 | |
917 | $this->mIsValid = true; |
918 | return $this->mIsValid; |
919 | } |
920 | |
921 | /** |
922 | * Determine whether the object refers to a page within |
923 | * this project (either this wiki or a wiki with a local |
924 | * interwiki, see https://www.mediawiki.org/wiki/Manual:Interwiki_table#iw_local ) |
925 | * |
926 | * @return bool True if this is an in-project interwiki link or a wikilink, false otherwise |
927 | */ |
928 | public function isLocal() { |
929 | if ( $this->isExternal() ) { |
930 | $iw = self::getInterwikiLookup()->fetch( $this->mInterwiki ); |
931 | if ( $iw ) { |
932 | return $iw->isLocal(); |
933 | } |
934 | } |
935 | return true; |
936 | } |
937 | |
938 | /** |
939 | * Get the interwiki prefix |
940 | * |
941 | * Use Title::isExternal to check if a interwiki is set |
942 | * |
943 | * @return string Interwiki prefix |
944 | */ |
945 | public function getInterwiki(): string { |
946 | return $this->mInterwiki; |
947 | } |
948 | |
949 | /** |
950 | * Was this a local interwiki link? |
951 | * |
952 | * @return bool |
953 | */ |
954 | public function wasLocalInterwiki() { |
955 | return $this->mLocalInterwiki; |
956 | } |
957 | |
958 | /** |
959 | * Determine whether the object refers to a page within |
960 | * this project and is transcludable. |
961 | * |
962 | * @return bool True if this is transcludable |
963 | */ |
964 | public function isTrans() { |
965 | if ( !$this->isExternal() ) { |
966 | return false; |
967 | } |
968 | |
969 | return self::getInterwikiLookup()->fetch( $this->mInterwiki )->isTranscludable(); |
970 | } |
971 | |
972 | /** |
973 | * Returns the DB name of the distant wiki which owns the object. |
974 | * |
975 | * @return string|false The DB name |
976 | */ |
977 | public function getTransWikiID() { |
978 | if ( !$this->isExternal() ) { |
979 | return false; |
980 | } |
981 | |
982 | return self::getInterwikiLookup()->fetch( $this->mInterwiki )->getWikiID(); |
983 | } |
984 | |
985 | /** |
986 | * Get a TitleValue object representing this Title. |
987 | * |
988 | * @note Not all valid Titles have a corresponding valid TitleValue |
989 | * (e.g. TitleValues cannot represent page-local links that have a |
990 | * fragment but no title text). |
991 | * |
992 | * @return TitleValue|null |
993 | */ |
994 | public function getTitleValue() { |
995 | if ( $this->mTitleValue === null ) { |
996 | try { |
997 | $this->mTitleValue = new TitleValue( |
998 | $this->mNamespace, |
999 | $this->mDbkeyform, |
1000 | $this->mFragment, |
1001 | $this->mInterwiki |
1002 | ); |
1003 | } catch ( InvalidArgumentException $ex ) { |
1004 | wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' . |
1005 | $this->getPrefixedText() . ']]: ' . $ex->getMessage() ); |
1006 | } |
1007 | } |
1008 | |
1009 | return $this->mTitleValue; |
1010 | } |
1011 | |
1012 | /** |
1013 | * Get the text form (spaces not underscores) of the main part |
1014 | * |
1015 | * @return string Main part of the title |
1016 | */ |
1017 | public function getText(): string { |
1018 | return $this->mTextform; |
1019 | } |
1020 | |
1021 | /** |
1022 | * Get the URL-encoded form of the main part |
1023 | * |
1024 | * @return string Main part of the title, URL-encoded |
1025 | */ |
1026 | public function getPartialURL() { |
1027 | return $this->mUrlform; |
1028 | } |
1029 | |
1030 | /** |
1031 | * Get the main part with underscores |
1032 | * |
1033 | * @return string Main part of the title, with underscores |
1034 | */ |
1035 | public function getDBkey(): string { |
1036 | return $this->mDbkeyform; |
1037 | } |
1038 | |
1039 | /** |
1040 | * Get the namespace index, i.e. one of the NS_xxxx constants. |
1041 | * |
1042 | * @return int Namespace index |
1043 | */ |
1044 | public function getNamespace(): int { |
1045 | return $this->mNamespace; |
1046 | } |
1047 | |
1048 | /** |
1049 | * @param int $flags |
1050 | * |
1051 | * @return bool Whether $flags indicates that the latest information should be |
1052 | * read from the primary database, bypassing caches. |
1053 | */ |
1054 | private function shouldReadLatest( int $flags ) { |
1055 | return ( $flags & ( IDBAccessObject::READ_LATEST ) ) > 0; |
1056 | } |
1057 | |
1058 | /** |
1059 | * Get the page's content model id, see the CONTENT_MODEL_XXX constants. |
1060 | * |
1061 | * @todo Deprecate this in favor of SlotRecord::getModel() |
1062 | * |
1063 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
1064 | * @return string Content model id |
1065 | */ |
1066 | public function getContentModel( $flags = 0 ) { |
1067 | if ( $this->mForcedContentModel ) { |
1068 | if ( !$this->mContentModel ) { |
1069 | throw new RuntimeException( 'Got out of sync; an empty model is being forced' ); |
1070 | } |
1071 | // Content model is locked to the currently loaded one |
1072 | return $this->mContentModel; |
1073 | } |
1074 | |
1075 | if ( $this->shouldReadLatest( $flags ) || !$this->mContentModel ) { |
1076 | $this->lazyFillContentModel( $this->getFieldFromPageStore( 'page_content_model', $flags ) ); |
1077 | } |
1078 | |
1079 | if ( !$this->mContentModel ) { |
1080 | $slotRoleregistry = MediaWikiServices::getInstance()->getSlotRoleRegistry(); |
1081 | $mainSlotHandler = $slotRoleregistry->getRoleHandler( 'main' ); |
1082 | $this->lazyFillContentModel( $mainSlotHandler->getDefaultModel( $this ) ); |
1083 | } |
1084 | |
1085 | return $this->mContentModel; |
1086 | } |
1087 | |
1088 | /** |
1089 | * Convenience method for checking a title's content model name |
1090 | * |
1091 | * @param string $id The content model ID (use the CONTENT_MODEL_XXX constants). |
1092 | * @return bool True if $this->getContentModel() == $id |
1093 | */ |
1094 | public function hasContentModel( $id ) { |
1095 | return $this->getContentModel() == $id; |
1096 | } |
1097 | |
1098 | /** |
1099 | * Set a proposed content model for the page for permissions checking |
1100 | * |
1101 | * This does not actually change the content model of a title in the DB. |
1102 | * It only affects this particular Title instance. The content model is |
1103 | * forced to remain this value until another setContentModel() call. |
1104 | * |
1105 | * ContentHandler::canBeUsedOn() should be checked before calling this |
1106 | * if there is any doubt regarding the applicability of the content model |
1107 | * |
1108 | * @warning This must only be used if the caller controls the further use of |
1109 | * this Title object, to avoid other code unexpectedly using the new value. |
1110 | * |
1111 | * @since 1.28 |
1112 | * @param string $model CONTENT_MODEL_XXX constant |
1113 | */ |
1114 | public function setContentModel( $model ) { |
1115 | if ( (string)$model === '' ) { |
1116 | throw new InvalidArgumentException( "Missing CONTENT_MODEL_* constant" ); |
1117 | } |
1118 | |
1119 | $this->uncache(); |
1120 | $this->mContentModel = $model; |
1121 | $this->mForcedContentModel = true; |
1122 | } |
1123 | |
1124 | /** |
1125 | * If the content model field is not frozen then update it with a retrieved value |
1126 | * |
1127 | * @param string|bool $model CONTENT_MODEL_XXX constant or false |
1128 | */ |
1129 | private function lazyFillContentModel( $model ) { |
1130 | if ( !$this->mForcedContentModel ) { |
1131 | $this->mContentModel = ( $model === false ) ? false : (string)$model; |
1132 | } |
1133 | } |
1134 | |
1135 | /** |
1136 | * Get the namespace text |
1137 | * |
1138 | * @return string|false Namespace name with underscores (not spaces), e.g. 'User_talk' |
1139 | */ |
1140 | public function getNsText() { |
1141 | if ( $this->isExternal() ) { |
1142 | // This probably shouldn't even happen, except for interwiki transclusion. |
1143 | // If possible, use the canonical name for the foreign namespace. |
1144 | if ( $this->mNamespace === NS_MAIN ) { |
1145 | // Optimisation |
1146 | return ''; |
1147 | } else { |
1148 | $nsText = MediaWikiServices::getInstance()->getNamespaceInfo()-> |
1149 | getCanonicalName( $this->mNamespace ); |
1150 | if ( $nsText !== false ) { |
1151 | return $nsText; |
1152 | } |
1153 | } |
1154 | } |
1155 | |
1156 | try { |
1157 | $formatter = self::getTitleFormatter(); |
1158 | return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform ); |
1159 | } catch ( InvalidArgumentException $ex ) { |
1160 | wfDebug( __METHOD__ . ': ' . $ex->getMessage() ); |
1161 | return false; |
1162 | } |
1163 | } |
1164 | |
1165 | /** |
1166 | * Get the namespace text of the subject (rather than talk) page |
1167 | * |
1168 | * @return string Namespace name with underscores (not spaces) |
1169 | */ |
1170 | public function getSubjectNsText() { |
1171 | $services = MediaWikiServices::getInstance(); |
1172 | return $services->getContentLanguage()-> |
1173 | getNsText( $services->getNamespaceInfo()->getSubject( $this->mNamespace ) ); |
1174 | } |
1175 | |
1176 | /** |
1177 | * Get the namespace text of the talk page |
1178 | * |
1179 | * @return string Namespace name with underscores (not spaces) |
1180 | */ |
1181 | public function getTalkNsText() { |
1182 | $services = MediaWikiServices::getInstance(); |
1183 | return $services->getContentLanguage()-> |
1184 | getNsText( $services->getNamespaceInfo()->getTalk( $this->mNamespace ) ); |
1185 | } |
1186 | |
1187 | /** |
1188 | * Can this title have a corresponding talk page? |
1189 | * |
1190 | * False for relative section links (with getText() === ''), |
1191 | * interwiki links (with getInterwiki() !== ''), and pages in NS_SPECIAL. |
1192 | * |
1193 | * @see NamespaceInfo::canHaveTalkPage |
1194 | * @since 1.30 |
1195 | * |
1196 | * @return bool True if this title either is a talk page or can have a talk page associated. |
1197 | */ |
1198 | public function canHaveTalkPage() { |
1199 | return MediaWikiServices::getInstance()->getNamespaceInfo()->canHaveTalkPage( $this ); |
1200 | } |
1201 | |
1202 | /** |
1203 | * Can this title represent a page in the wiki's database? |
1204 | * |
1205 | * Titles can exist as pages in the database if they are valid, and they |
1206 | * are not Special pages, interwiki links, or fragment-only links. |
1207 | * |
1208 | * @see isValid() |
1209 | * |
1210 | * @return bool true if and only if this title can be used to perform an edit. |
1211 | */ |
1212 | public function canExist(): bool { |
1213 | // NOTE: Don't use getArticleID(), we don't want to |
1214 | // trigger a database query here. This check is supposed to |
1215 | // act as an optimization, not add extra cost. |
1216 | if ( $this->mArticleID > 0 ) { |
1217 | // It exists, so it can exist. |
1218 | return true; |
1219 | } |
1220 | |
1221 | // NOTE: we call the relatively expensive isValid() method further down, |
1222 | // but we can bail out early if we already know the title is invalid. |
1223 | if ( $this->mIsValid === false ) { |
1224 | // It's invalid, so it can't exist. |
1225 | return false; |
1226 | } |
1227 | |
1228 | if ( $this->getNamespace() < NS_MAIN ) { |
1229 | // It's a special page, so it can't exist in the database. |
1230 | return false; |
1231 | } |
1232 | |
1233 | if ( $this->isExternal() ) { |
1234 | // If it's external, it's not local, so it can't exist. |
1235 | return false; |
1236 | } |
1237 | |
1238 | if ( $this->getText() === '' ) { |
1239 | // The title has no text, so it can't exist in the database. |
1240 | // It's probably an on-page section link, like "#something". |
1241 | return false; |
1242 | } |
1243 | |
1244 | // Double check that the title is valid. |
1245 | return $this->isValid(); |
1246 | } |
1247 | |
1248 | /** |
1249 | * Returns true if this is a special page. |
1250 | * |
1251 | * @return bool |
1252 | */ |
1253 | public function isSpecialPage() { |
1254 | return $this->mNamespace === NS_SPECIAL; |
1255 | } |
1256 | |
1257 | /** |
1258 | * Returns true if this title resolves to the named special page |
1259 | * |
1260 | * @param string $name The special page name |
1261 | * @return bool |
1262 | */ |
1263 | public function isSpecial( $name ) { |
1264 | if ( $this->isSpecialPage() ) { |
1265 | [ $thisName, /* $subpage */ ] = |
1266 | MediaWikiServices::getInstance()->getSpecialPageFactory()-> |
1267 | resolveAlias( $this->mDbkeyform ); |
1268 | if ( $name == $thisName ) { |
1269 | return true; |
1270 | } |
1271 | } |
1272 | return false; |
1273 | } |
1274 | |
1275 | /** |
1276 | * If the Title refers to a special page alias which is not the local default, resolve |
1277 | * the alias, and localise the name as necessary. Otherwise, return $this |
1278 | * |
1279 | * @return Title |
1280 | */ |
1281 | public function fixSpecialName() { |
1282 | if ( $this->isSpecialPage() ) { |
1283 | $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory(); |
1284 | [ $canonicalName, $par ] = $spFactory->resolveAlias( $this->mDbkeyform ); |
1285 | if ( $canonicalName ) { |
1286 | $localName = $spFactory->getLocalNameFor( $canonicalName, $par ); |
1287 | if ( $localName != $this->mDbkeyform ) { |
1288 | return self::makeTitle( NS_SPECIAL, $localName ); |
1289 | } |
1290 | } |
1291 | } |
1292 | return $this; |
1293 | } |
1294 | |
1295 | /** |
1296 | * Returns true if the title is inside the specified namespace. |
1297 | * |
1298 | * @param int $ns The namespace |
1299 | * @return bool |
1300 | * @since 1.19 |
1301 | */ |
1302 | public function inNamespace( int $ns ): bool { |
1303 | return MediaWikiServices::getInstance()->getNamespaceInfo()-> |
1304 | equals( $this->mNamespace, $ns ); |
1305 | } |
1306 | |
1307 | /** |
1308 | * Returns true if the title is inside one of the specified namespaces. |
1309 | * |
1310 | * @param int|int[] ...$namespaces The namespaces to check for |
1311 | * @return bool |
1312 | * @since 1.19 |
1313 | */ |
1314 | public function inNamespaces( ...$namespaces ) { |
1315 | if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) { |
1316 | $namespaces = $namespaces[0]; |
1317 | } |
1318 | |
1319 | foreach ( $namespaces as $ns ) { |
1320 | if ( $this->inNamespace( $ns ) ) { |
1321 | return true; |
1322 | } |
1323 | } |
1324 | |
1325 | return false; |
1326 | } |
1327 | |
1328 | /** |
1329 | * Returns true if the title has the same subject namespace as the |
1330 | * namespace specified. |
1331 | * For example this method will take NS_USER and return true if namespace |
1332 | * is either NS_USER or NS_USER_TALK since both of them have NS_USER |
1333 | * as their subject namespace. |
1334 | * |
1335 | * This is MUCH simpler than individually testing for equivalence |
1336 | * against both NS_USER and NS_USER_TALK, and is also forward compatible. |
1337 | * @since 1.19 |
1338 | * @param int $ns |
1339 | * @return bool |
1340 | */ |
1341 | public function hasSubjectNamespace( $ns ) { |
1342 | return MediaWikiServices::getInstance()->getNamespaceInfo()-> |
1343 | subjectEquals( $this->mNamespace, $ns ); |
1344 | } |
1345 | |
1346 | /** |
1347 | * Is this Title in a namespace which contains content? |
1348 | * In other words, is this a content page, for the purposes of calculating |
1349 | * statistics, etc? |
1350 | * |
1351 | * @return bool |
1352 | */ |
1353 | public function isContentPage() { |
1354 | return MediaWikiServices::getInstance()->getNamespaceInfo()-> |
1355 | isContent( $this->mNamespace ); |
1356 | } |
1357 | |
1358 | /** |
1359 | * Would anybody with sufficient privileges be able to move this page? |
1360 | * Some pages just aren't movable. |
1361 | * |
1362 | * @return bool |
1363 | */ |
1364 | public function isMovable() { |
1365 | $services = MediaWikiServices::getInstance(); |
1366 | if ( |
1367 | !$services->getNamespaceInfo()-> |
1368 | isMovable( $this->mNamespace ) || $this->isExternal() |
1369 | ) { |
1370 | // Interwiki title or immovable namespace. Hooks don't get to override here |
1371 | return false; |
1372 | } |
1373 | |
1374 | $result = true; |
1375 | ( new HookRunner( $services->getHookContainer() ) )->onTitleIsMovable( $this, $result ); |
1376 | return $result; |
1377 | } |
1378 | |
1379 | /** |
1380 | * Is this the mainpage? |
1381 | * @see T302186 |
1382 | * |
1383 | * @since 1.18 |
1384 | * @return bool |
1385 | */ |
1386 | public function isMainPage() { |
1387 | /** @var Title|null */ |
1388 | static $cachedMainPage; |
1389 | if ( $cachedMainPage === null ) { |
1390 | $cachedMainPage = self::newMainPage(); |
1391 | } |
1392 | return $this->equals( $cachedMainPage ); |
1393 | } |
1394 | |
1395 | /** |
1396 | * Is this a subpage? |
1397 | * |
1398 | * @return bool |
1399 | */ |
1400 | public function isSubpage() { |
1401 | return MediaWikiServices::getInstance() |
1402 | ->getNamespaceInfo() |
1403 | ->hasSubpages( $this->mNamespace ) |
1404 | && str_contains( $this->getText(), '/' ); |
1405 | } |
1406 | |
1407 | /** |
1408 | * Is this a conversion table for the LanguageConverter? |
1409 | * |
1410 | * @return bool |
1411 | */ |
1412 | public function isConversionTable() { |
1413 | // @todo ConversionTable should become a separate content model. |
1414 | // @todo And the prefix should be localized, too! |
1415 | |
1416 | return $this->mNamespace === NS_MEDIAWIKI && |
1417 | str_starts_with( $this->getText(), 'Conversiontable/' ); |
1418 | } |
1419 | |
1420 | /** |
1421 | * Does that page contain wikitext, or it is JS, CSS or whatever? |
1422 | * |
1423 | * @return bool |
1424 | */ |
1425 | public function isWikitextPage() { |
1426 | return $this->hasContentModel( CONTENT_MODEL_WIKITEXT ); |
1427 | } |
1428 | |
1429 | /** |
1430 | * Could this MediaWiki namespace page contain custom CSS, JSON, or JavaScript for the |
1431 | * global UI. This is generally true for pages in the MediaWiki namespace having |
1432 | * CONTENT_MODEL_CSS, CONTENT_MODEL_JSON, or CONTENT_MODEL_JAVASCRIPT. |
1433 | * |
1434 | * This method does *not* return true for per-user JS/JSON/CSS. Use isUserConfigPage() |
1435 | * for that! |
1436 | * |
1437 | * Note that this method should not return true for pages that contain and show |
1438 | * "inactive" CSS, JSON, or JS. |
1439 | * |
1440 | * @return bool |
1441 | * @since 1.31 |
1442 | */ |
1443 | public function isSiteConfigPage() { |
1444 | return ( |
1445 | $this->isSiteCssConfigPage() |
1446 | || $this->isSiteJsonConfigPage() |
1447 | || $this->isSiteJsConfigPage() |
1448 | ); |
1449 | } |
1450 | |
1451 | /** |
1452 | * Is this a "config" (.css, .json, or .js) sub-page of a user page? |
1453 | * |
1454 | * @return bool |
1455 | * @since 1.31 |
1456 | */ |
1457 | public function isUserConfigPage() { |
1458 | return ( |
1459 | $this->isUserCssConfigPage() |
1460 | || $this->isUserJsonConfigPage() |
1461 | || $this->isUserJsConfigPage() |
1462 | ); |
1463 | } |
1464 | |
1465 | /** |
1466 | * Trim down a .css, .json, or .js subpage title to get the corresponding skin name |
1467 | * |
1468 | * @return string Containing skin name from .css, .json, or .js subpage title |
1469 | * @since 1.31 |
1470 | */ |
1471 | public function getSkinFromConfigSubpage() { |
1472 | $text = $this->getText(); |
1473 | $lastSlashPos = $this->findSubpageDivider( $text, -1 ); |
1474 | if ( $lastSlashPos === false ) { |
1475 | return ''; |
1476 | } |
1477 | |
1478 | $lastDot = strrpos( $text, '.', $lastSlashPos ); |
1479 | if ( $lastDot === false ) { |
1480 | return ''; |
1481 | } |
1482 | |
1483 | return substr( $text, $lastSlashPos + 1, $lastDot - $lastSlashPos - 1 ); |
1484 | } |
1485 | |
1486 | /** |
1487 | * Is this a CSS "config" sub-page of a user page? |
1488 | * |
1489 | * @return bool |
1490 | * @since 1.31 |
1491 | */ |
1492 | public function isUserCssConfigPage() { |
1493 | return ( |
1494 | $this->mNamespace === NS_USER |
1495 | && $this->isSubpage() |
1496 | && $this->hasContentModel( CONTENT_MODEL_CSS ) |
1497 | ); |
1498 | } |
1499 | |
1500 | /** |
1501 | * Is this a JSON "config" sub-page of a user page? |
1502 | * |
1503 | * @return bool |
1504 | * @since 1.31 |
1505 | */ |
1506 | public function isUserJsonConfigPage() { |
1507 | return ( |
1508 | $this->mNamespace === NS_USER |
1509 | && $this->isSubpage() |
1510 | && $this->hasContentModel( CONTENT_MODEL_JSON ) |
1511 | ); |
1512 | } |
1513 | |
1514 | /** |
1515 | * Is this a JS "config" sub-page of a user page? |
1516 | * |
1517 | * @return bool |
1518 | * @since 1.31 |
1519 | */ |
1520 | public function isUserJsConfigPage() { |
1521 | return ( |
1522 | $this->mNamespace === NS_USER |
1523 | && $this->isSubpage() |
1524 | && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) |
1525 | ); |
1526 | } |
1527 | |
1528 | /** |
1529 | * Is this a sitewide CSS "config" page? |
1530 | * |
1531 | * @return bool |
1532 | * @since 1.32 |
1533 | */ |
1534 | public function isSiteCssConfigPage() { |
1535 | return ( |
1536 | $this->mNamespace === NS_MEDIAWIKI |
1537 | && ( |
1538 | $this->hasContentModel( CONTENT_MODEL_CSS ) |
1539 | // paranoia - a MediaWiki: namespace page with mismatching extension and content |
1540 | // model is probably by mistake and might get handled incorrectly (see e.g. T112937) |
1541 | || str_ends_with( $this->mDbkeyform, '.css' ) |
1542 | ) |
1543 | ); |
1544 | } |
1545 | |
1546 | /** |
1547 | * Is this a sitewide JSON "config" page? |
1548 | * |
1549 | * @return bool |
1550 | * @since 1.32 |
1551 | */ |
1552 | public function isSiteJsonConfigPage() { |
1553 | return ( |
1554 | $this->mNamespace === NS_MEDIAWIKI |
1555 | && ( |
1556 | $this->hasContentModel( CONTENT_MODEL_JSON ) |
1557 | // paranoia - a MediaWiki: namespace page with mismatching extension and content |
1558 | // model is probably by mistake and might get handled incorrectly (see e.g. T112937) |
1559 | || str_ends_with( $this->mDbkeyform, '.json' ) |
1560 | ) |
1561 | ); |
1562 | } |
1563 | |
1564 | /** |
1565 | * Is this a sitewide JS "config" page? |
1566 | * |
1567 | * @return bool |
1568 | * @since 1.31 |
1569 | */ |
1570 | public function isSiteJsConfigPage() { |
1571 | return ( |
1572 | $this->mNamespace === NS_MEDIAWIKI |
1573 | && ( |
1574 | $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) |
1575 | // paranoia - a MediaWiki: namespace page with mismatching extension and content |
1576 | // model is probably by mistake and might get handled incorrectly (see e.g. T112937) |
1577 | || str_ends_with( $this->mDbkeyform, '.js' ) |
1578 | ) |
1579 | ); |
1580 | } |
1581 | |
1582 | /** |
1583 | * Is this a message which can contain raw HTML? |
1584 | * |
1585 | * @return bool |
1586 | * @since 1.32 |
1587 | */ |
1588 | public function isRawHtmlMessage() { |
1589 | global $wgRawHtmlMessages; |
1590 | |
1591 | if ( !$this->inNamespace( NS_MEDIAWIKI ) ) { |
1592 | return false; |
1593 | } |
1594 | $message = lcfirst( $this->getRootTitle()->getDBkey() ); |
1595 | return in_array( $message, $wgRawHtmlMessages, true ); |
1596 | } |
1597 | |
1598 | /** |
1599 | * Is this a talk page of some sort? |
1600 | * |
1601 | * @return bool |
1602 | */ |
1603 | public function isTalkPage() { |
1604 | return MediaWikiServices::getInstance()->getNamespaceInfo()-> |
1605 | isTalk( $this->mNamespace ); |
1606 | } |
1607 | |
1608 | /** |
1609 | * Get a Title object associated with the talk page of this article |
1610 | * |
1611 | * @deprecated since 1.34, use getTalkPageIfDefined() or NamespaceInfo::getTalkPage() |
1612 | * with NamespaceInfo::canHaveTalkPage(). Note that the new method will |
1613 | * throw if asked for the talk page of a section-only link, or of an interwiki |
1614 | * link. |
1615 | * @return Title The object for the talk page |
1616 | * @throws MWException if $target doesn't have talk pages, e.g. because it's in NS_SPECIAL |
1617 | * or because it's a relative link, or an interwiki link. |
1618 | */ |
1619 | public function getTalkPage() { |
1620 | // NOTE: The equivalent code in NamespaceInfo is less lenient about producing invalid titles. |
1621 | // Instead of failing on invalid titles, let's just log the issue for now. |
1622 | // See the discussion on T227817. |
1623 | |
1624 | // Is this the same title? |
1625 | $talkNS = MediaWikiServices::getInstance()->getNamespaceInfo()->getTalk( $this->mNamespace ); |
1626 | if ( $this->mNamespace == $talkNS ) { |
1627 | return $this; |
1628 | } |
1629 | |
1630 | $title = self::makeTitle( $talkNS, $this->mDbkeyform ); |
1631 | |
1632 | $this->warnIfPageCannotExist( $title, __METHOD__ ); |
1633 | |
1634 | return $title; |
1635 | // TODO: replace the above with the code below: |
1636 | // return self::castFromLinkTarget( |
1637 | // MediaWikiServices::getInstance()->getNamespaceInfo()->getTalkPage( $this ) ); |
1638 | } |
1639 | |
1640 | /** |
1641 | * Get a Title object associated with the talk page of this article, |
1642 | * if such a talk page can exist. |
1643 | * |
1644 | * @since 1.30 |
1645 | * |
1646 | * @return Title|null The object for the talk page, |
1647 | * or null if no associated talk page can exist, according to canHaveTalkPage(). |
1648 | */ |
1649 | public function getTalkPageIfDefined() { |
1650 | if ( !$this->canHaveTalkPage() ) { |
1651 | return null; |
1652 | } |
1653 | |
1654 | return $this->getTalkPage(); |
1655 | } |
1656 | |
1657 | /** |
1658 | * Get a title object associated with the subject page of this |
1659 | * talk page |
1660 | * |
1661 | * @deprecated since 1.34, use NamespaceInfo::getSubjectPage |
1662 | * @return Title The object for the subject page |
1663 | */ |
1664 | public function getSubjectPage() { |
1665 | // Is this the same title? |
1666 | $subjectNS = MediaWikiServices::getInstance()->getNamespaceInfo() |
1667 | ->getSubject( $this->mNamespace ); |
1668 | if ( $this->mNamespace == $subjectNS ) { |
1669 | return $this; |
1670 | } |
1671 | // NOTE: The equivalent code in NamespaceInfo is less lenient about producing invalid titles. |
1672 | // Instead of failing on invalid titles, let's just log the issue for now. |
1673 | // See the discussion on T227817. |
1674 | $title = self::makeTitle( $subjectNS, $this->mDbkeyform ); |
1675 | |
1676 | $this->warnIfPageCannotExist( $title, __METHOD__ ); |
1677 | |
1678 | return $title; |
1679 | // TODO: replace the above with the code below: |
1680 | // return self::castFromLinkTarget( |
1681 | // MediaWikiServices::getInstance()->getNamespaceInfo()->getSubjectPage( $this ) ); |
1682 | } |
1683 | |
1684 | /** |
1685 | * @param Title $title |
1686 | * @param string $method |
1687 | * |
1688 | * @return bool whether a warning was issued |
1689 | */ |
1690 | private function warnIfPageCannotExist( Title $title, $method ) { |
1691 | if ( $this->getText() == '' ) { |
1692 | wfLogWarning( |
1693 | $method . ': called on empty title ' . $this->getFullText() . ', returning ' |
1694 | . $title->getFullText() |
1695 | ); |
1696 | |
1697 | return true; |
1698 | } |
1699 | |
1700 | if ( $this->getInterwiki() !== '' ) { |
1701 | wfLogWarning( |
1702 | $method . ': called on interwiki title ' . $this->getFullText() . ', returning ' |
1703 | . $title->getFullText() |
1704 | ); |
1705 | |
1706 | return true; |
1707 | } |
1708 | |
1709 | return false; |
1710 | } |
1711 | |
1712 | /** |
1713 | * Get the other title for this page, if this is a subject page |
1714 | * get the talk page, if it is a subject page get the talk page |
1715 | * |
1716 | * @deprecated since 1.34, use NamespaceInfo::getAssociatedPage |
1717 | * @since 1.25 |
1718 | * @throws MWException If the page doesn't have an other page |
1719 | * @return Title |
1720 | */ |
1721 | public function getOtherPage() { |
1722 | // NOTE: Depend on the methods in this class instead of their equivalent in NamespaceInfo, |
1723 | // until their semantics has become exactly the same. |
1724 | // See the discussion on T227817. |
1725 | if ( $this->isSpecialPage() ) { |
1726 | throw new MWException( 'Special pages cannot have other pages' ); |
1727 | } |
1728 | if ( $this->isTalkPage() ) { |
1729 | return $this->getSubjectPage(); |
1730 | } else { |
1731 | if ( !$this->canHaveTalkPage() ) { |
1732 | throw new MWException( "{$this->getPrefixedText()} does not have an other page" ); |
1733 | } |
1734 | return $this->getTalkPage(); |
1735 | } |
1736 | // TODO: replace the above with the code below: |
1737 | // return self::castFromLinkTarget( |
1738 | // MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociatedPage( $this ) ); |
1739 | } |
1740 | |
1741 | /** |
1742 | * Get the Title fragment (i.e.\ the bit after the #) in text form |
1743 | * |
1744 | * Use Title::hasFragment to check for a fragment |
1745 | * |
1746 | * @return string Title fragment |
1747 | */ |
1748 | public function getFragment(): string { |
1749 | return $this->mFragment; |
1750 | } |
1751 | |
1752 | /** |
1753 | * Get the fragment in URL form, including the "#" character if there is one |
1754 | * |
1755 | * @return string Fragment in URL form |
1756 | */ |
1757 | public function getFragmentForURL() { |
1758 | if ( !$this->hasFragment() ) { |
1759 | return ''; |
1760 | } elseif ( $this->isExternal() ) { |
1761 | // Note: If the interwiki is unknown, it's treated as a namespace on the local wiki, |
1762 | // so we treat it like a local interwiki. |
1763 | $interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki ); |
1764 | if ( $interwiki && !$interwiki->isLocal() ) { |
1765 | return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->mFragment ); |
1766 | } |
1767 | } |
1768 | |
1769 | return '#' . Sanitizer::escapeIdForLink( $this->mFragment ); |
1770 | } |
1771 | |
1772 | /** |
1773 | * Set the fragment for this title. Removes the first character from the |
1774 | * specified fragment before setting, so it assumes you're passing it with |
1775 | * an initial "#". |
1776 | * |
1777 | * @warning This must only be used if the caller controls the further use of |
1778 | * this Title object, to avoid other code unexpectedly using the new value. |
1779 | * |
1780 | * @param string $fragment Text |
1781 | */ |
1782 | public function setFragment( $fragment ) { |
1783 | $this->uncache(); |
1784 | $this->mFragment = self::normalizeFragment( $fragment ); |
1785 | } |
1786 | |
1787 | /** |
1788 | * Creates a new Title for a different fragment of the same page. |
1789 | * |
1790 | * @since 1.27 |
1791 | * @param string $fragment |
1792 | * @return Title |
1793 | */ |
1794 | public function createFragmentTarget( string $fragment ): self { |
1795 | return self::makeTitle( |
1796 | $this->mNamespace, |
1797 | $this->getText(), |
1798 | $fragment, |
1799 | $this->mInterwiki |
1800 | ); |
1801 | } |
1802 | |
1803 | /** |
1804 | * Normalizes fragment part of the title. |
1805 | * |
1806 | * @param string $fragment |
1807 | * @return string |
1808 | */ |
1809 | private static function normalizeFragment( $fragment ) { |
1810 | if ( str_starts_with( $fragment, '#' ) ) { |
1811 | $fragment = substr( $fragment, 1 ); |
1812 | } |
1813 | return strtr( $fragment, '_', ' ' ); |
1814 | } |
1815 | |
1816 | /** |
1817 | * Prefix some arbitrary text with the namespace or interwiki prefix |
1818 | * of this object |
1819 | * |
1820 | * @param string $name The text |
1821 | * @return string The prefixed text |
1822 | */ |
1823 | private function prefix( $name ) { |
1824 | $p = ''; |
1825 | if ( $this->isExternal() ) { |
1826 | $p = $this->mInterwiki . ':'; |
1827 | } |
1828 | |
1829 | if ( $this->mNamespace != 0 ) { |
1830 | $nsText = $this->getNsText(); |
1831 | |
1832 | if ( $nsText === false ) { |
1833 | // See T165149. Awkward, but better than erroneously linking to the main namespace. |
1834 | $nsText = MediaWikiServices::getInstance()->getContentLanguage()-> |
1835 | getNsText( NS_SPECIAL ) . ":Badtitle/NS{$this->mNamespace}"; |
1836 | } |
1837 | |
1838 | $p .= $nsText . ':'; |
1839 | } |
1840 | return $p . $name; |
1841 | } |
1842 | |
1843 | /** |
1844 | * Get the prefixed database key form |
1845 | * |
1846 | * @return string The prefixed title, with underscores and |
1847 | * any interwiki and namespace prefixes |
1848 | */ |
1849 | public function getPrefixedDBkey() { |
1850 | $s = $this->prefix( $this->mDbkeyform ); |
1851 | $s = strtr( $s, ' ', '_' ); |
1852 | return $s; |
1853 | } |
1854 | |
1855 | /** |
1856 | * Get the prefixed title with spaces. |
1857 | * This is the form usually used for display |
1858 | * |
1859 | * @return string The prefixed title, with spaces |
1860 | */ |
1861 | public function getPrefixedText() { |
1862 | if ( $this->prefixedText === null ) { |
1863 | $s = $this->prefix( $this->mTextform ); |
1864 | $s = strtr( $s, '_', ' ' ); |
1865 | $this->prefixedText = $s; |
1866 | } |
1867 | return $this->prefixedText; |
1868 | } |
1869 | |
1870 | /** |
1871 | * Return a string representation of this title |
1872 | * |
1873 | * @return string Representation of this title |
1874 | */ |
1875 | public function __toString(): string { |
1876 | return $this->getPrefixedText(); |
1877 | } |
1878 | |
1879 | /** |
1880 | * Get the prefixed title with spaces, plus any fragment |
1881 | * (part beginning with '#') |
1882 | * |
1883 | * @return string The prefixed title, with spaces and the fragment, including '#' |
1884 | */ |
1885 | public function getFullText() { |
1886 | $text = $this->getPrefixedText(); |
1887 | if ( $this->hasFragment() ) { |
1888 | $text .= '#' . $this->mFragment; |
1889 | } |
1890 | return $text; |
1891 | } |
1892 | |
1893 | /** |
1894 | * Finds the first or last subpage divider (slash) in the string. |
1895 | * Any leading sequence of slashes is ignored, since it does not divide |
1896 | * two parts of the string. Considering leading slashes dividers would |
1897 | * result in empty root title or base title (T229443). |
1898 | * |
1899 | * Note that trailing slashes are considered dividers, and empty subpage |
1900 | * names are allowed. |
1901 | * |
1902 | * @param string $text |
1903 | * @param int $dir -1 for the last or +1 for the first divider. |
1904 | * |
1905 | * @return false|int |
1906 | */ |
1907 | private function findSubpageDivider( $text, $dir ) { |
1908 | if ( $dir > 0 ) { |
1909 | // Skip leading slashes, but keep the last one when there is nothing but slashes |
1910 | $bottom = strspn( $text, '/', 0, -1 ); |
1911 | $idx = strpos( $text, '/', $bottom ); |
1912 | } else { |
1913 | // Any slash from the end can be a divider, as subpage names can be empty |
1914 | $idx = strrpos( $text, '/' ); |
1915 | } |
1916 | |
1917 | // The first character can never be a divider, as that would result in an empty base |
1918 | return $idx === 0 ? false : $idx; |
1919 | } |
1920 | |
1921 | /** |
1922 | * Whether this Title's namespace has subpages enabled. |
1923 | * @return bool |
1924 | */ |
1925 | private function hasSubpagesEnabled() { |
1926 | return MediaWikiServices::getInstance()->getNamespaceInfo()-> |
1927 | hasSubpages( $this->mNamespace ); |
1928 | } |
1929 | |
1930 | /** |
1931 | * Get the root page name text without a namespace, i.e. the leftmost part before any slashes |
1932 | * |
1933 | * @note the return value may contain trailing whitespace and is thus |
1934 | * not safe for use with makeTitle or TitleValue. |
1935 | * |
1936 | * @par Example: |
1937 | * @code |
1938 | * Title::newFromText('User:Foo/Bar/Baz')->getRootText(); |
1939 | * # returns: 'Foo' |
1940 | * @endcode |
1941 | * |
1942 | * @return string Root name |
1943 | * @since 1.20 |
1944 | */ |
1945 | public function getRootText() { |
1946 | $text = $this->getText(); |
1947 | if ( !$this->hasSubpagesEnabled() ) { |
1948 | return $text; |
1949 | } |
1950 | |
1951 | $firstSlashPos = $this->findSubpageDivider( $text, +1 ); |
1952 | // Don't discard the real title if there's no subpage involved |
1953 | if ( $firstSlashPos === false ) { |
1954 | return $text; |
1955 | } |
1956 | |
1957 | return substr( $text, 0, $firstSlashPos ); |
1958 | } |
1959 | |
1960 | /** |
1961 | * Get the root page name title, i.e. the leftmost part before any slashes |
1962 | * |
1963 | * @par Example: |
1964 | * @code |
1965 | * Title::newFromText('User:Foo/Bar/Baz')->getRootTitle(); |
1966 | * # returns: Title{User:Foo} |
1967 | * @endcode |
1968 | * |
1969 | * @return Title |
1970 | * @since 1.20 |
1971 | */ |
1972 | public function getRootTitle() { |
1973 | $title = self::makeTitleSafe( $this->mNamespace, $this->getRootText() ); |
1974 | |
1975 | if ( !$title ) { |
1976 | if ( !$this->isValid() ) { |
1977 | // If the title wasn't valid in the first place, we can't expect |
1978 | // to successfully parse it. T290194 |
1979 | return $this; |
1980 | } |
1981 | |
1982 | Assert::postcondition( |
1983 | $title !== null, |
1984 | 'makeTitleSafe() should always return a Title for the text ' . |
1985 | 'returned by getRootText().' |
1986 | ); |
1987 | } |
1988 | |
1989 | return $title; |
1990 | } |
1991 | |
1992 | /** |
1993 | * Get the base page name without a namespace, i.e. the part before the subpage name |
1994 | * |
1995 | * @note the return value may contain trailing whitespace and is thus |
1996 | * not safe for use with makeTitle or TitleValue. |
1997 | * |
1998 | * @par Example: |
1999 | * @code |
2000 | * Title::newFromText('User:Foo/Bar/Baz')->getBaseText(); |
2001 | * # returns: 'Foo/Bar' |
2002 | * @endcode |
2003 | * |
2004 | * @return string Base name |
2005 | */ |
2006 | public function getBaseText() { |
2007 | $text = $this->getText(); |
2008 | if ( !$this->hasSubpagesEnabled() ) { |
2009 | return $text; |
2010 | } |
2011 | |
2012 | $lastSlashPos = $this->findSubpageDivider( $text, -1 ); |
2013 | // Don't discard the real title if there's no subpage involved |
2014 | if ( $lastSlashPos === false ) { |
2015 | return $text; |
2016 | } |
2017 | |
2018 | return substr( $text, 0, $lastSlashPos ); |
2019 | } |
2020 | |
2021 | /** |
2022 | * Get the base page name title, i.e. the part before the subpage name. |
2023 | * |
2024 | * @par Example: |
2025 | * @code |
2026 | * Title::newFromText('User:Foo/Bar/Baz')->getBaseTitle(); |
2027 | * # returns: Title{User:Foo/Bar} |
2028 | * @endcode |
2029 | * |
2030 | * @return Title |
2031 | * @since 1.20 |
2032 | */ |
2033 | public function getBaseTitle() { |
2034 | $title = self::makeTitleSafe( $this->mNamespace, $this->getBaseText() ); |
2035 | |
2036 | if ( !$title ) { |
2037 | if ( !$this->isValid() ) { |
2038 | // If the title wasn't valid in the first place, we can't expect |
2039 | // to successfully parse it. T290194 |
2040 | return $this; |
2041 | } |
2042 | |
2043 | Assert::postcondition( |
2044 | $title !== null, |
2045 | 'makeTitleSafe() should always return a Title for the text ' . |
2046 | 'returned by getBaseText().' |
2047 | ); |
2048 | } |
2049 | |
2050 | return $title; |
2051 | } |
2052 | |
2053 | /** |
2054 | * Get the lowest-level subpage name, i.e. the rightmost part after any slashes |
2055 | * |
2056 | * @par Example: |
2057 | * @code |
2058 | * Title::newFromText('User:Foo/Bar/Baz')->getSubpageText(); |
2059 | * # returns: "Baz" |
2060 | * @endcode |
2061 | * |
2062 | * @return string Subpage name |
2063 | */ |
2064 | public function getSubpageText() { |
2065 | $text = $this->getText(); |
2066 | if ( !$this->hasSubpagesEnabled() ) { |
2067 | return $text; |
2068 | } |
2069 | |
2070 | $lastSlashPos = $this->findSubpageDivider( $text, -1 ); |
2071 | if ( $lastSlashPos === false ) { |
2072 | // T256922 - Return the title text if no subpages |
2073 | return $text; |
2074 | } |
2075 | return substr( $text, $lastSlashPos + 1 ); |
2076 | } |
2077 | |
2078 | /** |
2079 | * Get the title for a subpage of the current page |
2080 | * |
2081 | * @par Example: |
2082 | * @code |
2083 | * Title::newFromText('User:Foo/Bar/Baz')->getSubpage("Asdf"); |
2084 | * # returns: Title{User:Foo/Bar/Baz/Asdf} |
2085 | * @endcode |
2086 | * |
2087 | * @param string $text The subpage name to add to the title |
2088 | * @return Title|null Subpage title, or null on an error |
2089 | * @since 1.20 |
2090 | */ |
2091 | public function getSubpage( $text ) { |
2092 | return self::makeTitleSafe( |
2093 | $this->mNamespace, |
2094 | $this->getText() . '/' . $text, |
2095 | '', |
2096 | $this->mInterwiki |
2097 | ); |
2098 | } |
2099 | |
2100 | /** |
2101 | * Get a URL-encoded form of the subpage text |
2102 | * |
2103 | * @return string URL-encoded subpage name |
2104 | */ |
2105 | public function getSubpageUrlForm() { |
2106 | $text = $this->getSubpageText(); |
2107 | $text = wfUrlencode( strtr( $text, ' ', '_' ) ); |
2108 | return $text; |
2109 | } |
2110 | |
2111 | /** |
2112 | * Get a URL-encoded title (not an actual URL) including interwiki |
2113 | * |
2114 | * @return string The URL-encoded form |
2115 | */ |
2116 | public function getPrefixedURL() { |
2117 | $s = $this->prefix( $this->mDbkeyform ); |
2118 | $s = wfUrlencode( strtr( $s, ' ', '_' ) ); |
2119 | return $s; |
2120 | } |
2121 | |
2122 | /** |
2123 | * Get a real URL referring to this title, with interwiki link and |
2124 | * fragment |
2125 | * |
2126 | * @see self::getLocalURL for the arguments. |
2127 | * @see \MediaWiki\Utils\UrlUtils::expand() |
2128 | * @param string|array $query |
2129 | * @param false $query2 deprecated since MW 1.19; ignored since MW 1.41 |
2130 | * @param string|int|null $proto Protocol type to use in URL |
2131 | * @return string The URL |
2132 | */ |
2133 | public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) { |
2134 | $services = MediaWikiServices::getInstance(); |
2135 | |
2136 | $query = is_array( $query ) ? wfArrayToCgi( $query ) : $query; |
2137 | |
2138 | # Hand off all the decisions on urls to getLocalURL |
2139 | $url = $this->getLocalURL( $query ); |
2140 | |
2141 | # Expand the url to make it a full url. Note that getLocalURL has the |
2142 | # potential to output full urls for a variety of reasons, so we use |
2143 | # UrlUtils::expand() instead of simply prepending $wgServer |
2144 | $url = (string)$services->getUrlUtils()->expand( $url, $proto ); |
2145 | |
2146 | # Finally, add the fragment. |
2147 | $url .= $this->getFragmentForURL(); |
2148 | ( new HookRunner( $services->getHookContainer() ) )->onGetFullURL( $this, $url, $query ); |
2149 | return $url; |
2150 | } |
2151 | |
2152 | /** |
2153 | * Get a url appropriate for making redirects based on an untrusted url arg |
2154 | * |
2155 | * This is basically the same as getFullUrl(), but in the case of external |
2156 | * interwikis, we send the user to a landing page, to prevent possible |
2157 | * phishing attacks and the like. |
2158 | * |
2159 | * @note Uses current protocol by default, since technically relative urls |
2160 | * aren't allowed in redirects per HTTP spec, so this is not suitable for |
2161 | * places where the url gets cached, as might pollute between |
2162 | * https and non-https users. |
2163 | * @see self::getLocalURL for the arguments. |
2164 | * @param array|string $query |
2165 | * @param string $proto Protocol type to use in URL |
2166 | * @return string A url suitable to use in an HTTP location header. |
2167 | */ |
2168 | public function getFullUrlForRedirect( $query = '', $proto = PROTO_CURRENT ) { |
2169 | $target = $this; |
2170 | if ( $this->isExternal() ) { |
2171 | $target = SpecialPage::getTitleFor( |
2172 | 'GoToInterwiki', |
2173 | $this->getPrefixedDBkey() |
2174 | ); |
2175 | } |
2176 | return $target->getFullURL( $query, false, $proto ); |
2177 | } |
2178 | |
2179 | /** |
2180 | * Get a URL with no fragment or server name (relative URL) from a Title object. |
2181 | * If this page is generated with action=render, however, |
2182 | * $wgServer is prepended to make an absolute URL. |
2183 | * |
2184 | * @see self::getFullURL to always get an absolute URL. |
2185 | * @see self::getLinkURL to always get a URL that's the simplest URL that will be |
2186 | * valid to link, locally, to the current Title. |
2187 | * @see self::newFromText to produce a Title object. |
2188 | * |
2189 | * @param string|array $query An optional query string, |
2190 | * not used for interwiki links. Can be specified as an associative array as well, |
2191 | * e.g., [ 'action' => 'edit' ] (keys and values will be URL-escaped). |
2192 | * Some query patterns will trigger various shorturl path replacements. |
2193 | * |
2194 | * @return string |
2195 | */ |
2196 | public function getLocalURL( $query = '' ) { |
2197 | global $wgArticlePath, $wgScript, $wgMainPageIsDomainRoot; |
2198 | |
2199 | $query = is_array( $query ) ? wfArrayToCgi( $query ) : $query; |
2200 | |
2201 | $services = MediaWikiServices::getInstance(); |
2202 | $hookRunner = new HookRunner( $services->getHookContainer() ); |
2203 | $interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki ); |
2204 | if ( $interwiki ) { |
2205 | $namespace = $this->getNsText(); |
2206 | if ( $namespace != '' ) { |
2207 | # Can this actually happen? Interwikis shouldn't be parsed. |
2208 | # Yes! It can in interwiki transclusion. But... it probably shouldn't. |
2209 | $namespace .= ':'; |
2210 | } |
2211 | $url = $interwiki->getURL( $namespace . $this->mDbkeyform ); |
2212 | $url = wfAppendQuery( $url, $query ); |
2213 | } else { |
2214 | $dbkey = wfUrlencode( $this->getPrefixedDBkey() ); |
2215 | if ( $query == '' ) { |
2216 | if ( $wgMainPageIsDomainRoot && $this->isMainPage() ) { |
2217 | $url = '/'; |
2218 | } else { |
2219 | $url = str_replace( '$1', $dbkey, $wgArticlePath ); |
2220 | } |
2221 | $hookRunner->onGetLocalURL__Article( $this, $url ); |
2222 | } else { |
2223 | global $wgVariantArticlePath, $wgActionPaths; |
2224 | $url = false; |
2225 | $matches = []; |
2226 | |
2227 | $articlePaths = PathRouter::getActionPaths( $wgActionPaths, $wgArticlePath ); |
2228 | |
2229 | if ( $articlePaths |
2230 | && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) |
2231 | ) { |
2232 | $action = urldecode( $matches[2] ); |
2233 | if ( isset( $articlePaths[$action] ) ) { |
2234 | $query = $matches[1]; |
2235 | if ( isset( $matches[4] ) ) { |
2236 | $query .= $matches[4]; |
2237 | } |
2238 | $url = str_replace( '$1', $dbkey, $articlePaths[$action] ); |
2239 | if ( $query != '' ) { |
2240 | $url = wfAppendQuery( $url, $query ); |
2241 | } |
2242 | } |
2243 | } |
2244 | |
2245 | if ( $url === false |
2246 | && $wgVariantArticlePath |
2247 | && preg_match( '/^variant=([^&]*)$/', $query, $matches ) |
2248 | && $this->getPageLanguage()->equals( $services->getContentLanguage() ) |
2249 | && $this->getPageLanguageConverter()->hasVariants() |
2250 | ) { |
2251 | $variant = urldecode( $matches[1] ); |
2252 | if ( $this->getPageLanguageConverter()->hasVariant( $variant ) ) { |
2253 | // Only do the variant replacement if the given variant is a valid |
2254 | // variant for the page's language. |
2255 | $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath ); |
2256 | $url = str_replace( '$1', $dbkey, $url ); |
2257 | } |
2258 | } |
2259 | |
2260 | if ( $url === false ) { |
2261 | if ( $query == '-' ) { |
2262 | $query = ''; |
2263 | } |
2264 | $url = "{$wgScript}?title={$dbkey}&{$query}"; |
2265 | } |
2266 | } |
2267 | $hookRunner->onGetLocalURL__Internal( $this, $url, $query ); |
2268 | } |
2269 | |
2270 | $hookRunner->onGetLocalURL( $this, $url, $query ); |
2271 | return $url; |
2272 | } |
2273 | |
2274 | /** |
2275 | * Get a URL that's the simplest URL that will be valid to link, locally, |
2276 | * to the current Title. It includes the fragment, but does not include |
2277 | * the server unless action=render is used (or the link is external). If |
2278 | * there's a fragment but the prefixed text is empty, we just return a link |
2279 | * to the fragment. |
2280 | * |
2281 | * The result obviously should not be URL-escaped, but does need to be |
2282 | * HTML-escaped if it's being output in HTML. |
2283 | * |
2284 | * @param string|array $query |
2285 | * @param string|string[]|false $query2 deprecated since MW 1.19; ignored since MW 1.41 |
2286 | * @param string|int|false $proto A PROTO_* constant on how the URL should be expanded, |
2287 | * or false (default) for no expansion |
2288 | * @see self::getLocalURL for the arguments. |
2289 | * @return string The URL |
2290 | */ |
2291 | public function getLinkURL( $query = '', $query2 = false, $proto = false ) { |
2292 | if ( $this->isExternal() || $proto !== false ) { |
2293 | $ret = $this->getFullURL( $query, false, $proto ); |
2294 | } elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) { |
2295 | $ret = $this->getFragmentForURL(); |
2296 | } else { |
2297 | $ret = $this->getLocalURL( $query ) . $this->getFragmentForURL(); |
2298 | } |
2299 | return $ret; |
2300 | } |
2301 | |
2302 | /** |
2303 | * Get the URL form for an internal link. |
2304 | * - Used in various CDN-related code, in case we have a different |
2305 | * internal hostname for the server from the exposed one. |
2306 | * |
2307 | * This uses $wgInternalServer to qualify the path, or $wgServer |
2308 | * if $wgInternalServer is not set. If the server variable used is |
2309 | * protocol-relative, the URL will be expanded to http:// |
2310 | * |
2311 | * @see self::getLocalURL for the arguments. |
2312 | * @param string|array $query |
2313 | * @return string The URL |
2314 | */ |
2315 | public function getInternalURL( $query = '' ) { |
2316 | global $wgInternalServer, $wgServer; |
2317 | $services = MediaWikiServices::getInstance(); |
2318 | |
2319 | $query = is_array( $query ) ? wfArrayToCgi( $query ) : $query; |
2320 | |
2321 | $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer; |
2322 | $url = (string)$services->getUrlUtils()->expand( $server . $this->getLocalURL( $query ), PROTO_HTTP ); |
2323 | ( new HookRunner( $services->getHookContainer() ) ) |
2324 | ->onGetInternalURL( $this, $url, $query ); |
2325 | return $url; |
2326 | } |
2327 | |
2328 | /** |
2329 | * Get the URL for a canonical link, for use in things like IRC and |
2330 | * e-mail notifications. Uses $wgCanonicalServer and the |
2331 | * GetCanonicalURL hook. |
2332 | * |
2333 | * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment |
2334 | * |
2335 | * @see self::getLocalURL for the arguments. |
2336 | * @param string|array $query |
2337 | * @return string The URL |
2338 | * @since 1.18 |
2339 | */ |
2340 | public function getCanonicalURL( $query = '' ) { |
2341 | $services = MediaWikiServices::getInstance(); |
2342 | |
2343 | $query = is_array( $query ) ? wfArrayToCgi( $query ) : $query; |
2344 | |
2345 | $url = (string)$services->getUrlUtils()->expand( |
2346 | $this->getLocalURL( $query ) . $this->getFragmentForURL(), |
2347 | PROTO_CANONICAL |
2348 | ); |
2349 | ( new HookRunner( $services->getHookContainer() ) ) |
2350 | ->onGetCanonicalURL( $this, $url, $query ); |
2351 | return $url; |
2352 | } |
2353 | |
2354 | /** |
2355 | * Get the edit URL for this Title |
2356 | * |
2357 | * @return string The URL, or a null string if this is an interwiki link |
2358 | */ |
2359 | public function getEditURL() { |
2360 | if ( $this->isExternal() ) { |
2361 | return ''; |
2362 | } |
2363 | $s = $this->getLocalURL( 'action=edit' ); |
2364 | |
2365 | return $s; |
2366 | } |
2367 | |
2368 | /** |
2369 | * Is this title subject to title protection? |
2370 | * Title protection is the one applied against creation of such title. |
2371 | * |
2372 | * @deprecated since 1.37, use RestrictionStore::getCreateProtection() instead |
2373 | * |
2374 | * @return array|bool An associative array representing any existent title |
2375 | * protection, or false if there's none. |
2376 | */ |
2377 | public function getTitleProtection() { |
2378 | return MediaWikiServices::getInstance()->getRestrictionStore()->getCreateProtection( $this ) |
2379 | ?: false; |
2380 | } |
2381 | |
2382 | /** |
2383 | * Remove any title protection due to page existing |
2384 | * |
2385 | * @deprecated since 1.37, do not use (this is only for WikiPage::onArticleCreate) |
2386 | */ |
2387 | public function deleteTitleProtection() { |
2388 | MediaWikiServices::getInstance()->getRestrictionStore()->deleteCreateProtection( $this ); |
2389 | } |
2390 | |
2391 | /** |
2392 | * Load restrictions from the page_restrictions table |
2393 | * |
2394 | * @deprecated since 1.37, no public replacement |
2395 | * |
2396 | * @param int $flags A bit field. If IDBAccessObject::READ_LATEST is set, skip replicas and read |
2397 | * from the primary DB. |
2398 | */ |
2399 | public function loadRestrictions( $flags = 0 ) { |
2400 | MediaWikiServices::getInstance()->getRestrictionStore()->loadRestrictions( $this, $flags ); |
2401 | } |
2402 | |
2403 | /** |
2404 | * Flush the protection cache in this object and force reload from the database. |
2405 | * This is used when updating protection from WikiPage::doUpdateRestrictions(). |
2406 | * |
2407 | * @deprecated since 1.37, now internal |
2408 | */ |
2409 | public function flushRestrictions() { |
2410 | MediaWikiServices::getInstance()->getRestrictionStore()->flushRestrictions( $this ); |
2411 | } |
2412 | |
2413 | /** |
2414 | * Purge expired restrictions from the page_restrictions table |
2415 | * |
2416 | * This will purge no more than $wgUpdateRowsPerQuery page_restrictions rows |
2417 | */ |
2418 | public static function purgeExpiredRestrictions() { |
2419 | if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) { |
2420 | return; |
2421 | } |
2422 | |
2423 | DeferredUpdates::addUpdate( new AtomicSectionUpdate( |
2424 | MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(), |
2425 | __METHOD__, |
2426 | static function ( IDatabase $dbw, $fname ) { |
2427 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
2428 | $ids = $dbw->newSelectQueryBuilder() |
2429 | ->select( 'pr_id' ) |
2430 | ->from( 'page_restrictions' ) |
2431 | ->where( $dbw->expr( 'pr_expiry', '<', $dbw->timestamp() ) ) |
2432 | ->limit( $config->get( MainConfigNames::UpdateRowsPerQuery ) ) // T135470 |
2433 | ->caller( $fname )->fetchFieldValues(); |
2434 | if ( $ids ) { |
2435 | $dbw->newDeleteQueryBuilder() |
2436 | ->deleteFrom( 'page_restrictions' ) |
2437 | ->where( [ 'pr_id' => $ids ] ) |
2438 | ->caller( $fname )->execute(); |
2439 | } |
2440 | } |
2441 | ) ); |
2442 | |
2443 | DeferredUpdates::addUpdate( new AtomicSectionUpdate( |
2444 | MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(), |
2445 | __METHOD__, |
2446 | static function ( IDatabase $dbw, $fname ) { |
2447 | $dbw->newDeleteQueryBuilder() |
2448 | ->deleteFrom( 'protected_titles' ) |
2449 | ->where( $dbw->expr( 'pt_expiry', '<', $dbw->timestamp() ) ) |
2450 | ->caller( $fname )->execute(); |
2451 | } |
2452 | ) ); |
2453 | } |
2454 | |
2455 | /** |
2456 | * Does this have subpages? (Warning, usually requires an extra DB query.) |
2457 | * |
2458 | * @return bool |
2459 | */ |
2460 | public function hasSubpages() { |
2461 | if ( |
2462 | !MediaWikiServices::getInstance()->getNamespaceInfo()-> |
2463 | hasSubpages( $this->mNamespace ) |
2464 | ) { |
2465 | # Duh |
2466 | return false; |
2467 | } |
2468 | |
2469 | # We dynamically add a member variable for the purpose of this method |
2470 | # alone to cache the result. There's no point in having it hanging |
2471 | # around uninitialized in every Title object; therefore we only add it |
2472 | # if needed and don't declare it statically. |
2473 | if ( $this->mHasSubpages === null ) { |
2474 | $subpages = $this->getSubpages( 1 ); |
2475 | $this->mHasSubpages = $subpages instanceof TitleArrayFromResult && $subpages->count(); |
2476 | } |
2477 | |
2478 | return $this->mHasSubpages; |
2479 | } |
2480 | |
2481 | /** |
2482 | * Get all subpages of this page. |
2483 | * |
2484 | * @param int $limit Maximum number of subpages to fetch; -1 for no limit |
2485 | * @return TitleArrayFromResult|array TitleArrayFromResult, or empty array if this page's namespace |
2486 | * doesn't allow subpages |
2487 | */ |
2488 | public function getSubpages( $limit = -1 ) { |
2489 | if ( |
2490 | !MediaWikiServices::getInstance()->getNamespaceInfo()-> |
2491 | hasSubpages( $this->mNamespace ) |
2492 | ) { |
2493 | return []; |
2494 | } |
2495 | |
2496 | $services = MediaWikiServices::getInstance(); |
2497 | $pageStore = $services->getPageStore(); |
2498 | $titleFactory = $services->getTitleFactory(); |
2499 | $query = $pageStore->newSelectQueryBuilder() |
2500 | ->fields( $pageStore->getSelectFields() ) |
2501 | ->whereTitlePrefix( $this->getNamespace(), $this->getDBkey() . '/' ) |
2502 | ->caller( __METHOD__ ); |
2503 | if ( $limit > -1 ) { |
2504 | $query->limit( $limit ); |
2505 | } |
2506 | |
2507 | return $titleFactory->newTitleArrayFromResult( $query->fetchResultSet() ); |
2508 | } |
2509 | |
2510 | /** |
2511 | * Is there a version of this page in the deletion archive? |
2512 | * |
2513 | * @deprecated since 1.36. Use self::getDeletedEditsCount() |
2514 | * @return int The number of archived revisions |
2515 | */ |
2516 | public function isDeleted() { |
2517 | return $this->getDeletedEditsCount(); |
2518 | } |
2519 | |
2520 | /** |
2521 | * Is there a version of this page in the deletion archive? |
2522 | * |
2523 | * @since 1.36 |
2524 | * @return int The number of archived revisions |
2525 | */ |
2526 | public function getDeletedEditsCount() { |
2527 | if ( $this->mNamespace < 0 ) { |
2528 | return 0; |
2529 | } |
2530 | |
2531 | $dbr = $this->getDbProvider()->getReplicaDatabase(); |
2532 | $n = (int)$dbr->newSelectQueryBuilder() |
2533 | ->select( 'COUNT(*)' ) |
2534 | ->from( 'archive' ) |
2535 | ->where( [ 'ar_namespace' => $this->mNamespace, 'ar_title' => $this->mDbkeyform ] ) |
2536 | ->caller( __METHOD__ )->fetchField(); |
2537 | if ( $this->mNamespace === NS_FILE ) { |
2538 | $n += $dbr->newSelectQueryBuilder() |
2539 | ->select( 'COUNT(*)' ) |
2540 | ->from( 'filearchive' ) |
2541 | ->where( [ 'fa_name' => $this->mDbkeyform ] ) |
2542 | ->caller( __METHOD__ )->fetchField(); |
2543 | } |
2544 | return $n; |
2545 | } |
2546 | |
2547 | /** |
2548 | * Is there a version of this page in the deletion archive? |
2549 | * |
2550 | * @deprecated since 1.36, Use self::hasDeletedEdits() |
2551 | * @return bool |
2552 | */ |
2553 | public function isDeletedQuick() { |
2554 | return $this->hasDeletedEdits(); |
2555 | } |
2556 | |
2557 | /** |
2558 | * Is there a version of this page in the deletion archive? |
2559 | * |
2560 | * @since 1.36 |
2561 | * @return bool |
2562 | */ |
2563 | public function hasDeletedEdits() { |
2564 | if ( $this->mNamespace < 0 ) { |
2565 | return false; |
2566 | } |
2567 | $dbr = $this->getDbProvider()->getReplicaDatabase(); |
2568 | $deleted = (bool)$dbr->newSelectQueryBuilder() |
2569 | ->select( '1' ) |
2570 | ->from( 'archive' ) |
2571 | ->where( [ 'ar_namespace' => $this->mNamespace, 'ar_title' => $this->mDbkeyform ] ) |
2572 | ->caller( __METHOD__ )->fetchField(); |
2573 | if ( !$deleted && $this->mNamespace === NS_FILE ) { |
2574 | $deleted = (bool)$dbr->newSelectQueryBuilder() |
2575 | ->select( '1' ) |
2576 | ->from( 'filearchive' ) |
2577 | ->where( [ 'fa_name' => $this->mDbkeyform ] ) |
2578 | ->caller( __METHOD__ )->fetchField(); |
2579 | } |
2580 | return $deleted; |
2581 | } |
2582 | |
2583 | /** |
2584 | * Get the article ID for this Title from the link cache, |
2585 | * adding it if necessary |
2586 | * |
2587 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
2588 | * @return int The ID |
2589 | */ |
2590 | public function getArticleID( $flags = 0 ) { |
2591 | if ( $this->mArticleID === -1 && !$this->canExist() ) { |
2592 | $this->mArticleID = 0; |
2593 | |
2594 | return $this->mArticleID; |
2595 | } |
2596 | |
2597 | if ( $this->mArticleID === -1 || $this->shouldReadLatest( $flags ) ) { |
2598 | $this->mArticleID = (int)$this->getFieldFromPageStore( 'page_id', $flags ); |
2599 | } |
2600 | |
2601 | return $this->mArticleID; |
2602 | } |
2603 | |
2604 | /** |
2605 | * Is this an article that is a redirect page? |
2606 | * Uses link cache, adding it if necessary. |
2607 | * |
2608 | * This is intended to provide fast access to page_is_redirect for linking. |
2609 | * In rare cases, there might not be a valid target in the redirect table |
2610 | * even though this function returns true. |
2611 | * |
2612 | * To find a redirect target, just call WikiPage::getRedirectTarget() and |
2613 | * check if it returns null, there's no need to call this first. |
2614 | * |
2615 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
2616 | * @return bool |
2617 | */ |
2618 | public function isRedirect( $flags = 0 ) { |
2619 | if ( $this->shouldReadLatest( $flags ) || $this->mRedirect === null ) { |
2620 | $this->mRedirect = (bool)$this->getFieldFromPageStore( 'page_is_redirect', $flags ); |
2621 | } |
2622 | |
2623 | return $this->mRedirect; |
2624 | } |
2625 | |
2626 | /** |
2627 | * What is the length of this page? |
2628 | * Uses link cache, adding it if necessary |
2629 | * |
2630 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
2631 | * @return int |
2632 | */ |
2633 | public function getLength( $flags = 0 ) { |
2634 | if ( $this->shouldReadLatest( $flags ) || $this->mLength < 0 ) { |
2635 | $this->mLength = (int)$this->getFieldFromPageStore( 'page_len', $flags ); |
2636 | } |
2637 | |
2638 | if ( $this->mLength < 0 ) { |
2639 | $this->mLength = 0; |
2640 | } |
2641 | |
2642 | return $this->mLength; |
2643 | } |
2644 | |
2645 | /** |
2646 | * What is the page_latest field for this page? |
2647 | * |
2648 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
2649 | * @return int Int or 0 if the page doesn't exist |
2650 | */ |
2651 | public function getLatestRevID( $flags = 0 ) { |
2652 | if ( $this->shouldReadLatest( $flags ) || $this->mLatestID === false ) { |
2653 | $this->mLatestID = (int)$this->getFieldFromPageStore( 'page_latest', $flags ); |
2654 | } |
2655 | |
2656 | if ( !$this->mLatestID ) { |
2657 | $this->mLatestID = 0; |
2658 | } |
2659 | |
2660 | return $this->mLatestID; |
2661 | } |
2662 | |
2663 | /** |
2664 | * Inject a page ID, reset DB-loaded fields, and clear the link cache for this title |
2665 | * |
2666 | * This can be called on page insertion to allow loading of the new page_id without |
2667 | * having to create a new Title instance. Likewise with deletion. |
2668 | * |
2669 | * This is also used during page moves, to reflect the change in the relationship |
2670 | * between article ID and title text. |
2671 | * |
2672 | * @note This overrides Title::setContentModel() |
2673 | * |
2674 | * @param int|bool $id Page ID, 0 for non-existent, or false for "unknown" (lazy-load) |
2675 | */ |
2676 | public function resetArticleID( $id ) { |
2677 | if ( $id === false ) { |
2678 | $this->mArticleID = -1; |
2679 | } else { |
2680 | $this->mArticleID = (int)$id; |
2681 | } |
2682 | $this->mRedirect = null; |
2683 | $this->mLength = -1; |
2684 | $this->mLatestID = false; |
2685 | $this->mContentModel = false; |
2686 | $this->mForcedContentModel = false; |
2687 | $this->mEstimateRevisions = null; |
2688 | $this->mPageLanguage = null; |
2689 | $this->mDbPageLanguage = false; |
2690 | $this->mIsBigDeletion = null; |
2691 | |
2692 | $this->uncache(); |
2693 | MediaWikiServices::getInstance()->getLinkCache()->clearLink( $this ); |
2694 | MediaWikiServices::getInstance()->getRestrictionStore()->flushRestrictions( $this ); |
2695 | } |
2696 | |
2697 | public static function clearCaches() { |
2698 | $linkCache = MediaWikiServices::getInstance()->getLinkCache(); |
2699 | $linkCache->clear(); |
2700 | |
2701 | $titleCache = self::getTitleCache(); |
2702 | $titleCache->clear(); |
2703 | } |
2704 | |
2705 | /** |
2706 | * Capitalize a text string for a title if it belongs to a namespace that capitalizes |
2707 | * |
2708 | * @param string $text Containing title to capitalize |
2709 | * @param int $ns Namespace index, defaults to NS_MAIN |
2710 | * @return string Containing capitalized title |
2711 | */ |
2712 | public static function capitalize( $text, $ns = NS_MAIN ) { |
2713 | $services = MediaWikiServices::getInstance(); |
2714 | if ( $services->getNamespaceInfo()->isCapitalized( $ns ) ) { |
2715 | return $services->getContentLanguage()->ucfirst( $text ); |
2716 | } else { |
2717 | return $text; |
2718 | } |
2719 | } |
2720 | |
2721 | /** |
2722 | * Secure and split - main initialisation function for this object |
2723 | * |
2724 | * Assumes that $text is urldecoded |
2725 | * and uses underscores, but not otherwise munged. This function |
2726 | * removes illegal characters, splits off the interwiki and |
2727 | * namespace prefixes, sets the other forms, and canonicalizes |
2728 | * everything. |
2729 | * |
2730 | * If this method returns normally, the Title is valid. |
2731 | * |
2732 | * @param string $text |
2733 | * @param int|null $defaultNamespace |
2734 | * |
2735 | * @throws MalformedTitleException On malformed titles |
2736 | */ |
2737 | private function secureAndSplit( $text, $defaultNamespace = null ) { |
2738 | if ( $defaultNamespace === null ) { |
2739 | $defaultNamespace = self::DEFAULT_NAMESPACE; |
2740 | } |
2741 | |
2742 | // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share |
2743 | // the parsing code with Title, while avoiding massive refactoring. |
2744 | // @todo: get rid of secureAndSplit, refactor parsing code. |
2745 | // @note: getTitleParser() returns a TitleParser implementation which does not have a |
2746 | // splitTitleString method, but the only implementation (MediaWikiTitleCodec) does |
2747 | /** @var MediaWikiTitleCodec $titleCodec */ |
2748 | $titleCodec = MediaWikiServices::getInstance()->getTitleParser(); |
2749 | '@phan-var MediaWikiTitleCodec $titleCodec'; |
2750 | // MalformedTitleException can be thrown here |
2751 | $parts = $titleCodec->splitTitleString( $text, $defaultNamespace ); |
2752 | |
2753 | # Fill fields |
2754 | $this->setFragment( '#' . $parts['fragment'] ); |
2755 | $this->mInterwiki = $parts['interwiki']; |
2756 | $this->mLocalInterwiki = $parts['local_interwiki']; |
2757 | $this->mNamespace = $parts['namespace']; |
2758 | |
2759 | $this->mDbkeyform = $parts['dbkey']; |
2760 | $this->mUrlform = wfUrlencode( $this->mDbkeyform ); |
2761 | $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' ); |
2762 | |
2763 | // splitTitleString() guarantees that this title is valid. |
2764 | $this->mIsValid = true; |
2765 | |
2766 | # We already know that some pages won't be in the database! |
2767 | if ( $this->isExternal() || $this->isSpecialPage() || $this->mTextform === '' ) { |
2768 | $this->mArticleID = 0; |
2769 | } |
2770 | } |
2771 | |
2772 | /** |
2773 | * Get an array of Title objects linking to this Title |
2774 | * Also stores the IDs in the link cache. |
2775 | * |
2776 | * WARNING: do not use this function on arbitrary user-supplied titles! |
2777 | * On heavily-used templates it will max out the memory. |
2778 | * |
2779 | * @param array $options May be FOR UPDATE |
2780 | * @param string $table Table name |
2781 | * @param string $prefix Fields prefix |
2782 | * @return Title[] |
2783 | */ |
2784 | public function getLinksTo( $options = [], $table = 'pagelinks', $prefix = 'pl' ) { |
2785 | if ( count( $options ) > 0 ) { |
2786 | $db = $this->getDbProvider()->getPrimaryDatabase(); |
2787 | } else { |
2788 | $db = $this->getDbProvider()->getReplicaDatabase(); |
2789 | } |
2790 | |
2791 | $linksMigration = MediaWikiServices::getInstance()->getLinksMigration(); |
2792 | if ( isset( $linksMigration::$mapping[$table] ) ) { |
2793 | $titleConds = $linksMigration->getLinksConditions( $table, $this ); |
2794 | } else { |
2795 | $titleConds = [ |
2796 | "{$prefix}_namespace" => $this->mNamespace, |
2797 | "{$prefix}_title" => $this->mDbkeyform |
2798 | ]; |
2799 | } |
2800 | |
2801 | $res = $db->newSelectQueryBuilder() |
2802 | ->select( LinkCache::getSelectFields() ) |
2803 | ->from( $table ) |
2804 | ->join( 'page', null, "{$prefix}_from=page_id" ) |
2805 | ->where( $titleConds ) |
2806 | ->options( $options ) |
2807 | ->caller( __METHOD__ ) |
2808 | ->fetchResultSet(); |
2809 | |
2810 | $retVal = []; |
2811 | if ( $res->numRows() ) { |
2812 | $linkCache = MediaWikiServices::getInstance()->getLinkCache(); |
2813 | foreach ( $res as $row ) { |
2814 | $titleObj = self::makeTitle( $row->page_namespace, $row->page_title ); |
2815 | if ( $titleObj ) { |
2816 | $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); |
2817 | $retVal[] = $titleObj; |
2818 | } |
2819 | } |
2820 | } |
2821 | return $retVal; |
2822 | } |
2823 | |
2824 | /** |
2825 | * Get an array of Title objects using this Title as a template |
2826 | * Also stores the IDs in the link cache. |
2827 | * |
2828 | * WARNING: do not use this function on arbitrary user-supplied titles! |
2829 | * On heavily-used templates it will max out the memory. |
2830 | * |
2831 | * @param array $options Query option to Database::select() |
2832 | * @return Title[] |
2833 | */ |
2834 | public function getTemplateLinksTo( $options = [] ) { |
2835 | return $this->getLinksTo( $options, 'templatelinks', 'tl' ); |
2836 | } |
2837 | |
2838 | /** |
2839 | * Get an array of Title objects linked from this Title |
2840 | * Also stores the IDs in the link cache. |
2841 | * |
2842 | * WARNING: do not use this function on arbitrary user-supplied titles! |
2843 | * On heavily-used templates it will max out the memory. |
2844 | * |
2845 | * @param array $options Query option to Database::select() |
2846 | * @param string $table Table name |
2847 | * @param string $prefix Fields prefix |
2848 | * @return Title[] List of Titles linking here |
2849 | */ |
2850 | public function getLinksFrom( $options = [], $table = 'pagelinks', $prefix = 'pl' ) { |
2851 | $id = $this->getArticleID(); |
2852 | |
2853 | # If the page doesn't exist; there can't be any link from this page |
2854 | if ( !$id ) { |
2855 | return []; |
2856 | } |
2857 | |
2858 | $db = $this->getDbProvider()->getReplicaDatabase(); |
2859 | $linksMigration = MediaWikiServices::getInstance()->getLinksMigration(); |
2860 | |
2861 | if ( isset( $linksMigration::$mapping[$table] ) ) { |
2862 | [ $blNamespace, $blTitle ] = $linksMigration->getTitleFields( $table ); |
2863 | $linktargetQueryInfo = $linksMigration->getQueryInfo( $table ); |
2864 | $fields = $linktargetQueryInfo['fields']; |
2865 | $tables = $linktargetQueryInfo['tables']; |
2866 | $joins = $linktargetQueryInfo['joins']; |
2867 | } else { |
2868 | $blNamespace = "{$prefix}_namespace"; |
2869 | $blTitle = "{$prefix}_title"; |
2870 | $fields = [ $blNamespace, $blTitle ]; |
2871 | $tables = [ $table ]; |
2872 | $joins = []; |
2873 | } |
2874 | |
2875 | $pageQuery = WikiPage::getQueryInfo(); |
2876 | $res = $db->select( |
2877 | array_merge( $tables, [ 'nestpage' => $pageQuery['tables'] ] ), |
2878 | array_merge( |
2879 | $fields, |
2880 | $pageQuery['fields'] |
2881 | ), |
2882 | [ "{$prefix}_from" => $id ], |
2883 | __METHOD__, |
2884 | $options, |
2885 | [ 'nestpage' => [ |
2886 | 'LEFT JOIN', |
2887 | [ "page_namespace=$blNamespace", "page_title=$blTitle" ] |
2888 | ] ] + $pageQuery['joins'] + $joins |
2889 | ); |
2890 | |
2891 | $retVal = []; |
2892 | $linkCache = MediaWikiServices::getInstance()->getLinkCache(); |
2893 | foreach ( $res as $row ) { |
2894 | if ( $row->page_id ) { |
2895 | $titleObj = self::newFromRow( $row ); |
2896 | } else { |
2897 | $titleObj = self::makeTitle( $row->$blNamespace, $row->$blTitle ); |
2898 | $linkCache->addBadLinkObj( $titleObj ); |
2899 | } |
2900 | $retVal[] = $titleObj; |
2901 | } |
2902 | |
2903 | return $retVal; |
2904 | } |
2905 | |
2906 | /** |
2907 | * Get an array of Title objects used on this Title as a template |
2908 | * Also stores the IDs in the link cache. |
2909 | * |
2910 | * WARNING: do not use this function on arbitrary user-supplied titles! |
2911 | * On heavily-used templates it will max out the memory. |
2912 | * |
2913 | * @param array $options May be FOR UPDATE |
2914 | * @return Title[] |
2915 | */ |
2916 | public function getTemplateLinksFrom( $options = [] ) { |
2917 | return $this->getLinksFrom( $options, 'templatelinks', 'tl' ); |
2918 | } |
2919 | |
2920 | /** |
2921 | * Get an array of Title objects referring to non-existent articles linked |
2922 | * from this page. |
2923 | * |
2924 | * @deprecated since 1.42, unneeded |
2925 | * @return Title[] |
2926 | */ |
2927 | public function getBrokenLinksFrom() { |
2928 | wfDeprecated( __METHOD__, '1.42' ); |
2929 | if ( $this->getArticleID() == 0 ) { |
2930 | # All links from article ID 0 are false positives |
2931 | return []; |
2932 | } |
2933 | |
2934 | $dbr = $this->getDbProvider()->getReplicaDatabase(); |
2935 | $res = $dbr->newSelectQueryBuilder() |
2936 | ->select( [ 'pl_namespace', 'pl_title' ] ) |
2937 | ->from( 'pagelinks' ) |
2938 | ->leftJoin( 'page', null, [ 'pl_namespace=page_namespace', 'pl_title=page_title' ] ) |
2939 | ->where( [ 'pl_from' => $this->getArticleID(), 'page_namespace' => null ] ) |
2940 | ->caller( __METHOD__ )->fetchResultSet(); |
2941 | |
2942 | $retVal = []; |
2943 | foreach ( $res as $row ) { |
2944 | $retVal[] = self::makeTitle( $row->pl_namespace, $row->pl_title ); |
2945 | } |
2946 | return $retVal; |
2947 | } |
2948 | |
2949 | /** |
2950 | * Get a list of URLs to purge from the CDN cache when this page changes. |
2951 | * |
2952 | * @deprecated since 1.35 Use HTMLCacheUpdater; hard-deprecated in 1.42 |
2953 | * @return string[] |
2954 | */ |
2955 | public function getCdnUrls() { |
2956 | wfDeprecated( __METHOD__, '1.35' ); |
2957 | $htmlCache = MediaWikiServices::getInstance()->getHtmlCacheUpdater(); |
2958 | return $htmlCache->getUrls( $this ); |
2959 | } |
2960 | |
2961 | /** |
2962 | * Purge all applicable CDN URLs |
2963 | * @deprecated since 1.35 Use HTMLCacheUpdater; hard-deprecated in 1.42 |
2964 | */ |
2965 | public function purgeSquid() { |
2966 | wfDeprecated( __METHOD__, '1.35' ); |
2967 | $htmlCache = MediaWikiServices::getInstance()->getHtmlCacheUpdater(); |
2968 | $htmlCache->purgeTitleUrls( $this, $htmlCache::PURGE_INTENT_TXROUND_REFLECTED ); |
2969 | } |
2970 | |
2971 | /** |
2972 | * Locks the page row and check if this page is single revision redirect |
2973 | * |
2974 | * This updates the cached fields of this instance via Title::loadFromRow() |
2975 | * |
2976 | * @return bool |
2977 | */ |
2978 | public function isSingleRevRedirect() { |
2979 | $dbw = $this->getDbProvider()->getPrimaryDatabase(); |
2980 | $dbw->startAtomic( __METHOD__ ); |
2981 | $pageStore = MediaWikiServices::getInstance()->getPageStore(); |
2982 | |
2983 | $row = $dbw->newSelectQueryBuilder() |
2984 | ->select( $pageStore->getSelectFields() ) |
2985 | ->from( 'page' ) |
2986 | ->where( $this->pageCond() ) |
2987 | ->caller( __METHOD__ )->fetchRow(); |
2988 | // Update the cached fields |
2989 | $this->loadFromRow( $row ); |
2990 | |
2991 | if ( $this->mRedirect && $this->mLatestID ) { |
2992 | $isSingleRevRedirect = !$dbw->newSelectQueryBuilder() |
2993 | ->select( '1' ) |
2994 | ->forUpdate() |
2995 | ->from( 'revision' ) |
2996 | ->where( [ 'rev_page' => $this->mArticleID, 'rev_id != ' . (int)$this->mLatestID ] ) |
2997 | ->caller( __METHOD__ )->fetchField(); |
2998 | } else { |
2999 | $isSingleRevRedirect = false; |
3000 | } |
3001 | |
3002 | $dbw->endAtomic( __METHOD__ ); |
3003 | |
3004 | return $isSingleRevRedirect; |
3005 | } |
3006 | |
3007 | /** |
3008 | * Get categories to which this Title belongs and return an array of |
3009 | * categories' names. |
3010 | * |
3011 | * @return string[] Array of parents in the form: |
3012 | * $parent => $currentarticle |
3013 | */ |
3014 | public function getParentCategories() { |
3015 | $data = []; |
3016 | |
3017 | $titleKey = $this->getArticleID(); |
3018 | |
3019 | if ( $titleKey === 0 ) { |
3020 | return $data; |
3021 | } |
3022 | |
3023 | $dbr = $this->getDbProvider()->getReplicaDatabase(); |
3024 | |
3025 | $res = $dbr->newSelectQueryBuilder() |
3026 | ->select( 'cl_to' ) |
3027 | ->from( 'categorylinks' ) |
3028 | ->where( [ 'cl_from' => $titleKey ] ) |
3029 | ->caller( __METHOD__ )->fetchResultSet(); |
3030 | |
3031 | if ( $res->numRows() > 0 ) { |
3032 | $contLang = MediaWikiServices::getInstance()->getContentLanguage(); |
3033 | foreach ( $res as $row ) { |
3034 | // $data[] = Title::newFromText( $contLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to); |
3035 | $data[$contLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] = |
3036 | $this->getFullText(); |
3037 | } |
3038 | } |
3039 | return $data; |
3040 | } |
3041 | |
3042 | /** |
3043 | * Get a tree of parent categories |
3044 | * |
3045 | * @param array $children Array with the children in the keys, to check for circular refs |
3046 | * @return array Tree of parent categories |
3047 | */ |
3048 | public function getParentCategoryTree( $children = [] ) { |
3049 | $stack = []; |
3050 | $parents = $this->getParentCategories(); |
3051 | |
3052 | if ( $parents ) { |
3053 | foreach ( $parents as $parent => $current ) { |
3054 | if ( array_key_exists( $parent, $children ) ) { |
3055 | # Circular reference |
3056 | $stack[$parent] = []; |
3057 | } else { |
3058 | $nt = self::newFromText( $parent ); |
3059 | if ( $nt ) { |
3060 | $stack[$parent] = $nt->getParentCategoryTree( $children + [ $parent => 1 ] ); |
3061 | } |
3062 | } |
3063 | } |
3064 | } |
3065 | |
3066 | return $stack; |
3067 | } |
3068 | |
3069 | /** |
3070 | * Get an associative array for selecting this title from |
3071 | * the "page" table |
3072 | * |
3073 | * @return array Array suitable for the $where parameter of DB::select() |
3074 | */ |
3075 | public function pageCond() { |
3076 | if ( $this->mArticleID > 0 ) { |
3077 | // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs |
3078 | return [ 'page_id' => $this->mArticleID ]; |
3079 | } else { |
3080 | return [ 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ]; |
3081 | } |
3082 | } |
3083 | |
3084 | /** |
3085 | * Check if this is a new page. |
3086 | * |
3087 | * @note This returns false if the page does not exist. |
3088 | * @param int $flags one of the READ_XXX constants. |
3089 | * |
3090 | * @return bool |
3091 | */ |
3092 | public function isNewPage( $flags = IDBAccessObject::READ_NORMAL ) { |
3093 | // NOTE: we rely on PHP casting "0" to false here. |
3094 | return (bool)$this->getFieldFromPageStore( 'page_is_new', $flags ); |
3095 | } |
3096 | |
3097 | /** |
3098 | * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit |
3099 | * @deprecated since 1.37 External callers shouldn't need to know about this. |
3100 | * |
3101 | * @return bool |
3102 | */ |
3103 | public function isBigDeletion() { |
3104 | global $wgDeleteRevisionsLimit; |
3105 | |
3106 | if ( !$wgDeleteRevisionsLimit ) { |
3107 | return false; |
3108 | } |
3109 | |
3110 | if ( $this->mIsBigDeletion === null ) { |
3111 | $dbr = $this->getDbProvider()->getReplicaDatabase(); |
3112 | $revCount = $dbr->newSelectQueryBuilder() |
3113 | ->select( '1' ) |
3114 | ->from( 'revision' ) |
3115 | ->where( [ 'rev_page' => $this->getArticleID() ] ) |
3116 | ->limit( $wgDeleteRevisionsLimit + 1 ) |
3117 | ->caller( __METHOD__ )->fetchRowCount(); |
3118 | |
3119 | $this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit; |
3120 | } |
3121 | |
3122 | return $this->mIsBigDeletion; |
3123 | } |
3124 | |
3125 | /** |
3126 | * Get the approximate revision count of this page. |
3127 | * |
3128 | * @return int |
3129 | */ |
3130 | public function estimateRevisionCount() { |
3131 | if ( !$this->exists() ) { |
3132 | return 0; |
3133 | } |
3134 | |
3135 | if ( $this->mEstimateRevisions === null ) { |
3136 | $dbr = $this->getDbProvider()->getReplicaDatabase(); |
3137 | $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*', |
3138 | [ 'rev_page' => $this->getArticleID() ], __METHOD__ ); |
3139 | } |
3140 | |
3141 | return $this->mEstimateRevisions; |
3142 | } |
3143 | |
3144 | /** |
3145 | * Compares with another Title. |
3146 | * |
3147 | * A Title object is considered equal to another Title if it has the same text, |
3148 | * the same interwiki prefix, and the same namespace. |
3149 | * |
3150 | * @note This is different from isSameLinkAs(), which also compares the fragment part, |
3151 | * and from isSamePageAs(), which takes into account the page ID. |
3152 | * |
3153 | * @phpcs:disable MediaWiki.Commenting.FunctionComment.ObjectTypeHintParam |
3154 | * @param Title|object $other |
3155 | * |
3156 | * @return bool true if $other is a Title and refers to the same page. |
3157 | */ |
3158 | public function equals( object $other ) { |
3159 | // NOTE: In contrast to isSameLinkAs(), this ignores the fragment part! |
3160 | // NOTE: In contrast to isSamePageAs(), this ignores the page ID! |
3161 | // NOTE: === is necessary for proper matching of number-like titles |
3162 | return $other instanceof Title |
3163 | && $this->getInterwiki() === $other->getInterwiki() |
3164 | && $this->getNamespace() === $other->getNamespace() |
3165 | && $this->getDBkey() === $other->getDBkey(); |
3166 | } |
3167 | |
3168 | /** |
3169 | * @inheritDoc |
3170 | * @since 1.36 |
3171 | */ |
3172 | public function isSamePageAs( PageReference $other ): bool { |
3173 | // NOTE: keep in sync with PageReferenceValue::isSamePageAs()! |
3174 | return $this->getWikiId() === $other->getWikiId() |
3175 | && $this->getNamespace() === $other->getNamespace() |
3176 | && $this->getDBkey() === $other->getDBkey(); |
3177 | } |
3178 | |
3179 | /** |
3180 | * Check if this title is a subpage of another title |
3181 | * |
3182 | * @param Title $title |
3183 | * @return bool |
3184 | */ |
3185 | public function isSubpageOf( Title $title ) { |
3186 | return $this->mInterwiki === $title->mInterwiki |
3187 | && $this->mNamespace == $title->mNamespace |
3188 | && str_starts_with( $this->mDbkeyform, $title->mDbkeyform . '/' ); |
3189 | } |
3190 | |
3191 | /** |
3192 | * Check if page exists. For historical reasons, this function simply |
3193 | * checks for the existence of the title in the page table, and will |
3194 | * thus return false for interwiki links, special pages and the like. |
3195 | * If you want to know if a title can be meaningfully viewed, you should |
3196 | * probably call the isKnown() method instead. |
3197 | * |
3198 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
3199 | * @return bool |
3200 | */ |
3201 | public function exists( $flags = 0 ): bool { |
3202 | $exists = $this->getArticleID( $flags ) != 0; |
3203 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onTitleExists( $this, $exists ); |
3204 | return $exists; |
3205 | } |
3206 | |
3207 | /** |
3208 | * Should links to this title be shown as potentially viewable (i.e. as |
3209 | * "bluelinks"), even if there's no record by this title in the page |
3210 | * table? |
3211 | * |
3212 | * This function is semi-deprecated for public use, as well as somewhat |
3213 | * misleadingly named. You probably just want to call isKnown(), which |
3214 | * calls this function internally. |
3215 | * |
3216 | * (ISSUE: Most of these checks are cheap, but the file existence check |
3217 | * can potentially be quite expensive. Including it here fixes a lot of |
3218 | * existing code, but we might want to add an optional parameter to skip |
3219 | * it and any other expensive checks.) |
3220 | * |
3221 | * @return bool |
3222 | */ |
3223 | public function isAlwaysKnown() { |
3224 | $isKnown = null; |
3225 | |
3226 | $services = MediaWikiServices::getInstance(); |
3227 | /** |
3228 | * Allows overriding default behavior for determining if a page exists. |
3229 | * If $isKnown is kept as null, regular checks happen. If it's |
3230 | * a boolean, this value is returned by the isKnown method. |
3231 | * |
3232 | * @since 1.20 |
3233 | * |
3234 | * @param Title $title |
3235 | * @param bool|null $isKnown |
3236 | */ |
3237 | ( new HookRunner( $services->getHookContainer() ) )->onTitleIsAlwaysKnown( $this, $isKnown ); |
3238 | |
3239 | if ( $isKnown !== null ) { |
3240 | return $isKnown; |
3241 | } |
3242 | |
3243 | if ( $this->isExternal() ) { |
3244 | return true; // any interwiki link might be viewable, for all we know |
3245 | } |
3246 | |
3247 | switch ( $this->mNamespace ) { |
3248 | case NS_MEDIA: |
3249 | case NS_FILE: |
3250 | // file exists, possibly in a foreign repo |
3251 | return (bool)$services->getRepoGroup()->findFile( $this ); |
3252 | case NS_SPECIAL: |
3253 | // valid special page |
3254 | return $services->getSpecialPageFactory()->exists( $this->mDbkeyform ); |
3255 | case NS_MAIN: |
3256 | // selflink, possibly with fragment |
3257 | return $this->mDbkeyform == ''; |
3258 | case NS_MEDIAWIKI: |
3259 | // known system message |
3260 | return $this->hasSourceText() !== false; |
3261 | default: |
3262 | return false; |
3263 | } |
3264 | } |
3265 | |
3266 | /** |
3267 | * Does this title refer to a page that can (or might) be meaningfully |
3268 | * viewed? In particular, this function may be used to determine if |
3269 | * links to the title should be rendered as "bluelinks" (as opposed to |
3270 | * "redlinks" to non-existent pages). |
3271 | * Adding something else to this function will cause inconsistency |
3272 | * since LinkHolderArray calls isAlwaysKnown() and does its own |
3273 | * page existence check. |
3274 | * |
3275 | * @return bool |
3276 | */ |
3277 | public function isKnown() { |
3278 | return $this->isAlwaysKnown() || $this->exists(); |
3279 | } |
3280 | |
3281 | /** |
3282 | * Does this page have source text? |
3283 | * |
3284 | * @return bool |
3285 | */ |
3286 | public function hasSourceText() { |
3287 | if ( $this->exists() ) { |
3288 | return true; |
3289 | } |
3290 | |
3291 | if ( $this->mNamespace === NS_MEDIAWIKI ) { |
3292 | $services = MediaWikiServices::getInstance(); |
3293 | // If the page doesn't exist but is a known system message, default |
3294 | // message content will be displayed, same for language subpages- |
3295 | // Use always content language to avoid loading hundreds of languages |
3296 | // to get the link color. |
3297 | $contLang = $services->getContentLanguage(); |
3298 | [ $name, ] = $services->getMessageCache()->figureMessage( |
3299 | $contLang->lcfirst( $this->getText() ) |
3300 | ); |
3301 | $message = wfMessage( $name )->inLanguage( $contLang )->useDatabase( false ); |
3302 | return $message->exists(); |
3303 | } |
3304 | |
3305 | return false; |
3306 | } |
3307 | |
3308 | /** |
3309 | * Get the default (plain) message contents for a page that overrides an |
3310 | * interface message key. |
3311 | * |
3312 | * Primary use cases: |
3313 | * |
3314 | * - Article: |
3315 | * - Show default when viewing the page. The Article::getSubstituteContent |
3316 | * method displays the default message content, instead of the |
3317 | * 'noarticletext' placeholder message normally used. |
3318 | * |
3319 | * - EditPage: |
3320 | * - Title of edit page. When creating an interface message override, |
3321 | * the editor is told they are "Editing the page", instead of |
3322 | * "Creating the page". (EditPage::setHeaders) |
3323 | * - Edit notice. The 'translateinterface' edit notice is shown when creating |
3324 | * or editing an interface message override. (EditPage::showIntro) |
3325 | * - Opening the editor. The contents of the localisation message are used |
3326 | * as contents of the editor when creating a new page in the MediaWiki |
3327 | * namespace. This simplifies the process for editors when "changing" |
3328 | * an interface message by creating an override. (EditPage::getContentObject) |
3329 | * - Showing a diff. The left-hand side of a diff when an editor is |
3330 | * previewing their changes before saving the creation of a page in the |
3331 | * MediaWiki namespace. (EditPage::showDiff) |
3332 | * - Disallowing a save. When attempting to create a MediaWiki-namespace |
3333 | * page with the proposed content matching the interface message default, |
3334 | * the save is rejected, the same way we disallow blank pages from being |
3335 | * created. (EditPage::internalAttemptSave) |
3336 | * |
3337 | * - ApiEditPage: |
3338 | * - Default content, when using the 'prepend' or 'append' feature. |
3339 | * |
3340 | * - SkinTemplate: |
3341 | * - Label the create action as "Edit", if the page can be an override. |
3342 | * |
3343 | * @return string|false |
3344 | */ |
3345 | public function getDefaultMessageText() { |
3346 | $message = $this->getDefaultSystemMessage(); |
3347 | |
3348 | return $message ? $message->plain() : false; |
3349 | } |
3350 | |
3351 | /** |
3352 | * Same as getDefaultMessageText, but returns a Message object. |
3353 | * |
3354 | * @see ::getDefaultMessageText |
3355 | * |
3356 | * @return ?Message |
3357 | */ |
3358 | public function getDefaultSystemMessage(): ?Message { |
3359 | if ( $this->mNamespace !== NS_MEDIAWIKI ) { // Just in case |
3360 | return null; |
3361 | } |
3362 | |
3363 | [ $name, $lang ] = MediaWikiServices::getInstance()->getMessageCache()->figureMessage( |
3364 | MediaWikiServices::getInstance()->getContentLanguage()->lcfirst( $this->getText() ) |
3365 | ); |
3366 | |
3367 | if ( wfMessage( $name )->inLanguage( $lang )->useDatabase( false )->exists() ) { |
3368 | return wfMessage( $name )->inLanguage( $lang ); |
3369 | } else { |
3370 | return null; |
3371 | } |
3372 | } |
3373 | |
3374 | /** |
3375 | * Updates page_touched for this page; called from LinksUpdate.php |
3376 | * |
3377 | * @param string|null $purgeTime [optional] TS_MW timestamp |
3378 | * @return bool True if the update succeeded |
3379 | */ |
3380 | public function invalidateCache( $purgeTime = null ) { |
3381 | if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) { |
3382 | return false; |
3383 | } |
3384 | if ( $this->mArticleID === 0 ) { |
3385 | // avoid gap locking if we know it's not there |
3386 | return true; |
3387 | } |
3388 | |
3389 | $conds = $this->pageCond(); |
3390 | |
3391 | // Periodically recompute page_random (T309477). This mitigates bias on |
3392 | // Special:Random due deleted pages leaving "gaps" in the distribution. |
3393 | // |
3394 | // Optimization: Update page_random only for 10% of updates. |
3395 | // Optimization: Do this outside the main transaction to avoid locking for too long. |
3396 | // Optimization: Update page_random alongside page_touched to avoid extra database writes. |
3397 | DeferredUpdates::addUpdate( |
3398 | new AutoCommitUpdate( |
3399 | $this->getDbProvider()->getPrimaryDatabase(), |
3400 | __METHOD__, |
3401 | function ( IDatabase $dbw, $fname ) use ( $conds, $purgeTime ) { |
3402 | $dbTimestamp = $dbw->timestamp( $purgeTime ?: time() ); |
3403 | $update = $dbw->newUpdateQueryBuilder() |
3404 | ->update( 'page' ) |
3405 | ->set( [ 'page_touched' => $dbTimestamp ] ) |
3406 | ->where( $conds ) |
3407 | ->andWhere( $dbw->expr( 'page_touched', '<', $dbTimestamp ) ); |
3408 | |
3409 | if ( mt_rand( 1, 10 ) === 1 ) { |
3410 | $update->andSet( [ 'page_random' => wfRandom() ] ); |
3411 | } |
3412 | |
3413 | $update->caller( $fname )->execute(); |
3414 | |
3415 | MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $this ); |
3416 | |
3417 | WikiModule::invalidateModuleCache( |
3418 | $this, null, null, $dbw->getDomainID() ); |
3419 | } |
3420 | ), |
3421 | DeferredUpdates::PRESEND |
3422 | ); |
3423 | |
3424 | return true; |
3425 | } |
3426 | |
3427 | /** |
3428 | * Update page_touched timestamps and send CDN purge messages for |
3429 | * pages linking to this title. May be sent to the job queue depending |
3430 | * on the number of links. Typically called on create and delete. |
3431 | */ |
3432 | public function touchLinks() { |
3433 | $jobs = []; |
3434 | $jobs[] = HTMLCacheUpdateJob::newForBacklinks( |
3435 | $this, |
3436 | 'pagelinks', |
3437 | [ 'causeAction' => 'page-touch' ] |
3438 | ); |
3439 | if ( $this->mNamespace === NS_CATEGORY ) { |
3440 | $jobs[] = HTMLCacheUpdateJob::newForBacklinks( |
3441 | $this, |
3442 | 'categorylinks', |
3443 | [ 'causeAction' => 'category-touch' ] |
3444 | ); |
3445 | } |
3446 | |
3447 | MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $jobs ); |
3448 | } |
3449 | |
3450 | /** |
3451 | * Get the last touched timestamp |
3452 | * |
3453 | * @param int $flags one of the READ_XXX constants. |
3454 | * @return string|false Last-touched timestamp |
3455 | */ |
3456 | public function getTouched( int $flags = IDBAccessObject::READ_NORMAL ) { |
3457 | $touched = $this->getFieldFromPageStore( 'page_touched', $flags ); |
3458 | return $touched ? MWTimestamp::convert( TS_MW, $touched ) : false; |
3459 | } |
3460 | |
3461 | /** |
3462 | * Generate strings used for xml 'id' names in monobook tabs |
3463 | * |
3464 | * @param string $prepend Defaults to 'nstab-' |
3465 | * @return string XML 'id' name |
3466 | */ |
3467 | public function getNamespaceKey( $prepend = 'nstab-' ) { |
3468 | // Gets the subject namespace of this title |
3469 | $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); |
3470 | $subjectNS = $nsInfo->getSubject( $this->mNamespace ); |
3471 | // Prefer canonical namespace name for HTML IDs |
3472 | $namespaceKey = $nsInfo->getCanonicalName( $subjectNS ); |
3473 | if ( $namespaceKey === false ) { |
3474 | // Fallback to localised text |
3475 | $namespaceKey = $this->getSubjectNsText(); |
3476 | } |
3477 | // Makes namespace key lowercase |
3478 | $namespaceKey = MediaWikiServices::getInstance()->getContentLanguage()->lc( $namespaceKey ); |
3479 | // Uses main |
3480 | if ( $namespaceKey == '' ) { |
3481 | $namespaceKey = 'main'; |
3482 | } |
3483 | // Changes file to image for backwards compatibility |
3484 | if ( $namespaceKey == 'file' ) { |
3485 | $namespaceKey = 'image'; |
3486 | } |
3487 | return $prepend . $namespaceKey; |
3488 | } |
3489 | |
3490 | /** |
3491 | * Get all extant redirects to this Title |
3492 | * |
3493 | * @param int|null $ns Single namespace to consider; null to consider all namespaces |
3494 | * @return Title[] |
3495 | */ |
3496 | public function getRedirectsHere( $ns = null ) { |
3497 | $redirs = []; |
3498 | |
3499 | $queryBuilder = $this->getDbProvider()->getReplicaDatabase()->newSelectQueryBuilder() |
3500 | ->select( [ 'page_namespace', 'page_title' ] ) |
3501 | ->from( 'redirect' ) |
3502 | ->join( 'page', null, 'rd_from = page_id' ) |
3503 | ->where( [ |
3504 | 'rd_namespace' => $this->mNamespace, |
3505 | 'rd_title' => $this->mDbkeyform, |
3506 | 'rd_interwiki' => $this->isExternal() ? $this->mInterwiki : '', |
3507 | ] ); |
3508 | |
3509 | if ( $ns !== null ) { |
3510 | $queryBuilder->andWhere( [ 'page_namespace' => $ns ] ); |
3511 | } |
3512 | |
3513 | $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet(); |
3514 | |
3515 | foreach ( $res as $row ) { |
3516 | $redirs[] = self::newFromRow( $row ); |
3517 | } |
3518 | return $redirs; |
3519 | } |
3520 | |
3521 | /** |
3522 | * Check if this Title is a valid redirect target |
3523 | * |
3524 | * @return bool |
3525 | */ |
3526 | public function isValidRedirectTarget() { |
3527 | global $wgInvalidRedirectTargets; |
3528 | |
3529 | if ( $this->isSpecialPage() ) { |
3530 | // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here |
3531 | foreach ( [ 'Userlogout', ...$wgInvalidRedirectTargets ] as $target ) { |
3532 | if ( $this->isSpecial( $target ) ) { |
3533 | return false; |
3534 | } |
3535 | } |
3536 | return true; |
3537 | } |
3538 | |
3539 | // relative section links are not valid redirect targets (T278367) |
3540 | return $this->getDBkey() !== '' && $this->isValid(); |
3541 | } |
3542 | |
3543 | /** |
3544 | * Whether the magic words __INDEX__ and __NOINDEX__ function for this page. |
3545 | * |
3546 | * @return bool |
3547 | */ |
3548 | public function canUseNoindex() { |
3549 | global $wgExemptFromUserRobotsControl; |
3550 | |
3551 | $bannedNamespaces = $wgExemptFromUserRobotsControl ?? |
3552 | MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces(); |
3553 | |
3554 | return !in_array( $this->mNamespace, $bannedNamespaces ); |
3555 | } |
3556 | |
3557 | /** |
3558 | * Returns the raw sort key to be used for categories, with the specified |
3559 | * prefix. This will be fed to Collation::getSortKey() to get a |
3560 | * binary sortkey that can be used for actual sorting. |
3561 | * |
3562 | * @param string $prefix The prefix to be used, specified using |
3563 | * {{defaultsort:}} or like [[Category:Foo|prefix]]. Empty for no |
3564 | * prefix. |
3565 | * @return string |
3566 | */ |
3567 | public function getCategorySortkey( $prefix = '' ) { |
3568 | $unprefixed = $this->getText(); |
3569 | |
3570 | // Anything that uses this hook should only depend |
3571 | // on the Title object passed in, and should probably |
3572 | // tell the users to run updateCollations.php --force |
3573 | // in order to re-sort existing category relations. |
3574 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) ) |
3575 | ->onGetDefaultSortkey( $this, $unprefixed ); |
3576 | if ( $prefix !== '' ) { |
3577 | # Separate with a line feed, so the unprefixed part is only used as |
3578 | # a tiebreaker when two pages have the exact same prefix. |
3579 | # In UCA, tab is the only character that can sort above LF |
3580 | # so we strip both of them from the original prefix. |
3581 | $prefix = strtr( $prefix, "\n\t", ' ' ); |
3582 | return "$prefix\n$unprefixed"; |
3583 | } |
3584 | return $unprefixed; |
3585 | } |
3586 | |
3587 | /** |
3588 | * Returns the page language code saved in the database, if $wgPageLanguageUseDB is set |
3589 | * to true in LocalSettings.php, otherwise returns null. If there is no language saved in |
3590 | * the database, it will return null. |
3591 | * |
3592 | * @param int $flags |
3593 | * |
3594 | * @return ?string |
3595 | */ |
3596 | private function getDbPageLanguageCode( int $flags = 0 ): ?string { |
3597 | global $wgPageLanguageUseDB; |
3598 | |
3599 | // check, if the page language could be saved in the database, and if so and |
3600 | // the value is not requested already, lookup the page language using PageStore |
3601 | if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) { |
3602 | $this->mDbPageLanguage = $this->getFieldFromPageStore( 'page_lang', $flags ) ?: null; |
3603 | } |
3604 | |
3605 | return $this->mDbPageLanguage ?: null; |
3606 | } |
3607 | |
3608 | /** |
3609 | * Returns the Language object from the page language code saved in the database. |
3610 | * If $wgPageLanguageUseDB is set to false or there is no language saved in the database |
3611 | * or the language code in the database is invalid or unsupported, it will return null. |
3612 | * |
3613 | * @return Language|null |
3614 | */ |
3615 | private function getDbPageLanguage(): ?Language { |
3616 | $languageCode = $this->getDbPageLanguageCode(); |
3617 | if ( $languageCode === null ) { |
3618 | return null; |
3619 | } |
3620 | $services = MediaWikiServices::getInstance(); |
3621 | if ( !$services->getLanguageNameUtils()->isKnownLanguageTag( $languageCode ) ) { |
3622 | return null; |
3623 | } |
3624 | return $services->getLanguageFactory()->getLanguage( $languageCode ); |
3625 | } |
3626 | |
3627 | /** |
3628 | * Get the language in which the content of this page is written in |
3629 | * wikitext. Defaults to content language, but in certain cases it can be |
3630 | * e.g. $wgLang (such as special pages, which are in the user language). |
3631 | * |
3632 | * @since 1.18 |
3633 | * @return Language |
3634 | */ |
3635 | public function getPageLanguage() { |
3636 | global $wgLanguageCode; |
3637 | if ( $this->isSpecialPage() ) { |
3638 | // special pages are in the user language |
3639 | return RequestContext::getMain()->getLanguage(); |
3640 | } |
3641 | |
3642 | // Checking if DB language is set |
3643 | $dbPageLanguage = $this->getDbPageLanguage(); |
3644 | if ( $dbPageLanguage ) { |
3645 | return $dbPageLanguage; |
3646 | } |
3647 | |
3648 | $services = MediaWikiServices::getInstance(); |
3649 | if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) { |
3650 | // Note that this may depend on user settings, so the cache should |
3651 | // be only per-request. |
3652 | // NOTE: ContentHandler::getPageLanguage() may need to load the |
3653 | // content to determine the page language! |
3654 | // Checking $wgLanguageCode hasn't changed for the benefit of unit |
3655 | // tests. |
3656 | $contentHandler = $services->getContentHandlerFactory() |
3657 | ->getContentHandler( $this->getContentModel() ); |
3658 | $langObj = $contentHandler->getPageLanguage( $this ); |
3659 | $this->mPageLanguage = [ $langObj->getCode(), $wgLanguageCode ]; |
3660 | } else { |
3661 | $langObj = $services->getLanguageFactory() |
3662 | ->getLanguage( $this->mPageLanguage[0] ); |
3663 | } |
3664 | |
3665 | return $langObj; |
3666 | } |
3667 | |
3668 | /** |
3669 | * Get the language in which the content of this page is written when |
3670 | * viewed by user. Defaults to content language, but in certain cases it can be |
3671 | * e.g. the user language (such as special pages). |
3672 | * |
3673 | * @deprecated since 1.42 Use ParserOutput::getLanguage instead. See also OutputPage::getContLangForJS. |
3674 | * @since 1.20 |
3675 | * @return Language |
3676 | */ |
3677 | public function getPageViewLanguage() { |
3678 | $services = MediaWikiServices::getInstance(); |
3679 | |
3680 | if ( $this->isSpecialPage() ) { |
3681 | // If the user chooses a variant, the content is actually |
3682 | // in a language whose code is the variant code. |
3683 | $userLang = RequestContext::getMain()->getLanguage(); |
3684 | $variant = $this->getLanguageConverter( $userLang )->getPreferredVariant(); |
3685 | if ( $userLang->getCode() !== $variant ) { |
3686 | return $services->getLanguageFactory() |
3687 | ->getLanguage( $variant ); |
3688 | } |
3689 | |
3690 | return $userLang; |
3691 | } |
3692 | |
3693 | // Checking if DB language is set |
3694 | $pageLang = $this->getDbPageLanguage(); |
3695 | if ( $pageLang ) { |
3696 | $variant = $this->getLanguageConverter( $pageLang )->getPreferredVariant(); |
3697 | if ( $pageLang->getCode() !== $variant ) { |
3698 | return $services->getLanguageFactory() |
3699 | ->getLanguage( $variant ); |
3700 | } |
3701 | |
3702 | return $pageLang; |
3703 | } |
3704 | |
3705 | // @note Can't be cached persistently, depends on user settings. |
3706 | // @note ContentHandler::getPageViewLanguage() may need to load the |
3707 | // content to determine the page language! |
3708 | $contentHandler = $services->getContentHandlerFactory() |
3709 | ->getContentHandler( $this->getContentModel() ); |
3710 | $pageLang = $contentHandler->getPageViewLanguage( $this ); |
3711 | return $pageLang; |
3712 | } |
3713 | |
3714 | /** |
3715 | * Get a list of rendered edit notices for this page. |
3716 | * |
3717 | * Array is keyed by the original message key, and values are rendered using parseAsBlock, so |
3718 | * they will already be wrapped in paragraphs. |
3719 | * |
3720 | * @since 1.21 |
3721 | * @param int $oldid Revision ID that's being edited |
3722 | * @return string[] |
3723 | */ |
3724 | public function getEditNotices( $oldid = 0 ) { |
3725 | $notices = []; |
3726 | |
3727 | $editnotice_base = 'editnotice-' . $this->mNamespace; |
3728 | // Optional notice for the entire namespace |
3729 | $messages = [ $editnotice_base => 'namespace' ]; |
3730 | |
3731 | if ( |
3732 | MediaWikiServices::getInstance()->getNamespaceInfo()-> |
3733 | hasSubpages( $this->mNamespace ) |
3734 | ) { |
3735 | // Optional notice for page itself and any parent page |
3736 | foreach ( explode( '/', $this->mDbkeyform ) as $part ) { |
3737 | $editnotice_base .= '-' . $part; |
3738 | $messages[$editnotice_base] = 'base'; |
3739 | } |
3740 | } else { |
3741 | // Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys |
3742 | $messages[$editnotice_base . '-' . strtr( $this->mDbkeyform, '/', '-' )] = 'page'; |
3743 | } |
3744 | |
3745 | foreach ( $messages as $editnoticeText => $class ) { |
3746 | // The following messages are used here: |
3747 | // * editnotice-0 |
3748 | // * editnotice-0-Title |
3749 | // * editnotice-0-Title-Subpage |
3750 | // * editnotice-… |
3751 | $msg = wfMessage( $editnoticeText )->page( $this ); |
3752 | if ( $msg->exists() ) { |
3753 | $html = $msg->parseAsBlock(); |
3754 | // Edit notices may have complex logic, but output nothing (T91715) |
3755 | if ( trim( $html ) !== '' ) { |
3756 | $notices[$editnoticeText] = Html::rawElement( |
3757 | 'div', |
3758 | [ 'class' => [ |
3759 | 'mw-editnotice', |
3760 | // The following classes are used here: |
3761 | // * mw-editnotice-namespace |
3762 | // * mw-editnotice-base |
3763 | // * mw-editnotice-page |
3764 | "mw-editnotice-$class", |
3765 | // The following classes are used here: |
3766 | // * mw-editnotice-0 |
3767 | // * mw-editnotice-… |
3768 | Sanitizer::escapeClass( "mw-$editnoticeText" ) |
3769 | ] ], |
3770 | $html |
3771 | ); |
3772 | } |
3773 | } |
3774 | } |
3775 | |
3776 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) ) |
3777 | ->onTitleGetEditNotices( $this, $oldid, $notices ); |
3778 | return $notices; |
3779 | } |
3780 | |
3781 | /** |
3782 | * @param string $field |
3783 | * @param int $flags Bitfield of IDBAccessObject::READ_* constants |
3784 | * @return string|false |
3785 | */ |
3786 | private function getFieldFromPageStore( $field, $flags ) { |
3787 | $pageStore = MediaWikiServices::getInstance()->getPageStore(); |
3788 | |
3789 | if ( !in_array( $field, $pageStore->getSelectFields(), true ) ) { |
3790 | throw new InvalidArgumentException( "Unknown field: $field" ); |
3791 | } |
3792 | |
3793 | if ( $flags === IDBAccessObject::READ_NORMAL && $this->mArticleID === 0 ) { |
3794 | // page does not exist |
3795 | return false; |
3796 | } |
3797 | |
3798 | if ( !$this->canExist() ) { |
3799 | return false; |
3800 | } |
3801 | |
3802 | $page = $pageStore->getPageByReference( $this, $flags ); |
3803 | |
3804 | if ( $page instanceof PageStoreRecord ) { |
3805 | return $page->getField( $field ); |
3806 | } else { |
3807 | // The page record failed to load, remember the page as non-existing. |
3808 | // Note that this can happen even if a page ID was known before under some |
3809 | // rare circumstances, if this method is called with the READ_LATEST bit set |
3810 | // and the page has been deleted since the ID had initially been determined. |
3811 | $this->mArticleID = 0; |
3812 | return false; |
3813 | } |
3814 | } |
3815 | |
3816 | /** |
3817 | * @return array |
3818 | */ |
3819 | public function __sleep() { |
3820 | return [ |
3821 | 'mNamespace', |
3822 | 'mDbkeyform', |
3823 | 'mFragment', |
3824 | 'mInterwiki', |
3825 | 'mLocalInterwiki', |
3826 | ]; |
3827 | } |
3828 | |
3829 | public function __wakeup() { |
3830 | $this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0; |
3831 | $this->mUrlform = wfUrlencode( $this->mDbkeyform ); |
3832 | $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' ); |
3833 | } |
3834 | |
3835 | public function __clone() { |
3836 | $this->mInstanceCacheKey = null; |
3837 | } |
3838 | |
3839 | /** |
3840 | * Returns false to indicate that this Title belongs to the local wiki. |
3841 | * |
3842 | * @note The behavior of this method is considered undefined for interwiki links. |
3843 | * At the moment, this method always returns false. But this may change in the future. |
3844 | * |
3845 | * @since 1.36 |
3846 | * @return string|false Always self::LOCAL |
3847 | */ |
3848 | public function getWikiId() { |
3849 | return self::LOCAL; |
3850 | } |
3851 | |
3852 | /** |
3853 | * Returns the page ID. |
3854 | * |
3855 | * If this ID is 0, this means the page does not exist. |
3856 | * |
3857 | * @see getArticleID() |
3858 | * @since 1.36, since 1.35.6 as an alias of getArticleID() |
3859 | * |
3860 | * @param string|false $wikiId The wiki ID expected by the caller. |
3861 | * |
3862 | * @throws PreconditionException if this Title instance does not represent a proper page, |
3863 | * that is, if it is a section link, interwiki link, link to a special page, or such. |
3864 | * @throws PreconditionException if $wikiId is not false. |
3865 | * |
3866 | * @return int |
3867 | */ |
3868 | public function getId( $wikiId = self::LOCAL ): int { |
3869 | $this->assertWiki( $wikiId ); |
3870 | $this->assertProperPage(); |
3871 | return $this->getArticleID(); |
3872 | } |
3873 | |
3874 | /** |
3875 | * Code that requires this Title to be a "proper page" in the sense |
3876 | * defined by PageIdentity should call this method. |
3877 | * |
3878 | * For the purpose of the Title class, a proper page is one that can |
3879 | * exist in the page table. That is, a Title represents a proper page |
3880 | * if canExist() returns true. |
3881 | * |
3882 | * @see canExist() |
3883 | * |
3884 | * @throws PreconditionException |
3885 | */ |
3886 | private function assertProperPage() { |
3887 | Assert::precondition( |
3888 | $this->canExist(), |
3889 | 'This Title instance does not represent a proper page, but merely a link target.' |
3890 | ); |
3891 | } |
3892 | |
3893 | /** |
3894 | * Returns the page represented by this Title as a ProperPageIdentity. |
3895 | * The ProperPageIdentity returned by this method is guaranteed to be immutable. |
3896 | * If this Title does not represent a proper page, an exception is thrown. |
3897 | * |
3898 | * It is preferred to use this method rather than using the Title as a PageIdentity directly. |
3899 | * |
3900 | * @return ProperPageIdentity |
3901 | * @throws PreconditionException if the page is not a proper page, that is, if it is a section |
3902 | * link, interwiki link, link to a special page, or such. |
3903 | * @since 1.36 |
3904 | */ |
3905 | public function toPageIdentity(): ProperPageIdentity { |
3906 | // TODO: replace individual member fields with a PageIdentityValue that is always present |
3907 | |
3908 | $this->assertProperPage(); |
3909 | |
3910 | return new PageIdentityValue( |
3911 | $this->getId(), |
3912 | $this->getNamespace(), |
3913 | $this->getDBkey(), |
3914 | $this->getWikiId() |
3915 | ); |
3916 | } |
3917 | |
3918 | /** |
3919 | * Returns the page represented by this Title as a ProperPageRecord. |
3920 | * The PageRecord returned by this method is guaranteed to be immutable, |
3921 | * the page is guaranteed to exist. |
3922 | * |
3923 | * @note For now, this method queries the database on every call. |
3924 | * @since 1.36 |
3925 | * |
3926 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
3927 | * |
3928 | * @return ExistingPageRecord |
3929 | * @throws PreconditionException if the page does not exist, or is not a proper page, |
3930 | * that is, if it is a section link, interwiki link, link to a special page, or such. |
3931 | */ |
3932 | public function toPageRecord( $flags = 0 ): ExistingPageRecord { |
3933 | // TODO: Cache this? Construct is more efficiently? |
3934 | |
3935 | $this->assertProperPage(); |
3936 | |
3937 | Assert::precondition( |
3938 | $this->exists( $flags ), |
3939 | 'This Title instance does not represent an existing page: ' . $this |
3940 | ); |
3941 | |
3942 | return new PageStoreRecord( |
3943 | (object)[ |
3944 | 'page_id' => $this->getArticleID( $flags ), |
3945 | 'page_namespace' => $this->getNamespace(), |
3946 | 'page_title' => $this->getDBkey(), |
3947 | 'page_wiki_id' => $this->getWikiId(), |
3948 | 'page_latest' => $this->getLatestRevID( $flags ), |
3949 | 'page_is_new' => $this->isNewPage( $flags ), |
3950 | 'page_is_redirect' => $this->isRedirect( $flags ), |
3951 | 'page_touched' => $this->getTouched( $flags ), |
3952 | 'page_lang' => $this->getDbPageLanguageCode( $flags ), |
3953 | ], |
3954 | PageIdentity::LOCAL |
3955 | ); |
3956 | } |
3957 | |
3958 | } |
3959 | |
3960 | /** @deprecated class alias since 1.40 */ |
3961 | class_alias( Title::class, 'Title' ); |