[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/ -> Linker.php (source)

   1  <?php
   2  /**
   3   * Methods to make links and related items.
   4   *
   5   * This program is free software; you can redistribute it and/or modify
   6   * it under the terms of the GNU General Public License as published by
   7   * the Free Software Foundation; either version 2 of the License, or
   8   * (at your option) any later version.
   9   *
  10   * This program is distributed in the hope that it will be useful,
  11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13   * GNU General Public License for more details.
  14   *
  15   * You should have received a copy of the GNU General Public License along
  16   * with this program; if not, write to the Free Software Foundation, Inc.,
  17   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18   * http://www.gnu.org/copyleft/gpl.html
  19   *
  20   * @file
  21   */
  22  
  23  /**
  24   * Some internal bits split of from Skin.php. These functions are used
  25   * for primarily page content: links, embedded images, table of contents. Links
  26   * are also used in the skin.
  27   *
  28   * @todo turn this into a legacy interface for HtmlPageLinkRenderer and similar services.
  29   *
  30   * @ingroup Skins
  31   */
  32  class Linker {
  33      /**
  34       * Flags for userToolLinks()
  35       */
  36      const TOOL_LINKS_NOBLOCK = 1;
  37      const TOOL_LINKS_EMAIL = 2;
  38  
  39      /**
  40       * Get the appropriate HTML attributes to add to the "a" element of an
  41       * external link, as created by [wikisyntax].
  42       *
  43       * @param string $class The contents of the class attribute; if an empty
  44       *   string is passed, which is the default value, defaults to 'external'.
  45       * @return string
  46       * @deprecated since 1.18 Just pass the external class directly to something
  47       *   using Html::expandAttributes.
  48       */
  49  	static function getExternalLinkAttributes( $class = 'external' ) {
  50          wfDeprecated( __METHOD__, '1.18' );
  51          return self::getLinkAttributesInternal( '', $class );
  52      }
  53  
  54      /**
  55       * Get the appropriate HTML attributes to add to the "a" element of an interwiki link.
  56       *
  57       * @param string $title The title text for the link, URL-encoded (???) but
  58       *   not HTML-escaped
  59       * @param string $unused Unused
  60       * @param string $class The contents of the class attribute; if an empty
  61       *   string is passed, which is the default value, defaults to 'external'.
  62       * @return string
  63       */
  64  	static function getInterwikiLinkAttributes( $title, $unused = null, $class = 'external' ) {
  65          global $wgContLang;
  66  
  67          # @todo FIXME: We have a whole bunch of handling here that doesn't happen in
  68          # getExternalLinkAttributes, why?
  69          $title = urldecode( $title );
  70          $title = $wgContLang->checkTitleEncoding( $title );
  71          $title = preg_replace( '/[\\x00-\\x1f]/', ' ', $title );
  72  
  73          return self::getLinkAttributesInternal( $title, $class );
  74      }
  75  
  76      /**
  77       * Get the appropriate HTML attributes to add to the "a" element of an internal link.
  78       *
  79       * @param string $title The title text for the link, URL-encoded (???) but
  80       *   not HTML-escaped
  81       * @param string $unused Unused
  82       * @param string $class The contents of the class attribute, default none
  83       * @return string
  84       */
  85  	static function getInternalLinkAttributes( $title, $unused = null, $class = '' ) {
  86          $title = urldecode( $title );
  87          $title = str_replace( '_', ' ', $title );
  88          return self::getLinkAttributesInternal( $title, $class );
  89      }
  90  
  91      /**
  92       * Get the appropriate HTML attributes to add to the "a" element of an internal
  93       * link, given the Title object for the page we want to link to.
  94       *
  95       * @param Title $nt
  96       * @param string $unused Unused
  97       * @param string $class The contents of the class attribute, default none
  98       * @param string|bool $title Optional (unescaped) string to use in the title
  99       *   attribute; if false, default to the name of the page we're linking to
 100       * @return string
 101       */
 102  	static function getInternalLinkAttributesObj( $nt, $unused = null, $class = '', $title = false ) {
 103          if ( $title === false ) {
 104              $title = $nt->getPrefixedText();
 105          }
 106          return self::getLinkAttributesInternal( $title, $class );
 107      }
 108  
 109      /**
 110       * Common code for getLinkAttributesX functions
 111       *
 112       * @param string $title
 113       * @param string $class
 114       *
 115       * @return string
 116       */
 117  	private static function getLinkAttributesInternal( $title, $class ) {
 118          $title = htmlspecialchars( $title );
 119          $class = htmlspecialchars( $class );
 120          $r = '';
 121          if ( $class != '' ) {
 122              $r .= " class=\"$class\"";
 123          }
 124          if ( $title != '' ) {
 125              $r .= " title=\"$title\"";
 126          }
 127          return $r;
 128      }
 129  
 130      /**
 131       * Return the CSS colour of a known link
 132       *
 133       * @param Title $t
 134       * @param int $threshold User defined threshold
 135       * @return string CSS class
 136       */
 137  	public static function getLinkColour( $t, $threshold ) {
 138          $colour = '';
 139          if ( $t->isRedirect() ) {
 140              # Page is a redirect
 141              $colour = 'mw-redirect';
 142          } elseif ( $threshold > 0 && $t->isContentPage() &&
 143              $t->exists() && $t->getLength() < $threshold
 144          ) {
 145              # Page is a stub
 146              $colour = 'stub';
 147          }
 148          return $colour;
 149      }
 150  
 151      /**
 152       * This function returns an HTML link to the given target.  It serves a few
 153       * purposes:
 154       *   1) If $target is a Title, the correct URL to link to will be figured
 155       *      out automatically.
 156       *   2) It automatically adds the usual classes for various types of link
 157       *      targets: "new" for red links, "stub" for short articles, etc.
 158       *   3) It escapes all attribute values safely so there's no risk of XSS.
 159       *   4) It provides a default tooltip if the target is a Title (the page
 160       *      name of the target).
 161       * link() replaces the old functions in the makeLink() family.
 162       *
 163       * @since 1.18 Method exists since 1.16 as non-static, made static in 1.18.
 164       *
 165       * @param Title $target Can currently only be a Title, but this may
 166       *   change to support Images, literal URLs, etc.
 167       * @param string $html The HTML contents of the <a> element, i.e.,
 168       *   the link text.  This is raw HTML and will not be escaped.  If null,
 169       *   defaults to the prefixed text of the Title; or if the Title is just a
 170       *   fragment, the contents of the fragment.
 171       * @param array $customAttribs A key => value array of extra HTML attributes,
 172       *   such as title and class.  (href is ignored.)  Classes will be
 173       *   merged with the default classes, while other attributes will replace
 174       *   default attributes.  All passed attribute values will be HTML-escaped.
 175       *   A false attribute value means to suppress that attribute.
 176       * @param array $query The query string to append to the URL
 177       *   you're linking to, in key => value array form.  Query keys and values
 178       *   will be URL-encoded.
 179       * @param string|array $options String or array of strings:
 180       *     'known': Page is known to exist, so don't check if it does.
 181       *     'broken': Page is known not to exist, so don't check if it does.
 182       *     'noclasses': Don't add any classes automatically (includes "new",
 183       *       "stub", "mw-redirect", "extiw").  Only use the class attribute
 184       *       provided, if any, so you get a simple blue link with no funny i-
 185       *       cons.
 186       *     'forcearticlepath': Use the article path always, even with a querystring.
 187       *       Has compatibility issues on some setups, so avoid wherever possible.
 188       *     'http': Force a full URL with http:// as the scheme.
 189       *     'https': Force a full URL with https:// as the scheme.
 190       * @return string HTML <a> attribute
 191       */
 192  	public static function link(
 193          $target, $html = null, $customAttribs = array(), $query = array(), $options = array()
 194      ) {
 195          if ( !$target instanceof Title ) {
 196              wfWarn( __METHOD__ . ': Requires $target to be a Title object.' );
 197              return "<!-- ERROR -->$html";
 198          }
 199          wfProfileIn( __METHOD__ );
 200  
 201          if ( is_string( $query ) ) {
 202              // some functions withing core using this still hand over query strings
 203              wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' );
 204              $query = wfCgiToArray( $query );
 205          }
 206          $options = (array)$options;
 207  
 208          $dummy = new DummyLinker; // dummy linker instance for bc on the hooks
 209  
 210          $ret = null;
 211          if ( !wfRunHooks( 'LinkBegin', array( $dummy, $target, &$html,
 212          &$customAttribs, &$query, &$options, &$ret ) ) ) {
 213              wfProfileOut( __METHOD__ );
 214              return $ret;
 215          }
 216  
 217          # Normalize the Title if it's a special page
 218          $target = self::normaliseSpecialPage( $target );
 219  
 220          # If we don't know whether the page exists, let's find out.
 221          wfProfileIn( __METHOD__ . '-checkPageExistence' );
 222          if ( !in_array( 'known', $options ) and !in_array( 'broken', $options ) ) {
 223              if ( $target->isKnown() ) {
 224                  $options[] = 'known';
 225              } else {
 226                  $options[] = 'broken';
 227              }
 228          }
 229          wfProfileOut( __METHOD__ . '-checkPageExistence' );
 230  
 231          $oldquery = array();
 232          if ( in_array( "forcearticlepath", $options ) && $query ) {
 233              $oldquery = $query;
 234              $query = array();
 235          }
 236  
 237          # Note: we want the href attribute first, for prettiness.
 238          $attribs = array( 'href' => self::linkUrl( $target, $query, $options ) );
 239          if ( in_array( 'forcearticlepath', $options ) && $oldquery ) {
 240              $attribs['href'] = wfAppendQuery( $attribs['href'], $oldquery );
 241          }
 242  
 243          $attribs = array_merge(
 244              $attribs,
 245              self::linkAttribs( $target, $customAttribs, $options )
 246          );
 247          if ( is_null( $html ) ) {
 248              $html = self::linkText( $target );
 249          }
 250  
 251          $ret = null;
 252          if ( wfRunHooks( 'LinkEnd', array( $dummy, $target, $options, &$html, &$attribs, &$ret ) ) ) {
 253              $ret = Html::rawElement( 'a', $attribs, $html );
 254          }
 255  
 256          wfProfileOut( __METHOD__ );
 257          return $ret;
 258      }
 259  
 260      /**
 261       * Identical to link(), except $options defaults to 'known'.
 262       * @see Linker::link
 263       * @return string
 264       */
 265  	public static function linkKnown(
 266          $target, $html = null, $customAttribs = array(),
 267          $query = array(), $options = array( 'known', 'noclasses' )
 268      ) {
 269          return self::link( $target, $html, $customAttribs, $query, $options );
 270      }
 271  
 272      /**
 273       * Returns the Url used to link to a Title
 274       *
 275       * @param Title $target
 276       * @param array $query Query parameters
 277       * @param array $options
 278       * @return string
 279       */
 280  	private static function linkUrl( $target, $query, $options ) {
 281          wfProfileIn( __METHOD__ );
 282          # We don't want to include fragments for broken links, because they
 283          # generally make no sense.
 284          if ( in_array( 'broken', $options ) && $target->hasFragment() ) {
 285              $target = clone $target;
 286              $target->setFragment( '' );
 287          }
 288  
 289          # If it's a broken link, add the appropriate query pieces, unless
 290          # there's already an action specified, or unless 'edit' makes no sense
 291          # (i.e., for a nonexistent special page).
 292          if ( in_array( 'broken', $options ) && empty( $query['action'] )
 293              && !$target->isSpecialPage() ) {
 294              $query['action'] = 'edit';
 295              $query['redlink'] = '1';
 296          }
 297  
 298          if ( in_array( 'http', $options ) ) {
 299              $proto = PROTO_HTTP;
 300          } elseif ( in_array( 'https', $options ) ) {
 301              $proto = PROTO_HTTPS;
 302          } else {
 303              $proto = PROTO_RELATIVE;
 304          }
 305  
 306          $ret = $target->getLinkURL( $query, false, $proto );
 307          wfProfileOut( __METHOD__ );
 308          return $ret;
 309      }
 310  
 311      /**
 312       * Returns the array of attributes used when linking to the Title $target
 313       *
 314       * @param Title $target
 315       * @param array $attribs
 316       * @param array $options
 317       *
 318       * @return array
 319       */
 320  	private static function linkAttribs( $target, $attribs, $options ) {
 321          wfProfileIn( __METHOD__ );
 322          global $wgUser;
 323          $defaults = array();
 324  
 325          if ( !in_array( 'noclasses', $options ) ) {
 326              wfProfileIn( __METHOD__ . '-getClasses' );
 327              # Now build the classes.
 328              $classes = array();
 329  
 330              if ( in_array( 'broken', $options ) ) {
 331                  $classes[] = 'new';
 332              }
 333  
 334              if ( $target->isExternal() ) {
 335                  $classes[] = 'extiw';
 336              }
 337  
 338              if ( !in_array( 'broken', $options ) ) { # Avoid useless calls to LinkCache (see r50387)
 339                  $colour = self::getLinkColour( $target, $wgUser->getStubThreshold() );
 340                  if ( $colour !== '' ) {
 341                      $classes[] = $colour; # mw-redirect or stub
 342                  }
 343              }
 344              if ( $classes != array() ) {
 345                  $defaults['class'] = implode( ' ', $classes );
 346              }
 347              wfProfileOut( __METHOD__ . '-getClasses' );
 348          }
 349  
 350          # Get a default title attribute.
 351          if ( $target->getPrefixedText() == '' ) {
 352              # A link like [[#Foo]].  This used to mean an empty title
 353              # attribute, but that's silly.  Just don't output a title.
 354          } elseif ( in_array( 'known', $options ) ) {
 355              $defaults['title'] = $target->getPrefixedText();
 356          } else {
 357              $defaults['title'] = wfMessage( 'red-link-title', $target->getPrefixedText() )->text();
 358          }
 359  
 360          # Finally, merge the custom attribs with the default ones, and iterate
 361          # over that, deleting all "false" attributes.
 362          $ret = array();
 363          $merged = Sanitizer::mergeAttributes( $defaults, $attribs );
 364          foreach ( $merged as $key => $val ) {
 365              # A false value suppresses the attribute, and we don't want the
 366              # href attribute to be overridden.
 367              if ( $key != 'href' and $val !== false ) {
 368                  $ret[$key] = $val;
 369              }
 370          }
 371          wfProfileOut( __METHOD__ );
 372          return $ret;
 373      }
 374  
 375      /**
 376       * Default text of the links to the Title $target
 377       *
 378       * @param Title $target
 379       *
 380       * @return string
 381       */
 382  	private static function linkText( $target ) {
 383          if ( !$target instanceof Title ) {
 384              wfWarn( __METHOD__ . ': Requires $target to be a Title object.' );
 385              return '';
 386          }
 387          // If the target is just a fragment, with no title, we return the fragment
 388          // text.  Otherwise, we return the title text itself.
 389          if ( $target->getPrefixedText() === '' && $target->hasFragment() ) {
 390              return htmlspecialchars( $target->getFragment() );
 391          }
 392  
 393          return htmlspecialchars( $target->getPrefixedText() );
 394      }
 395  
 396      /**
 397       * Make appropriate markup for a link to the current article. This is
 398       * currently rendered as the bold link text. The calling sequence is the
 399       * same as the other make*LinkObj static functions, despite $query not
 400       * being used.
 401       *
 402       * @param Title $nt
 403       * @param string $html [optional]
 404       * @param string $query [optional]
 405       * @param string $trail [optional]
 406       * @param string $prefix [optional]
 407       *
 408       * @return string
 409       */
 410  	public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '' ) {
 411          $ret = "<strong class=\"selflink\">{$prefix}{$html}</strong>{$trail}";
 412          if ( !wfRunHooks( 'SelfLinkBegin', array( $nt, &$html, &$trail, &$prefix, &$ret ) ) ) {
 413              return $ret;
 414          }
 415  
 416          if ( $html == '' ) {
 417              $html = htmlspecialchars( $nt->getPrefixedText() );
 418          }
 419          list( $inside, $trail ) = self::splitTrail( $trail );
 420          return "<strong class=\"selflink\">{$prefix}{$html}{$inside}</strong>{$trail}";
 421      }
 422  
 423      /**
 424       * Get a message saying that an invalid title was encountered.
 425       * This should be called after a method like Title::makeTitleSafe() returned
 426       * a value indicating that the title object is invalid.
 427       *
 428       * @param IContextSource $context Context to use to get the messages
 429       * @param int $namespace Namespace number
 430       * @param string $title Text of the title, without the namespace part
 431       * @return string
 432       */
 433  	public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
 434          global $wgContLang;
 435  
 436          // First we check whether the namespace exists or not.
 437          if ( MWNamespace::exists( $namespace ) ) {
 438              if ( $namespace == NS_MAIN ) {
 439                  $name = $context->msg( 'blanknamespace' )->text();
 440              } else {
 441                  $name = $wgContLang->getFormattedNsText( $namespace );
 442              }
 443              return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
 444          } else {
 445              return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text();
 446          }
 447      }
 448  
 449      /**
 450       * @param Title $title
 451       * @return Title
 452       */
 453  	static function normaliseSpecialPage( Title $title ) {
 454          if ( $title->isSpecialPage() ) {
 455              list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
 456              if ( !$name ) {
 457                  return $title;
 458              }
 459              $ret = SpecialPage::getTitleFor( $name, $subpage, $title->getFragment() );
 460              return $ret;
 461          } else {
 462              return $title;
 463          }
 464      }
 465  
 466      /**
 467       * Returns the filename part of an url.
 468       * Used as alternative text for external images.
 469       *
 470       * @param string $url
 471       *
 472       * @return string
 473       */
 474  	private static function fnamePart( $url ) {
 475          $basename = strrchr( $url, '/' );
 476          if ( false === $basename ) {
 477              $basename = $url;
 478          } else {
 479              $basename = substr( $basename, 1 );
 480          }
 481          return $basename;
 482      }
 483  
 484      /**
 485       * Return the code for images which were added via external links,
 486       * via Parser::maybeMakeExternalImage().
 487       *
 488       * @param string $url
 489       * @param string $alt
 490       *
 491       * @return string
 492       */
 493  	public static function makeExternalImage( $url, $alt = '' ) {
 494          if ( $alt == '' ) {
 495              $alt = self::fnamePart( $url );
 496          }
 497          $img = '';
 498          $success = wfRunHooks( 'LinkerMakeExternalImage', array( &$url, &$alt, &$img ) );
 499          if ( !$success ) {
 500              wfDebug( "Hook LinkerMakeExternalImage changed the output of external image "
 501                  . "with url {$url} and alt text {$alt} to {$img}\n", true );
 502              return $img;
 503          }
 504          return Html::element( 'img',
 505              array(
 506                  'src' => $url,
 507                  'alt' => $alt ) );
 508      }
 509  
 510      /**
 511       * Given parameters derived from [[Image:Foo|options...]], generate the
 512       * HTML that that syntax inserts in the page.
 513       *
 514       * @param Parser $parser
 515       * @param Title $title Title object of the file (not the currently viewed page)
 516       * @param File $file File object, or false if it doesn't exist
 517       * @param array $frameParams Associative array of parameters external to the media handler.
 518       *     Boolean parameters are indicated by presence or absence, the value is arbitrary and
 519       *     will often be false.
 520       *          thumbnail       If present, downscale and frame
 521       *          manualthumb     Image name to use as a thumbnail, instead of automatic scaling
 522       *          framed          Shows image in original size in a frame
 523       *          frameless       Downscale but don't frame
 524       *          upright         If present, tweak default sizes for portrait orientation
 525       *          upright_factor  Fudge factor for "upright" tweak (default 0.75)
 526       *          border          If present, show a border around the image
 527       *          align           Horizontal alignment (left, right, center, none)
 528       *          valign          Vertical alignment (baseline, sub, super, top, text-top, middle,
 529       *                          bottom, text-bottom)
 530       *          alt             Alternate text for image (i.e. alt attribute). Plain text.
 531       *          class           HTML for image classes. Plain text.
 532       *          caption         HTML for image caption.
 533       *          link-url        URL to link to
 534       *          link-title      Title object to link to
 535       *          link-target     Value for the target attribute, only with link-url
 536       *          no-link         Boolean, suppress description link
 537       *
 538       * @param array $handlerParams Associative array of media handler parameters, to be passed
 539       *       to transform(). Typical keys are "width" and "page".
 540       * @param string|bool $time Timestamp of the file, set as false for current
 541       * @param string $query Query params for desc url
 542       * @param int|null $widthOption Used by the parser to remember the user preference thumbnailsize
 543       * @since 1.20
 544       * @return string HTML for an image, with links, wrappers, etc.
 545       */
 546  	public static function makeImageLink( Parser $parser, Title $title,
 547          $file, $frameParams = array(), $handlerParams = array(), $time = false,
 548          $query = "", $widthOption = null
 549      ) {
 550          $res = null;
 551          $dummy = new DummyLinker;
 552          if ( !wfRunHooks( 'ImageBeforeProduceHTML', array( &$dummy, &$title,
 553              &$file, &$frameParams, &$handlerParams, &$time, &$res ) ) ) {
 554              return $res;
 555          }
 556  
 557          if ( $file && !$file->allowInlineDisplay() ) {
 558              wfDebug( __METHOD__ . ': ' . $title->getPrefixedDBkey() . " does not allow inline display\n" );
 559              return self::link( $title );
 560          }
 561  
 562          // Shortcuts
 563          $fp =& $frameParams;
 564          $hp =& $handlerParams;
 565  
 566          // Clean up parameters
 567          $page = isset( $hp['page'] ) ? $hp['page'] : false;
 568          if ( !isset( $fp['align'] ) ) {
 569              $fp['align'] = '';
 570          }
 571          if ( !isset( $fp['alt'] ) ) {
 572              $fp['alt'] = '';
 573          }
 574          if ( !isset( $fp['title'] ) ) {
 575              $fp['title'] = '';
 576          }
 577          if ( !isset( $fp['class'] ) ) {
 578              $fp['class'] = '';
 579          }
 580  
 581          $prefix = $postfix = '';
 582  
 583          if ( 'center' == $fp['align'] ) {
 584              $prefix = '<div class="center">';
 585              $postfix = '</div>';
 586              $fp['align'] = 'none';
 587          }
 588          if ( $file && !isset( $hp['width'] ) ) {
 589              if ( isset( $hp['height'] ) && $file->isVectorized() ) {
 590                  // If its a vector image, and user only specifies height
 591                  // we don't want it to be limited by its "normal" width.
 592                  global $wgSVGMaxSize;
 593                  $hp['width'] = $wgSVGMaxSize;
 594              } else {
 595                  $hp['width'] = $file->getWidth( $page );
 596              }
 597  
 598              if ( isset( $fp['thumbnail'] )
 599                  || isset( $fp['manualthumb'] )
 600                  || isset( $fp['framed'] )
 601                  || isset( $fp['frameless'] )
 602                  || !$hp['width']
 603              ) {
 604                  global $wgThumbLimits, $wgThumbUpright;
 605  
 606                  if ( $widthOption === null || !isset( $wgThumbLimits[$widthOption] ) ) {
 607                      $widthOption = User::getDefaultOption( 'thumbsize' );
 608                  }
 609  
 610                  // Reduce width for upright images when parameter 'upright' is used
 611                  if ( isset( $fp['upright'] ) && $fp['upright'] == 0 ) {
 612                      $fp['upright'] = $wgThumbUpright;
 613                  }
 614  
 615                  // For caching health: If width scaled down due to upright
 616                  // parameter, round to full __0 pixel to avoid the creation of a
 617                  // lot of odd thumbs.
 618                  $prefWidth = isset( $fp['upright'] ) ?
 619                      round( $wgThumbLimits[$widthOption] * $fp['upright'], -1 ) :
 620                      $wgThumbLimits[$widthOption];
 621  
 622                  // Use width which is smaller: real image width or user preference width
 623                  // Unless image is scalable vector.
 624                  if ( !isset( $hp['height'] ) && ( $hp['width'] <= 0 ||
 625                          $prefWidth < $hp['width'] || $file->isVectorized() ) ) {
 626                      $hp['width'] = $prefWidth;
 627                  }
 628              }
 629          }
 630  
 631          if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) ) {
 632              # Create a thumbnail. Alignment depends on the writing direction of
 633              # the page content language (right-aligned for LTR languages,
 634              # left-aligned for RTL languages)
 635              #
 636              # If a thumbnail width has not been provided, it is set
 637              # to the default user option as specified in Language*.php
 638              if ( $fp['align'] == '' ) {
 639                  $fp['align'] = $parser->getTargetLanguage()->alignEnd();
 640              }
 641              return $prefix . self::makeThumbLink2( $title, $file, $fp, $hp, $time, $query ) . $postfix;
 642          }
 643  
 644          if ( $file && isset( $fp['frameless'] ) ) {
 645              $srcWidth = $file->getWidth( $page );
 646              # For "frameless" option: do not present an image bigger than the
 647              # source (for bitmap-style images). This is the same behavior as the
 648              # "thumb" option does it already.
 649              if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
 650                  $hp['width'] = $srcWidth;
 651              }
 652          }
 653  
 654          if ( $file && isset( $hp['width'] ) ) {
 655              # Create a resized image, without the additional thumbnail features
 656              $thumb = $file->transform( $hp );
 657          } else {
 658              $thumb = false;
 659          }
 660  
 661          if ( !$thumb ) {
 662              $s = self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true );
 663          } else {
 664              self::processResponsiveImages( $file, $thumb, $hp );
 665              $params = array(
 666                  'alt' => $fp['alt'],
 667                  'title' => $fp['title'],
 668                  'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false,
 669                  'img-class' => $fp['class'] );
 670              if ( isset( $fp['border'] ) ) {
 671                  $params['img-class'] .= ( $params['img-class'] !== '' ? ' ' : '' ) . 'thumbborder';
 672              }
 673              $params = self::getImageLinkMTOParams( $fp, $query, $parser ) + $params;
 674  
 675              $s = $thumb->toHtml( $params );
 676          }
 677          if ( $fp['align'] != '' ) {
 678              $s = "<div class=\"float{$fp['align']}\">{$s}</div>";
 679          }
 680          return str_replace( "\n", ' ', $prefix . $s . $postfix );
 681      }
 682  
 683      /**
 684       * See makeImageLink()
 685       * When this function is removed, remove if( $parser instanceof Parser ) check there too
 686       * @deprecated since 1.20
 687       */
 688  	public static function makeImageLink2( Title $title, $file, $frameParams = array(),
 689          $handlerParams = array(), $time = false, $query = "", $widthOption = null ) {
 690          return self::makeImageLink( null, $title, $file, $frameParams,
 691              $handlerParams, $time, $query, $widthOption );
 692      }
 693  
 694      /**
 695       * Get the link parameters for MediaTransformOutput::toHtml() from given
 696       * frame parameters supplied by the Parser.
 697       * @param array $frameParams The frame parameters
 698       * @param string $query An optional query string to add to description page links
 699       * @param Parser|null $parser
 700       * @return array
 701       */
 702  	private static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
 703          $mtoParams = array();
 704          if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
 705              $mtoParams['custom-url-link'] = $frameParams['link-url'];
 706              if ( isset( $frameParams['link-target'] ) ) {
 707                  $mtoParams['custom-target-link'] = $frameParams['link-target'];
 708              }
 709              if ( $parser ) {
 710                  $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
 711                  foreach ( $extLinkAttrs as $name => $val ) {
 712                      // Currently could include 'rel' and 'target'
 713                      $mtoParams['parser-extlink-' . $name] = $val;
 714                  }
 715              }
 716          } elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
 717              $mtoParams['custom-title-link'] = self::normaliseSpecialPage( $frameParams['link-title'] );
 718          } elseif ( !empty( $frameParams['no-link'] ) ) {
 719              // No link
 720          } else {
 721              $mtoParams['desc-link'] = true;
 722              $mtoParams['desc-query'] = $query;
 723          }
 724          return $mtoParams;
 725      }
 726  
 727      /**
 728       * Make HTML for a thumbnail including image, border and caption
 729       * @param Title $title
 730       * @param File|bool $file File object or false if it doesn't exist
 731       * @param string $label
 732       * @param string $alt
 733       * @param string $align
 734       * @param array $params
 735       * @param bool $framed
 736       * @param string $manualthumb
 737       * @return string
 738       */
 739  	public static function makeThumbLinkObj( Title $title, $file, $label = '', $alt,
 740          $align = 'right', $params = array(), $framed = false, $manualthumb = ""
 741      ) {
 742          $frameParams = array(
 743              'alt' => $alt,
 744              'caption' => $label,
 745              'align' => $align
 746          );
 747          if ( $framed ) {
 748              $frameParams['framed'] = true;
 749          }
 750          if ( $manualthumb ) {
 751              $frameParams['manualthumb'] = $manualthumb;
 752          }
 753          return self::makeThumbLink2( $title, $file, $frameParams, $params );
 754      }
 755  
 756      /**
 757       * @param Title $title
 758       * @param File $file
 759       * @param array $frameParams
 760       * @param array $handlerParams
 761       * @param bool $time
 762       * @param string $query
 763       * @return string
 764       */
 765  	public static function makeThumbLink2( Title $title, $file, $frameParams = array(),
 766          $handlerParams = array(), $time = false, $query = ""
 767      ) {
 768          $exists = $file && $file->exists();
 769  
 770          # Shortcuts
 771          $fp =& $frameParams;
 772          $hp =& $handlerParams;
 773  
 774          $page = isset( $hp['page'] ) ? $hp['page'] : false;
 775          if ( !isset( $fp['align'] ) ) {
 776              $fp['align'] = 'right';
 777          }
 778          if ( !isset( $fp['alt'] ) ) {
 779              $fp['alt'] = '';
 780          }
 781          if ( !isset( $fp['title'] ) ) {
 782              $fp['title'] = '';
 783          }
 784          if ( !isset( $fp['caption'] ) ) {
 785              $fp['caption'] = '';
 786          }
 787  
 788          if ( empty( $hp['width'] ) ) {
 789              // Reduce width for upright images when parameter 'upright' is used
 790              $hp['width'] = isset( $fp['upright'] ) ? 130 : 180;
 791          }
 792          $thumb = false;
 793          $noscale = false;
 794          $manualthumb = false;
 795  
 796          if ( !$exists ) {
 797              $outerWidth = $hp['width'] + 2;
 798          } else {
 799              if ( isset( $fp['manualthumb'] ) ) {
 800                  # Use manually specified thumbnail
 801                  $manual_title = Title::makeTitleSafe( NS_FILE, $fp['manualthumb'] );
 802                  if ( $manual_title ) {
 803                      $manual_img = wfFindFile( $manual_title );
 804                      if ( $manual_img ) {
 805                          $thumb = $manual_img->getUnscaledThumb( $hp );
 806                          $manualthumb = true;
 807                      } else {
 808                          $exists = false;
 809                      }
 810                  }
 811              } elseif ( isset( $fp['framed'] ) ) {
 812                  // Use image dimensions, don't scale
 813                  $thumb = $file->getUnscaledThumb( $hp );
 814                  $noscale = true;
 815              } else {
 816                  # Do not present an image bigger than the source, for bitmap-style images
 817                  # This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
 818                  $srcWidth = $file->getWidth( $page );
 819                  if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
 820                      $hp['width'] = $srcWidth;
 821                  }
 822                  $thumb = $file->transform( $hp );
 823              }
 824  
 825              if ( $thumb ) {
 826                  $outerWidth = $thumb->getWidth() + 2;
 827              } else {
 828                  $outerWidth = $hp['width'] + 2;
 829              }
 830          }
 831  
 832          # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
 833          # So we don't need to pass it here in $query. However, the URL for the
 834          # zoom icon still needs it, so we make a unique query for it. See bug 14771
 835          $url = $title->getLocalURL( $query );
 836          if ( $page ) {
 837              $url = wfAppendQuery( $url, array( 'page' => $page ) );
 838          }
 839          if ( $manualthumb
 840              && !isset( $fp['link-title'] )
 841              && !isset( $fp['link-url'] )
 842              && !isset( $fp['no-link'] ) ) {
 843              $fp['link-url'] = $url;
 844          }
 845  
 846          $s = "<div class=\"thumb t{$fp['align']}\">"
 847              . "<div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
 848  
 849          if ( !$exists ) {
 850              $s .= self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true );
 851              $zoomIcon = '';
 852          } elseif ( !$thumb ) {
 853              $s .= wfMessage( 'thumbnail_error', '' )->escaped();
 854              $zoomIcon = '';
 855          } else {
 856              if ( !$noscale && !$manualthumb ) {
 857                  self::processResponsiveImages( $file, $thumb, $hp );
 858              }
 859              $params = array(
 860                  'alt' => $fp['alt'],
 861                  'title' => $fp['title'],
 862                  'img-class' => ( isset( $fp['class'] ) && $fp['class'] !== ''
 863                      ? $fp['class'] . ' '
 864                      : '' ) . 'thumbimage'
 865              );
 866              $params = self::getImageLinkMTOParams( $fp, $query ) + $params;
 867              $s .= $thumb->toHtml( $params );
 868              if ( isset( $fp['framed'] ) ) {
 869                  $zoomIcon = "";
 870              } else {
 871                  $zoomIcon = Html::rawElement( 'div', array( 'class' => 'magnify' ),
 872                      Html::rawElement( 'a', array(
 873                          'href' => $url,
 874                          'class' => 'internal',
 875                          'title' => wfMessage( 'thumbnail-more' )->text() ),
 876                          "" ) );
 877              }
 878          }
 879          $s .= '  <div class="thumbcaption">' . $zoomIcon . $fp['caption'] . "</div></div></div>";
 880          return str_replace( "\n", ' ', $s );
 881      }
 882  
 883      /**
 884       * Process responsive images: add 1.5x and 2x subimages to the thumbnail, where
 885       * applicable.
 886       *
 887       * @param File $file
 888       * @param MediaTransformOutput $thumb
 889       * @param array $hp Image parameters
 890       */
 891  	public static function processResponsiveImages( $file, $thumb, $hp ) {
 892          global $wgResponsiveImages;
 893          if ( $wgResponsiveImages && $thumb && !$thumb->isError() ) {
 894              $hp15 = $hp;
 895              $hp15['width'] = round( $hp['width'] * 1.5 );
 896              $hp20 = $hp;
 897              $hp20['width'] = $hp['width'] * 2;
 898              if ( isset( $hp['height'] ) ) {
 899                  $hp15['height'] = round( $hp['height'] * 1.5 );
 900                  $hp20['height'] = $hp['height'] * 2;
 901              }
 902  
 903              $thumb15 = $file->transform( $hp15 );
 904              $thumb20 = $file->transform( $hp20 );
 905              if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
 906                  $thumb->responsiveUrls['1.5'] = $thumb15->getUrl();
 907              }
 908              if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
 909                  $thumb->responsiveUrls['2'] = $thumb20->getUrl();
 910              }
 911          }
 912      }
 913  
 914      /**
 915       * Make a "broken" link to an image
 916       *
 917       * @param Title $title
 918       * @param string $label Link label (plain text)
 919       * @param string $query Query string
 920       * @param string $unused1 Unused parameter kept for b/c
 921       * @param string $unused2 Unused parameter kept for b/c
 922       * @param bool $time A file of a certain timestamp was requested
 923       * @return string
 924       */
 925  	public static function makeBrokenImageLinkObj( $title, $label = '',
 926          $query = '', $unused1 = '', $unused2 = '', $time = false
 927      ) {
 928          if ( !$title instanceof Title ) {
 929              wfWarn( __METHOD__ . ': Requires $title to be a Title object.' );
 930              return "<!-- ERROR -->" . htmlspecialchars( $label );
 931          }
 932  
 933          global $wgEnableUploads, $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
 934          wfProfileIn( __METHOD__ );
 935          if ( $label == '' ) {
 936              $label = $title->getPrefixedText();
 937          }
 938          $encLabel = htmlspecialchars( $label );
 939          $currentExists = $time ? ( wfFindFile( $title ) != false ) : false;
 940  
 941          if ( ( $wgUploadMissingFileUrl || $wgUploadNavigationUrl || $wgEnableUploads )
 942              && !$currentExists
 943          ) {
 944              $redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title );
 945  
 946              if ( $redir ) {
 947                  wfProfileOut( __METHOD__ );
 948                  return self::linkKnown( $title, $encLabel, array(), wfCgiToArray( $query ) );
 949              }
 950  
 951              $href = self::getUploadUrl( $title, $query );
 952  
 953              wfProfileOut( __METHOD__ );
 954              return '<a href="' . htmlspecialchars( $href ) . '" class="new" title="' .
 955                  htmlspecialchars( $title->getPrefixedText(), ENT_QUOTES ) . '">' .
 956                  $encLabel . '</a>';
 957          }
 958  
 959          wfProfileOut( __METHOD__ );
 960          return self::linkKnown( $title, $encLabel, array(), wfCgiToArray( $query ) );
 961      }
 962  
 963      /**
 964       * Get the URL to upload a certain file
 965       *
 966       * @param Title $destFile Title object of the file to upload
 967       * @param string $query Urlencoded query string to prepend
 968       * @return string Urlencoded URL
 969       */
 970  	protected static function getUploadUrl( $destFile, $query = '' ) {
 971          global $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
 972          $q = 'wpDestFile=' . $destFile->getPartialURL();
 973          if ( $query != '' ) {
 974              $q .= '&' . $query;
 975          }
 976  
 977          if ( $wgUploadMissingFileUrl ) {
 978              return wfAppendQuery( $wgUploadMissingFileUrl, $q );
 979          } elseif ( $wgUploadNavigationUrl ) {
 980              return wfAppendQuery( $wgUploadNavigationUrl, $q );
 981          } else {
 982              $upload = SpecialPage::getTitleFor( 'Upload' );
 983              return $upload->getLocalURL( $q );
 984          }
 985      }
 986  
 987      /**
 988       * Create a direct link to a given uploaded file.
 989       *
 990       * @param Title $title
 991       * @param string $html Pre-sanitized HTML
 992       * @param string $time MW timestamp of file creation time
 993       * @return string HTML
 994       */
 995  	public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
 996          $img = wfFindFile( $title, array( 'time' => $time ) );
 997          return self::makeMediaLinkFile( $title, $img, $html );
 998      }
 999  
