From: Andrew Garrett Date: Wed, 10 Jan 2007 23:32:38 +0000 (+0000) Subject: Merge from branches/werdna/restrictions-separation (forked at r18959): * Branch page_... X-Git-Tag: 1.31.0-rc.0~54526 X-Git-Url: http://git.cyclocoop.org/%28?a=commitdiff_plain;h=b3a8d488a81cd1e535765f5d800be011188b18e7;p=lhc%2Fweb%2Fwiklou.git 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 --- diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 456253f8d7..ce97c3498a 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -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 == diff --git a/includes/Article.php b/includes/Article.php index f36a674a12..bf6127a030 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -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 ); + } + } ?> diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 69d5d94ff7..ed72d25899 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -2416,4 +2416,9 @@ $wgBreakFrames = false; */ $wgDisableQueryPageUpdate = false; +/** + * Set this to false to disable cascading protection + */ +$wgEnableCascadingProtection = true; + ?> diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 4ca9e88a48..2636d8bb68 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -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; diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index f96262fe6b..53e2af30ce 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -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 .= "\n"; $out .= "\n"; + global $wgEnableCascadingProtection; + + if ($wgEnableCascadingProtection) + $out .= $this->buildCascadeInput(); + if( !$this->disabled ) { $out .= "\n"; $out .= "\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', diff --git a/includes/SpecialUndelete.php b/includes/SpecialUndelete.php index 7c9b119111..3cdda7889f 100644 --- a/includes/SpecialUndelete.php +++ b/includes/SpecialUndelete.php @@ -532,8 +532,7 @@ class UndeleteForm { if( $this->mPreview ) { $wgOut->addHtml( "
\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" ); diff --git a/includes/Title.php b/includes/Title.php index 20fa71ad8f..99cc45a58a 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -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] diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index ad1ee126a4..ea7aeec08c 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -1745,6 +1745,8 @@ page protection levels. Here are the current settings for the page $1 '(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', diff --git a/maintenance/tables.sql b/maintenance/tables.sql index 188ca63e36..4a5cba51ee 100644 --- a/maintenance/tables.sql +++ b/maintenance/tables.sql @@ -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 diff --git a/maintenance/updaters.inc b/maintenance/updaters.inc index 7909b13d2c..9e6433f60d 100644 --- a/maintenance/updaters.inc +++ b/maintenance/updaters.inc @@ -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;