MediaWiki
REL1_23
|
00001 <?php 00032 class Linker { 00033 00037 const TOOL_LINKS_NOBLOCK = 1; 00038 const TOOL_LINKS_EMAIL = 2; 00039 00049 static function getExternalLinkAttributes( $class = 'external' ) { 00050 wfDeprecated( __METHOD__, '1.18' ); 00051 return self::getLinkAttributesInternal( '', $class ); 00052 } 00053 00064 static function getInterwikiLinkAttributes( $title, $unused = null, $class = 'external' ) { 00065 global $wgContLang; 00066 00067 # @todo FIXME: We have a whole bunch of handling here that doesn't happen in 00068 # getExternalLinkAttributes, why? 00069 $title = urldecode( $title ); 00070 $title = $wgContLang->checkTitleEncoding( $title ); 00071 $title = preg_replace( '/[\\x00-\\x1f]/', ' ', $title ); 00072 00073 return self::getLinkAttributesInternal( $title, $class ); 00074 } 00075 00085 static function getInternalLinkAttributes( $title, $unused = null, $class = '' ) { 00086 $title = urldecode( $title ); 00087 $title = str_replace( '_', ' ', $title ); 00088 return self::getLinkAttributesInternal( $title, $class ); 00089 } 00090 00102 static function getInternalLinkAttributesObj( $nt, $unused = null, $class = '', $title = false ) { 00103 if ( $title === false ) { 00104 $title = $nt->getPrefixedText(); 00105 } 00106 return self::getLinkAttributesInternal( $title, $class ); 00107 } 00108 00117 private static function getLinkAttributesInternal( $title, $class ) { 00118 $title = htmlspecialchars( $title ); 00119 $class = htmlspecialchars( $class ); 00120 $r = ''; 00121 if ( $class != '' ) { 00122 $r .= " class=\"$class\""; 00123 } 00124 if ( $title != '' ) { 00125 $r .= " title=\"$title\""; 00126 } 00127 return $r; 00128 } 00129 00137 public static function getLinkColour( $t, $threshold ) { 00138 $colour = ''; 00139 if ( $t->isRedirect() ) { 00140 # Page is a redirect 00141 $colour = 'mw-redirect'; 00142 } elseif ( $threshold > 0 && $t->isContentPage() && 00143 $t->exists() && $t->getLength() < $threshold 00144 ) { 00145 # Page is a stub 00146 $colour = 'stub'; 00147 } 00148 return $colour; 00149 } 00150 00192 public static function link( 00193 $target, $html = null, $customAttribs = array(), $query = array(), $options = array() 00194 ) { 00195 wfProfileIn( __METHOD__ ); 00196 if ( !$target instanceof Title ) { 00197 wfProfileOut( __METHOD__ ); 00198 return "<!-- ERROR -->$html"; 00199 } 00200 00201 if ( is_string( $query ) ) { 00202 // some functions withing core using this still hand over query strings 00203 wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' ); 00204 $query = wfCgiToArray( $query ); 00205 } 00206 $options = (array)$options; 00207 00208 $dummy = new DummyLinker; // dummy linker instance for bc on the hooks 00209 00210 $ret = null; 00211 if ( !wfRunHooks( 'LinkBegin', array( $dummy, $target, &$html, 00212 &$customAttribs, &$query, &$options, &$ret ) ) ) { 00213 wfProfileOut( __METHOD__ ); 00214 return $ret; 00215 } 00216 00217 # Normalize the Title if it's a special page 00218 $target = self::normaliseSpecialPage( $target ); 00219 00220 # If we don't know whether the page exists, let's find out. 00221 wfProfileIn( __METHOD__ . '-checkPageExistence' ); 00222 if ( !in_array( 'known', $options ) and !in_array( 'broken', $options ) ) { 00223 if ( $target->isKnown() ) { 00224 $options[] = 'known'; 00225 } else { 00226 $options[] = 'broken'; 00227 } 00228 } 00229 wfProfileOut( __METHOD__ . '-checkPageExistence' ); 00230 00231 $oldquery = array(); 00232 if ( in_array( "forcearticlepath", $options ) && $query ) { 00233 $oldquery = $query; 00234 $query = array(); 00235 } 00236 00237 # Note: we want the href attribute first, for prettiness. 00238 $attribs = array( 'href' => self::linkUrl( $target, $query, $options ) ); 00239 if ( in_array( 'forcearticlepath', $options ) && $oldquery ) { 00240 $attribs['href'] = wfAppendQuery( $attribs['href'], $oldquery ); 00241 } 00242 00243 $attribs = array_merge( 00244 $attribs, 00245 self::linkAttribs( $target, $customAttribs, $options ) 00246 ); 00247 if ( is_null( $html ) ) { 00248 $html = self::linkText( $target ); 00249 } 00250 00251 $ret = null; 00252 if ( wfRunHooks( 'LinkEnd', array( $dummy, $target, $options, &$html, &$attribs, &$ret ) ) ) { 00253 $ret = Html::rawElement( 'a', $attribs, $html ); 00254 } 00255 00256 wfProfileOut( __METHOD__ ); 00257 return $ret; 00258 } 00259 00264 public static function linkKnown( 00265 $target, $html = null, $customAttribs = array(), 00266 $query = array(), $options = array( 'known', 'noclasses' ) 00267 ) { 00268 return self::link( $target, $html, $customAttribs, $query, $options ); 00269 } 00270 00279 private static function linkUrl( $target, $query, $options ) { 00280 wfProfileIn( __METHOD__ ); 00281 # We don't want to include fragments for broken links, because they 00282 # generally make no sense. 00283 if ( in_array( 'broken', $options ) && $target->hasFragment() ) { 00284 $target = clone $target; 00285 $target->setFragment( '' ); 00286 } 00287 00288 # If it's a broken link, add the appropriate query pieces, unless 00289 # there's already an action specified, or unless 'edit' makes no sense 00290 # (i.e., for a nonexistent special page). 00291 if ( in_array( 'broken', $options ) && empty( $query['action'] ) 00292 && !$target->isSpecialPage() ) { 00293 $query['action'] = 'edit'; 00294 $query['redlink'] = '1'; 00295 } 00296 00297 if ( in_array( 'http', $options ) ) { 00298 $proto = PROTO_HTTP; 00299 } elseif ( in_array( 'https', $options ) ) { 00300 $proto = PROTO_HTTPS; 00301 } else { 00302 $proto = PROTO_RELATIVE; 00303 } 00304 00305 $ret = $target->getLinkURL( $query, false, $proto ); 00306 wfProfileOut( __METHOD__ ); 00307 return $ret; 00308 } 00309 00319 private static function linkAttribs( $target, $attribs, $options ) { 00320 wfProfileIn( __METHOD__ ); 00321 global $wgUser; 00322 $defaults = array(); 00323 00324 if ( !in_array( 'noclasses', $options ) ) { 00325 wfProfileIn( __METHOD__ . '-getClasses' ); 00326 # Now build the classes. 00327 $classes = array(); 00328 00329 if ( in_array( 'broken', $options ) ) { 00330 $classes[] = 'new'; 00331 } 00332 00333 if ( $target->isExternal() ) { 00334 $classes[] = 'extiw'; 00335 } 00336 00337 if ( !in_array( 'broken', $options ) ) { # Avoid useless calls to LinkCache (see r50387) 00338 $colour = self::getLinkColour( $target, $wgUser->getStubThreshold() ); 00339 if ( $colour !== '' ) { 00340 $classes[] = $colour; # mw-redirect or stub 00341 } 00342 } 00343 if ( $classes != array() ) { 00344 $defaults['class'] = implode( ' ', $classes ); 00345 } 00346 wfProfileOut( __METHOD__ . '-getClasses' ); 00347 } 00348 00349 # Get a default title attribute. 00350 if ( $target->getPrefixedText() == '' ) { 00351 # A link like [[#Foo]]. This used to mean an empty title 00352 # attribute, but that's silly. Just don't output a title. 00353 } elseif ( in_array( 'known', $options ) ) { 00354 $defaults['title'] = $target->getPrefixedText(); 00355 } else { 00356 $defaults['title'] = wfMessage( 'red-link-title', $target->getPrefixedText() )->text(); 00357 } 00358 00359 # Finally, merge the custom attribs with the default ones, and iterate 00360 # over that, deleting all "false" attributes. 00361 $ret = array(); 00362 $merged = Sanitizer::mergeAttributes( $defaults, $attribs ); 00363 foreach ( $merged as $key => $val ) { 00364 # A false value suppresses the attribute, and we don't want the 00365 # href attribute to be overridden. 00366 if ( $key != 'href' and $val !== false ) { 00367 $ret[$key] = $val; 00368 } 00369 } 00370 wfProfileOut( __METHOD__ ); 00371 return $ret; 00372 } 00373 00381 private static function linkText( $target ) { 00382 // We might be passed a non-Title by make*LinkObj(). Fail gracefully. 00383 if ( !$target instanceof Title ) { 00384 return ''; 00385 } 00386 00387 // If the target is just a fragment, with no title, we return the fragment 00388 // text. Otherwise, we return the title text itself. 00389 if ( $target->getPrefixedText() === '' && $target->hasFragment() ) { 00390 return htmlspecialchars( $target->getFragment() ); 00391 } 00392 return htmlspecialchars( $target->getPrefixedText() ); 00393 } 00394 00409 public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '' ) { 00410 if ( $html == '' ) { 00411 $html = htmlspecialchars( $nt->getPrefixedText() ); 00412 } 00413 list( $inside, $trail ) = self::splitTrail( $trail ); 00414 return "<strong class=\"selflink\">{$prefix}{$html}{$inside}</strong>{$trail}"; 00415 } 00416 00427 public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) { 00428 global $wgContLang; 00429 00430 // First we check whether the namespace exists or not. 00431 if ( MWNamespace::exists( $namespace ) ) { 00432 if ( $namespace == NS_MAIN ) { 00433 $name = $context->msg( 'blanknamespace' )->text(); 00434 } else { 00435 $name = $wgContLang->getFormattedNsText( $namespace ); 00436 } 00437 return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text(); 00438 } else { 00439 return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text(); 00440 } 00441 } 00442 00447 static function normaliseSpecialPage( Title $title ) { 00448 if ( $title->isSpecialPage() ) { 00449 list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() ); 00450 if ( !$name ) { 00451 return $title; 00452 } 00453 $ret = SpecialPage::getTitleFor( $name, $subpage, $title->getFragment() ); 00454 return $ret; 00455 } else { 00456 return $title; 00457 } 00458 } 00459 00468 private static function fnamePart( $url ) { 00469 $basename = strrchr( $url, '/' ); 00470 if ( false === $basename ) { 00471 $basename = $url; 00472 } else { 00473 $basename = substr( $basename, 1 ); 00474 } 00475 return $basename; 00476 } 00477 00487 public static function makeExternalImage( $url, $alt = '' ) { 00488 if ( $alt == '' ) { 00489 $alt = self::fnamePart( $url ); 00490 } 00491 $img = ''; 00492 $success = wfRunHooks( 'LinkerMakeExternalImage', array( &$url, &$alt, &$img ) ); 00493 if ( !$success ) { 00494 wfDebug( "Hook LinkerMakeExternalImage changed the output of external image with url {$url} and alt text {$alt} to {$img}\n", true ); 00495 return $img; 00496 } 00497 return Html::element( 'img', 00498 array( 00499 'src' => $url, 00500 'alt' => $alt ) ); 00501 } 00502 00539 public static function makeImageLink( /*Parser*/ $parser, Title $title, $file, $frameParams = array(), 00540 $handlerParams = array(), $time = false, $query = "", $widthOption = null 00541 ) { 00542 $res = null; 00543 $dummy = new DummyLinker; 00544 if ( !wfRunHooks( 'ImageBeforeProduceHTML', array( &$dummy, &$title, 00545 &$file, &$frameParams, &$handlerParams, &$time, &$res ) ) ) { 00546 return $res; 00547 } 00548 00549 if ( $file && !$file->allowInlineDisplay() ) { 00550 wfDebug( __METHOD__ . ': ' . $title->getPrefixedDBkey() . " does not allow inline display\n" ); 00551 return self::link( $title ); 00552 } 00553 00554 // Shortcuts 00555 $fp =& $frameParams; 00556 $hp =& $handlerParams; 00557 00558 // Clean up parameters 00559 $page = isset( $hp['page'] ) ? $hp['page'] : false; 00560 if ( !isset( $fp['align'] ) ) { 00561 $fp['align'] = ''; 00562 } 00563 if ( !isset( $fp['alt'] ) ) { 00564 $fp['alt'] = ''; 00565 } 00566 if ( !isset( $fp['title'] ) ) { 00567 $fp['title'] = ''; 00568 } 00569 if ( !isset( $fp['class'] ) ) { 00570 $fp['class'] = ''; 00571 } 00572 00573 $prefix = $postfix = ''; 00574 00575 if ( 'center' == $fp['align'] ) { 00576 $prefix = '<div class="center">'; 00577 $postfix = '</div>'; 00578 $fp['align'] = 'none'; 00579 } 00580 if ( $file && !isset( $hp['width'] ) ) { 00581 if ( isset( $hp['height'] ) && $file->isVectorized() ) { 00582 // If its a vector image, and user only specifies height 00583 // we don't want it to be limited by its "normal" width. 00584 global $wgSVGMaxSize; 00585 $hp['width'] = $wgSVGMaxSize; 00586 } else { 00587 $hp['width'] = $file->getWidth( $page ); 00588 } 00589 00590 if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) || isset( $fp['frameless'] ) || !$hp['width'] ) { 00591 global $wgThumbLimits, $wgThumbUpright; 00592 if ( $widthOption === null || !isset( $wgThumbLimits[$widthOption] ) ) { 00593 $widthOption = User::getDefaultOption( 'thumbsize' ); 00594 } 00595 00596 // Reduce width for upright images when parameter 'upright' is used 00597 if ( isset( $fp['upright'] ) && $fp['upright'] == 0 ) { 00598 $fp['upright'] = $wgThumbUpright; 00599 } 00600 // For caching health: If width scaled down due to upright parameter, round to full __0 pixel to avoid the creation of a lot of odd thumbs 00601 $prefWidth = isset( $fp['upright'] ) ? 00602 round( $wgThumbLimits[$widthOption] * $fp['upright'], -1 ) : 00603 $wgThumbLimits[$widthOption]; 00604 00605 // Use width which is smaller: real image width or user preference width 00606 // Unless image is scalable vector. 00607 if ( !isset( $hp['height'] ) && ( $hp['width'] <= 0 || 00608 $prefWidth < $hp['width'] || $file->isVectorized() ) ) { 00609 $hp['width'] = $prefWidth; 00610 } 00611 } 00612 } 00613 00614 if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) ) { 00615 # Create a thumbnail. Alignment depends on the writing direction of 00616 # the page content language (right-aligned for LTR languages, 00617 # left-aligned for RTL languages) 00618 # 00619 # If a thumbnail width has not been provided, it is set 00620 # to the default user option as specified in Language*.php 00621 if ( $fp['align'] == '' ) { 00622 if ( $parser instanceof Parser ) { 00623 $fp['align'] = $parser->getTargetLanguage()->alignEnd(); 00624 } else { 00625 # backwards compatibility, remove with makeImageLink2() 00626 global $wgContLang; 00627 $fp['align'] = $wgContLang->alignEnd(); 00628 } 00629 } 00630 return $prefix . self::makeThumbLink2( $title, $file, $fp, $hp, $time, $query ) . $postfix; 00631 } 00632 00633 if ( $file && isset( $fp['frameless'] ) ) { 00634 $srcWidth = $file->getWidth( $page ); 00635 # For "frameless" option: do not present an image bigger than the source (for bitmap-style images) 00636 # This is the same behavior as the "thumb" option does it already. 00637 if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) { 00638 $hp['width'] = $srcWidth; 00639 } 00640 } 00641 00642 if ( $file && isset( $hp['width'] ) ) { 00643 # Create a resized image, without the additional thumbnail features 00644 $thumb = $file->transform( $hp ); 00645 } else { 00646 $thumb = false; 00647 } 00648 00649 if ( !$thumb ) { 00650 $s = self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true ); 00651 } else { 00652 self::processResponsiveImages( $file, $thumb, $hp ); 00653 $params = array( 00654 'alt' => $fp['alt'], 00655 'title' => $fp['title'], 00656 'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false, 00657 'img-class' => $fp['class'] ); 00658 if ( isset( $fp['border'] ) ) { 00659 $params['img-class'] .= ( $params['img-class'] !== '' ? ' ' : '' ) . 'thumbborder'; 00660 } 00661 $params = self::getImageLinkMTOParams( $fp, $query, $parser ) + $params; 00662 00663 $s = $thumb->toHtml( $params ); 00664 } 00665 if ( $fp['align'] != '' ) { 00666 $s = "<div class=\"float{$fp['align']}\">{$s}</div>"; 00667 } 00668 return str_replace( "\n", ' ', $prefix . $s . $postfix ); 00669 } 00670 00676 public static function makeImageLink2( Title $title, $file, $frameParams = array(), 00677 $handlerParams = array(), $time = false, $query = "", $widthOption = null ) { 00678 return self::makeImageLink( null, $title, $file, $frameParams, 00679 $handlerParams, $time, $query, $widthOption ); 00680 } 00681 00689 private static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) { 00690 $mtoParams = array(); 00691 if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) { 00692 $mtoParams['custom-url-link'] = $frameParams['link-url']; 00693 if ( isset( $frameParams['link-target'] ) ) { 00694 $mtoParams['custom-target-link'] = $frameParams['link-target']; 00695 } 00696 if ( $parser ) { 00697 $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] ); 00698 foreach ( $extLinkAttrs as $name => $val ) { 00699 // Currently could include 'rel' and 'target' 00700 $mtoParams['parser-extlink-' . $name] = $val; 00701 } 00702 } 00703 } elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) { 00704 $mtoParams['custom-title-link'] = self::normaliseSpecialPage( $frameParams['link-title'] ); 00705 } elseif ( !empty( $frameParams['no-link'] ) ) { 00706 // No link 00707 } else { 00708 $mtoParams['desc-link'] = true; 00709 $mtoParams['desc-query'] = $query; 00710 } 00711 return $mtoParams; 00712 } 00713 00726 public static function makeThumbLinkObj( Title $title, $file, $label = '', $alt, 00727 $align = 'right', $params = array(), $framed = false, $manualthumb = "" 00728 ) { 00729 $frameParams = array( 00730 'alt' => $alt, 00731 'caption' => $label, 00732 'align' => $align 00733 ); 00734 if ( $framed ) { 00735 $frameParams['framed'] = true; 00736 } 00737 if ( $manualthumb ) { 00738 $frameParams['manualthumb'] = $manualthumb; 00739 } 00740 return self::makeThumbLink2( $title, $file, $frameParams, $params ); 00741 } 00742 00752 public static function makeThumbLink2( Title $title, $file, $frameParams = array(), 00753 $handlerParams = array(), $time = false, $query = "" 00754 ) { 00755 global $wgStylePath, $wgContLang; 00756 $exists = $file && $file->exists(); 00757 00758 # Shortcuts 00759 $fp =& $frameParams; 00760 $hp =& $handlerParams; 00761 00762 $page = isset( $hp['page'] ) ? $hp['page'] : false; 00763 if ( !isset( $fp['align'] ) ) { 00764 $fp['align'] = 'right'; 00765 } 00766 if ( !isset( $fp['alt'] ) ) { 00767 $fp['alt'] = ''; 00768 } 00769 if ( !isset( $fp['title'] ) ) { 00770 $fp['title'] = ''; 00771 } 00772 if ( !isset( $fp['caption'] ) ) { 00773 $fp['caption'] = ''; 00774 } 00775 00776 if ( empty( $hp['width'] ) ) { 00777 // Reduce width for upright images when parameter 'upright' is used 00778 $hp['width'] = isset( $fp['upright'] ) ? 130 : 180; 00779 } 00780 $thumb = false; 00781 $noscale = false; 00782 $manualthumb = false; 00783 00784 if ( !$exists ) { 00785 $outerWidth = $hp['width'] + 2; 00786 } else { 00787 if ( isset( $fp['manualthumb'] ) ) { 00788 # Use manually specified thumbnail 00789 $manual_title = Title::makeTitleSafe( NS_FILE, $fp['manualthumb'] ); 00790 if ( $manual_title ) { 00791 $manual_img = wfFindFile( $manual_title ); 00792 if ( $manual_img ) { 00793 $thumb = $manual_img->getUnscaledThumb( $hp ); 00794 $manualthumb = true; 00795 } else { 00796 $exists = false; 00797 } 00798 } 00799 } elseif ( isset( $fp['framed'] ) ) { 00800 // Use image dimensions, don't scale 00801 $thumb = $file->getUnscaledThumb( $hp ); 00802 $noscale = true; 00803 } else { 00804 # Do not present an image bigger than the source, for bitmap-style images 00805 # This is a hack to maintain compatibility with arbitrary pre-1.10 behavior 00806 $srcWidth = $file->getWidth( $page ); 00807 if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) { 00808 $hp['width'] = $srcWidth; 00809 } 00810 $thumb = $file->transform( $hp ); 00811 } 00812 00813 if ( $thumb ) { 00814 $outerWidth = $thumb->getWidth() + 2; 00815 } else { 00816 $outerWidth = $hp['width'] + 2; 00817 } 00818 } 00819 00820 # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs 00821 # So we don't need to pass it here in $query. However, the URL for the 00822 # zoom icon still needs it, so we make a unique query for it. See bug 14771 00823 $url = $title->getLocalURL( $query ); 00824 if ( $page ) { 00825 $url = wfAppendQuery( $url, array( 'page' => $page ) ); 00826 } 00827 if ( $manualthumb 00828 && !isset( $fp['link-title'] ) 00829 && !isset( $fp['link-url'] ) 00830 && !isset( $fp['no-link'] ) ) { 00831 $fp['link-url'] = $url; 00832 } 00833 00834 $s = "<div class=\"thumb t{$fp['align']}\"><div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">"; 00835 if ( !$exists ) { 00836 $s .= self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true ); 00837 $zoomIcon = ''; 00838 } elseif ( !$thumb ) { 00839 $s .= wfMessage( 'thumbnail_error', '' )->escaped(); 00840 $zoomIcon = ''; 00841 } else { 00842 if ( !$noscale && !$manualthumb ) { 00843 self::processResponsiveImages( $file, $thumb, $hp ); 00844 } 00845 $params = array( 00846 'alt' => $fp['alt'], 00847 'title' => $fp['title'], 00848 'img-class' => ( isset( $fp['class'] ) && $fp['class'] !== '' ? $fp['class'] . ' ' : '' ) . 'thumbimage' 00849 ); 00850 $params = self::getImageLinkMTOParams( $fp, $query ) + $params; 00851 $s .= $thumb->toHtml( $params ); 00852 if ( isset( $fp['framed'] ) ) { 00853 $zoomIcon = ""; 00854 } else { 00855 $zoomIcon = Html::rawElement( 'div', array( 'class' => 'magnify' ), 00856 Html::rawElement( 'a', array( 00857 'href' => $url, 00858 'class' => 'internal', 00859 'title' => wfMessage( 'thumbnail-more' )->text() ), 00860 Html::element( 'img', array( 00861 'src' => $wgStylePath . '/common/images/magnify-clip' . ( $wgContLang->isRTL() ? '-rtl' : '' ) . '.png', 00862 'width' => 15, 00863 'height' => 11, 00864 'alt' => "" ) ) ) ); 00865 } 00866 } 00867 $s .= ' <div class="thumbcaption">' . $zoomIcon . $fp['caption'] . "</div></div></div>"; 00868 return str_replace( "\n", ' ', $s ); 00869 } 00870 00879 public static function processResponsiveImages( $file, $thumb, $hp ) { 00880 global $wgResponsiveImages; 00881 if ( $wgResponsiveImages ) { 00882 $hp15 = $hp; 00883 $hp15['width'] = round( $hp['width'] * 1.5 ); 00884 $hp20 = $hp; 00885 $hp20['width'] = $hp['width'] * 2; 00886 if ( isset( $hp['height'] ) ) { 00887 $hp15['height'] = round( $hp['height'] * 1.5 ); 00888 $hp20['height'] = $hp['height'] * 2; 00889 } 00890 00891 $thumb15 = $file->transform( $hp15 ); 00892 $thumb20 = $file->transform( $hp20 ); 00893 if ( $thumb15 && $thumb15->getUrl() !== $thumb->getUrl() ) { 00894 $thumb->responsiveUrls['1.5'] = $thumb15->getUrl(); 00895 } 00896 if ( $thumb20 && $thumb20->getUrl() !== $thumb->getUrl() ) { 00897 $thumb->responsiveUrls['2'] = $thumb20->getUrl(); 00898 } 00899 } 00900 } 00901 00913 public static function makeBrokenImageLinkObj( $title, $label = '', $query = '', $unused1 = '', $unused2 = '', $time = false ) { 00914 global $wgEnableUploads, $wgUploadMissingFileUrl, $wgUploadNavigationUrl; 00915 if ( ! $title instanceof Title ) { 00916 return "<!-- ERROR -->" . htmlspecialchars( $label ); 00917 } 00918 wfProfileIn( __METHOD__ ); 00919 if ( $label == '' ) { 00920 $label = $title->getPrefixedText(); 00921 } 00922 $encLabel = htmlspecialchars( $label ); 00923 $currentExists = $time ? ( wfFindFile( $title ) != false ) : false; 00924 00925 if ( ( $wgUploadMissingFileUrl || $wgUploadNavigationUrl || $wgEnableUploads ) && !$currentExists ) { 00926 $redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title ); 00927 00928 if ( $redir ) { 00929 wfProfileOut( __METHOD__ ); 00930 return self::linkKnown( $title, $encLabel, array(), wfCgiToArray( $query ) ); 00931 } 00932 00933 $href = self::getUploadUrl( $title, $query ); 00934 00935 wfProfileOut( __METHOD__ ); 00936 return '<a href="' . htmlspecialchars( $href ) . '" class="new" title="' . 00937 htmlspecialchars( $title->getPrefixedText(), ENT_QUOTES ) . '">' . 00938 $encLabel . '</a>'; 00939 } 00940 00941 wfProfileOut( __METHOD__ ); 00942 return self::linkKnown( $title, $encLabel, array(), wfCgiToArray( $query ) ); 00943 } 00944 00952 protected static function getUploadUrl( $destFile, $query = '' ) { 00953 global $wgUploadMissingFileUrl, $wgUploadNavigationUrl; 00954 $q = 'wpDestFile=' . $destFile->getPartialURL(); 00955 if ( $query != '' ) { 00956 $q .= '&' . $query; 00957 } 00958 00959 if ( $wgUploadMissingFileUrl ) { 00960 return wfAppendQuery( $wgUploadMissingFileUrl, $q ); 00961 } elseif ( $wgUploadNavigationUrl ) { 00962 return wfAppendQuery( $wgUploadNavigationUrl, $q ); 00963 } else { 00964 $upload = SpecialPage::getTitleFor( 'Upload' ); 00965 return $upload->getLocalURL( $q ); 00966 } 00967 } 00968 00977 public static function makeMediaLinkObj( $title, $html = '', $time = false ) { 00978 $img = wfFindFile( $title, array( 'time' => $time ) ); 00979 return self::makeMediaLinkFile( $title, $img, $html ); 00980 } 00981 00993 public static function makeMediaLinkFile( Title $title, $file, $html = '' ) { 00994 if ( $file && $file->exists() ) { 00995 $url = $file->getURL(); 00996 $class = 'internal'; 00997 } else { 00998 $url = self::getUploadUrl( $title ); 00999 $class = 'new'; 01000 } 01001 $alt = htmlspecialchars( $title->getText(), ENT_QUOTES ); 01002 if ( $html == '' ) { 01003 $html = $alt; 01004 } 01005 $u = htmlspecialchars( $url ); 01006 return "<a href=\"{$u}\" class=\"$class\" title=\"{$alt}\">{$html}</a>"; 01007 } 01008 01016 public static function specialLink( $name, $key = '' ) { 01017 if ( $key == '' ) { 01018 $key = strtolower( $name ); 01019 } 01020 01021 return self::linkKnown( SpecialPage::getTitleFor( $name ), wfMessage( $key )->text() ); 01022 } 01023 01034 public static function makeExternalLink( $url, $text, $escape = true, $linktype = '', $attribs = array(), $title = null ) { 01035 global $wgTitle; 01036 $class = "external"; 01037 if ( $linktype ) { 01038 $class .= " $linktype"; 01039 } 01040 if ( isset( $attribs['class'] ) && $attribs['class'] ) { 01041 $class .= " {$attribs['class']}"; 01042 } 01043 $attribs['class'] = $class; 01044 01045 if ( $escape ) { 01046 $text = htmlspecialchars( $text ); 01047 } 01048 01049 if ( !$title ) { 01050 $title = $wgTitle; 01051 } 01052 $attribs['rel'] = Parser::getExternalLinkRel( $url, $title ); 01053 $link = ''; 01054 $success = wfRunHooks( 'LinkerMakeExternalLink', 01055 array( &$url, &$text, &$link, &$attribs, $linktype ) ); 01056 if ( !$success ) { 01057 wfDebug( "Hook LinkerMakeExternalLink changed the output of link with url {$url} and text {$text} to {$link}\n", true ); 01058 return $link; 01059 } 01060 $attribs['href'] = $url; 01061 return Html::rawElement( 'a', $attribs, $text ); 01062 } 01063 01072 public static function userLink( $userId, $userName, $altUserName = false ) { 01073 if ( $userId == 0 ) { 01074 $page = SpecialPage::getTitleFor( 'Contributions', $userName ); 01075 if ( $altUserName === false ) { 01076 $altUserName = IP::prettifyIP( $userName ); 01077 } 01078 } else { 01079 $page = Title::makeTitle( NS_USER, $userName ); 01080 } 01081 01082 return self::link( 01083 $page, 01084 htmlspecialchars( $altUserName !== false ? $altUserName : $userName ), 01085 array( 'class' => 'mw-userlink' ) 01086 ); 01087 } 01088 01100 public static function userToolLinks( 01101 $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null 01102 ) { 01103 global $wgUser, $wgDisableAnonTalk, $wgLang; 01104 $talkable = !( $wgDisableAnonTalk && 0 == $userId ); 01105 $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK ); 01106 $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId; 01107 01108 $items = array(); 01109 if ( $talkable ) { 01110 $items[] = self::userTalkLink( $userId, $userText ); 01111 } 01112 if ( $userId ) { 01113 // check if the user has an edit 01114 $attribs = array(); 01115 if ( $redContribsWhenNoEdits ) { 01116 if ( intval( $edits ) === 0 && $edits !== 0 ) { 01117 $user = User::newFromId( $userId ); 01118 $edits = $user->getEditCount(); 01119 } 01120 if ( $edits === 0 ) { 01121 $attribs['class'] = 'new'; 01122 } 01123 } 01124 $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText ); 01125 01126 $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs ); 01127 } 01128 if ( $blockable && $wgUser->isAllowed( 'block' ) ) { 01129 $items[] = self::blockLink( $userId, $userText ); 01130 } 01131 01132 if ( $addEmailLink && $wgUser->canSendEmail() ) { 01133 $items[] = self::emailLink( $userId, $userText ); 01134 } 01135 01136 wfRunHooks( 'UserToolLinksEdit', array( $userId, $userText, &$items ) ); 01137 01138 if ( $items ) { 01139 return wfMessage( 'word-separator' )->plain() 01140 . '<span class="mw-usertoollinks">' 01141 . wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped() 01142 . '</span>'; 01143 } else { 01144 return ''; 01145 } 01146 } 01147 01155 public static function userToolLinksRedContribs( $userId, $userText, $edits = null ) { 01156 return self::userToolLinks( $userId, $userText, true, 0, $edits ); 01157 } 01158 01164 public static function userTalkLink( $userId, $userText ) { 01165 $userTalkPage = Title::makeTitle( NS_USER_TALK, $userText ); 01166 $userTalkLink = self::link( $userTalkPage, wfMessage( 'talkpagelinktext' )->escaped() ); 01167 return $userTalkLink; 01168 } 01169 01175 public static function blockLink( $userId, $userText ) { 01176 $blockPage = SpecialPage::getTitleFor( 'Block', $userText ); 01177 $blockLink = self::link( $blockPage, wfMessage( 'blocklink' )->escaped() ); 01178 return $blockLink; 01179 } 01180 01186 public static function emailLink( $userId, $userText ) { 01187 $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText ); 01188 $emailLink = self::link( $emailPage, wfMessage( 'emaillink' )->escaped() ); 01189 return $emailLink; 01190 } 01191 01198 public static function revUserLink( $rev, $isPublic = false ) { 01199 if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) { 01200 $link = wfMessage( 'rev-deleted-user' )->escaped(); 01201 } elseif ( $rev->userCan( Revision::DELETED_USER ) ) { 01202 $link = self::userLink( $rev->getUser( Revision::FOR_THIS_USER ), 01203 $rev->getUserText( Revision::FOR_THIS_USER ) ); 01204 } else { 01205 $link = wfMessage( 'rev-deleted-user' )->escaped(); 01206 } 01207 if ( $rev->isDeleted( Revision::DELETED_USER ) ) { 01208 return '<span class="history-deleted">' . $link . '</span>'; 01209 } 01210 return $link; 01211 } 01212 01219 public static function revUserTools( $rev, $isPublic = false ) { 01220 if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) { 01221 $link = wfMessage( 'rev-deleted-user' )->escaped(); 01222 } elseif ( $rev->userCan( Revision::DELETED_USER ) ) { 01223 $userId = $rev->getUser( Revision::FOR_THIS_USER ); 01224 $userText = $rev->getUserText( Revision::FOR_THIS_USER ); 01225 $link = self::userLink( $userId, $userText ) 01226 . wfMessage( 'word-separator' )->plain() 01227 . self::userToolLinks( $userId, $userText ); 01228 } else { 01229 $link = wfMessage( 'rev-deleted-user' )->escaped(); 01230 } 01231 if ( $rev->isDeleted( Revision::DELETED_USER ) ) { 01232 return ' <span class="history-deleted">' . $link . '</span>'; 01233 } 01234 return $link; 01235 } 01236 01254 public static function formatComment( $comment, $title = null, $local = false ) { 01255 wfProfileIn( __METHOD__ ); 01256 01257 # Sanitize text a bit: 01258 $comment = str_replace( "\n", " ", $comment ); 01259 # Allow HTML entities (for bug 13815) 01260 $comment = Sanitizer::escapeHtmlAllowEntities( $comment ); 01261 01262 # Render autocomments and make links: 01263 $comment = self::formatAutocomments( $comment, $title, $local ); 01264 $comment = self::formatLinksInComment( $comment, $title, $local ); 01265 01266 wfProfileOut( __METHOD__ ); 01267 return $comment; 01268 } 01269 01273 static $autocommentTitle; 01274 static $autocommentLocal; 01275 01289 private static function formatAutocomments( $comment, $title = null, $local = false ) { 01290 // Bah! 01291 self::$autocommentTitle = $title; 01292 self::$autocommentLocal = $local; 01293 $comment = preg_replace_callback( 01294 '!(.*)/\*\s*(.*?)\s*\*/(.*)!', 01295 array( 'Linker', 'formatAutocommentsCallback' ), 01296 $comment ); 01297 self::$autocommentTitle = null; 01298 self::$autocommentLocal = null; 01299 return $comment; 01300 } 01301 01307 private static function formatAutocommentsCallback( $match ) { 01308 global $wgLang; 01309 $title = self::$autocommentTitle; 01310 $local = self::$autocommentLocal; 01311 01312 $pre = $match[1]; 01313 $auto = $match[2]; 01314 $post = $match[3]; 01315 $comment = null; 01316 wfRunHooks( 'FormatAutocomments', array( &$comment, $pre, $auto, $post, $title, $local ) ); 01317 if ( $comment === null ) { 01318 $link = ''; 01319 if ( $title ) { 01320 $section = $auto; 01321 01322 # Remove links that a user may have manually put in the autosummary 01323 # This could be improved by copying as much of Parser::stripSectionName as desired. 01324 $section = str_replace( '[[:', '', $section ); 01325 $section = str_replace( '[[', '', $section ); 01326 $section = str_replace( ']]', '', $section ); 01327 01328 $section = Sanitizer::normalizeSectionNameWhitespace( $section ); # bug 22784 01329 if ( $local ) { 01330 $sectionTitle = Title::newFromText( '#' . $section ); 01331 } else { 01332 $sectionTitle = Title::makeTitleSafe( $title->getNamespace(), 01333 $title->getDBkey(), $section ); 01334 } 01335 if ( $sectionTitle ) { 01336 $link = self::link( $sectionTitle, 01337 $wgLang->getArrow(), array(), array(), 01338 'noclasses' ); 01339 } else { 01340 $link = ''; 01341 } 01342 } 01343 if ( $pre ) { 01344 # written summary $presep autocomment (summary /* section */) 01345 $pre .= wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped(); 01346 } 01347 if ( $post ) { 01348 # autocomment $postsep written summary (/* section */ summary) 01349 $auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped(); 01350 } 01351 $auto = '<span class="autocomment">' . $auto . '</span>'; 01352 $comment = $pre . $link . $wgLang->getDirMark() . '<span dir="auto">' . $auto . $post . '</span>'; 01353 } 01354 return $comment; 01355 } 01356 01360 static $commentContextTitle; 01361 static $commentLocal; 01362 01373 public static function formatLinksInComment( $comment, $title = null, $local = false ) { 01374 self::$commentContextTitle = $title; 01375 self::$commentLocal = $local; 01376 $html = preg_replace_callback( 01377 '/ 01378 \[\[ 01379 :? # ignore optional leading colon 01380 ([^\]|]+) # 1. link target; page names cannot include ] or | 01381 (?:\| 01382 # 2. a pipe-separated substring; only the last is captured 01383 # Stop matching at | and ]] without relying on backtracking. 01384 ((?:]?[^\]|])*+) 01385 )* 01386 \]\] 01387 ([^[]*) # 3. link trail (the text up until the next link) 01388 /x', 01389 array( 'Linker', 'formatLinksInCommentCallback' ), 01390 $comment ); 01391 self::$commentContextTitle = null; 01392 self::$commentLocal = null; 01393 return $html; 01394 } 01395 01400 protected static function formatLinksInCommentCallback( $match ) { 01401 global $wgContLang; 01402 01403 $medians = '(?:' . preg_quote( MWNamespace::getCanonicalName( NS_MEDIA ), '/' ) . '|'; 01404 $medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):'; 01405 01406 $comment = $match[0]; 01407 01408 # fix up urlencoded title texts (copied from Parser::replaceInternalLinks) 01409 if ( strpos( $match[1], '%' ) !== false ) { 01410 $match[1] = str_replace( array( '<', '>' ), array( '<', '>' ), rawurldecode( $match[1] ) ); 01411 } 01412 01413 # Handle link renaming [[foo|text]] will show link as "text" 01414 if ( $match[2] != "" ) { 01415 $text = $match[2]; 01416 } else { 01417 $text = $match[1]; 01418 } 01419 $submatch = array(); 01420 $thelink = null; 01421 if ( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) { 01422 # Media link; trail not supported. 01423 $linkRegexp = '/\[\[(.*?)\]\]/'; 01424 $title = Title::makeTitleSafe( NS_FILE, $submatch[1] ); 01425 if ( $title ) { 01426 $thelink = self::makeMediaLinkObj( $title, $text ); 01427 } 01428 } else { 01429 # Other kind of link 01430 if ( preg_match( $wgContLang->linkTrail(), $match[3], $submatch ) ) { 01431 $trail = $submatch[1]; 01432 } else { 01433 $trail = ""; 01434 } 01435 $linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/'; 01436 if ( isset( $match[1][0] ) && $match[1][0] == ':' ) { 01437 $match[1] = substr( $match[1], 1 ); 01438 } 01439 list( $inside, $trail ) = self::splitTrail( $trail ); 01440 01441 $linkText = $text; 01442 $linkTarget = self::normalizeSubpageLink( self::$commentContextTitle, 01443 $match[1], $linkText ); 01444 01445 $target = Title::newFromText( $linkTarget ); 01446 if ( $target ) { 01447 if ( $target->getText() == '' && !$target->isExternal() 01448 && !self::$commentLocal && self::$commentContextTitle 01449 ) { 01450 $newTarget = clone ( self::$commentContextTitle ); 01451 $newTarget->setFragment( '#' . $target->getFragment() ); 01452 $target = $newTarget; 01453 } 01454 $thelink = self::link( 01455 $target, 01456 $linkText . $inside 01457 ) . $trail; 01458 } 01459 } 01460 if ( $thelink ) { 01461 // If the link is still valid, go ahead and replace it in! 01462 $comment = preg_replace( $linkRegexp, StringUtils::escapeRegexReplacement( $thelink ), $comment, 1 ); 01463 } 01464 01465 return $comment; 01466 } 01467 01474 public static function normalizeSubpageLink( $contextTitle, $target, &$text ) { 01475 # Valid link forms: 01476 # Foobar -- normal 01477 # :Foobar -- override special treatment of prefix (images, language links) 01478 # /Foobar -- convert to CurrentPage/Foobar 01479 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text 01480 # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage 01481 # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage 01482 01483 wfProfileIn( __METHOD__ ); 01484 $ret = $target; # default return value is no change 01485 01486 # Some namespaces don't allow subpages, 01487 # so only perform processing if subpages are allowed 01488 if ( $contextTitle && MWNamespace::hasSubpages( $contextTitle->getNamespace() ) ) { 01489 $hash = strpos( $target, '#' ); 01490 if ( $hash !== false ) { 01491 $suffix = substr( $target, $hash ); 01492 $target = substr( $target, 0, $hash ); 01493 } else { 01494 $suffix = ''; 01495 } 01496 # bug 7425 01497 $target = trim( $target ); 01498 # Look at the first character 01499 if ( $target != '' && $target[0] === '/' ) { 01500 # / at end means we don't want the slash to be shown 01501 $m = array(); 01502 $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m ); 01503 if ( $trailingSlashes ) { 01504 $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) ); 01505 } else { 01506 $noslash = substr( $target, 1 ); 01507 } 01508 01509 $ret = $contextTitle->getPrefixedText() . '/' . trim( $noslash ) . $suffix; 01510 if ( $text === '' ) { 01511 $text = $target . $suffix; 01512 } # this might be changed for ugliness reasons 01513 } else { 01514 # check for .. subpage backlinks 01515 $dotdotcount = 0; 01516 $nodotdot = $target; 01517 while ( strncmp( $nodotdot, "../", 3 ) == 0 ) { 01518 ++$dotdotcount; 01519 $nodotdot = substr( $nodotdot, 3 ); 01520 } 01521 if ( $dotdotcount > 0 ) { 01522 $exploded = explode( '/', $contextTitle->getPrefixedText() ); 01523 if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page 01524 $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) ); 01525 # / at the end means don't show full path 01526 if ( substr( $nodotdot, -1, 1 ) === '/' ) { 01527 $nodotdot = substr( $nodotdot, 0, -1 ); 01528 if ( $text === '' ) { 01529 $text = $nodotdot . $suffix; 01530 } 01531 } 01532 $nodotdot = trim( $nodotdot ); 01533 if ( $nodotdot != '' ) { 01534 $ret .= '/' . $nodotdot; 01535 } 01536 $ret .= $suffix; 01537 } 01538 } 01539 } 01540 } 01541 01542 wfProfileOut( __METHOD__ ); 01543 return $ret; 01544 } 01545 01556 public static function commentBlock( $comment, $title = null, $local = false ) { 01557 // '*' used to be the comment inserted by the software way back 01558 // in antiquity in case none was provided, here for backwards 01559 // compatibility, acc. to brion -ævar 01560 if ( $comment == '' || $comment == '*' ) { 01561 return ''; 01562 } else { 01563 $formatted = self::formatComment( $comment, $title, $local ); 01564 $formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped(); 01565 return " <span class=\"comment\">$formatted</span>"; 01566 } 01567 } 01568 01578 public static function revComment( Revision $rev, $local = false, $isPublic = false ) { 01579 if ( $rev->getRawComment() == "" ) { 01580 return ""; 01581 } 01582 if ( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) { 01583 $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>"; 01584 } elseif ( $rev->userCan( Revision::DELETED_COMMENT ) ) { 01585 $block = self::commentBlock( $rev->getComment( Revision::FOR_THIS_USER ), 01586 $rev->getTitle(), $local ); 01587 } else { 01588 $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>"; 01589 } 01590 if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) { 01591 return " <span class=\"history-deleted\">$block</span>"; 01592 } 01593 return $block; 01594 } 01595 01600 public static function formatRevisionSize( $size ) { 01601 if ( $size == 0 ) { 01602 $stxt = wfMessage( 'historyempty' )->escaped(); 01603 } else { 01604 $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped(); 01605 $stxt = wfMessage( 'parentheses' )->rawParams( $stxt )->escaped(); 01606 } 01607 return "<span class=\"history-size\">$stxt</span>"; 01608 } 01609 01615 public static function tocIndent() { 01616 return "\n<ul>"; 01617 } 01618 01624 public static function tocUnindent( $level ) { 01625 return "</li>\n" . str_repeat( "</ul>\n</li>\n", $level > 0 ? $level : 0 ); 01626 } 01627 01633 public static function tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex = false ) { 01634 $classes = "toclevel-$level"; 01635 if ( $sectionIndex !== false ) { 01636 $classes .= " tocsection-$sectionIndex"; 01637 } 01638 return "\n<li class=\"$classes\"><a href=\"#" . 01639 $anchor . '"><span class="tocnumber">' . 01640 $tocnumber . '</span> <span class="toctext">' . 01641 $tocline . '</span></a>'; 01642 } 01643 01650 public static function tocLineEnd() { 01651 return "</li>\n"; 01652 } 01653 01661 public static function tocList( $toc, $lang = false ) { 01662 $lang = wfGetLangObj( $lang ); 01663 $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped(); 01664 01665 return '<div id="toc" class="toc">' 01666 . '<div id="toctitle"><h2>' . $title . "</h2></div>\n" 01667 . $toc 01668 . "</ul>\n</div>\n"; 01669 } 01670 01678 public static function generateTOC( $tree ) { 01679 $toc = ''; 01680 $lastLevel = 0; 01681 foreach ( $tree as $section ) { 01682 if ( $section['toclevel'] > $lastLevel ) { 01683 $toc .= self::tocIndent(); 01684 } elseif ( $section['toclevel'] < $lastLevel ) { 01685 $toc .= self::tocUnindent( 01686 $lastLevel - $section['toclevel'] ); 01687 } else { 01688 $toc .= self::tocLineEnd(); 01689 } 01690 01691 $toc .= self::tocLine( $section['anchor'], 01692 $section['line'], $section['number'], 01693 $section['toclevel'], $section['index'] ); 01694 $lastLevel = $section['toclevel']; 01695 } 01696 $toc .= self::tocLineEnd(); 01697 return self::tocList( $toc ); 01698 } 01699 01715 public static function makeHeadline( $level, $attribs, $anchor, $html, $link, $legacyAnchor = false ) { 01716 $ret = "<h$level$attribs" 01717 . "<span class=\"mw-headline\" id=\"$anchor\">$html</span>" 01718 . $link 01719 . "</h$level>"; 01720 if ( $legacyAnchor !== false ) { 01721 $ret = "<div id=\"$legacyAnchor\"></div>$ret"; 01722 } 01723 return $ret; 01724 } 01725 01731 static function splitTrail( $trail ) { 01732 global $wgContLang; 01733 $regex = $wgContLang->linkTrail(); 01734 $inside = ''; 01735 if ( $trail !== '' ) { 01736 $m = array(); 01737 if ( preg_match( $regex, $trail, $m ) ) { 01738 $inside = $m[1]; 01739 $trail = $m[2]; 01740 } 01741 } 01742 return array( $inside, $trail ); 01743 } 01744 01770 public static function generateRollback( $rev, IContextSource $context = null, $options = array( 'verify' ) ) { 01771 if ( $context === null ) { 01772 $context = RequestContext::getMain(); 01773 } 01774 $editCount = false; 01775 if ( in_array( 'verify', $options ) ) { 01776 $editCount = self::getRollbackEditCount( $rev, true ); 01777 if ( $editCount === false ) { 01778 return ''; 01779 } 01780 } 01781 01782 $inner = self::buildRollbackLink( $rev, $context, $editCount ); 01783 01784 if ( !in_array( 'noBrackets', $options ) ) { 01785 $inner = $context->msg( 'brackets' )->rawParams( $inner )->plain(); 01786 } 01787 01788 return '<span class="mw-rollback-link">' . $inner . '</span>'; 01789 } 01790 01806 public static function getRollbackEditCount( $rev, $verify ) { 01807 global $wgShowRollbackEditCount; 01808 if ( !is_int( $wgShowRollbackEditCount ) || !$wgShowRollbackEditCount > 0 ) { 01809 // Nothing has happened, indicate this by returning 'null' 01810 return null; 01811 } 01812 01813 $dbr = wfGetDB( DB_SLAVE ); 01814 01815 // Up to the value of $wgShowRollbackEditCount revisions are counted 01816 $res = $dbr->select( 01817 'revision', 01818 array( 'rev_user_text', 'rev_deleted' ), 01819 // $rev->getPage() returns null sometimes 01820 array( 'rev_page' => $rev->getTitle()->getArticleID() ), 01821 __METHOD__, 01822 array( 01823 'USE INDEX' => array( 'revision' => 'page_timestamp' ), 01824 'ORDER BY' => 'rev_timestamp DESC', 01825 'LIMIT' => $wgShowRollbackEditCount + 1 01826 ) 01827 ); 01828 01829 $editCount = 0; 01830 $moreRevs = false; 01831 foreach ( $res as $row ) { 01832 if ( $rev->getRawUserText() != $row->rev_user_text ) { 01833 if ( $verify && ( $row->rev_deleted & Revision::DELETED_TEXT || $row->rev_deleted & Revision::DELETED_USER ) ) { 01834 // If the user or the text of the revision we might rollback to is deleted in some way we can't rollback 01835 // Similar to the sanity checks in WikiPage::commitRollback 01836 return false; 01837 } 01838 $moreRevs = true; 01839 break; 01840 } 01841 $editCount++; 01842 } 01843 01844 if ( $verify && $editCount <= $wgShowRollbackEditCount && !$moreRevs ) { 01845 // We didn't find at least $wgShowRollbackEditCount revisions made by the current user 01846 // and there weren't any other revisions. That means that the current user is the only 01847 // editor, so we can't rollback 01848 return false; 01849 } 01850 return $editCount; 01851 } 01852 01861 public static function buildRollbackLink( $rev, IContextSource $context = null, $editCount = false ) { 01862 global $wgShowRollbackEditCount, $wgMiserMode; 01863 01864 // To config which pages are effected by miser mode 01865 $disableRollbackEditCountSpecialPage = array( 'Recentchanges', 'Watchlist' ); 01866 01867 if ( $context === null ) { 01868 $context = RequestContext::getMain(); 01869 } 01870 01871 $title = $rev->getTitle(); 01872 $query = array( 01873 'action' => 'rollback', 01874 'from' => $rev->getUserText(), 01875 'token' => $context->getUser()->getEditToken( array( $title->getPrefixedText(), $rev->getUserText() ) ), 01876 ); 01877 if ( $context->getRequest()->getBool( 'bot' ) ) { 01878 $query['bot'] = '1'; 01879 $query['hidediff'] = '1'; // bug 15999 01880 } 01881 01882 $disableRollbackEditCount = false; 01883 if ( $wgMiserMode ) { 01884 foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) { 01885 if ( $context->getTitle()->isSpecial( $specialPage ) ) { 01886 $disableRollbackEditCount = true; 01887 break; 01888 } 01889 } 01890 } 01891 01892 if ( !$disableRollbackEditCount && is_int( $wgShowRollbackEditCount ) && $wgShowRollbackEditCount > 0 ) { 01893 if ( !is_numeric( $editCount ) ) { 01894 $editCount = self::getRollbackEditCount( $rev, false ); 01895 } 01896 01897 if ( $editCount > $wgShowRollbackEditCount ) { 01898 $editCount_output = $context->msg( 'rollbacklinkcount-morethan' )->numParams( $wgShowRollbackEditCount )->parse(); 01899 } else { 01900 $editCount_output = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse(); 01901 } 01902 01903 return self::link( 01904 $title, 01905 $editCount_output, 01906 array( 'title' => $context->msg( 'tooltip-rollback' )->text() ), 01907 $query, 01908 array( 'known', 'noclasses' ) 01909 ); 01910 } else { 01911 return self::link( 01912 $title, 01913 $context->msg( 'rollbacklink' )->escaped(), 01914 array( 'title' => $context->msg( 'tooltip-rollback' )->text() ), 01915 $query, 01916 array( 'known', 'noclasses' ) 01917 ); 01918 } 01919 } 01920 01936 public static function formatTemplates( $templates, $preview = false, $section = false, $more = null ) { 01937 global $wgLang; 01938 wfProfileIn( __METHOD__ ); 01939 01940 $outText = ''; 01941 if ( count( $templates ) > 0 ) { 01942 # Do a batch existence check 01943 $batch = new LinkBatch; 01944 foreach ( $templates as $title ) { 01945 $batch->addObj( $title ); 01946 } 01947 $batch->execute(); 01948 01949 # Construct the HTML 01950 $outText = '<div class="mw-templatesUsedExplanation">'; 01951 if ( $preview ) { 01952 $outText .= wfMessage( 'templatesusedpreview' )->numParams( count( $templates ) ) 01953 ->parseAsBlock(); 01954 } elseif ( $section ) { 01955 $outText .= wfMessage( 'templatesusedsection' )->numParams( count( $templates ) ) 01956 ->parseAsBlock(); 01957 } else { 01958 $outText .= wfMessage( 'templatesused' )->numParams( count( $templates ) ) 01959 ->parseAsBlock(); 01960 } 01961 $outText .= "</div><ul>\n"; 01962 01963 usort( $templates, 'Title::compare' ); 01964 foreach ( $templates as $titleObj ) { 01965 $protected = ''; 01966 $restrictions = $titleObj->getRestrictions( 'edit' ); 01967 if ( $restrictions ) { 01968 // Check backwards-compatible messages 01969 $msg = null; 01970 if ( $restrictions === array( 'sysop' ) ) { 01971 $msg = wfMessage( 'template-protected' ); 01972 } elseif ( $restrictions === array( 'autoconfirmed' ) ) { 01973 $msg = wfMessage( 'template-semiprotected' ); 01974 } 01975 if ( $msg && !$msg->isDisabled() ) { 01976 $protected = $msg->parse(); 01977 } else { 01978 // Construct the message from restriction-level-* 01979 // e.g. restriction-level-sysop, restriction-level-autoconfirmed 01980 $msgs = array(); 01981 foreach ( $restrictions as $r ) { 01982 $msgs[] = wfMessage( "restriction-level-$r" )->parse(); 01983 } 01984 $protected = wfMessage( 'parentheses' ) 01985 ->rawParams( $wgLang->commaList( $msgs ) )->escaped(); 01986 } 01987 } 01988 if ( $titleObj->quickUserCan( 'edit' ) ) { 01989 $editLink = self::link( 01990 $titleObj, 01991 wfMessage( 'editlink' )->text(), 01992 array(), 01993 array( 'action' => 'edit' ) 01994 ); 01995 } else { 01996 $editLink = self::link( 01997 $titleObj, 01998 wfMessage( 'viewsourcelink' )->text(), 01999 array(), 02000 array( 'action' => 'edit' ) 02001 ); 02002 } 02003 $outText .= '<li>' . self::link( $titleObj ) 02004 . wfMessage( 'word-separator' )->escaped() 02005 . wfMessage( 'parentheses' )->rawParams( $editLink )->escaped() 02006 . wfMessage( 'word-separator' )->escaped() 02007 . $protected . '</li>'; 02008 } 02009 02010 if ( $more instanceof Title ) { 02011 $outText .= '<li>' . self::link( $more, wfMessage( 'moredotdotdot' ) ) . '</li>'; 02012 } elseif ( $more ) { 02013 $outText .= "<li>$more</li>"; 02014 } 02015 02016 $outText .= '</ul>'; 02017 } 02018 wfProfileOut( __METHOD__ ); 02019 return $outText; 02020 } 02021 02029 public static function formatHiddenCategories( $hiddencats ) { 02030 wfProfileIn( __METHOD__ ); 02031 02032 $outText = ''; 02033 if ( count( $hiddencats ) > 0 ) { 02034 # Construct the HTML 02035 $outText = '<div class="mw-hiddenCategoriesExplanation">'; 02036 $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock(); 02037 $outText .= "</div><ul>\n"; 02038 02039 foreach ( $hiddencats as $titleObj ) { 02040 $outText .= '<li>' . self::link( $titleObj, null, array(), array(), 'known' ) . "</li>\n"; # If it's hidden, it must exist - no need to check with a LinkBatch 02041 } 02042 $outText .= '</ul>'; 02043 } 02044 wfProfileOut( __METHOD__ ); 02045 return $outText; 02046 } 02047 02055 public static function formatSize( $size ) { 02056 global $wgLang; 02057 return htmlspecialchars( $wgLang->formatSize( $size ) ); 02058 } 02059 02072 public static function titleAttrib( $name, $options = null ) { 02073 wfProfileIn( __METHOD__ ); 02074 02075 $message = wfMessage( "tooltip-$name" ); 02076 02077 if ( !$message->exists() ) { 02078 $tooltip = false; 02079 } else { 02080 $tooltip = $message->text(); 02081 # Compatibility: formerly some tooltips had [alt-.] hardcoded 02082 $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip ); 02083 # Message equal to '-' means suppress it. 02084 if ( $tooltip == '-' ) { 02085 $tooltip = false; 02086 } 02087 } 02088 02089 if ( $options == 'withaccess' ) { 02090 $accesskey = self::accesskey( $name ); 02091 if ( $accesskey !== false ) { 02092 if ( $tooltip === false || $tooltip === '' ) { 02093 $tooltip = wfMessage( 'brackets', $accesskey )->escaped(); 02094 } else { 02095 $tooltip .= wfMessage( 'word-separator' )->escaped(); 02096 $tooltip .= wfMessage( 'brackets', $accesskey )->escaped(); 02097 } 02098 } 02099 } 02100 02101 wfProfileOut( __METHOD__ ); 02102 return $tooltip; 02103 } 02104 02105 static $accesskeycache; 02106 02117 public static function accesskey( $name ) { 02118 if ( isset( self::$accesskeycache[$name] ) ) { 02119 return self::$accesskeycache[$name]; 02120 } 02121 wfProfileIn( __METHOD__ ); 02122 02123 $message = wfMessage( "accesskey-$name" ); 02124 02125 if ( !$message->exists() ) { 02126 $accesskey = false; 02127 } else { 02128 $accesskey = $message->plain(); 02129 if ( $accesskey === '' || $accesskey === '-' ) { 02130 # @todo FIXME: Per standard MW behavior, a value of '-' means to suppress the 02131 # attribute, but this is broken for accesskey: that might be a useful 02132 # value. 02133 $accesskey = false; 02134 } 02135 } 02136 02137 wfProfileOut( __METHOD__ ); 02138 self::$accesskeycache[$name] = $accesskey; 02139 return self::$accesskeycache[$name]; 02140 } 02141 02155 public static function getRevDeleteLink( User $user, Revision $rev, Title $title ) { 02156 $canHide = $user->isAllowed( 'deleterevision' ); 02157 if ( !$canHide && !( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) ) { 02158 return ''; 02159 } 02160 02161 if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) { 02162 return Linker::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops 02163 } else { 02164 if ( $rev->getId() ) { 02165 // RevDelete links using revision ID are stable across 02166 // page deletion and undeletion; use when possible. 02167 $query = array( 02168 'type' => 'revision', 02169 'target' => $title->getPrefixedDBkey(), 02170 'ids' => $rev->getId() 02171 ); 02172 } else { 02173 // Older deleted entries didn't save a revision ID. 02174 // We have to refer to these by timestamp, ick! 02175 $query = array( 02176 'type' => 'archive', 02177 'target' => $title->getPrefixedDBkey(), 02178 'ids' => $rev->getTimestamp() 02179 ); 02180 } 02181 return Linker::revDeleteLink( $query, 02182 $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide ); 02183 } 02184 } 02185 02196 public static function revDeleteLink( $query = array(), $restricted = false, $delete = true ) { 02197 $sp = SpecialPage::getTitleFor( 'Revisiondelete' ); 02198 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted'; 02199 $html = wfMessage( $msgKey )->escaped(); 02200 $tag = $restricted ? 'strong' : 'span'; 02201 $link = self::link( $sp, $html, array(), $query, array( 'known', 'noclasses' ) ); 02202 return Xml::tags( $tag, array( 'class' => 'mw-revdelundel-link' ), wfMessage( 'parentheses' )->rawParams( $link )->escaped() ); 02203 } 02204 02213 public static function revDeleteLinkDisabled( $delete = true ) { 02214 $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted'; 02215 $html = wfMessage( $msgKey )->escaped(); 02216 $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped(); 02217 return Xml::tags( 'span', array( 'class' => 'mw-revdelundel-link' ), $htmlParentheses ); 02218 } 02219 02220 /* Deprecated methods */ 02221 02238 static function makeLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { 02239 wfDeprecated( __METHOD__, '1.21' ); 02240 02241 wfProfileIn( __METHOD__ ); 02242 $query = wfCgiToArray( $query ); 02243 list( $inside, $trail ) = self::splitTrail( $trail ); 02244 if ( $text === '' ) { 02245 $text = self::linkText( $nt ); 02246 } 02247 02248 $ret = self::link( $nt, "$prefix$text$inside", array(), $query ) . $trail; 02249 02250 wfProfileOut( __METHOD__ ); 02251 return $ret; 02252 } 02253 02270 static function makeKnownLinkObj( 02271 $title, $text = '', $query = '', $trail = '', $prefix = '', $aprops = '', $style = '' 02272 ) { 02273 wfDeprecated( __METHOD__, '1.21' ); 02274 02275 wfProfileIn( __METHOD__ ); 02276 02277 if ( $text == '' ) { 02278 $text = self::linkText( $title ); 02279 } 02280 $attribs = Sanitizer::mergeAttributes( 02281 Sanitizer::decodeTagAttributes( $aprops ), 02282 Sanitizer::decodeTagAttributes( $style ) 02283 ); 02284 $query = wfCgiToArray( $query ); 02285 list( $inside, $trail ) = self::splitTrail( $trail ); 02286 02287 $ret = self::link( $title, "$prefix$text$inside", $attribs, $query, 02288 array( 'known', 'noclasses' ) ) . $trail; 02289 02290 wfProfileOut( __METHOD__ ); 02291 return $ret; 02292 } 02293 02298 public static function tooltipAndAccesskeyAttribs( $name ) { 02299 # @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output 02300 # no attribute" instead of "output '' as value for attribute", this 02301 # would be three lines. 02302 $attribs = array( 02303 'title' => self::titleAttrib( $name, 'withaccess' ), 02304 'accesskey' => self::accesskey( $name ) 02305 ); 02306 if ( $attribs['title'] === false ) { 02307 unset( $attribs['title'] ); 02308 } 02309 if ( $attribs['accesskey'] === false ) { 02310 unset( $attribs['accesskey'] ); 02311 } 02312 return $attribs; 02313 } 02314 02319 public static function tooltip( $name, $options = null ) { 02320 # @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output 02321 # no attribute" instead of "output '' as value for attribute", this 02322 # would be two lines. 02323 $tooltip = self::titleAttrib( $name, $options ); 02324 if ( $tooltip === false ) { 02325 return ''; 02326 } 02327 return Xml::expandAttributes( array( 02328 'title' => $tooltip 02329 ) ); 02330 } 02331 } 02332 02336 class DummyLinker { 02337 02346 public function __call( $fname, $args ) { 02347 return call_user_func_array( array( 'Linker', $fname ), $args ); 02348 } 02349 }