* API: (bug 17357) Add subpage moving to the API
authorRoan Kattouw <catrope@users.mediawiki.org>
Mon, 9 Feb 2009 14:16:51 +0000 (14:16 +0000)
committerRoan Kattouw <catrope@users.mediawiki.org>
Mon, 9 Feb 2009 14:16:51 +0000 (14:16 +0000)
* Added Title::moveSubpages() as a backend function
** Didn't update SpecialMovepage to use this; left to braver souls
* Fix handling of errors on talk page move (will backport)

RELEASE-NOTES
includes/Title.php
includes/api/ApiBase.php
includes/api/ApiMove.php

index cc1a57d..cb58432 100644 (file)
@@ -176,12 +176,13 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN
 * (bug 17326) BREAKING CHANGE: Changed output format for iiprop=metadata
 * (bug 17355) Added auwitheditsonly parameter to list=allusers
 * (bug 17007) Added action=import
-* BREAKING CHANGE: Removed rctitles parameter from list=recentchanges because of
-  performance concerns
-* Listing (semi-)deleted revisions and log entries as well in prop=revisions and
-  list=logevents
-* (bug 11430) BREAKING CHANGE: Modules may return fewer results than the limit
-  and still set a query-continue in some cases
+* BREAKING CHANGE: Removed rctitles parameter from list=recentchanges because
+  of performance concerns
+* Listing (semi-)deleted revisions and log entries as well in prop=revisions
+  and list=logevents
+* (bug 11430) BREAKING CHANGE: Modules may return fewer results than the
+  limit and still set a query-continue in some cases
+* (bug 17357) Added movesubpages parameter to action=move
 
 === Languages updated in 1.15 ===
 
index 50cd6c7..d7b57ef 100644 (file)
@@ -1613,10 +1613,36 @@ class Title {
                }
 
                $db = wfGetDB( DB_SLAVE );
-               return $this->mHasSubpages = (bool)$db->selectField( 'page', '1',
-                       "page_namespace = {$this->mNamespace} AND page_title LIKE '"
-                       . $db->escapeLike( $this->mDbkeyform ) . "/%'",
-                       __METHOD__
+               $subpages = $this->getSubpages( 1 );
+               if( $subpages instanceof TitleArray )
+                       return $this->mHasSubpages = (bool)$subpages->count();
+               return $this->mHasSubpages = false;
+       }
+       
+       /**
+        * Get all subpages of this page.
+        * @param $limit Maximum number of subpages to fetch; -1 for no limit
+        * @return mixed TitleArray, or empty array if this page's namespace
+        *  doesn't allow subpages
+        */
+       public function getSubpages($limit = -1) {
+               if( !MWNamespace::hasSubpages( $this->getNamespace() ) )
+                       return array();
+
+               $dbr = wfGetDB( DB_SLAVE );
+               $conds['page_namespace'] = $this->getNamespace();
+               $conds[] = 'page_title LIKE ' . $dbr->addQuotes(
+                               $dbr->escapeLike( $this->getDBkey() ) . '/%' );
+               $options = array();
+               if( $limit > -1 )
+                       $options['LIMIT'] = $limit;
+               return $this->mSubpages = TitleArray::newFromResult(
+                       $dbr->select( 'page',
+                               array( 'page_id', 'page_namespace', 'page_title' ),
+                               $conds,
+                               __METHOD__,
+                               $options
+                       )
                );
        }
 
@@ -2939,6 +2965,67 @@ class Title {
                
        }
        
