* Use local context to get messages
[lhc/web/wiklou.git] / includes / Title.php
index 6b4aad9..769adb9 100644 (file)
@@ -63,7 +63,7 @@ class Title {
        var $mFragment;                   // /< Title fragment (i.e. the bit after the #)
        var $mArticleID = -1;             // /< Article ID, fetched from the link cache on demand
        var $mLatestID = false;           // /< ID of most recent revision
-       var $mCounter = -1;               // /< Number of times this page has been viewed (-1 means "not loaded")
+       private $mEstimateRevisions;      // /< Estimated number of revisions; null of not loaded
        var $mRestrictions = array();     // /< Array of groups allowed to edit this article
        var $mOldRestrictions = false;
        var $mCascadeRestriction;         ///< Cascade restrictions on this page to included templates and images?
@@ -260,8 +260,7 @@ class Title {
         * Load Title object fields from a DB row.
         * If false is given, the title will be treated as non-existing.
         *
-        * @param $row Object|false database row
-        * @return void
+        * @param $row Object|bool database row
         */
        public function loadFromRow( $row ) {
                if ( $row ) { // page found
@@ -273,14 +272,11 @@ class Title {
                                $this->mRedirect = (bool)$row->page_is_redirect;
                        if ( isset( $row->page_latest ) )
                                $this->mLatestID = (int)$row->page_latest;
-                       if ( isset( $row->page_counter ) )
-                               $this->mCounter = (int)$row->page_counter;
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
-                       $this->mCounter = 0;
                }
        }
 
@@ -764,7 +760,7 @@ class Title {
         * @internal note -- uses hardcoded namespace index instead of constants
         */
        public function canExist() {
-               return $this->mNamespace >= 0 && $this->mNamespace != NS_MEDIA;
+               return $this->mNamespace >= NS_MAIN;
        }
 
        /**
@@ -866,6 +862,7 @@ class Title {
         * This is MUCH simpler than individually testing for equivilance
         * against both NS_USER and NS_USER_TALK, and is also forward compatible.
         * @since 1.19
+        * @return bool
         */
        public function hasSubjectNamespace( $ns ) {
                return MWNamespace::subjectEquals( $this->getNamespace(), $ns );
@@ -966,16 +963,6 @@ class Title {
                return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
        }
 
-       /**
-        * Is this a *valid* .css or .js subpage of a user page?
-        *
-        * @return Bool
-        * @deprecated since 1.17
-        */
-       public function isValidCssJsSubpage() {
-               return $this->isCssJsSubpage();
-       }
-
        /**
         * Trim down a .css or .js subpage title to get the corresponding skin name
         *
@@ -1232,8 +1219,19 @@ class Title {
 
        /**
         * Helper to fix up the get{Local,Full,Link,Canonical}URL args
+        * get{Canonical,Full,Link,Local}URL methods accepted an optional
+        * second argument named variant. This was deprecated in favor
+        * of passing an array of option with a "variant" key
+        * Once $query2 is removed for good, this helper can be dropped
+        * andthe wfArrayToCGI moved to getLocalURL();
+        *
+        * @since 1.19 (r105919)
+        * @return String
         */
-       private static function fixUrlQueryArgs( $query, $query2 ) {
+       private static function fixUrlQueryArgs( $query, $query2 = false ) {
+               if( $query2 !== false ) {
+                       wfDeprecated( "Title::get{Canonical,Full,Link,Local} method called with a second parameter is deprecated. Add your parameter to an array passed as the first parameter.", "1.19" );
+               }
                if ( is_array( $query ) ) {
                        $query = wfArrayToCGI( $query );
                }
@@ -1259,10 +1257,9 @@ class Title {
         * Get a real URL referring to this title, with interwiki link and
         * fragment
         *
-        * @param $query \twotypes{\string,\array} an optional query string, not used for interwiki
-        *   links. Can be specified as an associative array as well, e.g.,
-        *   array( 'action' => 'edit' ) (keys and values will be URL-escaped).
-        * @param $variant String language variant of url (for sr, zh..)
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
         * @return String the URL
         */
        public function getFullURL( $query = '', $query2 = false ) {
@@ -1287,11 +1284,18 @@ class Title {
         * Get a URL with no fragment or server name.  If this page is generated
         * with action=render, $wgServer is prepended.
         *
-        * @param $query Mixed: an optional query string; if not specified,
-        *   $wgArticlePath will be used.  Can be specified as an associative array
-        *   as well, e.g., array( 'action' => 'edit' ) (keys and values will be
-        *   URL-escaped).
-        * @param $variant String language variant of url (for sr, zh..)
+
+        * @param $query string|array an optional query string,
+        *   not used for interwiki     links. Can be specified as an associative array as well,
+        *   e.g., array( 'action' => 'edit' ) (keys and values will be URL-escaped).
+        *   Some query patterns will trigger various shorturl path replacements.
+        * @param $query2 Mixed: An optional secondary query array. This one MUST
+        *   be an array. If a string is passed it will be interpreted as a deprecated
+        *   variant argument and urlencoded into a variant= argument.
+        *   This second query argument will be added to the $query
+        *   The second parameter is deprecated since 1.19. Pass it as a key,value
+        *   pair in the first parameter array instead.
+        *
         * @return String the URL
         */
        public function getLocalURL( $query = '', $query2 = false ) {
@@ -1379,11 +1383,9 @@ class Title {
         * The result obviously should not be URL-escaped, but does need to be
         * HTML-escaped if it's being output in HTML.
         *
-        * @param $query Array of Strings An associative array of key => value pairs for the
-        *   query string.  Keys and values will be escaped.
-        * @param $variant String language variant of URL (for sr, zh..).  Ignored
-        *   for external links.  Default is "false" (same variant as current page,
-        *   for anonymous users).
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
         * @return String the URL
         */
        public function getLinkURL( $query = '', $query2 = false ) {
@@ -1403,7 +1405,9 @@ class Title {
         * Get an HTML-escaped version of the URL form, suitable for
         * using in a link, without a server name or fragment
         *
-        * @param $query String an optional query string
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
         * @return String the URL
         */
        public function escapeLocalURL( $query = '', $query2 = false ) {
@@ -1415,7 +1419,9 @@ class Title {
         * Get an HTML-escaped version of the URL form, suitable for
         * using in a link, including the server name and fragment
         *
-        * @param $query String an optional query string
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
         * @return String the URL
         */
        public function escapeFullURL( $query = '', $query2 = false ) {
@@ -1432,8 +1438,9 @@ class Title {
         * if $wgInternalServer is not set. If the server variable used is
         * protocol-relative, the URL will be expanded to http://
         *
-        * @param $query String an optional query string
-        * @param $variant String language variant of url (for sr, zh..)
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
         * @return String the URL
         */
        public function getInternalURL( $query = '', $query2 = false ) {
@@ -1452,21 +1459,27 @@ class Title {
         *
         * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment
         *
-        * @param $query string An optional query string
-        * @param $variant string Language variant of URL (for sr, zh, ...)
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
         * @return string The URL
         * @since 1.18
         */
        public function getCanonicalURL( $query = '', $query2 = false ) {
                $query = self::fixUrlQueryArgs( $query, $query2 );
-               $url = wfExpandUrl( $this->getLocalURL( $query, $variant ) . $this->getFragmentForURL(), PROTO_CANONICAL );
+               $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
                wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query ) );
                return $url;
        }
 
        /**
         * HTML-escaped version of getCanonicalURL()
+        *
+        * See getLocalURL for the arguments.
+        *
+        * @see self::getLocalURL
         * @since 1.18
+        * @return string
         */
        public function escapeCanonicalURL( $query = '', $query2 = false ) {
                wfDeprecated( __METHOD__, '1.19' );
@@ -1514,6 +1527,7 @@ class Title {
         * @todo fold these checks into userCan()
         */
        public function userCanRead() {
+               wfDeprecated( __METHOD__, '1.19' );
                return $this->userCan( 'read' );
        }
 
@@ -1841,6 +1855,8 @@ class Title {
         * @return Array list of errors
         */
        private function checkActionPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
+               global $wgDeleteRevisionsLimit, $wgLang;
+
                if ( $action == 'protect' ) {
                        if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $doExpensiveQueries, true ) ) ) {
                                // If they can't edit, they shouldn't protect.
@@ -1873,6 +1889,12 @@ class Title {
                        } elseif ( !$this->isMovable() ) {
                                $errors[] = array( 'immobile-target-page' );
                        }
+               } elseif ( $action == 'delete' ) {
+                       if ( $doExpensiveQueries && $wgDeleteRevisionsLimit
+                               && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion() )
+                       {
+                               $errors[] = array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) );
+                       }
                }
                return $errors;
        }
@@ -1905,7 +1927,7 @@ class Title {
                        // Don't block the user from editing their own talk page unless they've been
                        // explicitly blocked from that too.
                } elseif( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) {
-                       $block = $user->mBlock;
+                       $block = $user->getBlock();
 
                        // This is from OutputPage::blockedPage
                        // Copied at r23888 by werdna
@@ -1925,15 +1947,15 @@ class Title {
 
                        $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]";
                        $blockid = $block->getId();
-                       $blockExpiry = $user->mBlock->mExpiry;
-                       $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $user->mBlock->mTimestamp ), true );
+                       $blockExpiry = $block->getExpiry();
+                       $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $block->mTimestamp ), true );
                        if ( $blockExpiry == 'infinity' ) {
                                $blockExpiry = wfMessage( 'infiniteblock' )->text();
                        } else {
                                $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true );
                        }
 
-                       $intended = strval( $user->mBlock->getTarget() );
+                       $intended = strval( $block->getTarget() );
 
                        $errors[] = array( ( $block->mAuto ? 'autoblockedtext' : 'blockedtext' ), $link, $reason, $ip, $name,
                                $blockid, $blockExpiry, $intended, $blockTimestamp );
@@ -1954,11 +1976,11 @@ class Title {
         * @return Array list of errors
         */
        private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
+               global $wgWhitelistRead, $wgGroupPermissions, $wgRevokePermissions;
                static $useShortcut = null;
 
                # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below
                if ( is_null( $useShortcut ) ) {
-                       global $wgGroupPermissions, $wgRevokePermissions;
                        $useShortcut = true;
                        if ( empty( $wgGroupPermissions['*']['read'] ) ) {
                                # Not a public wiki, so no shortcut
@@ -1980,61 +2002,56 @@ class Title {
                        }
                }
 
-               # Shortcut for public wikis, allows skipping quite a bit of code
+               $whitelisted = false;
                if ( $useShortcut ) {
-                       return $errors;
-               }
-
-               # If the user is allowed to read pages, he is allowed to read all pages
-               if ( $user->isAllowed( 'read' ) ) {
-                       return $errors;
-               }
-
-               # Always grant access to the login page.
-               # Even anons need to be able to log in.
-               if ( $this->isSpecial( 'Userlogin' )
+                       # Shortcut for public wikis, allows skipping quite a bit of code
+                       $whitelisted = true;
+               } elseif ( $user->isAllowed( 'read' ) ) {
+                       # If the user is allowed to read pages, he is allowed to read all pages
+                       $whitelisted = true;
+               } elseif ( $this->isSpecial( 'Userlogin' )
                        || $this->isSpecial( 'ChangePassword' )
                        || $this->isSpecial( 'PasswordReset' )
                ) {
-                       return $errors;
-               }
-
-               # Time to check the whitelist
-               global $wgWhitelistRead;
-
-               # Only to these checks is there's something to check against
-               if ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
-                       # Check for explicit whitelisting
+                       # Always grant access to the login page.
+                       # Even anons need to be able to log in.
+                       $whitelisted = true;
+               } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
+                       # Time to check the whitelist
+                       # Only do these checks is there's something to check against
                        $name = $this->getPrefixedText();
                        $dbName = $this->getPrefixedDBKey();
 
-                       // Check with and without underscores
+                       // Check for explicit whitelisting with and without underscores
                        if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
-                               return $errors;
-                       }
-
-                       # Old settings might have the title prefixed with
-                       # a colon for main-namespace pages
-                       if ( $this->getNamespace() == NS_MAIN ) {
+                               $whitelisted = true;
+                       } elseif ( $this->getNamespace() == NS_MAIN ) {
+                               # Old settings might have the title prefixed with
+                               # a colon for main-namespace pages
                                if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
-                                       return $errors;
+                                       $whitelisted = true;
                                }
-                       }
-
-                       # If it's a special page, ditch the subpage bit and check again
-                       if ( $this->isSpecialPage() ) {
+                       } elseif ( $this->isSpecialPage() ) {
+                               # If it's a special page, ditch the subpage bit and check again
                                $name = $this->getDBkey();
                                list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name );
                                if ( $name !== false ) {
                                        $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
                                        if ( in_array( $pure, $wgWhitelistRead, true ) ) {
-                                               return $errors;
+                                               $whitelisted = true;
                                        }
                                }
                        }
                }
 
-               $errors[] = $this->missingPermissionError( $action, $short );
+               if ( !$whitelisted ) {
+                       # If the title is not whitelisted, give extensions a chance to do so...
+                       wfRunHooks( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) );
+                       if ( !$whitelisted ) {
+                               $errors[] = $this->missingPermissionError( $action, $short );
+                       }
+               }
+
                return $errors;
        }
 
@@ -2120,7 +2137,7 @@ class Title {
         */
        public function userCanEditCssSubpage() {
                global $wgUser;
-               wfDeprecated( __METHOD__ );
+               wfDeprecated( __METHOD__, '1.19' );
                return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) )
                        || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
        }
@@ -2134,7 +2151,7 @@ class Title {
         */
        public function userCanEditJsSubpage() {
                global $wgUser;
-               wfDeprecated( __METHOD__ );
+               wfDeprecated( __METHOD__, '1.19' );
                return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) )
                           || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
        }
@@ -2217,66 +2234,24 @@ class Title {
        /**
         * Update the title protection status
         *
+        * @deprecated in 1.19; will be removed in 1.20. Use WikiPage::doUpdateRestrictions() instead.
         * @param $create_perm String Permission required for creation
         * @param $reason String Reason for protection
         * @param $expiry String Expiry timestamp
         * @return boolean true
         */
        public function updateTitleProtection( $create_perm, $reason, $expiry ) {
-               global $wgUser, $wgContLang;
-
-               if ( $create_perm == implode( ',', $this->getRestrictions( 'create' ) )
-                       && $expiry == $this->mRestrictionsExpiry['create'] ) {
-                       // No change
-                       return true;
-               }
-
-               list ( $namespace, $title ) = array( $this->getNamespace(), $this->getDBkey() );
-
-               $dbw = wfGetDB( DB_MASTER );
-
-               $encodedExpiry = $dbw->encodeExpiry( $expiry );
+               wfDeprecated( __METHOD__, '1.19' );
 
-               $expiry_description = '';
-               if ( $encodedExpiry != $dbw->getInfinity() ) {
-                       $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ),
-                               $wgContLang->date( $expiry ) , $wgContLang->time( $expiry ) ) . ')';
-               } else {
-                       $expiry_description .= ' (' . wfMsgForContent( 'protect-expiry-indefinite' ) . ')';
-               }
-
-               # Update protection table
-               if ( $create_perm != '' ) {
-                       $this->mTitleProtection = array(
-                                       'pt_namespace' => $namespace,
-                                       'pt_title' => $title,
-                                       'pt_create_perm' => $create_perm,
-                                       'pt_timestamp' => $dbw->encodeExpiry( wfTimestampNow() ),
-                                       'pt_expiry' => $encodedExpiry,
-                                       'pt_user' => $wgUser->getId(),
-                                       'pt_reason' => $reason,
-                               );
-                       $dbw->replace( 'protected_titles', array( array( 'pt_namespace', 'pt_title' ) ),
-                               $this->mTitleProtection, __METHOD__     );
-               } else {
-                       $dbw->delete( 'protected_titles', array( 'pt_namespace' => $namespace,
-                               'pt_title' => $title ), __METHOD__ );
-                       $this->mTitleProtection = false;
-               }
+               global $wgUser;
 
-               # Update the protection log
-               if ( $dbw->affectedRows() ) {
-                       $log = new LogPage( 'protect' );
+               $limit = array( 'create' => $create_perm );
+               $expiry = array( 'create' => $expiry );
 
-                       if ( $create_perm ) {
-                               $params = array( "[create=$create_perm] $expiry_description", '' );
-                               $log->addEntry( ( isset( $this->mRestrictions['create'] ) && $this->mRestrictions['create'] ) ? 'modify' : 'protect', $this, trim( $reason ), $params );
-                       } else {
-                               $log->addEntry( 'unprotect', $this, $reason );
-                       }
-               }
+               $page = WikiPage::factory( $this );
+               $status = $page->doUpdateRestrictions( $limit, $expiry, false, $reason, $wgUser );
 
-               return true;
+               return $status->isOK();
        }
 
        /**
@@ -2563,7 +2538,7 @@ class Title {
 
                if ( $oldFashionedRestrictions === null ) {
                        $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions',
-                               array( 'page_id' => $this->getArticleId() ), __METHOD__ );
+                               array( 'page_id' => $this->getArticleID() ), __METHOD__ );
                }
 
                if ( $oldFashionedRestrictions != '' ) {
@@ -2634,7 +2609,7 @@ class Title {
                                $res = $dbr->select(
                                        'page_restrictions',
                                        '*',
-                                       array( 'pr_page' => $this->getArticleId() ),
+                                       array( 'pr_page' => $this->getArticleID() ),
                                        __METHOD__
                                );
 
@@ -2662,6 +2637,15 @@ class Title {
                }
        }
 
+       /**
+        * Flush the protection cache in this object and force reload from the database.
+        * This is used when updating protection from WikiPage::doUpdateRestrictions().
+        */
+       public function flushRestrictions() {
+               $this->mRestrictionsLoaded = false;
+               $this->mTitleProtection = null;
+       }
+
        /**
         * Purge expired restrictions from the page_restrictions table
         */
@@ -2783,28 +2767,6 @@ class Title {
                return $deleted;
        }
 
-       /**
-        * Get the number of views of this page
-        *
-        * @return int The view count for the page
-        */
-       public function getCount() {
-               if ( $this->mCounter == -1 ) {
-                       if ( $this->exists() ) {
-                               $dbr = wfGetDB( DB_SLAVE );
-                               $this->mCounter = $dbr->selectField( 'page',
-                                       'page_counter',
-                                       array( 'page_id' => $this->getArticleID() ),
-                                       __METHOD__
-                               );
-                       } else {
-                               $this->mCounter = 0;
-                       }
-               }
-
-               return $this->mCounter;
-       }
-
        /**
         * Get the article ID for this Title from the link cache,
         * adding it if necessary
@@ -2897,9 +2859,9 @@ class Title {
         * This clears some fields in this object, and clears any associated
         * keys in the "bad links" section of the link cache.
         *
-        * - This is called from Article::doEdit() and Article::insertOn() to allow
+        * - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow
         * loading of the new page_id. It's also called from
-        * Article::doDeleteArticle()
+        * WikiPage::doDeleteArticle()
         *
         * @param $newid Int the new Article ID
         */
@@ -2917,7 +2879,7 @@ class Title {
                $this->mRedirect = null;
                $this->mLength = -1;
                $this->mLatestID = false;
-               $this->mCounter = -1;
+               $this->mEstimateRevisions = null;
        }
 
        /**
@@ -3147,8 +3109,6 @@ class Title {
         * @return Array of Title objects linking here
         */
        public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
-               $linkCache = LinkCache::singleton();
-
                if ( count( $options ) > 0 ) {
                        $db = wfGetDB( DB_MASTER );
                } else {
@@ -3167,7 +3127,8 @@ class Title {
                );
 
                $retVal = array();
-               if ( $db->numRows( $res ) ) {
+               if ( $res->numRows() ) {
+                       $linkCache = LinkCache::singleton();
                        foreach ( $res as $row ) {
                                $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title );
                                if ( $titleObj ) {
@@ -3193,6 +3154,76 @@ class Title {
                return $this->getLinksTo( $options, 'templatelinks', 'tl' );
        }
 
+       /**
+        * Get an array of Title objects linked from this Title
+        * Also stores the IDs in the link cache.
+        *
+        * WARNING: do not use this function on arbitrary user-supplied titles!
+        * On heavily-used templates it will max out the memory.
+        *
+        * @param $options Array: may be FOR UPDATE
+        * @param $table String: table name
+        * @param $prefix String: fields prefix
+        * @return Array of Title objects linking here
+        */
+       public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
+               $id = $this->getArticleID();
+
+               # If the page doesn't exist; there can't be any link from this page
+               if ( !$id ) {
+                       return array();
+               }
+
+               if ( count( $options ) > 0 ) {
+                       $db = wfGetDB( DB_MASTER );
+               } else {
+                       $db = wfGetDB( DB_SLAVE );
+               }
+
+               $namespaceFiled = "{$prefix}_namespace";
+               $titleField = "{$prefix}_title";
+
+               $res = $db->select(
+                       array( $table, 'page' ),
+                       array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+                       array( "{$prefix}_from" => $id ),
+                       __METHOD__,
+                       $options,
+                       array( 'page' => array( 'LEFT JOIN', array( "page_namespace=$namespaceFiled", "page_title=$titleField" ) ) )
+               );
+
+               $retVal = array();
+               if ( $res->numRows() ) {
+                       $linkCache = LinkCache::singleton();
+                       foreach ( $res as $row ) {
+                               $titleObj = Title::makeTitle( $row->$namespaceFiled, $row->$titleField );
+                               if ( $titleObj ) {
+                                       if ( $row->page_id ) {
+                                               $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
+                                       } else {
+                                               $linkCache->addBadLinkObj( $titleObj );
+                                       }
+                                       $retVal[] = $titleObj;
+                               }
+                       }
+               }
+               return $retVal;
+       }
+
+       /**
+        * Get an array of Title objects used on this Title as a template
+        * Also stores the IDs in the link cache.
+        *
+        * WARNING: do not use this function on arbitrary user-supplied titles!
+        * On heavily-used templates it will max out the memory.
+        *
+        * @param $options Array: may be FOR UPDATE
+        * @return Array of Title the Title objects used here
+        */
+       public function getTemplateLinksFrom( $options = array() ) {
+               return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
+       }
+
        /**
         * Get an array of Title objects referring to non-existent articles linked from this page
         *
@@ -3200,7 +3231,7 @@ class Title {
         * @return Array of Title the Title objects
         */
        public function getBrokenLinksFrom() {
-               if ( $this->getArticleId() == 0 ) {
+               if ( $this->getArticleID() == 0 ) {
                        # All links from article ID 0 are false positives
                        return array();
                }
@@ -3210,7 +3241,7 @@ class Title {
                        array( 'page', 'pagelinks' ),
                        array( 'pl_namespace', 'pl_title' ),
                        array(
-                               'pl_from' => $this->getArticleId(),
+                               'pl_from' => $this->getArticleID(),
                                'page_namespace IS NULL'
                        ),
                        __METHOD__, array(),
@@ -3440,26 +3471,23 @@ class Title {
                                        return $status->getErrorsArray();
                                }
                        }
+                       // Clear RepoGroup process cache
+                       RepoGroup::singleton()->clearCache( $this );
+                       RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache
                }
-               // Clear RepoGroup process cache
-               RepoGroup::singleton()->clearCache( $this );
-               RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache
 
-               $dbw->begin(); # If $file was a LocalFile, its transaction would have closed our own.
+               $dbw->begin( __METHOD__ ); # If $file was a LocalFile, its transaction would have closed our own.
                $pageid = $this->getArticleID( self::GAID_FOR_UPDATE );
                $protected = $this->isProtected();
-               $pageCountChange = ( $createRedirect ? 1 : 0 ) - ( $nt->exists() ? 1 : 0 );
 
                // Do the actual move
                $err = $this->moveToInternal( $nt, $reason, $createRedirect );
                if ( is_array( $err ) ) {
                        # @todo FIXME: What about the File we have already moved?
-                       $dbw->rollback();
+                       $dbw->rollback( __METHOD__ );
                        return $err;
                }
 
-               $redirid = $this->getArticleID();
-
                // Refresh the sortkey for this row.  Be careful to avoid resetting
                // cl_timestamp, which may disturb time-based lists on some sites.
                $prefixes = $dbw->select(
@@ -3483,6 +3511,8 @@ class Title {
                        );
                }
 
+               $redirid = $this->getArticleID();
+
                if ( $protected ) {
                        # Protect the redirect title as the title used to be...
                        $dbw->insertSelect( 'page_restrictions', 'page_restrictions',
@@ -3518,49 +3548,7 @@ class Title {
                        WatchedItem::duplicateEntries( $this, $nt );
                }
 
-               # Update search engine
-               $u = new SearchUpdate( $pageid, $nt->getPrefixedDBkey() );
-               $u->doUpdate();
-               $u = new SearchUpdate( $redirid, $this->getPrefixedDBkey(), '' );
-               $u->doUpdate();
-
-               $dbw->commit();
-
-               # Update site_stats
-               if ( $this->isContentPage() && !$nt->isContentPage() ) {
-                       # No longer a content page
-                       # Not viewed, edited, removing
-                       $u = new SiteStatsUpdate( 0, 1, -1, $pageCountChange );
-               } elseif ( !$this->isContentPage() && $nt->isContentPage() ) {
-                       # Now a content page
-                       # Not viewed, edited, adding
-                       $u = new SiteStatsUpdate( 0, 1, + 1, $pageCountChange );
-               } elseif ( $pageCountChange ) {
-                       # Redirect added
-                       $u = new SiteStatsUpdate( 0, 0, 0, 1 );
-               } else {
-                       # Nothing special
-                       $u = false;
-               }
-               if ( $u ) {
-                       $u->doUpdate();
-               }
-
-               # Update message cache for interface messages
-               if ( $this->getNamespace() == NS_MEDIAWIKI ) {
-                       # @bug 17860: old article can be deleted, if this the case,
-                       # delete it from message cache
-                       if ( $this->getArticleID() === 0 ) {
-                               MessageCache::singleton()->replace( $this->getDBkey(), false );
-                       } else {
-                               $rev = Revision::newFromTitle( $this );
-                               MessageCache::singleton()->replace( $this->getDBkey(), $rev->getText() );
-                       }
-               }
-               if ( $nt->getNamespace() == NS_MEDIAWIKI ) {
-                       $rev = Revision::newFromTitle( $nt );
-                       MessageCache::singleton()->replace( $nt->getDBkey(), $rev->getText() );
-               }
+               $dbw->commit( __METHOD__ );
 
                wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) );
                return true;
@@ -3607,40 +3595,21 @@ class Title {
                $comment = $wgContLang->truncate( $comment, 255 );
 
                $oldid = $this->getArticleID();
-               $latest = $this->getLatestRevID();
 
                $dbw = wfGetDB( DB_MASTER );
 
-               if ( $moveOverRedirect ) {
-                       $rcts = $dbw->timestamp( $nt->getEarliestRevTime() );
+               $newpage = WikiPage::factory( $nt );
 
+               if ( $moveOverRedirect ) {
                        $newid = $nt->getArticleID();
-                       $newns = $nt->getNamespace();
-                       $newdbk = $nt->getDBkey();
 
                        # Delete the old redirect. We don't save it to history since
                        # by definition if we've got here it's rather uninteresting.
                        # We have to remove it so that the next step doesn't trigger
                        # a conflict on the unique namespace+title index...
                        $dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__ );
-                       if ( !$dbw->cascadingDeletes() ) {
-                               $dbw->delete( 'revision', array( 'rev_page' => $newid ), __METHOD__ );
-
-                               $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'imagelinks', array( 'il_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'categorylinks', array( 'cl_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'templatelinks', array( 'tl_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'externallinks', array( 'el_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'langlinks', array( 'll_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'iwlinks', array( 'iwl_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'redirect', array( 'rd_from' => $newid ), __METHOD__ );
-                               $dbw->delete( 'page_props', array( 'pp_page' => $newid ), __METHOD__ );
-                       }
-                       // If the target page was recently created, it may have an entry in recentchanges still
-                       $dbw->delete( 'recentchanges',
-                               array( 'rc_timestamp' => $rcts, 'rc_namespace' => $newns, 'rc_title' => $newdbk, 'rc_new' => 1 ),
-                               __METHOD__
-                       );
+
+                       $newpage->doDeleteUpdates( $newid );
                }
 
                # Save a null revision in the page's history notifying of the move
@@ -3650,27 +3619,34 @@ class Title {
                }
                $nullRevId = $nullRevision->insertOn( $dbw );
 
-               $now = wfTimestampNow();
                # Change the name of the target page:
                $dbw->update( 'page',
                        /* SET */ array(
-                               'page_touched'   => $dbw->timestamp( $now ),
                                'page_namespace' => $nt->getNamespace(),
                                'page_title'     => $nt->getDBkey(),
-                               'page_latest'    => $nullRevId,
                        ),
                        /* WHERE */ array( 'page_id' => $oldid ),
                        __METHOD__
                );
+
+               $this->resetArticleID( 0 );
                $nt->resetArticleID( $oldid );
 
-               $article = WikiPage::factory( $nt );
+               $newpage->updateRevisionOn( $dbw, $nullRevision );
+
                wfRunHooks( 'NewRevisionFromEditComplete',
-                       array( $article, $nullRevision, $latest, $wgUser ) );
-               $article->setCachedLastEditTime( $now );
+                       array( $newpage, $nullRevision, $nullRevision->getParentId(), $wgUser ) );
+
+               $newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) );
+
+               if ( !$moveOverRedirect ) {
+                       WikiPage::onArticleCreate( $nt );
+               }
 
                # Recreate the redirect, this time in the other direction.
-               if ( $createRedirect || !$wgUser->isAllowed( 'suppressredirect' ) ) {
+               if ( $redirectSuppressed ) {
+                       WikiPage::onArticleDelete( $this );
+               } else {
                        $mwRedir = MagicWord::get( 'redirect' );
                        $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
                        $redirectArticle = WikiPage::factory( $this );
@@ -3686,33 +3662,13 @@ class Title {
                                wfRunHooks( 'NewRevisionFromEditComplete',
                                        array( $redirectArticle, $redirectRevision, false, $wgUser ) );
 
-                               # Now, we record the link from the redirect to the new title.
-                               # It should have no other outgoing links...
-                               $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ );
-                               $dbw->insert( 'pagelinks',
-                                       array(
-                                               'pl_from'      => $newid,
-                                               'pl_namespace' => $nt->getNamespace(),
-                                               'pl_title'     => $nt->getDBkey() ),
-                                       __METHOD__ );
+                               $redirectArticle->doEditUpdates( $redirectRevision, $wgUser, array( 'created' => true ) );
                        }
-               } else {
-                       $this->resetArticleID( 0 );
                }
 
                # Log the move
                $logid = $logEntry->insert();
                $logEntry->publish( $logid );
-
-               # Purge caches for old and new titles
-               if ( $moveOverRedirect ) {
-                       # A simple purge is enough when moving over a redirect
-                       $nt->purgeSquid();
-               } else {
-                       # Purge caches as per article creation, including any pages that link to this title
-                       Article::onArticleCreate( $nt );
-               }
-               $this->purgeSquid();
        }
 
        /**
@@ -3758,8 +3714,8 @@ class Title {
                        // We don't know whether this function was called before
                        // or after moving the root page, so check both
                        // $this and $nt
-                       if ( $oldSubpage->getArticleId() == $this->getArticleId() ||
-                                       $oldSubpage->getArticleID() == $nt->getArticleId() )
+                       if ( $oldSubpage->getArticleID() == $this->getArticleID() ||
+                                       $oldSubpage->getArticleID() == $nt->getArticleID() )
                        {
                                // When moving a page to a subpage of itself,
                                // don't move it twice
@@ -3883,7 +3839,7 @@ class Title {
 
                $data = array();
 
-               $titleKey = $this->getArticleId();
+               $titleKey = $this->getArticleID();
 
                if ( $titleKey === 0 ) {
                        return $data;
@@ -3961,7 +3917,7 @@ class Title {
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                return $db->selectField( 'revision', 'rev_id',
                        array(
-                               'rev_page' => $this->getArticleId( $flags ),
+                               'rev_page' => $this->getArticleID( $flags ),
                                'rev_id < ' . intval( $revId )
                        ),
                        __METHOD__,
@@ -3980,7 +3936,7 @@ class Title {
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                return $db->selectField( 'revision', 'rev_id',
                        array(
-                               'rev_page' => $this->getArticleId( $flags ),
+                               'rev_page' => $this->getArticleID( $flags ),
                                'rev_id > ' . intval( $revId )
                        ),
                        __METHOD__,
@@ -3995,7 +3951,7 @@ class Title {
         * @return Revision|Null if page doesn't exist
         */
        public function getFirstRevision( $flags = 0 ) {
-               $pageId = $this->getArticleId( $flags );
+               $pageId = $this->getArticleID( $flags );
                if ( $pageId ) {
                        $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                        $row = $db->selectRow( 'revision', '*',
@@ -4031,6 +3987,41 @@ class Title {
                return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
        }
 
+       /**
+        * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit
+        *
+        * @return bool
+        */
+       public function isBigDeletion() {
+               global $wgDeleteRevisionsLimit;
+
+               if ( !$wgDeleteRevisionsLimit ) {
+                       return false;
+               }
+
+               $revCount = $this->estimateRevisionCount();
+               return $revCount > $wgDeleteRevisionsLimit;
+       }
+
+       /**
+        * Get the  approximate revision count of this page.
+        *
+        * @return int
+        */
+       public function estimateRevisionCount() {
+               if ( !$this->exists() ) {
+                       return 0;
+               }
+
+               if ( $this->mEstimateRevisions === null ) {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
+                               array( 'rev_page' => $this->getArticleID() ), __METHOD__ );
+               }
+
+               return $this->mEstimateRevisions;
+       }
+
        /**
         * Get the number of revisions between the given revision.
         * Used for diffs and other things that really need it.
@@ -4052,7 +4043,7 @@ class Title {
                $dbr = wfGetDB( DB_SLAVE );
                return (int)$dbr->selectField( 'revision', 'count(*)',
                        array(
-                               'rev_page' => $this->getArticleId(),
+                               'rev_page' => $this->getArticleID(),
                                'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
                                'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
                        ),
@@ -4126,7 +4117,7 @@ class Title {
         * @return Bool
         */
        public function exists() {
-               return $this->getArticleId() != 0;
+               return $this->getArticleID() != 0;
        }
 
        /**
@@ -4146,9 +4137,28 @@ class Title {
         * @return Bool
         */
        public function isAlwaysKnown() {
+               $isKnown = null;
+
+               /**
+                * Allows overriding default behaviour for determining if a page exists.
+                * If $isKnown is kept as null, regular checks happen. If it's
+                * a boolean, this value is returned by the isKnown method.
+                *
+                * @since 1.20
+                *
+                * @param Title $title
+                * @param boolean|null $isKnown
+                */
+               wfRunHooks( 'TitleIsAlwaysKnown', array( $this, &$isKnown ) );
+
+               if ( !is_null( $isKnown ) ) {
+                       return $isKnown;
+               }
+
                if ( $this->mInterwiki != '' ) {
                        return true;  // any interwiki link might be viewable, for all we know
                }
+
                switch( $this->mNamespace ) {
                        case NS_MEDIA:
                        case NS_FILE:
@@ -4173,6 +4183,9 @@ class Title {
         * viewed?  In particular, this function may be used to determine if
         * links to the title should be rendered as "bluelinks" (as opposed to
         * "redlinks" to non-existent pages).
+        * Adding something else to this function will cause inconsistency
+        * since LinkHolderArray calls isAlwaysKnown() and does its own
+        * page existence check.
         *
         * @return Bool
         */
@@ -4465,7 +4478,7 @@ class Title {
                if ( $this->isSpecialPage() ) {
                        // special pages are in the user language
                        return $wgLang;
-               } elseif ( $this->isCssOrJsPage() ) {
+               } elseif ( $this->isCssOrJsPage() || $this->isCssJsSubpage() ) {
                        // css/js should always be LTR and is, in fact, English
                        return wfGetLangObj( 'en' );
                } elseif ( $this->getNamespace() == NS_MEDIAWIKI ) {