Preload the logo using link rel="preload" http header
[lhc/web/wiklou.git] / includes / OutputPage.php
index 91fc75c..3f10c06 100644 (file)
@@ -302,6 +302,11 @@ class OutputPage extends ContextSource {
        /** @var array Profiling data */
        private $limitReportJSData = [];
 
+       /**
+        * Link: header contents
+        */
+       private $mLinkHeader = [];
+
        /**
         * Constructor for OutputPage. This should not be called directly.
         * Instead a new RequestContext should be created and it will implicitly create
@@ -534,14 +539,32 @@ class OutputPage extends ContextSource {
                        if ( $module instanceof ResourceLoaderModule
                                && $module->getOrigin() <= $this->getAllowedModules( $type )
                                && ( is_null( $position ) || $module->getPosition() == $position )
-                               && ( !$this->mTarget || in_array( $this->mTarget, $module->getTargets() ) )
                        ) {
+                               if ( $this->mTarget && !in_array( $this->mTarget, $module->getTargets() ) ) {
+                                       $this->warnModuleTargetFilter( $module->getName() );
+                                       continue;
+                               }
                                $filteredModules[] = $val;
                        }
                }
                return $filteredModules;
        }
 
+       private function warnModuleTargetFilter( $moduleName ) {
+               static $warnings = [];
+               if ( isset( $warnings[$this->mTarget][$moduleName] ) ) {
+                       return;
+               }
+               $warnings[$this->mTarget][$moduleName] = true;
+               $this->getResourceLoader()->getLogger()->debug(
+                       'Module "{module}" not loadable on target "{target}".',
+                       [
+                               'module' => $moduleName,
+                               'target' => $this->mTarget,
+                       ]
+               );
+       }
+
        /**
         * Get the list of modules to include on this page
         *
@@ -763,7 +786,7 @@ class OutputPage extends ContextSource {
                        'epoch' => $config->get( 'CacheEpoch' )
                ];
                if ( $config->get( 'UseSquid' ) ) {
-                       // bug 44570: the core page itself may not change, but resources might
+                       // T46570: the core page itself may not change, but resources might
                        $modifiedTimes['sepoch'] = wfTimestamp( TS_MW, time() - $config->get( 'SquidMaxage' ) );
                }
                Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this ] );
@@ -1448,7 +1471,7 @@ class OutputPage extends ContextSource {
                        ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
                );
 
-               // Site-wide styles are controlled by a config setting, see bug 71621
+               // Site-wide styles are controlled by a config setting, see T73621
                // for background on why. User styles are never allowed.
                if ( $this->getConfig()->get( 'AllowSiteCSSOnRestrictedPages' ) ) {
                        $styleOrigin = ResourceLoaderModule::ORIGIN_USER_SITEWIDE;
@@ -1550,6 +1573,7 @@ class OutputPage extends ContextSource {
                        // been changed somehow, and keep it if so.
                        $anonPO = ParserOptions::newFromAnon();
                        $anonPO->setEditSection( false );
+                       $anonPO->setAllowUnsafeRawHtml( false );
                        if ( !$options->matches( $anonPO ) ) {
                                wfLogWarning( __METHOD__ . ': Setting a changed bogus ParserOptions: ' . wfGetAllCallers( 5 ) );
                                $options->isBogus = false;
@@ -1563,6 +1587,7 @@ class OutputPage extends ContextSource {
                                // either.
                                $po = ParserOptions::newFromAnon();
                                $po->setEditSection( false );
+                               $po->setAllowUnsafeRawHtml( false );
                                $po->isBogus = true;
                                if ( $options !== null ) {
                                        $this->mParserOptions = empty( $options->isBogus ) ? $options : null;
@@ -1572,6 +1597,7 @@ class OutputPage extends ContextSource {
 
                        $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() );
                        $this->mParserOptions->setEditSection( false );
+                       $this->mParserOptions->setAllowUnsafeRawHtml( false );
                }
 
                if ( $options !== null && !empty( $options->isBogus ) ) {
@@ -2084,6 +2110,28 @@ class OutputPage extends ContextSource {
                return 'Vary: ' . implode( ', ', array_keys( $this->mVaryHeader ) );
        }
 
+       /**
+        * Add an HTTP Link: header
+        *
+        * @param string $header Header value
+        */
+       public function addLinkHeader( $header ) {
+               $this->mLinkHeader[] = $header;
+       }
+
+       /**
+        * Return a Link: header. Based on the values of $mLinkHeader.
+        *
+        * @return string
+        */
+       public function getLinkHeader() {
+               if ( !$this->mLinkHeader ) {
+                       return false;
+               }
+
+               return 'Link: ' . implode( ',', $this->mLinkHeader );
+       }
+
        /**
         * Get a complete Key header
         *
@@ -2340,6 +2388,12 @@ class OutputPage extends ContextSource {
                // jQuery etc. can work correctly.
                $response->header( 'X-UA-Compatible: IE=Edge' );
 
+               $this->addLogoPreloadLinkHeaders();
+               $linkHeader = $this->getLinkHeader();
+               if ( $linkHeader ) {
+                       $response->header( $linkHeader );
+               }
+
                // Prevent framing, if requested
                $frameOptions = $this->getFrameOptions();
                if ( $frameOptions ) {
@@ -2484,7 +2538,7 @@ class OutputPage extends ContextSource {
                ) {
                        $displayReturnto = null;
 
-                       # Due to bug 32276, if a user does not have read permissions,
+                       # Due to T34276, 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.
@@ -2706,7 +2760,9 @@ class OutputPage extends ContextSource {
                } else {
                        $titleObj = Title::newFromText( $returnto );
                }
-               if ( !is_object( $titleObj ) ) {
+               // We don't want people to return to external interwiki. That
+               // might potentially be used as part of a phishing scheme
+               if ( !is_object( $titleObj ) || $titleObj->isExternal() ) {
                        $titleObj = Title::newMainPage();
                }
 
@@ -3066,7 +3122,7 @@ class OutputPage extends ContextSource {
 
                $curRevisionId = 0;
                $articleId = 0;
-               $canonicalSpecialPageName = false; # bug 21115
+               $canonicalSpecialPageName = false; # T23115
 
                $title = $this->getTitle();
                $ns = $title->getNamespace();
@@ -3076,7 +3132,7 @@ class OutputPage extends ContextSource {
 
                $sk = $this->getSkin();
                // Get the relevant title so that AJAX features can use the correct page name
-               // when making API requests from certain special pages (bug 34972).
+               // when making API requests from certain special pages (T36972).
                $relevantTitle = $sk->getRelevantTitle();
                $relevantUser = $sk->getRelevantUser();
 
@@ -3697,6 +3753,8 @@ class OutputPage extends ContextSource {
         */
        public static function transformResourcePath( Config $config, $path ) {
                global $IP;
+
+               $localDir = $IP;
                $remotePathPrefix = $config->get( 'ResourceBasePath' );
                if ( $remotePathPrefix === '' ) {
                        // The configured base path is required to be empty string for
@@ -3710,8 +3768,18 @@ class OutputPage extends ContextSource {
                        // - Path is protocol-relative. Fixes T155310. Not supported by RelPath lib.
                        return $path;
                }
+               // For files in resources, extensions/ or skins/, ResourceBasePath is preferred here.
+               // For other misc files in $IP, we'll fallback to that as well. There is, however, a fourth
+               // supported dir/path pair in the configuration (wgUploadDirectory, wgUploadPath)
+               // which is not expected to be in wgResourceBasePath on CDNs. (T155146)
+               $uploadPath = $config->get( 'UploadPath' );
+               if ( strpos( $path, $uploadPath ) === 0 ) {
+                       $localDir = $config->get( 'UploadDirectory' );
+                       $remotePathPrefix = $remotePath = $uploadPath;
+               }
+
                $path = RelPath\getRelativePath( $path, $remotePath );
-               return self::transformFilePath( $remotePathPrefix, $IP, $path );
+               return self::transformFilePath( $remotePathPrefix, $localDir, $path );
        }
 
        /**
@@ -3825,7 +3893,7 @@ class OutputPage extends ContextSource {
         *    $wgOut->addWikiText( "<div class='error'>\n"
         *        . wfMessage( 'some-error' )->plain() . "\n</div>" );
         *
-        * The newline after the opening div is needed in some wikitext. See bug 19226.
+        * The newline after the opening div is needed in some wikitext. See T21226.
         *
         * @param string $wrap
         */
@@ -3925,4 +3993,82 @@ class OutputPage extends ContextSource {
                        'mediawiki.widgets.styles',
                ] );
        }
