00001 <?php
00006 class Parser_LinkHooks extends Parser
00007 {
00013 const VERSION = '1.6.4';
00014
00015 # Flags for Parser::setLinkHook
00016 # Also available as global constants from Defines.php
00017 const SLH_PATTERN = 1;
00018
00019 # Constants needed for external link processing
00020 # Everything except bracket, space, or control characters
00021 const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
00022 const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
00023 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
00024
00028 # Persistent:
00029 var $mLinkHooks;
00030
00038 function __construct( $conf = array() ) {
00039 parent::__construct( $conf );
00040 $this->mLinkHooks = array();
00041 }
00042
00046 function firstCallInit() {
00047 parent::__construct();
00048 if ( !$this->mFirstCall ) {
00049 return;
00050 }
00051 $this->mFirstCall = false;
00052
00053 wfProfileIn( __METHOD__ );
00054
00055 $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
00056 CoreParserFunctions::register( $this );
00057 CoreLinkFunctions::register( $this );
00058 $this->initialiseVariables();
00059
00060 wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
00061 wfProfileOut( __METHOD__ );
00062 }
00063
00088 function setLinkHook( $ns, $callback, $flags = 0 ) {
00089 if( $flags & SLH_PATTERN && !is_string($ns) )
00090 throw new MWException( __METHOD__.'() expecting a regex string pattern.' );
00091 elseif( $flags | ~SLH_PATTERN && !is_int($ns) )
00092 throw new MWException( __METHOD__.'() expecting a namespace index.' );
00093 $oldVal = isset( $this->mLinkHooks[$ns] ) ? $this->mLinkHooks[$ns][0] : null;
00094 $this->mLinkHooks[$ns] = array( $callback, $flags );
00095 return $oldVal;
00096 }
00097
00103 function getLinkHooks() {
00104 return array_keys( $this->mLinkHooks );
00105 }
00106
00113 function replaceInternalLinks2( &$s ) {
00114 global $wgContLang;
00115
00116 wfProfileIn( __METHOD__ );
00117
00118 wfProfileIn( __METHOD__.'-setup' );
00119 static $tc = FALSE, $titleRegex;
00120 if( !$tc ) {
00121 # the % is needed to support urlencoded titles as well
00122 $tc = Title::legalChars() . '#%';
00123 # Match a link having the form [[namespace:link|alternate]]trail
00124
00125 # Match cases where there is no "]]", which might still be images
00126
00127 # Match a valid plain title
00128 $titleRegex = "/^([{$tc}]+)$/sD";
00129 }
00130
00131 $sk = $this->mOptions->getSkin();
00132 $holders = new LinkHolderArray( $this );
00133
00134 if( is_null( $this->mTitle ) ) {
00135 wfProfileOut( __METHOD__ );
00136 wfProfileOut( __METHOD__.'-setup' );
00137 throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
00138 }
00139 $nottalk = !$this->mTitle->isTalkPage();
00140
00141 if($wgContLang->hasVariants()) {
00142 $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText());
00143 } else {
00144 $selflink = array($this->mTitle->getPrefixedText());
00145 }
00146 wfProfileOut( __METHOD__.'-setup' );
00147
00148 $offset = 0;
00149 $offsetStack = array();
00150 $markers = new LinkMarkerReplacer( $this, $holders, array( &$this, 'replaceInternalLinksCallback' ) );
00151 while( true ) {
00152 $startBracketOffset = strpos( $s, '[[', $offset );
00153 $endBracketOffset = strpos( $s, ']]', $offset );
00154 # Finish when there are no more brackets
00155 if( $startBracketOffset === false && $endBracketOffset === false ) break;
00156 # Determine if the bracket is a starting or ending bracket
00157 # When we find both, use the first one
00158 elseif( $startBracketOffset !== false && $endBracketOffset !== false )
00159 $isStart = $startBracketOffset <= $endBracketOffset;
00160 # When we only found one, check which it is
00161 else $isStart = $startBracketOffset !== false;
00162 $bracketOffset = $isStart ? $startBracketOffset : $endBracketOffset;
00163 if( $isStart ) {
00165 # Just push our current offset in the string onto the stack
00166 $offsetStack[] = $startBracketOffset;
00167 } else {
00169 # Pop the start pos for our current link zone off the stack
00170 $startBracketOffset = array_pop($offsetStack);
00171 # Just to clean up the code, lets place offsets on the outer ends
00172 $endBracketOffset += 2;
00173
00174 # Only do logic if we actually have a opening bracket for this
00175 if( isset($startBracketOffset) ) {
00176 # Extract text inside the link
00177 @list( $titleText, $paramText ) = explode('|',
00178 substr($s, $startBracketOffset+2, $endBracketOffset-$startBracketOffset-4), 2);
00179 # Create markers only for valid links
00180 if( preg_match( $titleRegex, $titleText ) ) {
00181 # Store the text for the marker
00182 $marker = $markers->addMarker($titleText, $paramText);
00183 # Replace the current link with the marker
00184 $s = substr($s,0,$startBracketOffset).
00185 $marker.
00186 substr($s, $endBracketOffset);
00187 # We have modified $s, because of this we need to set the
00188 # offset manually since the end position is different now
00189 $offset = $startBracketOffset+strlen($marker);
00190 continue;
00191 }
00192 # ToDo: Some LinkHooks may allow recursive links inside of
00193 # the link text, create a regex that also matches our
00194 # <!-- LINKMARKER ### --> sequence in titles
00195 # ToDO: Some LinkHooks use patterns rather than namespaces
00196 # these need to be tested at this point here
00197 }
00198
00199 }
00200 # Bump our offset to after our current bracket
00201 $offset = $bracketOffset+2;
00202 }
00203
00204
00205 # Now expand our tree
00206 wfProfileIn( __METHOD__.'-expand' );
00207 $s = $markers->expand( $s );
00208 wfProfileOut( __METHOD__.'-expand' );
00209
00210 wfProfileOut( __METHOD__ );
00211 return $holders;
00212 }
00213
00214 function replaceInternalLinksCallback( $parser, $holders, $markers, $titleText, $paramText ) {
00215 wfProfileIn( __METHOD__ );
00216 $wt = isset($paramText) ? "[[$titleText|$paramText]]" : "[[$titleText]]";
00217 wfProfileIn( __METHOD__."-misc" );
00218 # Don't allow internal links to pages containing
00219 # PROTO: where PROTO is a valid URL protocol; these
00220 # should be external links.
00221 if( preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $titleText) ) {
00222 wfProfileOut( __METHOD__ );
00223 return $wt;
00224 }
00225
00226 # Make subpage if necessary
00227 if( $this->areSubpagesAllowed() ) {
00228 $titleText = $this->maybeDoSubpageLink( $titleText, $paramText );
00229 }
00230
00231 # Check for a leading colon and strip it if it is there
00232 $leadingColon = $titleText[0] == ':';
00233 if( $leadingColon ) $titleText = substr( $titleText, 1 );
00234
00235 wfProfileOut( __METHOD__."-misc" );
00236 # Make title object
00237 wfProfileIn( __METHOD__."-title" );
00238 $title = Title::newFromText( $this->mStripState->unstripNoWiki($titleText) );
00239 if( !$title ) {
00240 wfProfileOut( __METHOD__."-title" );
00241 wfProfileOut( __METHOD__ );
00242 return $wt;
00243 }
00244 $ns = $title->getNamespace();
00245 wfProfileOut( __METHOD__."-title" );
00246
00247 # Default for Namespaces is a default link
00248 # ToDo: Default for patterns is plain wikitext
00249 $return = true;
00250 if( isset($this->mLinkHooks[$ns]) ) {
00251 list( $callback, $flags ) = $this->mLinkHooks[$ns];
00252 if( $flags & SLH_PATTERN ) {
00253 $args = array( $parser, $holders, $markers, $titleText, &$paramText, &$leadingColon );
00254 } else {
00255 $args = array( $parser, $holders, $markers, $title, $titleText, &$paramText, &$leadingColon );
00256 }
00257 # Workaround for PHP bug 35229 and similar
00258 if ( !is_callable( $callback ) ) {
00259 throw new MWException( "Tag hook for $name is not callable\n" );
00260 }
00261 $return = call_user_func_array( $callback, $args );
00262 }
00263 if( $return === true ) {
00264 # True (treat as plain link) was returned, call the defaultLinkHook
00265 $args = array( $parser, $holders, $markers, $title, $titleText, &$paramText, &$leadingColon );
00266 $return = call_user_func_array( array( 'CoreLinkFunctions', 'defaultLinkHook' ), $args );
00267 }
00268 if( $return === false ) {
00269 # False (no link) was returned, output plain wikitext
00270 # Build it again as the hook is allowed to modify $paramText
00271 return isset($paramText) ? "[[$titleText|$paramText]]" : "[[$titleText]]";
00272 }
00273 # Content was returned, return it
00274 return $return;
00275 }
00276
00277 }
00278
00279 class LinkMarkerReplacer {
00280
00281 protected $markers, $nextId, $parser, $holders, $callback;
00282
00283 function __construct( $parser, $holders, $callback ) {
00284 $this->nextId = 0;
00285 $this->markers = array();
00286 $this->parser = $parser;
00287 $this->holders = $holders;
00288 $this->callback = $callback;
00289 }
00290
00291 function addMarker($titleText, $paramText) {
00292 $id = $this->nextId++;
00293 $this->markers[$id] = array( $titleText, $paramText );
00294 return "<!-- LINKMARKER $id -->";
00295 }
00296
00297 function findMarker( $string ) {
00298 return (bool) preg_match('/<!-- LINKMARKER [0-9]+ -->/', $string );
00299 }
00300
00301 function expand( $string ) {
00302 return StringUtils::delimiterReplaceCallback( "<!-- LINKMARKER ", " -->", array( &$this, 'callback' ), $string );
00303 }
00304
00305 function callback( $m ) {
00306 $id = intval($m[1]);
00307 if( !array_key_exists($id, $this->markers) ) return $m[0];
00308 $args = $this->markers[$id];
00309 array_unshift( $args, $this );
00310 array_unshift( $args, $this->holders );
00311 array_unshift( $args, $this->parser );
00312 return call_user_func_array( $this->callback, $args );
00313 }
00314
00315 }