Made loadFromFileCache() always disable $wgOut regardless of whether compression...
[lhc/web/wiklou.git] / includes / OutputPage.php
index 8d17e8b..9723690 100644 (file)
@@ -67,7 +67,7 @@ class OutputPage extends ContextSource {
         * Contains the page subtitle. Special pages usually have some links here.
         * Don't confuse with site subtitle added by skins.
         */
-       var $mSubtitle = '';
+       private $mSubtitle = array();
 
        var $mRedirect = '';
        var $mStatusCode;
@@ -197,6 +197,7 @@ class OutputPage extends ContextSource {
 
        /// should be private. To include the variable {{REVISIONID}}
        var $mRevisionId = null;
+       private $mRevisionTimestamp = null;
 
        var $mFileVersion = null;
 
@@ -222,6 +223,14 @@ class OutputPage extends ContextSource {
                'Cookie' => null
        );
 
+       /**
+        * If the current page was reached through a redirect, $mRedirectedFrom contains the Title
+        * of the redirect.
+        *
+        * @var Title
+        */
+       private $mRedirectedFrom = null;
+
        /**
         * Constructor for OutputPage. This should not be called directly.
         * Instead a new RequestContext should be created and it will implicitly create
@@ -776,6 +785,15 @@ class OutputPage extends ContextSource {
                return $this->mHTMLtitle;
        }
 
+       /**
+        * Set $mRedirectedFrom, the Title of the page which redirected us to the current page.
+        *
+        * param @t Title
+        */
+       public function setRedirectedFrom( $t ) {
+               $this->mRedirectedFrom = $t;
+       }
+
        /**
         * "Page title" means the contents of \<h1\>. It is stored as a valid HTML fragment.
         * This function allows good tags like \<sup\> in the \<h1\> tag, but not bad tags like \<script\>.
@@ -795,7 +813,7 @@ class OutputPage extends ContextSource {
                $this->mPagetitle = $nameWithTags;
 
                # change "<i>foo&amp;bar</i>" to "foo&bar"
-               $this->setHTMLTitle( $this->msg( 'pagetitle', Sanitizer::stripAllTags( $nameWithTags ) ) );
+               $this->setHTMLTitle( $this->msg( 'pagetitle' )->rawParams( Sanitizer::stripAllTags( $nameWithTags ) ) );
        }
 
        /**
@@ -820,19 +838,54 @@ class OutputPage extends ContextSource {
        /**
         * Replace the subtile with $str
         *
-        * @param $str String: new value of the subtitle
+        * @param $str String|Message: new value of the subtitle
         */
        public function setSubtitle( $str ) {
-               $this->mSubtitle = /*$this->parse(*/ $str /*)*/; // @bug 2514
+               $this->clearSubtitle();
+               $this->addSubtitle( $str );
        }
 
        /**
         * Add $str to the subtitle
         *
-        * @param $str String to add to the subtitle
+        * @deprecated in 1.19; use addSubtitle() instead
+        * @param $str String|Message to add to the subtitle
         */
        public function appendSubtitle( $str ) {
-               $this->mSubtitle .= /*$this->parse(*/ $str /*)*/; // @bug 2514
+               $this->addSubtitle( $str );
+       }
+
+       /**
+        * Add $str to the subtitle
+        *
+        * @param $str String|Message to add to the subtitle
+        */
+       public function addSubtitle( $str ) {
+               if ( $str instanceof Message ) {
+                       $this->mSubtitle[] = $str->setContext( $this->getContext() )->parse();
+               } else {
+                       $this->mSubtitle[] = $str;
+               }
+       }
+
+       /**
+        * Add a subtitle containing a backlink to a page
+        *
+        * @param $title Title to link to
+        */
+       public function addBacklinkSubtitle( Title $title ) {
+               $query = array();
+               if ( $title->isRedirect() ) {
+                       $query['redirect'] = 'no';
+               }
+               $this->addSubtitle( $this->msg( 'backlinksubtitle' )->rawParams( Linker::link( $title, null, array(), $query ) ) );
+       }
+
+       /**
+        * Clear the subtitles
+        */
+       public function clearSubtitle() {
+               $this->mSubtitle = array();
        }
 
        /**
@@ -841,7 +894,7 @@ class OutputPage extends ContextSource {
         * @return String
         */
        public function getSubtitle() {
-               return $this->mSubtitle;
+               return implode( "<br />\n\t\t\t\t", $this->mSubtitle );
        }
 
        /**
@@ -1022,7 +1075,7 @@ class OutputPage extends ContextSource {
        /**
         * Add new language links
         *
-        * @param $newLinkArray Associative array mapping language code to the page
+        * @param $newLinkArray array Associative array mapping language code to the page
         *                      name
         */
        public function addLanguageLinks( $newLinkArray ) {
@@ -1032,7 +1085,7 @@ class OutputPage extends ContextSource {
        /**
         * Reset the language links and add new language links
         *
-        * @param $newLinkArray Associative array mapping language code to the page
+        * @param $newLinkArray array Associative array mapping language code to the page
         *                      name
         */
        public function setLanguageLinks( $newLinkArray ) {
@@ -1155,9 +1208,11 @@ class OutputPage extends ContextSource {
         * Return whether user JavaScript is allowed for this page
         * @deprecated since 1.18 Load modules with ResourceLoader, and origin and
         *     trustworthiness is identified and enforced automagically.
+        *     Will be removed in 1.20.
         * @return Boolean
         */
        public function isUserJsAllowed() {
+               wfDeprecated( __METHOD__, '1.18' );
                return $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS ) >= ResourceLoaderModule::ORIGIN_USER_INDIVIDUAL;
        }
 
@@ -1287,10 +1342,31 @@ class OutputPage extends ContextSource {
                return $this->mRevisionId;
        }
 
+       /**
+        * Set the timestamp of the revision which will be displayed. This is used
+        * to avoid a extra DB call in Skin::lastModified().
+        *
+        * @param $revid Mixed: string, or null
+        * @return Mixed: previous value
+        */
+       public function setRevisionTimestamp( $timestmap ) {
+               return wfSetVar( $this->mRevisionTimestamp, $timestmap );
+       }
+
+       /**
+        * Get the timestamp of displayed revision.
+        * This will be null if not filled by setRevisionTimestamp().
+        *
+        * @return String or null
+        */
+       public function getRevisionTimestamp() {
+               return $this->mRevisionTimestamp;
+       }
+
        /**
         * Set the displayed file version
         *
-        * @param $file File|false
+        * @param $file File|bool
         * @return Mixed: previous value
         */
        public function setFileVersion( $file ) {
@@ -1391,8 +1467,6 @@ class OutputPage extends ContextSource {
 
                wfProfileIn( __METHOD__ );
 
-               wfIncrStats( 'pcache_not_possible' );
-
                $popts = $this->parserOptions();
                $oldTidy = $popts->setTidy( $tidy );
                $popts->setInterfaceMessage( (bool) $interface );
@@ -1844,27 +1918,34 @@ class OutputPage extends ContextSource {
                if ( $this->mRedirect != '' ) {
                        # Standards require redirect URLs to be absolute
                        $this->mRedirect = wfExpandUrl( $this->mRedirect, PROTO_CURRENT );
-                       if( $this->mRedirectCode == '301' || $this->mRedirectCode == '303' ) {
-                               if( !$wgDebugRedirects ) {
-                                       $message = HttpStatus::getMessage( $this->mRedirectCode );
-                                       $response->header( "HTTP/1.1 {$this->mRedirectCode} $message" );
+
+                       $redirect = $this->mRedirect;
+                       $code = $this->mRedirectCode;
+
+                       if( wfRunHooks( "BeforePageRedirect", array( $this, &$redirect, &$code ) ) ) {
+                               if( $code == '301' || $code == '303' ) {
+                                       if( !$wgDebugRedirects ) {
+                                               $message = HttpStatus::getMessage( $code );
+                                               $response->header( "HTTP/1.1 $code $message" );
+                                       }
+                                       $this->mLastModified = wfTimestamp( TS_RFC2822 );
+                               }
+                               if ( $wgVaryOnXFP ) {
+                                       $this->addVaryHeader( 'X-Forwarded-Proto' );
+                               }
+                               $this->sendCacheControl();
+
+                               $response->header( "Content-Type: text/html; charset=utf-8" );
+                               if( $wgDebugRedirects ) {
+                                       $url = htmlspecialchars( $redirect );
+                                       print "<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
+                                       print "<p>Location: <a href=\"$url\">$url</a></p>\n";
+                                       print "</body>\n</html>\n";
+                               } else {
+                                       $response->header( 'Location: ' . $redirect );
                                }
-                               $this->mLastModified = wfTimestamp( TS_RFC2822 );
-                       }
-                       if ( $wgVaryOnXFP ) {
-                               $this->addVaryHeader( 'X-Forwarded-Proto' );
-                       }
-                       $this->sendCacheControl();
-
-                       $response->header( "Content-Type: text/html; charset=utf-8" );
-                       if( $wgDebugRedirects ) {
-                               $url = htmlspecialchars( $this->mRedirect );
-                               print "<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
-                               print "<p>Location: <a href=\"$url\">$url</a></p>\n";
-                               print "</body>\n</html>\n";
-                       } else {
-                               $response->header( 'Location: ' . $this->mRedirect );
                        }
+
                        wfProfileOut( __METHOD__ );
                        return;
                } elseif ( $this->mStatusCode ) {
@@ -1947,6 +2028,7 @@ class OutputPage extends ContextSource {
                $this->setArticleRelated( false );
                $this->enableClientCache( false );
                $this->mRedirect = '';
+               $this->clearSubtitle();
                $this->clearHTML();
        }
 
@@ -1993,7 +2075,13 @@ class OutputPage extends ContextSource {
                        || ( isset( $wgGroupPermissions['autoconfirmed'][$action] ) && $wgGroupPermissions['autoconfirmed'][$action] ) )
                ) {
                        $displayReturnto = null;
-                       $returnto = $this->getTitle();
+
+                       # Due to bug 32276, if a user does not have read permissions,
+                       # $this->getTitle() will just give Special:Badtitle, which is
+                       # not especially useful as a returnto parameter. Use the title
+                       # from the request instead, if there was one.
+                       $request = $this->getRequest();
+                       $returnto = Title::newFromURL( $request->getVal( 'title', '' ) );
                        if ( $action == 'edit' ) {
                                $msg = 'whitelistedittext';
                                $displayReturnto = $returnto;
@@ -2007,9 +2095,10 @@ class OutputPage extends ContextSource {
                        }
 
                        $query = array();
+
                        if ( $returnto ) {
                                $query['returnto'] = $returnto->getPrefixedText();
-                               $request = $this->getRequest();
+
                                if ( !$request->wasPosted() ) {
                                        $returntoquery = $request->getValues();
                                        unset( $returntoquery['title'] );
@@ -2030,7 +2119,7 @@ class OutputPage extends ContextSource {
 
                        # Don't return to a page the user can't read otherwise
                        # we'll end up in a pointless loop
-                       if ( $displayReturnto && $displayReturnto->userCanRead() ) {
+                       if ( $displayReturnto && $displayReturnto->userCan( 'read', $this->getUser() ) ) {
                                $this->returnToMain( null, $displayReturnto );
                        }
                } else {
@@ -2140,10 +2229,8 @@ class OutputPage extends ContextSource {
                if ( !empty( $reasons ) ) {
                        // Permissions error
                        if( $source ) {
-                               $this->setPageTitle( $this->msg( 'viewsource' ) );
-                               $this->setSubtitle(
-                                       $this->msg( 'viewsourcefor', Linker::linkKnown( $this->getTitle() ) )->text()
-                               );
+                               $this->setPageTitle( $this->msg( 'viewsource-title', $this->getTitle()->getPrefixedText() ) );
+                               $this->addBacklinkSubtitle( $this->getTitle() );
                        } else {
                                $this->setPageTitle( $this->msg( 'badaccess' ) );
                        }
@@ -2164,14 +2251,13 @@ class OutputPage extends ContextSource {
                                'cols' => $this->getUser()->getOption( 'cols' ),
                                'rows' => $this->getUser()->getOption( 'rows' ),
                                'readonly' => 'readonly',
-                               'lang' => $pageLang->getCode(),
+                               'lang' => $pageLang->getHtmlCode(),
                                'dir' => $pageLang->getDir(),
                        );
                        $this->addHTML( Html::element( 'textarea', $params, $source ) );
 
                        // Show templates used by this article
-                       $article = new Article( $this->getTitle() );
-                       $templates = Linker::formatTemplates( $article->getUsedTemplates() );
+                       $templates = Linker::formatTemplates( $this->getTitle()->getTemplateLinksFrom() );
                        $this->addHTML( "<div class='templatesUsed'>
 $templates
 </div>
@@ -2210,7 +2296,7 @@ $templates
                                ? 'lag-warn-normal'
                                : 'lag-warn-high';
                        $wrap = Html::rawElement( 'div', array( 'class' => "mw-{$message}" ), "\n$1\n" );
-                       $this->wrapWikiMsg( "$wrap\n", array( $message, $this->getLang()->formatNum( $lag ) ) );
+                       $this->wrapWikiMsg( "$wrap\n", array( $message, $this->getLanguage()->formatNum( $lag ) ) );
                }
        }
 
@@ -2293,15 +2379,16 @@ $templates
         * @return String: The doctype, opening <html>, and head element.
         */
        public function headElement( Skin $sk, $includeStyle = true ) {
-               global $wgContLang, $wgUseTrackbacks;
-               $userdir = $this->getLang()->getDir();
+               global $wgContLang;
+
+               $userdir = $this->getLanguage()->getDir();
                $sitedir = $wgContLang->getDir();
 
                if ( $sk->commonPrintStylesheet() ) {
                        $this->addModuleStyles( 'mediawiki.legacy.wikiprintable' );
                }
 
-               $ret = Html::htmlHeader( array( 'lang' => $this->getLang()->getCode(), 'dir' => $userdir, 'class' => 'client-nojs' ) );
+               $ret = Html::htmlHeader( array( 'lang' => $this->getLanguage()->getHtmlCode(), 'dir' => $userdir, 'class' => 'client-nojs' ) );
 
                if ( $this->getHTMLTitle() == '' ) {
                        $this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() ) );
@@ -2322,10 +2409,6 @@ $templates
                        $this->getHeadItems()
                ) );
 
-               if ( $wgUseTrackbacks && $this->isArticleRelated() ) {
-                       $ret .= $this->getTitle()->trackbackRDF();
-               }
-
                $closeHead = Html::closeElement( 'head' );
                if ( $closeHead ) {
                        $ret .= "$closeHead\n";
@@ -2336,12 +2419,13 @@ $templates
                # Classes for LTR/RTL directionality support
                $bodyAttrs['class'] = "mediawiki $userdir sitedir-$sitedir";
 
-               if ( $this->getLang()->capitalizeAllNouns() ) {
+               if ( $this->getLanguage()->capitalizeAllNouns() ) {
                        # A <body> class is probably not the best way to do this . . .
                        $bodyAttrs['class'] .= ' capitalize-all-nouns';
                }
                $bodyAttrs['class'] .= ' ' . $sk->getPageClasses( $this->getTitle() );
                $bodyAttrs['class'] .= ' skin-' . Sanitizer::escapeClass( $sk->getSkinName() );
+               $bodyAttrs['class'] .= ' action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) );
 
                $sk->addToBodyAttributes( $this, $bodyAttrs ); // Allow skins to add body attributes they need
                wfRunHooks( 'OutputPageBodyAttributes', array( $this, $sk, &$bodyAttrs ) );
@@ -2355,12 +2439,12 @@ $templates
         * Add the default ResourceLoader modules to this object
         */
        private function addDefaultModules() {
-               global $wgIncludeLegacyJavaScript, $wgUseAjax, $wgAjaxWatch, $wgEnableMWSuggest;
+               global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil, $wgUseAjax,
+                       $wgAjaxWatch, $wgEnableMWSuggest;
 
                // Add base resources
                $this->addModules( array(
                        'mediawiki.user',
-                       'mediawiki.util',
                        'mediawiki.page.startup',
                        'mediawiki.page.ready',
                ) );
@@ -2368,6 +2452,12 @@ $templates
                        $this->addModules( 'mediawiki.legacy.wikibits' );
                }
 
+               if ( $wgPreloadJavaScriptMwUtil ) {
+                       $this->addModules( 'mediawiki.util' );
+               }
+
+               MWDebug::addModules( $this );
+
                // Add various resources if required
                if ( $wgUseAjax ) {
                        $this->addModules( 'mediawiki.legacy.ajax' );
@@ -2411,9 +2501,10 @@ $templates
         * @param $only String ResourceLoaderModule TYPE_ class constant
         * @param $useESI boolean
         * @param $extraQuery Array with extra query parameters to add to each request. array( param => value )
+        * @param $loadCall boolean If true, output a mw.loader.load() call rather than a <script src="..."> tag
         * @return string html <script> and <style> tags
         */
-       protected function makeResourceLoaderLink( $modules, $only, $useESI = false, array $extraQuery = array() ) {
+       protected function makeResourceLoaderLink( $modules, $only, $useESI = false, array $extraQuery = array(), $loadCall = false ) {
                global $wgResourceLoaderUseESI, $wgResourceLoaderInlinePrivateModules;
 
                if ( !count( $modules ) ) {
@@ -2442,7 +2533,8 @@ $templates
                foreach ( (array) $modules as $name ) {
                        $module = $resourceLoader->getModule( $name );
                        # Check that we're allowed to include this module on this page
-                       if ( ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS )
+                       if ( !$module
+                               || ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS )
                                        && $only == ResourceLoaderModule::TYPE_SCRIPTS )
                                || ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_STYLES )
                                        && $only == ResourceLoaderModule::TYPE_STYLES )
@@ -2470,7 +2562,7 @@ $templates
                        // correct timestamp and emptiness data
                        $query = ResourceLoader::makeLoaderQuery(
                                array(), // modules; not determined yet
-                               $this->getLang()->getCode(),
+                               $this->getLanguage()->getCode(),
                                $this->getSkin()->getSkinName(),
                                $user,
                                null, // version; not determined yet
@@ -2492,7 +2584,9 @@ $templates
                                continue;
                        }
 
-                       // Support inlining of private modules if configured as such
+                       // Support inlining of private modules if configured as such. Note that these
+                       // modules should be loaded from getHeadScripts() before the first loader call.
+                       // Otherwise other modules can't properly use them as dependencies (bug 30914)
                        if ( $group === 'private' && $wgResourceLoaderInlinePrivateModules ) {
                                if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
                                        $links .= Html::inlineStyle(
@@ -2526,7 +2620,7 @@ $templates
 
                        $url = ResourceLoader::makeLoaderURL(
                                array_keys( $modules ),
-                               $this->getLang()->getCode(),
+                               $this->getLanguage()->getCode(),
                                $this->getSkin()->getSkinName(),
                                $user,
                                $version,
@@ -2547,6 +2641,12 @@ $templates
                                // Automatically select style/script elements
                                if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
                                        $link = Html::linkedStyle( $url );
+                               } else if ( $loadCall ) { 
+                                       $link = Html::inlineScript(
+                                               ResourceLoader::makeLoaderConditionalScript(
+                                                       Xml::encodeJsCall( 'mw.loader.load', array( $url ) )
+                                               )
+                                       );
                                } else {
                                        $link = Html::linkedScript( $url );
                                }
@@ -2568,6 +2668,8 @@ $templates
         * @return String: HTML fragment
         */
        function getHeadScripts() {
+               global $wgResourceLoaderExperimentalAsyncLoading;
+               
                // Startup - this will immediately load jquery and mediawiki modules
                $scripts = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true );
 
@@ -2578,6 +2680,12 @@ $templates
                        )
                );
 
+               // Load embeddable private modules before any loader links
+               // This needs to be TYPE_COMBINED so these modules are properly wrapped
+               // in mw.loader.implement() calls and deferred until mw.user is available
+               $embedScripts = array( 'user.options', 'user.tokens' );
+               $scripts .= $this->makeResourceLoaderLink( $embedScripts, ResourceLoaderModule::TYPE_COMBINED );
+
                // Script and Messages "only" requests marked for top inclusion
                // Messages should go first
                $scripts .= $this->makeResourceLoaderLink( $this->getModuleMessages( true, 'top' ), ResourceLoaderModule::TYPE_MESSAGES );
@@ -2589,27 +2697,41 @@ $templates
                if ( $modules ) {
                        $scripts .= Html::inlineScript(
                                ResourceLoader::makeLoaderConditionalScript(
-                                       Xml::encodeJsCall( 'mw.loader.load', array( $modules ) )
+                                       Xml::encodeJsCall( 'mw.loader.load', array( $modules, null, true ) )
                                )
                        );
                }
+               
+               if ( $wgResourceLoaderExperimentalAsyncLoading ) {
+                       $scripts .= $this->getScriptsForBottomQueue( true );
+               }
 
                return $scripts;
        }
 
        /**
-        * JS stuff to put at the bottom of the <body>: modules marked with position 'bottom',
-        * legacy scripts ($this->mScripts), user preferences, site JS and user JS
+        * JS stuff to put at the 'bottom', which can either be the bottom of the <body>
+        * or the bottom of the <head> depending on $wgResourceLoaderExperimentalAsyncLoading:
+        * modules marked with position 'bottom', legacy scripts ($this->mScripts),
+        * user preferences, site JS and user JS
         *
+        * @param $inHead boolean If true, this HTML goes into the <head>, if false it goes into the <body>
         * @return string
         */
-       function getBottomScripts() {
+       function getScriptsForBottomQueue( $inHead ) {
                global $wgUseSiteJs, $wgAllowUserJs;
 
                // Script and Messages "only" requests marked for bottom inclusion
+               // If we're in the <head>, use load() calls rather than <script src="..."> tags
                // Messages should go first
-               $scripts = $this->makeResourceLoaderLink( $this->getModuleMessages( true, 'bottom' ), ResourceLoaderModule::TYPE_MESSAGES );
-               $scripts .= $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'bottom' ), ResourceLoaderModule::TYPE_SCRIPTS );
+               $scripts = $this->makeResourceLoaderLink( $this->getModuleMessages( true, 'bottom' ),
+                       ResourceLoaderModule::TYPE_MESSAGES, /* $useESI = */ false, /* $extraQuery = */ array(),
+                       /* $loadCall = */ $inHead
+               );
+               $scripts .= $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'bottom' ),
+                       ResourceLoaderModule::TYPE_SCRIPTS, /* $useESI = */ false, /* $extraQuery = */ array(),
+                       /* $loadCall = */ $inHead
+               );
 
                // Modules requests - let the client calculate dependencies and batch requests as it likes
                // Only load modules that have marked themselves for loading at the bottom
@@ -2625,11 +2747,13 @@ $templates
                // Legacy Scripts
                $scripts .= "\n" . $this->mScripts;
 
-               $userScripts = array( 'user.options', 'user.tokens' );
+               $userScripts = array();
 
                // Add site JS if enabled
                if ( $wgUseSiteJs ) {
-                       $scripts .= $this->makeResourceLoaderLink( 'site', ResourceLoaderModule::TYPE_SCRIPTS );
+                       $scripts .= $this->makeResourceLoaderLink( 'site', ResourceLoaderModule::TYPE_SCRIPTS,
+                               /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
+                       );
                        if( $this->getUser()->isLoggedIn() ){
                                $userScripts[] = 'user.groups';
                        }
@@ -2642,7 +2766,7 @@ $templates
                                // We're on a preview of a JS subpage
                                // Exclude this page from the user module in case it's in there (bug 26283)
                                $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false,
-                                       array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() )
+                                       array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead
                                );
                                // Load the previewed JS
                                $scripts .= Html::inlineScript( "\n" . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n";
@@ -2650,21 +2774,37 @@ $templates
                                // Include the user module normally
                                // We can't do $userScripts[] = 'user'; because the user module would end up
                                // being wrapped in a closure, so load it raw like 'site'
-                               $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS );
+                               $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS,
+                                       /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
+                               );
                        }
                }
-               $scripts .= $this->makeResourceLoaderLink( $userScripts, ResourceLoaderModule::TYPE_COMBINED );
+               $scripts .= $this->makeResourceLoaderLink( $userScripts, ResourceLoaderModule::TYPE_COMBINED,
+                       /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
+               );
 
                return $scripts;
        }
 
+       /**
+        * JS stuff to put at the bottom of the <body>
+        */
+       function getBottomScripts() {
+               global $wgResourceLoaderExperimentalAsyncLoading;
+               if ( !$wgResourceLoaderExperimentalAsyncLoading ) {
+                       return $this->getScriptsForBottomQueue( false );
+               } else {
+                       return '';
+               }
+       }
+
        /**
         * Add one or more variables to be set in mw.config in JavaScript.
         *
         * @param $key {String|Array} Key or array of key/value pars.
-        * @param $value {Mixed} Value of the configuration variable.
+        * @param $value {Mixed} [optional] Value of the configuration variable.
         */
-       public function addJsConfigVars( $keys, $value ) {
+       public function addJsConfigVars( $keys, $value = null ) {
                if ( is_array( $keys ) ) {
                        foreach ( $keys as $key => $value ) {
                                $this->mJsConfigVars[$key] = $value;
@@ -2679,39 +2819,68 @@ $templates
        /**
         * Get an array containing the variables to be set in mw.config in JavaScript.
         *
+        * DO NOT CALL THIS FROM OUTSIDE OF THIS CLASS OR Skin::makeGlobalVariablesScript().
+        * This is only public until that function is removed. You have been warned.
+        *
         * Do not add things here which can be evaluated in ResourceLoaderStartupScript
         * - in other words, page-independent/site-wide variables (without state).
         * You will only be adding bloat to the html page and causing page caches to
         * have to be purged on configuration changes.
+        * @return array
         */
-       protected function getJSVars() {
+       public function getJSVars() {
                global $wgUseAjax, $wgEnableMWSuggest;
 
+               $latestRevID = 0;
+               $pageID = 0;
+               $canonicalName = false; # bug 21115
+
                $title = $this->getTitle();
                $ns = $title->getNamespace();
                $nsname = MWNamespace::exists( $ns ) ? MWNamespace::getCanonicalName( $ns ) : $title->getNsText();
+
                if ( $ns == NS_SPECIAL ) {
                        list( $canonicalName, /*...*/ ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
-               } else {
-                       $canonicalName = false; # bug 21115
+               } elseif ( $this->canUseWikiPage() ) {
+                       $wikiPage = $this->getWikiPage();
+                       $latestRevID = $wikiPage->getLatest();
+                       $pageID = $wikiPage->getId();
                }
 
+               $lang = $title->getPageLanguage();
+
+               // Pre-process information
+               $separatorTransTable = $lang->separatorTransformTable();
+               $separatorTransTable = $separatorTransTable ? $separatorTransTable : array();
+               $compactSeparatorTransTable = array(
+                       implode( "\t", array_keys( $separatorTransTable ) ),
+                       implode( "\t", $separatorTransTable ),
+               );
+               $digitTransTable = $lang->digitTransformTable();
+               $digitTransTable = $digitTransTable ? $digitTransTable : array();
+               $compactDigitTransTable = array(
+                       implode( "\t", array_keys( $digitTransTable ) ),
+                       implode( "\t", $digitTransTable ),
+               );
+
                $vars = array(
                        'wgCanonicalNamespace' => $nsname,
                        'wgCanonicalSpecialPageName' => $canonicalName,
                        'wgNamespaceNumber' => $title->getNamespace(),
                        'wgPageName' => $title->getPrefixedDBKey(),
                        'wgTitle' => $title->getText(),
-                       'wgCurRevisionId' => $title->getLatestRevID(),
-                       'wgArticleId' => $title->getArticleId(),
+                       'wgCurRevisionId' => $latestRevID,
+                       'wgArticleId' => $pageID,
                        'wgIsArticle' => $this->isArticle(),
-                       'wgAction' => $this->getRequest()->getText( 'action', 'view' ),
+                       'wgAction' => Action::getActionName( $this->getContext() ),
                        'wgUserName' => $this->getUser()->isAnon() ? null : $this->getUser()->getName(),
                        'wgUserGroups' => $this->getUser()->getEffectiveGroups(),
                        'wgCategories' => $this->getCategories(),
                        'wgBreakFrames' => $this->getFrameOptions() == 'DENY',
+                       'wgPageContentLanguage' => $lang->getCode(),
+                       'wgSeparatorTransformTable' => $compactSeparatorTransTable,
+                       'wgDigitTransformTable' => $compactDigitTransTable,
                );
-               $lang = $this->getTitle()->getPageLanguage();
                if ( $lang->hasVariants() ) {
                        $vars['wgUserVariant'] = $lang->getPreferredVariant();
                }
@@ -2724,6 +2893,9 @@ $templates
                if ( $title->isMainPage() ) {
                        $vars['wgIsMainPage'] = true;
                }
+               if ( $this->mRedirectedFrom ) {
+                       $vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBKey();
+               }
 
                // Allow extensions to add their custom variables to the mw.config map.
                // Use the 'ResourceLoaderGetConfigVars' hook if the variable is not
@@ -2910,7 +3082,7 @@ $templates
                                                $tags[] = Html::element( 'link', array(
                                                        'rel' => 'alternate',
                                                        'hreflang' => $_v,
-                                                       'href' => $this->getTitle()->getLocalURL( '', $_v ) )
+                                                       'href' => $this->getTitle()->getLocalURL( array( 'variant' => $_v ) ) )
                                                );
                                        }
                                } else {
@@ -2976,10 +3148,11 @@ $templates
                                        );
                                }
                        } elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) {
+                               $rctitle = SpecialPage::getTitleFor( 'Recentchanges' );
                                foreach ( $wgAdvertisedFeedTypes as $format ) {
                                        $tags[] = $this->feedLink(
                                                $format,
-                                               $this->getTitle()->getLocalURL( "feed={$format}" ),
+                                               $rctitle->getLocalURL( "feed={$format}" ),
                                                $this->msg( "site-{$format}-feed", $wgSitename )->text() # For grep: 'site-rss-feed', 'site-atom-feed'.
                                        );
                                }
@@ -3036,7 +3209,7 @@ $templates
         * @param $flip String: Set to 'flip' to flip the CSS if needed
         */
        public function addInlineStyle( $style_css, $flip = 'noflip' ) {
-               if( $flip === 'flip' && $this->getLang()->isRTL() ) {
+               if( $flip === 'flip' && $this->getLanguage()->isRTL() ) {
                        # If wanted, and the interface is right-to-left, flip the CSS
                        $style_css = CSSJanus::transform( $style_css, true, false );
                }
@@ -3050,7 +3223,8 @@ $templates
         * @return string
         */
        public function buildCssLinks() {
-               global $wgUseSiteCss, $wgAllowUserCss, $wgAllowUserCssPrefs;
+               global $wgUseSiteCss, $wgAllowUserCss, $wgAllowUserCssPrefs,
+                       $wgLang, $wgContLang;
 
                $this->getSkin()->setupSkinUserCss( $this );
 
@@ -3079,8 +3253,15 @@ $templates
                                $otherTags .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_STYLES, false,
                                        array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() )
                                );
+                               
                                // Load the previewed CSS
-                               $otherTags .= Html::inlineStyle( $this->getRequest()->getText( 'wpTextbox1' ) );
+                               // If needed, Janus it first. This is user-supplied CSS, so it's
+                               // assumed to be right for the content language directionality.
+                               $previewedCSS = $this->getRequest()->getText( 'wpTextbox1' );
+                               if ( $wgLang->getDir() !== $wgContLang->getDir() ) {
+                                       $previewedCSS = CSSJanus::transform( $previewedCSS, true, false );
+                               }
+                               $otherTags .= Html::inlineStyle( $previewedCSS );
                        } else {
                                // Load the user styles normally
                                $moduleStyles[] = 'user';
@@ -3089,11 +3270,15 @@ $templates
 
                // Per-user preference styles
                if ( $wgAllowUserCssPrefs ) {
-                       $moduleStyles[] = 'user.options';
+                       $moduleStyles[] = 'user.cssprefs';
                }
 
                foreach ( $moduleStyles as $name ) {
-                       $group = $resourceLoader->getModule( $name )->getGroup();
+                       $module = $resourceLoader->getModule( $name );
+                       if ( !$module ) {
+                               continue;
+                       }
+                       $group = $module->getGroup();
                        // Modules in groups named "other" or anything different than "user", "site" or "private"
                        // will be placed in the "other" group
                        $styles[isset( $styles[$group] ) ? $group : 'other'][] = $name;
@@ -3155,7 +3340,7 @@ $templates
         */
        protected function styleLink( $style, $options ) {
                if( isset( $options['dir'] ) ) {
-                       if( $this->getLang()->getDir() != $options['dir'] ) {
+                       if( $this->getLanguage()->getDir() != $options['dir'] ) {
                                return '';
                        }
                }