+
+       /**
+        * Add Link headers for preloading the wiki's logo.
+        *
+        * @since  1.26
+        */
+       protected function addLogoPreloadLinkHeaders() {
+               $logo = $this->getConfig()->get( 'Logo' ); // wgLogo
+               $logoHD = $this->getConfig()->get( 'LogoHD' ); // wgLogoHD
+
+               $tags = [];
+               $logosPerDppx = [];
+               $logos = [];
+
+               $logosPerDppx['1.0'] = $logo;
+
+               if ( !$logoHD ) {
+                       // No media queries required if we only have one variant
+                       $this->addLinkHeader( '<' . $logo . '>;rel=preload;as=image' );
+                       return;
+               }
+
+               foreach ( $logoHD as $dppx => $src ) {
+                       // Only 1.5x and 2x are supported
+                       // Note: Keep in sync with ResourceLoaderSkinModule
+                       if ( in_array( $dppx, [ '1.5x', '2x' ] ) ) {
+                               // LogoHD uses a string in this format: "1.5x"
+                               $dppx = substr( $dppx, 0, -1 );
+                               $logosPerDppx[$dppx] = $src;
+                       }
+               }
+
+               // Because PHP can't have floats as array keys
+               uksort( $logosPerDppx, function ( $a , $b ) {
+                       $a = floatval( $a );
+                       $b = floatval( $b );
+
+                       if ( $a == $b ) {
+                               return 0;
+                       }
+                       // Sort from smallest to largest (e.g. 1x, 1.5x, 2x)
+                       return ( $a < $b ) ? -1 : 1;
+               } );
+
+               foreach ( $logosPerDppx as $dppx => $src ) {
+                       $logos[] = [ 'dppx' => $dppx, 'src' => $src ];
+               }
+
+               $logosCount = count( $logos );
+               // Logic must match ResourceLoaderSkinModule:
+               // - 1x applies to resolution < 1.5dppx
+               // - 1.5x applies to resolution >= 1.5dppx && < 2dppx
+               // - 2x applies to resolution >= 2dppx
+               // Note that min-resolution and max-resolution are both inclusive.
+               for ( $i = 0; $i < $logosCount; $i++ ) {
+                       if ( $i === 0 ) {
+                               // Smallest dppx
+                               // min-resolution is ">=" (larger than or equal to)
+                               // "not min-resolution" is essentially "<"
+                               $media_query = 'not all and (min-resolution: ' . $logos[ 1 ]['dppx'] . 'dppx)';
+                       } elseif ( $i !== $logosCount - 1 ) {
+                               // In between
+                               // Media query expressions can only apply "not" to the entire expression
+                               // (e.g. can't express ">= 1.5 and not >= 2).
+                               // Workaround: Use <= 1.9999 in place of < 2.
+                               $upper_bound = floatval( $logos[ $i + 1 ]['dppx'] ) - 0.000001;
+                               $media_query = '(min-resolution: ' .  $logos[ $i ]['dppx'] .
+                                       'dppx) and (max-resolution: ' . $upper_bound . 'dppx)';
+                       } else {
+                               // Largest dppx
+                               $media_query = '(min-resolution: ' . $logos[ $i ]['dppx'] . 'dppx)';
+                       }
+
+                       $this->addLinkHeader(
+                               '<' . $logos[$i]['src'] . '>;rel=preload;as=image;media=' . $media_query
+                       );
+               }
+       }
 }