+       /**
+        * Move this page's subpages to be subpages of $nt
+        * @param $nt Title Move target
+        * @param $auth bool Whether $wgUser's permissions should be checked
+        * @param $reason string The reason for the move
+        * @param $createRedirect bool Whether to create redirects from the old subpages to the new ones
+        *  Ignored if the user doesn't have the 'suppressredirect' right
+        * @return mixed array with old page titles as keys, and strings (new page titles) or
+        *  arrays (errors) as values, or an error array with numeric indices if no pages were moved
+        */
+       public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) {
+               global $wgUser, $wgMaximumMovedPages;
+               // Check permissions
+               if( !$this->userCan( 'move-subpages' ) )
+                       return array( 'cant-move-subpages' );
+               // Do the source and target namespaces support subpages?
+               if( !MWNamespace::hasSubpages( $this->getNamespace() ) )
+                       return array( 'namespace-nosubpages',
+                               MWNamespace::getCanonicalName( $this->getNamespace() ) );
+               if( !MWNamespace::hasSubpages( $nt->getNamespace() ) )
+                       return array( 'namespace-nosubpages',
+                               MWNamespace::getCanonicalName( $nt->getNamespace() ) );
+
+               $subpages = $this->getSubpages($wgMaximumMovedPages + 1);
+               $retval = array();
+               $count = 0;
+               foreach( $subpages as $oldSubpage ) {
+                       $count++;
+                       if( $count > $wgMaximumMovedPages ) {
+                               $retval[$oldSubpage->getPrefixedTitle()] =
+                                               array( 'movepage-max-pages',
+                                                       $wgMaximumMovedPages );
+                               break;
+                       }
+
+                       if( $oldSubpage->getArticleId() == $this->getArticleId() )
+                               // When moving a page to a subpage of itself,
+                               // don't move it twice
+                               continue;
+                       $newPageName = preg_replace(
+                                       '#^'.preg_quote( $this->getDBKey(), '#' ).'#',
+                                       $nt->getDBKey(), $oldSubpage->getDBKey() );
+                       if( $oldSubpage->isTalkPage() ) {
+                               $newNs = $nt->getTalkPage()->getNamespace();
+                       } else {
+                               $newNs = $nt->getSubjectPage()->getNamespace();
+                       }
+                       # Bug 14385: we need makeTitleSafe because the new page names may
+                       # be longer than 255 characters.
+                       $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
+
+                       $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect );
+                       if( $success === true ) {
+                               $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
+                       } else {
+                               $retval[$oldSubpage->getPrefixedText()] = $success;
+                       }
+               }
+               return $retval;
+       }
+       
        /**
         * Checks if this page is just a one-rev redirect.
         * Adds lock, so don't use just for light purposes.
index 643a741..2be4c54 100644 (file)
@@ -761,14 +761,28 @@ abstract class ApiBase {
 
        /**
         * Output the error message related to a certain array
-        * @param array $error Element of a getUserPermissionsErrors()
+        * @param array $error Element of a getUserPermissionsErrors()-style array
         */
        public function dieUsageMsg($error) {
+               $parsed = $this->parseMsg($error);
+               $this->dieUsage($parsed['code'], $parsed['info']);
+       }
+       
+       /**
+        * Return the error message related to a certain array
+        * @param array $error Element of a getUserPermissionsErrors()-style array
+        * @return array('code' => code, 'info' => info)
+        */
+       public function parseMsg($error) {
                $key = array_shift($error);
                if(isset(self::$messageMap[$key]))
-                       $this->dieUsage(wfMsgReplaceArgs(self::$messageMap[$key]['info'], $error), wfMsgReplaceArgs(self::$messageMap[$key]['code'], $error));
+                       return array(   'code' =>
+                               wfMsgReplaceArgs(self::$messageMap[$key]['code'], $error),
+                                       'info' =>
+                               wfMsgReplaceArgs(self::$messageMap[$key]['info'], $error)
+                       );
                // If the key isn't present, throw an "unknown error"
-               $this->dieUsageMsg(array('unknownerror', $key));
+               return $this->parseMsg(array('unknownerror', $key));
        }
 
        /**
index cba5f91..1045f3e 100644 (file)
@@ -73,6 +73,7 @@ class ApiMove extends ApiBase {
                        $this->dieUsageMsg(array('invalidtitle', $params['to']));
                $toTalk = $toTitle->getTalkPage();
 
+               # Move the page
                $hookErr = null;
                $retval = $fromTitle->moveTo($toTitle, true, $params['reason'], !$params['noredirect']);
                if($retval !== true)
@@ -82,10 +83,9 @@ class ApiMove extends ApiBase {
                if(!$params['noredirect'] || !$wgUser->isAllowed('suppressredirect'))
                        $r['redirectcreated'] = '';
 
+               # Move the talk page
                if($params['movetalk'] && $fromTalk->exists() && !$fromTitle->isTalkPage())
                {
-                       // We need to move the talk page as well
-                       $toTalk = $toTitle->getTalkPage();
                        $retval = $fromTalk->moveTo($toTalk, true, $params['reason'], !$params['noredirect']);
                        if($retval === true)
                        {
@@ -95,8 +95,24 @@ class ApiMove extends ApiBase {
                        // We're not gonna dieUsage() on failure, since we already changed something
                        else
                        {
-                               $r['talkmove-error-code'] = ApiBase::$messageMap[reset($retval)]['code'];
-                               $r['talkmove-error-info'] = ApiBase::$messageMap[reset($retval)]['info'];
+                               $parsed = $this->parseMsg(reset($retval));
+                               $r['talkmove-error-code'] = $parsed['code'];
+                               $r['talkmove-error-info'] = $parsed['info'];
+                       }
+               }
+
+               # Move subpages
+               if($params['movesubpages'])
+               {
+                       $r['subpages'] = $this->moveSubpages($fromTitle, $toTitle,
+                                       $params['reason'], $params['noredirect']);
+                       $this->getResult()->setIndexedTagName($r['subpages'], 'subpage');
+                       // TODO: Should we move talk subpages if moving the talk page failed?
+                       if($params['movetalk'])
+                       {
+                               $r['subpages-talk'] = $this->moveSubpages($fromTalk, $toTalk,
+                                       $params['reason'], $params['noredirect']);
+                               $this->getResult()->setIndexedTagName($r['subpages-talk'], 'subpage');
                        }
                }
 
@@ -113,6 +129,30 @@ class ApiMove extends ApiBase {
                }
                $this->getResult()->addValue(null, $this->getModuleName(), $r);
        }
+       
+       public function moveSubpages($fromTitle, $toTitle, $reason, $noredirect)
+       {
+               $retval = array();
+               $success = $fromTitle->moveSubpages($toTitle, true, $reason, !$noredirect);
+               if(isset($success[0]))
+                       return array('error' => $this->parseMsg($success));
+               else
+               {
+                       // At least some pages could be moved
+                       // Report each of them separately
+                       foreach($success as $oldTitle => $newTitle)
+                       {
+                               $r = array('from' => $oldTitle);
+                               if(is_array($newTitle))
+                                       $r['error'] = $this->parseMsg(reset($newTitle));
+                               else
+                                       // Success
+                                       $r['to'] = $newTitle;
+                               $retval[] = $r;
+                       }
+               }
+               return $retval;
+       }
 
        public function mustBePosted() { return true; }
 
@@ -126,6 +166,7 @@ class ApiMove extends ApiBase {
                        'token' => null,
                        'reason' => null,
                        'movetalk' => false,
+                       'movesubpages' => false,
                        'noredirect' => false,
                        'watch' => false,
                        'unwatch' => false
@@ -140,6 +181,7 @@ class ApiMove extends ApiBase {
                        'token' => 'A move token previously retrieved through prop=info',
                        'reason' => 'Reason for the move (optional).',
                        'movetalk' => 'Move the talk page, if it exists.',
+                       'movesubpages' => 'Move subpages, if applicable',
                        'noredirect' => 'Don\'t create a redirect',
                        'watch' => 'Add the page and the redirect to your watchlist',
                        'unwatch' => 'Remove the page and the redirect from your watchlist'