1000      /**
1001       * Create a direct link to a given uploaded file.
1002       * This will make a broken link if $file is false.
1003       *
1004       * @param Title $title
1005       * @param File|bool $file File object or false
1006       * @param string $html Pre-sanitized HTML
1007       * @return string HTML
1008       *
1009       * @todo Handle invalid or missing images better.
1010       */
1011  	public static function makeMediaLinkFile( Title $title, $file, $html = '' ) {
1012          if ( $file && $file->exists() ) {
1013              $url = $file->getURL();
1014              $class = 'internal';
1015          } else {
1016              $url = self::getUploadUrl( $title );
1017              $class = 'new';
1018          }
1019  
1020          $alt = $title->getText();
1021          if ( $html == '' ) {
1022              $html = $alt;
1023          }
1024  
1025          $ret = '';
1026          $attribs = array(
1027              'href' => $url,
1028              'class' => $class,
1029              'title' => $alt
1030          );
1031  
1032          if ( !wfRunHooks( 'LinkerMakeMediaLinkFile',
1033              array( $title, $file, &$html, &$attribs, &$ret ) ) ) {
1034              wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
1035                  . "with url {$url} and text {$html} to {$ret}\n", true );
1036              return $ret;
1037          }
1038  
1039          return Html::rawElement( 'a', $attribs, $html );
1040      }
1041  
1042      /**
1043       * Make a link to a special page given its name and, optionally,
1044       * a message key from the link text.
1045       * Usage example: Linker::specialLink( 'Recentchanges' )
1046       *
1047       * @param string $name
1048       * @param string $key
1049       * @return string
1050       */
1051  	public static function specialLink( $name, $key = '' ) {
1052          if ( $key == '' ) {
1053              $key = strtolower( $name );
1054          }
1055  
1056          return self::linkKnown( SpecialPage::getTitleFor( $name ), wfMessage( $key )->text() );
1057      }
1058  
1059      /**
1060       * Make an external link
1061       * @param string $url URL to link to
1062       * @param string $text Text of link
1063       * @param bool $escape Do we escape the link text?
1064       * @param string $linktype Type of external link. Gets added to the classes
1065       * @param array $attribs Array of extra attributes to <a>
1066       * @param Title|null $title Title object used for title specific link attributes
1067       * @return string
1068       */
1069  	public static function makeExternalLink( $url, $text, $escape = true,
1070          $linktype = '', $attribs = array(), $title = null
1071      ) {
1072          global $wgTitle;
1073          $class = "external";
1074          if ( $linktype ) {
1075              $class .= " $linktype";
1076          }
1077          if ( isset( $attribs['class'] ) && $attribs['class'] ) {
1078              $class .= " {$attribs['class']}";
1079          }
1080          $attribs['class'] = $class;
1081  
1082          if ( $escape ) {
1083              $text = htmlspecialchars( $text );
1084          }
1085  
1086          if ( !$title ) {
1087              $title = $wgTitle;
1088          }
1089          $attribs['rel'] = Parser::getExternalLinkRel( $url, $title );
1090          $link = '';
1091          $success = wfRunHooks( 'LinkerMakeExternalLink',
1092              array( &$url, &$text, &$link, &$attribs, $linktype ) );
1093          if ( !$success ) {
1094              wfDebug( "Hook LinkerMakeExternalLink changed the output of link "
1095                  . "with url {$url} and text {$text} to {$link}\n", true );
1096              return $link;
1097          }
1098          $attribs['href'] = $url;
1099          return Html::rawElement( 'a', $attribs, $text );
1100      }
1101  
1102      /**
1103       * Make user link (or user contributions for unregistered users)
1104       * @param int $userId User id in database.
1105       * @param string $userName User name in database.
1106       * @param string $altUserName Text to display instead of the user name (optional)
1107       * @return string HTML fragment
1108       * @since 1.19 Method exists for a long time. $altUserName was added in 1.19.
1109       */
1110  	public static function userLink( $userId, $userName, $altUserName = false ) {
1111          $classes = 'mw-userlink';
1112          if ( $userId == 0 ) {
1113              $page = SpecialPage::getTitleFor( 'Contributions', $userName );
1114              if ( $altUserName === false ) {
1115                  $altUserName = IP::prettifyIP( $userName );
1116              }
1117              $classes .= ' mw-anonuserlink'; // Separate link class for anons (bug 43179)
1118          } else {
1119              $page = Title::makeTitle( NS_USER, $userName );
1120          }
1121  
1122          return self::link(
1123              $page,
1124              htmlspecialchars( $altUserName !== false ? $altUserName : $userName ),
1125              array( 'class' => $classes )
1126          );
1127      }
1128  
1129      /**
1130       * Generate standard user tool links (talk, contributions, block link, etc.)
1131       *
1132       * @param int $userId User identifier
1133       * @param string $userText User name or IP address
1134       * @param bool $redContribsWhenNoEdits Should the contributions link be
1135       *   red if the user has no edits?
1136       * @param int $flags Customisation flags (e.g. Linker::TOOL_LINKS_NOBLOCK
1137       *   and Linker::TOOL_LINKS_EMAIL).
1138       * @param int $edits User edit count (optional, for performance)
1139       * @return string HTML fragment
1140       */
1141  	public static function userToolLinks(
1142          $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
1143      ) {
1144          global $wgUser, $wgDisableAnonTalk, $wgLang;
1145          $talkable = !( $wgDisableAnonTalk && 0 == $userId );
1146          $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
1147          $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1148  
1149          $items = array();
1150          if ( $talkable ) {
1151              $items[] = self::userTalkLink( $userId, $userText );
1152          }
1153          if ( $userId ) {
1154              // check if the user has an edit
1155              $attribs = array();
1156              if ( $redContribsWhenNoEdits ) {
1157                  if ( intval( $edits ) === 0 && $edits !== 0 ) {
1158                      $user = User::newFromId( $userId );
1159                      $edits = $user->getEditCount();
1160                  }
1161                  if ( $edits === 0 ) {
1162                      $attribs['class'] = 'new';
1163                  }
1164              }
1165              $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
1166  
1167              $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
1168          }
1169          if ( $blockable && $wgUser->isAllowed( 'block' ) ) {
1170              $items[] = self::blockLink( $userId, $userText );
1171          }
1172  
1173          if ( $addEmailLink && $wgUser->canSendEmail() ) {
1174              $items[] = self::emailLink( $userId, $userText );
1175          }
1176  
1177          wfRunHooks( 'UserToolLinksEdit', array( $userId, $userText, &$items ) );
1178  
1179          if ( $items ) {
1180              return wfMessage( 'word-separator' )->plain()
1181                  . '<span class="mw-usertoollinks">'
1182                  . wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped()
1183                  . '</span>';
1184          } else {
1185              return '';
1186          }
1187      }
1188  
1189      /**
1190       * Alias for userToolLinks( $userId, $userText, true );
1191       * @param int $userId User identifier
1192       * @param string $userText User name or IP address
1193       * @param int $edits User edit count (optional, for performance)
1194       * @return string
1195       */
1196  	public static function userToolLinksRedContribs( $userId, $userText, $edits = null ) {
1197          return self::userToolLinks( $userId, $userText, true, 0, $edits );
1198      }
1199  
1200      /**
1201       * @param int $userId User id in database.
1202       * @param string $userText User name in database.
1203       * @return string HTML fragment with user talk link
1204       */
1205  	public static function userTalkLink( $userId, $userText ) {
1206          $userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
1207          $userTalkLink = self::link( $userTalkPage, wfMessage( 'talkpagelinktext' )->escaped() );
1208          return $userTalkLink;
1209      }
1210  
1211      /**
1212       * @param int $userId Userid
1213       * @param string $userText User name in database.
1214       * @return string HTML fragment with block link
1215       */
1216  	public static function blockLink( $userId, $userText ) {
1217          $blockPage = SpecialPage::getTitleFor( 'Block', $userText );
1218          $blockLink = self::link( $blockPage, wfMessage( 'blocklink' )->escaped() );
1219          return $blockLink;
1220      }
1221  
1222      /**
1223       * @param int $userId Userid
1224       * @param string $userText User name in database.
1225       * @return string HTML fragment with e-mail user link
1226       */
1227  	public static function emailLink( $userId, $userText ) {
1228          $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
1229          $emailLink = self::link( $emailPage, wfMessage( 'emaillink' )->escaped() );
1230          return $emailLink;
1231      }
1232  
1233      /**
1234       * Generate a user link if the current user is allowed to view it
1235       * @param Revision $rev
1236       * @param bool $isPublic Show only if all users can see it
1237       * @return string HTML fragment
1238       */
1239  	public static function revUserLink( $rev, $isPublic = false ) {
1240          if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
1241              $link = wfMessage( 'rev-deleted-user' )->escaped();
1242          } elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
1243              $link = self::userLink( $rev->getUser( Revision::FOR_THIS_USER ),
1244                  $rev->getUserText( Revision::FOR_THIS_USER ) );
1245          } else {
1246              $link = wfMessage( 'rev-deleted-user' )->escaped();
1247          }
1248          if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
1249              return '<span class="history-deleted">' . $link . '</span>';
1250          }
1251          return $link;
1252      }
1253  
1254      /**
1255       * Generate a user tool link cluster if the current user is allowed to view it
1256       * @param Revision $rev
1257       * @param bool $isPublic Show only if all users can see it
1258       * @return string HTML
1259       */
1260  	public static function revUserTools( $rev, $isPublic = false ) {
1261          if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
1262              $link = wfMessage( 'rev-deleted-user' )->escaped();
1263          } elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
1264              $userId = $rev->getUser( Revision::FOR_THIS_USER );
1265              $userText = $rev->getUserText( Revision::FOR_THIS_USER );
1266              $link = self::userLink( $userId, $userText )
1267                  . wfMessage( 'word-separator' )->plain()
1268                  . self::userToolLinks( $userId, $userText );
1269          } else {
1270              $link = wfMessage( 'rev-deleted-user' )->escaped();
1271          }
1272          if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
1273              return ' <span class="history-deleted">' . $link . '</span>';
1274          }
1275          return $link;
1276      }
1277  
1278      /**
1279       * This function is called by all recent changes variants, by the page history,
1280       * and by the user contributions list. It is responsible for formatting edit
1281       * summaries. It escapes any HTML in the summary, but adds some CSS to format
1282       * auto-generated comments (from section editing) and formats [[wikilinks]].
1283       *
1284       * @author Erik Moeller <[email protected]>
1285       *
1286       * Note: there's not always a title to pass to this function.
1287       * Since you can't set a default parameter for a reference, I've turned it
1288       * temporarily to a value pass. Should be adjusted further. --brion
1289       *
1290       * @param string $comment
1291       * @param Title|null $title Title object (to generate link to the section in autocomment) or null
1292       * @param bool $local Whether section links should refer to local page
1293       * @return mixed|string
1294       */
1295  	public static function formatComment( $comment, $title = null, $local = false ) {
1296          wfProfileIn( __METHOD__ );
1297  
1298          # Sanitize text a bit:
1299          $comment = str_replace( "\n", " ", $comment );
1300          # Allow HTML entities (for bug 13815)
1301          $comment = Sanitizer::escapeHtmlAllowEntities( $comment );
1302  
1303          # Render autocomments and make links:
1304          $comment = self::formatAutocomments( $comment, $title, $local );
1305          $comment = self::formatLinksInComment( $comment, $title, $local );
1306  
1307          wfProfileOut( __METHOD__ );
1308          return $comment;
1309      }
1310  
1311      /**
1312       * Converts autogenerated comments in edit summaries into section links.
1313       * The pattern for autogen comments is / * foo * /, which makes for
1314       * some nasty regex.
1315       * We look for all comments, match any text before and after the comment,
1316       * add a separator where needed and format the comment itself with CSS
1317       * Called by Linker::formatComment.
1318       *
1319       * @param string $comment Comment text
1320       * @param Title|null $title An optional title object used to links to sections
1321       * @param bool $local Whether section links should refer to local page
1322       * @return string Formatted comment
1323       */
1324  	private static function formatAutocomments( $comment, $title = null, $local = false ) {
1325          return preg_replace_callback(
1326              '!(.*)/\*\s*(.*?)\s*\*/(.*)!',
1327              function ( $match ) use ( $title, $local ) {
1328                  global $wgLang;
1329  
1330                  $pre = $match[1];
1331                  $auto = $match[2];
1332                  $post = $match[3];
1333                  $comment = null;
1334                  wfRunHooks( 'FormatAutocomments', array( &$comment, $pre, $auto, $post, $title, $local ) );
1335                  if ( $comment === null ) {
1336                      $link = '';
1337                      if ( $title ) {
1338                          $section = $auto;
1339                          # Remove links that a user may have manually put in the autosummary
1340                          # This could be improved by copying as much of Parser::stripSectionName as desired.
1341                          $section = str_replace( '[[:', '', $section );
1342                          $section = str_replace( '[[', '', $section );
1343                          $section = str_replace( ']]', '', $section );
1344  
1345                          $section = Sanitizer::normalizeSectionNameWhitespace( $section ); # bug 22784
1346                          if ( $local ) {
1347                              $sectionTitle = Title::newFromText( '#' . $section );
1348                          } else {
1349                              $sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
1350                                  $title->getDBkey(), $section );
1351                          }
1352                          if ( $sectionTitle ) {
1353                              $link = Linker::link( $sectionTitle,
1354                                  $wgLang->getArrow(), array(), array(),
1355                                  'noclasses' );
1356                          } else {
1357                              $link = '';
1358                          }
1359                      }
1360                      if ( $pre ) {
1361                          # written summary $presep autocomment (summary /* section */)
1362                          $pre .= wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped();
1363                      }
1364                      if ( $post ) {
1365                          # autocomment $postsep written summary (/* section */ summary)
1366                          $auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped();
1367                      }
1368                      $auto = '<span class="autocomment">' . $auto . '</span>';
1369                      $comment = $pre . $link . $wgLang->getDirMark()
1370                          . '<span dir="auto">' . $auto . $post . '</span>';
1371                  }
1372                  return $comment;
1373              },
1374              $comment
1375          );
1376      }
1377  
1378      /**
1379       * Formats wiki links and media links in text; all other wiki formatting
1380       * is ignored
1381       *
1382       * @todo FIXME: Doesn't handle sub-links as in image thumb texts like the main parser
1383       * @param string $comment Text to format links in
1384       * @param Title|null $title An optional title object used to links to sections
1385       * @param bool $local Whether section links should refer to local page
1386       * @return string
1387       */
1388  	public static function formatLinksInComment( $comment, $title = null, $local = false ) {
1389          return preg_replace_callback(
1390              '/
1391                  \[\[
1392                  :? # ignore optional leading colon
1393                  ([^\]|]+) # 1. link target; page names cannot include ] or |
1394                  (?:\|
1395                      # 2. a pipe-separated substring; only the last is captured
1396                      # Stop matching at | and ]] without relying on backtracking.
1397                      ((?:]?[^\]|])*+)
1398                  )*
1399                  \]\]
1400                  ([^[]*) # 3. link trail (the text up until the next link)
1401              /x',
1402              function ( $match ) use ( $title, $local ) {
1403                  global $wgContLang;
1404  
1405                  $medians = '(?:' . preg_quote( MWNamespace::getCanonicalName( NS_MEDIA ), '/' ) . '|';
1406                  $medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):';
1407  
1408                  $comment = $match[0];
1409  
1410                  # fix up urlencoded title texts (copied from Parser::replaceInternalLinks)
1411                  if ( strpos( $match[1], '%' ) !== false ) {
1412                      $match[1] = str_replace(
1413                          array( '<', '>' ),
1414                          array( '&lt;', '&gt;' ),
1415                          rawurldecode( $match[1] )
1416                      );
1417                  }
1418  
1419                  # Handle link renaming [[foo|text]] will show link as "text"
1420                  if ( $match[2] != "" ) {
1421                      $text = $match[2];
1422                  } else {
1423                      $text = $match[1];
1424                  }
1425                  $submatch = array();
1426                  $thelink = null;
1427                  if ( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
1428                      # Media link; trail not supported.
1429                      $linkRegexp = '/\[\[(.*?)\]\]/';
1430                      $title = Title::makeTitleSafe( NS_FILE, $submatch[1] );
1431                      if ( $title ) {
1432                          $thelink = Linker::makeMediaLinkObj( $title, $text );
1433                      }
1434                  } else {
1435                      # Other kind of link
1436                      if ( preg_match( $wgContLang->linkTrail(), $match[3], $submatch ) ) {
1437                          $trail = $submatch[1];
1438                      } else {
1439                          $trail = "";
1440                      }
1441                      $linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
1442                      if ( isset( $match[1][0] ) && $match[1][0] == ':' ) {
1443                          $match[1] = substr( $match[1], 1 );
1444                      }
1445                      list( $inside, $trail ) = Linker::splitTrail( $trail );
1446  
1447                      $linkText = $text;
1448                      $linkTarget = Linker::normalizeSubpageLink( $title, $match[1], $linkText );
1449  
1450                      $target = Title::newFromText( $linkTarget );
1451                      if ( $target ) {
1452                          if ( $target->getText() == '' && !$target->isExternal()
1453                              && !$local && $title
1454                          ) {
1455                              $newTarget = clone ( $title );
1456                              $newTarget->setFragment( '#' . $target->getFragment() );
1457                              $target = $newTarget;
1458                          }
1459                          $thelink = Linker::link(
1460                              $target,
1461                              $linkText . $inside
1462                          ) . $trail;
1463                      }
1464                  }
1465                  if ( $thelink ) {
1466                      // If the link is still valid, go ahead and replace it in!
1467                      $comment = preg_replace(
1468                          $linkRegexp,
1469                          StringUtils::escapeRegexReplacement( $thelink ),
1470                          $comment,
1471                          1
1472                      );
1473                  }
1474  
1475                  return $comment;
1476              },
1477              $comment
1478          );
1479      }
1480  
1481      /**
1482       * @param Title $contextTitle
1483       * @param string $target
1484       * @param string $text
1485       * @return string
1486       */
1487  	public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1488          # Valid link forms:
1489          # Foobar -- normal
1490          # :Foobar -- override special treatment of prefix (images, language links)
1491          # /Foobar -- convert to CurrentPage/Foobar
1492          # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
1493          # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1494          # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
1495  
1496          wfProfileIn( __METHOD__ );
1497          $ret = $target; # default return value is no change
1498  
1499          # Some namespaces don't allow subpages,
1500          # so only perform processing if subpages are allowed
1501          if ( $contextTitle && MWNamespace::hasSubpages( $contextTitle->getNamespace() ) ) {
1502              $hash = strpos( $target, '#' );
1503              if ( $hash !== false ) {
1504                  $suffix = substr( $target, $hash );
1505                  $target = substr( $target, 0, $hash );
1506              } else {
1507                  $suffix = '';
1508              }
1509              # bug 7425
1510              $target = trim( $target );
1511              # Look at the first character
1512              if ( $target != '' && $target[0] === '/' ) {
1513                  # / at end means we don't want the slash to be shown
1514                  $m = array();
1515                  $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1516                  if ( $trailingSlashes ) {
1517                      $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1518                  } else {
1519                      $noslash = substr( $target, 1 );
1520                  }
1521  
1522                  $ret = $contextTitle->getPrefixedText() . '/' . trim( $noslash ) . $suffix;
1523                  if ( $text === '' ) {
1524                      $text = $target . $suffix;
1525                  } # this might be changed for ugliness reasons
1526              } else {
1527                  # check for .. subpage backlinks
1528                  $dotdotcount = 0;
1529                  $nodotdot = $target;
1530                  while ( strncmp( $nodotdot, "../", 3 ) == 0 ) {
1531                      ++$dotdotcount;
1532                      $nodotdot = substr( $nodotdot, 3 );
1533                  }
1534                  if ( $dotdotcount > 0 ) {
1535                      $exploded = explode( '/', $contextTitle->getPrefixedText() );
1536                      if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1537                          $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1538                          # / at the end means don't show full path
1539                          if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1540                              $nodotdot = substr( $nodotdot, 0, -1 );
1541                              if ( $text === '' ) {
1542                                  $text = $nodotdot . $suffix;
1543                              }
1544                          }
1545                          $nodotdot = trim( $nodotdot );
1546                          if ( $nodotdot != '' ) {
1547                              $ret .= '/' . $nodotdot;
1548                          }
1549                          $ret .= $suffix;
1550                      }
1551                  }
1552              }
1553          }
1554  
1555          wfProfileOut( __METHOD__ );
1556          return $ret;
1557      }
1558  
1559      /**
1560       * Wrap a comment in standard punctuation and formatting if
1561       * it's non-empty, otherwise return empty string.
1562       *
1563       * @param string $comment
1564       * @param Title|null $title Title object (to generate link to section in autocomment) or null
1565       * @param bool $local Whether section links should refer to local page
1566       *
1567       * @return string
1568       */
1569  	public static function commentBlock( $comment, $title = null, $local = false ) {
1570          // '*' used to be the comment inserted by the software way back
1571          // in antiquity in case none was provided, here for backwards
1572          // compatibility, acc. to brion -ævar
1573          if ( $comment == '' || $comment == '*' ) {
1574              return '';
1575          } else {
1576              $formatted = self::formatComment( $comment, $title, $local );
1577              $formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
1578              return " <span class=\"comment\">$formatted</span>";
1579          }
1580      }
1581  
1582      /**
1583       * Wrap and format the given revision's comment block, if the current
1584       * user is allowed to view it.
1585       *
1586       * @param Revision $rev
1587       * @param bool $local Whether section links should refer to local page
1588       * @param bool $isPublic Show only if all users can see it
1589       * @return string HTML fragment
1590       */
1591  	public static function revComment( Revision $rev, $local = false, $isPublic = false ) {
1592          if ( $rev->getRawComment() == "" ) {
1593              return "";
1594          }
1595          if ( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) {
1596              $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
1597          } elseif ( $rev->userCan( Revision::DELETED_COMMENT ) ) {
1598              $block = self::commentBlock( $rev->getComment( Revision::FOR_THIS_USER ),
1599                  $rev->getTitle(), $local );
1600          } else {
1601              $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
1602          }
1603          if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
1604              return " <span class=\"history-deleted\">$block</span>";
1605          }
1606          return $block;
1607      }
1608  
1609      /**
1610       * @param int $size
1611       * @return string
1612       */
1613  	public static function formatRevisionSize( $size ) {
1614          if ( $size == 0 ) {
1615              $stxt = wfMessage( 'historyempty' )->escaped();
1616          } else {
1617              $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1618              $stxt = wfMessage( 'parentheses' )->rawParams( $stxt )->escaped();
1619          }
1620          return "<span class=\"history-size\">$stxt</span>";
1621      }
1622  
1623      /**
1624       * Add another level to the Table of Contents
1625       *
1626       * @return string
1627       */
1628  	public static function tocIndent() {
1629          return "\n<ul>";
1630      }
1631  
1632      /**
1633       * Finish one or more sublevels on the Table of Contents
1634       *
1635       * @param int $level
1636       * @return string
1637       */
1638  	public static function tocUnindent( $level ) {
1639          return "</li>\n" . str_repeat( "</ul>\n</li>\n", $level > 0 ? $level : 0 );
1640      }
1641  
1642      /**
1643       * parameter level defines if we are on an indentation level
1644       *
1645       * @param string $anchor
1646       * @param string $tocline
1647       * @param string $tocnumber
1648       * @param string $level
1649       * @param string|bool $sectionIndex
1650       * @return string
1651       */
1652  	public static function tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex = false ) {
1653          $classes = "toclevel-$level";
1654          if ( $sectionIndex !== false ) {
1655              $classes .= " tocsection-$sectionIndex";
1656          }
1657          return "\n<li class=\"$classes\"><a href=\"#" .
1658              $anchor . '"><span class="tocnumber">' .
1659              $tocnumber . '</span> <span class="toctext">' .
1660              $tocline . '</span></a>';
1661      }
1662  
1663      /**
1664       * End a Table Of Contents line.
1665       * tocUnindent() will be used instead if we're ending a line below
1666       * the new level.
1667       * @return string
1668       */
1669  	public static function tocLineEnd() {
1670          return "</li>\n";
1671      }
1672  
1673      /**
1674       * Wraps the TOC in a table and provides the hide/collapse javascript.
1675       *
1676       * @param string $toc Html of the Table Of Contents
1677       * @param string|Language|bool $lang Language for the toc title, defaults to user language
1678       * @return string Full html of the TOC
1679       */
1680  	public static function tocList( $toc, $lang = false ) {
1681          $lang = wfGetLangObj( $lang );
1682          $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped();
1683  
1684          return '<div id="toc" class="toc">'
1685              . '<div id="toctitle"><h2>' . $title . "</h2></div>\n"
1686              . $toc
1687              . "</ul>\n</div>\n";
1688      }
1689  
1690      /**
1691       * Generate a table of contents from a section tree
1692       * Currently unused.
1693       *
1694       * @param array $tree Return value of ParserOutput::getSections()
1695       * @return string HTML fragment
1696       */
1697  	public static function generateTOC( $tree ) {
1698          $toc = '';
1699          $lastLevel = 0;
1700          foreach ( $tree as $section ) {
1701              if ( $section['toclevel'] > $lastLevel ) {
1702                  $toc .= self::tocIndent();
1703              } elseif ( $section['toclevel'] < $lastLevel ) {
1704                  $toc .= self::tocUnindent(
1705                      $lastLevel - $section['toclevel'] );
1706              } else {
1707                  $toc .= self::tocLineEnd();
1708              }
1709  
1710              $toc .= self::tocLine( $section['anchor'],
1711                  $section['line'], $section['number'],
1712                  $section['toclevel'], $section['index'] );
1713              $lastLevel = $section['toclevel'];
1714          }
1715          $toc .= self::tocLineEnd();
1716          return self::tocList( $toc );
1717      }
1718  
1719      /**
1720       * Create a headline for content
1721       *
1722       * @param int $level The level of the headline (1-6)
1723       * @param string $attribs Any attributes for the headline, starting with
1724       *   a space and ending with '>'
1725       *   This *must* be at least '>' for no attribs
1726       * @param string $anchor The anchor to give the headline (the bit after the #)
1727       * @param string $html Html for the text of the header
1728       * @param string $link HTML to add for the section edit link
1729       * @param bool|string $legacyAnchor A second, optional anchor to give for
1730       *   backward compatibility (false to omit)
1731       *
1732       * @return string HTML headline
1733       */
1734  	public static function makeHeadline( $level, $attribs, $anchor, $html,
1735          $link, $legacyAnchor = false
1736      ) {
1737          $ret = "<h$level$attribs"
1738              . "<span class=\"mw-headline\" id=\"$anchor\">$html</span>"
1739              . $link
1740              . "</h$level>";
1741          if ( $legacyAnchor !== false ) {
1742              $ret = "<div id=\"$legacyAnchor\"></div>$ret";
1743          }
1744          return $ret;
1745      }
1746  
1747      /**
1748       * Split a link trail, return the "inside" portion and the remainder of the trail
1749       * as a two-element array
1750       * @param string $trail
1751       * @return array
1752       */
1753  	static function splitTrail( $trail ) {
1754          global $wgContLang;
1755          $regex = $wgContLang->linkTrail();
1756          $inside = '';
1757          if ( $trail !== '' ) {
1758              $m = array();
1759              if ( preg_match( $regex, $trail, $m ) ) {
1760                  $inside = $m[1];
1761                  $trail = $m[2];
1762              }
1763          }
1764          return array( $inside, $trail );
1765      }
1766  
1767      /**
1768       * Generate a rollback link for a given revision.  Currently it's the
1769       * caller's responsibility to ensure that the revision is the top one. If
1770       * it's not, of course, the user will get an error message.
1771       *
1772       * If the calling page is called with the parameter &bot=1, all rollback
1773       * links also get that parameter. It causes the edit itself and the rollback
1774       * to be marked as "bot" edits. Bot edits are hidden by default from recent
1775       * changes, so this allows sysops to combat a busy vandal without bothering
1776       * other users.
1777       *
1778       * If the option verify is set this function will return the link only in case the
1779       * revision can be reverted. Please note that due to performance limitations
1780       * it might be assumed that a user isn't the only contributor of a page while
1781       * (s)he is, which will lead to useless rollback links. Furthermore this wont
1782       * work if $wgShowRollbackEditCount is disabled, so this can only function
1783       * as an additional check.
1784       *
1785       * If the option noBrackets is set the rollback link wont be enclosed in []
1786       *
1787       * @param Revision $rev
1788       * @param IContextSource $context Context to use or null for the main context.
1789       * @param array $options
1790       * @return string
1791       */
1792  	public static function generateRollback( $rev, IContextSource $context = null,
1793          $options = array( 'verify' )
1794      ) {
1795          if ( $context === null ) {
1796              $context = RequestContext::getMain();
1797          }
1798  
1799          $editCount = false;
1800          if ( in_array( 'verify', $options ) ) {
1801              $editCount = self::getRollbackEditCount( $rev, true );
1802              if ( $editCount === false ) {
1803                  return '';
1804              }
1805          }
1806  
1807          $inner = self::buildRollbackLink( $rev, $context, $editCount );
1808  
1809          if ( !in_array( 'noBrackets', $options ) ) {
1810              $inner = $context->msg( 'brackets' )->rawParams( $inner )->plain();
1811          }
1812  
1813          return '<span class="mw-rollback-link">' . $inner . '</span>';
1814      }
1815  
1816      /**
1817       * This function will return the number of revisions which a rollback
1818       * would revert and, if $verify is set it will verify that a revision
1819       * can be reverted (that the user isn't the only contributor and the
1820       * revision we might rollback to isn't deleted). These checks can only
1821       * function as an additional check as this function only checks against
1822       * the last $wgShowRollbackEditCount edits.
1823       *
1824       * Returns null if $wgShowRollbackEditCount is disabled or false if $verify
1825       * is set and the user is the only contributor of the page.
1826       *
1827       * @param Revision $rev
1828       * @param bool $verify Try to verify that this revision can really be rolled back
1829       * @return int|bool|null
1830       */
1831  	public static function getRollbackEditCount( $rev, $verify ) {
1832          global $wgShowRollbackEditCount;
1833          if ( !is_int( $wgShowRollbackEditCount ) || !$wgShowRollbackEditCount > 0 ) {
1834              // Nothing has happened, indicate this by returning 'null'
1835              return null;
1836          }
1837  
1838          $dbr = wfGetDB( DB_SLAVE );
1839  
1840          // Up to the value of $wgShowRollbackEditCount revisions are counted
1841          $res = $dbr->select(
1842              'revision',
1843              array( 'rev_user_text', 'rev_deleted' ),
1844              // $rev->getPage() returns null sometimes
1845              array( 'rev_page' => $rev->getTitle()->getArticleID() ),
1846              __METHOD__,
1847              array(
1848                  'USE INDEX' => array( 'revision' => 'page_timestamp' ),
1849                  'ORDER BY' => 'rev_timestamp DESC',
1850                  'LIMIT' => $wgShowRollbackEditCount + 1
1851              )
1852          );
1853  
1854          $editCount = 0;
1855          $moreRevs = false;
1856          foreach ( $res as $row ) {
1857              if ( $rev->getRawUserText() != $row->rev_user_text ) {
1858                  if ( $verify &&
1859                      ( $row->rev_deleted & Revision::DELETED_TEXT
1860                          || $row->rev_deleted & Revision::DELETED_USER
1861                  ) ) {
1862                      // If the user or the text of the revision we might rollback
1863                      // to is deleted in some way we can't rollback. Similar to
1864                      // the sanity checks in WikiPage::commitRollback.
1865                      return false;
1866                  }
1867                  $moreRevs = true;
1868                  break;
1869              }
1870              $editCount++;
1871          }
1872  
1873          if ( $verify && $editCount <= $wgShowRollbackEditCount && !$moreRevs ) {
1874              // We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1875              // and there weren't any other revisions. That means that the current user is the only
1876              // editor, so we can't rollback
1877              return false;
1878          }
1879          return $editCount;
1880      }
1881  
1882      /**
1883       * Build a raw rollback link, useful for collections of "tool" links
1884       *
1885       * @param Revision $rev
1886       * @param IContextSource|null $context Context to use or null for the main context.
1887       * @param int $editCount Number of edits that would be reverted
1888       * @return string HTML fragment
1889       */
1890  	public static function buildRollbackLink( $rev, IContextSource $context = null,
1891          $editCount = false
1892      ) {
1893          global $wgShowRollbackEditCount, $wgMiserMode;
1894  
1895          // To config which pages are effected by miser mode
1896          $disableRollbackEditCountSpecialPage = array( 'Recentchanges', 'Watchlist' );
1897  
1898          if ( $context === null ) {
1899              $context = RequestContext::getMain();
1900          }
1901  
1902          $title = $rev->getTitle();
1903          $query = array(
1904              'action' => 'rollback',
1905              'from' => $rev->getUserText(),
1906              'token' => $context->getUser()->getEditToken( array(
1907                  $title->getPrefixedText(),
1908                  $rev->getUserText()
1909              ) ),
1910          );
1911          if ( $context->getRequest()->getBool( 'bot' ) ) {
1912              $query['bot'] = '1';
1913              $query['hidediff'] = '1'; // bug 15999
1914          }
1915  
1916          $disableRollbackEditCount = false;
1917          if ( $wgMiserMode ) {
1918              foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1919                  if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1920                      $disableRollbackEditCount = true;
1921                      break;
1922                  }
1923              }
1924          }
1925  
1926          if ( !$disableRollbackEditCount
1927              && is_int( $wgShowRollbackEditCount )
1928              && $wgShowRollbackEditCount > 0
1929          ) {
1930              if ( !is_numeric( $editCount ) ) {
1931                  $editCount = self::getRollbackEditCount( $rev, false );
1932              }
1933  
1934              if ( $editCount > $wgShowRollbackEditCount ) {
1935                  $editCount_output = $context->msg( 'rollbacklinkcount-morethan' )
1936                      ->numParams( $wgShowRollbackEditCount )->parse();
1937              } else {
1938                  $editCount_output = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse();
1939              }
1940  
1941              return self::link(
1942                  $title,
1943                  $editCount_output,
1944                  array( 'title' => $context->msg( 'tooltip-rollback' )->text() ),
1945                  $query,
1946                  array( 'known', 'noclasses' )
1947              );
1948          } else {
1949              return self::link(
1950                  $title,
1951                  $context->msg( 'rollbacklink' )->escaped(),
1952                  array( 'title' => $context->msg( 'tooltip-rollback' )->text() ),
1953                  $query,
1954                  array( 'known', 'noclasses' )
1955              );
1956          }
1957      }
1958  
1959      /**
1960       * Returns HTML for the "templates used on this page" list.
1961       *
1962       * Make an HTML list of templates, and then add a "More..." link at
1963       * the bottom. If $more is null, do not add a "More..." link. If $more
1964       * is a Title, make a link to that title and use it. If $more is a string,
1965       * directly paste it in as the link (escaping needs to be done manually).
1966       * Finally, if $more is a Message, call toString().
1967       *
1968       * @param array $templates Array of templates from Article::getUsedTemplate or similar
1969       * @param bool $preview Whether this is for a preview
1970       * @param bool $section Whether this is for a section edit
1971       * @param Title|Message|string|null $more An escaped link for "More..." of the templates
1972       * @return string HTML output
1973       */
1974  	public static function formatTemplates( $templates, $preview = false,
1975          $section = false, $more = null
1976      ) {
1977          global $wgLang;
1978          wfProfileIn( __METHOD__ );
1979  
1980          $outText = '';
1981          if ( count( $templates ) > 0 ) {
1982              # Do a batch existence check
1983              $batch = new LinkBatch;
1984              foreach ( $templates as $title ) {
1985                  $batch->addObj( $title );
1986              }
1987              $batch->execute();
1988  
1989              # Construct the HTML
1990              $outText = '<div class="mw-templatesUsedExplanation">';
1991              if ( $preview ) {
1992                  $outText .= wfMessage( 'templatesusedpreview' )->numParams( count( $templates ) )
1993                      ->parseAsBlock();
1994              } elseif ( $section ) {
1995                  $outText .= wfMessage( 'templatesusedsection' )->numParams( count( $templates ) )
1996                      ->parseAsBlock();
1997              } else {
1998                  $outText .= wfMessage( 'templatesused' )->numParams( count( $templates ) )
1999                      ->parseAsBlock();
2000              }
2001              $outText .= "</div><ul>\n";
2002  
2003              usort( $templates, 'Title::compare' );
2004              foreach ( $templates as $titleObj ) {
2005                  $protected = '';
2006                  $restrictions = $titleObj->getRestrictions( 'edit' );
2007                  if ( $restrictions ) {
2008                      // Check backwards-compatible messages
2009                      $msg = null;
2010                      if ( $restrictions === array( 'sysop' ) ) {
2011                          $msg = wfMessage( 'template-protected' );
2012                      } elseif ( $restrictions === array( 'autoconfirmed' ) ) {
2013                          $msg = wfMessage( 'template-semiprotected' );
2014                      }
2015                      if ( $msg && !$msg->isDisabled() ) {
2016                          $protected = $msg->parse();
2017                      } else {
2018                          // Construct the message from restriction-level-*
2019                          // e.g. restriction-level-sysop, restriction-level-autoconfirmed
2020                          $msgs = array();
2021                          foreach ( $restrictions as $r ) {
2022                              $msgs[] = wfMessage( "restriction-level-$r" )->parse();
2023                          }
2024                          $protected = wfMessage( 'parentheses' )
2025                              ->rawParams( $wgLang->commaList( $msgs ) )->escaped();
2026                      }
2027                  }
2028                  if ( $titleObj->quickUserCan( 'edit' ) ) {
2029                      $editLink = self::link(
2030                          $titleObj,
2031                          wfMessage( 'editlink' )->text(),
2032                          array(),
2033                          array( 'action' => 'edit' )
2034                      );
2035                  } else {
2036                      $editLink = self::link(
2037                          $titleObj,
2038                          wfMessage( 'viewsourcelink' )->text(),
2039                          array(),
2040                          array( 'action' => 'edit' )
2041                      );
2042                  }
2043                  $outText .= '<li>' . self::link( $titleObj )
2044                      . wfMessage( 'word-separator' )->escaped()
2045                      . wfMessage( 'parentheses' )->rawParams( $editLink )->escaped()
2046                      . wfMessage( 'word-separator' )->escaped()
2047                      . $protected . '</li>';
2048              }
2049  
2050              if ( $more instanceof Title ) {
2051                  $outText .= '<li>' . self::link( $more, wfMessage( 'moredotdotdot' ) ) . '</li>';
2052              } elseif ( $more ) {
2053                  $outText .= "<li>$more</li>";
2054              }
2055  
2056              $outText .= '</ul>';
2057          }
2058          wfProfileOut( __METHOD__ );
2059          return $outText;
2060      }
2061  
2062      /**
2063       * Returns HTML for the "hidden categories on this page" list.
2064       *
2065       * @param array $hiddencats Array of hidden categories from Article::getHiddenCategories
2066       *   or similar
2067       * @return string HTML output
2068       */
2069  	public static function formatHiddenCategories( $hiddencats ) {
2070          wfProfileIn( __METHOD__ );
2071  
2072          $outText = '';
2073          if ( count( $hiddencats ) > 0 ) {
2074              # Construct the HTML
2075              $outText = '<div class="mw-hiddenCategoriesExplanation">';
2076              $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
2077              $outText .= "</div><ul>\n";
2078  
2079              foreach ( $hiddencats as $titleObj ) {
2080                  # If it's hidden, it must exist - no need to check with a LinkBatch
2081                  $outText .= '<li>'
2082                      . self::link( $titleObj, null, array(), array(), 'known' )
2083                      . "</li>\n";
2084              }
2085              $outText .= '</ul>';
2086          }
2087          wfProfileOut( __METHOD__ );
2088          return $outText;
2089      }
2090  
2091      /**
2092       * Format a size in bytes for output, using an appropriate
2093       * unit (B, KB, MB or GB) according to the magnitude in question
2094       *
2095       * @param int $size Size to format
2096       * @return string
2097       */
2098  	public static function formatSize( $size ) {
2099          global $wgLang;
2100          return htmlspecialchars( $wgLang->formatSize( $size ) );
2101      }
2102  
2103      /**
2104       * Given the id of an interface element, constructs the appropriate title
2105       * attribute from the system messages.  (Note, this is usually the id but
2106       * isn't always, because sometimes the accesskey needs to go on a different
2107       * element than the id, for reverse-compatibility, etc.)
2108       *
2109       * @param string $name Id of the element, minus prefixes.
2110       * @param string|null $options Null or the string 'withaccess' to add an access-
2111       *   key hint
2112       * @return string Contents of the title attribute (which you must HTML-
2113       *   escape), or false for no title attribute
2114       */
2115  	public static function titleAttrib( $name, $options = null ) {
2116          wfProfileIn( __METHOD__ );
2117  
2118          $message = wfMessage( "tooltip-$name" );
2119  
2120          if ( !$message->exists() ) {
2121              $tooltip = false;
2122          } else {
2123              $tooltip = $message->text();
2124              # Compatibility: formerly some tooltips had [alt-.] hardcoded
2125              $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
2126              # Message equal to '-' means suppress it.
2127              if ( $tooltip == '-' ) {
2128                  $tooltip = false;
2129              }
2130          }
2131  
2132          if ( $options == 'withaccess' ) {
2133              $accesskey = self::accesskey( $name );
2134              if ( $accesskey !== false ) {
2135                  // Should be build the same as in jquery.accessKeyLabel.js
2136                  if ( $tooltip === false || $tooltip === '' ) {
2137                      $tooltip = wfMessage( 'brackets', $accesskey )->text();
2138                  } else {
2139                      $tooltip .= wfMessage( 'word-separator' )->text();
2140                      $tooltip .= wfMessage( 'brackets', $accesskey )->text();
2141                  }
2142              }
2143          }
2144  
2145          wfProfileOut( __METHOD__ );
2146          return $tooltip;
2147      }
2148  
2149      public static $accesskeycache;
2150  
2151      /**
2152       * Given the id of an interface element, constructs the appropriate
2153       * accesskey attribute from the system messages.  (Note, this is usually
2154       * the id but isn't always, because sometimes the accesskey needs to go on
2155       * a different element than the id, for reverse-compatibility, etc.)
2156       *
2157       * @param string $name Id of the element, minus prefixes.
2158       * @return string Contents of the accesskey attribute (which you must HTML-
2159       *   escape), or false for no accesskey attribute
2160       */
2161  	public static function accesskey( $name ) {
2162          if ( isset( self::$accesskeycache[$name] ) ) {
2163              return self::$accesskeycache[$name];
2164          }
2165          wfProfileIn( __METHOD__ );
2166  
2167          $message = wfMessage( "accesskey-$name" );
2168  
2169          if ( !$message->exists() ) {
2170              $accesskey = false;
2171          } else {
2172              $accesskey = $message->plain();
2173              if ( $accesskey === '' || $accesskey === '-' ) {
2174                  # @todo FIXME: Per standard MW behavior, a value of '-' means to suppress the
2175                  # attribute, but this is broken for accesskey: that might be a useful
2176                  # value.
2177                  $accesskey = false;
2178              }
2179          }
2180  
2181          wfProfileOut( __METHOD__ );
2182          self::$accesskeycache[$name] = $accesskey;
2183          return self::$accesskeycache[$name];
2184      }
2185  
2186      /**
2187       * Get a revision-deletion link, or disabled link, or nothing, depending
2188       * on user permissions & the settings on the revision.
2189       *
2190       * Will use forward-compatible revision ID in the Special:RevDelete link
2191       * if possible, otherwise the timestamp-based ID which may break after
2192       * undeletion.
2193       *
2194       * @param User $user
2195       * @param Revision $rev
2196       * @param Title $title
2197       * @return string HTML fragment
2198       */
2199  	public static function getRevDeleteLink( User $user, Revision $rev, Title $title ) {
2200          $canHide = $user->isAllowed( 'deleterevision' );
2201          if ( !$canHide && !( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) ) {
2202              return '';
2203          }
2204  
2205          if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) {
2206              return Linker::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
2207          } else {
2208              if ( $rev->getId() ) {
2209                  // RevDelete links using revision ID are stable across
2210                  // page deletion and undeletion; use when possible.
2211                  $query = array(
2212                      'type' => 'revision',
2213                      'target' => $title->getPrefixedDBkey(),
2214                      'ids' => $rev->getId()
2215                  );
2216              } else {
2217                  // Older deleted entries didn't save a revision ID.
2218                  // We have to refer to these by timestamp, ick!
2219                  $query = array(
2220                      'type' => 'archive',
2221                      'target' => $title->getPrefixedDBkey(),
2222                      'ids' => $rev->getTimestamp()
2223                  );
2224              }
2225              return Linker::revDeleteLink( $query,
2226                  $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
2227          }
2228      }
2229  
2230      /**
2231       * Creates a (show/hide) link for deleting revisions/log entries
2232       *
2233       * @param array $query Query parameters to be passed to link()
2234       * @param bool $restricted Set to true to use a "<strong>" instead of a "<span>"
2235       * @param bool $delete Set to true to use (show/hide) rather than (show)
2236       *
2237       * @return string HTML "<a>" link to Special:Revisiondelete, wrapped in a
2238       * span to allow for customization of appearance with CSS
2239       */
2240  	public static function revDeleteLink( $query = array(), $restricted = false, $delete = true ) {
2241          $sp = SpecialPage::getTitleFor( 'Revisiondelete' );
2242          $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2243          $html = wfMessage( $msgKey )->escaped();
2244          $tag = $restricted ? 'strong' : 'span';
2245          $link = self::link( $sp, $html, array(), $query, array( 'known', 'noclasses' ) );
2246          return Xml::tags(
2247              $tag,
2248              array( 'class' => 'mw-revdelundel-link' ),
2249              wfMessage( 'parentheses' )->rawParams( $link )->escaped()
2250          );
2251      }
2252  
2253      /**
2254       * Creates a dead (show/hide) link for deleting revisions/log entries
2255       *
2256       * @param bool $delete Set to true to use (show/hide) rather than (show)
2257       *
2258       * @return string HTML text wrapped in a span to allow for customization
2259       * of appearance with CSS
2260       */
2261  	public static function revDeleteLinkDisabled( $delete = true ) {
2262          $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2263          $html = wfMessage( $msgKey )->escaped();
2264          $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
2265          return Xml::tags( 'span', array( 'class' => 'mw-revdelundel-link' ), $htmlParentheses );
2266      }
2267  
2268      /* Deprecated methods */
2269  
2270      /**
2271       * @deprecated since 1.16 Use link(); warnings since 1.21
2272       *
2273       * Make a link for a title which may or may not be in the database. If you need to
2274       * call this lots of times, pre-fill the link cache with a LinkBatch, otherwise each
2275       * call to this will result in a DB query.
2276       *
2277       * @param Title $nt The title object to make the link from, e.g. from Title::newFromText.
2278       * @param string $text Link text
2279       * @param string $query Optional query part
2280       * @param string $trail Optional trail. Alphabetic characters at the start of this string will
2281       *   be included in the link text. Other characters will be appended after
2282       *   the end of the link.
2283       * @param string $prefix Optional prefix. As trail, only before instead of after.
2284       * @return string
2285       */
2286  	static function makeLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
2287          wfDeprecated( __METHOD__, '1.21' );
2288  
2289          wfProfileIn( __METHOD__ );
2290          $query = wfCgiToArray( $query );
2291          list( $inside, $trail ) = self::splitTrail( $trail );
2292          if ( $text === '' ) {
2293              $text = self::linkText( $nt );
2294          }
2295  
2296          $ret = self::link( $nt, "$prefix$text$inside", array(), $query ) . $trail;
2297  
2298          wfProfileOut( __METHOD__ );
2299          return $ret;
2300      }
2301  
2302      /**
2303       * @deprecated since 1.16 Use link(); warnings since 1.21
2304       *
2305       * Make a link for a title which definitely exists. This is faster than makeLinkObj because
2306       * it doesn't have to do a database query. It's also valid for interwiki titles and special
2307       * pages.
2308       *
2309       * @param Title $title Title object of target page
2310       * @param string $text Text to replace the title
2311       * @param string $query Link target
2312       * @param string $trail Text after link
2313       * @param string $prefix Text before link text
2314       * @param string $aprops Extra attributes to the a-element
2315       * @param string $style Style to apply - if empty, use getInternalLinkAttributesObj instead
2316       * @return string The a-element
2317       */
2318  	static function makeKnownLinkObj(
2319          $title, $text = '', $query = '', $trail = '', $prefix = '', $aprops = '', $style = ''
2320      ) {
2321          wfDeprecated( __METHOD__, '1.21' );
2322  
2323          wfProfileIn( __METHOD__ );
2324  
2325          if ( $text == '' ) {
2326              $text = self::linkText( $title );
2327          }
2328          $attribs = Sanitizer::mergeAttributes(
2329              Sanitizer::decodeTagAttributes( $aprops ),
2330              Sanitizer::decodeTagAttributes( $style )
2331          );
2332          $query = wfCgiToArray( $query );
2333          list( $inside, $trail ) = self::splitTrail( $trail );
2334  
2335          $ret = self::link( $title, "$prefix$text$inside", $attribs, $query,
2336              array( 'known', 'noclasses' ) ) . $trail;
2337  
2338          wfProfileOut( __METHOD__ );
2339          return $ret;
2340      }
2341  
2342      /**
2343       * Returns the attributes for the tooltip and access key.
2344       * @param string $name
2345       * @return array
2346       */
2347  	public static function tooltipAndAccesskeyAttribs( $name ) {
2348          # @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output
2349          # no attribute" instead of "output '' as value for attribute", this
2350          # would be three lines.
2351          $attribs = array(
2352              'title' => self::titleAttrib( $name, 'withaccess' ),
2353              'accesskey' => self::accesskey( $name )
2354          );
2355          if ( $attribs['title'] === false ) {
2356              unset( $attribs['title'] );
2357          }
2358          if ( $attribs['accesskey'] === false ) {
2359              unset( $attribs['accesskey'] );
2360          }
2361          return $attribs;
2362      }
2363  
2364      /**
2365       * Returns raw bits of HTML, use titleAttrib()
2366       * @param string $name
2367       * @param array|null $options
2368       * @return null|string
2369       */
2370  	public static function tooltip( $name, $options = null ) {
2371          # @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output
2372          # no attribute" instead of "output '' as value for attribute", this
2373          # would be two lines.
2374          $tooltip = self::titleAttrib( $name, $options );
2375          if ( $tooltip === false ) {
2376              return '';
2377          }
2378          return Xml::expandAttributes( array(
2379              'title' => $tooltip
2380          ) );
2381      }
2382  }
2383  
2384  /**
2385   * @since 1.18
2386   */
2387  class DummyLinker {
2388  
2389      /**
2390       * Use PHP's magic __call handler to transform instance calls to a dummy instance
2391       * into static calls to the new Linker for backwards compatibility.
2392       *
2393       * @param string $fname Name of called method
2394       * @param array $args Arguments to the method
2395       * @return mixed
2396       */
2397  	public function __call( $fname, $args ) {
2398          return call_user_func_array( array( 'Linker', $fname ), $args );
2399      }
2400  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1