Merge "Skin: Avoid redirect=no for links to non-redirects"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 21 Aug 2018 19:49:43 +0000 (19:49 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 21 Aug 2018 19:49:43 +0000 (19:49 +0000)
1  2 
includes/skins/Skin.php

diff --combined includes/skins/Skin.php
   * @file
   */
  
 +use MediaWiki\MediaWikiServices;
 +use Wikimedia\WrappedString;
 +use Wikimedia\WrappedStringList;
 +
  /**
   * @defgroup Skins Skins
   */
   * @ingroup Skins
   */
  abstract class Skin extends ContextSource {
 +      /**
 +       * @var string|null
 +       */
        protected $skinname = null;
 +
        protected $mRelevantTitle = null;
        protected $mRelevantUser = null;
  
        static function normalizeKey( $key ) {
                global $wgDefaultSkin, $wgFallbackSkin;
  
 -              $skinNames = Skin::getSkinNames();
 +              $skinNames = self::getSkinNames();
  
                // Make keys lowercase for case-insensitive matching.
                $skinNames = array_change_key_case( $skinNames, CASE_LOWER );
        }
  
        /**
 -       * @return string Skin name
 +       * @since 1.31
 +       * @param string|null $skinname
 +       */
 +      public function __construct( $skinname = null ) {
 +              if ( is_string( $skinname ) ) {
 +                      $this->skinname = $skinname;
 +              }
 +      }
 +
 +      /**
 +       * @return string|null Skin name
         */
        public function getSkinName() {
                return $this->skinname;
        /**
         * @param OutputPage $out
         */
 -      function initPage( OutputPage $out ) {
 -
 +      public function initPage( OutputPage $out ) {
                $this->preloadExistence();
 -
        }
  
        /**
         * Defines the ResourceLoader modules that should be added to the skin
         * It is recommended that skins wishing to override call parent::getDefaultModules()
         * and substitute out any modules they wish to change by using a key to look them up
 +       *
 +       * Any modules defined with the 'styles' key will be added as render blocking CSS via
 +       * Output::addModuleStyles. Similarly, each key should refer to a list of modules
 +       *
         * @return array Array of modules with helper keys for easy overriding
         */
        public function getDefaultModules() {
 -              global $wgUseAjax, $wgEnableAPI, $wgEnableWriteAPI;
 -
                $out = $this->getOutput();
 -              $user = $out->getUser();
 +              $config = $this->getConfig();
 +              $user = $this->getUser();
 +
 +              // Modules declared in the $modules literal are loaded
 +              // for ALL users, on ALL pages, in ALL skins.
 +              // Keep this list as small as possible!
                $modules = [
 -                      // modules that enhance the page content in some way
 +                      'styles' => [
 +                              // The 'styles' key sets render-blocking style modules
 +                              // Unlike other keys in $modules, this is an associative array
 +                              // where each key is its own group pointing to a list of modules
 +                              'core' => [
 +                                      'mediawiki.legacy.shared',
 +                                      'mediawiki.legacy.commonPrint',
 +                              ],
 +                              'content' => [],
 +                              'syndicate' => [],
 +                      ],
 +                      'core' => [
 +                              'site',
 +                              'mediawiki.page.startup',
 +                              'mediawiki.user',
 +                      ],
 +                      // modules that enhance the content in some way
                        'content' => [
                                'mediawiki.page.ready',
                        ],
 -                      // modules that exist for legacy reasons
 -                      'legacy' => ResourceLoaderStartUpModule::getLegacyModules(),
                        // modules relating to search functionality
                        'search' => [],
                        // modules relating to functionality relating to watching an article
                        'watch' => [],
                        // modules which relate to the current users preferences
                        'user' => [],
 +                      // modules relating to RSS/Atom Feeds
 +                      'syndicate' => [],
                ];
  
 -              // Add various resources if required
 -              if ( $wgUseAjax && $wgEnableAPI ) {
 -                      if ( $wgEnableWriteAPI && $user->isLoggedIn()
 -                              && $user->isAllowedAll( 'writeapi', 'viewmywatchlist', 'editmywatchlist' )
 -                              && $this->getRelevantTitle()->canExist()
 -                      ) {
 -                              $modules['watch'][] = 'mediawiki.page.watch.ajax';
 -                      }
 +              // Preload jquery.tablesorter for mediawiki.page.ready
 +              if ( strpos( $out->getHTML(), 'sortable' ) !== false ) {
 +                      $modules['content'][] = 'jquery.tablesorter';
 +              }
 +
 +              // Preload jquery.makeCollapsible for mediawiki.page.ready
 +              if ( strpos( $out->getHTML(), 'mw-collapsible' ) !== false ) {
 +                      $modules['content'][] = 'jquery.makeCollapsible';
 +                      $modules['styles']['content'][] = 'jquery.makeCollapsible.styles';
 +              }
 +
 +              // Deprecated since 1.26: Unconditional loading of mediawiki.ui.button
 +              // on every page is deprecated. Express a dependency instead.
 +              if ( strpos( $out->getHTML(), 'mw-ui-button' ) !== false ) {
 +                      $modules['styles']['content'][] = 'mediawiki.ui.button';
 +              }
  
 -                      $modules['search'][] = 'mediawiki.searchSuggest';
 +              if ( $out->isTOCEnabled() ) {
 +                      $modules['content'][] = 'mediawiki.toc';
 +                      $modules['styles']['content'][] = 'mediawiki.toc.styles';
                }
  
 +              // Add various resources if required
 +              if ( $user->isLoggedIn()
 +                      && $user->isAllowedAll( 'writeapi', 'viewmywatchlist', 'editmywatchlist' )
 +                      && $this->getRelevantTitle()->canExist()
 +              ) {
 +                      $modules['watch'][] = 'mediawiki.page.watch.ajax';
 +              }
 +
 +              $modules['search'][] = 'mediawiki.searchSuggest';
 +
                if ( $user->getBoolOption( 'editsectiononrightclick' ) ) {
                        $modules['user'][] = 'mediawiki.action.view.rightClickEdit';
                }
                if ( $out->isArticle() && $user->getOption( 'editondblclick' ) ) {
                        $modules['user'][] = 'mediawiki.action.view.dblClickEdit';
                }
 +
 +              if ( $out->isSyndicated() ) {
 +                      $modules['styles']['syndicate'][] = 'mediawiki.feedlink';
 +              }
 +
                return $modules;
        }
  
        /**
         * Preload the existence of three commonly-requested pages in a single query
         */
 -      function preloadExistence() {
 +      protected function preloadExistence() {
                $titles = [];
  
 -              $user = $this->getUser();
 -              $title = $this->getRelevantTitle();
 -
                // User/talk link
 +              $user = $this->getUser();
                if ( $user->isLoggedIn() ) {
                        $titles[] = $user->getUserPage();
                        $titles[] = $user->getTalkPage();
                }
  
                // Check, if the page can hold some kind of content, otherwise do nothing
 -              if ( !$title->canExist() ) {
 -                      // nothing
 -              } elseif ( $title->isTalkPage() ) {
 -                      $titles[] = $title->getSubjectPage();
 -              } else {
 -                      $titles[] = $title->getTalkPage();
 +              $title = $this->getRelevantTitle();
 +              if ( $title->canExist() ) {
 +                      if ( $title->isTalkPage() ) {
 +                              $titles[] = $title->getSubjectPage();
 +                      } else {
 +                              $titles[] = $title->getTalkPage();
 +                      }
 +              }
 +
 +              // Footer links (used by SkinTemplate::prepareQuickTemplate)
 +              foreach ( [
 +                      $this->footerLinkTitle( 'privacy', 'privacypage' ),
 +                      $this->footerLinkTitle( 'aboutsite', 'aboutpage' ),
 +                      $this->footerLinkTitle( 'disclaimers', 'disclaimerpage' ),
 +              ] as $title ) {
 +                      if ( $title ) {
 +                              $titles[] = $title;
 +                      }
                }
  
                Hooks::run( 'SkinPreloadExistence', [ &$titles, $this ] );
  
 -              if ( count( $titles ) ) {
 +              if ( $titles ) {
                        $lb = new LinkBatch( $titles );
                        $lb->setCaller( __METHOD__ );
                        $lb->execute();
  
        /**
         * Outputs the HTML generated by other functions.
 -       * @param OutputPage $out
 +       * @param OutputPage|null $out
         */
        abstract function outputPage( OutputPage $out = null );
  
        /**
         * @param array $data
 -       * @return string
 +       * @param string|null $nonce OutputPage::getCSPNonce()
 +       * @return string|WrappedString HTML
         */
 -      static function makeVariablesScript( $data ) {
 +      public static function makeVariablesScript( $data, $nonce = null ) {
                if ( $data ) {
                        return ResourceLoader::makeInlineScript(
 -                              ResourceLoader::makeConfigSetScript( $data )
 +                              ResourceLoader::makeConfigSetScript( $data ),
 +                              $nonce
                        );
 -              } else {
 -                      return '';
                }
 +              return '';
        }
  
        /**
        }
  
        /**
 -       * Add skin specific stylesheets
 -       * Calling this method with an $out of anything but the same OutputPage
 -       * inside ->getOutput() is deprecated. The $out arg is kept
 -       * for compatibility purposes with skins.
 -       * @param OutputPage $out
 -       * @todo delete
 +       * Hook point for adding style modules to OutputPage.
 +       *
 +       * @deprecated since 1.32 Use getDefaultModules() instead.
 +       * @param OutputPage $out Legacy parameter, identical to $this->getOutput()
         */
 -      abstract function setupSkinUserCss( OutputPage $out );
 +      public function setupSkinUserCss( OutputPage $out ) {
 +              // Stub.
 +      }
  
        /**
         * TODO: document
  
                if ( $title->isSpecialPage() ) {
                        $type = 'ns-special';
 -                      // bug 23315: provide a class based on the canonical special page name without subpages
 -                      list( $canonicalName ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
 +                      // T25315: provide a class based on the canonical special page name without subpages
 +                      list( $canonicalName ) = MediaWikiServices::getInstance()->getSpecialPageFactory()->
 +                              resolveAlias( $title->getDBkey() );
                        if ( $canonicalName ) {
                                $type .= ' ' . Sanitizer::escapeClass( "mw-special-$canonicalName" );
                        } else {
         * "<body>" tag, skins can override it if they have a need to add in any
         * body attributes or classes of their own.
         * @param OutputPage $out
 -       * @param array $bodyAttrs
 +       * @param array &$bodyAttrs
         */
        function addToBodyAttributes( $out, &$bodyAttrs ) {
                // does nothing by default
                return $wgLogo;
        }
  
 +      /**
 +       * Whether the logo should be preloaded with an HTTP link header or not
 +       * @since 1.29
 +       * @return bool
 +       */
 +      public function shouldPreloadLogo() {
 +              return false;
 +      }
 +
        /**
         * @return string HTML
         */
        /**
         * This gets called shortly before the "</body>" tag.
         *
 -       * @return string HTML-wrapped JS code to be put before "</body>"
 +       * @return string|WrappedStringList HTML containing scripts to put before `</body>`
         */
        function bottomScripts() {
                // TODO and the suckage continues. This function is really just a wrapper around
                // OutputPage::getBottomScripts() which takes a Skin param. This should be cleaned
                // up at some point
 -              $bottomScriptText = $this->getOutput()->getBottomScripts();
 -              Hooks::run( 'SkinAfterBottomScripts', [ $this, &$bottomScriptText ] );
 -
 -              return $bottomScriptText;
 +              $chunks = [ $this->getOutput()->getBottomScripts() ];
 +
 +              // Keep the hook appendage separate to preserve WrappedString objects.
 +              // This enables BaseTemplate::getTrail() to merge them where possible.
 +              $extraHtml = '';
 +              Hooks::run( 'SkinAfterBottomScripts', [ $this, &$extraHtml ] );
 +              if ( $extraHtml !== '' ) {
 +                      $chunks[] = $extraHtml;
 +              }
 +              return WrappedString::join( "\n", $chunks );
        }
  
        /**
        }
  
        /**
 +       * @param OutputPage|null $out Defaults to $this->getOutput() if left as null
         * @return string
         */
 -      function subPageSubtitle() {
 -              $out = $this->getOutput();
 +      function subPageSubtitle( $out = null ) {
 +              if ( $out === null ) {
 +                      $out = $this->getOutput();
 +              }
 +              $title = $out->getTitle();
                $subpages = '';
  
                if ( !Hooks::run( 'SkinSubPageSubtitle', [ &$subpages, $this, $out ] ) ) {
                        return $subpages;
                }
  
 -              if ( $out->isArticle() && MWNamespace::hasSubpages( $out->getTitle()->getNamespace() ) ) {
 -                      $ptext = $this->getTitle()->getPrefixedText();
 -                      if ( preg_match( '/\//', $ptext ) ) {
 +              if ( $out->isArticle() && MWNamespace::hasSubpages( $title->getNamespace() ) ) {
 +                      $ptext = $title->getPrefixedText();
 +                      if ( strpos( $ptext, '/' ) !== false ) {
                                $links = explode( '/', $ptext );
                                array_pop( $links );
                                $c = 0;
                return $subpages;
        }
  
 -      /**
 -       * @deprecated since 1.27, feature removed
 -       * @return bool Always false
 -       */
 -      function showIPinHeader() {
 -              wfDeprecated( __METHOD__, '1.27' );
 -              return false;
 -      }
 -
        /**
         * @return string
         */
                        $s = '';
                }
  
 -              if ( wfGetLB()->getLaggedSlaveMode() ) {
 +              if ( MediaWikiServices::getInstance()->getDBLoadBalancer()->getLaggedReplicaMode() ) {
                        $s .= ' <strong>' . $this->msg( 'laggedslavemode' )->parse() . '</strong>';
                }
  
                if ( is_string( $icon ) ) {
                        $html = $icon;
                } else { // Assuming array
 -                      $url = isset( $icon["url"] ) ? $icon["url"] : null;
 +                      $url = $icon["url"] ?? null;
                        unset( $icon["url"] );
                        if ( isset( $icon["src"] ) && $withImage === 'withImage' ) {
                                // do this the lazy way, just pass icon data as an attribute array
                                $html = htmlspecialchars( $icon["alt"] );
                        }
                        if ( $url ) {
 -                              $html = Html::rawElement( 'a', [ "href" => $url ], $html );
 +                              global $wgExternalLinkTarget;
 +                              $html = Html::rawElement( 'a',
 +                                      [ "href" => $url, "target" => $wgExternalLinkTarget ],
 +                                      $html );
                        }
                }
                return $html;
         * @return string HTML anchor
         */
        public function footerLink( $desc, $page ) {
 -              // if the link description has been set to "-" in the default language,
 -              if ( $this->msg( $desc )->inContentLanguage()->isDisabled() ) {
 -                      // then it is disabled, for all languages.
 +              $title = $this->footerLinkTitle( $desc, $page );
 +              if ( !$title ) {
                        return '';
 -              } else {
 -                      // Otherwise, we display the link for the user, described in their
 -                      // language (which may or may not be the same as the default language),
 -                      // but we make the link target be the one site-wide page.
 -                      $title = Title::newFromText( $this->msg( $page )->inContentLanguage()->text() );
 +              }
  
 -                      if ( !$title ) {
 -                              return '';
 -                      }
 +              return Linker::linkKnown(
 +                      $title,
 +                      $this->msg( $desc )->escaped()
 +              );
 +      }
  
 -                      return Linker::linkKnown(
 -                              $title,
 -                              $this->msg( $desc )->escaped()
 -                      );
 +      /**
 +       * @param string $desc
 +       * @param string $page
 +       * @return Title|null
 +       */
 +      private function footerLinkTitle( $desc, $page ) {
 +              // If the link description has been set to "-" in the default language,
 +              if ( $this->msg( $desc )->inContentLanguage()->isDisabled() ) {
 +                      // then it is disabled, for all languages.
 +                      return null;
                }
 +              // Otherwise, we display the link for the user, described in their
 +              // language (which may or may not be the same as the default language),
 +              // but we make the link target be the one site-wide page.
 +              $title = Title::newFromText( $this->msg( $page )->inContentLanguage()->text() );
 +
 +              return $title ?: null;
        }
  
        /**
                        $targetUser = User::newFromId( $id );
                }
  
 -              # The sending user must have a confirmed email address and the target
 -              # user must have a confirmed email address and allow emails from users.
 -              return $this->getUser()->canSendEmail() &&
 -                      $targetUser->canReceiveEmail();
 +              # The sending user must have a confirmed email address and the receiving
 +              # user must accept emails from the sender.
 +              return $this->getUser()->canSendEmail()
 +                      && SpecialEmailUser::validateTarget( $targetUser, $this->getUser() ) === '';
        }
  
        /**
 -       * Return a fully resolved style path url to images or styles stored in the current skins's folder.
 -       * This method returns a url resolved using the configured skin style path
 -       * and includes the style version inside of the url.
 +       * Return a fully resolved style path URL to images or styles stored in the
 +       * current skin's folder. This method returns a URL resolved using the
 +       * configured skin style path.
         *
         * Requires $stylename to be set, otherwise throws MWException.
         *
         * @param string $name The name or path of a skin resource file
 -       * @return string The fully resolved style path url including styleversion
 +       * @return string The fully resolved style path URL
         * @throws MWException
         */
        function getSkinStylePath( $name ) {
 -              global $wgStylePath, $wgStyleVersion;
 +              global $wgStylePath;
  
                if ( $this->stylename === null ) {
 -                      $class = get_class( $this );
 +                      $class = static::class;
                        throw new MWException( "$class::\$stylename must be set to use getSkinStylePath()" );
                }
  
 -              return "$wgStylePath/{$this->stylename}/$name?$wgStyleVersion";
 +              return "$wgStylePath/{$this->stylename}/$name";
        }
  
        /* these are used extensively in SkinTemplate, but also some other places */
  
        /**
 -       * @param string $urlaction
 +       * @param string|string[] $urlaction
         * @return string
         */
        static function makeMainPageUrl( $urlaction = '' ) {
                $title = Title::newMainPage();
                self::checkTitle( $title, '' );
  
 -              return $title->getLocalURL( $urlaction );
 +              return $title->getLinkURL( $urlaction );
        }
  
        /**
         * URL with the protocol specified.
         *
         * @param string $name Name of the Special page
 -       * @param string $urlaction Query to append
 +       * @param string|string[] $urlaction Query to append
         * @param string|null $proto Protocol to use or null for a local URL
         * @return string
         */
        /**
         * @param string $name
         * @param string $subpage
 -       * @param string $urlaction
 +       * @param string|string[] $urlaction
         * @return string
         */
        static function makeSpecialUrlSubpage( $name, $subpage, $urlaction = '' ) {
  
        /**
         * @param string $name
 -       * @param string $urlaction
 +       * @param string|string[] $urlaction
         * @return string
         */
        static function makeI18nUrl( $name, $urlaction = '' ) {
  
        /**
         * @param string $name
 -       * @param string $urlaction
 +       * @param string|string[] $urlaction
         * @return string
         */
        static function makeUrl( $name, $urlaction = '' ) {
        /**
         * this can be passed the NS number as defined in Language.php
         * @param string $name
 -       * @param string $urlaction
 +       * @param string|string[] $urlaction
         * @param int $namespace
         * @return string
         */
        /**
         * these return an array with the 'href' and boolean 'exists'
         * @param string $name
 -       * @param string $urlaction
 +       * @param string|string[] $urlaction
         * @return array
         */
        static function makeUrlDetails( $name, $urlaction = '' ) {
        /**
         * Make URL details where the article exists (or at least it's convenient to think so)
         * @param string $name Article name
 -       * @param string $urlaction
 +       * @param string|string[] $urlaction
         * @return array
         */
        static function makeKnownUrlDetails( $name, $urlaction = '' ) {
        /**
         * make sure we have some title to operate on
         *
 -       * @param Title $title
 +       * @param Title &$title
         * @param string $name
         */
        static function checkTitle( &$title, $name ) {
         *
         * BaseTemplate::getSidebar can be used to simplify the format and id generation in new skins.
         *
 -       * The format of the returned array is array( heading => content, ... ), where:
 +       * The format of the returned array is [ heading => content, ... ], where:
         * - heading is the heading of a navigation portlet. It is either:
         *   - magic string to be handled by the skins ('SEARCH' / 'LANGUAGES' / 'TOOLBOX' / ...)
         *   - a message name (e.g. 'navigation'), the message should be HTML-escaped by the skin
         *
         * @return array
         */
 -      function buildSidebar() {
 +      public function buildSidebar() {
                global $wgEnableSidebarCache, $wgSidebarCacheExpiry;
  
 -              $that = $this;
 -              $callback = function () use ( $that ) {
 +              $callback = function ( $old = null, &$ttl = null ) {
                        $bar = [];
 -                      $that->addToSidebar( $bar, 'sidebar' );
 -                      Hooks::run( 'SkinBuildSidebar', [ $that, &$bar ] );
 +                      $this->addToSidebar( $bar, 'sidebar' );
 +                      Hooks::run( 'SkinBuildSidebar', [ $this, &$bar ] );
 +                      if ( MessageCache::singleton()->isDisabled() ) {
 +                              $ttl = WANObjectCache::TTL_UNCACHEABLE; // bug T133069
 +                      }
  
                        return $bar;
                };
  
 -              if ( $wgEnableSidebarCache ) {
 -                      $cache = ObjectCache::getMainWANInstance();
 -                      $sidebar = $cache->getWithSetCallback(
 -                              $cache->makeKey( 'sidebar', $this->getLanguage()->getCode() ),
 -                              MessageCache::singleton()->isDisabled()
 -                                      ? $cache::TTL_UNCACHEABLE // bug T133069
 -                                      : $wgSidebarCacheExpiry,
 +              $msgCache = MessageCache::singleton();
 +              $wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
 +
 +              $sidebar = $wgEnableSidebarCache
 +                      ? $wanCache->getWithSetCallback(
 +                              $wanCache->makeKey( 'sidebar', $this->getLanguage()->getCode() ),
 +                              $wgSidebarCacheExpiry,
                                $callback,
 -                              [ 'lockTSE' => 30 ]
 -                      );
 -              } else {
 -                      $sidebar = $callback();
 -              }
 +                              [
 +                                      'checkKeys' => [
 +                                              // Unless there is both no exact $code override nor an i18n definition
 +                                              // in the software, the only MediaWiki page to check is for $code.
 +                                              $msgCache->getCheckKey( $this->getLanguage()->getCode() )
 +                                      ],
 +                                      'lockTSE' => 30
 +                              ]
 +                      )
 +                      : $callback();
  
                // Apply post-processing to the cached value
                Hooks::run( 'SidebarBeforeOutput', [ $this, &$sidebar ] );
         *
         * This is just a wrapper around addToSidebarPlain() for backwards compatibility
         *
 -       * @param array $bar
 +       * @param array &$bar
         * @param string $message
         */
        public function addToSidebar( &$bar, $message ) {
        /**
         * Add content from plain text
         * @since 1.17
 -       * @param array $bar
 +       * @param array &$bar
         * @param string $text
         * @return array
         */
                $lines = explode( "\n", $text );
  
                $heading = '';
 +              $messageTitle = $this->getConfig()->get( 'EnableSidebarCache' )
 +                      ? Title::newMainPage() : $this->getTitle();
  
                foreach ( $lines as $line ) {
                        if ( strpos( $line, '*' ) !== 0 ) {
                                $line = trim( $line, '* ' );
  
                                if ( strpos( $line, '|' ) !== false ) { // sanity check
 -                                      $line = MessageCache::singleton()->transform( $line, false, null, $this->getTitle() );
 +                                      $line = MessageCache::singleton()->transform( $line, false, null, $messageTitle );
                                        $line = array_map( 'trim', explode( '|', $line, 2 ) );
                                        if ( count( $line ) !== 2 ) {
                                                // Second sanity check, could be hit by people doing
 -                                              // funky stuff with parserfuncs... (bug 33321)
 +                                              // funky stuff with parserfuncs... (T35321)
                                                continue;
                                        }
  
                                        $extraAttribs = [];
  
 -                                      $msgLink = $this->msg( $line[0] )->inContentLanguage();
 +                                      $msgLink = $this->msg( $line[0] )->title( $messageTitle )->inContentLanguage();
                                        if ( $msgLink->exists() ) {
                                                $link = $msgLink->text();
                                                if ( $link == '-' ) {
                                        } else {
                                                $link = $line[0];
                                        }
 -                                      $msgText = $this->msg( $line[1] );
 +                                      $msgText = $this->msg( $line[1] )->title( $messageTitle );
                                        if ( $msgText->exists() ) {
                                                $text = $msgText->text();
                                        } else {
                                        $bar[$heading][] = array_merge( [
                                                'text' => $text,
                                                'href' => $href,
 -                                              'id' => 'n-' . Sanitizer::escapeId( strtr( $line[1], ' ', '-' ), 'noninitial' ),
 -                                              'active' => false
 +                                              'id' => Sanitizer::escapeIdForAttribute( 'n-' . strtr( $line[1], ' ', '-' ) ),
 +                                              'active' => false,
                                        ], $extraAttribs );
                                } else {
                                        continue;
         * @return string
         */
        function getNewtalks() {
 -
                $newMessagesAlert = '';
                $user = $this->getUser();
                $newtalks = $user->getNewMessageLinks();
  
                if ( count( $newtalks ) == 1 && $newtalks[0]['wiki'] === wfWikiID() ) {
                        $uTalkTitle = $user->getTalkPage();
 -                      $lastSeenRev = isset( $newtalks[0]['rev'] ) ? $newtalks[0]['rev'] : null;
 +                      $lastSeenRev = $newtalks[0]['rev'] ?? null;
                        $nofAuthors = 0;
                        if ( $lastSeenRev !== null ) {
                                $plural = true; // Default if we have a last seen revision: if unknown, use plural
                                $uTalkTitle,
                                $this->msg( 'newmessageslinkplural' )->params( $plural )->escaped(),
                                [],
-                               [ 'redirect' => 'no' ]
+                               $uTalkTitle->isRedirect() ? [ 'redirect' => 'no' ] : []
                        );
  
                        $newMessagesDiffLink = Linker::linkKnown(
         *   should fall back to the next notice in its sequence
         */
        private function getCachedNotice( $name ) {
 -              global $wgRenderHashAppend, $parserMemc, $wgContLang;
 +              global $wgRenderHashAppend;
  
                $needParse = false;
  
                        $notice = $msg->plain();
                }
  
 -              // Use the extra hash appender to let eg SSL variants separately cache.
 -              $key = wfMemcKey( $name . $wgRenderHashAppend );
 -              $cachedNotice = $parserMemc->get( $key );
 -              if ( is_array( $cachedNotice ) ) {
 -                      if ( md5( $notice ) == $cachedNotice['hash'] ) {
 -                              $notice = $cachedNotice['html'];
 -                      } else {
 -                              $needParse = true;
 +              $services = MediaWikiServices::getInstance();
 +              $cache = $services->getMainWANObjectCache();
 +              $parsed = $cache->getWithSetCallback(
 +                      // Use the extra hash appender to let eg SSL variants separately cache
 +                      // Key is verified with md5 hash of unparsed wikitext
 +                      $cache->makeKey( $name, $wgRenderHashAppend, md5( $notice ) ),
 +                      // TTL in seconds
 +                      600,
 +                      function () use ( $notice ) {
 +                              return $this->getOutput()->parse( $notice );
                        }
 -              } else {
 -                      $needParse = true;
 -              }
 -
 -              if ( $needParse ) {
 -                      $parsed = $this->getOutput()->parse( $notice );
 -                      $parserMemc->set( $key, [ 'html' => $parsed, 'hash' => md5( $notice ) ], 600 );
 -                      $notice = $parsed;
 -              }
 +              );
  
 -              $notice = Html::rawElement( 'div', [ 'id' => 'localNotice',
 -                      'lang' => $wgContLang->getHtmlCode(), 'dir' => $wgContLang->getDir() ], $notice );
 -              return $notice;
 +              $contLang = $services->getContentLanguage();
 +              return Html::rawElement(
 +                      'div',
 +                      [
 +                              'id' => 'localNotice',
 +                              'lang' => $contLang->getHtmlCode(),
 +                              'dir' => $contLang->getDir()
 +                      ],
 +                      $parsed
 +              );
        }
  
        /**
        }
  
        /**
 -       * Create a section edit link.  This supersedes editSectionLink() and
 -       * editSectionLinkForOther().
 +       * Create a section edit link.
         *
         * @param Title $nt The title being linked to (may not be the same as
         *   the current page, if the section is included from a template)
         * @param string $section The designation of the section being pointed to,
         *   to be included in the link, like "&section=$section"
 -       * @param string $tooltip The tooltip to use for the link: will be escaped
 +       * @param string|null $tooltip The tooltip to use for the link: will be escaped
         *   and wrapped in the 'editsectionhint' message
         * @param string $lang Language code
         * @return string HTML to use for edit link
  
                $attribs = [];
                if ( !is_null( $tooltip ) ) {
 -                      # Bug 25462: undo double-escaping.
 -                      $tooltip = Sanitizer::decodeCharReferences( $tooltip );
                        $attribs['title'] = wfMessage( 'editsectionhint' )->rawParams( $tooltip )
                                ->inLanguage( $lang )->text();
                }
  
                $result .= implode(
                        '<span class="mw-editsection-divider">'
 -                              . wfMessage( 'pipe-separator' )->inLanguage( $lang )->text()
 +                              . wfMessage( 'pipe-separator' )->inLanguage( $lang )->escaped()
                                . '</span>',
                        $linksHtml
                );
                return $result;
        }
  
 -      /** @deprecated in 1.21 */
 -      public function commentBlock( $comment, $title = null, $local = false, $wikiId = null ) {
 -              wfDeprecated( __METHOD__, '1.21' );
 -              return Linker::commentBlock( $comment, $title, $local, $wikiId );
 -      }
 -
 -      /** @deprecated in 1.21 */
 -      public function generateRollback(
 -              $rev,
 -              IContextSource $context = null,
 -              $options = [ 'verify' ]
 -      ) {
 -              wfDeprecated( __METHOD__, '1.21' );
 -              return Linker::generateRollback( $rev, $context, $options );
 -      }
 -
 -      /** @deprecated in 1.21 */
 -      public function link( $target, $html = null, $customAttribs = [], $query = [], $options = [] ) {
 -              wfDeprecated( __METHOD__, '1.21' );
 -              return Linker::link( $target, $html, $customAttribs, $query, $options );
 -      }
 -
 -      /** @deprecated in 1.21 */
 -      public function linkKnown(
 -              $target,
 -              $html = null,
 -              $customAttribs = [],
 -              $query = [],
 -              $options = [ 'known', 'noclasses' ]
 -      ) {
 -              wfDeprecated( __METHOD__, '1.21' );
 -              return Linker::linkKnown( $target, $html, $customAttribs, $query, $options );
 -      }
 -
 -      /** @deprecated in 1.21 */
 -      public function userLink( $userId, $userName, $altUserName = false ) {
 -              wfDeprecated( __METHOD__, '1.21' );
 -              return Linker::userLink( $userId, $userName, $altUserName );
 -      }
 -
 -      /** @deprecated in 1.21 */
 -      public function userToolLinks(
 -              $userId,
 -              $userText,
 -              $redContribsWhenNoEdits = false,
 -              $flags = 0,
 -              $edits = null
 -      ) {
 -              wfDeprecated( __METHOD__, '1.21' );
 -              return Linker::userToolLinks( $userId, $userText, $redContribsWhenNoEdits, $flags, $edits );
 -      }
 -
  }