[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
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( '<', '>' ), 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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |