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