Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F7535
Article.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
66 KB
Referenced Files
None
Subscribers
None
Article.php
View Options
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use
MediaWiki\Block\DatabaseBlock
;
use
MediaWiki\Block\DatabaseBlockStore
;
use
MediaWiki\CommentFormatter\CommentFormatter
;
use
MediaWiki\Context\IContextSource
;
use
MediaWiki\Context\RequestContext
;
use
MediaWiki\EditPage\EditPage
;
use
MediaWiki\HookContainer\HookRunner
;
use
MediaWiki\HookContainer\ProtectedHookAccessorTrait
;
use
MediaWiki\Html\Html
;
use
MediaWiki\Language\Language
;
use
MediaWiki\Linker\Linker
;
use
MediaWiki\Linker\LinkRenderer
;
use
MediaWiki\MainConfigNames
;
use
MediaWiki\MediaWikiServices
;
use
MediaWiki\Message\Message
;
use
MediaWiki\Output\OutputPage
;
use
MediaWiki\Page\ParserOutputAccess
;
use
MediaWiki\Page\ProtectionForm
;
use
MediaWiki\Page\WikiPageFactory
;
use
MediaWiki\Parser\Parser
;
use
MediaWiki\Parser\ParserOptions
;
use
MediaWiki\Parser\ParserOutput
;
use
MediaWiki\Permissions\Authority
;
use
MediaWiki\Permissions\PermissionStatus
;
use
MediaWiki\Permissions\RestrictionStore
;
use
MediaWiki\Revision\ArchivedRevisionLookup
;
use
MediaWiki\Revision\BadRevisionException
;
use
MediaWiki\Revision\RevisionRecord
;
use
MediaWiki\Revision\RevisionStore
;
use
MediaWiki\Revision\SlotRecord
;
use
MediaWiki\Status\Status
;
use
MediaWiki\Title\Title
;
use
MediaWiki\User\Options\UserOptionsLookup
;
use
MediaWiki\User\User
;
use
MediaWiki\User\UserIdentity
;
use
MediaWiki\User\UserNameUtils
;
use
MediaWiki\Xml\Xml
;
use
Wikimedia\IPUtils
;
use
Wikimedia\LightweightObjectStore\ExpirationAwareness
;
use
Wikimedia\NonSerializable\NonSerializableTrait
;
use
Wikimedia\Rdbms\IConnectionProvider
;
/**
* Legacy class representing an editable page and handling UI for some page actions.
*
* This has largely been superseded by WikiPage, with Action subclasses for the
* user interface of page actions, and service classes for their backend logic.
*
* @todo Move and refactor remaining code
* @todo Deprecate
*/
class
Article
implements
Page
{
use
ProtectedHookAccessorTrait
;
use
NonSerializableTrait
;
/**
* @var IContextSource|null The context this Article is executed in.
* If null, RequestContext::getMain() is used.
* @deprecated since 1.35, must be private, use {@link getContext}
*/
protected
$mContext
;
/** @var WikiPage The WikiPage object of this instance */
protected
$mPage
;
/**
* @var int|null The oldid of the article that was requested to be shown,
* 0 for the current revision.
*/
public
$mOldId
;
/** @var Title|null Title from which we were redirected here, if any. */
public
$mRedirectedFrom
=
null
;
/** @var string|false URL to redirect to or false if none */
public
$mRedirectUrl
=
false
;
/**
* @var Status|null represents the outcome of fetchRevisionRecord().
* $fetchResult->value is the RevisionRecord object, if the operation was successful.
*/
private
$fetchResult
=
null
;
/**
* @var ParserOutput|null|false The ParserOutput generated for viewing the page,
* initialized by view(). If no ParserOutput could be generated, this is set to false.
* @deprecated since 1.32
*/
public
$mParserOutput
=
null
;
/**
* @var bool Whether render() was called. With the way subclasses work
* here, there doesn't seem to be any other way to stop calling
* OutputPage::enableSectionEditLinks() and still have it work as it did before.
*/
protected
$viewIsRenderAction
=
false
;
protected
LinkRenderer
$linkRenderer
;
private
RevisionStore
$revisionStore
;
private
UserNameUtils
$userNameUtils
;
private
UserOptionsLookup
$userOptionsLookup
;
private
CommentFormatter
$commentFormatter
;
private
WikiPageFactory
$wikiPageFactory
;
private
JobQueueGroup
$jobQueueGroup
;
private
ArchivedRevisionLookup
$archivedRevisionLookup
;
protected
IConnectionProvider
$dbProvider
;
protected
DatabaseBlockStore
$blockStore
;
protected
RestrictionStore
$restrictionStore
;
/**
* @var RevisionRecord|null Revision to be shown
*
* Initialized by getOldIDFromRequest() or fetchRevisionRecord(). While the output of
* Article::view is typically based on this revision, it may be replaced by extensions.
*/
private
$mRevisionRecord
=
null
;
/**
* @param Title $title
* @param int|null $oldId Revision ID, null to fetch from request, zero for current
*/
public
function
__construct
(
Title
$title
,
$oldId
=
null
)
{
$this
->
mOldId
=
$oldId
;
$this
->
mPage
=
$this
->
newPage
(
$title
);
$services
=
MediaWikiServices
::
getInstance
();
$this
->
linkRenderer
=
$services
->
getLinkRenderer
();
$this
->
revisionStore
=
$services
->
getRevisionStore
();
$this
->
userNameUtils
=
$services
->
getUserNameUtils
();
$this
->
userOptionsLookup
=
$services
->
getUserOptionsLookup
();
$this
->
commentFormatter
=
$services
->
getCommentFormatter
();
$this
->
wikiPageFactory
=
$services
->
getWikiPageFactory
();
$this
->
jobQueueGroup
=
$services
->
getJobQueueGroup
();
$this
->
archivedRevisionLookup
=
$services
->
getArchivedRevisionLookup
();
$this
->
dbProvider
=
$services
->
getConnectionProvider
();
$this
->
blockStore
=
$services
->
getDatabaseBlockStore
();
$this
->
restrictionStore
=
$services
->
getRestrictionStore
();
}
/**
* @param Title $title
* @return WikiPage
*/
protected
function
newPage
(
Title
$title
)
{
return
new
WikiPage
(
$title
);
}
/**
* Constructor from a page id
* @param int $id Article ID to load
* @return Article|null
*/
public
static
function
newFromID
(
$id
)
{
$t
=
Title
::
newFromID
(
$id
);
return
$t
===
null
?
null
:
new
static
(
$t
);
}
/**
* Create an Article object of the appropriate class for the given page.
*
* @param Title $title
* @param IContextSource $context
* @return Article
*/
public
static
function
newFromTitle
(
$title
,
IContextSource
$context
):
self
{
if
(
$title
->
getNamespace
()
===
NS_MEDIA
)
{
// XXX: This should not be here, but where should it go?
$title
=
Title
::
makeTitle
(
NS_FILE
,
$title
->
getDBkey
()
);
}
$page
=
null
;
(
new
HookRunner
(
MediaWikiServices
::
getInstance
()->
getHookContainer
()
)
)
// @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
->
onArticleFromTitle
(
$title
,
$page
,
$context
);
if
(
!
$page
)
{
switch
(
$title
->
getNamespace
()
)
{
case
NS_FILE
:
$page
=
new
ImagePage
(
$title
);
break
;
case
NS_CATEGORY
:
$page
=
new
CategoryPage
(
$title
);
break
;
default
:
$page
=
new
Article
(
$title
);
}
}
$page
->
setContext
(
$context
);
return
$page
;
}
/**
* Create an Article object of the appropriate class for the given page.
*
* @param WikiPage $page
* @param IContextSource $context
* @return Article
*/
public
static
function
newFromWikiPage
(
WikiPage
$page
,
IContextSource
$context
)
{
$article
=
self
::
newFromTitle
(
$page
->
getTitle
(),
$context
);
$article
->
mPage
=
$page
;
// override to keep process cached vars
return
$article
;
}
/**
* Get the page this view was redirected from
* @return Title|null
* @since 1.28
*/
public
function
getRedirectedFrom
()
{
return
$this
->
mRedirectedFrom
;
}
/**
* Tell the page view functions that this view was redirected
* from another page on the wiki.
* @param Title $from
*/
public
function
setRedirectedFrom
(
Title
$from
)
{
$this
->
mRedirectedFrom
=
$from
;
}
/**
* Get the title object of the article
*
* @return Title Title object of this page
*/
public
function
getTitle
()
{
return
$this
->
mPage
->
getTitle
();
}
/**
* Get the WikiPage object of this instance
*
* @since 1.19
* @return WikiPage
*/
public
function
getPage
()
{
return
$this
->
mPage
;
}
public
function
clear
()
{
$this
->
mRedirectedFrom
=
null
;
# Title object if set
$this
->
mRedirectUrl
=
false
;
$this
->
mRevisionRecord
=
null
;
$this
->
fetchResult
=
null
;
// TODO hard-deprecate direct access to public fields
$this
->
mPage
->
clear
();
}
/**
* @see getOldIDFromRequest()
* @see getRevIdFetched()
*
* @return int The oldid of the article that is was requested in the constructor or via the
* context's WebRequest.
*/
public
function
getOldID
()
{
if
(
$this
->
mOldId
===
null
)
{
$this
->
mOldId
=
$this
->
getOldIDFromRequest
();
}
return
$this
->
mOldId
;
}
/**
* Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect
*
* @return int The old id for the request
*/
public
function
getOldIDFromRequest
()
{
$this
->
mRedirectUrl
=
false
;
$request
=
$this
->
getContext
()->
getRequest
();
$oldid
=
$request
->
getIntOrNull
(
'oldid'
);
if
(
$oldid
===
null
)
{
return
0
;
}
if
(
$oldid
!==
0
)
{
# Load the given revision and check whether the page is another one.
# In that case, update this instance to reflect the change.
if
(
$oldid
===
$this
->
mPage
->
getLatest
()
)
{
$this
->
mRevisionRecord
=
$this
->
mPage
->
getRevisionRecord
();
}
else
{
$this
->
mRevisionRecord
=
$this
->
revisionStore
->
getRevisionById
(
$oldid
);
if
(
$this
->
mRevisionRecord
!==
null
)
{
$revPageId
=
$this
->
mRevisionRecord
->
getPageId
();
// Revision title doesn't match the page title given?
if
(
$this
->
mPage
->
getId
()
!==
$revPageId
)
{
$this
->
mPage
=
$this
->
wikiPageFactory
->
newFromID
(
$revPageId
);
}
}
}
}
$oldRev
=
$this
->
mRevisionRecord
;
if
(
$request
->
getRawVal
(
'direction'
)
===
'next'
)
{
$nextid
=
0
;
if
(
$oldRev
)
{
$nextRev
=
$this
->
revisionStore
->
getNextRevision
(
$oldRev
);
if
(
$nextRev
)
{
$nextid
=
$nextRev
->
getId
();
}
}
if
(
$nextid
)
{
$oldid
=
$nextid
;
$this
->
mRevisionRecord
=
null
;
}
else
{
$this
->
mRedirectUrl
=
$this
->
getTitle
()->
getFullURL
(
'redirect=no'
);
}
}
elseif
(
$request
->
getRawVal
(
'direction'
)
===
'prev'
)
{
$previd
=
0
;
if
(
$oldRev
)
{
$prevRev
=
$this
->
revisionStore
->
getPreviousRevision
(
$oldRev
);
if
(
$prevRev
)
{
$previd
=
$prevRev
->
getId
();
}
}
if
(
$previd
)
{
$oldid
=
$previd
;
$this
->
mRevisionRecord
=
null
;
}
}
return
$oldid
;
}
/**
* Fetches the revision to work on.
* The revision is loaded from the database. Refer to $this->fetchResult for the revision
* or any errors encountered while loading it.
*
* Public since 1.35
*
* @return RevisionRecord|null
*/
public
function
fetchRevisionRecord
()
{
if
(
$this
->
fetchResult
)
{
return
$this
->
mRevisionRecord
;
}
$oldid
=
$this
->
getOldID
();
// $this->mRevisionRecord might already be fetched by getOldIDFromRequest()
if
(
!
$this
->
mRevisionRecord
)
{
if
(
!
$oldid
)
{
$this
->
mRevisionRecord
=
$this
->
mPage
->
getRevisionRecord
();
if
(
!
$this
->
mRevisionRecord
)
{
wfDebug
(
__METHOD__
.
" failed to find page data for title "
.
$this
->
getTitle
()->
getPrefixedText
()
);
// Output for this case is done by showMissingArticle().
$this
->
fetchResult
=
Status
::
newFatal
(
'noarticletext'
);
return
null
;
}
}
else
{
$this
->
mRevisionRecord
=
$this
->
revisionStore
->
getRevisionById
(
$oldid
);
if
(
!
$this
->
mRevisionRecord
)
{
wfDebug
(
__METHOD__
.
" failed to load revision, rev_id $oldid"
);
$this
->
fetchResult
=
Status
::
newFatal
(
$this
->
getMissingRevisionMsg
(
$oldid
)
);
return
null
;
}
}
}
if
(
!
$this
->
mRevisionRecord
->
userCan
(
RevisionRecord
::
DELETED_TEXT
,
$this
->
getContext
()->
getAuthority
()
)
)
{
wfDebug
(
__METHOD__
.
" failed to retrieve content of revision "
.
$this
->
mRevisionRecord
->
getId
()
);
// Output for this case is done by showDeletedRevisionHeader().
// title used in wikilinks, should not contain whitespaces
$this
->
fetchResult
=
new
Status
;
$title
=
$this
->
getTitle
()->
getPrefixedDBkey
();
if
(
$this
->
mRevisionRecord
->
isDeleted
(
RevisionRecord
::
DELETED_RESTRICTED
)
)
{
$this
->
fetchResult
->
fatal
(
'rev-suppressed-text'
);
}
else
{
$this
->
fetchResult
->
fatal
(
'rev-deleted-text-permission'
,
$title
);
}
return
null
;
}
$this
->
fetchResult
=
Status
::
newGood
(
$this
->
mRevisionRecord
);
return
$this
->
mRevisionRecord
;
}
/**
* Returns true if the currently-referenced revision is the current edit
* to this page (and it exists).
* @return bool
*/
public
function
isCurrent
()
{
# If no oldid, this is the current version.
if
(
$this
->
getOldID
()
==
0
)
{
return
true
;
}
return
$this
->
mPage
->
exists
()
&&
$this
->
mRevisionRecord
&&
$this
->
mRevisionRecord
->
isCurrent
();
}
/**
* Use this to fetch the rev ID used on page views
*
* Before fetchRevisionRecord was called, this returns the page's latest revision,
* regardless of what getOldID() returns.
*
* @return int Revision ID of last article revision
*/
public
function
getRevIdFetched
()
{
if
(
$this
->
fetchResult
&&
$this
->
fetchResult
->
isOK
()
)
{
/** @var RevisionRecord $rev */
$rev
=
$this
->
fetchResult
->
getValue
();
return
$rev
->
getId
();
}
else
{
return
$this
->
mPage
->
getLatest
();
}
}
/**
* This is the default action of the index.php entry point: just view the
* page of the given title.
*/
public
function
view
()
{
$context
=
$this
->
getContext
();
$useFileCache
=
$context
->
getConfig
()->
get
(
MainConfigNames
::
UseFileCache
);
# Get variables from query string
# As side effect this will load the revision and update the title
# in a revision ID is passed in the request, so this should remain
# the first call of this method even if $oldid is used way below.
$oldid
=
$this
->
getOldID
();
$authority
=
$context
->
getAuthority
();
# Another check in case getOldID() is altering the title
$permissionStatus
=
PermissionStatus
::
newEmpty
();
if
(
!
$authority
->
authorizeRead
(
'read'
,
$this
->
getTitle
(),
$permissionStatus
)
)
{
wfDebug
(
__METHOD__
.
": denied on secondary read check"
);
throw
new
PermissionsError
(
'read'
,
$permissionStatus
);
}
$outputPage
=
$context
->
getOutput
();
# getOldID() may as well want us to redirect somewhere else
if
(
$this
->
mRedirectUrl
)
{
$outputPage
->
redirect
(
$this
->
mRedirectUrl
);
wfDebug
(
__METHOD__
.
": redirecting due to oldid"
);
return
;
}
# If we got diff in the query, we want to see a diff page instead of the article.
if
(
$context
->
getRequest
()->
getCheck
(
'diff'
)
)
{
wfDebug
(
__METHOD__
.
": showing diff page"
);
$this
->
showDiffPage
();
return
;
}
$this
->
showProtectionIndicator
();
# Set page title (may be overridden from ParserOutput if title conversion is enabled or DISPLAYTITLE is used)
$outputPage
->
setPageTitle
(
Parser
::
formatPageTitle
(
str_replace
(
'_'
,
' '
,
$this
->
getTitle
()->
getNsText
()
),
':'
,
$this
->
getTitle
()->
getText
()
)
);
$outputPage
->
setArticleFlag
(
true
);
# Allow frames by default
$outputPage
->
getMetadata
()->
setPreventClickjacking
(
false
);
$parserOptions
=
$this
->
getParserOptions
();
$poOptions
=
[];
# Allow extensions to vary parser options used for article rendering
(
new
HookRunner
(
MediaWikiServices
::
getInstance
()->
getHookContainer
()
)
)
->
onArticleParserOptions
(
$this
,
$parserOptions
);
# Render printable version, use printable version cache
if
(
$outputPage
->
isPrintable
()
)
{
$parserOptions
->
setIsPrintable
(
true
);
$poOptions
[
'enableSectionEditLinks'
]
=
false
;
$outputPage
->
prependHTML
(
Html
::
warningBox
(
$outputPage
->
msg
(
'printableversion-deprecated-warning'
)->
escaped
()
)
);
}
elseif
(
$this
->
viewIsRenderAction
||
!
$this
->
isCurrent
()
||
!
$authority
->
probablyCan
(
'edit'
,
$this
->
getTitle
()
)
)
{
$poOptions
[
'enableSectionEditLinks'
]
=
false
;
}
# Try client and file cache
if
(
$oldid
===
0
&&
$this
->
mPage
->
checkTouched
()
)
{
# Try to stream the output from file cache
if
(
$useFileCache
&&
$this
->
tryFileCache
()
)
{
wfDebug
(
__METHOD__
.
": done file cache"
);
# tell wgOut that output is taken care of
$outputPage
->
disable
();
$this
->
mPage
->
doViewUpdates
(
$authority
,
$oldid
);
return
;
}
}
$this
->
showRedirectedFromHeader
();
$this
->
showNamespaceHeader
();
if
(
$this
->
viewIsRenderAction
)
{
$poOptions
+=
[
'absoluteURLs'
=>
true
];
}
$poOptions
+=
[
'includeDebugInfo'
=>
true
];
try
{
$continue
=
$this
->
generateContentOutput
(
$authority
,
$parserOptions
,
$oldid
,
$outputPage
,
$poOptions
);
}
catch
(
BadRevisionException
$e
)
{
$continue
=
false
;
$this
->
showViewError
(
wfMessage
(
'badrevision'
)->
text
()
);
}
if
(
!
$continue
)
{
return
;
}
# For the main page, overwrite the <title> element with the con-
# tents of 'pagetitle-view-mainpage' instead of the default (if
# that's not empty).
# This message always exists because it is in the i18n files
if
(
$this
->
getTitle
()->
isMainPage
()
)
{
$msg
=
$context
->
msg
(
'pagetitle-view-mainpage'
)->
inContentLanguage
();
if
(
!
$msg
->
isDisabled
()
)
{
$outputPage
->
setHTMLTitle
(
$msg
->
text
()
);
}
}
# Use adaptive TTLs for CDN so delayed/failed purges are noticed less often.
# This could use getTouched(), but that could be scary for major template edits.
$outputPage
->
adaptCdnTTL
(
$this
->
mPage
->
getTimestamp
(),
ExpirationAwareness
::
TTL_DAY
);
$this
->
showViewFooter
();
$this
->
mPage
->
doViewUpdates
(
$authority
,
$oldid
,
$this
->
fetchRevisionRecord
()
);
# Load the postEdit module if the user just saved this revision
# See also EditPage::setPostEditCookie
$request
=
$context
->
getRequest
();
$cookieKey
=
EditPage
::
POST_EDIT_COOKIE_KEY_PREFIX
.
$this
->
getRevIdFetched
();
$postEdit
=
$request
->
getCookie
(
$cookieKey
);
if
(
$postEdit
)
{
# Clear the cookie. This also prevents caching of the response.
$request
->
response
()->
clearCookie
(
$cookieKey
);
$outputPage
->
addJsConfigVars
(
'wgPostEdit'
,
$postEdit
);
$outputPage
->
addModules
(
'mediawiki.action.view.postEdit'
);
// FIXME: test this
if
(
$this
->
getContext
()->
getConfig
()->
get
(
MainConfigNames
::
EnableEditRecovery
)
&&
$this
->
userOptionsLookup
->
getOption
(
$this
->
getContext
()->
getUser
(),
'editrecovery'
)
)
{
$outputPage
->
addModules
(
'mediawiki.editRecovery.postEdit'
);
}
}
}
/**
* Show a lock icon above the article body if the page is protected.
*/
public
function
showProtectionIndicator
():
void
{
$title
=
$this
->
getTitle
();
$context
=
$this
->
getContext
();
$outputPage
=
$context
->
getOutput
();
$protectionIndicatorsAreEnabled
=
$context
->
getConfig
()
->
get
(
MainConfigNames
::
EnableProtectionIndicators
);
if
(
!
$protectionIndicatorsAreEnabled
||
$title
->
isMainPage
()
)
{
return
;
}
$protection
=
$this
->
restrictionStore
->
getRestrictions
(
$title
,
'edit'
);
$cascadeProtection
=
$this
->
restrictionStore
->
getCascadeProtectionSources
(
$title
)[
1
];
$isCascadeProtected
=
array_key_exists
(
'edit'
,
$cascadeProtection
);
if
(
!
$protection
&&
!
$isCascadeProtected
)
{
return
;
}
if
(
$isCascadeProtected
)
{
// Cascade-protected pages are protected at the sysop level. So it
// should not matter if we take the protection level of the first
// or last page that is being cascaded to the current page.
$protectionLevel
=
$cascadeProtection
[
'edit'
][
0
];
}
else
{
$protectionLevel
=
$protection
[
0
];
}
// Protection levels are stored in the database as plain text, but
// they are expected to be valid protection levels. So we should be able to
// safely use them. However phan thinks this could be a XSS problem so we
// are being paranoid and escaping them once more.
$protectionLevel
=
htmlspecialchars
(
$protectionLevel
);
$protectionExpiry
=
$this
->
restrictionStore
->
getRestrictionExpiry
(
$title
,
'edit'
);
$formattedProtectionExpiry
=
$context
->
getLanguage
()
->
formatExpiry
(
$protectionExpiry
??
''
);
$protectionMsg
=
'protection-indicator-title'
;
if
(
$protectionExpiry
===
'infinity'
||
!
$protectionExpiry
)
{
$protectionMsg
.=
'-infinity'
;
}
// Potential values: 'protection-sysop', 'protection-autoconfirmed',
// 'protection-sysop-cascade' etc.
// If the wiki has more protection levels, the additional ids that get
// added take the form 'protection-<protectionLevel>' and
// 'protection-<protectionLevel>-cascade'.
$protectionIndicatorId
=
'protection-'
.
$protectionLevel
;
$protectionIndicatorId
.=
(
$isCascadeProtected
?
'-cascade'
:
''
);
// Messages 'protection-indicator-title', 'protection-indicator-title-infinity'
$protectionMsg
=
$outputPage
->
msg
(
$protectionMsg
,
$protectionLevel
,
$formattedProtectionExpiry
)->
text
();
// Use a trick similar to the one used in Action::addHelpLink() to allow wikis
// to customize where the help link points to.
$protectionHelpLink
=
$outputPage
->
msg
(
$protectionIndicatorId
.
'-helppage'
);
if
(
$protectionHelpLink
->
isDisabled
()
)
{
$protectionHelpLink
=
'https://mediawiki.org/wiki/Special:MyLanguage/Help:Protection'
;
}
else
{
$protectionHelpLink
=
$protectionHelpLink
->
text
();
}
$outputPage
->
setIndicators
(
[
$protectionIndicatorId
=>
Html
::
rawElement
(
'a'
,
[
'class'
=>
'mw-protection-indicator-icon--lock'
,
'title'
=>
$protectionMsg
,
'href'
=>
$protectionHelpLink
],
// Screen reader-only text describing the same thing as
// was mentioned in the title attribute.
Html
::
element
(
'span'
,
[],
$protectionMsg
)
)
]
);
$outputPage
->
addModuleStyles
(
'mediawiki.protectionIndicators.styles'
);
}
/**
* Determines the desired ParserOutput and passes it to $outputPage.
*
* @param Authority $performer
* @param ParserOptions $parserOptions
* @param int $oldid
* @param OutputPage $outputPage
* @param array $textOptions
*
* @return bool True if further processing like footer generation should be applied,
* false to skip further processing.
*/
private
function
generateContentOutput
(
Authority
$performer
,
ParserOptions
$parserOptions
,
int
$oldid
,
OutputPage
$outputPage
,
array
$textOptions
):
bool
{
# Should the parser cache be used?
$useParserCache
=
true
;
$pOutput
=
null
;
$parserOutputAccess
=
MediaWikiServices
::
getInstance
()->
getParserOutputAccess
();
// NOTE: $outputDone and $useParserCache may be changed by the hook
$this
->
getHookRunner
()->
onArticleViewHeader
(
$this
,
$outputDone
,
$useParserCache
);
if
(
$outputDone
)
{
if
(
$outputDone
instanceof
ParserOutput
)
{
$pOutput
=
$outputDone
;
}
if
(
$pOutput
)
{
$this
->
doOutputMetaData
(
$pOutput
,
$outputPage
);
}
return
true
;
}
// Early abort if the page doesn't exist
if
(
!
$this
->
mPage
->
exists
()
)
{
wfDebug
(
__METHOD__
.
": showing missing article"
);
$this
->
showMissingArticle
();
$this
->
mPage
->
doViewUpdates
(
$performer
);
return
false
;
// skip all further output to OutputPage
}
// Try the latest parser cache
// NOTE: try latest-revision cache first to avoid loading revision.
if
(
$useParserCache
&&
!
$oldid
)
{
$pOutput
=
$parserOutputAccess
->
getCachedParserOutput
(
$this
->
getPage
(),
$parserOptions
,
null
,
ParserOutputAccess
::
OPT_NO_AUDIENCE_CHECK
// we already checked
);
if
(
$pOutput
)
{
$this
->
doOutputFromParserCache
(
$pOutput
,
$outputPage
,
$textOptions
);
$this
->
doOutputMetaData
(
$pOutput
,
$outputPage
);
return
true
;
}
}
$rev
=
$this
->
fetchRevisionRecord
();
if
(
!
$this
->
fetchResult
->
isOK
()
)
{
$this
->
showViewError
(
$this
->
fetchResult
->
getWikiText
(
false
,
false
,
$this
->
getContext
()->
getLanguage
()
)
);
return
true
;
}
# Are we looking at an old revision
if
(
$oldid
)
{
$this
->
setOldSubtitle
(
$oldid
);
if
(
!
$this
->
showDeletedRevisionHeader
()
)
{
wfDebug
(
__METHOD__
.
": cannot view deleted revision"
);
return
false
;
// skip all further output to OutputPage
}
// Try the old revision parser cache
// NOTE: Repeating cache check for old revision to avoid fetching $rev
// before it's absolutely necessary.
if
(
$useParserCache
)
{
$pOutput
=
$parserOutputAccess
->
getCachedParserOutput
(
$this
->
getPage
(),
$parserOptions
,
$rev
,
ParserOutputAccess
::
OPT_NO_AUDIENCE_CHECK
// we already checked in fetchRevisionRecord
);
if
(
$pOutput
)
{
$this
->
doOutputFromParserCache
(
$pOutput
,
$outputPage
,
$textOptions
);
$this
->
doOutputMetaData
(
$pOutput
,
$outputPage
);
return
true
;
}
}
}
# Ensure that UI elements requiring revision ID have
# the correct version information. (This may be overwritten after creation of ParserOutput)
$outputPage
->
setRevisionId
(
$this
->
getRevIdFetched
()
);
$outputPage
->
setRevisionIsCurrent
(
$rev
->
isCurrent
()
);
# Preload timestamp to avoid a DB hit
$outputPage
->
setRevisionTimestamp
(
$rev
->
getTimestamp
()
);
# Pages containing custom CSS or JavaScript get special treatment
if
(
$this
->
getTitle
()->
isSiteConfigPage
()
||
$this
->
getTitle
()->
isUserConfigPage
()
)
{
$dir
=
$this
->
getContext
()->
getLanguage
()->
getDir
();
$lang
=
$this
->
getContext
()->
getLanguage
()->
getHtmlCode
();
$outputPage
->
wrapWikiMsg
(
"<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>
\n
$1
\n
</div>"
,
'clearyourcache'
);
$outputPage
->
addModuleStyles
(
'mediawiki.action.styles'
);
}
elseif
(
!
$this
->
getHookRunner
()->
onArticleRevisionViewCustom
(
$rev
,
$this
->
getTitle
(),
$oldid
,
$outputPage
)
)
{
// NOTE: sync with hooks called in DifferenceEngine::renderNewRevision()
// Allow extensions do their own custom view for certain pages
$this
->
doOutputMetaData
(
$pOutput
,
$outputPage
);
return
true
;
}
# Run the parse, protected by a pool counter
wfDebug
(
__METHOD__
.
": doing uncached parse"
);
$opt
=
0
;
// we already checked the cache in case 2, don't check again.
$opt
|=
ParserOutputAccess
::
OPT_NO_CHECK_CACHE
;
// we already checked in fetchRevisionRecord()
$opt
|=
ParserOutputAccess
::
OPT_NO_AUDIENCE_CHECK
;
// enable stampede protection and allow stale content
$opt
|=
ParserOutputAccess
::
OPT_FOR_ARTICLE_VIEW
;
// Attempt to trigger WikiPage::triggerOpportunisticLinksUpdate
// Ideally this should not be the responsibility of the ParserCache to control this.
// See https://phabricator.wikimedia.org/T329842#8816557 for more context.
$opt
|=
ParserOutputAccess
::
OPT_LINKS_UPDATE
;
if
(
!
$rev
->
getId
()
||
!
$useParserCache
)
{
// fake revision or uncacheable options
$opt
|=
ParserOutputAccess
::
OPT_NO_CACHE
;
}
$renderStatus
=
$parserOutputAccess
->
getParserOutput
(
$this
->
getPage
(),
$parserOptions
,
$rev
,
$opt
);
// T327164: If parsoid cache warming is enabled, we want to ensure that the page
// the user is currently looking at has a cached parsoid rendering, in case they
// open visual editor. The cache entry would typically be missing if it has expired
// from the cache or it was invalidated by RefreshLinksJob. When "traditional"
// parser output has been invalidated by RefreshLinksJob, we will render it on
// the fly when a user requests the page, and thereby populate the cache again,
// per the code above.
// The code below is intended to do the same for parsoid output, but asynchronously
// in a job, so the user does not have to wait.
// Note that we get here if the traditional parser output was missing from the cache.
// We do not check if the parsoid output is present in the cache, because that check
// takes time. The assumption is that if we have traditional parser output
// cached, we probably also have parsoid output cached.
// So we leave it to ParsoidCachePrewarmJob to determine whether or not parsing is
// needed.
if
(
$oldid
===
0
||
$oldid
===
$this
->
getPage
()->
getLatest
()
)
{
$parsoidCacheWarmingEnabled
=
$this
->
getContext
()->
getConfig
()
->
get
(
MainConfigNames
::
ParsoidCacheConfig
)[
'WarmParsoidParserCache'
];
if
(
$parsoidCacheWarmingEnabled
)
{
$parsoidJobSpec
=
ParsoidCachePrewarmJob
::
newSpec
(
$rev
->
getId
(),
$this
->
getPage
()->
toPageRecord
(),
[
'causeAction'
=>
'view'
]
);
$this
->
jobQueueGroup
->
lazyPush
(
$parsoidJobSpec
);
}
}
$this
->
doOutputFromRenderStatus
(
$rev
,
$renderStatus
,
$outputPage
,
$textOptions
);
if
(
!
$renderStatus
->
isOK
()
)
{
return
true
;
}
$pOutput
=
$renderStatus
->
getValue
();
$this
->
doOutputMetaData
(
$pOutput
,
$outputPage
);
return
true
;
}
/**
* @param ?ParserOutput $pOutput
* @param OutputPage $outputPage
*/
private
function
doOutputMetaData
(
?
ParserOutput
$pOutput
,
OutputPage
$outputPage
)
{
# Adjust title for main page & pages with displaytitle
if
(
$pOutput
)
{
$this
->
adjustDisplayTitle
(
$pOutput
);
// It would be nice to automatically set this during the first call
// to OutputPage::addParserOutputMetadata, but we can't because doing
// so would break non-pageview actions where OutputPage::getContLangForJS
// has different requirements.
$pageLang
=
$pOutput
->
getLanguage
();
if
(
$pageLang
)
{
$outputPage
->
setContentLangForJS
(
$pageLang
);
}
}
# Check for any __NOINDEX__ tags on the page using $pOutput
$policy
=
$this
->
getRobotPolicy
(
'view'
,
$pOutput
?:
null
);
$outputPage
->
getMetadata
()->
setIndexPolicy
(
$policy
[
'index'
]
);
$outputPage
->
setFollowPolicy
(
$policy
[
'follow'
]
);
// FIXME: test this
$this
->
mParserOutput
=
$pOutput
;
}
/**
* @param ParserOutput $pOutput
* @param OutputPage $outputPage
* @param array $textOptions
*/
private
function
doOutputFromParserCache
(
ParserOutput
$pOutput
,
OutputPage
$outputPage
,
array
$textOptions
)
{
# Ensure that UI elements requiring revision ID have
# the correct version information.
$oldid
=
$pOutput
->
getCacheRevisionId
()
??
$this
->
getRevIdFetched
();
$outputPage
->
setRevisionId
(
$oldid
);
$outputPage
->
setRevisionIsCurrent
(
$oldid
===
$this
->
mPage
->
getLatest
()
);
$outputPage
->
addParserOutput
(
$pOutput
,
$textOptions
);
# Preload timestamp to avoid a DB hit
$cachedTimestamp
=
$pOutput
->
getRevisionTimestamp
();
if
(
$cachedTimestamp
!==
null
)
{
$outputPage
->
setRevisionTimestamp
(
$cachedTimestamp
);
$this
->
mPage
->
setTimestamp
(
$cachedTimestamp
);
}
}
/**
* @param RevisionRecord $rev
* @param Status $renderStatus
* @param OutputPage $outputPage
* @param array $textOptions
*/
private
function
doOutputFromRenderStatus
(
RevisionRecord
$rev
,
Status
$renderStatus
,
OutputPage
$outputPage
,
array
$textOptions
)
{
$context
=
$this
->
getContext
();
if
(
!
$renderStatus
->
isOK
()
)
{
$this
->
showViewError
(
$renderStatus
->
getWikiText
(
false
,
'view-pool-error'
,
$context
->
getLanguage
()
)
);
return
;
}
$pOutput
=
$renderStatus
->
getValue
();
// Cache stale ParserOutput object with a short expiry
if
(
$renderStatus
->
hasMessage
(
'view-pool-dirty-output'
)
)
{
$outputPage
->
lowerCdnMaxage
(
$context
->
getConfig
()->
get
(
MainConfigNames
::
CdnMaxageStale
)
);
$outputPage
->
setLastModified
(
$pOutput
->
getCacheTime
()
);
$staleReason
=
$renderStatus
->
hasMessage
(
'view-pool-contention'
)
?
$context
->
msg
(
'view-pool-contention'
)->
escaped
()
:
$context
->
msg
(
'view-pool-timeout'
)->
escaped
();
$outputPage
->
addHTML
(
"<!-- parser cache is expired, "
.
"sending anyway due to $staleReason-->
\n
"
);
// Ensure OutputPage knowns the id from the dirty cache, but keep the current flag (T341013)
$cachedId
=
$pOutput
->
getCacheRevisionId
();
if
(
$cachedId
!==
null
)
{
$outputPage
->
setRevisionId
(
$cachedId
);
$outputPage
->
setRevisionTimestamp
(
$pOutput
->
getTimestamp
()
);
}
}
$outputPage
->
addParserOutput
(
$pOutput
,
$textOptions
);
if
(
$this
->
getRevisionRedirectTarget
(
$rev
)
)
{
$outputPage
->
addSubtitle
(
"<span id=
\"
redirectsub
\"
>"
.
$context
->
msg
(
'redirectpagesub'
)->
parse
()
.
"</span>"
);
}
}
/**
* @param RevisionRecord $revision
* @return null|Title
*/
private
function
getRevisionRedirectTarget
(
RevisionRecord
$revision
)
{
// TODO: find a *good* place for the code that determines the redirect target for
// a given revision!
// NOTE: Use main slot content. Compare code in DerivedPageDataUpdater::revisionIsRedirect.
$content
=
$revision
->
getContent
(
SlotRecord
::
MAIN
);
return
$content
?
$content
->
getRedirectTarget
()
:
null
;
}
/**
* Adjust title for pages with displaytitle, -{T|}- or language conversion
* @param ParserOutput $pOutput
*/
public
function
adjustDisplayTitle
(
ParserOutput
$pOutput
)
{
$out
=
$this
->
getContext
()->
getOutput
();
# Adjust the title if it was set by displaytitle, -{T|}- or language conversion
$titleText
=
$pOutput
->
getTitleText
();
if
(
strval
(
$titleText
)
!==
''
)
{
$out
->
setPageTitle
(
$titleText
);
$out
->
setDisplayTitle
(
$titleText
);
}
}
/**
* Show a diff page according to current request variables. For use within
* Article::view() only, other callers should use the DifferenceEngine class.
*/
protected
function
showDiffPage
()
{
$context
=
$this
->
getContext
();
$outputPage
=
$context
->
getOutput
();
$outputPage
->
addBodyClasses
(
'mw-article-diff'
);
$request
=
$context
->
getRequest
();
$diff
=
$request
->
getVal
(
'diff'
);
$rcid
=
$request
->
getInt
(
'rcid'
);
$purge
=
$request
->
getRawVal
(
'action'
)
===
'purge'
;
$unhide
=
$request
->
getInt
(
'unhide'
)
===
1
;
$oldid
=
$this
->
getOldID
();
$rev
=
$this
->
fetchRevisionRecord
();
if
(
!
$rev
)
{
// T213621: $rev maybe null due to either lack of permission to view the
// revision or actually not existing. So let's try loading it from the id
$rev
=
$this
->
revisionStore
->
getRevisionById
(
$oldid
);
if
(
$rev
)
{
// Revision exists but $user lacks permission to diff it.
// Do nothing here.
// The $rev will later be used to create standard diff elements however.
}
else
{
$outputPage
->
setPageTitleMsg
(
$context
->
msg
(
'errorpagetitle'
)
);
$msg
=
$context
->
msg
(
'difference-missing-revision'
)
->
params
(
$oldid
)
->
numParams
(
1
)
->
parseAsBlock
();
$outputPage
->
addHTML
(
$msg
);
return
;
}
}
$services
=
MediaWikiServices
::
getInstance
();
$contentHandler
=
$services
->
getContentHandlerFactory
()
->
getContentHandler
(
$rev
->
getSlot
(
SlotRecord
::
MAIN
,
RevisionRecord
::
RAW
)->
getModel
()
);
$de
=
$contentHandler
->
createDifferenceEngine
(
$context
,
$oldid
,
$diff
,
$rcid
,
$purge
,
$unhide
);
$diffType
=
$request
->
getVal
(
'diff-type'
);
if
(
$diffType
===
null
)
{
$diffType
=
$this
->
userOptionsLookup
->
getOption
(
$context
->
getUser
(),
'diff-type'
);
}
else
{
$de
->
setExtraQueryParams
(
[
'diff-type'
=>
$diffType
]
);
}
$de
->
setSlotDiffOptions
(
[
'diff-type'
=>
$diffType
,
'expand-url'
=>
$this
->
viewIsRenderAction
,
'inline-toggle'
=>
true
,
]
);
$de
->
showDiffPage
(
$this
->
isDiffOnlyView
()
);
// Run view updates for the newer revision being diffed (and shown
// below the diff if not diffOnly).
[
,
$new
]
=
$de
->
mapDiffPrevNext
(
$oldid
,
$diff
);
// New can be false, convert it to 0 - this conveniently means the latest revision
$this
->
mPage
->
doViewUpdates
(
$context
->
getAuthority
(),
(
int
)
$new
);
// Add link to help page; see T321569
$context
->
getOutput
()->
addHelpLink
(
'Help:Diff'
);
}
protected
function
isDiffOnlyView
()
{
return
$this
->
getContext
()->
getRequest
()->
getBool
(
'diffonly'
,
$this
->
userOptionsLookup
->
getBoolOption
(
$this
->
getContext
()->
getUser
(),
'diffonly'
)
);
}
/**
* Get the robot policy to be used for the current view
* @param string $action The action= GET parameter
* @param ParserOutput|null $pOutput
* @return string[] The policy that should be set
* @todo actions other than 'view'
*/
public
function
getRobotPolicy
(
$action
,
?
ParserOutput
$pOutput
=
null
)
{
$context
=
$this
->
getContext
();
$mainConfig
=
$context
->
getConfig
();
$articleRobotPolicies
=
$mainConfig
->
get
(
MainConfigNames
::
ArticleRobotPolicies
);
$namespaceRobotPolicies
=
$mainConfig
->
get
(
MainConfigNames
::
NamespaceRobotPolicies
);
$defaultRobotPolicy
=
$mainConfig
->
get
(
MainConfigNames
::
DefaultRobotPolicy
);
$title
=
$this
->
getTitle
();
$ns
=
$title
->
getNamespace
();
# Don't index user and user talk pages for blocked users (T13443)
if
(
$ns
===
NS_USER
||
$ns
===
NS_USER_TALK
)
{
$specificTarget
=
null
;
$vagueTarget
=
null
;
$titleText
=
$title
->
getText
();
if
(
IPUtils
::
isValid
(
$titleText
)
)
{
$vagueTarget
=
$titleText
;
}
else
{
$specificTarget
=
$title
->
getRootText
();
}
if
(
$this
->
blockStore
->
newFromTarget
(
$specificTarget
,
$vagueTarget
)
instanceof
DatabaseBlock
)
{
return
[
'index'
=>
'noindex'
,
'follow'
=>
'nofollow'
];
}
}
if
(
$this
->
mPage
->
getId
()
===
0
||
$this
->
getOldID
()
)
{
# Non-articles (special pages etc), and old revisions
return
[
'index'
=>
'noindex'
,
'follow'
=>
'nofollow'
];
}
elseif
(
$context
->
getOutput
()->
isPrintable
()
)
{
# Discourage indexing of printable versions, but encourage following
return
[
'index'
=>
'noindex'
,
'follow'
=>
'follow'
];
}
elseif
(
$context
->
getRequest
()->
getInt
(
'curid'
)
)
{
# For ?curid=x urls, disallow indexing
return
[
'index'
=>
'noindex'
,
'follow'
=>
'follow'
];
}
# Otherwise, construct the policy based on the various config variables.
$policy
=
self
::
formatRobotPolicy
(
$defaultRobotPolicy
);
if
(
isset
(
$namespaceRobotPolicies
[
$ns
]
)
)
{
# Honour customised robot policies for this namespace
$policy
=
array_merge
(
$policy
,
self
::
formatRobotPolicy
(
$namespaceRobotPolicies
[
$ns
]
)
);
}
if
(
$title
->
canUseNoindex
()
&&
$pOutput
&&
$pOutput
->
getIndexPolicy
()
)
{
# __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates
# a final check that we have really got the parser output.
$policy
=
array_merge
(
$policy
,
[
'index'
=>
$pOutput
->
getIndexPolicy
()
]
);
}
if
(
isset
(
$articleRobotPolicies
[
$title
->
getPrefixedText
()]
)
)
{
# (T16900) site config can override user-defined __INDEX__ or __NOINDEX__
$policy
=
array_merge
(
$policy
,
self
::
formatRobotPolicy
(
$articleRobotPolicies
[
$title
->
getPrefixedText
()]
)
);
}
return
$policy
;
}
/**
* Converts a String robot policy into an associative array, to allow
* merging of several policies using array_merge().
* @param array|string $policy Returns empty array on null/false/'', transparent
* to already-converted arrays, converts string.
* @return array 'index' => \<indexpolicy\>, 'follow' => \<followpolicy\>
*/
public
static
function
formatRobotPolicy
(
$policy
)
{
if
(
is_array
(
$policy
)
)
{
return
$policy
;
}
elseif
(
!
$policy
)
{
return
[];
}
$arr
=
[];
foreach
(
explode
(
','
,
$policy
)
as
$var
)
{
$var
=
trim
(
$var
);
if
(
$var
===
'index'
||
$var
===
'noindex'
)
{
$arr
[
'index'
]
=
$var
;
}
elseif
(
$var
===
'follow'
||
$var
===
'nofollow'
)
{
$arr
[
'follow'
]
=
$var
;
}
}
return
$arr
;
}
/**
* If this request is a redirect view, send "redirected from" subtitle to
* the output. Returns true if the header was needed, false if this is not
* a redirect view. Handles both local and remote redirects.
*
* @return bool
*/
public
function
showRedirectedFromHeader
()
{
$context
=
$this
->
getContext
();
$redirectSources
=
$context
->
getConfig
()->
get
(
MainConfigNames
::
RedirectSources
);
$outputPage
=
$context
->
getOutput
();
$request
=
$context
->
getRequest
();
$rdfrom
=
$request
->
getVal
(
'rdfrom'
);
// Construct a URL for the current page view, but with the target title
$query
=
$request
->
getValues
();
unset
(
$query
[
'rdfrom'
]
);
unset
(
$query
[
'title'
]
);
if
(
$this
->
getTitle
()->
isRedirect
()
)
{
// Prevent double redirects
$query
[
'redirect'
]
=
'no'
;
}
$redirectTargetUrl
=
$this
->
getTitle
()->
getLinkURL
(
$query
);
if
(
isset
(
$this
->
mRedirectedFrom
)
)
{
// This is an internally redirected page view.
// We'll need a backlink to the source page for navigation.
if
(
$this
->
getHookRunner
()->
onArticleViewRedirect
(
$this
)
)
{
$redir
=
$this
->
linkRenderer
->
makeKnownLink
(
$this
->
mRedirectedFrom
,
null
,
[],
[
'redirect'
=>
'no'
]
);
$outputPage
->
addSubtitle
(
"<span class=
\"
mw-redirectedfrom
\"
>"
.
$context
->
msg
(
'redirectedfrom'
)->
rawParams
(
$redir
)->
parse
()
.
"</span>"
);
// Add the script to update the displayed URL and
// set the fragment if one was specified in the redirect
$outputPage
->
addJsConfigVars
(
[
'wgInternalRedirectTargetUrl'
=>
$redirectTargetUrl
,
]
);
$outputPage
->
addModules
(
'mediawiki.action.view.redirect'
);
// Add a <link rel="canonical"> tag
$outputPage
->
setCanonicalUrl
(
$this
->
getTitle
()->
getCanonicalURL
()
);
// Tell the output object that the user arrived at this article through a redirect
$outputPage
->
setRedirectedFrom
(
$this
->
mRedirectedFrom
);
return
true
;
}
}
elseif
(
$rdfrom
)
{
// This is an externally redirected view, from some other wiki.
// If it was reported from a trusted site, supply a backlink.
if
(
$redirectSources
&&
preg_match
(
$redirectSources
,
$rdfrom
)
)
{
$redir
=
$this
->
linkRenderer
->
makeExternalLink
(
$rdfrom
,
$rdfrom
,
$this
->
getTitle
()
);
$outputPage
->
addSubtitle
(
"<span class=
\"
mw-redirectedfrom
\"
>"
.
$context
->
msg
(
'redirectedfrom'
)->
rawParams
(
$redir
)->
parse
()
.
"</span>"
);
// Add the script to update the displayed URL
$outputPage
->
addJsConfigVars
(
[
'wgInternalRedirectTargetUrl'
=>
$redirectTargetUrl
,
]
);
$outputPage
->
addModules
(
'mediawiki.action.view.redirect'
);
return
true
;
}
}
return
false
;
}
/**
* Show a header specific to the namespace currently being viewed, like
* [[MediaWiki:Talkpagetext]]. For Article::view().
*/
public
function
showNamespaceHeader
()
{
if
(
$this
->
getTitle
()->
isTalkPage
()
&&
!
$this
->
getContext
()->
msg
(
'talkpageheader'
)->
isDisabled
()
)
{
$this
->
getContext
()->
getOutput
()->
wrapWikiMsg
(
"<div class=
\"
mw-talkpageheader
\"
>
\n
$1
\n
</div>"
,
[
'talkpageheader'
]
);
}
}
/**
* Show the footer section of an ordinary page view
*/
public
function
showViewFooter
()
{
# check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
if
(
$this
->
getTitle
()->
getNamespace
()
===
NS_USER_TALK
&&
IPUtils
::
isValid
(
$this
->
getTitle
()->
getText
()
)
)
{
$this
->
getContext
()->
getOutput
()->
addWikiMsg
(
'anontalkpagetext'
);
}
// Show a footer allowing the user to patrol the shown revision or page if possible
$patrolFooterShown
=
$this
->
showPatrolFooter
();
$this
->
getHookRunner
()->
onArticleViewFooter
(
$this
,
$patrolFooterShown
);
}
/**
* If patrol is possible, output a patrol UI box. This is called from the
* footer section of ordinary page views. If patrol is not possible or not
* desired, does nothing.
*
* Side effect: When the patrol link is build, this method will call
* OutputPage::setPreventClickjacking(true) and load a JS module.
*
* @return bool
*/
public
function
showPatrolFooter
()
{
$context
=
$this
->
getContext
();
$mainConfig
=
$context
->
getConfig
();
$useNPPatrol
=
$mainConfig
->
get
(
MainConfigNames
::
UseNPPatrol
);
$useRCPatrol
=
$mainConfig
->
get
(
MainConfigNames
::
UseRCPatrol
);
$useFilePatrol
=
$mainConfig
->
get
(
MainConfigNames
::
UseFilePatrol
);
// Allow hooks to decide whether to not output this at all
if
(
!
$this
->
getHookRunner
()->
onArticleShowPatrolFooter
(
$this
)
)
{
return
false
;
}
$outputPage
=
$context
->
getOutput
();
$user
=
$context
->
getUser
();
$title
=
$this
->
getTitle
();
$rc
=
false
;
if
(
!
$context
->
getAuthority
()->
probablyCan
(
'patrol'
,
$title
)
||
!(
$useRCPatrol
||
$useNPPatrol
||
(
$useFilePatrol
&&
$title
->
inNamespace
(
NS_FILE
)
)
)
)
{
// Patrolling is disabled or the user isn't allowed to
return
false
;
}
if
(
$this
->
mRevisionRecord
&&
!
RecentChange
::
isInRCLifespan
(
$this
->
mRevisionRecord
->
getTimestamp
(),
21600
)
)
{
// The current revision is already older than what could be in the RC table
// 6h tolerance because the RC might not be cleaned out regularly
return
false
;
}
// Check for cached results
$cache
=
MediaWikiServices
::
getInstance
()->
getMainWANObjectCache
();
$key
=
$cache
->
makeKey
(
'unpatrollable-page'
,
$title
->
getArticleID
()
);
if
(
$cache
->
get
(
$key
)
)
{
return
false
;
}
$dbr
=
$this
->
dbProvider
->
getReplicaDatabase
();
$oldestRevisionRow
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
[
'rev_id'
,
'rev_timestamp'
]
)
->
from
(
'revision'
)
->
where
(
[
'rev_page'
=>
$title
->
getArticleID
()
]
)
->
orderBy
(
[
'rev_timestamp'
,
'rev_id'
]
)
->
caller
(
__METHOD__
)->
fetchRow
();
$oldestRevisionTimestamp
=
$oldestRevisionRow
?
$oldestRevisionRow
->
rev_timestamp
:
false
;
// New page patrol: Get the timestamp of the oldest revision which
// the revision table holds for the given page. Then we look
// whether it's within the RC lifespan and if it is, we try
// to get the recentchanges row belonging to that entry.
$recentPageCreation
=
false
;
if
(
$oldestRevisionTimestamp
&&
RecentChange
::
isInRCLifespan
(
$oldestRevisionTimestamp
,
21600
)
)
{
// 6h tolerance because the RC might not be cleaned out regularly
$recentPageCreation
=
true
;
$rc
=
RecentChange
::
newFromConds
(
[
'rc_this_oldid'
=>
intval
(
$oldestRevisionRow
->
rev_id
),
// Avoid selecting a categorization entry
'rc_type'
=>
RC_NEW
,
],
__METHOD__
);
if
(
$rc
)
{
// Use generic patrol message for new pages
$markPatrolledMsg
=
$context
->
msg
(
'markaspatrolledtext'
);
}
}
// File patrol: Get the timestamp of the latest upload for this page,
// check whether it is within the RC lifespan and if it is, we try
// to get the recentchanges row belonging to that entry
// (with rc_type = RC_LOG, rc_log_type = upload).
$recentFileUpload
=
false
;
if
(
(
!
$rc
||
$rc
->
getAttribute
(
'rc_patrolled'
)
)
&&
$useFilePatrol
&&
$title
->
getNamespace
()
===
NS_FILE
)
{
// Retrieve timestamp from the current file (lastest upload)
$newestUploadTimestamp
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'img_timestamp'
)
->
from
(
'image'
)
->
where
(
[
'img_name'
=>
$title
->
getDBkey
()
]
)
->
caller
(
__METHOD__
)->
fetchField
();
if
(
$newestUploadTimestamp
&&
RecentChange
::
isInRCLifespan
(
$newestUploadTimestamp
,
21600
)
)
{
// 6h tolerance because the RC might not be cleaned out regularly
$recentFileUpload
=
true
;
$rc
=
RecentChange
::
newFromConds
(
[
'rc_type'
=>
RC_LOG
,
'rc_log_type'
=>
'upload'
,
'rc_timestamp'
=>
$newestUploadTimestamp
,
'rc_namespace'
=>
NS_FILE
,
'rc_cur_id'
=>
$title
->
getArticleID
()
],
__METHOD__
);
if
(
$rc
)
{
// Use patrol message specific to files
$markPatrolledMsg
=
$context
->
msg
(
'markaspatrolledtext-file'
);
}
}
}
if
(
!
$recentPageCreation
&&
!
$recentFileUpload
)
{
// Page creation and latest upload (for files) is too old to be in RC
// We definitely can't patrol so cache the information
// When a new file version is uploaded, the cache is cleared
$cache
->
set
(
$key
,
'1'
);
return
false
;
}
if
(
!
$rc
)
{
// Don't cache: This can be hit if the page gets accessed very fast after
// its creation / latest upload or in case we have high replica DB lag. In case
// the revision is too old, we will already return above.
return
false
;
}
if
(
$rc
->
getAttribute
(
'rc_patrolled'
)
)
{
// Patrolled RC entry around
// Cache the information we gathered above in case we can't patrol
// Don't cache in case we can patrol as this could change
$cache
->
set
(
$key
,
'1'
);
return
false
;
}
if
(
$rc
->
getPerformerIdentity
()->
equals
(
$user
)
)
{
// Don't show a patrol link for own creations/uploads. If the user could
// patrol them, they already would be patrolled
return
false
;
}
$outputPage
->
getMetadata
()->
setPreventClickjacking
(
true
);
$outputPage
->
addModules
(
'mediawiki.misc-authed-curate'
);
$link
=
$this
->
linkRenderer
->
makeKnownLink
(
$title
,
new
HtmlArmor
(
'<button class="cdx-button cdx-button--action-progressive">'
// @phan-suppress-next-line PhanPossiblyUndeclaredVariable $markPatrolledMsg is always set
.
$markPatrolledMsg
->
escaped
()
.
'</button>'
),
[],
[
'action'
=>
'markpatrolled'
,
'rcid'
=>
$rc
->
getAttribute
(
'rc_id'
),
]
);
$outputPage
->
addModuleStyles
(
'mediawiki.action.styles'
);
$outputPage
->
addHTML
(
"<div class='patrollink' data-mw='interface'>$link</div>"
);
return
true
;
}
/**
* Purge the cache used to check if it is worth showing the patrol footer
* For example, it is done during re-uploads when file patrol is used.
* @param int $articleID ID of the article to purge
* @since 1.27
*/
public
static
function
purgePatrolFooterCache
(
$articleID
)
{
$cache
=
MediaWikiServices
::
getInstance
()->
getMainWANObjectCache
();
$cache
->
delete
(
$cache
->
makeKey
(
'unpatrollable-page'
,
$articleID
)
);
}
/**
* Show the error text for a missing article. For articles in the MediaWiki
* namespace, show the default message text. To be called from Article::view().
*/
public
function
showMissingArticle
()
{
$context
=
$this
->
getContext
();
$send404Code
=
$context
->
getConfig
()->
get
(
MainConfigNames
::
Send404Code
);
$outputPage
=
$context
->
getOutput
();
// Whether the page is a root user page of an existing user (but not a subpage)
$validUserPage
=
false
;
$title
=
$this
->
getTitle
();
$services
=
MediaWikiServices
::
getInstance
();
$contextUser
=
$context
->
getUser
();
# Show info in user (talk) namespace. Does the user exist? Is he blocked?
if
(
$title
->
getNamespace
()
===
NS_USER
||
$title
->
getNamespace
()
===
NS_USER_TALK
)
{
$rootPart
=
$title
->
getRootText
();
$user
=
User
::
newFromName
(
$rootPart
,
false
/* allow IP users */
);
$ip
=
$this
->
userNameUtils
->
isIP
(
$rootPart
);
$block
=
$this
->
blockStore
->
newFromTarget
(
$user
,
$user
);
if
(
$user
&&
$user
->
isRegistered
()
&&
$user
->
isHidden
()
&&
!
$context
->
getAuthority
()->
isAllowed
(
'hideuser'
)
)
{
// T120883 if the user is hidden and the viewer cannot see hidden
// users, pretend like it does not exist at all.
$user
=
false
;
}
if
(
!(
$user
&&
$user
->
isRegistered
()
)
&&
!
$ip
)
{
// User does not exist
$outputPage
->
addHTML
(
Html
::
warningBox
(
$context
->
msg
(
'userpage-userdoesnotexist-view'
,
wfEscapeWikiText
(
$rootPart
)
)->
parse
(),
'mw-userpage-userdoesnotexist'
)
);
// Show renameuser log extract
LogEventsList
::
showLogExtract
(
$outputPage
,
'renameuser'
,
Title
::
makeTitleSafe
(
NS_USER
,
$rootPart
),
''
,
[
'lim'
=>
10
,
'showIfEmpty'
=>
false
,
'msgKey'
=>
[
'renameuser-renamed-notice'
,
$title
->
getBaseText
()
]
]
);
}
elseif
(
$block
!==
null
&&
$block
->
getType
()
!=
DatabaseBlock
::
TYPE_AUTO
&&
(
$block
->
isSitewide
()
||
$services
->
getPermissionManager
()->
isBlockedFrom
(
$user
,
$title
,
true
)
)
)
{
// Show log extract if the user is sitewide blocked or is partially
// blocked and not allowed to edit their user page or user talk page
LogEventsList
::
showLogExtract
(
$outputPage
,
'block'
,
$services
->
getNamespaceInfo
()->
getCanonicalName
(
NS_USER
)
.
':'
.
$block
->
getTargetName
(),
''
,
[
'lim'
=>
1
,
'showIfEmpty'
=>
false
,
'msgKey'
=>
[
'blocked-notice-logextract'
,
$user
->
getName
()
# Support GENDER in notice
]
]
);
$validUserPage
=
!
$title
->
isSubpage
();
}
else
{
$validUserPage
=
!
$title
->
isSubpage
();
}
}
$this
->
getHookRunner
()->
onShowMissingArticle
(
$this
);
# Show delete and move logs if there were any such events.
# The logging query can DOS the site when bots/crawlers cause 404 floods,
# so be careful showing this. 404 pages must be cheap as they are hard to cache.
$dbCache
=
MediaWikiServices
::
getInstance
()->
getMainObjectStash
();
$key
=
$dbCache
->
makeKey
(
'page-recent-delete'
,
md5
(
$title
->
getPrefixedText
()
)
);
$isRegistered
=
$contextUser
->
isRegistered
();
$sessionExists
=
$context
->
getRequest
()->
getSession
()->
isPersistent
();
if
(
$isRegistered
||
$dbCache
->
get
(
$key
)
||
$sessionExists
)
{
$logTypes
=
[
'delete'
,
'move'
,
'protect'
,
'merge'
];
$dbr
=
$this
->
dbProvider
->
getReplicaDatabase
();
$conds
=
[
$dbr
->
expr
(
'log_action'
,
'!='
,
'revision'
)
];
// Give extensions a chance to hide their (unrelated) log entries
$this
->
getHookRunner
()->
onArticle__MissingArticleConditions
(
$conds
,
$logTypes
);
LogEventsList
::
showLogExtract
(
$outputPage
,
$logTypes
,
$title
,
''
,
[
'lim'
=>
10
,
'conds'
=>
$conds
,
'showIfEmpty'
=>
false
,
'msgKey'
=>
[
$isRegistered
||
$sessionExists
?
'moveddeleted-notice'
:
'moveddeleted-notice-recent'
]
]
);
}
if
(
!
$this
->
mPage
->
hasViewableContent
()
&&
$send404Code
&&
!
$validUserPage
)
{
// If there's no backing content, send a 404 Not Found
// for better machine handling of broken links.
$context
->
getRequest
()->
response
()->
statusHeader
(
404
);
}
// Also apply the robot policy for nonexisting pages (even if a 404 was used)
$policy
=
$this
->
getRobotPolicy
(
'view'
);
$outputPage
->
getMetadata
()->
setIndexPolicy
(
$policy
[
'index'
]
);
$outputPage
->
setFollowPolicy
(
$policy
[
'follow'
]
);
$hookResult
=
$this
->
getHookRunner
()->
onBeforeDisplayNoArticleText
(
$this
);
if
(
!
$hookResult
)
{
return
;
}
# Show error message
$oldid
=
$this
->
getOldID
();
if
(
!
$oldid
&&
$title
->
getNamespace
()
===
NS_MEDIAWIKI
&&
$title
->
hasSourceText
()
)
{
$text
=
$this
->
getTitle
()->
getDefaultMessageText
()
??
''
;
$outputPage
->
addWikiTextAsContent
(
$text
);
}
else
{
if
(
$oldid
)
{
$text
=
$this
->
getMissingRevisionMsg
(
$oldid
)->
plain
();
}
elseif
(
$context
->
getAuthority
()->
probablyCan
(
'edit'
,
$title
)
)
{
$message
=
$isRegistered
?
'noarticletext'
:
'noarticletextanon'
;
$text
=
$context
->
msg
(
$message
)->
plain
();
}
else
{
$text
=
$context
->
msg
(
'noarticletext-nopermission'
)->
plain
();
}
$dir
=
$context
->
getLanguage
()->
getDir
();
$lang
=
$context
->
getLanguage
()->
getHtmlCode
();
$outputPage
->
addWikiTextAsInterface
(
Xml
::
openElement
(
'div'
,
[
'class'
=>
"noarticletext mw-content-$dir"
,
'dir'
=>
$dir
,
'lang'
=>
$lang
,
]
)
.
"
\n
$text
\n
</div>"
);
}
}
/**
* Show error text for errors generated in Article::view().
* @param string $errortext localized wikitext error message
*/
private
function
showViewError
(
string
$errortext
)
{
$outputPage
=
$this
->
getContext
()->
getOutput
();
$outputPage
->
setPageTitleMsg
(
$this
->
getContext
()->
msg
(
'errorpagetitle'
)
);
$outputPage
->
disableClientCache
();
$outputPage
->
setRobotPolicy
(
'noindex,nofollow'
);
$outputPage
->
clearHTML
();
$outputPage
->
addHTML
(
Html
::
errorBox
(
$outputPage
->
parseAsContent
(
$errortext
)
)
);
}
/**
* If the revision requested for view is deleted, check permissions.
* Send either an error message or a warning header to the output.
*
* @return bool True if the view is allowed, false if not.
*/
public
function
showDeletedRevisionHeader
()
{
if
(
!
$this
->
mRevisionRecord
->
isDeleted
(
RevisionRecord
::
DELETED_TEXT
)
)
{
// Not deleted
return
true
;
}
$outputPage
=
$this
->
getContext
()->
getOutput
();
// Used in wikilinks, should not contain whitespaces
$titleText
=
$this
->
getTitle
()->
getPrefixedDBkey
();
// If the user is not allowed to see it...
if
(
!
$this
->
mRevisionRecord
->
userCan
(
RevisionRecord
::
DELETED_TEXT
,
$this
->
getContext
()->
getAuthority
()
)
)
{
$outputPage
->
addHTML
(
Html
::
warningBox
(
$outputPage
->
msg
(
'rev-deleted-text-permission'
,
$titleText
)->
parse
(),
'plainlinks'
)
);
return
false
;
// If the user needs to confirm that they want to see it...
}
elseif
(
$this
->
getContext
()->
getRequest
()->
getInt
(
'unhide'
)
!==
1
)
{
# Give explanation and add a link to view the revision...
$oldid
=
intval
(
$this
->
getOldID
()
);
$link
=
$this
->
getTitle
()->
getFullURL
(
"oldid={$oldid}&unhide=1"
);
$msg
=
$this
->
mRevisionRecord
->
isDeleted
(
RevisionRecord
::
DELETED_RESTRICTED
)
?
'rev-suppressed-text-unhide'
:
'rev-deleted-text-unhide'
;
$outputPage
->
addHTML
(
Html
::
warningBox
(
$outputPage
->
msg
(
$msg
,
$link
)->
parse
(),
'plainlinks'
)
);
return
false
;
// We are allowed to see...
}
else
{
$msg
=
$this
->
mRevisionRecord
->
isDeleted
(
RevisionRecord
::
DELETED_RESTRICTED
)
?
[
'rev-suppressed-text-view'
,
$titleText
]
:
[
'rev-deleted-text-view'
,
$titleText
];
$outputPage
->
addHTML
(
Html
::
warningBox
(
$outputPage
->
msg
(
$msg
[
0
],
$msg
[
1
]
)->
parse
(),
'plainlinks'
)
);
return
true
;
}
}
/**
* Generate the navigation links when browsing through an article revisions
* It shows the information as:
* Revision as of \<date\>; view current revision
* \<- Previous version | Next Version -\>
*
* @param int $oldid Revision ID of this article revision
*/
public
function
setOldSubtitle
(
$oldid
=
0
)
{
if
(
!
$this
->
getHookRunner
()->
onDisplayOldSubtitle
(
$this
,
$oldid
)
)
{
return
;
}
$context
=
$this
->
getContext
();
$unhide
=
$context
->
getRequest
()->
getInt
(
'unhide'
)
===
1
;
# Cascade unhide param in links for easy deletion browsing
$extraParams
=
[];
if
(
$unhide
)
{
$extraParams
[
'unhide'
]
=
1
;
}
if
(
$this
->
mRevisionRecord
&&
$this
->
mRevisionRecord
->
getId
()
===
$oldid
)
{
$revisionRecord
=
$this
->
mRevisionRecord
;
}
else
{
$revisionRecord
=
$this
->
revisionStore
->
getRevisionById
(
$oldid
);
}
if
(
!
$revisionRecord
)
{
throw
new
LogicException
(
'There should be a revision record at this point.'
);
}
$timestamp
=
$revisionRecord
->
getTimestamp
();
$current
=
(
$oldid
==
$this
->
mPage
->
getLatest
()
);
$language
=
$context
->
getLanguage
();
$user
=
$context
->
getUser
();
$td
=
$language
->
userTimeAndDate
(
$timestamp
,
$user
);
$tddate
=
$language
->
userDate
(
$timestamp
,
$user
);
$tdtime
=
$language
->
userTime
(
$timestamp
,
$user
);
# Show user links if allowed to see them. If hidden, then show them only if requested...
$userlinks
=
Linker
::
revUserTools
(
$revisionRecord
,
!
$unhide
);
$infomsg
=
$current
&&
!
$context
->
msg
(
'revision-info-current'
)->
isDisabled
()
?
'revision-info-current'
:
'revision-info'
;
$outputPage
=
$context
->
getOutput
();
$outputPage
->
addModuleStyles
(
[
'mediawiki.codex.messagebox.styles'
,
'mediawiki.action.styles'
,
'mediawiki.interface.helpers.styles'
]
);
$revisionUser
=
$revisionRecord
->
getUser
();
$revisionInfo
=
"<div id=
\"
mw-{$infomsg}
\"
>"
.
$context
->
msg
(
$infomsg
,
$td
)
->
rawParams
(
$userlinks
)
->
params
(
$revisionRecord
->
getId
(),
$tddate
,
$tdtime
,
$revisionUser
?
$revisionUser
->
getName
()
:
''
)
->
rawParams
(
$this
->
commentFormatter
->
formatRevision
(
$revisionRecord
,
$user
,
true
,
!
$unhide
)
)
->
parse
()
.
"</div>"
;
$lnk
=
$current
?
$context
->
msg
(
'currentrevisionlink'
)->
escaped
()
:
$this
->
linkRenderer
->
makeKnownLink
(
$this
->
getTitle
(),
$context
->
msg
(
'currentrevisionlink'
)->
text
(),
[],
$extraParams
);
$curdiff
=
$current
?
$context
->
msg
(
'diff'
)->
escaped
()
:
$this
->
linkRenderer
->
makeKnownLink
(
$this
->
getTitle
(),
$context
->
msg
(
'diff'
)->
text
(),
[],
[
'diff'
=>
'cur'
,
'oldid'
=>
$oldid
]
+
$extraParams
);
$prevExist
=
(
bool
)
$this
->
revisionStore
->
getPreviousRevision
(
$revisionRecord
);
$prevlink
=
$prevExist
?
$this
->
linkRenderer
->
makeKnownLink
(
$this
->
getTitle
(),
$context
->
msg
(
'previousrevision'
)->
text
(),
[],
[
'direction'
=>
'prev'
,
'oldid'
=>
$oldid
]
+
$extraParams
)
:
$context
->
msg
(
'previousrevision'
)->
escaped
();
$prevdiff
=
$prevExist
?
$this
->
linkRenderer
->
makeKnownLink
(
$this
->
getTitle
(),
$context
->
msg
(
'diff'
)->
text
(),
[],
[
'diff'
=>
'prev'
,
'oldid'
=>
$oldid
]
+
$extraParams
)
:
$context
->
msg
(
'diff'
)->
escaped
();
$nextlink
=
$current
?
$context
->
msg
(
'nextrevision'
)->
escaped
()
:
$this
->
linkRenderer
->
makeKnownLink
(
$this
->
getTitle
(),
$context
->
msg
(
'nextrevision'
)->
text
(),
[],
[
'direction'
=>
'next'
,
'oldid'
=>
$oldid
]
+
$extraParams
);
$nextdiff
=
$current
?
$context
->
msg
(
'diff'
)->
escaped
()
:
$this
->
linkRenderer
->
makeKnownLink
(
$this
->
getTitle
(),
$context
->
msg
(
'diff'
)->
text
(),
[],
[
'diff'
=>
'next'
,
'oldid'
=>
$oldid
]
+
$extraParams
);
$cdel
=
Linker
::
getRevDeleteLink
(
$context
->
getAuthority
(),
$revisionRecord
,
$this
->
getTitle
()
);
if
(
$cdel
!==
''
)
{
$cdel
.=
' '
;
}
// the outer div is need for styling the revision info and nav in MobileFrontend
$outputPage
->
addSubtitle
(
Html
::
warningBox
(
$revisionInfo
.
"<div id=
\"
mw-revision-nav
\"
>"
.
$cdel
.
$context
->
msg
(
'revision-nav'
)->
rawParams
(
$prevdiff
,
$prevlink
,
$lnk
,
$curdiff
,
$nextlink
,
$nextdiff
)->
escaped
()
.
"</div>"
,
'mw-revision'
)
);
}
/**
* Return the HTML for the top of a redirect page
*
* Chances are you should just be using the ParserOutput from
* WikitextContent::getParserOutput instead of calling this for redirects.
*
* @since 1.23
* @param Language $lang
* @param Title $target Destination to redirect
* @param bool $forceKnown Should the image be shown as a bluelink regardless of existence?
* @return string Containing HTML with redirect link
* @deprecated since 1.41, use LinkRenderer::makeRedirectHeader() instead
*/
public
static
function
getRedirectHeaderHtml
(
Language
$lang
,
Title
$target
,
$forceKnown
=
false
)
{
wfDeprecated
(
__METHOD__
,
'1.41'
);
$linkRenderer
=
MediaWikiServices
::
getInstance
()->
getLinkRenderer
();
return
$linkRenderer
->
makeRedirectHeader
(
$lang
,
$target
,
$forceKnown
);
}
/**
* Adds help link with an icon via page indicators.
* Link target can be overridden by a local message containing a wikilink:
* the message key is: 'namespace-' + namespace number + '-helppage'.
* @param string $to Target MediaWiki.org page title or encoded URL.
* @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o.
* @since 1.25
*/
public
function
addHelpLink
(
$to
,
$overrideBaseUrl
=
false
)
{
$out
=
$this
->
getContext
()->
getOutput
();
$msg
=
$out
->
msg
(
'namespace-'
.
$this
->
getTitle
()->
getNamespace
()
.
'-helppage'
);
if
(
!
$msg
->
isDisabled
()
)
{
$title
=
Title
::
newFromText
(
$msg
->
plain
()
);
if
(
$title
instanceof
Title
)
{
$out
->
addHelpLink
(
$title
->
getLocalURL
(),
true
);
}
}
else
{
$out
->
addHelpLink
(
$to
,
$overrideBaseUrl
);
}
}
/**
* Handle action=render
*/
public
function
render
()
{
$this
->
getContext
()->
getRequest
()->
response
()->
header
(
'X-Robots-Tag: noindex'
);
$this
->
getContext
()->
getOutput
()->
setArticleBodyOnly
(
true
);
// We later set 'enableSectionEditLinks=false' based on this; also used by ImagePage
$this
->
viewIsRenderAction
=
true
;
$this
->
view
();
}
/**
* action=protect handler
*/
public
function
protect
()
{
$form
=
new
ProtectionForm
(
$this
);
$form
->
execute
();
}
/**
* action=unprotect handler (alias)
*/
public
function
unprotect
()
{
$this
->
protect
();
}
/* Caching functions */
/**
* checkLastModified returns true if it has taken care of all
* output to the client that is necessary for this request.
* (that is, it has sent a cached version of the page)
*
* @return bool True if cached version send, false otherwise
*/
protected
function
tryFileCache
()
{
static
$called
=
false
;
if
(
$called
)
{
wfDebug
(
"Article::tryFileCache(): called twice!?"
);
return
false
;
}
$called
=
true
;
if
(
$this
->
isFileCacheable
()
)
{
$cache
=
new
HTMLFileCache
(
$this
->
getTitle
(),
'view'
);
if
(
$cache
->
isCacheGood
(
$this
->
mPage
->
getTouched
()
)
)
{
wfDebug
(
"Article::tryFileCache(): about to load file"
);
$cache
->
loadFromFileCache
(
$this
->
getContext
()
);
return
true
;
}
else
{
wfDebug
(
"Article::tryFileCache(): starting buffer"
);
ob_start
(
[
&
$cache
,
'saveToFileCache'
]
);
}
}
else
{
wfDebug
(
"Article::tryFileCache(): not cacheable"
);
}
return
false
;
}
/**
* Check if the page can be cached
* @param int $mode One of the HTMLFileCache::MODE_* constants (since 1.28)
* @return bool
*/
public
function
isFileCacheable
(
$mode
=
HTMLFileCache
::
MODE_NORMAL
)
{
$cacheable
=
false
;
if
(
HTMLFileCache
::
useFileCache
(
$this
->
getContext
(),
$mode
)
)
{
$cacheable
=
$this
->
mPage
->
getId
()
&&
!
$this
->
mRedirectedFrom
&&
!
$this
->
getTitle
()->
isRedirect
();
// Extension may have reason to disable file caching on some pages.
if
(
$cacheable
)
{
$cacheable
=
$this
->
getHookRunner
()->
onIsFileCacheable
(
$this
)
??
false
;
}
}
return
$cacheable
;
}
/**
* Lightweight method to get the parser output for a page, checking the parser cache
* and so on. Doesn't consider most of the stuff that Article::view() is forced to
* consider, so it's not appropriate to use there.
*
* @since 1.16 (r52326) for LiquidThreads
*
* @param int|null $oldid Revision ID or null
* @param UserIdentity|null $user The relevant user
* @return ParserOutput|false ParserOutput or false if the given revision ID is not found
*/
public
function
getParserOutput
(
$oldid
=
null
,
?
UserIdentity
$user
=
null
)
{
if
(
$user
===
null
)
{
$parserOptions
=
$this
->
getParserOptions
();
}
else
{
$parserOptions
=
$this
->
mPage
->
makeParserOptions
(
$user
);
$parserOptions
->
setRenderReason
(
'page-view'
);
}
return
$this
->
mPage
->
getParserOutput
(
$parserOptions
,
$oldid
);
}
/**
* Get parser options suitable for rendering the primary article wikitext
* @return ParserOptions
*/
public
function
getParserOptions
()
{
$parserOptions
=
$this
->
mPage
->
makeParserOptions
(
$this
->
getContext
()
);
$parserOptions
->
setRenderReason
(
'page-view'
);
return
$parserOptions
;
}
/**
* Sets the context this Article is executed in
*
* @param IContextSource $context
* @since 1.18
*/
public
function
setContext
(
$context
)
{
$this
->
mContext
=
$context
;
}
/**
* Gets the context this Article is executed in
*
* @return IContextSource
* @since 1.18
*/
public
function
getContext
():
IContextSource
{
if
(
$this
->
mContext
instanceof
IContextSource
)
{
return
$this
->
mContext
;
}
else
{
wfDebug
(
__METHOD__
.
" called and
\$
mContext is null. "
.
"Return RequestContext::getMain()"
);
return
RequestContext
::
getMain
();
}
}
/**
* Call to WikiPage function for backwards compatibility.
* @see ContentHandler::getActionOverrides
* @return array
*/
public
function
getActionOverrides
()
{
return
$this
->
mPage
->
getActionOverrides
();
}
private
function
getMissingRevisionMsg
(
int
$oldid
):
Message
{
// T251066: Try loading the revision from the archive table.
// Show link to view it if it exists and the user has permission to view it.
// (Ignore the given title, if any; look it up from the revision instead.)
$context
=
$this
->
getContext
();
$revRecord
=
$this
->
archivedRevisionLookup
->
getArchivedRevisionRecord
(
null
,
$oldid
);
if
(
$revRecord
&&
$revRecord
->
userCan
(
RevisionRecord
::
DELETED_TEXT
,
$context
->
getAuthority
()
)
&&
$context
->
getAuthority
()->
isAllowedAny
(
'deletedtext'
,
'undelete'
)
)
{
return
$context
->
msg
(
'missing-revision-permission'
,
$oldid
,
$revRecord
->
getTimestamp
(),
Title
::
newFromPageIdentity
(
$revRecord
->
getPage
()
)->
getPrefixedDBkey
()
);
}
return
$context
->
msg
(
'missing-revision'
,
$oldid
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Wed, Sep 10, 03:34 (5 h, 14 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
31/56/e1b93939d8c0b3832e4350b04aed
Default Alt Text
Article.php (66 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment