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