]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/parser/Parser_LinkHooks.php
MediaWiki 1.17.0
[autoinstalls/mediawiki.git] / includes / parser / Parser_LinkHooks.php
1 <?php
2 /**
3  * Modified version of the PHP parser with hooks for wiki links; experimental
4  *
5  * @file
6  */
7
8 /**
9  * Parser with LinkHooks experiment
10  * @ingroup Parser
11  */
12 class Parser_LinkHooks extends Parser
13 {
14         /**
15          * Update this version number when the ParserOutput format
16          * changes in an incompatible way, so the parser cache
17          * can automatically discard old data.
18          */
19         const VERSION = '1.6.4';
20         
21         # Flags for Parser::setLinkHook
22         # Also available as global constants from Defines.php
23         const SLH_PATTERN = 1;
24
25         # Constants needed for external link processing
26         # Everything except bracket, space, or control characters
27         const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
28         const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
29                 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
30
31         /**#@+
32          * @private
33          */
34         # Persistent:
35         var $mLinkHooks;
36
37         /**#@-*/
38
39         /**
40          * Constructor
41          *
42          * @public
43          */
44         function __construct( $conf = array() ) {
45                 parent::__construct( $conf );
46                 $this->mLinkHooks = array();
47         }
48
49         /**
50          * Do various kinds of initialisation on the first call of the parser
51          */
52         function firstCallInit() {
53                 parent::__construct();
54                 if ( !$this->mFirstCall ) {
55                         return;
56                 }
57                 $this->mFirstCall = false;
58
59                 wfProfileIn( __METHOD__ );
60
61                 $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
62                 CoreParserFunctions::register( $this );
63                 CoreLinkFunctions::register( $this );
64                 $this->initialiseVariables();
65
66                 wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
67                 wfProfileOut( __METHOD__ );
68         }
69
70         /**
71          * Create a link hook, e.g. [[Namepsace:...|display}}
72          * The callback function should have the form:
73          *    function myLinkCallback( $parser, $holders, $markers,
74          *      Title $title, $titleText, &$sortText = null, &$leadingColon = false ) { ... }
75          *
76          * Or with SLH_PATTERN:
77          *    function myLinkCallback( $parser, $holders, $markers, )
78          *      &$titleText, &$sortText = null, &$leadingColon = false ) { ... }
79          *
80          * The callback may either return a number of different possible values:
81          * String) Text result of the link
82          * True) (Treat as link) Parse the link according to normal link rules
83          * False) (Bad link) Just output the raw wikitext (You may modify the text first)
84          *
85          * @public
86          *
87          * @param $ns Integer or String: the Namespace ID or regex pattern if SLH_PATTERN is set
88          * @param $callback Mixed: the callback function (and object) to use
89          * @param $flags Integer: a combination of the following flags:
90          *     SLH_PATTERN   Use a regex link pattern rather than a namespace
91          *
92          * @return The old callback function for this name, if any
93          */
94         function setLinkHook( $ns, $callback, $flags = 0 ) {
95                 if( $flags & SLH_PATTERN && !is_string($ns) )
96                         throw new MWException( __METHOD__.'() expecting a regex string pattern.' );
97                 elseif( $flags | ~SLH_PATTERN && !is_int($ns) )
98                         throw new MWException( __METHOD__.'() expecting a namespace index.' );
99                 $oldVal = isset( $this->mLinkHooks[$ns] ) ? $this->mLinkHooks[$ns][0] : null;
100                 $this->mLinkHooks[$ns] = array( $callback, $flags );
101                 return $oldVal;
102         }
103         
104         /**
105          * Get all registered link hook identifiers
106          *
107          * @return array
108          */
109         function getLinkHooks() {
110                 return array_keys( $this->mLinkHooks );
111         }
112         
113         /**
114          * Process [[ ]] wikilinks
115          * @return LinkHolderArray
116          *
117          * @private
118          */
119         function replaceInternalLinks2( &$s ) {
120                 wfProfileIn( __METHOD__ );
121
122                 wfProfileIn( __METHOD__.'-setup' );
123                 static $tc = FALSE, $titleRegex;//$e1, $e1_img;
124                 if( !$tc ) {
125                         # the % is needed to support urlencoded titles as well
126                         $tc = Title::legalChars() . '#%';
127                         # Match a link having the form [[namespace:link|alternate]]trail
128                         //$e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
129                         # Match cases where there is no "]]", which might still be images
130                         //$e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
131                         # Match a valid plain title
132                         $titleRegex = "/^([{$tc}]+)$/sD";
133                 }
134
135                 $holders = new LinkHolderArray( $this );
136                 
137                 if( is_null( $this->mTitle ) ) {
138                         wfProfileOut( __METHOD__ );
139                         wfProfileOut( __METHOD__.'-setup' );
140                         throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
141                 }
142
143                 wfProfileOut( __METHOD__.'-setup' );
144                 
145                 $offset = 0;
146                 $offsetStack = array();
147                 $markers = new LinkMarkerReplacer( $this, $holders, array( &$this, 'replaceInternalLinksCallback' ) );
148                 while( true ) {
149                         $startBracketOffset = strpos( $s, '[[', $offset );
150                         $endBracketOffset   = strpos( $s, ']]', $offset );
151                         # Finish when there are no more brackets
152                         if( $startBracketOffset === false && $endBracketOffset === false ) break;
153                         # Determine if the bracket is a starting or ending bracket
154                         # When we find both, use the first one
155                         elseif( $startBracketOffset !== false && $endBracketOffset !== false )
156                              $isStart = $startBracketOffset <= $endBracketOffset;
157                         # When we only found one, check which it is
158                         else $isStart = $startBracketOffset !== false;
159                         $bracketOffset = $isStart ? $startBracketOffset : $endBracketOffset;
160                         if( $isStart ) {
161                                 /** Opening bracket **/
162                                 # Just push our current offset in the string onto the stack
163                                 $offsetStack[] = $startBracketOffset;
164                         } else {
165                                 /** Closing bracket **/
166                                 # Pop the start pos for our current link zone off the stack
167                                 $startBracketOffset = array_pop($offsetStack);
168                                 # Just to clean up the code, lets place offsets on the outer ends
169                                 $endBracketOffset += 2;
170                                 
171                                 # Only do logic if we actually have a opening bracket for this
172                                 if( isset($startBracketOffset) ) {
173                                         # Extract text inside the link
174                                         @list( $titleText, $paramText ) = explode('|',
175                                                 substr($s, $startBracketOffset+2, $endBracketOffset-$startBracketOffset-4), 2);
176                                         # Create markers only for valid links
177                                         if( preg_match( $titleRegex, $titleText ) ) {
178                                                 # Store the text for the marker
179                                                 $marker = $markers->addMarker($titleText, $paramText);
180                                                 # Replace the current link with the marker
181                                                 $s = substr($s,0,$startBracketOffset).
182                                                         $marker.
183                                                         substr($s, $endBracketOffset);
184                                                 # We have modified $s, because of this we need to set the
185                                                 # offset manually since the end position is different now
186                                                 $offset = $startBracketOffset+strlen($marker);
187                                                 continue;
188                                         }
189                                         # ToDo: Some LinkHooks may allow recursive links inside of
190                                         # the link text, create a regex that also matches our
191                                         # <!-- LINKMARKER ### --> sequence in titles
192                                         # ToDO: Some LinkHooks use patterns rather than namespaces
193                                         # these need to be tested at this point here
194                                 }
195                                 
196                         }
197                         # Bump our offset to after our current bracket
198                         $offset = $bracketOffset+2;
199                 }
200                 
201                 
202                 # Now expand our tree
203                 wfProfileIn( __METHOD__.'-expand' );
204                 $s = $markers->expand( $s );
205                 wfProfileOut( __METHOD__.'-expand' );
206                 
207                 wfProfileOut( __METHOD__ );
208                 return $holders;
209         }
210         
211         function replaceInternalLinksCallback( $parser, $holders, $markers, $titleText, $paramText ) {
212                 wfProfileIn( __METHOD__ );
213                 $wt = isset($paramText) ? "[[$titleText|$paramText]]" : "[[$titleText]]";
214                 wfProfileIn( __METHOD__."-misc" );
215                 # Don't allow internal links to pages containing
216                 # PROTO: where PROTO is a valid URL protocol; these
217                 # should be external links.
218                 if( preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $titleText) ) {
219                         wfProfileOut( __METHOD__ );
220                         return $wt;
221                 }
222                 
223                 # Make subpage if necessary
224                 if( $this->areSubpagesAllowed() ) {
225                         $titleText = $this->maybeDoSubpageLink( $titleText, $paramText );
226                 }
227                 
228                 # Check for a leading colon and strip it if it is there
229                 $leadingColon = $titleText[0] == ':';
230                 if( $leadingColon ) $titleText = substr( $titleText, 1 );
231                 
232                 wfProfileOut( __METHOD__."-misc" );
233                 # Make title object
234                 wfProfileIn( __METHOD__."-title" );
235                 $title = Title::newFromText( $this->mStripState->unstripNoWiki($titleText) );
236                 if( !$title ) {
237                         wfProfileOut( __METHOD__."-title" );
238                         wfProfileOut( __METHOD__ );
239                         return $wt;
240                 }
241                 $ns = $title->getNamespace();
242                 wfProfileOut( __METHOD__."-title" );
243                 
244                 # Default for Namespaces is a default link
245                 # ToDo: Default for patterns is plain wikitext
246                 $return = true;
247                 if( isset($this->mLinkHooks[$ns]) ) {
248                         list( $callback, $flags ) = $this->mLinkHooks[$ns];
249                         if( $flags & SLH_PATTERN ) {
250                                 $args = array( $parser, $holders, $markers, $titleText, &$paramText, &$leadingColon );
251                         } else {
252                                 $args = array( $parser, $holders, $markers, $title, $titleText, &$paramText, &$leadingColon );
253                         }
254                         # Workaround for PHP bug 35229 and similar
255                         if ( !is_callable( $callback ) ) {
256                                 throw new MWException( "Tag hook for $name is not callable\n" );
257                         }
258                         $return = call_user_func_array( $callback, $args );
259                 }
260                 if( $return === true ) {
261                         # True (treat as plain link) was returned, call the defaultLinkHook
262                         $args = array( $parser, $holders, $markers, $title, $titleText, &$paramText, &$leadingColon );
263                         $return = call_user_func_array( array( 'CoreLinkFunctions', 'defaultLinkHook' ), $args );
264                 }
265                 if( $return === false ) {
266                         # False (no link) was returned, output plain wikitext
267                         # Build it again as the hook is allowed to modify $paramText
268                         $return = isset($paramText) ? "[[$titleText|$paramText]]" : "[[$titleText]]";
269                 }
270                 # Content was returned, return it
271                 wfProfileOut( __METHOD__ );
272                 return $return;
273         }
274         
275 }
276
277 class LinkMarkerReplacer {
278         
279         protected $markers, $nextId, $parser, $holders, $callback;
280         
281         function __construct( $parser, $holders, $callback ) {
282                 $this->nextId   = 0;
283                 $this->markers  = array();
284                 $this->parser   = $parser;
285                 $this->holders  = $holders;
286                 $this->callback = $callback;
287         }
288         
289         function addMarker($titleText, $paramText) {
290                 $id = $this->nextId++;
291                 $this->markers[$id] = array( $titleText, $paramText );
292                 return "<!-- LINKMARKER $id -->";
293         }
294         
295         function findMarker( $string ) {
296                 return (bool) preg_match('/<!-- LINKMARKER [0-9]+ -->/', $string );
297         }
298         
299         function expand( $string ) {
300                 return StringUtils::delimiterReplaceCallback( "<!-- LINKMARKER ", " -->", array( &$this, 'callback' ), $string );
301         }
302         
303         function callback( $m ) {
304                 $id = intval($m[1]);
305                 if( !array_key_exists($id, $this->markers) ) return $m[0];
306                 $args = $this->markers[$id];
307                 array_unshift( $args, $this );
308                 array_unshift( $args, $this->holders );
309                 array_unshift( $args, $this->parser );
310                 return call_user_func_array( $this->callback, $args );
311         }
312         
313 }