gplx/mediawiki/extensions/ApprovedRevs/includes/ApprovedRevsHooks.php

1422 lines
42 KiB
PHP

<?php
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
use MediaWiki\Page\ProperPageIdentity;
use MediaWiki\Permissions\Authority;
use MediaWiki\Revision\RenderedRevision;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Storage\EditResult;
use MediaWiki\User\UserIdentity;
/**
* Functions for the Approved Revs extension called by hooks in the MediaWiki
* code.
*
* @file
* @ingroup Extensions
*
* @author Yaron Koren
* @author Jeroen De Dauw
*/
class ApprovedRevsHooks {
private static $mApprovalMagicWords = [
'MAG_APPROVALYEAR',
'MAG_APPROVALMONTH',
'MAG_APPROVALDAY',
'MAG_APPROVALTIMESTAMP',
'MAG_APPROVALUSER'
];
public static function userRevsApprovedAutomatically( User $user, Title $title ) {
global $egApprovedRevsAutomaticApprovals, $egApprovedRevsFileAutomaticApprovals;
if ( $title->getNamespace() == NS_FILE ) {
$automaticApproval = $egApprovedRevsFileAutomaticApprovals;
} else {
$automaticApproval = $egApprovedRevsAutomaticApprovals;
}
return ( ApprovedRevs::userCanApprove( $user, $title ) && $automaticApproval );
}
/**
* "noindex" and "nofollow" meta-tags are added to every revision page,
* so that search engines won't index them - remove those if this is
* the approved revision.
* There doesn't seem to be an ideal MediaWiki hook to use for this
* function - it currently uses 'ParserBeforeInternalParse', which works.
*/
public static function removeRobotsTag( Parser &$parser, &$text, &$strip_state ) {
global $wgRequest;
if ( !ApprovedRevs::isDefaultPageRequest( $wgRequest ) ) {
return;
}
$revisionID = ApprovedRevs::getApprovedRevID( $parser->getTitle() );
if ( !empty( $revisionID ) ) {
global $wgOut;
$wgOut->getMetadata()->setIndexPolicy( 'index' );
$wgOut->setFollowPolicy( 'follow' );
}
}
/**
* Called by updateLinksAfterEdit().
*/
public static function getUpdateForTitle( Title $title ) {
if ( !ApprovedRevs::pageIsApprovable( $title ) ) {
return null;
}
$wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
// If this user's revisions get approved automatically, exit
// now, because this will be the approved revision anyway.
$userID = $wikiPage->getUser();
$user = User::newFromId( $userID );
if ( self::userRevsApprovedAutomatically( $user, $title ) ) {
return null;
}
$content = ApprovedRevs::getApprovedContent( $title );
// If there's no approved revision, and 'blank if
// unapproved' is set to true, set the content to blank.
if ( $content == null ) {
global $egApprovedRevsBlankIfUnapproved;
if ( $egApprovedRevsBlankIfUnapproved ) {
$content = $wikiPage->getContentHandler()->makeEmptyContent();
} else {
// If it's an unapproved page and there's no
// page blanking, exit here.
return null;
}
}
$editInfo = $wikiPage->prepareContentForEdit( $content, null, $user );
$luClass = class_exists( 'LinksUpdate' ) ? LinksUpdate::class :
MediaWiki\Deferred\LinksUpdate\LinksUpdate::class;
return new $luClass( $title, $editInfo->output );
}
/**
* Hook: RevisionDataUpdates
*
* Call LinksUpdate on the text of this page's approved revision,
* if there is one.
*/
public static function updateLinksAfterEdit(
Title $title,
RenderedRevision $renderedRevision,
array &$updates
) {
$update = self::getUpdateForTitle( $title );
if ( $update !== null ) {
// Wipe out any existing updates.
$updates = [ $update ];
}
}
/**
* Hook: PageSaveComplete
*
* If the user saving this page has approval power, and automatic
* approvals are enabled, and the page is approvable, and either
* (a) this page already has an approved revision, or (b) unapproved
* pages are shown as blank on this wiki, automatically set this
* latest revision to be the approved one - don't bother logging
* the approval, though; the log is reserved for manual approvals.
*/
public static function setLatestAsApproved(
WikiPage $wikiPage,
UserIdentity $user,
string $summary,
int $flags,
RevisionRecord $revisionRecord,
EditResult $editResult
) {
if ( $revisionRecord === null ) {
return;
}
$title = $wikiPage->getTitle();
if ( !self::userRevsApprovedAutomatically( $user, $title ) ) {
return;
}
if ( $title->getNamespace() == NS_FILE ) {
self::setOriginalFileRevAsApproved( $user, $title );
return;
}
if ( !ApprovedRevs::pageIsApprovable( $title ) ) {
return;
}
global $egApprovedRevsBlankIfUnapproved;
if ( !$egApprovedRevsBlankIfUnapproved ) {
$approvedRevID = ApprovedRevs::getApprovedRevID( $title );
if ( empty( $approvedRevID ) ) {
return;
}
}
// Save approval without logging.
ApprovedRevs::saveApprovedRevIDInDB( $title, $revisionRecord->getID(), $user, true );
}
/**
* Hook: PageMoveComplete
*
* Automatically approve a page move (which, like an edit, also counts
* as a revision) if the user has auto-approve rights, and the revision
* right before this one was approved - we do this so that page moves
* don't lead to unnecessary approval work for anyone.
*/
public static function setPageMoveAsApproved(
LinkTarget $old,
LinkTarget $new,
UserIdentity $user,
int $pageid,
int $redirid,
string $reason,
RevisionRecord $revision
) {
if ( $revision === null ) {
return;
}
$approvedRevID = ApprovedRevs::getApprovedRevID( $new );
if ( empty( $approvedRevID ) ) {
return;
}
$revID = $revision->getID();
// We need to know if the revision before this current move
// was approved - unfortunately, there's no convenient function
// to determine that, so we do a DB query.
$dbr = ApprovedRevs::getReadDB();
$lastPreMoveRevID = $dbr->selectField(
[ 'page', 'revision' ],
'MAX(rev_id)',
"rev_page = $pageid AND rev_id < $revID",
__METHOD__
);
if ( $approvedRevID !== $lastPreMoveRevID ) {
return;
}
// Save approval without logging.
ApprovedRevs::saveApprovedRevIDInDB( $new, $revID, $user, true );
}
/**
* This method will actually work no matter how many revisions a file has,
* but in practice it's only called when the first revision is created.
*/
public static function setOriginalFileRevAsApproved( $user, $title ) {
global $wgLocalFileRepo;
$fileRepo = new LocalRepo( $wgLocalFileRepo );
$file = LocalFile::newFromTitle( $title, $fileRepo );
if ( $file->getTimestamp() == '' ) {
// Probably a page in the "File:" namespace was created,
// with no corresponding file upload.
return;
}
if ( defined( 'MW_QUIBBLE_CI' ) ) {
// FIXME: The actual save doesn't work in CI...
return;
}
ApprovedRevs::setApprovedFileInDB(
$title, $file->getTimestamp(), $file->getSha1(), $user
);
}
/**
* Hook: UploadComplete
*
* Automatically approve a revision of a file, other than the first
* one, if the permissions for it are set.
*/
public static function setLatestFileRevAsApproved( UploadBase $image ) {
$user = RequestContext::getMain()->getUser();
$file = $image->getLocalFile();
$title = $file->getTitle();
// A newly-uploaded file will still have an article ID of 0 -
// ignore it. (It will get handled by the PageSaveComplete,
// or PageContentSaveComplete, hook anyway.)
if ( $title->getArticleID() == 0 ) {
return;
}
if ( !self::userRevsApprovedAutomatically( $user, $title ) ) {
return;
}
ApprovedRevs::setApprovedFileInDB(
$title, $file->getTimestamp(), $file->getSha1(), $user
);
}
/**
* Hook: PageSaveComplete
*
* Set the text that's stored for the page for standard searches.
*/
public static function setSearchText(
WikiPage $wikiPage,
UserIdentity $user,
string $summary,
int $flags,
RevisionRecord $revisionRecord,
EditResult $editResult
) {
if ( $revisionRecord === null ) {
return;
}
$title = $wikiPage->getTitle();
if ( !ApprovedRevs::pageIsApprovable( $title ) ) {
return;
}
$revisionID = ApprovedRevs::getApprovedRevID( $title );
if ( $revisionID === null ) {
return;
}
// We only need to modify the search text if the approved
// revision is not the latest one.
if ( $revisionID != $wikiPage->getLatest() ) {
$approvedPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
$approvedContent = $approvedPage->getContent();
ApprovedRevs::setPageSearchText( $title, $approvedContent );
}
}
/**
* Hook: SearchResultInitFromTitle
*
* Sets the correct page revision to display the "text snippet" for
* a search result.
*/
public static function setSearchRevisionID( $title, &$id ) {
$revisionID = ApprovedRevs::getApprovedRevID( $title );
if ( $revisionID !== null ) {
$id = $revisionID;
}
}
/**
* Hook: ArticleFromTitle
*
* Return the approved revision of the page, if there is one, and if
* the page is simply being viewed, and if no specific revision has
* been requested.
*/
public static function showApprovedRevision( &$title, &$article, $context ) {
$request = $context->getRequest();
if ( !ApprovedRevs::isDefaultPageRequest( $request ) ) {
return;
}
if ( !ApprovedRevs::pageIsApprovable( $title ) ) {
return;
}
global $egApprovedRevsBlankIfUnapproved;
$revisionID = ApprovedRevs::getApprovedRevID( $title );
// Blanking of unapproved pages seems to not work unless code
// like this is called - possibly because the cache needs to be
// disabled. There may be a better way to accomplish that than
// this, but this works, and it doesn't seem to have a
// noticeable negative impact, so we'll go with it for now, at
// least.
if ( !empty( $revisionID ) || $egApprovedRevsBlankIfUnapproved ) {
$article = new Article( $title, $revisionID );
// This call is necessary because it
// causes $article->mRevision to get initialized,
// which in turn allows "edit section" links to show
// up if the approved revision is also the latest.
$article->fetchRevisionRecord();
}
}
/**
* Hook: ArticleRevisionViewCustom
*
* @param RevisionStoreRecord $revisionStoreRecord
* @param Title $title
* @param int $oldid
* @param OutputPage $output
*/
public static function showBlankIfUnapproved( $revisionStoreRecord, $title, $oldid, $output ) {
global $egApprovedRevsBlankIfUnapproved;
if ( !$egApprovedRevsBlankIfUnapproved ) {
return true;
}
if ( !ApprovedRevs::pageIsApprovable( $title ) ) {
return true;
}
$revisionID = ApprovedRevs::getApprovedRevID( $title );
if ( !empty( $revisionID ) ) {
return true;
}
// If the user is looking at a specified revision of the page,
// always show it.
if ( $oldid > 0 ) {
return true;
}
// We need to do this just to check if we're in the 'view'
// action, not 'edit', 'history' etc.
$article = new Article( $title, $oldid );
$context = $article->getContext();
$request = $context->getRequest();
if ( !ApprovedRevs::isDefaultPageRequest( $request ) ) {
return true;
}
// We're still here - the page should be blank.
ApprovedRevs::addCSS();
// Returning false sets the content to blank.
return false;
}
/**
* Sets the subtitle when viewing old revisions of a page.
* This function's code is mostly copied from Article::setOldSubtitle(),
* and it is meant to serve as a replacement for that function, using
* the 'DisplayOldSubtitle' hook.
* This display has the following differences from the standard one:
* - It includes a link to the approved revision, which goes to the
* default page.
* - It includes a "diff" link alongside it.
* - The "Latest revision" link points to the correct revision ID,
* instead of to the default page (unless the latest revision is also
* the approved one).
*
* @author Eli Handel
*/
public static function setOldSubtitle( $article, $revisionID ) {
$title = $article->getTitle();
$context = $article->getContext();
$unhide = $context->getRequest()->getInt( 'unhide' ) == 1;
// Cascade unhide param in links for easy deletion browsing.
$extraParams = [];
if ( $unhide ) {
$extraParams['unhide'] = 1;
}
$revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup();
$rev = $revisionLookup->getRevisionById( $revisionID );
$timestamp = $rev->getTimestamp();
$wikiPage = $article->getPage();
$latestID = $wikiPage->getLatest();
$current = ( $revisionID == $latestID );
$approvedID = ApprovedRevs::getApprovedRevID( $title );
$language = $context->getLanguage();
$user = $context->getUser();
$td = $language->userTimeAndDate( $timestamp, $user );
$tddate = $language->userDate( $timestamp, $user );
$tdtime = $language->userTime( $timestamp, $user );
// Show the user links if they're allowed to see them.
// If hidden, then show them only if requested.
$userlinks = Linker::revUserTools( $rev, !$unhide );
$infomsg = $current && !wfMessage( 'revision-info-current' )->isDisabled()
? 'revision-info-current'
: 'revision-info';
$outputPage = $context->getOutput();
$revUser = $rev->getUser();
$userText = $revUser ? $revUser->getName() : '';
$comment = MediaWikiServices::getInstance()->getCommentFormatter()
->formatRevision( $rev, $user, true, true );
$revisionInfo = "<div id=\"mw-{$infomsg}\">" .
$context->msg( $infomsg, $td )
->rawParams( $userlinks )
->params( $revisionID, $tddate, $tdtime, $userText )
->rawParams( $comment )
->parse() .
"</div>";
$outputPage->addSubtitle( $revisionInfo );
// Created for Approved Revs
$latestLinkParams = [];
if ( $latestID != $approvedID ) {
$latestLinkParams['oldid'] = $latestID;
}
$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
$lnk = $current
? wfMessage( 'currentrevisionlink' )->escaped()
: $linkRenderer->makeLink(
$title,
wfMessage( 'currentrevisionlink' )->escaped(),
[],
$latestLinkParams + $extraParams
);
$curdiff = $current
? wfMessage( 'diff' )->escaped()
: $linkRenderer->makeLink(
$title,
wfMessage( 'diff' )->escaped(),
[],
[
'diff' => 'cur',
'oldid' => $revisionID
] + $extraParams
);
$prev = $revisionLookup->getPreviousRevision( $rev );
$prevlink = $prev
? $linkRenderer->makeLink(
$title,
wfMessage( 'previousrevision' )->escaped(),
[],
[
'direction' => 'prev',
'oldid' => $revisionID
] + $extraParams
)
: wfMessage( 'previousrevision' )->escaped();
$prevdiff = $prev
? $linkRenderer->makeLink(
$title,
wfMessage( 'diff' )->escaped(),
[],
[
'diff' => 'prev',
'oldid' => $revisionID
] + $extraParams
)
: wfMessage( 'diff' )->escaped();
$nextlink = $current
? wfMessage( 'nextrevision' )->escaped()
: $linkRenderer->makeLink(
$title,
wfMessage( 'nextrevision' )->escaped(),
[],
[
'direction' => 'next',
'oldid' => $revisionID
] + $extraParams
);
$nextdiff = $current
? wfMessage( 'diff' )->escaped()
: $linkRenderer->makeLink(
$title,
wfMessage( 'diff' )->escaped(),
[],
[
'diff' => 'next',
'oldid' => $revisionID
] + $extraParams
);
// Added for Approved Revs
$approved = ( $approvedID != null && $revisionID == $approvedID );
$approvedlink = $approved
? wfMessage( 'approvedrevs-approvedrevision' )->escaped()
: $linkRenderer->makeLink(
$title,
wfMessage( 'approvedrevs-approvedrevision' )->escaped(),
[],
$extraParams
);
$approveddiff = $approved
? wfMessage( 'diff' )->escaped()
: $linkRenderer->makeLink(
$title,
wfMessage( 'diff' )->escaped(),
[],
[
'diff' => $approvedID,
'oldid' => $revisionID
] + $extraParams
);
$cdel = Linker::getRevDeleteLink( $user, $rev, $title );
if ( $cdel !== '' ) {
$cdel .= ' ';
}
// Modified for Approved Revs
$outputPage->addSubtitle( "<div id=\"mw-revision-nav\">" . $cdel .
wfMessage( 'approvedrevs-revision-nav' )->rawParams(
$prevdiff, $prevlink, $approvedlink, $approveddiff, $lnk, $curdiff, $nextlink, $nextdiff
)->escaped() . "</div>" );
}
/**
* If user is viewing the page via its main URL, and what they're
* seeing is the approved revision of the page, remove the standard
* subtitle shown for all non-latest revisions, and replace it with
* either nothing or a message explaining the situation, depending
* on the user's rights.
*/
public static function setSubtitle( &$article, &$revisionID ) {
$title = $article->getTitle();
if ( !ApprovedRevs::hasApprovedRevision( $title ) ) {
return true;
}
$context = $article->getContext();
$request = $context->getRequest();
if ( $request->getCheck( 'oldid' ) ) {
// If the user is looking at the latest revision,
// disable caching, to avoid the wiki getting the
// contents from the cache, and thus getting the
// approved contents instead.
if ( $revisionID == $article->getPage()->getLatest() ) {
global $wgEnableParserCache;
$wgEnableParserCache = false;
}
self::setOldSubtitle( $article, $revisionID );
// Don't show default Article::setOldSubtitle().
return false;
}
$text = "";
ApprovedRevs::addCSS();
$user = $context->getUser();
if ( ApprovedRevs::checkPermission( $user, $title, "viewlinktolatest" ) ) {
if ( $revisionID == $article->getPage()->getLatest() ) {
$text .= Xml::element(
'span',
[ 'class' => 'approvedAndLatestMsg' ],
wfMessage( 'approvedrevs-approvedandlatest' )->text()
);
} else {
$text .= wfMessage( 'approvedrevs-notlatest' )->parse();
$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
$text .= ' ' . $linkRenderer->makeLink(
$title,
wfMessage( 'approvedrevs-viewlatestrev' )->parse(),
[],
[ 'oldid' => $article->getPage()->getLatest() ]
);
$text = Xml::tags(
'span',
[ 'class' => 'notLatestMsg' ],
$text
);
}
}
if ( ApprovedRevs::checkPermission( $user, $title, "viewapprover" ) ) {
$revisionUser = ApprovedRevs::getRevApprover( $title );
if ( $revisionUser ) {
$text .= Html::rawElement(
'span',
[ 'class' => 'approvingUser' ],
wfMessage( 'approvedrevs-approver' )->rawParams(
Linker::userLink( $revisionUser->getId(), $revisionUser->getName() )
)->escaped()
);
}
}
if ( $text !== "" ) {
$out = $context->getOutput();
if ( $out->getSubtitle() != '' ) {
$out->addSubtitle( '<br />' . $text );
} else {
$out->setSubtitle( $text );
}
}
return false;
}
/**
* Hook: TitleGetEditNotices
*
* Add an edit notice if the approved revision
* is not the same as the latest one, so that users don't
* get confused, since they'll be seeing the latest one.
* Also add an edit notice if the user can't automatically
* approve.
*
* @param Title $title
* @param int $oldid
* @param array &$notices
*/
public static function addEditNotice( Title $title, $oldid, array &$notices ) {
$editPage = new EditPage( new Article( $title, $oldid ) );
$editPage->oldid = $oldid;
$article = $editPage->getArticle();
$context = $article->getContext();
$request = $context->getRequest();
$user = $context->getUser();
$approvedRevID = ApprovedRevs::getApprovedRevID( $title );
$latestRevID = $title->getLatestRevID();
if ( empty( $approvedRevID ) ) {
return true;
}
if ( $approvedRevID != $latestRevID && !$request->getCheck( 'oldid' ) ) {
// MediaWiki 1.40+
if ( class_exists( 'MediaWiki\Html\Html' ) ) {
$notices['approvedrevs-editwarning'] = MediaWiki\Html\Html::warningBox(
$context->msg( 'approvedrevs-editwarning',
$title->getFullURL( [
'diff' => $latestRevID,
'oldid' => $approvedRevID,
] )
)->parse() );
} else {
$notices['approvedrevs-editwarning'] = Html::warningBox(
$context->msg( 'approvedrevs-editwarning',
$title->getFullURL( [
'diff' => $latestRevID,
'oldid' => $approvedRevID,
] )
)->parse() );
}
}
if ( !self::userRevsApprovedAutomatically( $user, $title ) ) {
if ( class_exists( 'MediaWiki\Html\Html' ) ) {
$notices['approvedrevs-editwarning-pending'] =
MediaWiki\Html\Html::warningBox( $context->msg( 'approvedrevs-editwarning-pending' )->escaped() );
} else {
$notices['approvedrevs-editwarning-pending'] =
Html::warningBox( $context->msg( 'approvedrevs-editwarning-pending' )->escaped() );
}
}
return true;
}
/**
* Hook: PageForms::HTMLBeforeForm
*
* Same as addWarningToEditPage(), but for the Page Forms
* 'edit with form' tab.
*/
public static function addWarningToPFForm( &$title, &$preFormHTML ) {
if ( $title == null || !$title->exists() ) {
return true;
}
$approvedRevID = ApprovedRevs::getApprovedRevID( $title );
$latestRevID = $title->getLatestRevID();
if ( !empty( $approvedRevID ) && $approvedRevID != $latestRevID ) {
ApprovedRevs::addCSS();
$preFormHTML .= Xml::element( 'p',
[ 'class' => 'approvedRevsEditWarning' ],
wfMessage( 'approvedrevs-editwarning' )->text() ) . "\n";
}
}
/**
* Hook: SkinTemplateNavigation::Universal
*
* If user is looking at a revision through a main 'view' URL (no
* revision specified), have the 'edit' tab link to the basic
* 'action=edit' URL (i.e., the latest revision), no matter which
* revision they're actually on.
*/
public static function changeEditLink( SkinTemplate &$skinTemplate, &$links ) {
$context = $skinTemplate->getContext();
$request = $context->getRequest();
if ( $request->getCheck( 'oldid' ) ) {
return;
}
$title = $skinTemplate->getTitle();
if ( ApprovedRevs::hasApprovedRevision( $title ) ) {
$contentActions = &$links['views'];
// The URL is the same regardless of whether the tab
// is 'edit' or 'view source', but the "action" is
// different.
if ( array_key_exists( 'edit', $contentActions ) ) {
$contentActions['edit']['href'] = $title->getLocalUrl( [ 'action' => 'edit' ] );
}
if ( array_key_exists( 've-edit', $contentActions ) ) {
$contentActions['ve-edit']['href'] = $title->getLocalUrl( [ 'veaction' => 'edit' ] );
}
if ( array_key_exists( 'viewsource', $contentActions ) ) {
$contentActions['viewsource']['href'] = $title->getLocalUrl( [ 'action' => 'edit' ] );
$skinTemplate->getOutput()->addJsConfigVars( 'wgEditLatestRevision', true );
}
}
}
/**
* Hook: PageHistoryLineEnding
*
* If the user is allowed to make revision approvals, add either an
* 'approve' or 'unapprove' link to the end of this row in the page
* history, depending on whether or not this is already the approved
* revision. If it's the approved revision also add on a "star"
* icon, regardless of the user.
*/
public static function addApprovalLink( $historyPage, &$row, &$s, &$classes ) {
$title = $historyPage->getTitle();
if ( !ApprovedRevs::pageIsApprovable( $title ) ) {
return;
}
$context = $historyPage->getContext();
$user = $context->getUser();
$approvedRevID = ApprovedRevs::getApprovedRevID( $title );
if ( $row->rev_id == $approvedRevID ) {
$s .= ' &#9733;';
// add class to this line to highlight the approved rev with CSS
if ( is_array( $classes ) ) {
$classes[] = "approved-revision";
} else {
$classes = [ "approved-revision" ];
}
// We also need to add the CSS.
ApprovedRevs::addCSS();
}
if ( ApprovedRevs::userCanApprove( $user, $title ) ) {
if ( $row->rev_id == $approvedRevID ) {
$url = $title->getLocalUrl(
[ 'action' => 'unapprove' ]
);
$msg = wfMessage( 'approvedrevs-unapprove' )->text();
} else {
$url = $title->getLocalUrl(
[ 'action' => 'approve', 'oldid' => $row->rev_id ]
);
$msg = wfMessage( 'approvedrevs-approve' )->text();
}
$s .= ' (' . Xml::element(
'a',
[ 'href' => $url ],
$msg
) . ')';
}
}
public static function addApprovalDiffLink(
RevisionRecord $newRevision,
array &$links,
?RevisionRecord $prevRevision,
UserIdentity $userIdentity
) {
$title = Title::castFromLinkTarget( $newRevision->getPageAsLinkTarget() );
if ( $prevRevision === null || $title === null || !ApprovedRevs::pageIsApprovable( $title ) ) {
return;
}
$approvedRevID = ApprovedRevs::getApprovedRevID( $title );
if ( ApprovedRevs::userCanApprove( $userIdentity, $title ) && $prevRevision->getID() == $approvedRevID ) {
// array key is class applied to <span> wrapping around link
// default if blank is mw-diff-tool; add that along with extension-specific class
$links['mw-diff-tool ext-approved-revs-approval-span'] = HTML::element(
'a',
[
'href' => $title->getLocalUrl( [
'action' => 'approve',
'oldid' => $newRevision->getId()
] ),
'class' => 'ext-approved-revs-approval-link',
'title' => wfMessage( 'approvedrevs-approvethisrev' )->text()
],
wfMessage( 'approvedrevs-approve' )->text()
);
}
}
/**
* @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeParserFetchTemplateRevisionRecord
*
* Use the approved revision, if it exists, for templates and other
* transcluded pages.
*/
public static function setTranscludedPageRev(
?LinkTarget $contextTitle,
LinkTarget $titleTarget,
bool &$skip,
?RevisionRecord &$revRecord
) {
$title = Title::castFromLinkTarget( $titleTarget );
$revisionID = ApprovedRevs::getApprovedRevID( $title );
if ( !empty( $revisionID ) ) {
$revLookup = MediaWikiServices::getInstance()->getRevisionLookup();
$revRecord = $revLookup->getRevisionById( $revisionID );
}
}
/**
* Hook: PageDeleteComplete
*
* Delete the approval record in the database if the page itself is
* deleted.
*/
public static function deleteRevisionApproval(
ProperPageIdentity $page, Authority $deleter, string $reason, int $pageID,
RevisionRecord $deletedRev, ManualLogEntry $logEntry, int $archivedRevisionCount
) {
ApprovedRevs::unsetApprovalInDB( $page->getTitle() );
}
/**
* @see https://www.mediawiki.org/wiki/Manual:Hooks/GetMagicVariableIDs
*
* Register magic-word variable IDs
*/
public static function addMagicWordVariableIDs( &$magicWordVariableIDs ) {
$magicWordVariableIDs[] = 'MAG_APPROVEDREVS';
$magicWordVariableIDs = array_merge( $magicWordVariableIDs, self::$mApprovalMagicWords );
}
/**
* Hook: ParserAfterTidy
*
* Set values in the page_props table based on the presence of the
* 'APPROVEDREVS' magic word in a page
*/
public static function handleMagicWords( &$parser, &$text ) {
$factory = MediaWikiServices::getInstance()->getMagicWordFactory();
$mw_hide = $factory->get( 'MAG_APPROVEDREVS' );
if ( $mw_hide->matchAndRemove( $text ) ) {
$parserOutput = $parser->getOutput();
$parserOutput->setPageProperty( 'approvedrevs', 'y' );
}
}
/**
* Hook: ParserGetVariableValueSwitch
*
* Assign a value to our variable.
*
* @param Parser $parser
* @param array &$variableCache
* @param string $magicWordId
* @param string &$ret
*/
public static function assignAValue( $parser, &$variableCache, $magicWordId, &$ret ) {
if ( !in_array( $magicWordId, self::$mApprovalMagicWords ) ) {
return;
}
$approvalInfo = ARParserFunctions::getApprovalInfo( $parser->getTitle() );
if ( $approvalInfo == null ) {
$variableCache[$magicWordId] = '';
$ret = '';
return;
}
switch ( $magicWordId ) {
case 'MAG_APPROVALYEAR':
$ret = date( 'Y', $approvalInfo[0] );
break;
case 'MAG_APPROVALMONTH':
$ret = date( 'm', $approvalInfo[0] );
break;
case 'MAG_APPROVALDAY':
$ret = date( 'd', $approvalInfo[0] );
break;
case 'MAG_APPROVALTIMESTAMP':
$ret = date( 'YmdHis', $approvalInfo[0] );
break;
case 'MAG_APPROVALUSER':
$userID = $approvalInfo[1];
$user = User::newFromID( $userID );
$ret = $user->getName();
break;
default:
return;
}
$variableCache[$magicWordId] = $ret;
}
/**
* Hook: ParserFirstCallInit
*
* Register parser function(s).
*
* @param Parser &$parser
* @since 1.0
*/
public static function registerFunctions( &$parser ) {
$parser->setFunctionHook(
'approvable_by',
'ARParserFunctions::renderApprovableBy',
Parser::SFH_OBJECT_ARGS
);
$parser->setFunctionHook(
'MAG_APPROVALYEAR',
'ARParserFunctions::renderApprovalYear',
Parser::SFH_NO_HASH
);
$parser->setFunctionHook(
'MAG_APPROVALMONTH',
'ARParserFunctions::renderApprovalMonth',
Parser::SFH_NO_HASH
);
$parser->setFunctionHook(
'MAG_APPROVALDAY',
'ARParserFunctions::renderApprovalDay',
Parser::SFH_NO_HASH
);
$parser->setFunctionHook(
'MAG_APPROVALTIMESTAMP',
'ARParserFunctions::renderApprovalTimestamp',
Parser::SFH_NO_HASH
);
$parser->setFunctionHook(
'MAG_APPROVALUSER',
'ARParserFunctions::renderApprovalUser',
Parser::SFH_NO_HASH
);
}
/**
* Hook: AdminLinks
*
* Add a link to Special:ApprovedPages to the page
* Special:AdminLinks, defined by the Admin Links extension.
*/
public static function addToAdminLinks( &$admin_links_tree ) {
$general_section = $admin_links_tree->getSection( wfMessage( 'adminlinks_general' )->text() );
$extensions_row = $general_section->getRow( 'extensions' );
if ( $extensions_row === null ) {
$extensions_row = new ALRow( 'extensions' );
$general_section->addRow( $extensions_row );
}
$extensions_row->addItem( ALItem::newFromSpecialPage( 'ApprovedRevs' ) );
}
public static function describeDBSchema( DatabaseUpdater $updater ) {
$dir = __DIR__;
// DB updates
// For now, there's just a single SQL file for all DB types.
//if ( $updater->getDB()->getType() == 'mysql' ) {
$updater->addExtensionUpdate( [
'addTable', 'approved_revs', "$dir/../sql/ApprovedRevs.sql", true
] );
$updater->addExtensionUpdate( [
'addField', 'approved_revs', 'approver_id', "$dir/../sql/patch-approver_id.sql", true
] );
$updater->addExtensionUpdate( [
'addField', 'approved_revs', 'approval_date', "$dir/../sql/patch-approval_date.sql", true
] );
$updater->addExtensionUpdate( [
'addTable', 'approved_revs_files', "$dir/../sql/ApprovedFiles.sql", true
] );
// }
}
/**
* Hook: ArticleViewHeader
*
* Display a message to the user if (a) "blank if unapproved" is set,
* (b) the page is approvable, (c) the user has 'viewlinktolatest'
* permission, and (d) either the page has no approved revision, or
* the user is looking at a revision that's not the latest - the
* displayed message depends on which of those cases it is.
* @todo - this should probably get split up into two methods.
*
* @since 0.5.6
*
* @param Article $article
* @param bool &$outputDone
* @param bool &$useParserCache
*/
public static function setArticleHeader( Article $article, &$outputDone, &$useParserCache ) {
global $egApprovedRevsBlankIfUnapproved;
// For now, we only set the header if "blank if unapproved"
// is set.
if ( !$egApprovedRevsBlankIfUnapproved ) {
return;
}
$title = $article->getTitle();
$context = $article->getContext();
$user = $context->getUser();
$out = $context->getOutput();
$request = $context->getRequest();
if ( !ApprovedRevs::pageIsApprovable( $title ) ) {
return;
}
// If the user isn't supposed to see these kinds of
// messages, exit.
if ( !ApprovedRevs::checkPermission( $user, $title, "viewlinktolatest" ) ) {
return;
}
// If there's an approved revision for this page, and the
// user is looking at it - either by simply going to the page,
// or by looking at the revision that happens to be approved -
// don't display anything.
$approvedRevID = ApprovedRevs::getApprovedRevID( $title );
if ( !empty( $approvedRevID ) &&
( !$request->getCheck( 'oldid' ) ||
$request->getInt( 'oldid' ) == $approvedRevID ) ) {
return;
}
// Disable caching, so that if it's a specific ID being shown
// that happens to be the latest, it doesn't show a blank page.
$useParserCache = false;
$out->addHTML( '<span style="margin-left: 10.75px">' );
// If the user is looking at a specific revision, show an
// "approve this revision" message - otherwise, it means
// there's no approved revision (we would have exited out if
// there were), so show a message explaining why the page is
// blank, with a link to the latest revision.
if ( $request->getCheck( 'oldid' ) ) {
if ( ApprovedRevs::userCanApprove( $user, $title ) ) {
// @TODO - why is this message being shown
// at all? Aren't the "approve this revision"
// links in the history page always good
// enough?
$out->addHTML( Xml::tags( 'span', [ 'id' => 'contentSub2' ],
Xml::element( 'a',
[ 'href' => $title->getLocalUrl(
[
'action' => 'approve',
'oldid' => $request->getInt( 'oldid' )
]
) ],
wfMessage( 'approvedrevs-approvethisrev' )->text()
) ) );
}
} else {
$out->addSubtitle(
htmlspecialchars( wfMessage( 'approvedrevs-blankpageshown' )->text() ) . '&#160;' .
Xml::element( 'a',
[ 'href' => $title->getLocalUrl(
[
'oldid' => $article->getRevIdFetched()
]
) ],
wfMessage( 'approvedrevs-viewlatestrev' )->text()
)
);
}
$out->addHTML( '</span>' );
}
/**
* Hook: ArticleViewHeader
*
* If this page is approvable, but has no approved revision, display
* a header message stating that, if the setting to display this
* message is activated.
*/
public static function displayNotApprovedHeader( Article $article, &$outputDone, &$useParserCache ) {
global $egApprovedRevsShowNotApprovedMessage;
if ( !$egApprovedRevsShowNotApprovedMessage ) {
return;
}
// If we're looking at an old revision of the page, no need to
// display anything.
if ( $article->mOldId !== 0 ) {
return;
}
$title = $article->getTitle();
if ( !ApprovedRevs::pageIsApprovable( $title ) ) {
return;
}
if ( ApprovedRevs::hasApprovedRevision( $title ) ) {
return;
}
$text = wfMessage( 'approvedrevs-noapprovedrevision' )->escaped();
$context = $article->getContext();
$out = $context->getOutput();
if ( $out->getSubtitle() != '' ) {
$out->addSubtitle( '<br />' . $text );
} else {
$out->setSubtitle( $text );
}
}
/**
* Add a class to the <body> tag indicating the approval status
* of this page, so it can be styled accordingly.
*/
public static function addBodyClass( $out, $skin, &$bodyAttrs ) {
$title = $skin->getTitle();
$context = $skin->getContext();
$request = $context->getRequest();
if ( !ApprovedRevs::hasApprovedRevision( $title ) ) {
// This page has no approved rev.
$bodyAttrs['class'] .= " approvedRevs-noapprovedrev";
} else {
// The page has an approved rev - see if this is it.
$approvedRevID = ApprovedRevs::getApprovedRevID( $title );
if ( !empty( $approvedRevID ) &&
( !$request->getCheck( 'oldid' ) ||
$request->getInt( 'oldid' ) == $approvedRevID ) ) {
// This is the approved rev.
$bodyAttrs['class'] .= " approvedRevs-approved";
} else {
// This is not the approved rev.
$bodyAttrs['class'] .= " approvedRevs-notapproved";
}
}
}
/**
* Hook: ImagePageFileHistoryLine
*
* On image pages (pages in NS_FILE), modify each line in the file history
* (file history, not history of wikitext on file page). Add
* "approved-revision" class to the appropriate row. For users with
* approve permissions on this page add "approve" and "unapprove" links as
* required.
*/
public static function onImagePageFileHistoryLine( $hist, $file, &$s, &$rowClass ) {
$fileTitle = $file->getTitle();
if ( !ApprovedRevs::fileIsApprovable( $fileTitle ) ) {
return;
}
$rowTimestamp = $file->getTimestamp();
$rowSha1 = $file->getSha1();
[ $approvedRevTimestamp, $approvedRevSha1 ] =
ApprovedRevs::getApprovedFileInfo( $file->getTitle() );
ApprovedRevs::addCSS();
// Apply class to row of approved revision
// Note: both here and below in the "userCanApprove" section, if the
// timestamp condition is removed then all rows with the same sha1 as
// the approved rev will be given the class "approved-revision", and
// highlighted. Only the actual approved rev will be given the
// star icon and approved revision label, though.
if ( $rowSha1 == $approvedRevSha1 && $rowTimestamp == $approvedRevTimestamp ) {
if ( $rowClass ) {
$rowClass .= ' ';
}
$rowClass .= "approved-revision";
$pattern = "/<td[^>]+filehistory-selected+[^>]+>/";
$replace = "$0 &#9733; " . wfMessage( 'approvedrevs-approvedrevision' )->escaped() . "<br />";
$s = preg_replace( $pattern, $replace, $s );
}
$user = $hist->getContext()->getUser();
if ( ApprovedRevs::userCanApprove( $user, $fileTitle ) ) {
if ( $rowSha1 == $approvedRevSha1 && $rowTimestamp == $approvedRevTimestamp ) {
$url = $fileTitle->getLocalUrl(
[ 'action' => 'unapprovefile' ]
);
$msg = wfMessage( 'approvedrevs-unapprove' )->text();
} else {
$url = $fileTitle->getLocalUrl(
[
'action' => 'approvefile',
'ts' => $rowTimestamp,
'sha1' => $rowSha1
]
);
$msg = wfMessage( 'approvedrevs-approve' )->text();
}
$s .= '<td>' . Xml::element(
'a',
[ 'href' => $url ],
$msg
) . '</td>';
}
}
/**
* Hook: BeforeParserFetchFileAndTitle
*
* Changes links and thumbnails of files to point to the approved
* revision in all cases except the primary file on file pages (e.g.
* the big image in the top left on File:My File.png). To modify that
* image, see self::onImagePageFindFile().
*/
public static function modifyFileLinks( $parser, Title $fileTitle, &$options, &$query ) {
if ( $fileTitle->getNamespace() == NS_MEDIA ) {
$fileTitle = Title::makeTitle( NS_FILE, $fileTitle->getDBkey() );
// avoid extra queries
$fileTitle->resetArticleId( $fileTitle->getArticleID() );
// Media link redirects don't get caught by the normal
// redirect check, so this extra check is required
$fileWikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()
->newFromID( $fileTitle->getArticleID() );
if ( $fileWikiPage && $fileWikiPage->getRedirectTarget() ) {
$fileTitle = $fileWikiPage->getTitle();
}
}
if ( $fileTitle->isRedirect() ) {
$page = MediaWikiServices::getInstance()->getWikiPageFactory()
->newFromID( $fileTitle->getArticleID() );
$fileTitle = $page->getRedirectTarget();
// avoid extra queries
$fileTitle->resetArticleId( $fileTitle->getArticleID() );
}
# Tell Parser what file version to use
[ $approvedRevTimestamp, $approvedRevSha1 ] =
ApprovedRevs::getApprovedFileInfo( $fileTitle );
// No approved version of this file - just exit here, and
// possibly make it look like the file doesn't exist.
if ( ( !$approvedRevTimestamp ) || ( !$approvedRevSha1 ) ) {
global $egApprovedRevsBlankFileIfUnapproved;
if ( $egApprovedRevsBlankFileIfUnapproved ) {
$options['broken'] = true;
}
return true;
}
$options['time'] = wfTimestampOrNull( TS_MW, $approvedRevTimestamp );
$options['sha1'] = $approvedRevSha1;
# Stabilize the file link
if ( $query != '' ) {
$query .= '&';
}
$query .= "filetimestamp=" . urlencode(
wfTimestamp( TS_MW, $approvedRevTimestamp )
);
}
/**
* Hook: ImagePageFindFile
*
* Applicable on image pages only, this changes the primary image
* on the page from the most recent to the approved revision.
*/
public static function onImagePageFindFile( $imagePage, &$normalFile, &$displayFile ) {
[ $approvedRevTimestamp, $approvedRevSha1 ] =
ApprovedRevs::getApprovedFileInfo( $imagePage->getFile()->getTitle() );
if ( ( !$approvedRevTimestamp ) || ( !$approvedRevSha1 ) ) {
return;
}
$repo = MediaWikiServices::getInstance()->getRepoGroup();
$title = $imagePage->getTitle();
$displayFile = $repo->findFile(
$title, [ 'time' => $approvedRevTimestamp ]
);
# If none found, try current
if ( !$displayFile ) {
wfDebug( __METHOD__ . ": {$title->getPrefixedDBkey()}: " .
"$approvedRevTimestamp not found, using current\n" );
$displayFile = $repo->findFile( $title );
// If still none found, use a valid local placeholder.
if ( !$displayFile ) {
// fallback to current
$displayFile = $repo->getLocalRepo()->newFile( $title );
}
$normalFile = $displayFile;
# If found, set $normalFile
} else {
wfDebug( __METHOD__ . ": {$title->getPrefixedDBkey()}: " .
"using timestamp $approvedRevTimestamp\n" );
$normalFile = $repo->findFile( $title );
}
}
/**
* Hook: FileDeleteComplete
*
* If a file is deleted, check if the sha1 (and timestamp?) exist in the
* approved_revs_files table, and delete that row accordingly. A deleted
* version of a file should not be the approved version!
*/
public static function onFileDeleteComplete( File $file, $oldimage, $article, $user, $reason ) {
$dbr = ApprovedRevs::getReadDB();
// check if this file has an approved revision
$approvedFile = $dbr->selectRow(
'approved_revs_files',
[ 'approved_timestamp', 'approved_sha1' ],
[ 'file_title' => $file->getTitle()->getDBkey() ],
__METHOD__
);
// If an approved revision exists, loop through all files in
// history. Since this hook happens AFTER deletion (there is
// no hook before deletion), check to see if the sha1 of the
// approved revision is NOT in the history. If it is not in
// the history, then it has no business being in the
// approved_revs_files table, and should be deleted.
if ( $approvedFile ) {
$revs = [];
$approvedExists = false;
$hist = $file->getHistory();
foreach ( $hist as $OldLocalFile ) {
// need to check both sha1 and timestamp, since
// reverted files can have the same sha1, but
// different timestamps
if (
$OldLocalFile->getTimestamp() == $approvedFile->approved_timestamp
&& $OldLocalFile->getSha1() == $approvedFile->approved_sha1 ) {
$approvedExists = true;
}
}
if ( !$approvedExists ) {
ApprovedRevs::unsetApprovedFileInDB( $file->getTitle(), $user );
}
}
}
/**
* Hook: wgQueryPages
*
* @param array &$qp
*/
public static function onwgQueryPages( &$qp ) {
$qp['SpecialApprovedRevsPage'] = 'ApprovedRevs';
}
public static function onMpdfGetArticle( $title, &$article ) {
$revisionID = ApprovedRevs::getApprovedRevID( $title );
if ( $revisionID === null ) {
return;
}
$article = new Article( $title, $revisionID );
}
}