MediaWiki  REL1_24
Linker.php
Go to the documentation of this file.
00001 <?php
00032 class Linker {
00036     const TOOL_LINKS_NOBLOCK = 1;
00037     const TOOL_LINKS_EMAIL = 2;
00038 
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         if ( !$target instanceof Title ) {
00196             wfWarn( __METHOD__ . ': Requires $target to be a Title object.' );
00197             return "<!-- ERROR -->$html";
00198         }
00199         wfProfileIn( __METHOD__ );
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 
00265     public static function linkKnown(
00266         $target, $html = null, $customAttribs = array(),
00267         $query = array(), $options = array( 'known', 'noclasses' )
00268     ) {
00269         return self::link( $target, $html, $customAttribs, $query, $options );
00270     }
00271 
00280     private static function linkUrl( $target, $query, $options ) {
00281         wfProfileIn( __METHOD__ );
00282         # We don't want to include fragments for broken links, because they
00283         # generally make no sense.
00284         if ( in_array( 'broken', $options ) && $target->hasFragment() ) {
00285             $target = clone $target;
00286             $target->setFragment( '' );
00287         }
00288 
00289         # If it's a broken link, add the appropriate query pieces, unless
00290         # there's already an action specified, or unless 'edit' makes no sense
00291         # (i.e., for a nonexistent special page).
00292         if ( in_array( 'broken', $options ) && empty( $query['action'] )
00293             && !$target->isSpecialPage() ) {
00294             $query['action'] = 'edit';
00295             $query['redlink'] = '1';
00296         }
00297 
00298         if ( in_array( 'http', $options ) ) {
00299             $proto = PROTO_HTTP;
00300         } elseif ( in_array( 'https', $options ) ) {
00301             $proto = PROTO_HTTPS;
00302         } else {
00303             $proto = PROTO_RELATIVE;
00304         }
00305 
00306         $ret = $target->getLinkURL( $query, false, $proto );
00307         wfProfileOut( __METHOD__ );
00308         return $ret;
00309     }
00310 
00320     private static function linkAttribs( $target, $attribs, $options ) {
00321         wfProfileIn( __METHOD__ );
00322         global $wgUser;
00323         $defaults = array();
00324 
00325         if ( !in_array( 'noclasses', $options ) ) {
00326             wfProfileIn( __METHOD__ . '-getClasses' );
00327             # Now build the classes.
00328             $classes = array();
00329 
00330             if ( in_array( 'broken', $options ) ) {
00331                 $classes[] = 'new';
00332             }
00333 
00334             if ( $target->isExternal() ) {
00335                 $classes[] = 'extiw';
00336             }
00337 
00338             if ( !in_array( 'broken', $options ) ) { # Avoid useless calls to LinkCache (see r50387)
00339                 $colour = self::getLinkColour( $target, $wgUser->getStubThreshold() );
00340                 if ( $colour !== '' ) {
00341                     $classes[] = $colour; # mw-redirect or stub
00342                 }
00343             }
00344             if ( $classes != array() ) {
00345                 $defaults['class'] = implode( ' ', $classes );
00346             }
00347             wfProfileOut( __METHOD__ . '-getClasses' );
00348         }
00349 
00350         # Get a default title attribute.
00351         if ( $target->getPrefixedText() == '' ) {
00352             # A link like [[#Foo]].  This used to mean an empty title
00353             # attribute, but that's silly.  Just don't output a title.
00354         } elseif ( in_array( 'known', $options ) ) {
00355             $defaults['title'] = $target->getPrefixedText();
00356         } else {
00357             $defaults['title'] = wfMessage( 'red-link-title', $target->getPrefixedText() )->text();
00358         }
00359 
00360         # Finally, merge the custom attribs with the default ones, and iterate
00361         # over that, deleting all "false" attributes.
00362         $ret = array();
00363         $merged = Sanitizer::mergeAttributes( $defaults, $attribs );
00364         foreach ( $merged as $key => $val ) {
00365             # A false value suppresses the attribute, and we don't want the
00366             # href attribute to be overridden.
00367             if ( $key != 'href' and $val !== false ) {
00368                 $ret[$key] = $val;
00369             }
00370         }
00371         wfProfileOut( __METHOD__ );
00372         return $ret;
00373     }
00374 
00382     private static function linkText( $target ) {
00383         if ( !$target instanceof Title ) {
00384             wfWarn( __METHOD__ . ': Requires $target to be a Title object.' );
00385             return '';
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 
00393         return htmlspecialchars( $target->getPrefixedText() );
00394     }
00395 
00410     public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '' ) {
00411         $ret = "<strong class=\"selflink\">{$prefix}{$html}</strong>{$trail}";
00412         if ( !wfRunHooks( 'SelfLinkBegin', array( $nt, &$html, &$trail, &$prefix, &$ret ) ) ) {
00413             return $ret;
00414         }
00415 
00416         if ( $html == '' ) {
00417             $html = htmlspecialchars( $nt->getPrefixedText() );
00418         }
00419         list( $inside, $trail ) = self::splitTrail( $trail );
00420         return "<strong class=\"selflink\">{$prefix}{$html}{$inside}</strong>{$trail}";
00421     }
00422 
00433     public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
00434         global $wgContLang;
00435 
00436         // First we check whether the namespace exists or not.
00437         if ( MWNamespace::exists( $namespace ) ) {
00438             if ( $namespace == NS_MAIN ) {
00439                 $name = $context->msg( 'blanknamespace' )->text();
00440             } else {
00441                 $name = $wgContLang->getFormattedNsText( $namespace );
00442             }
00443             return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
00444         } else {
00445             return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text();
00446         }
00447     }
00448 
00453     static function normaliseSpecialPage( Title $title ) {
00454         if ( $title->isSpecialPage() ) {
00455             list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
00456             if ( !$name ) {
00457                 return $title;
00458             }
00459             $ret = SpecialPage::getTitleFor( $name, $subpage, $title->getFragment() );
00460             return $ret;
00461         } else {
00462             return $title;
00463         }
00464     }
00465 
00474     private static function fnamePart( $url ) {
00475         $basename = strrchr( $url, '/' );
00476         if ( false === $basename ) {
00477             $basename = $url;
00478         } else {
00479             $basename = substr( $basename, 1 );
00480         }
00481         return $basename;
00482     }
00483 
00493     public static function makeExternalImage( $url, $alt = '' ) {
00494         if ( $alt == '' ) {
00495             $alt = self::fnamePart( $url );
00496         }
00497         $img = '';
00498         $success = wfRunHooks( 'LinkerMakeExternalImage', array( &$url, &$alt, &$img ) );
00499         if ( !$success ) {
00500             wfDebug( "Hook LinkerMakeExternalImage changed the output of external image "
00501                 . "with url {$url} and alt text {$alt} to {$img}\n", true );
00502             return $img;
00503         }
00504         return Html::element( 'img',
00505             array(
00506                 'src' => $url,
00507                 'alt' => $alt ) );
00508     }
00509 
00546     public static function makeImageLink( Parser $parser, Title $title,
00547         $file, $frameParams = array(), $handlerParams = array(), $time = false,
00548         $query = "", $widthOption = null
00549     ) {
00550         $res = null;
00551         $dummy = new DummyLinker;
00552         if ( !wfRunHooks( 'ImageBeforeProduceHTML', array( &$dummy, &$title,
00553             &$file, &$frameParams, &$handlerParams, &$time, &$res ) ) ) {
00554             return $res;
00555         }
00556 
00557         if ( $file && !$file->allowInlineDisplay() ) {
00558             wfDebug( __METHOD__ . ': ' . $title->getPrefixedDBkey() . " does not allow inline display\n" );
00559             return self::link( $title );
00560         }
00561 
00562         // Shortcuts
00563         $fp =& $frameParams;
00564         $hp =& $handlerParams;
00565 
00566         // Clean up parameters
00567         $page = isset( $hp['page'] ) ? $hp['page'] : false;
00568         if ( !isset( $fp['align'] ) ) {
00569             $fp['align'] = '';
00570         }
00571         if ( !isset( $fp['alt'] ) ) {
00572             $fp['alt'] = '';
00573         }
00574         if ( !isset( $fp['title'] ) ) {
00575             $fp['title'] = '';
00576         }
00577         if ( !isset( $fp['class'] ) ) {
00578             $fp['class'] = '';
00579         }
00580 
00581         $prefix = $postfix = '';
00582 
00583         if ( 'center' == $fp['align'] ) {
00584             $prefix = '<div class="center">';
00585             $postfix = '</div>';
00586             $fp['align'] = 'none';
00587         }
00588         if ( $file && !isset( $hp['width'] ) ) {
00589             if ( isset( $hp['height'] ) && $file->isVectorized() ) {
00590                 // If its a vector image, and user only specifies height
00591                 // we don't want it to be limited by its "normal" width.
00592                 global $wgSVGMaxSize;
00593                 $hp['width'] = $wgSVGMaxSize;
00594             } else {
00595                 $hp['width'] = $file->getWidth( $page );
00596             }
00597 
00598             if ( isset( $fp['thumbnail'] )
00599                 || isset( $fp['manualthumb'] )
00600                 || isset( $fp['framed'] )
00601                 || isset( $fp['frameless'] )
00602                 || !$hp['width']
00603             ) {
00604                 global $wgThumbLimits, $wgThumbUpright;
00605 
00606                 if ( $widthOption === null || !isset( $wgThumbLimits[$widthOption] ) ) {
00607                     $widthOption = User::getDefaultOption( 'thumbsize' );
00608                 }
00609 
00610                 // Reduce width for upright images when parameter 'upright' is used
00611                 if ( isset( $fp['upright'] ) && $fp['upright'] == 0 ) {
00612                     $fp['upright'] = $wgThumbUpright;
00613                 }
00614 
00615                 // For caching health: If width scaled down due to upright
00616                 // parameter, round to full __0 pixel to avoid the creation of a
00617                 // lot of odd thumbs.
00618                 $prefWidth = isset( $fp['upright'] ) ?
00619                     round( $wgThumbLimits[$widthOption] * $fp['upright'], -1 ) :
00620                     $wgThumbLimits[$widthOption];
00621 
00622                 // Use width which is smaller: real image width or user preference width
00623                 // Unless image is scalable vector.
00624                 if ( !isset( $hp['height'] ) && ( $hp['width'] <= 0 ||
00625                         $prefWidth < $hp['width'] || $file->isVectorized() ) ) {
00626                     $hp['width'] = $prefWidth;
00627                 }
00628             }
00629         }
00630 
00631         if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) ) {
00632             # Create a thumbnail. Alignment depends on the writing direction of
00633             # the page content language (right-aligned for LTR languages,
00634             # left-aligned for RTL languages)
00635             #
00636             # If a thumbnail width has not been provided, it is set
00637             # to the default user option as specified in Language*.php
00638             if ( $fp['align'] == '' ) {
00639                 $fp['align'] = $parser->getTargetLanguage()->alignEnd();
00640             }
00641             return $prefix . self::makeThumbLink2( $title, $file, $fp, $hp, $time, $query ) . $postfix;
00642         }
00643 
00644         if ( $file && isset( $fp['frameless'] ) ) {
00645             $srcWidth = $file->getWidth( $page );
00646             # For "frameless" option: do not present an image bigger than the
00647             # source (for bitmap-style images). This is the same behavior as the
00648             # "thumb" option does it already.
00649             if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
00650                 $hp['width'] = $srcWidth;
00651             }
00652         }
00653 
00654         if ( $file && isset( $hp['width'] ) ) {
00655             # Create a resized image, without the additional thumbnail features
00656             $thumb = $file->transform( $hp );
00657         } else {
00658             $thumb = false;
00659         }
00660 
00661         if ( !$thumb ) {
00662             $s = self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true );
00663         } else {
00664             self::processResponsiveImages( $file, $thumb, $hp );
00665             $params = array(
00666                 'alt' => $fp['alt'],
00667                 'title' => $fp['title'],
00668                 'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false,
00669                 'img-class' => $fp['class'] );
00670             if ( isset( $fp['border'] ) ) {
00671                 $params['img-class'] .= ( $params['img-class'] !== '' ? ' ' : '' ) . 'thumbborder';
00672             }
00673             $params = self::getImageLinkMTOParams( $fp, $query, $parser ) + $params;
00674 
00675             $s = $thumb->toHtml( $params );
00676         }
00677         if ( $fp['align'] != '' ) {
00678             $s = "<div class=\"float{$fp['align']}\">{$s}</div>";
00679         }
00680         return str_replace( "\n", ' ', $prefix . $s . $postfix );
00681     }
00682 
00688     public static function makeImageLink2( Title $title, $file, $frameParams = array(),
00689         $handlerParams = array(), $time = false, $query = "", $widthOption = null ) {
00690         return self::makeImageLink( null, $title, $file, $frameParams,
00691             $handlerParams, $time, $query, $widthOption );
00692     }
00693 
00702     private static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
00703         $mtoParams = array();
00704         if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
00705             $mtoParams['custom-url-link'] = $frameParams['link-url'];
00706             if ( isset( $frameParams['link-target'] ) ) {
00707                 $mtoParams['custom-target-link'] = $frameParams['link-target'];
00708             }
00709             if ( $parser ) {
00710                 $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
00711                 foreach ( $extLinkAttrs as $name => $val ) {
00712                     // Currently could include 'rel' and 'target'
00713                     $mtoParams['parser-extlink-' . $name] = $val;
00714                 }
00715             }
00716         } elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
00717             $mtoParams['custom-title-link'] = self::normaliseSpecialPage( $frameParams['link-title'] );
00718         } elseif ( !empty( $frameParams['no-link'] ) ) {
00719             // No link
00720         } else {
00721             $mtoParams['desc-link'] = true;
00722             $mtoParams['desc-query'] = $query;
00723         }
00724         return $mtoParams;
00725     }
00726 
00739     public static function makeThumbLinkObj( Title $title, $file, $label = '', $alt,
00740         $align = 'right', $params = array(), $framed = false, $manualthumb = ""
00741     ) {
00742         $frameParams = array(
00743             'alt' => $alt,
00744             'caption' => $label,
00745             'align' => $align
00746         );
00747         if ( $framed ) {
00748             $frameParams['framed'] = true;
00749         }
00750         if ( $manualthumb ) {
00751             $frameParams['manualthumb'] = $manualthumb;
00752         }
00753         return self::makeThumbLink2( $title, $file, $frameParams, $params );
00754     }
00755 
00765     public static function makeThumbLink2( Title $title, $file, $frameParams = array(),
00766         $handlerParams = array(), $time = false, $query = ""
00767     ) {
00768         $exists = $file && $file->exists();
00769 
00770         # Shortcuts
00771         $fp =& $frameParams;
00772         $hp =& $handlerParams;
00773 
00774         $page = isset( $hp['page'] ) ? $hp['page'] : false;
00775         if ( !isset( $fp['align'] ) ) {
00776             $fp['align'] = 'right';
00777         }
00778         if ( !isset( $fp['alt'] ) ) {
00779             $fp['alt'] = '';
00780         }
00781         if ( !isset( $fp['title'] ) ) {
00782             $fp['title'] = '';
00783         }
00784         if ( !isset( $fp['caption'] ) ) {
00785             $fp['caption'] = '';
00786         }
00787 
00788         if ( empty( $hp['width'] ) ) {
00789             // Reduce width for upright images when parameter 'upright' is used
00790             $hp['width'] = isset( $fp['upright'] ) ? 130 : 180;
00791         }
00792         $thumb = false;
00793         $noscale = false;
00794         $manualthumb = false;
00795 
00796         if ( !$exists ) {
00797             $outerWidth = $hp['width'] + 2;
00798         } else {
00799             if ( isset( $fp['manualthumb'] ) ) {
00800                 # Use manually specified thumbnail
00801                 $manual_title = Title::makeTitleSafe( NS_FILE, $fp['manualthumb'] );
00802                 if ( $manual_title ) {
00803                     $manual_img = wfFindFile( $manual_title );
00804                     if ( $manual_img ) {
00805                         $thumb = $manual_img->getUnscaledThumb( $hp );
00806                         $manualthumb = true;
00807                     } else {
00808                         $exists = false;
00809                     }
00810                 }
00811             } elseif ( isset( $fp['framed'] ) ) {
00812                 // Use image dimensions, don't scale
00813                 $thumb = $file->getUnscaledThumb( $hp );
00814                 $noscale = true;
00815             } else {
00816                 # Do not present an image bigger than the source, for bitmap-style images
00817                 # This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
00818                 $srcWidth = $file->getWidth( $page );
00819                 if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
00820                     $hp['width'] = $srcWidth;
00821                 }
00822                 $thumb = $file->transform( $hp );
00823             }
00824 
00825             if ( $thumb ) {
00826                 $outerWidth = $thumb->getWidth() + 2;
00827             } else {
00828                 $outerWidth = $hp['width'] + 2;
00829             }
00830         }
00831 
00832         # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
00833         # So we don't need to pass it here in $query. However, the URL for the
00834         # zoom icon still needs it, so we make a unique query for it. See bug 14771
00835         $url = $title->getLocalURL( $query );
00836         if ( $page ) {
00837             $url = wfAppendQuery( $url, array( 'page' => $page ) );
00838         }
00839         if ( $manualthumb
00840             && !isset( $fp['link-title'] )
00841             && !isset( $fp['link-url'] )
00842             && !isset( $fp['no-link'] ) ) {
00843             $fp['link-url'] = $url;
00844         }
00845 
00846         $s = "<div class=\"thumb t{$fp['align']}\">"
00847             . "<div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
00848 
00849         if ( !$exists ) {
00850             $s .= self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true );
00851             $zoomIcon = '';
00852         } elseif ( !$thumb ) {
00853             $s .= wfMessage( 'thumbnail_error', '' )->escaped();
00854             $zoomIcon = '';
00855         } else {
00856             if ( !$noscale && !$manualthumb ) {
00857                 self::processResponsiveImages( $file, $thumb, $hp );
00858             }
00859             $params = array(
00860                 'alt' => $fp['alt'],
00861                 'title' => $fp['title'],
00862                 'img-class' => ( isset( $fp['class'] ) && $fp['class'] !== ''
00863                     ? $fp['class'] . ' '
00864                     : '' ) . 'thumbimage'
00865             );
00866             $params = self::getImageLinkMTOParams( $fp, $query ) + $params;
00867             $s .= $thumb->toHtml( $params );
00868             if ( isset( $fp['framed'] ) ) {
00869                 $zoomIcon = "";
00870             } else {
00871                 $zoomIcon = Html::rawElement( 'div', array( 'class' => 'magnify' ),
00872                     Html::rawElement( 'a', array(
00873                         'href' => $url,
00874                         'class' => 'internal',
00875                         'title' => wfMessage( 'thumbnail-more' )->text() ),
00876                         "" ) );
00877             }
00878         }
00879         $s .= '  <div class="thumbcaption">' . $zoomIcon . $fp['caption'] . "</div></div></div>";
00880         return str_replace( "\n", ' ', $s );
00881     }
00882 
00891     public static function processResponsiveImages( $file, $thumb, $hp ) {
00892         global $wgResponsiveImages;
00893         if ( $wgResponsiveImages && $thumb && !$thumb->isError() ) {
00894             $hp15 = $hp;
00895             $hp15['width'] = round( $hp['width'] * 1.5 );
00896             $hp20 = $hp;
00897             $hp20['width'] = $hp['width'] * 2;
00898             if ( isset( $hp['height'] ) ) {
00899                 $hp15['height'] = round( $hp['height'] * 1.5 );
00900                 $hp20['height'] = $hp['height'] * 2;
00901             }
00902 
00903             $thumb15 = $file->transform( $hp15 );
00904             $thumb20 = $file->transform( $hp20 );
00905             if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
00906                 $thumb->responsiveUrls['1.5'] = $thumb15->getUrl();
00907             }
00908             if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
00909                 $thumb->responsiveUrls['2'] = $thumb20->getUrl();
00910             }
00911         }
00912     }
00913 
00925     public static function makeBrokenImageLinkObj( $title, $label = '',
00926         $query = '', $unused1 = '', $unused2 = '', $time = false
00927     ) {
00928         if ( !$title instanceof Title ) {
00929             wfWarn( __METHOD__ . ': Requires $title to be a Title object.' );
00930             return "<!-- ERROR -->" . htmlspecialchars( $label );
00931         }
00932 
00933         global $wgEnableUploads, $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
00934         wfProfileIn( __METHOD__ );
00935         if ( $label == '' ) {
00936             $label = $title->getPrefixedText();
00937         }
00938         $encLabel = htmlspecialchars( $label );
00939         $currentExists = $time ? ( wfFindFile( $title ) != false ) : false;
00940 
00941         if ( ( $wgUploadMissingFileUrl || $wgUploadNavigationUrl || $wgEnableUploads )
00942             && !$currentExists
00943         ) {
00944             $redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title );
00945 
00946             if ( $redir ) {
00947                 wfProfileOut( __METHOD__ );
00948                 return self::linkKnown( $title, $encLabel, array(), wfCgiToArray( $query ) );
00949             }
00950 
00951             $href = self::getUploadUrl( $title, $query );
00952 
00953             wfProfileOut( __METHOD__ );
00954             return '<a href="' . htmlspecialchars( $href ) . '" class="new" title="' .
00955                 htmlspecialchars( $title->getPrefixedText(), ENT_QUOTES ) . '">' .
00956                 $encLabel . '</a>';
00957         }
00958 
00959         wfProfileOut( __METHOD__ );
00960         return self::linkKnown( $title, $encLabel, array(), wfCgiToArray( $query ) );
00961     }
00962 
00970     protected static function getUploadUrl( $destFile, $query = '' ) {
00971         global $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
00972         $q = 'wpDestFile=' . $destFile->getPartialURL();
00973         if ( $query != '' ) {
00974             $q .= '&' . $query;
00975         }
00976 
00977         if ( $wgUploadMissingFileUrl ) {
00978             return wfAppendQuery( $wgUploadMissingFileUrl, $q );
00979         } elseif ( $wgUploadNavigationUrl ) {
00980             return wfAppendQuery( $wgUploadNavigationUrl, $q );
00981         } else {
00982             $upload = SpecialPage::getTitleFor( 'Upload' );
00983             return $upload->getLocalURL( $q );
00984         }
00985     }
00986 
00995     public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
00996         $img = wfFindFile( $title, array( 'time' => $time ) );
00997         return self::makeMediaLinkFile( $title, $img, $html );
00998     }
00999 
01011     public static function makeMediaLinkFile( Title $title, $file, $html = '' ) {
01012         if ( $file && $file->exists() ) {
01013             $url = $file->getURL();
01014             $class = 'internal';
01015         } else {
01016             $url = self::getUploadUrl( $title );
01017             $class = 'new';
01018         }
01019 
01020         $alt = $title->getText();
01021         if ( $html == '' ) {
01022             $html = $alt;
01023         }
01024 
01025         $ret = '';
01026         $attribs = array(
01027             'href' => $url,
01028             'class' => $class,
01029             'title' => $alt
01030         );
01031 
01032         if ( !wfRunHooks( 'LinkerMakeMediaLinkFile',
01033             array( $title, $file, &$html, &$attribs, &$ret ) ) ) {
01034             wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
01035                 . "with url {$url} and text {$html} to {$ret}\n", true );
01036             return $ret;
01037         }
01038 
01039         return Html::rawElement( 'a', $attribs, $html );
01040     }
01041 
01051     public static function specialLink( $name, $key = '' ) {
01052         if ( $key == '' ) {
01053             $key = strtolower( $name );
01054         }
01055 
01056         return self::linkKnown( SpecialPage::getTitleFor( $name ), wfMessage( $key )->text() );
01057     }
01058 
01069     public static function makeExternalLink( $url, $text, $escape = true,
01070         $linktype = '', $attribs = array(), $title = null
01071     ) {
01072         global $wgTitle;
01073         $class = "external";
01074         if ( $linktype ) {
01075             $class .= " $linktype";
01076         }
01077         if ( isset( $attribs['class'] ) && $attribs['class'] ) {
01078             $class .= " {$attribs['class']}";
01079         }
01080         $attribs['class'] = $class;
01081 
01082         if ( $escape ) {
01083             $text = htmlspecialchars( $text );
01084         }
01085 
01086         if ( !$title ) {
01087             $title = $wgTitle;
01088         }
01089         $attribs['rel'] = Parser::getExternalLinkRel( $url, $title );
01090         $link = '';
01091         $success = wfRunHooks( 'LinkerMakeExternalLink',
01092             array( &$url, &$text, &$link, &$attribs, $linktype ) );
01093         if ( !$success ) {
01094             wfDebug( "Hook LinkerMakeExternalLink changed the output of link "
01095                 . "with url {$url} and text {$text} to {$link}\n", true );
01096             return $link;
01097         }
01098         $attribs['href'] = $url;
01099         return Html::rawElement( 'a', $attribs, $text );
01100     }
01101 
01110     public static function userLink( $userId, $userName, $altUserName = false ) {
01111         $classes = 'mw-userlink';
01112         if ( $userId == 0 ) {
01113             $page = SpecialPage::getTitleFor( 'Contributions', $userName );
01114             if ( $altUserName === false ) {
01115                 $altUserName = IP::prettifyIP( $userName );
01116             }
01117             $classes .= ' mw-anonuserlink'; // Separate link class for anons (bug 43179)
01118         } else {
01119             $page = Title::makeTitle( NS_USER, $userName );
01120         }
01121 
01122         return self::link(
01123             $page,
01124             htmlspecialchars( $altUserName !== false ? $altUserName : $userName ),
01125             array( 'class' => $classes )
01126         );
01127     }
01128 
01141     public static function userToolLinks(
01142         $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
01143     ) {
01144         global $wgUser, $wgDisableAnonTalk, $wgLang;
01145         $talkable = !( $wgDisableAnonTalk && 0 == $userId );
01146         $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
01147         $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
01148 
01149         $items = array();
01150         if ( $talkable ) {
01151             $items[] = self::userTalkLink( $userId, $userText );
01152         }
01153         if ( $userId ) {
01154             // check if the user has an edit
01155             $attribs = array();
01156             if ( $redContribsWhenNoEdits ) {
01157                 if ( intval( $edits ) === 0 && $edits !== 0 ) {
01158                     $user = User::newFromId( $userId );
01159                     $edits = $user->getEditCount();
01160                 }
01161                 if ( $edits === 0 ) {
01162                     $attribs['class'] = 'new';
01163                 }
01164             }
01165             $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
01166 
01167             $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
01168         }
01169         if ( $blockable && $wgUser->isAllowed( 'block' ) ) {
01170             $items[] = self::blockLink( $userId, $userText );
01171         }
01172 
01173         if ( $addEmailLink && $wgUser->canSendEmail() ) {
01174             $items[] = self::emailLink( $userId, $userText );
01175         }
01176 
01177         wfRunHooks( 'UserToolLinksEdit', array( $userId, $userText, &$items ) );
01178 
01179         if ( $items ) {
01180             return wfMessage( 'word-separator' )->plain()
01181                 . '<span class="mw-usertoollinks">'
01182                 . wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped()
01183                 . '</span>';
01184         } else {
01185             return '';
01186         }
01187     }
01188 
01196     public static function userToolLinksRedContribs( $userId, $userText, $edits = null ) {
01197         return self::userToolLinks( $userId, $userText, true, 0, $edits );
01198     }
01199 
01205     public static function userTalkLink( $userId, $userText ) {
01206         $userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
01207         $userTalkLink = self::link( $userTalkPage, wfMessage( 'talkpagelinktext' )->escaped() );
01208         return $userTalkLink;
01209     }
01210 
01216     public static function blockLink( $userId, $userText ) {
01217         $blockPage = SpecialPage::getTitleFor( 'Block', $userText );
01218         $blockLink = self::link( $blockPage, wfMessage( 'blocklink' )->escaped() );
01219         return $blockLink;
01220     }
01221 
01227     public static function emailLink( $userId, $userText ) {
01228         $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
01229         $emailLink = self::link( $emailPage, wfMessage( 'emaillink' )->escaped() );
01230         return $emailLink;
01231     }
01232 
01239     public static function revUserLink( $rev, $isPublic = false ) {
01240         if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
01241             $link = wfMessage( 'rev-deleted-user' )->escaped();
01242         } elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
01243             $link = self::userLink( $rev->getUser( Revision::FOR_THIS_USER ),
01244                 $rev->getUserText( Revision::FOR_THIS_USER ) );
01245         } else {
01246             $link = wfMessage( 'rev-deleted-user' )->escaped();
01247         }
01248         if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
01249             return '<span class="history-deleted">' . $link . '</span>';
01250         }
01251         return $link;
01252     }
01253 
01260     public static function revUserTools( $rev, $isPublic = false ) {
01261         if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
01262             $link = wfMessage( 'rev-deleted-user' )->escaped();
01263         } elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
01264             $userId = $rev->getUser( Revision::FOR_THIS_USER );
01265             $userText = $rev->getUserText( Revision::FOR_THIS_USER );
01266             $link = self::userLink( $userId, $userText )
01267                 . wfMessage( 'word-separator' )->plain()
01268                 . self::userToolLinks( $userId, $userText );
01269         } else {
01270             $link = wfMessage( 'rev-deleted-user' )->escaped();
01271         }
01272         if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
01273             return ' <span class="history-deleted">' . $link . '</span>';
01274         }
01275         return $link;
01276     }
01277 
01295     public static function formatComment( $comment, $title = null, $local = false ) {
01296         wfProfileIn( __METHOD__ );
01297 
01298         # Sanitize text a bit:
01299         $comment = str_replace( "\n", " ", $comment );
01300         # Allow HTML entities (for bug 13815)
01301         $comment = Sanitizer::escapeHtmlAllowEntities( $comment );
01302 
01303         # Render autocomments and make links:
01304         $comment = self::formatAutocomments( $comment, $title, $local );
01305         $comment = self::formatLinksInComment( $comment, $title, $local );
01306 
01307         wfProfileOut( __METHOD__ );
01308         return $comment;
01309     }
01310 
01324     private static function formatAutocomments( $comment, $title = null, $local = false ) {
01325         return preg_replace_callback(
01326             '!(.*)/\*\s*(.*?)\s*\*/(.*)!',
01327             function ( $match ) use ( $title, $local ) {
01328                 global $wgLang;
01329 
01330                 $pre = $match[1];
01331                 $auto = $match[2];
01332                 $post = $match[3];
01333                 $comment = null;
01334                 wfRunHooks( 'FormatAutocomments', array( &$comment, $pre, $auto, $post, $title, $local ) );
01335                 if ( $comment === null ) {
01336                     $link = '';
01337                     if ( $title ) {
01338                         $section = $auto;
01339                         # Remove links that a user may have manually put in the autosummary
01340                         # This could be improved by copying as much of Parser::stripSectionName as desired.
01341                         $section = str_replace( '[[:', '', $section );
01342                         $section = str_replace( '[[', '', $section );
01343                         $section = str_replace( ']]', '', $section );
01344 
01345                         $section = Sanitizer::normalizeSectionNameWhitespace( $section ); # bug 22784
01346                         if ( $local ) {
01347                             $sectionTitle = Title::newFromText( '#' . $section );
01348                         } else {
01349                             $sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
01350                                 $title->getDBkey(), $section );
01351                         }
01352                         if ( $sectionTitle ) {
01353                             $link = Linker::link( $sectionTitle,
01354                                 $wgLang->getArrow(), array(), array(),
01355                                 'noclasses' );
01356                         } else {
01357                             $link = '';
01358                         }
01359                     }
01360                     if ( $pre ) {
01361                         # written summary $presep autocomment (summary /* section */)
01362                         $pre .= wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped();
01363                     }
01364                     if ( $post ) {
01365                         # autocomment $postsep written summary (/* section */ summary)
01366                         $auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped();
01367                     }
01368                     $auto = '<span class="autocomment">' . $auto . '</span>';
01369                     $comment = $pre . $link . $wgLang->getDirMark()
01370                         . '<span dir="auto">' . $auto . $post . '</span>';
01371                 }
01372                 return $comment;
01373             },
01374             $comment
01375         );
01376     }
01377 
01388     public static function formatLinksInComment( $comment, $title = null, $local = false ) {
01389         return preg_replace_callback(
01390             '/
01391                 \[\[
01392                 :? # ignore optional leading colon
01393                 ([^\]|]+) # 1. link target; page names cannot include ] or |
01394                 (?:\|
01395                     # 2. a pipe-separated substring; only the last is captured
01396                     # Stop matching at | and ]] without relying on backtracking.
01397                     ((?:]?[^\]|])*+)
01398                 )*
01399                 \]\]
01400                 ([^[]*) # 3. link trail (the text up until the next link)
01401             /x',
01402             function ( $match ) use ( $title, $local ) {
01403                 global $wgContLang;
01404 
01405                 $medians = '(?:' . preg_quote( MWNamespace::getCanonicalName( NS_MEDIA ), '/' ) . '|';
01406                 $medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):';
01407 
01408                 $comment = $match[0];
01409 
01410                 # fix up urlencoded title texts (copied from Parser::replaceInternalLinks)
01411                 if ( strpos( $match[1], '%' ) !== false ) {
01412                     $match[1] = str_replace(
01413                         array( '<', '>' ),
01414                         array( '&lt;', '&gt;' ),
01415                         rawurldecode( $match[1] )
01416                     );
01417                 }
01418 
01419                 # Handle link renaming [[foo|text]] will show link as "text"
01420                 if ( $match[2] != "" ) {
01421                     $text = $match[2];
01422                 } else {
01423                     $text = $match[1];
01424                 }
01425                 $submatch = array();
01426                 $thelink = null;
01427                 if ( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
01428                     # Media link; trail not supported.
01429                     $linkRegexp = '/\[\[(.*?)\]\]/';
01430                     $title = Title::makeTitleSafe( NS_FILE, $submatch[1] );
01431                     if ( $title ) {
01432                         $thelink = Linker::makeMediaLinkObj( $title, $text );
01433                     }
01434                 } else {
01435                     # Other kind of link
01436                     if ( preg_match( $wgContLang->linkTrail(), $match[3], $submatch ) ) {
01437                         $trail = $submatch[1];
01438                     } else {
01439                         $trail = "";
01440                     }
01441                     $linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
01442                     if ( isset( $match[1][0] ) && $match[1][0] == ':' ) {
01443                         $match[1] = substr( $match[1], 1 );
01444                     }
01445                     list( $inside, $trail ) = Linker::splitTrail( $trail );
01446 
01447                     $linkText = $text;
01448                     $linkTarget = Linker::normalizeSubpageLink( $title, $match[1], $linkText );
01449 
01450                     $target = Title::newFromText( $linkTarget );
01451                     if ( $target ) {
01452                         if ( $target->getText() == '' && !$target->isExternal()
01453                             && !$local && $title
01454                         ) {
01455                             $newTarget = clone ( $title );
01456                             $newTarget->setFragment( '#' . $target->getFragment() );
01457                             $target = $newTarget;
01458                         }
01459                         $thelink = Linker::link(
01460                             $target,
01461                             $linkText . $inside
01462                         ) . $trail;
01463                     }
01464                 }
01465                 if ( $thelink ) {
01466                     // If the link is still valid, go ahead and replace it in!
01467                     $comment = preg_replace(
01468                         $linkRegexp,
01469                         StringUtils::escapeRegexReplacement( $thelink ),
01470                         $comment,
01471                         1
01472                     );
01473                 }
01474 
01475                 return $comment;
01476             },
01477             $comment
01478         );
01479     }
01480 
01487     public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
01488         # Valid link forms:
01489         # Foobar -- normal
01490         # :Foobar -- override special treatment of prefix (images, language links)
01491         # /Foobar -- convert to CurrentPage/Foobar
01492         # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
01493         # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
01494         # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
01495 
01496         wfProfileIn( __METHOD__ );
01497         $ret = $target; # default return value is no change
01498 
01499         # Some namespaces don't allow subpages,
01500         # so only perform processing if subpages are allowed
01501         if ( $contextTitle && MWNamespace::hasSubpages( $contextTitle->getNamespace() ) ) {
01502             $hash = strpos( $target, '#' );
01503             if ( $hash !== false ) {
01504                 $suffix = substr( $target, $hash );
01505                 $target = substr( $target, 0, $hash );
01506             } else {
01507                 $suffix = '';
01508             }
01509             # bug 7425
01510             $target = trim( $target );
01511             # Look at the first character
01512             if ( $target != '' && $target[0] === '/' ) {
01513                 # / at end means we don't want the slash to be shown
01514                 $m = array();
01515                 $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
01516                 if ( $trailingSlashes ) {
01517                     $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
01518                 } else {
01519                     $noslash = substr( $target, 1 );
01520                 }
01521 
01522                 $ret = $contextTitle->getPrefixedText() . '/' . trim( $noslash ) . $suffix;
01523                 if ( $text === '' ) {
01524                     $text = $target . $suffix;
01525                 } # this might be changed for ugliness reasons
01526             } else {
01527                 # check for .. subpage backlinks
01528                 $dotdotcount = 0;
01529                 $nodotdot = $target;
01530                 while ( strncmp( $nodotdot, "../", 3 ) == 0 ) {
01531                     ++$dotdotcount;
01532                     $nodotdot = substr( $nodotdot, 3 );
01533                 }
01534                 if ( $dotdotcount > 0 ) {
01535                     $exploded = explode( '/', $contextTitle->getPrefixedText() );
01536                     if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
01537                         $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
01538                         # / at the end means don't show full path
01539                         if ( substr( $nodotdot, -1, 1 ) === '/' ) {
01540                             $nodotdot = substr( $nodotdot, 0, -1 );
01541                             if ( $text === '' ) {
01542                                 $text = $nodotdot . $suffix;
01543                             }
01544                         }
01545                         $nodotdot = trim( $nodotdot );
01546                         if ( $nodotdot != '' ) {
01547                             $ret .= '/' . $nodotdot;
01548                         }
01549                         $ret .= $suffix;
01550                     }
01551                 }
01552             }
01553         }
01554 
01555         wfProfileOut( __METHOD__ );
01556         return $ret;
01557     }
01558 
01569     public static function commentBlock( $comment, $title = null, $local = false ) {
01570         // '*' used to be the comment inserted by the software way back
01571         // in antiquity in case none was provided, here for backwards
01572         // compatibility, acc. to brion -ævar
01573         if ( $comment == '' || $comment == '*' ) {
01574             return '';
01575         } else {
01576             $formatted = self::formatComment( $comment, $title, $local );
01577             $formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
01578             return " <span class=\"comment\">$formatted</span>";
01579         }
01580     }
01581 
01591     public static function revComment( Revision $rev, $local = false, $isPublic = false ) {
01592         if ( $rev->getRawComment() == "" ) {
01593             return "";
01594         }
01595         if ( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) {
01596             $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
01597         } elseif ( $rev->userCan( Revision::DELETED_COMMENT ) ) {
01598             $block = self::commentBlock( $rev->getComment( Revision::FOR_THIS_USER ),
01599                 $rev->getTitle(), $local );
01600         } else {
01601             $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
01602         }
01603         if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
01604             return " <span class=\"history-deleted\">$block</span>";
01605         }
01606         return $block;
01607     }
01608 
01613     public static function formatRevisionSize( $size ) {
01614         if ( $size == 0 ) {
01615             $stxt = wfMessage( 'historyempty' )->escaped();
01616         } else {
01617             $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
01618             $stxt = wfMessage( 'parentheses' )->rawParams( $stxt )->escaped();
01619         }
01620         return "<span class=\"history-size\">$stxt</span>";
01621     }
01622 
01628     public static function tocIndent() {
01629         return "\n<ul>";
01630     }
01631 
01638     public static function tocUnindent( $level ) {
01639         return "</li>\n" . str_repeat( "</ul>\n</li>\n", $level > 0 ? $level : 0 );
01640     }
01641 
01652     public static function tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex = false ) {
01653         $classes = "toclevel-$level";
01654         if ( $sectionIndex !== false ) {
01655             $classes .= " tocsection-$sectionIndex";
01656         }
01657         return "\n<li class=\"$classes\"><a href=\"#" .
01658             $anchor . '"><span class="tocnumber">' .
01659             $tocnumber . '</span> <span class="toctext">' .
01660             $tocline . '</span></a>';
01661     }
01662 
01669     public static function tocLineEnd() {
01670         return "</li>\n";
01671     }
01672 
01680     public static function tocList( $toc, $lang = false ) {
01681         $lang = wfGetLangObj( $lang );
01682         $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped();
01683 
01684         return '<div id="toc" class="toc">'
01685             . '<div id="toctitle"><h2>' . $title . "</h2></div>\n"
01686             . $toc
01687             . "</ul>\n</div>\n";
01688     }
01689 
01697     public static function generateTOC( $tree ) {
01698         $toc = '';
01699         $lastLevel = 0;
01700         foreach ( $tree as $section ) {
01701             if ( $section['toclevel'] > $lastLevel ) {
01702                 $toc .= self::tocIndent();
01703             } elseif ( $section['toclevel'] < $lastLevel ) {
01704                 $toc .= self::tocUnindent(
01705                     $lastLevel - $section['toclevel'] );
01706             } else {
01707                 $toc .= self::tocLineEnd();
01708             }
01709 
01710             $toc .= self::tocLine( $section['anchor'],
01711                 $section['line'], $section['number'],
01712                 $section['toclevel'], $section['index'] );
01713             $lastLevel = $section['toclevel'];
01714         }
01715         $toc .= self::tocLineEnd();
01716         return self::tocList( $toc );
01717     }
01718 
01734     public static function makeHeadline( $level, $attribs, $anchor, $html,
01735         $link, $legacyAnchor = false
01736     ) {
01737         $ret = "<h$level$attribs"
01738             . "<span class=\"mw-headline\" id=\"$anchor\">$html</span>"
01739             . $link
01740             . "</h$level>";
01741         if ( $legacyAnchor !== false ) {
01742             $ret = "<div id=\"$legacyAnchor\"></div>$ret";
01743         }
01744         return $ret;
01745     }
01746 
01753     static function splitTrail( $trail ) {
01754         global $wgContLang;
01755         $regex = $wgContLang->linkTrail();
01756         $inside = '';
01757         if ( $trail !== '' ) {
01758             $m = array();
01759             if ( preg_match( $regex, $trail, $m ) ) {
01760                 $inside = $m[1];
01761                 $trail = $m[2];
01762             }
01763         }
01764         return array( $inside, $trail );
01765     }
01766 
01792     public static function generateRollback( $rev, IContextSource $context = null,
01793         $options = array( 'verify' )
01794     ) {
01795         if ( $context === null ) {
01796             $context = RequestContext::getMain();
01797         }
01798 
01799         $editCount = false;
01800         if ( in_array( 'verify', $options ) ) {
01801             $editCount = self::getRollbackEditCount( $rev, true );
01802             if ( $editCount === false ) {
01803                 return '';
01804             }
01805         }
01806 
01807         $inner = self::buildRollbackLink( $rev, $context, $editCount );
01808 
01809         if ( !in_array( 'noBrackets', $options ) ) {
01810             $inner = $context->msg( 'brackets' )->rawParams( $inner )->plain();
01811         }
01812 
01813         return '<span class="mw-rollback-link">' . $inner . '</span>';
01814     }
01815 
01831     public static function getRollbackEditCount( $rev, $verify ) {
01832         global $wgShowRollbackEditCount;
01833         if ( !is_int( $wgShowRollbackEditCount ) || !$wgShowRollbackEditCount > 0 ) {
01834             // Nothing has happened, indicate this by returning 'null'
01835             return null;
01836         }
01837 
01838         $dbr = wfGetDB( DB_SLAVE );
01839 
01840         // Up to the value of $wgShowRollbackEditCount revisions are counted
01841         $res = $dbr->select(
01842             'revision',
01843             array( 'rev_user_text', 'rev_deleted' ),
01844             // $rev->getPage() returns null sometimes
01845             array( 'rev_page' => $rev->getTitle()->getArticleID() ),
01846             __METHOD__,
01847             array(
01848                 'USE INDEX' => array( 'revision' => 'page_timestamp' ),
01849                 'ORDER BY' => 'rev_timestamp DESC',
01850                 'LIMIT' => $wgShowRollbackEditCount + 1
01851             )
01852         );
01853 
01854         $editCount = 0;
01855         $moreRevs = false;
01856         foreach ( $res as $row ) {
01857             if ( $rev->getRawUserText() != $row->rev_user_text ) {
01858                 if ( $verify &&
01859                     ( $row->rev_deleted & Revision::DELETED_TEXT
01860                         || $row->rev_deleted & Revision::DELETED_USER
01861                 ) ) {
01862                     // If the user or the text of the revision we might rollback
01863                     // to is deleted in some way we can't rollback. Similar to
01864                     // the sanity checks in WikiPage::commitRollback.
01865                     return false;
01866                 }
01867                 $moreRevs = true;
01868                 break;
01869             }
01870             $editCount++;
01871         }
01872 
01873         if ( $verify && $editCount <= $wgShowRollbackEditCount && !$moreRevs ) {
01874             // We didn't find at least $wgShowRollbackEditCount revisions made by the current user
01875             // and there weren't any other revisions. That means that the current user is the only
01876             // editor, so we can't rollback
01877             return false;
01878         }
01879         return $editCount;
01880     }
01881 
01890     public static function buildRollbackLink( $rev, IContextSource $context = null,
01891         $editCount = false
01892     ) {
01893         global $wgShowRollbackEditCount, $wgMiserMode;
01894 
01895         // To config which pages are effected by miser mode
01896         $disableRollbackEditCountSpecialPage = array( 'Recentchanges', 'Watchlist' );
01897 
01898         if ( $context === null ) {
01899             $context = RequestContext::getMain();
01900         }
01901 
01902         $title = $rev->getTitle();
01903         $query = array(
01904             'action' => 'rollback',
01905             'from' => $rev->getUserText(),
01906             'token' => $context->getUser()->getEditToken( array(
01907                 $title->getPrefixedText(),
01908                 $rev->getUserText()
01909             ) ),
01910         );
01911         if ( $context->getRequest()->getBool( 'bot' ) ) {
01912             $query['bot'] = '1';
01913             $query['hidediff'] = '1'; // bug 15999
01914         }
01915 
01916         $disableRollbackEditCount = false;
01917         if ( $wgMiserMode ) {
01918             foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
01919                 if ( $context->getTitle()->isSpecial( $specialPage ) ) {
01920                     $disableRollbackEditCount = true;
01921                     break;
01922                 }
01923             }
01924         }
01925 
01926         if ( !$disableRollbackEditCount
01927             && is_int( $wgShowRollbackEditCount )
01928             && $wgShowRollbackEditCount > 0
01929         ) {
01930             if ( !is_numeric( $editCount ) ) {
01931                 $editCount = self::getRollbackEditCount( $rev, false );
01932             }
01933 
01934             if ( $editCount > $wgShowRollbackEditCount ) {
01935                 $editCount_output = $context->msg( 'rollbacklinkcount-morethan' )
01936                     ->numParams( $wgShowRollbackEditCount )->parse();
01937             } else {
01938                 $editCount_output = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse();
01939             }
01940 
01941             return self::link(
01942                 $title,
01943                 $editCount_output,
01944                 array( 'title' => $context->msg( 'tooltip-rollback' )->text() ),
01945                 $query,
01946                 array( 'known', 'noclasses' )
01947             );
01948         } else {
01949             return self::link(
01950                 $title,
01951                 $context->msg( 'rollbacklink' )->escaped(),
01952                 array( 'title' => $context->msg( 'tooltip-rollback' )->text() ),
01953                 $query,
01954                 array( 'known', 'noclasses' )
01955             );
01956         }
01957     }
01958 
01974     public static function formatTemplates( $templates, $preview = false,
01975         $section = false, $more = null
01976     ) {
01977         global $wgLang;
01978         wfProfileIn( __METHOD__ );
01979 
01980         $outText = '';
01981         if ( count( $templates ) > 0 ) {
01982             # Do a batch existence check
01983             $batch = new LinkBatch;
01984             foreach ( $templates as $title ) {
01985                 $batch->addObj( $title );
01986             }
01987             $batch->execute();
01988 
01989             # Construct the HTML
01990             $outText = '<div class="mw-templatesUsedExplanation">';
01991             if ( $preview ) {
01992                 $outText .= wfMessage( 'templatesusedpreview' )->numParams( count( $templates ) )
01993                     ->parseAsBlock();
01994             } elseif ( $section ) {
01995                 $outText .= wfMessage( 'templatesusedsection' )->numParams( count( $templates ) )
01996                     ->parseAsBlock();
01997             } else {
01998                 $outText .= wfMessage( 'templatesused' )->numParams( count( $templates ) )
01999                     ->parseAsBlock();
02000             }
02001             $outText .= "</div><ul>\n";
02002 
02003             usort( $templates, 'Title::compare' );
02004             foreach ( $templates as $titleObj ) {
02005                 $protected = '';
02006                 $restrictions = $titleObj->getRestrictions( 'edit' );
02007                 if ( $restrictions ) {
02008                     // Check backwards-compatible messages
02009                     $msg = null;
02010                     if ( $restrictions === array( 'sysop' ) ) {
02011                         $msg = wfMessage( 'template-protected' );
02012                     } elseif ( $restrictions === array( 'autoconfirmed' ) ) {
02013                         $msg = wfMessage( 'template-semiprotected' );
02014                     }
02015                     if ( $msg && !$msg->isDisabled() ) {
02016                         $protected = $msg->parse();
02017                     } else {
02018                         // Construct the message from restriction-level-*
02019                         // e.g. restriction-level-sysop, restriction-level-autoconfirmed
02020                         $msgs = array();
02021                         foreach ( $restrictions as $r ) {
02022                             $msgs[] = wfMessage( "restriction-level-$r" )->parse();
02023                         }
02024                         $protected = wfMessage( 'parentheses' )
02025                             ->rawParams( $wgLang->commaList( $msgs ) )->escaped();
02026                     }
02027                 }
02028                 if ( $titleObj->quickUserCan( 'edit' ) ) {
02029                     $editLink = self::link(
02030                         $titleObj,
02031                         wfMessage( 'editlink' )->text(),
02032                         array(),
02033                         array( 'action' => 'edit' )
02034                     );
02035                 } else {
02036                     $editLink = self::link(
02037                         $titleObj,
02038                         wfMessage( 'viewsourcelink' )->text(),
02039                         array(),
02040                         array( 'action' => 'edit' )
02041                     );
02042                 }
02043                 $outText .= '<li>' . self::link( $titleObj )
02044                     . wfMessage( 'word-separator' )->escaped()
02045                     . wfMessage( 'parentheses' )->rawParams( $editLink )->escaped()
02046                     . wfMessage( 'word-separator' )->escaped()
02047                     . $protected . '</li>';
02048             }
02049 
02050             if ( $more instanceof Title ) {
02051                 $outText .= '<li>' . self::link( $more, wfMessage( 'moredotdotdot' ) ) . '</li>';
02052             } elseif ( $more ) {
02053                 $outText .= "<li>$more</li>";
02054             }
02055 
02056             $outText .= '</ul>';
02057         }
02058         wfProfileOut( __METHOD__ );
02059         return $outText;
02060     }
02061 
02069     public static function formatHiddenCategories( $hiddencats ) {
02070         wfProfileIn( __METHOD__ );
02071 
02072         $outText = '';
02073         if ( count( $hiddencats ) > 0 ) {
02074             # Construct the HTML
02075             $outText = '<div class="mw-hiddenCategoriesExplanation">';
02076             $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
02077             $outText .= "</div><ul>\n";
02078 
02079             foreach ( $hiddencats as $titleObj ) {
02080                 # If it's hidden, it must exist - no need to check with a LinkBatch
02081                 $outText .= '<li>'
02082                     . self::link( $titleObj, null, array(), array(), 'known' )
02083                     . "</li>\n";
02084             }
02085             $outText .= '</ul>';
02086         }
02087         wfProfileOut( __METHOD__ );
02088         return $outText;
02089     }
02090 
02098     public static function formatSize( $size ) {
02099         global $wgLang;
02100         return htmlspecialchars( $wgLang->formatSize( $size ) );
02101     }
02102 
02115     public static function titleAttrib( $name, $options = null ) {
02116         wfProfileIn( __METHOD__ );
02117 
02118         $message = wfMessage( "tooltip-$name" );
02119 
02120         if ( !$message->exists() ) {
02121             $tooltip = false;
02122         } else {
02123             $tooltip = $message->text();
02124             # Compatibility: formerly some tooltips had [alt-.] hardcoded
02125             $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
02126             # Message equal to '-' means suppress it.
02127             if ( $tooltip == '-' ) {
02128                 $tooltip = false;
02129             }
02130         }
02131 
02132         if ( $options == 'withaccess' ) {
02133             $accesskey = self::accesskey( $name );
02134             if ( $accesskey !== false ) {
02135                 // Should be build the same as in jquery.accessKeyLabel.js
02136                 if ( $tooltip === false || $tooltip === '' ) {
02137                     $tooltip = wfMessage( 'brackets', $accesskey )->text();
02138                 } else {
02139                     $tooltip .= wfMessage( 'word-separator' )->text();
02140                     $tooltip .= wfMessage( 'brackets', $accesskey )->text();
02141                 }
02142             }
02143         }
02144 
02145         wfProfileOut( __METHOD__ );
02146         return $tooltip;
02147     }
02148 
02149     public static $accesskeycache;
02150 
02161     public static function accesskey( $name ) {
02162         if ( isset( self::$accesskeycache[$name] ) ) {
02163             return self::$accesskeycache[$name];
02164         }
02165         wfProfileIn( __METHOD__ );
02166 
02167         $message = wfMessage( "accesskey-$name" );
02168 
02169         if ( !$message->exists() ) {
02170             $accesskey = false;
02171         } else {
02172             $accesskey = $message->plain();
02173             if ( $accesskey === '' || $accesskey === '-' ) {
02174                 # @todo FIXME: Per standard MW behavior, a value of '-' means to suppress the
02175                 # attribute, but this is broken for accesskey: that might be a useful
02176                 # value.
02177                 $accesskey = false;
02178             }
02179         }
02180 
02181         wfProfileOut( __METHOD__ );
02182         self::$accesskeycache[$name] = $accesskey;
02183         return self::$accesskeycache[$name];
02184     }
02185 
02199     public static function getRevDeleteLink( User $user, Revision $rev, Title $title ) {
02200         $canHide = $user->isAllowed( 'deleterevision' );
02201         if ( !$canHide && !( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) ) {
02202             return '';
02203         }
02204 
02205         if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) {
02206             return Linker::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
02207         } else {
02208             if ( $rev->getId() ) {
02209                 // RevDelete links using revision ID are stable across
02210                 // page deletion and undeletion; use when possible.
02211                 $query = array(
02212                     'type' => 'revision',
02213                     'target' => $title->getPrefixedDBkey(),
02214                     'ids' => $rev->getId()
02215                 );
02216             } else {
02217                 // Older deleted entries didn't save a revision ID.
02218                 // We have to refer to these by timestamp, ick!
02219                 $query = array(
02220                     'type' => 'archive',
02221                     'target' => $title->getPrefixedDBkey(),
02222                     'ids' => $rev->getTimestamp()
02223                 );
02224             }
02225             return Linker::revDeleteLink( $query,
02226                 $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
02227         }
02228     }
02229 
02240     public static function revDeleteLink( $query = array(), $restricted = false, $delete = true ) {
02241         $sp = SpecialPage::getTitleFor( 'Revisiondelete' );
02242         $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
02243         $html = wfMessage( $msgKey )->escaped();
02244         $tag = $restricted ? 'strong' : 'span';
02245         $link = self::link( $sp, $html, array(), $query, array( 'known', 'noclasses' ) );
02246         return Xml::tags(
02247             $tag,
02248             array( 'class' => 'mw-revdelundel-link' ),
02249             wfMessage( 'parentheses' )->rawParams( $link )->escaped()
02250         );
02251     }
02252 
02261     public static function revDeleteLinkDisabled( $delete = true ) {
02262         $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
02263         $html = wfMessage( $msgKey )->escaped();
02264         $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
02265         return Xml::tags( 'span', array( 'class' => 'mw-revdelundel-link' ), $htmlParentheses );
02266     }
02267 
02268     /* Deprecated methods */
02269 
02286     static function makeLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
02287         wfDeprecated( __METHOD__, '1.21' );
02288 
02289         wfProfileIn( __METHOD__ );
02290         $query = wfCgiToArray( $query );
02291         list( $inside, $trail ) = self::splitTrail( $trail );
02292         if ( $text === '' ) {
02293             $text = self::linkText( $nt );
02294         }
02295 
02296         $ret = self::link( $nt, "$prefix$text$inside", array(), $query ) . $trail;
02297 
02298         wfProfileOut( __METHOD__ );
02299         return $ret;
02300     }
02301 
02318     static function makeKnownLinkObj(
02319         $title, $text = '', $query = '', $trail = '', $prefix = '', $aprops = '', $style = ''
02320     ) {
02321         wfDeprecated( __METHOD__, '1.21' );
02322 
02323         wfProfileIn( __METHOD__ );
02324 
02325         if ( $text == '' ) {
02326             $text = self::linkText( $title );
02327         }
02328         $attribs = Sanitizer::mergeAttributes(
02329             Sanitizer::decodeTagAttributes( $aprops ),
02330             Sanitizer::decodeTagAttributes( $style )
02331         );
02332         $query = wfCgiToArray( $query );
02333         list( $inside, $trail ) = self::splitTrail( $trail );
02334 
02335         $ret = self::link( $title, "$prefix$text$inside", $attribs, $query,
02336             array( 'known', 'noclasses' ) ) . $trail;
02337 
02338         wfProfileOut( __METHOD__ );
02339         return $ret;
02340     }
02341 
02347     public static function tooltipAndAccesskeyAttribs( $name ) {
02348         # @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output
02349         # no attribute" instead of "output '' as value for attribute", this
02350         # would be three lines.
02351         $attribs = array(
02352             'title' => self::titleAttrib( $name, 'withaccess' ),
02353             'accesskey' => self::accesskey( $name )
02354         );
02355         if ( $attribs['title'] === false ) {
02356             unset( $attribs['title'] );
02357         }
02358         if ( $attribs['accesskey'] === false ) {
02359             unset( $attribs['accesskey'] );
02360         }
02361         return $attribs;
02362     }
02363 
02370     public static function tooltip( $name, $options = null ) {
02371         # @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output
02372         # no attribute" instead of "output '' as value for attribute", this
02373         # would be two lines.
02374         $tooltip = self::titleAttrib( $name, $options );
02375         if ( $tooltip === false ) {
02376             return '';
02377         }
02378         return Xml::expandAttributes( array(
02379             'title' => $tooltip
02380         ) );
02381     }
02382 }
02383 
02387 class DummyLinker {
02388 
02397     public function __call( $fname, $args ) {
02398         return call_user_func_array( array( 'Linker', $fname ), $args );
02399     }
02400 }