1017 lines
30 KiB
PHP
1017 lines
30 KiB
PHP
<?php
|
|
|
|
use MediaWiki\Content\Renderer\ContentParseParams;
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\Revision\SlotRecord;
|
|
use MediaWiki\User\UserNameUtils;
|
|
|
|
/**
|
|
* Main class for the Approved Revs extension.
|
|
*
|
|
* @file
|
|
* @ingroup Extensions
|
|
*
|
|
* @author Yaron Koren
|
|
*/
|
|
class ApprovedRevs {
|
|
|
|
/**
|
|
* Static arrays to prevent querying the database more than necessary.
|
|
*/
|
|
private static $mApprovedContentForPage = [];
|
|
private static $mApprovedRevIDForPage = [];
|
|
private static $mApproverForPage = [];
|
|
private static $mApprovablePages = [];
|
|
private static $mApprovableFiles = [];
|
|
|
|
/**
|
|
* Array in form $mUserCanApprove["<user id>:<article id>"] = <bool>
|
|
* @var array
|
|
*/
|
|
private static $mUserCanApprove = [];
|
|
private static $mApprovedFileInfo = [];
|
|
|
|
private static $mApprovedRevsNamespaces;
|
|
|
|
/**
|
|
* Get array of approvable namespaces. Handles backwards compatibility.
|
|
*/
|
|
public static function getApprovableNamespaces() {
|
|
global $egApprovedRevsNamespaces, $egApprovedRevsEnabledNamespaces;
|
|
|
|
if ( is_array( self::$mApprovedRevsNamespaces ) ) {
|
|
return self::$mApprovedRevsNamespaces;
|
|
}
|
|
|
|
self::$mApprovedRevsNamespaces = [];
|
|
|
|
// Definition of $egApprovedRevsNamespaces removed from Approved
|
|
// Revs to handle extension.json method of config settings. Now,
|
|
// if $egApprovedRevsNamespaces is defined, then it must have been
|
|
// defined by the user in LocalSettings.php. Since no good method of
|
|
// providing backwards-compatibility was determined, instead notify user
|
|
// that $egApprovedRevsNamespaces is obsolete.
|
|
if ( isset( $egApprovedRevsNamespaces ) ) {
|
|
throw new MWException( '$egApprovedRevsNamespaces is no longer supported - ' .
|
|
'please use $egApprovedRevsEnabledNamespaces (which has a different format) instead' );
|
|
}
|
|
|
|
// since extension.json values have to be strings, convert to int
|
|
// changes [ "0" => true, "10" => false, "14" => true ] to [0, 14]
|
|
self::$mApprovedRevsNamespaces = array_keys( array_filter( $egApprovedRevsEnabledNamespaces ) );
|
|
|
|
return self::$mApprovedRevsNamespaces;
|
|
}
|
|
|
|
/**
|
|
* Gets the approved revision User for this page, or null if there isn't
|
|
* one.
|
|
*
|
|
* @param Title $title The page title
|
|
* @return User|null The approver
|
|
*/
|
|
public static function getRevApprover( $title ) {
|
|
if ( self::pageIsApprovable( $title ) ) {
|
|
return null;
|
|
}
|
|
|
|
$pageID = $title->getArticleID();
|
|
if ( !isset( self::$mApproverForPage[$pageID] ) ) {
|
|
$dbr = self::getReadDB();
|
|
$approverID = $dbr->selectField( 'approved_revs', 'approver_id',
|
|
[ 'page_id' => $pageID ] );
|
|
$approver = $approverID ? User::newFromID( $approverID ) : null;
|
|
self::$mApproverForPage[$pageID] = $approver;
|
|
}
|
|
return self::$mApproverForPage[$pageID];
|
|
}
|
|
|
|
/**
|
|
* Gets the approved revision ID for this page, or null if there isn't
|
|
* one.
|
|
*/
|
|
public static function getApprovedRevID( $title ) {
|
|
if ( $title == null ) {
|
|
return null;
|
|
}
|
|
|
|
$pageID = $title->getArticleID();
|
|
if ( array_key_exists( $pageID, self::$mApprovedRevIDForPage ) ) {
|
|
return self::$mApprovedRevIDForPage[$pageID];
|
|
}
|
|
|
|
if ( !self::pageIsApprovable( $title ) ) {
|
|
return null;
|
|
}
|
|
|
|
$dbr = self::getReadDB();
|
|
$revID = $dbr->selectField( 'approved_revs', 'rev_id', [ 'page_id' => $pageID ] );
|
|
self::$mApprovedRevIDForPage[$pageID] = $revID;
|
|
return $revID;
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not this page has a revision ID.
|
|
*/
|
|
public static function hasApprovedRevision( $title ) {
|
|
$revision_id = self::getApprovedRevID( $title );
|
|
return ( !empty( $revision_id ) );
|
|
}
|
|
|
|
public static function getContent( $title, $revisionID = 0 ) {
|
|
$revisionRecord = MediaWikiServices::getInstance()->getRevisionLookup()
|
|
->getRevisionByTitle( $title, $revisionID );
|
|
return $revisionRecord->getContent( SlotRecord::MAIN );
|
|
}
|
|
|
|
/**
|
|
* Returns the contents of the specified wiki page, at either the
|
|
* specified revision (if there is one) or the latest revision
|
|
* (otherwise).
|
|
*/
|
|
public static function getPageText( $title, $revisionID = null ) {
|
|
return self::getContent( $title, $revisionID )->getText();
|
|
}
|
|
|
|
/**
|
|
* Returns the content of the approved revision of this page, or null
|
|
* if there isn't one.
|
|
*/
|
|
public static function getApprovedContent( $title ) {
|
|
$pageID = $title->getArticleID();
|
|
if ( array_key_exists( $pageID, self::$mApprovedContentForPage ) ) {
|
|
return self::$mApprovedContentForPage[$pageID];
|
|
}
|
|
|
|
$revisionID = self::getApprovedRevID( $title );
|
|
if ( empty( $revisionID ) ) {
|
|
return null;
|
|
}
|
|
|
|
$content = self::getContent( $title, $revisionID );
|
|
self::$mApprovedContentForPage[$pageID] = $content;
|
|
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
* Helper function - returns whether the user is currently requesting
|
|
* a page via the simple URL for it - not specfying a version number,
|
|
* not editing the page, etc.
|
|
*/
|
|
public static function isDefaultPageRequest( $request ) {
|
|
if ( $request->getCheck( 'oldid' ) ) {
|
|
return false;
|
|
}
|
|
// Check if it's an action other than viewing.
|
|
if ( $request->getCheck( 'action' ) &&
|
|
$request->getVal( 'action' ) != 'view' &&
|
|
$request->getVal( 'action' ) != 'purge' &&
|
|
$request->getVal( 'action' ) != 'render' ) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static function getReadDB() {
|
|
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
|
|
if ( method_exists( $lbFactory, 'getReplicaDatabase' ) ) {
|
|
// MW 1.40+
|
|
return $lbFactory->getReplicaDatabase();
|
|
} else {
|
|
return $lbFactory->getMainLB()->getConnection( DB_REPLICA );
|
|
}
|
|
}
|
|
|
|
public static function getWriteDB() {
|
|
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
|
|
if ( method_exists( $lbFactory, 'getPrimaryDatabase' ) ) {
|
|
// MW 1.40+
|
|
return $lbFactory->getPrimaryDatabase();
|
|
} else {
|
|
return $lbFactory->getMainLB()->getConnection( DB_PRIMARY );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether this page can be approved - either because it's in
|
|
* a supported namespace, or because it's been specially marked as
|
|
* approvable. Also stores the boolean answer as a field in the page
|
|
* object, to speed up processing if it's called more than once.
|
|
*/
|
|
public static function pageIsApprovable( Title $title ) {
|
|
$articleID = $title->getArticleID();
|
|
|
|
// If this function was already called for this page, the value
|
|
// should have been stored.
|
|
if ( array_key_exists( $articleID, self::$mApprovablePages ) ) {
|
|
return self::$mApprovablePages[$articleID];
|
|
}
|
|
|
|
if ( !$title->exists() ) {
|
|
self::$mApprovablePages[$articleID] = false;
|
|
return false;
|
|
}
|
|
|
|
// File *pages* are not ever approvable. Files themselves can
|
|
// be, but checks for file approvability are handled by
|
|
// fileIsApprovable(). This constraint is to avoid confusion
|
|
// between approving file pages and approving files themselves.
|
|
if ( $title->getNamespace() === NS_FILE ) {
|
|
self::$mApprovablePages[$articleID] = false;
|
|
return false;
|
|
}
|
|
|
|
// Allow custom setting of whether the page is approvable.
|
|
if ( !MediaWikiServices::getInstance()->getHookContainer()
|
|
->run( 'ApprovedRevsPageIsApprovable', [ $title, &$isApprovable ] )
|
|
) {
|
|
self::$mApprovablePages[$articleID] = $isApprovable;
|
|
return $isApprovable;
|
|
}
|
|
|
|
// Check the namespace.
|
|
if ( in_array( $title->getNamespace(), self::getApprovableNamespaces() ) ) {
|
|
self::$mApprovablePages[$articleID] = true;
|
|
return true;
|
|
}
|
|
|
|
// It's not in an included namespace, so check for the page
|
|
// properties for the parser functions - for some reason,
|
|
// calling the standard getProperty() function doesn't work, so
|
|
// we just do a DB query on the page_props table.
|
|
$dbr = self::getReadDB();
|
|
$count = $dbr->selectField( 'page_props', 'COUNT(*)',
|
|
[
|
|
'pp_page' => $articleID,
|
|
'pp_propname' => [
|
|
'approvedrevs-approver-users', 'approvedrevs-approver-groups'
|
|
],
|
|
],
|
|
__METHOD__
|
|
);
|
|
if ( $count > 0 ) {
|
|
self::$mApprovablePages[$articleID] = true;
|
|
return true;
|
|
}
|
|
|
|
// parser function page properties not present. Check for magic word.
|
|
$count = $dbr->selectField( 'page_props', 'COUNT(*)',
|
|
[
|
|
'pp_page' => $articleID,
|
|
'pp_propname' => 'approvedrevs',
|
|
'pp_value' => 'y'
|
|
],
|
|
__METHOD__
|
|
);
|
|
$isApprovable = ( $count == '1' );
|
|
self::$mApprovablePages[$articleID] = $isApprovable;
|
|
return $isApprovable;
|
|
}
|
|
|
|
public static function fileIsApprovable( Title $title ) {
|
|
$articleID = $title->getArticleID();
|
|
|
|
// If this function was already called for this page, the value
|
|
// should have been stored.
|
|
if ( array_key_exists( $articleID, self::$mApprovableFiles ) ) {
|
|
return self::$mApprovableFiles[$articleID];
|
|
}
|
|
|
|
if ( !$title->exists() ) {
|
|
self::$mApprovableFiles[$articleID] = false;
|
|
return false;
|
|
}
|
|
|
|
// Allow custom setting of whether the page is approvable.
|
|
if ( !MediaWikiServices::getInstance()->getHookContainer()
|
|
->run( 'ApprovedRevsFileIsApprovable', [ $title, &$fileIsApprovable ] )
|
|
) {
|
|
self::$mApprovableFiles[$articleID] = $fileIsApprovable;
|
|
return $fileIsApprovable;
|
|
}
|
|
|
|
// Check if NS_FILE is in approvable namespaces
|
|
$approvedRevsNamespaces = self::getApprovableNamespaces();
|
|
if ( in_array( NS_FILE, $approvedRevsNamespaces ) ) {
|
|
self::$mApprovableFiles[$articleID] = true;
|
|
return true;
|
|
}
|
|
|
|
// It's not in an included namespace, so check for the page
|
|
// properties for the parser functions - for some reason,
|
|
// calling the standard getProperty() function doesn't work, so
|
|
// we just do a DB query on the page_props table.
|
|
$dbr = self::getReadDB();
|
|
$count = $dbr->selectField( 'page_props', 'COUNT(*)',
|
|
[
|
|
'pp_page' => $articleID,
|
|
'pp_propname' => [
|
|
'approvedrevs-approver-users',
|
|
'approvedrevs-approver-groups'
|
|
],
|
|
],
|
|
__METHOD__
|
|
);
|
|
if ( $count > 0 ) {
|
|
self::$mApprovableFiles[$articleID] = true;
|
|
return true;
|
|
}
|
|
|
|
// Parser function page properties not present. Check for magic word.
|
|
$count = $dbr->selectField( 'page_props', 'COUNT(*)',
|
|
[
|
|
'pp_page' => $articleID,
|
|
'pp_propname' => 'approvedrevs',
|
|
'pp_value' => 'y'
|
|
],
|
|
__METHOD__
|
|
);
|
|
if ( $count == '1' ) {
|
|
self::$mApprovableFiles[$articleID] = true;
|
|
return true;
|
|
}
|
|
|
|
// If a file already has an approval, it must be considered
|
|
// approvable in order for the user to be able to view/modify
|
|
// approvals. Though this wasn't the case on versions of
|
|
// ApprovedRevs before v1.0, it is necessary now since
|
|
// approvability can change much more easily.
|
|
|
|
// if title in approved_revs_files table
|
|
[ $timestamp, $sha1 ] = self::getApprovedFileInfo( $title );
|
|
if ( $timestamp !== false ) {
|
|
// only approvable because it already has an approved rev, not
|
|
// because it is in ApprovedRevs::$permissions
|
|
self::$mApprovableFiles[$articleID] = true;
|
|
return true;
|
|
}
|
|
|
|
self::$mApprovableFiles[$articleID] = false;
|
|
return false;
|
|
}
|
|
|
|
public static function checkPermission( User $user, Title $title, $permission ) {
|
|
$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
|
|
return ( $permissionManager->userCan( $permission, $user, $title ) ||
|
|
$permissionManager->userHasRight( $user, $permission ) );
|
|
}
|
|
|
|
public static function userCanApprove( User $user, Title $title ) {
|
|
global $egApprovedRevsSelfOwnedNamespaces;
|
|
$permission = 'approverevisions';
|
|
|
|
$userAndPageKey = $user->getId() . ':' . $title->getArticleID();
|
|
|
|
// set to null to avoid notices below
|
|
if ( !isset( self::$mUserCanApprove[$userAndPageKey] ) ) {
|
|
self::$mUserCanApprove[$userAndPageKey] = null;
|
|
}
|
|
|
|
// $mUserCanApprove is a static variable used for
|
|
// "caching" the result of this function, so that
|
|
// it only has to be called once per user/page combination
|
|
if ( self::$mUserCanApprove[$userAndPageKey] ) {
|
|
return true;
|
|
} elseif ( self::$mUserCanApprove[$userAndPageKey] === false ) {
|
|
return false;
|
|
} elseif ( self::checkPermission( $user, $title, $permission ) ) {
|
|
self::$mUserCanApprove[$userAndPageKey] = true;
|
|
return true;
|
|
} elseif ( self::checkParserFunctionPermission( $user, $title ) ) {
|
|
self::$mUserCanApprove[$userAndPageKey] = true;
|
|
return true;
|
|
} else {
|
|
// If the user doesn't have the 'approverevisions'
|
|
// permission, nor does #approvable_by grant them
|
|
// permission, they still might be able to approve
|
|
// revisions - it depends on whether the current
|
|
// namespace is within the admin-defined
|
|
// $egApprovedRevsSelfOwnedNamespaces array.
|
|
$namespace = $title->getNamespace();
|
|
if ( in_array( $namespace, $egApprovedRevsSelfOwnedNamespaces ) ) {
|
|
if ( $namespace == NS_USER ) {
|
|
// If the page is in the 'User:'
|
|
// namespace, this user can approve
|
|
// revisions if it's their user page.
|
|
if ( $title->getText() == $user->getName() ) {
|
|
self::$mUserCanApprove[$userAndPageKey] = true;
|
|
return true;
|
|
}
|
|
} else {
|
|
// Otherwise, they can approve revisions
|
|
// if they created the page.
|
|
// We get that information via a SQL
|
|
// query - is there an easier way?
|
|
|
|
// We can't just call self::getReadDB() here, because of
|
|
// the tableExists() call.
|
|
$lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
|
|
$dbr = $lb->getConnectionRef( DB_REPLICA );
|
|
if ( $dbr->tableExists( 'revision_actor_temp' ) ) {
|
|
$tables = [ 'ra' => 'revision_actor_temp', 'a' => 'actor' ];
|
|
$userIDField = 'a.actor_user';
|
|
$pageIDField = 'ra.revactor_page';
|
|
$revIDField = 'ra.revactor_rev';
|
|
$joinOn = [ 'ra' => [ 'JOIN', 'ra.revactor_actor = a.actor_id' ] ];
|
|
} else {
|
|
// The "rev_actor" table field was added in MW 1.35, but for
|
|
// some reason it appears to not always get populated. So,
|
|
// only use it if the temp table is gone.
|
|
$tables = [ 'r' => 'revision', 'a' => 'actor' ];
|
|
$userIDField = 'a.actor_user';
|
|
$pageIDField = 'r.rev_page';
|
|
$revIDField = 'r.rev_id';
|
|
$joinOn = [ 'r' => [ 'JOIN', 'r.rev_actor = a.actor_id' ] ];
|
|
}
|
|
$row = $dbr->selectRow(
|
|
$tables,
|
|
[ 'user_id' => $userIDField ],
|
|
[ $pageIDField => $title->getArticleID() ],
|
|
__METHOD__,
|
|
[ 'ORDER BY' => $revIDField . ' ASC', 'LIMIT' => 1 ],
|
|
$joinOn
|
|
);
|
|
if ( $row->user_id !== null && $row->user_id == $user->getID() ) {
|
|
self::$mUserCanApprove[$userAndPageKey] = true;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self::$mUserCanApprove[$userAndPageKey] = false;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if a user is allowed to approve a page based upon being listed
|
|
* in the page properties approvedrevs-approver-users and
|
|
* approvedrevs-approver-groups.
|
|
*
|
|
* @param User $user Check if this user has #approvable_by permissions on title
|
|
* @param Title $title Title to check
|
|
* @return bool Whether or not approving revisions is allowed
|
|
* @since 1.0
|
|
*/
|
|
public static function checkParserFunctionPermission( User $user, Title $title ) {
|
|
$articleID = $title->getArticleID();
|
|
|
|
$dbr = self::getReadDB();
|
|
|
|
// First check:
|
|
// Users
|
|
|
|
$result = $dbr->selectField(
|
|
'page_props',
|
|
'pp_value',
|
|
[
|
|
'pp_page' => $articleID,
|
|
'pp_propname' => "approvedrevs-approver-users"
|
|
],
|
|
__METHOD__
|
|
);
|
|
if ( $result !== false ) {
|
|
// If user is listed as an approver, allow approval.
|
|
$userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
|
|
$approverUsers = array_map( static function ( $name ) use ( $userNameUtils ) {
|
|
return $userNameUtils->getCanonical( $name, UserNameUtils::RIGOR_USABLE );
|
|
}, explode( ',', $result ) );
|
|
if ( in_array( $user->getName(), $approverUsers ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Second check:
|
|
// Groups
|
|
|
|
$result = $dbr->selectField(
|
|
'page_props',
|
|
'pp_value',
|
|
[
|
|
'pp_page' => $articleID,
|
|
'pp_propname' => "approvedrevs-approver-groups"
|
|
],
|
|
__METHOD__
|
|
);
|
|
if ( $result !== false ) {
|
|
$groups = MediaWikiServices::getInstance()->getUserGroupManager()
|
|
->getUserGroups( $user );
|
|
// intersect groups that can approve with user's group
|
|
$userGroupsWithApprove = array_intersect(
|
|
explode( ',', $result ), $groups
|
|
);
|
|
|
|
// if user has any groups in list of approver groups, allow approval
|
|
if ( count( $userGroupsWithApprove ) > 0 ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// neither group nor username allowed approval...disallow
|
|
return false;
|
|
}
|
|
|
|
public static function saveApprovedRevIDInDB( $title, $rev_id, User $user, $isAutoApprove = true ) {
|
|
$timestamp = date( 'Y-m-d H:i:s' );
|
|
$approvalInfo = [
|
|
'rev_id' => $rev_id,
|
|
'approval_date' => $timestamp
|
|
];
|
|
|
|
if ( !$isAutoApprove ) {
|
|
$approvalInfo['approver_id'] = $user->getID();
|
|
}
|
|
|
|
$dbw = self::getWriteDB();
|
|
$page_id = $title->getArticleID();
|
|
$old_rev_id = $dbw->selectField( 'approved_revs', 'rev_id', [ 'page_id' => $page_id ] );
|
|
if ( $old_rev_id ) {
|
|
$dbw->update( 'approved_revs', $approvalInfo, [ 'page_id' => $page_id ] );
|
|
} else {
|
|
$approvalInfo['page_id'] = $page_id;
|
|
$dbw->insert( 'approved_revs', $approvalInfo );
|
|
}
|
|
// Update "cache" in memory
|
|
self::$mApprovedRevIDForPage[$page_id] = $rev_id;
|
|
self::$mApproverForPage[$page_id] = $user;
|
|
|
|
$content = self::getContent( $title, $rev_id );
|
|
MediaWikiServices::getInstance()->getHookContainer()
|
|
->run( 'ApprovedRevsRevisionApproved', [ $output = null, $title, $rev_id, $content ] );
|
|
}
|
|
|
|
/**
|
|
* @param Title $title
|
|
* @param Content|null $content
|
|
*/
|
|
public static function setPageSearchText( $title, $content ) {
|
|
DeferredUpdates::addUpdate( new SearchUpdate( $title->getArticleID(), $title, $content ) );
|
|
}
|
|
|
|
/**
|
|
* Sets a certain revision as the approved one for this page in the
|
|
* approved_revs DB table; calls a "links update" on this revision
|
|
* so that category information can be stored correctly, as well as
|
|
* info for extensions such as Semantic MediaWiki; and logs the action.
|
|
*/
|
|
public static function setApprovedRevID( $title, $rev_id, User $user, $is_latest = false ) {
|
|
// Don't approve it if it's already approved.
|
|
if ( $rev_id == self::getApprovedRevID( $title ) ) {
|
|
return;
|
|
}
|
|
|
|
self::saveApprovedRevIDInDB( $title, $rev_id, $user, false );
|
|
|
|
// If the revision being approved is definitely the latest
|
|
// one, there's no need to call the parser on it.
|
|
if ( !$is_latest ) {
|
|
$content = self::getContent( $title, $rev_id );
|
|
$contentHandler = $content->getContentHandler();
|
|
$output = self::getParserOutput( $contentHandler, $content, $title, $rev_id, $user );
|
|
$luClass = class_exists( 'LinksUpdate' ) ? LinksUpdate::class :
|
|
MediaWiki\Deferred\LinksUpdate\LinksUpdate::class;
|
|
$u = new $luClass( $title, $output );
|
|
$u->doUpdate();
|
|
self::setPageSearchText( $title, $content );
|
|
}
|
|
|
|
$log = new LogPage( 'approval' );
|
|
$rev_url = $title->getFullURL( [ 'oldid' => $rev_id ] );
|
|
$rev_link = Xml::element(
|
|
'a',
|
|
[ 'href' => $rev_url ],
|
|
$rev_id
|
|
);
|
|
$logParams = [ $rev_link ];
|
|
$log->addEntry(
|
|
'approve',
|
|
$title,
|
|
'',
|
|
$logParams,
|
|
$user
|
|
);
|
|
}
|
|
|
|
public static function unsetApprovalInDB( $title, $content = null ) {
|
|
$dbw = self::getWriteDB();
|
|
$page_id = $title->getArticleID();
|
|
$dbw->delete( 'approved_revs', [ 'page_id' => $page_id ] );
|
|
unset( self::$mApprovedContentForPage[ $page_id ] );
|
|
unset( self::$mApprovedRevIDForPage[ $page_id ] );
|
|
unset( self::$mApproverForPage[ $page_id ] );
|
|
|
|
MediaWikiServices::getInstance()->getHookContainer()
|
|
->run( 'ApprovedRevsRevisionUnapproved', [ $output = null, $title, $content ] );
|
|
}
|
|
|
|
/**
|
|
* Unsets the approved revision for this page in the approved_revs DB
|
|
* table; calls a "links update" on this page so that category
|
|
* information can be stored correctly, as well as info for
|
|
* extensions such as Semantic MediaWiki; and logs the action.
|
|
*/
|
|
public static function unsetApproval( $title, User $user ) {
|
|
global $egApprovedRevsBlankIfUnapproved;
|
|
|
|
// Make sure the page actually has an approved revision.
|
|
if ( self::getApprovedRevID( $title ) == null ) {
|
|
return;
|
|
}
|
|
|
|
$content = self::getContent( $title );
|
|
$contentHandler = $content->getContentHandler();
|
|
if ( $egApprovedRevsBlankIfUnapproved ) {
|
|
$content = $contentHandler->makeEmptyContent();
|
|
}
|
|
$output = self::getParserOutput( $contentHandler, $content, $title, null, $user );
|
|
$luClass = class_exists( 'LinksUpdate' ) ? LinksUpdate::class :
|
|
MediaWiki\Deferred\LinksUpdate\LinksUpdate::class;
|
|
$u = new $luClass( $title, $output );
|
|
$u->doUpdate();
|
|
self::setPageSearchText( $title, $content );
|
|
|
|
self::unsetApprovalInDB( $title, $content );
|
|
|
|
$log = new LogPage( 'approval' );
|
|
$log->addEntry(
|
|
'unapprove',
|
|
$title,
|
|
'',
|
|
[],
|
|
$user
|
|
);
|
|
}
|
|
|
|
public static function getParserOutput( $contentHandler, $content, $title, $revID, $user ) {
|
|
$parserOptions = ParserOptions::newFromUser( $user );
|
|
$contentParseParams = new ContentParseParams( $title, $revID, $parserOptions );
|
|
return $contentHandler->getParserOutput( $content, $contentParseParams );
|
|
}
|
|
|
|
public static function addCSS() {
|
|
global $wgOut;
|
|
$wgOut->addModuleStyles( 'ext.ApprovedRevs' );
|
|
}
|
|
|
|
public static function setApprovedFileInDB( $title, $timestamp, $sha1, User $user ) {
|
|
$parser = MediaWikiServices::getInstance()->getParserFactory()->create();
|
|
$parser->setTitle( $title );
|
|
|
|
$dbw = self::getWriteDB();
|
|
$fileTitle = $title->getDBkey();
|
|
$oldFileTitle = $dbw->selectField(
|
|
'approved_revs_files', 'file_title',
|
|
[ 'file_title' => $fileTitle ]
|
|
);
|
|
if ( $oldFileTitle ) {
|
|
$dbw->update( 'approved_revs_files',
|
|
[
|
|
'approved_timestamp' => $timestamp,
|
|
'approved_sha1' => $sha1
|
|
],
|
|
[ 'file_title' => $fileTitle ]
|
|
);
|
|
} else {
|
|
$dbw->insert( 'approved_revs_files',
|
|
[
|
|
'file_title' => $fileTitle,
|
|
'approved_timestamp' => $timestamp,
|
|
'approved_sha1' => $sha1
|
|
]
|
|
);
|
|
}
|
|
// Update "cache" in memory
|
|
self::$mApprovedFileInfo[$fileTitle] = [ $timestamp, $sha1 ];
|
|
|
|
$log = new LogPage( 'approval' );
|
|
|
|
$imagepage = ImagePage::newFromID( $title->getArticleID() );
|
|
$displayedFileUrl = $imagepage->getDisplayedFile()->getFullURL();
|
|
|
|
$revisionAnchorTag = Xml::element(
|
|
'a',
|
|
[
|
|
'href' => $displayedFileUrl,
|
|
'title' => 'unique identifier: ' . $sha1
|
|
],
|
|
// There's no simple "revision ID" for file uploads. Instead
|
|
// uniqueness is determined by sha1, but dumping out the sha1 here
|
|
// would be ugly. Instead show a timestamp of the file upload.
|
|
wfTimestamp( TS_RFC2822, $timestamp )
|
|
);
|
|
$logParams = [ $revisionAnchorTag ];
|
|
$log->addEntry(
|
|
'approvefile',
|
|
$title,
|
|
'',
|
|
$logParams,
|
|
$user
|
|
);
|
|
|
|
MediaWikiServices::getInstance()->getHookContainer()->run(
|
|
'ApprovedRevsFileRevisionApproved',
|
|
[ $parser, $title, $timestamp, $sha1 ]
|
|
);
|
|
}
|
|
|
|
public static function unsetApprovedFileInDB( $title, User $user ) {
|
|
$parser = MediaWikiServices::getInstance()->getParserFactory()->create();
|
|
$parser->setTitle( $title );
|
|
|
|
$fileTitle = $title->getDBkey();
|
|
|
|
$dbw = self::getWriteDB();
|
|
$dbw->delete( 'approved_revs_files',
|
|
[ 'file_title' => $fileTitle ]
|
|
);
|
|
// the unapprove page method had LinksUpdate and Parser
|
|
// objects here, but the page text has not changed at all with
|
|
// a file approval, so I don't think those are necessary.
|
|
|
|
$log = new LogPage( 'approval' );
|
|
$log->addEntry(
|
|
'unapprove',
|
|
$title,
|
|
'',
|
|
[],
|
|
$user
|
|
);
|
|
|
|
// Delete from the in-memory cache as well.
|
|
self::clearApprovedFileInfo( $title );
|
|
|
|
MediaWikiServices::getInstance()->getHookContainer()->run(
|
|
'ApprovedRevsFileRevisionUnapproved', [ $parser, $title ]
|
|
);
|
|
}
|
|
|
|
public static function clearApprovedFileInfo( Title $fileTitle ) {
|
|
unset( self::$mApprovedFileInfo[ $fileTitle->getDBkey() ] );
|
|
}
|
|
|
|
/**
|
|
* Pulls from the DB table approved_revs_files which revision of a file,
|
|
* if any besides the most recent, should be used as the approved
|
|
* revision.
|
|
*/
|
|
public static function getApprovedFileInfo( $fileTitle ) {
|
|
if ( isset( self::$mApprovedFileInfo[ $fileTitle->getDBkey() ] ) ) {
|
|
return self::$mApprovedFileInfo[ $fileTitle->getDBkey() ];
|
|
}
|
|
|
|
$dbr = self::getReadDB();
|
|
$row = $dbr->selectRow(
|
|
'approved_revs_files',
|
|
[ 'approved_timestamp', 'approved_sha1' ],
|
|
[ 'file_title' => $fileTitle->getDBkey() ],
|
|
__METHOD__
|
|
);
|
|
if ( $row ) {
|
|
$return = [ $row->approved_timestamp, $row->approved_sha1 ];
|
|
} else {
|
|
$return = [ false, false ];
|
|
}
|
|
|
|
self::$mApprovedFileInfo[ $fileTitle->getDBkey() ] = $return;
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* (non-PHPdoc)
|
|
* @see QueryPage::getSQL()
|
|
*/
|
|
public static function getQueryInfoPageApprovals( $mode ) {
|
|
$approvedRevsNamespaces = self::getApprovableNamespaces();
|
|
// Don't include the "File:" namespace in this query - file pages are not
|
|
// approvable. (The presence of NS_FILE instead indicates that files
|
|
// themselves are approvable.)
|
|
$key = array_search( NS_FILE, $approvedRevsNamespaces );
|
|
if ( $key !== false ) {
|
|
unset( $approvedRevsNamespaces[$key] );
|
|
}
|
|
|
|
$mainCondsString = "( pp_propname = 'approvedrevs' AND pp_value = 'y' " .
|
|
"OR pp_propname = 'approvedrevs-approver-users' " .
|
|
"OR pp_propname = 'approvedrevs-approver-groups' )";
|
|
if ( $mode == 'invalid' ) {
|
|
$mainCondsString = "( pp_propname IS NULL OR NOT $mainCondsString )";
|
|
}
|
|
if ( count( $approvedRevsNamespaces ) > 0 ) {
|
|
if ( $mode == 'invalid' ) {
|
|
$mainCondsString .= " AND ( p.page_namespace NOT IN ( " .
|
|
implode( ',', $approvedRevsNamespaces ) . " ) )";
|
|
} else {
|
|
$mainCondsString .= " OR ( p.page_namespace IN ( " .
|
|
implode( ',', $approvedRevsNamespaces ) . " ) )";
|
|
}
|
|
}
|
|
|
|
if ( $mode == 'all' ) {
|
|
return [
|
|
'tables' => [
|
|
'ar' => 'approved_revs',
|
|
'p' => 'page',
|
|
'pp' => 'page_props',
|
|
],
|
|
'fields' => [
|
|
'p.page_id AS id',
|
|
'ar.rev_id AS rev_id',
|
|
'p.page_latest AS latest_id',
|
|
'p.page_namespace',
|
|
'p.page_title',
|
|
],
|
|
'join_conds' => [
|
|
'p' => [
|
|
'JOIN', 'ar.page_id=p.page_id'
|
|
],
|
|
'pp' => [
|
|
'LEFT OUTER JOIN', 'ar.page_id=pp_page'
|
|
],
|
|
],
|
|
'conds' => [ $mainCondsString ],
|
|
'options' => [ 'DISTINCT' ]
|
|
];
|
|
} elseif ( $mode == 'unapproved' ) {
|
|
return [
|
|
'tables' => [
|
|
'ar' => 'approved_revs',
|
|
'p' => 'page',
|
|
'pp' => 'page_props',
|
|
],
|
|
'fields' => [
|
|
'p.page_id AS id',
|
|
'p.page_latest AS latest_id',
|
|
'p.page_namespace',
|
|
'p.page_title',
|
|
],
|
|
'join_conds' => [
|
|
'ar' => [
|
|
'LEFT OUTER JOIN', 'p.page_id=ar.page_id'
|
|
],
|
|
'pp' => [
|
|
'LEFT OUTER JOIN', 'p.page_id=pp_page'
|
|
],
|
|
],
|
|
'conds' => [ 'ar.page_id IS NULL', $mainCondsString ],
|
|
'options' => [ 'DISTINCT' ]
|
|
];
|
|
} elseif ( $mode == 'invalid' ) {
|
|
return [
|
|
'tables' => [
|
|
'ar' => 'approved_revs',
|
|
'p' => 'page',
|
|
'pp' => 'page_props',
|
|
],
|
|
'fields' => [
|
|
'p.page_id AS id',
|
|
'p.page_latest AS latest_id',
|
|
'p.page_namespace',
|
|
'p.page_title',
|
|
],
|
|
'join_conds' => [
|
|
'p' => [
|
|
'LEFT OUTER JOIN', 'p.page_id=ar.page_id'
|
|
],
|
|
'pp' => [
|
|
'LEFT OUTER JOIN', 'ar.page_id=pp_page'
|
|
],
|
|
],
|
|
'conds' => [ $mainCondsString ],
|
|
'options' => [ 'DISTINCT' ]
|
|
];
|
|
} else {
|
|
// 'approved revision is not latest'
|
|
return [
|
|
'tables' => [
|
|
'ar' => 'approved_revs',
|
|
'p' => 'page',
|
|
'pp' => 'page_props',
|
|
],
|
|
'fields' => [
|
|
'p.page_id AS id',
|
|
'ar.rev_id AS rev_id',
|
|
'p.page_latest AS latest_id',
|
|
'p.page_namespace',
|
|
'p.page_title',
|
|
],
|
|
'join_conds' => [
|
|
'p' => [
|
|
'JOIN', 'ar.page_id=p.page_id'
|
|
],
|
|
'pp' => [
|
|
'LEFT OUTER JOIN', 'ar.page_id=pp_page'
|
|
],
|
|
],
|
|
'conds' => [ 'p.page_latest != ar.rev_id', $mainCondsString ],
|
|
'options' => [ 'DISTINCT' ]
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* (non-PHPdoc)
|
|
* @see QueryPage::getSQL()
|
|
*/
|
|
public static function getQueryInfoFileApprovals( $mode ) {
|
|
$tables = [
|
|
'ar' => 'approved_revs_files',
|
|
'im' => 'image',
|
|
'p' => 'page',
|
|
];
|
|
|
|
$fields = [
|
|
'im.img_name AS title',
|
|
'ar.approved_sha1 AS approved_sha1',
|
|
'ar.approved_timestamp AS approved_ts',
|
|
'im.img_sha1 AS latest_sha1',
|
|
'im.img_timestamp AS latest_ts',
|
|
];
|
|
|
|
$conds = [ 'p.page_namespace' => NS_FILE ];
|
|
|
|
$join_conds = [
|
|
'im' => [ null, 'ar.file_title=im.img_name' ],
|
|
'p' => [ 'JOIN', 'im.img_name=p.page_title' ],
|
|
];
|
|
|
|
$pagePropsConditions = "( (pp_propname = 'approvedrevs' AND pp_value = 'y') " .
|
|
"OR pp_propname = 'approvedrevs-approver-users' " .
|
|
"OR pp_propname = 'approvedrevs-approver-groups' )";
|
|
|
|
#
|
|
# mode: approvedfiles
|
|
#
|
|
if ( $mode == 'approvedfiles' ) {
|
|
|
|
$join_conds['im'][0] = 'JOIN';
|
|
|
|
// get everything from approved_revs table
|
|
|
|
#
|
|
# mode: unapprovedfiles
|
|
#
|
|
} elseif ( $mode == 'unapprovedfiles' ) {
|
|
|
|
$join_conds['im'][0] = 'RIGHT OUTER JOIN';
|
|
|
|
$tables['pp'] = 'page_props';
|
|
$join_conds['pp'] = [ 'LEFT OUTER JOIN', 'p.page_id=pp_page' ];
|
|
|
|
$approvedRevsNamespaces = self::getApprovableNamespaces();
|
|
|
|
// if all files are not approvable then need to find files matching
|
|
// __APPROVEDREVS__ and {{#approvable_by: ... }} permissions
|
|
if ( !in_array( NS_FILE, $approvedRevsNamespaces ) ) {
|
|
$conds[] = $pagePropsConditions;
|
|
}
|
|
|
|
$conds['ar.file_title'] = null;
|
|
|
|
#
|
|
# mode: invalidfiles
|
|
#
|
|
} elseif ( $mode == 'invalidfiles' ) {
|
|
|
|
$join_conds['im'][0] = 'LEFT OUTER JOIN';
|
|
|
|
$tables['pp'] = 'page_props';
|
|
$join_conds['pp'] = [ 'LEFT OUTER JOIN', 'p.page_id=pp_page' ];
|
|
|
|
$approvedRevsNamespaces = self::getApprovableNamespaces();
|
|
|
|
if ( in_array( NS_FILE, $approvedRevsNamespaces ) ) {
|
|
|
|
// if all files are approvable, no files should have invalid
|
|
// approvals. Below is an impossible condition that prevents any
|
|
// results from being returned.
|
|
$conds[] = 'p.page_namespace=1 AND p.page_namespace=2';
|
|
} else {
|
|
|
|
$conds[] = "( pp_propname IS NULL OR NOT $pagePropsConditions )";
|
|
|
|
}
|
|
|
|
#
|
|
# mode: notlatestfiles
|
|
#
|
|
} elseif ( $mode == 'notlatestfiles' ) {
|
|
|
|
$join_conds['im'][0] = 'JOIN';
|
|
|
|
// Name/Title both exist, sha1's don't match OR timestamps
|
|
// don't match
|
|
$conds[] = "(ar.approved_sha1!=im.img_sha1 OR ar.approved_timestamp!=im.img_timestamp)";
|
|
|
|
}
|
|
|
|
return [
|
|
'tables' => $tables,
|
|
'fields' => $fields,
|
|
'join_conds' => $join_conds,
|
|
'conds' => $conds,
|
|
'options' => [ 'DISTINCT' ],
|
|
];
|
|
}
|
|
|
|
}
|