Merge from branches/werdna/restrictions-separation (forked at r18959): * Branch page_restrictions column out into its own table, also creating a 'cascading protection' feature, which automagically disallows edits to pages transcluded into a page protected with this new option. Various other code tidiness fixes and refactoring in the log messages of branches/werdna/restrictions-separation. REQUIRES DATABASE SCHEMA UPGRADE

This commit is contained in:
Andrew Garrett 2007-01-10 23:32:38 +00:00
parent 5da03354d3
commit b3a8d488a8
10 changed files with 332 additions and 44 deletions

View file

@ -39,6 +39,10 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN
starts page output (http://lists.wikimedia.org/pipermail/wikitech-l/2007-January/028554.html)
* Fix SpecialVersion->formatCredits input. Version and Url parameters should be null
to be treated properly with isset.
* Branch page_restrictions column out into its own table, also creating a "cascading protection"
feature, which automagically disallows edits to pages transcluded into a page protected with
this new option. Various other code tidiness fixes and refactoring in the log messages of
branches/werdna/restrictions-separation.
== Languages updated ==

View file

@ -252,7 +252,6 @@ class Article {
'page_id',
'page_namespace',
'page_title',
'page_restrictions',
'page_counter',
'page_is_redirect',
'page_is_new',
@ -305,8 +304,6 @@ class Article {
$lc->addGoodLinkObj( $data->page_id, $this->mTitle );
$this->mTitle->mArticleID = $data->page_id;
$this->mTitle->loadRestrictions( $data->page_restrictions );
$this->mTitle->mRestrictionsLoaded = true;
$this->mCounter = $data->page_counter;
$this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
@ -795,7 +792,7 @@ class Article {
$wgOut->addParserOutputNoText( $parseout );
} else if ( $pcache ) {
# Display content and save to parser cache
$wgOut->addPrimaryWikiText( $text, $this );
$this->outputWikiText( $text );
} else {
# Display content, don't attempt to save to parser cache
# Don't show section-edit links on old revisions... this way lies madness.
@ -803,7 +800,7 @@ class Article {
$oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
}
# Display content and don't save to parser cache
$wgOut->addPrimaryWikiText( $text, $this, false );
$this->outputWikiText( $text, false );
if( !$this->isCurrent() ) {
$wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
@ -970,7 +967,6 @@ class Article {
'page_namespace' => $this->mTitle->getNamespace(),
'page_title' => $this->mTitle->getDBkey(),
'page_counter' => 0,
'page_restrictions' => $restrictions,
'page_is_redirect' => 0, # Will set this shortly...
'page_is_new' => 1,
'page_random' => wfRandom(),
@ -1635,7 +1631,7 @@ class Article {
* @param string $reason
* @return bool true on success
*/
function updateRestrictions( $limit = array(), $reason = '' ) {
function updateRestrictions( $limit = array(), $reason = '', $cascade = 0 ) {
global $wgUser, $wgRestrictionTypes, $wgContLang;
$id = $this->mTitle->getArticleID();
@ -1653,6 +1649,7 @@ class Article {
$updated = Article::flattenRestrictions( $limit );
$changed = ( $current != $updated );
$changed = $changed || ($this->mTitle->getRestrictionCascadingFlags() != $cascade);
$protect = ( $updated != '' );
# If nothing's changed, do nothing
@ -1669,23 +1666,32 @@ class Article {
$comment .= " [$updated]";
$nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true );
$nullRevId = $nullRevision->insertOn( $dbw );
# Update restrictions table
foreach( $limit as $action => $restrictions ) {
if ($restrictions != '' ) {
$dbw->replace( 'page_restrictions', array( 'pr_pagetype'),
array( 'pr_page' => $id, 'pr_type' => $action
, 'pr_level' => $restrictions, 'pr_cascade' => $cascade ), __METHOD__ );
} else {
$dbw->delete( 'page_restrictions', array( 'pr_page' => $id,
'pr_type' => $action ), __METHOD__ );
}
}
# Update page record
$dbw->update( 'page',
array( /* SET */
'page_touched' => $dbw->timestamp(),
'page_restrictions' => $updated,
'page_latest' => $nullRevId
), array( /* WHERE */
'page_id' => $id
), 'Article::protect'
);
wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) );
# Update the protection log
$log = new LogPage( 'protect' );
$cascade_description = '';
if ($cascade) {
$cascade_description = ' ['.wfMsg('protect-summary-cascade').']';
}
if( $protect ) {
$log->addEntry( 'protect', $this->mTitle, trim( $reason . " [$updated]" ) );
$log->addEntry( 'protect', $this->mTitle, trim( $reason . " [$updated]$cascade_description" ) );
} else {
$log->addEntry( 'unprotect', $this->mTitle, $reason );
}
@ -2010,6 +2016,9 @@ class Article {
), __METHOD__
);
# Delete restrictions for it
$dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
# Now that it's safely backed up, delete it
$dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__);
@ -2785,6 +2794,67 @@ class Article {
return $summary;
}
/**
* Add the primary page-view wikitext to the output buffer
* Saves the text into the parser cache if possible.
*
* @param string $text
* @param Article $article
* @param bool $cache
*/
public function outputWikiText( $text, $cache = true ) {
global $wgParser, $wgUser, $wgOut;
$article = $this;
$popts = $wgOut->parserOptions();
$popts->setTidy(true);
$parserOutput = $wgParser->parse( $text, $article->mTitle,
$popts, true, true, $this->mRevisionId );
$popts->setTidy(false);
if ( $cache && $article && $parserOutput->getCacheTime() != -1 ) {
$parserCache =& ParserCache::singleton();
$parserCache->save( $parserOutput, $article, $wgUser );
}
if ( !wfReadOnly() ) {
# Get templates from templatelinks
$tlTemplates_titles = $this->getUsedTemplates();
$tlTemplates = array ();
foreach( $tlTemplates_titles as $template_title) {
$tlTemplates[] = $template_title->getDBkey();
}
# Get templates from parser output.
$poTemplates_allns = $parserOutput->getTemplates();
$poTemplates = array ();
foreach ( $poTemplates_allns as $ns_templates ) {
$poTemplates = array_merge( $poTemplates, $ns_templates );
}
# Get the diff
$templates_diff = array_diff( $poTemplates, $tlTemplates );
if ( count( $templates_diff ) > 0 ) {
# Whee, link updates time.
$u = new LinksUpdate( $this->mTitle, $parserOutput );
$dbw =& wfGetDb( DB_MASTER );
$dbw->begin();
$u->doUpdate();
$dbw->commit();
}
}
$wgOut->addParserOutput( $parserOutput );
}
}
?>

View file

@ -2416,4 +2416,9 @@ $wgBreakFrames = false;
*/
$wgDisableQueryPageUpdate = false;
/**
* Set this to false to disable cascading protection
*/
$wgEnableCascadingProtection = true;
?>

View file

@ -315,14 +315,26 @@ class OutputPage {
$this->addWikiTextTitle($text, $title, $linestart);
}
private function addWikiTextTitle($text, &$title, $linestart) {
function addWikiTextTitleTidy($text, &$title, $linestart = true) {
addWikiTextTitle( $text, $title, $linestart, true );
}
public function addWikiTextTitle($text, &$title, $linestart, $tidy = false) {
global $wgParser;
$fname = 'OutputPage:addWikiTextTitle';
wfProfileIn($fname);
wfIncrStats('pcache_not_possible');
$parserOutput = $wgParser->parse( $text, $title, $this->parserOptions(),
$popts = $this->parserOptions();
$popts->setTidy($tidy);
$parserOutput = $wgParser->parse( $text, $title, $popts,
$linestart, true, $this->mRevisionId );
$this->addParserOutput( $parserOutput );
wfProfileOut($fname);
}
@ -366,6 +378,7 @@ class OutputPage {
* @param string $text
* @param Article $article
* @param bool $cache
* @deprecated Use Article::outputWikitext
*/
public function addPrimaryWikiText( $text, $article, $cache = true ) {
global $wgParser, $wgUser;

View file

@ -25,6 +25,7 @@
class ProtectionForm {
var $mRestrictions = array();
var $mReason = '';
var $mCascade = false;
function ProtectionForm( &$article ) {
global $wgRequest, $wgUser;
@ -38,6 +39,7 @@ class ProtectionForm {
// but the db allows multiples separated by commas.
$this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
}
$this->mCascade = $this->mTitle->getRestrictionCascadingFlags() & 1;
}
// The form will be available in read-only to show levels.
@ -48,6 +50,7 @@ class ProtectionForm {
if( $wgRequest->wasPosted() ) {
$this->mReason = $wgRequest->getText( 'mwProtect-reason' );
$this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' );
foreach( $wgRestrictionTypes as $action ) {
$val = $wgRequest->getVal( "mwProtect-level-$action" );
if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) {
@ -101,7 +104,7 @@ class ProtectionForm {
throw new FatalError( wfMsg( 'sessionfailure' ) );
}
$ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $this->mReason );
$ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $this->mReason, $this->mCascade );
if( !$ok ) {
throw new FatalError( "Unknown error at restriction save time." );
}
@ -148,6 +151,11 @@ class ProtectionForm {
$out .= "</tbody>\n";
$out .= "</table>\n";
global $wgEnableCascadingProtection;
if ($wgEnableCascadingProtection)
$out .= $this->buildCascadeInput();
if( !$this->disabled ) {
$out .= "<table>\n";
$out .= "<tbody>\n";
@ -205,6 +213,13 @@ class ProtectionForm {
'id' => $id ) );
}
function buildCascadeInput() {
$id = 'mwProtect-cascade';
$ci = wfCheckLabel( wfMsg( 'protect-cascade' ), $id, $id, $this->mCascade, array ());
return $ci;
}
function buildSubmit() {
return wfElement( 'input', array(
'type' => 'submit',

View file

@ -532,8 +532,7 @@ class UndeleteForm {
if( $this->mPreview ) {
$wgOut->addHtml( "<hr />\n" );
$article = new Article ( $archive->title ); # OutputPage wants an Article obj
$wgOut->addPrimaryWikiText( $rev->getText(), $article, false );
$wgOut->addWikiTextTitle( $rev->getText(), $archive->title, false );
}
$self = SpecialPage::getTitleFor( "Undelete" );

View file

@ -50,7 +50,7 @@ class Title {
var $mArticleID; # Article ID, fetched from the link cache on demand
var $mLatestID; # ID of most recent revision
var $mRestrictions; # Array of groups allowed to edit this article
# Only null or "sysop" are supported
var $mCascadeRestrictionFlags;
var $mRestrictionsLoaded; # Boolean for initialisation on demand
var $mPrefixedText; # Text form including namespace/interwiki, initialised on demand
var $mDefaultNamespace; # Namespace index when there is no namespace
@ -1115,6 +1115,18 @@ class Title {
return false;
}
if ( ( $this->isCascadeProtectedPage() ) ||
($this->getNamespace() == NS_IMAGE && $this->isCascadeProtectedImage() ) ) {
# We /could/ use the protection level on the source page, but it's fairly ugly
# as we have to establish a precedence hierarchy for pages included by multiple
# cascade-protected pages. So just restrict it to people with 'protect' permission,
# as they could remove the protection anyway.
if ( !$wgUser->isAllowed('protect') ) {
wfProfileOut( $fname );
return false;
}
}
foreach( $this->getRestrictions($action) as $right ) {
// Backwards compatibility, rewrite sysop -> protect
if ( $right == 'sysop' ) {
@ -1307,34 +1319,114 @@ class Title {
return ( $wgUser->isAllowed('editinterface') or preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) );
}
/**
* Cascading protects: Check if the current image is protected due to a cascading restriction
*
* @return bool If the current page is protected due to a cascading restriction.
* @access public
*/
function isCascadeProtectedImage() {
global $wgEnableCascadingProtection;
if (!$wgEnableCascadingProtection)
return;
wfProfileIn(__METHOD__);
$dbr =& wfGetDb( DB_SLAVE );
$cols = array( 'il_to' );
$tables = array ('imagelinks', 'page_restrictions');
$where_clauses = array( 'il_to' => $this->getDBkey(), 'il_from=pr_page', 'pr_cascade' => 1 );
$res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__);
//die($dbr->numRows($res));
if ($dbr->numRows($res)) {
wfProfileOut(__METHOD__);
return true;
} else {
wfProfileOut(__METHOD__);
return false;
}
}
/**
* Cascading protects: Check if the current page is protected due to a cascading restriction.
*
* @return bool if the current page is protected due to a cascading restriction
* @access public
*/
function isCascadeProtectedPage() {
global $wgEnableCascadingProtection;
if (!$wgEnableCascadingProtection)
return;
wfProfileIn(__METHOD__);
$dbr =& wfGetDb( DB_SLAVE );
$cols = array( 'tl_namespace', 'tl_title'/*, 'pr_level', 'pr_type'*/ );
$tables = array ('templatelinks', 'page_restrictions');
$where_clauses = array( 'tl_namespace' => $this->getNamespace(), 'tl_title' => $this->getDBkey(), 'tl_from=pr_page', 'pr_cascade' => 1 );
$res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__);
if ($dbr->numRows($res)) {
wfProfileOut(__METHOD__);
return true;
} else {
wfProfileOut(__METHOD__);
return false;
}
}
function getRestrictionCascadingFlags() {
if (!$this->mRestrictionsLoaded) {
$this->loadRestrictions();
}
return $this->mCascadeRestrictionFlags;
}
/**
* Loads a string into mRestrictions array
* @param string $res restrictions in string format
* @param resource $res restrictions as an SQL result.
* @access public
*/
function loadRestrictions( $res ) {
$this->mRestrictions['edit'] = array();
$this->mRestrictions['move'] = array();
if( !$res ) {
# No restrictions (page_restrictions blank)
function loadRestrictionsFromRow( $res ) {
$dbr =& wfGetDb( DB_SLAVE );
if (!$dbr->numRows( $res ) ) {
# No restrictions
$this->mRestrictionsLoaded = true;
return;
}
foreach( explode( ':', trim( $res ) ) as $restrict ) {
$temp = explode( '=', trim( $restrict ) );
if(count($temp) == 1) {
// old format should be treated as edit/move restriction
$this->mRestrictions["edit"] = explode( ',', trim( $temp[0] ) );
$this->mRestrictions["move"] = explode( ',', trim( $temp[0] ) );
} else {
$this->mRestrictions[$temp[0]] = explode( ',', trim( $temp[1] ) );
}
$this->mRestrictions['edit'] = array();
$this->mRestrictions['move'] = array();
while ($row = $dbr->fetchObject( $res ) ) {
# Cycle through all the restrictions.
$this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
$this->mCascadeRestrictionFlags |= $row->pr_cascade;
}
$this->mRestrictionsLoaded = true;
}
function loadRestrictions() {
if( !$this->mRestrictionsLoaded ) {
$dbr =& wfGetDB( DB_SLAVE );
$res = $dbr->select( 'page_restrictions', '*',
array ( 'pr_page' => $this->getArticleId() ), __METHOD__ );
$this->loadRestrictionsFromRow( $res );
}
}
/**
* Accessor/initialisation for mRestrictions
*
@ -1345,9 +1437,7 @@ class Title {
function getRestrictions( $action ) {
if( $this->exists() ) {
if( !$this->mRestrictionsLoaded ) {
$dbr =& wfGetDB( DB_SLAVE );
$res = $dbr->selectField( 'page', 'page_restrictions', array( 'page_id' => $this->getArticleId() ) );
$this->loadRestrictions( $res );
$this->loadRestrictions();
}
return isset( $this->mRestrictions[$action] )
? $this->mRestrictions[$action]

View file

@ -1745,6 +1745,8 @@ page protection levels. Here are the current settings for the page <strong>$1</s
'protect-default' => '(default)',
'protect-level-autoconfirmed' => 'Block unregistered users',
'protect-level-sysop' => 'Sysops only',
'protect-summary-cascade' => 'cascading',
'protect-cascade' => 'Cascading protection - protect any pages transcluded in this page.',
# restrictions (nouns)
'restriction-edit' => 'Edit',

View file

@ -1075,4 +1075,27 @@ CREATE TABLE /*$wgDBprefix*/querycachetwo (
) TYPE=InnoDB;
--- Used for storing page restrictions (i.e. protection levels)
CREATE TABLE /*$wgDBprefix*/page_restrictions (
-- Page to apply restrictions to (Foreign Key to page).
pr_page int(8) NOT NULL,
-- The protection type (edit, move, etc)
pr_type varchar(255) NOT NULL,
-- The protection level (Sysop, autoconfirmed, etc)
pr_level varchar(255) NOT NULL,
-- Whether or not to cascade the protection down to pages transcluded.
pr_cascade tinyint(4) NOT NULL,
-- Field for future support of per-user restriction.
pr_user int(8) NULL,
-- Field for future support of time-limited protection.
pr_expiry char(14) binary NULL,
PRIMARY KEY (pr_page,pr_type),
KEY pr_page (pr_page),
KEY pr_typelevel (pr_type,pr_level),
KEY pr_level (pr_level),
KEY pr_cascade (pr_cascade)
) TYPE=InnoDB;
-- vim: sw=2 sts=2 et

View file

@ -37,6 +37,7 @@ $wgNewTables = array(
array( 'filearchive', 'patch-filearchive.sql' ),
array( 'redirect', 'patch-redirect.sql' ),
array( 'querycachetwo', 'patch-querycachetwo.sql' ),
# array( 'page_restrictions', 'patch-page_restrictions.sql' ),
);
$wgNewFields = array(
@ -905,6 +906,8 @@ function do_all_updates( $shared = false, $purge = true ) {
do_backlinking_indices_update(); flush();
do_restrictions_update(); flush ();
echo "Deleting old default messages..."; flush();
deleteDefaultMessages();
echo "Done\n"; flush();
@ -925,6 +928,70 @@ function archive($name) {
}
}
function do_restrictions_update() {
# Adding page_restrictions table, obsoleting page.page_restrictions.
# Migrating old restrictions to new table
# -- Andrew Garrett, January 2007.
global $wgDatabase;
$name = 'page_restrictions';
$patch = 'patch-page_restrictions.sql';
if ( $wgDatabase->tableExists( $name ) ) {
echo "...$name table already exists.\n";
} else {
echo "Creating $name table...";
dbsource( archive($patch), $wgDatabase );
echo "ok\n";
echo "Migrating old restrictions to new table...";
$res = $wgDatabase->select( 'page', array( 'page_id', 'page_restrictions' ), array("page_restrictions!=''", "page_restrictions!='edit=:move='"), __METHOD__ );
$count = 0;
while ($row = $wgDatabase->fetchObject($res) ) {
$count = ($count + 1) % 100;
if ($count == 0) {
if ( function_exists( 'wfWaitForSlaves' ) ) {
wfWaitForSlaves( 10 );
} else {
sleep( 1 );
}
}
# Figure out what the restrictions are..
$id = $row->page_id;
$flatterrestrictions = $row->page_restrictions;
$flatrestrictions = explode( ':', $flatterrestrictions );
$restrictions = array ();
foreach( $flatrestrictions as $restriction ) {
$thisrestriction = explode('=', $restriction);
$restriction_type = $thisrestriction[0];
$restriction_level = $thisrestriction[1];
$restrictions[$restriction_type] = $restriction_level;
if ($restriction_level != '') {
$wgDatabase->insert( 'page_restrictions', array ( 'pr_page' => $id,
'pr_type' => $restriction_type,
'pr_level' => $restriction_level,
'pr_cascade' => 0 ), __METHOD );
}
}
}
print "ok\n";
}
}
function do_postgres_updates() {
global $wgDatabase, $wgVersion, $wgDBmwschema;