Removed trailing spaces
[lhc/web/wiklou.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 return $return;
272 }
273
274 }
275
276 class LinkMarkerReplacer {
277
278 protected $markers, $nextId, $parser, $holders, $callback;
279
280 function __construct( $parser, $holders, $callback ) {
281 $this->nextId = 0;
282 $this->markers = array();
283 $this->parser = $parser;
284 $this->holders = $holders;
285 $this->callback = $callback;
286 }
287
288 function addMarker($titleText, $paramText) {
289 $id = $this->nextId++;
290 $this->markers[$id] = array( $titleText, $paramText );
291 return "<!-- LINKMARKER $id -->";
292 }
293
294 function findMarker( $string ) {
295 return (bool) preg_match('/<!-- LINKMARKER [0-9]+ -->/', $string );
296 }
297
298 function expand( $string ) {
299 return StringUtils::delimiterReplaceCallback( "<!-- LINKMARKER ", " -->", array( &$this, 'callback' ), $string );
300 }
301
302 function callback( $m ) {
303 $id = intval($m[1]);
304 if( !array_key_exists($id, $this->markers) ) return $m[0];
305 $args = $this->markers[$id];
306 array_unshift( $args, $this );
307 array_unshift( $args, $this->holders );
308 array_unshift( $args, $this->parser );
309 return call_user_func_array( $this->callback, $args );
310 }
311
312 }