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