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