Merge from branches/werdna/restrictions-separation (forked at r18959): * Branch page_...
authorAndrew Garrett <werdna@users.mediawiki.org>
Wed, 10 Jan 2007 23:32:38 +0000 (23:32 +0000)
committerAndrew Garrett <werdna@users.mediawiki.org>
Wed, 10 Jan 2007 23:32:38 +0000 (23:32 +0000)
RELEASE-NOTES
includes/Article.php
includes/DefaultSettings.php
includes/OutputPage.php
includes/ProtectionForm.php
includes/SpecialUndelete.php
includes/Title.php
languages/messages/MessagesEn.php
maintenance/tables.sql
maintenance/updaters.inc

index 456253f..ce97c34 100644 (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 ==
 
index f36a674..bf6127a 100644 (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 );
+       }
+
 }
 
 ?>
index 69d5d94..ed72d25 100644 (file)
@@ -2416,4 +2416,9 @@ $wgBreakFrames = false;
  */
 $wgDisableQueryPageUpdate = false;
 
+/**
+ * Set this to false to disable cascading protection
+ */
+$wgEnableCascadingProtection = true;
+
 ?>
index 4ca9e88..2636d8b 100644 (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;
index f96262f..53e2af3 100644 (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',
index 7c9b119..3cdda78 100644 (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" );
index 20fa71a..99cc45a 100644 (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]
index ad1ee12..ea7aeec 100644 (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',
index 188ca63..4a5cba5 100644 (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
index 7909b13..9e6433f 100644 (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;