Merge "Add LinkRenderer (rewrite of Linker::link())"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 24 May 2016 03:29:32 +0000 (03:29 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 24 May 2016 03:29:32 +0000 (03:29 +0000)
1  2 
autoload.php
docs/hooks.txt
includes/Linker.php
includes/MediaWikiServices.php

diff --combined autoload.php
@@@ -557,6 -557,7 +557,7 @@@ $wgAutoloadLocalClasses = 
        'HistoryPager' => __DIR__ . '/includes/actions/HistoryAction.php',
        'Hooks' => __DIR__ . '/includes/Hooks.php',
        'Html' => __DIR__ . '/includes/Html.php',
+       'HtmlArmor' => __DIR__ . '/includes/libs/HtmlArmor.php',
        'HtmlFormatter' => __DIR__ . '/includes/HtmlFormatter.php',
        'Http' => __DIR__ . '/includes/HttpFunctions.php',
        'HttpError' => __DIR__ . '/includes/exception/HttpError.php',
        'MediaWikiSite' => __DIR__ . '/includes/site/MediaWikiSite.php',
        'MediaWikiTitleCodec' => __DIR__ . '/includes/title/MediaWikiTitleCodec.php',
        'MediaWikiVersionFetcher' => __DIR__ . '/includes/MediaWikiVersionFetcher.php',
 -      'MediaWiki\\Interwiki\\ClassicInterwikiLookup' => __DIR__ . '/includes/interwiki/ClassicInterwikiLookup.php',
 -      'MediaWiki\\Interwiki\\InterwikiLookup' => __DIR__ . '/includes/interwiki/InterwikiLookup.php',
        'MediaWiki\\Auth\\AbstractAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractAuthenticationProvider.php',
        'MediaWiki\\Auth\\AbstractPasswordPrimaryAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php',
        'MediaWiki\\Auth\\AbstractPreAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractPreAuthenticationProvider.php',
        'MediaWiki\\Auth\\UserDataAuthenticationRequest' => __DIR__ . '/includes/auth/UserDataAuthenticationRequest.php',
        'MediaWiki\\Auth\\UsernameAuthenticationRequest' => __DIR__ . '/includes/auth/UsernameAuthenticationRequest.php',
        'MediaWiki\\Diff\\WordAccumulator' => __DIR__ . '/includes/diff/WordAccumulator.php',
 +      'MediaWiki\\Interwiki\\ClassicInterwikiLookup' => __DIR__ . '/includes/interwiki/ClassicInterwikiLookup.php',
 +      'MediaWiki\\Interwiki\\InterwikiLookup' => __DIR__ . '/includes/interwiki/InterwikiLookup.php',
        'MediaWiki\\Languages\\Data\\Names' => __DIR__ . '/languages/data/Names.php',
        'MediaWiki\\Languages\\Data\\ZhConversion' => __DIR__ . '/languages/data/ZhConversion.php',
+       'MediaWiki\\Linker\\LinkRenderer' => __DIR__ . '/includes/linker/LinkRenderer.php',
+       'MediaWiki\\Linker\\LinkRendererFactory' => __DIR__ . '/includes/linker/LinkRendererFactory.php',
        'MediaWiki\\Linker\\LinkTarget' => __DIR__ . '/includes/linker/LinkTarget.php',
        'MediaWiki\\Logger\\LegacyLogger' => __DIR__ . '/includes/debug/logger/LegacyLogger.php',
        'MediaWiki\\Logger\\LegacySpi' => __DIR__ . '/includes/debug/logger/LegacySpi.php',
        'MediaWiki\\Services\\CannotReplaceActiveServiceException' => __DIR__ . '/includes/Services/CannotReplaceActiveServiceException.php',
        'MediaWiki\\Services\\ContainerDisabledException' => __DIR__ . '/includes/Services/ContainerDisabledException.php',
        'MediaWiki\\Services\\DestructibleService' => __DIR__ . '/includes/Services/DestructibleService.php',
 -      'MediaWiki\\Services\\SalvageableService' => __DIR__ . '/includes/Services/SalvageableService.php',
        'MediaWiki\\Services\\NoSuchServiceException' => __DIR__ . '/includes/Services/NoSuchServiceException.php',
 +      'MediaWiki\\Services\\SalvageableService' => __DIR__ . '/includes/Services/SalvageableService.php',
        'MediaWiki\\Services\\ServiceAlreadyDefinedException' => __DIR__ . '/includes/Services/ServiceAlreadyDefinedException.php',
        'MediaWiki\\Services\\ServiceContainer' => __DIR__ . '/includes/Services/ServiceContainer.php',
        'MediaWiki\\Services\\ServiceDisabledException' => __DIR__ . '/includes/Services/ServiceDisabledException.php',
diff --combined docs/hooks.txt
@@@ -1783,7 -1783,8 +1783,8 @@@ $title: The page's Title
  $out: The output page.
  $cssClassName: CSS class name of the language selector.
  
- 'LinkBegin': Used when generating internal and interwiki links in
+ 'LinkBegin': DEPRECATED! Use HtmlPageLinkRendererBegin instead.
+ Used when generating internal and interwiki links in
  Linker::link(), before processing starts.  Return false to skip default
  processing and return $ret. See documentation for Linker::link() for details on
  the expected meanings of parameters.
@@@ -1800,7 -1801,8 +1801,8 @@@ $target: the Title that the link is poi
  &$options: array of options.  Can include 'known', 'broken', 'noclasses'.
  &$ret: the value to return if your hook returns false.
  
- 'LinkEnd': Used when generating internal and interwiki links in Linker::link(),
+ 'LinkEnd': DEPRECATED! Use HtmlPageLinkRendererEnd hook instead
+ Used when generating internal and interwiki links in Linker::link(),
  just before the function returns a value.  If you return true, an <a> element
  with HTML attributes $attribs and contents $html will be returned.  If you
  return false, $ret will be returned.
@@@ -1835,6 -1837,35 +1837,35 @@@ $file: the File object or false if brok
  &$attribs: the attributes to be applied
  &$ret: the value to return if your hook returns false
  
+ 'LinkRendererBegin':
+ Used when generating internal and interwiki links in
+ LinkRenderer, before processing starts.  Return false to skip default
+ processing and return $ret.
+ $linkRenderer: the LinkRenderer object
+ $target: the LinkTarget that the link is pointing to
+ &$html: the contents that the <a> tag should have (raw HTML); null means
+   "default".
+ &$customAttribs: the HTML attributes that the <a> tag should have, in
+   associative array form, with keys and values unescaped.  Should be merged
+   with default values, with a value of false meaning to suppress the
+   attribute.
+ &$query: the query string to add to the generated URL (the bit after the "?"),
+   in associative array form, with keys and values unescaped.
+ &$ret: the value to return if your hook returns false.
+ 'LinkRendererEnd':
+ Used when generating internal and interwiki links in LinkRenderer,
+ just before the function returns a value.  If you return true, an <a> element
+ with HTML attributes $attribs and contents $html will be returned.  If you
+ return false, $ret will be returned.
+ $linkRenderer: the LinkRenderer object
+ $target: the LinkTarget object that the link is pointing to
+ $isKnown: boolean indicating whether the page is known or not
+ &$html: the final (raw HTML) contents of the <a> tag, after processing.
+ &$attribs: the final HTML attributes of the <a> tag, after processing, in
+   associative array form.
+ &$ret: the value to return if your hook returns false.
  'LinksUpdate': At the beginning of LinksUpdate::doUpdate() just before the
  actual update.
  &$linksUpdate: the LinksUpdate object
@@@ -2494,12 -2525,6 +2525,12 @@@ $context: (IContextSource) The RequestC
  &$skin: A variable reference you may set a Skin instance or string key on to
    override the skin that will be used for the context.
  
 +'RequestHasSameOriginSecurity': Called to determine if the request is somehow
 +flagged to lack same-origin security. Return false to indicate the lack. Note
 +if the "somehow" involves HTTP headers, you'll probably need to make sure
 +the header is varied on.
 +WebRequest $request: The request.
 +
  'ResetPasswordExpiration': Allow extensions to set a default password expiration
  $user: The user having their password expiration reset
  &$newExpire: The new expiration date
diff --combined includes/Linker.php
@@@ -20,6 -20,7 +20,7 @@@
   * @file
   */
  use MediaWiki\Linker\LinkTarget;
+ use MediaWiki\MediaWikiServices;
  
  /**
   * Some internal bits split of from Skin.php. These functions are used
@@@ -210,55 -211,33 +211,33 @@@ class Linker 
                        wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' );
                        $query = wfCgiToArray( $query );
                }
-               $options = (array)$options;
-               $dummy = new DummyLinker; // dummy linker instance for bc on the hooks
-               $ret = null;
-               if ( !Hooks::run( 'LinkBegin',
-                       [ $dummy, $target, &$html, &$customAttribs, &$query, &$options, &$ret ] )
-               ) {
-                       return $ret;
-               }
-               # Normalize the Title if it's a special page
-               $target = self::normaliseSpecialPage( $target );
  
-               # If we don't know whether the page exists, let's find out.
-               if ( !in_array( 'known', $options, true ) && !in_array( 'broken', $options, true ) ) {
-                       if ( $target->isKnown() ) {
-                               $options[] = 'known';
-                       } else {
-                               $options[] = 'broken';
+               $services = MediaWikiServices::getInstance();
+               $options = (array)$options;
+               if ( $options ) {
+                       // Custom options, create new LinkRenderer
+                       if ( !isset( $options['stubThreshold'] ) ) {
+                               global $wgUser;
+                               $options['stubThreshold'] = $wgUser->getStubThreshold();
                        }
+                       $linkRenderer = $services->getLinkRendererFactory()
+                               ->createFromLegacyOptions( $options );
+               } else {
+                       $linkRenderer = $services->getLinkRenderer();
                }
  
-               $oldquery = [];
-               if ( in_array( "forcearticlepath", $options, true ) && $query ) {
-                       $oldquery = $query;
-                       $query = [];
-               }
-               # Note: we want the href attribute first, for prettiness.
-               $attribs = [ 'href' => self::linkUrl( $target, $query, $options ) ];
-               if ( in_array( 'forcearticlepath', $options, true ) && $oldquery ) {
-                       $attribs['href'] = wfAppendQuery( $attribs['href'], $oldquery );
-               }
-               $attribs = array_merge(
-                       $attribs,
-                       self::linkAttribs( $target, $customAttribs, $options )
-               );
-               if ( is_null( $html ) ) {
-                       $html = self::linkText( $target );
+               if ( $html !== null ) {
+                       $text = new HtmlArmor( $html );
+               } else {
+                       $text = $html; // null
                }
-               $ret = null;
-               if ( Hooks::run( 'LinkEnd', [ $dummy, $target, $options, &$html, &$attribs, &$ret ] ) ) {
-                       $ret = Html::rawElement( 'a', $attribs, $html );
+               if ( in_array( 'known', $options, true ) ) {
+                       return $linkRenderer->makeKnownLink( $target, $text, $customAttribs, $query );
+               } elseif ( in_array( 'broken', $options, true ) ) {
+                       return $linkRenderer->makeBrokenLink( $target, $text, $customAttribs, $query );
+               } else {
+                       return $linkRenderer->makeLink( $target, $text, $customAttribs, $query );
                }
-               return $ret;
        }
  
        /**
                return self::link( $target, $html, $customAttribs, $query, $options );
        }
  
-       /**
-        * Returns the Url used to link to a Title
-        *
-        * @param LinkTarget $target
-        * @param array $query Query parameters
-        * @param array $options
-        * @return string
-        */
-       private static function linkUrl( LinkTarget $target, $query, $options ) {
-               # We don't want to include fragments for broken links, because they
-               # generally make no sense.
-               if ( in_array( 'broken', $options, true ) && $target->hasFragment() ) {
-                       $target = $target->createFragmentTarget( '' );
-               }
-               # If it's a broken link, add the appropriate query pieces, unless
-               # there's already an action specified, or unless 'edit' makes no sense
-               # (i.e., for a nonexistent special page).
-               if ( in_array( 'broken', $options, true ) && empty( $query['action'] )
-                       && $target->getNamespace() !== NS_SPECIAL ) {
-                       $query['action'] = 'edit';
-                       $query['redlink'] = '1';
-               }
-               if ( in_array( 'http', $options, true ) ) {
-                       $proto = PROTO_HTTP;
-               } elseif ( in_array( 'https', $options, true ) ) {
-                       $proto = PROTO_HTTPS;
-               } else {
-                       $proto = PROTO_RELATIVE;
-               }
-               $title = Title::newFromLinkTarget( $target );
-               $ret = $title->getLinkURL( $query, false, $proto );
-               return $ret;
-       }
-       /**
-        * Returns the array of attributes used when linking to the Title $target
-        *
-        * @param Title $target
-        * @param array $attribs
-        * @param array $options
-        *
-        * @return array
-        */
-       private static function linkAttribs( $target, $attribs, $options ) {
-               global $wgUser;
-               $defaults = [];
-               if ( !in_array( 'noclasses', $options, true ) ) {
-                       # Now build the classes.
-                       $classes = [];
-                       if ( in_array( 'broken', $options, true ) ) {
-                               $classes[] = 'new';
-                       }
-                       if ( $target->isExternal() ) {
-                               $classes[] = 'extiw';
-                       }
-                       if ( !in_array( 'broken', $options, true ) ) { # Avoid useless calls to LinkCache (see r50387)
-                               $colour = self::getLinkColour(
-                                       $target,
-                                       isset( $options['stubThreshold'] ) ? $options['stubThreshold'] : $wgUser->getStubThreshold()
-                               );
-                               if ( $colour !== '' ) {
-                                       $classes[] = $colour; # mw-redirect or stub
-                               }
-                       }
-                       if ( $classes != [] ) {
-                               $defaults['class'] = implode( ' ', $classes );
-                       }
-               }
-               # Get a default title attribute.
-               if ( $target->getPrefixedText() == '' ) {
-                       # A link like [[#Foo]].  This used to mean an empty title
-                       # attribute, but that's silly.  Just don't output a title.
-               } elseif ( in_array( 'known', $options, true ) ) {
-                       $defaults['title'] = $target->getPrefixedText();
-               } else {
-                       // This ends up in parser cache!
-                       $defaults['title'] = wfMessage( 'red-link-title', $target->getPrefixedText() )
-                               ->inContentLanguage()
-                               ->text();
-               }
-               # Finally, merge the custom attribs with the default ones, and iterate
-               # over that, deleting all "false" attributes.
-               $ret = [];
-               $merged = Sanitizer::mergeAttributes( $defaults, $attribs );
-               foreach ( $merged as $key => $val ) {
-                       # A false value suppresses the attribute, and we don't want the
-                       # href attribute to be overridden.
-                       if ( $key != 'href' && $val !== false ) {
-                               $ret[$key] = $val;
-                       }
-               }
-               return $ret;
-       }
-       /**
-        * Default text of the links to the Title $target
-        *
-        * @param Title $target
-        *
-        * @return string
-        */
-       private static function linkText( $target ) {
-               if ( !$target instanceof Title ) {
-                       wfWarn( __METHOD__ . ': Requires $target to be a Title object.' );
-                       return '';
-               }
-               // If the target is just a fragment, with no title, we return the fragment
-               // text.  Otherwise, we return the title text itself.
-               if ( $target->getPrefixedText() === '' && $target->hasFragment() ) {
-                       return htmlspecialchars( $target->getFragment() );
-               }
-               return htmlspecialchars( $target->getPrefixedText() );
-       }
        /**
         * Make appropriate markup for a link to the current article. This is
         * currently rendered as the bold link text. The calling sequence is the
                if ( !$title ) {
                        $title = $wgTitle;
                }
 -              $attribs['rel'] = Parser::getExternalLinkRel( $url, $title );
 +              $newRel = Parser::getExternalLinkRel( $url, $title );
 +              if ( !isset( $attribs['rel'] ) || $attribs['rel'] === '' ) {
 +                      $attribs['rel'] = $newRel;
 +              } elseif ( $newRel !== '' ) {
 +                      // Merge the rel attributes.
 +                      $newRels = explode( ' ', $newRel );
 +                      $oldRels = explode( ' ', $attribs['rel'] );
 +                      $combined = array_unique( array_merge( $newRels, $oldRels ) );
 +                      $attribs['rel'] = implode( ' ', $combined );
 +              }
                $link = '';
                $success = Hooks::run( 'LinkerMakeExternalLink',
                        [ &$url, &$text, &$link, &$attribs, $linktype ] );
         * work if $wgShowRollbackEditCount is disabled, so this can only function
         * as an additional check.
         *
 -       * If the option noBrackets is set the rollback link wont be enclosed in []
 +       * If the option noBrackets is set the rollback link wont be enclosed in "[]".
 +       *
 +       * See the "mediawiki.page.rollback" module for the client-side handling of this link.
         *
         * @since 1.16.3. $context added in 1.20. $options added in 1.21
         *
                        $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
                }
  
 +              $context->getOutput()->addModules( 'mediawiki.page.rollback' );
 +
                return '<span class="mw-rollback-link">' . $inner . '</span>';
        }
  
                $query = [
                        'action' => 'rollback',
                        'from' => $rev->getUserText(),
 -                      'token' => $context->getUser()->getEditToken( [
 -                              $title->getPrefixedText(),
 -                              $rev->getUserText()
 -                      ] ),
                ];
 +              $attrs = [
 +                      'data-mw' => 'interface',
 +                      'title' => $context->msg( 'tooltip-rollback' )->text(),
 +              ];
 +              $options = [ 'known', 'noclasses' ];
 +
                if ( $context->getRequest()->getBool( 'bot' ) ) {
                        $query['bot'] = '1';
                        $query['hidediff'] = '1'; // bug 15999
                        }
  
                        if ( $editCount > $wgShowRollbackEditCount ) {
 -                              $editCount_output = $context->msg( 'rollbacklinkcount-morethan' )
 +                              $html = $context->msg( 'rollbacklinkcount-morethan' )
                                        ->numParams( $wgShowRollbackEditCount )->parse();
                        } else {
 -                              $editCount_output = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse();
 +                              $html = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse();
                        }
  
 -                      return self::link(
 -                              $title,
 -                              $editCount_output,
 -                              [ 'title' => $context->msg( 'tooltip-rollback' )->text() ],
 -                              $query,
 -                              [ 'known', 'noclasses' ]
 -                      );
 +                      return self::link( $title, $html, $attrs, $query, $options );
                } else {
 -                      return self::link(
 -                              $title,
 -                              $context->msg( 'rollbacklink' )->escaped(),
 -                              [ 'title' => $context->msg( 'tooltip-rollback' )->text() ],
 -                              $query,
 -                              [ 'known', 'noclasses' ]
 -                      );
 +                      $html = $context->msg( 'rollbacklink' )->escaped();
 +                      return self::link( $title, $html, $attrs, $query, $options );
                }
        }
  
@@@ -11,10 -11,13 +11,12 @@@ use LBFactory
  use LinkCache;
  use Liuggio\StatsdClient\Factory\StatsdDataFactory;
  use LoadBalancer;
+ use MediaWiki\Linker\LinkRenderer;
+ use MediaWiki\Linker\LinkRendererFactory;
  use MediaWiki\Services\SalvageableService;
  use MediaWiki\Services\ServiceContainer;
  use MWException;
  use ObjectCache;
 -use ResourceLoader;
  use SearchEngine;
  use SearchEngineConfig;
  use SearchEngineFactory;
@@@ -516,6 -519,25 +518,25 @@@ class MediaWikiServices extends Service
                return $this->getService( 'LinkCache' );
        }
  
+       /**
+        * @since 1.28
+        * @return LinkRendererFactory
+        */
+       public function getLinkRendererFactory() {
+               return $this->getService( 'LinkRendererFactory' );
+       }
+       /**
+        * LinkRenderer instance that can be used
+        * if no custom options are needed
+        *
+        * @since 1.28
+        * @return LinkRenderer
+        */
+       public function getLinkRenderer() {
+               return $this->getService( 'LinkRenderer' );
+       }
        /**
         * @since 1.28
         * @return TitleFormatter