Allowed HTML list has been frozen for some time, and we're trying not to add new...
[lhc/web/wiklou.git] / includes / Parser.php
1 <?php
2
3 /**
4 * File for Parser and related classes
5 *
6 * @package MediaWiki
7 */
8
9 /**
10 * Update this version number when the ParserOutput format
11 * changes in an incompatible way, so the parser cache
12 * can automatically discard old data.
13 */
14 define( 'MW_PARSER_VERSION', '1.4.0' );
15
16 /**
17 * Variable substitution O(N^2) attack
18 *
19 * Without countermeasures, it would be possible to attack the parser by saving
20 * a page filled with a large number of inclusions of large pages. The size of
21 * the generated page would be proportional to the square of the input size.
22 * Hence, we limit the number of inclusions of any given page, thus bringing any
23 * attack back to O(N).
24 */
25
26 define( 'MAX_INCLUDE_REPEAT', 100 );
27 define( 'MAX_INCLUDE_SIZE', 1000000 ); // 1 Million
28
29 define( 'RLH_FOR_UPDATE', 1 );
30
31 # Allowed values for $mOutputType
32 define( 'OT_HTML', 1 );
33 define( 'OT_WIKI', 2 );
34 define( 'OT_MSG' , 3 );
35
36 # string parameter for extractTags which will cause it
37 # to strip HTML comments in addition to regular
38 # <XML>-style tags. This should not be anything we
39 # may want to use in wikisyntax
40 define( 'STRIP_COMMENTS', 'HTMLCommentStrip' );
41
42 # prefix for escaping, used in two functions at least
43 define( 'UNIQ_PREFIX', 'NaodW29');
44
45 # Constants needed for external link processing
46 define( 'URL_PROTOCOLS', 'http|https|ftp|irc|gopher|news|mailto' );
47 define( 'HTTP_PROTOCOLS', 'http|https' );
48 # Everything except bracket, space, or control characters
49 define( 'EXT_LINK_URL_CLASS', '[^]<>"\\x00-\\x20\\x7F]' );
50 # Including space
51 define( 'EXT_LINK_TEXT_CLASS', '[^\]\\x00-\\x1F\\x7F]' );
52 define( 'EXT_IMAGE_FNAME_CLASS', '[A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]' );
53 define( 'EXT_IMAGE_EXTENSIONS', 'gif|png|jpg|jpeg' );
54 define( 'EXT_LINK_BRACKETED', '/\[(('.URL_PROTOCOLS.'):'.EXT_LINK_URL_CLASS.'+) *('.EXT_LINK_TEXT_CLASS.'*?)\]/S' );
55 define( 'EXT_IMAGE_REGEX',
56 '/^('.HTTP_PROTOCOLS.':)'. # Protocol
57 '('.EXT_LINK_URL_CLASS.'+)\\/'. # Hostname and path
58 '('.EXT_IMAGE_FNAME_CLASS.'+)\\.((?i)'.EXT_IMAGE_EXTENSIONS.')$/S' # Filename
59 );
60
61 /**
62 * PHP Parser
63 *
64 * Processes wiki markup
65 *
66 * <pre>
67 * There are three main entry points into the Parser class:
68 * parse()
69 * produces HTML output
70 * preSaveTransform().
71 * produces altered wiki markup.
72 * transformMsg()
73 * performs brace substitution on MediaWiki messages
74 *
75 * Globals used:
76 * objects: $wgLang, $wgDateFormatter, $wgLinkCache
77 *
78 * NOT $wgArticle, $wgUser or $wgTitle. Keep them away!
79 *
80 * settings:
81 * $wgUseTex*, $wgUseDynamicDates*, $wgInterwikiMagic*,
82 * $wgNamespacesWithSubpages, $wgAllowExternalImages*,
83 * $wgLocaltimezone
84 *
85 * * only within ParserOptions
86 * </pre>
87 *
88 * @package MediaWiki
89 */
90 class Parser
91 {
92 /**#@+
93 * @access private
94 */
95 # Persistent:
96 var $mTagHooks;
97
98 # Cleared with clearState():
99 var $mOutput, $mAutonumber, $mDTopen, $mStripState = array();
100 var $mVariables, $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
101
102 # Temporary:
103 var $mOptions, $mTitle, $mOutputType,
104 $mTemplates, // cache of already loaded templates, avoids
105 // multiple SQL queries for the same string
106 $mTemplatePath; // stores an unsorted hash of all the templates already loaded
107 // in this path. Used for loop detection.
108
109 /**#@-*/
110
111 /**
112 * Constructor
113 *
114 * @access public
115 */
116 function Parser() {
117 $this->mTemplates = array();
118 $this->mTemplatePath = array();
119 $this->mTagHooks = array();
120 $this->clearState();
121 }
122
123 /**
124 * Clear Parser state
125 *
126 * @access private
127 */
128 function clearState() {
129 $this->mOutput = new ParserOutput;
130 $this->mAutonumber = 0;
131 $this->mLastSection = "";
132 $this->mDTopen = false;
133 $this->mVariables = false;
134 $this->mIncludeCount = array();
135 $this->mStripState = array();
136 $this->mArgStack = array();
137 $this->mInPre = false;
138 }
139
140 /**
141 * First pass--just handle <nowiki> sections, pass the rest off
142 * to internalParse() which does all the real work.
143 *
144 * @access private
145 * @return ParserOutput a ParserOutput
146 */
147 function parse( $text, &$title, $options, $linestart = true, $clearState = true ) {
148 global $wgUseTidy, $wgContLang;
149 $fname = 'Parser::parse';
150 wfProfileIn( $fname );
151
152 if ( $clearState ) {
153 $this->clearState();
154 }
155
156 $this->mOptions = $options;
157 $this->mTitle =& $title;
158 $this->mOutputType = OT_HTML;
159
160 $stripState = NULL;
161 $text = $this->strip( $text, $this->mStripState );
162
163 $text = $this->internalParse( $text, $linestart );
164 $text = $this->unstrip( $text, $this->mStripState );
165 # Clean up special characters, only run once, next-to-last before doBlockLevels
166 if(!$wgUseTidy) {
167 $fixtags = array(
168 # french spaces, last one Guillemet-left
169 # only if there is something before the space
170 '/(.) (?=\\?|:|;|!|\\302\\273)/i' => '\\1&nbsp;\\2',
171 # french spaces, Guillemet-right
172 "/(\\302\\253) /i"=>"\\1&nbsp;",
173 '/<hr *>/i' => '<hr />',
174 '/<br *>/i' => '<br />',
175 '/<center *>/i' => '<div class="center">',
176 '/<\\/center *>/i' => '</div>',
177 # Clean up spare ampersands; note that we probably ought to be
178 # more careful about named entities.
179 '/&(?!:amp;|#[Xx][0-9A-fa-f]+;|#[0-9]+;|[a-zA-Z0-9]+;)/' => '&amp;'
180 );
181 $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
182 } else {
183 $fixtags = array(
184 # french spaces, last one Guillemet-left
185 '/ (\\?|:|;|!|\\302\\273)/i' => '&nbsp;\\1',
186 # french spaces, Guillemet-right
187 '/(\\302\\253) /i' => '\\1&nbsp;',
188 '/<center *>/i' => '<div class="center">',
189 '/<\\/center *>/i' => '</div>'
190 );
191 $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
192 }
193 # only once and last
194 $text = $this->doBlockLevels( $text, $linestart );
195
196 $this->replaceLinkHolders( $text );
197 $text = $wgContLang->convert($text);
198
199 $text = $this->unstripNoWiki( $text, $this->mStripState );
200 global $wgUseTidy;
201 if ($wgUseTidy) {
202 $text = Parser::tidy($text);
203 }
204
205 $this->mOutput->setText( $text );
206 wfProfileOut( $fname );
207 return $this->mOutput;
208 }
209
210 /**
211 * Get a random string
212 *
213 * @access private
214 * @static
215 */
216 function getRandomString() {
217 return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
218 }
219
220 /**
221 * Replaces all occurrences of <$tag>content</$tag> in the text
222 * with a random marker and returns the new text. the output parameter
223 * $content will be an associative array filled with data on the form
224 * $unique_marker => content.
225 *
226 * If $content is already set, the additional entries will be appended
227 * If $tag is set to STRIP_COMMENTS, the function will extract
228 * <!-- HTML comments -->
229 *
230 * @access private
231 * @static
232 */
233 function extractTags($tag, $text, &$content, $uniq_prefix = ''){
234 $rnd = $uniq_prefix . '-' . $tag . Parser::getRandomString();
235 if ( !$content ) {
236 $content = array( );
237 }
238 $n = 1;
239 $stripped = '';
240
241 while ( '' != $text ) {
242 if($tag==STRIP_COMMENTS) {
243 $p = preg_split( '/<!--/i', $text, 2 );
244 } else {
245 $p = preg_split( "/<\\s*$tag\\s*>/i", $text, 2 );
246 }
247 $stripped .= $p[0];
248 if ( ( count( $p ) < 2 ) || ( '' == $p[1] ) ) {
249 $text = '';
250 } else {
251 if($tag==STRIP_COMMENTS) {
252 $q = preg_split( '/-->/i', $p[1], 2 );
253 } else {
254 $q = preg_split( "/<\\/\\s*$tag\\s*>/i", $p[1], 2 );
255 }
256 $marker = $rnd . sprintf('%08X', $n++);
257 $content[$marker] = $q[0];
258 $stripped .= $marker;
259 $text = $q[1];
260 }
261 }
262 return $stripped;
263 }
264
265 /**
266 * Strips and renders nowiki, pre, math, hiero
267 * If $render is set, performs necessary rendering operations on plugins
268 * Returns the text, and fills an array with data needed in unstrip()
269 * If the $state is already a valid strip state, it adds to the state
270 *
271 * @param bool $stripcomments when set, HTML comments <!-- like this -->
272 * will be stripped in addition to other tags. This is important
273 * for section editing, where these comments cause confusion when
274 * counting the sections in the wikisource
275 *
276 * @access private
277 */
278 function strip( $text, &$state, $stripcomments = false ) {
279 $render = ($this->mOutputType == OT_HTML);
280 $html_content = array();
281 $nowiki_content = array();
282 $math_content = array();
283 $pre_content = array();
284 $comment_content = array();
285 $ext_content = array();
286 $gallery_content = array();
287
288 # Replace any instances of the placeholders
289 $uniq_prefix = UNIQ_PREFIX;
290 #$text = str_replace( $uniq_prefix, wfHtmlEscapeFirst( $uniq_prefix ), $text );
291
292 # html
293 global $wgRawHtml, $wgWhitelistEdit;
294 if( $wgRawHtml && $wgWhitelistEdit ) {
295 $text = Parser::extractTags('html', $text, $html_content, $uniq_prefix);
296 foreach( $html_content as $marker => $content ) {
297 if ($render ) {
298 # Raw and unchecked for validity.
299 $html_content[$marker] = $content;
300 } else {
301 $html_content[$marker] = '<html>'.$content.'</html>';
302 }
303 }
304 }
305
306 # nowiki
307 $text = Parser::extractTags('nowiki', $text, $nowiki_content, $uniq_prefix);
308 foreach( $nowiki_content as $marker => $content ) {
309 if( $render ){
310 $nowiki_content[$marker] = wfEscapeHTMLTagsOnly( $content );
311 } else {
312 $nowiki_content[$marker] = '<nowiki>'.$content.'</nowiki>';
313 }
314 }
315
316 # math
317 $text = Parser::extractTags('math', $text, $math_content, $uniq_prefix);
318 foreach( $math_content as $marker => $content ){
319 if( $render ) {
320 if( $this->mOptions->getUseTeX() ) {
321 $math_content[$marker] = renderMath( $content );
322 } else {
323 $math_content[$marker] = '&lt;math&gt;'.$content.'&lt;math&gt;';
324 }
325 } else {
326 $math_content[$marker] = '<math>'.$content.'</math>';
327 }
328 }
329
330 # pre
331 $text = Parser::extractTags('pre', $text, $pre_content, $uniq_prefix);
332 foreach( $pre_content as $marker => $content ){
333 if( $render ){
334 $pre_content[$marker] = '<pre>' . wfEscapeHTMLTagsOnly( $content ) . '</pre>';
335 } else {
336 $pre_content[$marker] = '<pre>'.$content.'</pre>';
337 }
338 }
339
340 # gallery
341 $text = Parser::extractTags('gallery', $text, $gallery_content, $uniq_prefix);
342 foreach( $gallery_content as $marker => $content ) {
343 require_once( 'ImageGallery.php' );
344 if ( $render ) {
345 $gallery_content[$marker] = Parser::renderImageGallery( $content );
346 } else {
347 $gallery_content[$marker] = '<gallery>'.$content.'</gallery>';
348 }
349 }
350
351 # Comments
352 if($stripcomments) {
353 $text = Parser::extractTags(STRIP_COMMENTS, $text, $comment_content, $uniq_prefix);
354 foreach( $comment_content as $marker => $content ){
355 $comment_content[$marker] = '<!--'.$content.'-->';
356 }
357 }
358
359 # Extensions
360 foreach ( $this->mTagHooks as $tag => $callback ) {
361 $ext_contents[$tag] = array();
362 $text = Parser::extractTags( $tag, $text, $ext_content[$tag], $uniq_prefix );
363 foreach( $ext_content[$tag] as $marker => $content ) {
364 if ( $render ) {
365 $ext_content[$tag][$marker] = $callback( $content );
366 } else {
367 $ext_content[$tag][$marker] = "<$tag>$content</$tag>";
368 }
369 }
370 }
371
372 # Merge state with the pre-existing state, if there is one
373 if ( $state ) {
374 $state['html'] = $state['html'] + $html_content;
375 $state['nowiki'] = $state['nowiki'] + $nowiki_content;
376 $state['math'] = $state['math'] + $math_content;
377 $state['pre'] = $state['pre'] + $pre_content;
378 $state['comment'] = $state['comment'] + $comment_content;
379 $state['gallery'] = $state['gallery'] + $gallery_content;
380
381 foreach( $ext_content as $tag => $array ) {
382 if ( array_key_exists( $tag, $state ) ) {
383 $state[$tag] = $state[$tag] + $array;
384 }
385 }
386 } else {
387 $state = array(
388 'html' => $html_content,
389 'nowiki' => $nowiki_content,
390 'math' => $math_content,
391 'pre' => $pre_content,
392 'comment' => $comment_content,
393 'gallery' => $gallery_content,
394 ) + $ext_content;
395 }
396 return $text;
397 }
398
399 /**
400 * restores pre, math, and hiero removed by strip()
401 *
402 * always call unstripNoWiki() after this one
403 * @access private
404 */
405 function unstrip( $text, &$state ) {
406 # Must expand in reverse order, otherwise nested tags will be corrupted
407 $contentDict = end( $state );
408 for ( $contentDict = end( $state ); $contentDict !== false; $contentDict = prev( $state ) ) {
409 if( key($state) != 'nowiki' && key($state) != 'html') {
410 for ( $content = end( $contentDict ); $content !== false; $content = prev( $contentDict ) ) {
411 $text = str_replace( key( $contentDict ), $content, $text );
412 }
413 }
414 }
415
416 return $text;
417 }
418
419 /**
420 * always call this after unstrip() to preserve the order
421 *
422 * @access private
423 */
424 function unstripNoWiki( $text, &$state ) {
425 # Must expand in reverse order, otherwise nested tags will be corrupted
426 for ( $content = end($state['nowiki']); $content !== false; $content = prev( $state['nowiki'] ) ) {
427 $text = str_replace( key( $state['nowiki'] ), $content, $text );
428 }
429
430 global $wgRawHtml;
431 if ($wgRawHtml) {
432 for ( $content = end($state['html']); $content !== false; $content = prev( $state['html'] ) ) {
433 $text = str_replace( key( $state['html'] ), $content, $text );
434 }
435 }
436
437 return $text;
438 }
439
440 /**
441 * Add an item to the strip state
442 * Returns the unique tag which must be inserted into the stripped text
443 * The tag will be replaced with the original text in unstrip()
444 *
445 * @access private
446 */
447 function insertStripItem( $text, &$state ) {
448 $rnd = UNIQ_PREFIX . '-item' . Parser::getRandomString();
449 if ( !$state ) {
450 $state = array(
451 'html' => array(),
452 'nowiki' => array(),
453 'math' => array(),
454 'pre' => array()
455 );
456 }
457 $state['item'][$rnd] = $text;
458 return $rnd;
459 }
460
461 /**
462 * Return allowed HTML attributes
463 *
464 * @access private
465 */
466 function getHTMLattrs () {
467 $htmlattrs = array( # Allowed attributes--no scripting, etc.
468 'title', 'align', 'lang', 'dir', 'width', 'height',
469 'bgcolor', 'clear', /* BR */ 'noshade', /* HR */
470 'cite', /* BLOCKQUOTE, Q */ 'size', 'face', 'color',
471 /* FONT */ 'type', 'start', 'value', 'compact',
472 /* For various lists, mostly deprecated but safe */
473 'summary', 'width', 'border', 'frame', 'rules',
474 'cellspacing', 'cellpadding', 'valign', 'char',
475 'charoff', 'colgroup', 'col', 'span', 'abbr', 'axis',
476 'headers', 'scope', 'rowspan', 'colspan', /* Tables */
477 'id', 'class', 'name', 'style' /* For CSS */
478 );
479 return $htmlattrs ;
480 }
481
482 /**
483 * Remove non approved attributes and javascript in css
484 *
485 * @access private
486 */
487 function fixTagAttributes ( $t ) {
488 if ( trim ( $t ) == '' ) return '' ; # Saves runtime ;-)
489 $htmlattrs = $this->getHTMLattrs() ;
490
491 # Strip non-approved attributes from the tag
492 $t = preg_replace(
493 '/(\\w+)(\\s*=\\s*([^\\s\">]+|\"[^\">]*\"))?/e',
494 "(in_array(strtolower(\"\$1\"),\$htmlattrs)?(\"\$1\".((\"x\$3\" != \"x\")?\"=\$3\":'')):'')",
495 $t);
496
497 $t = str_replace ( '<></>' , '' , $t ) ; # This should fix bug 980557
498
499 # Strip javascript "expression" from stylesheets. Brute force approach:
500 # If anythin offensive is found, all attributes of the HTML tag are dropped
501
502 if( preg_match(
503 '/style\\s*=.*(expression|tps*:\/\/|url\\s*\().*/is',
504 wfMungeToUtf8( $t ) ) )
505 {
506 $t='';
507 }
508
509 return trim ( $t ) ;
510 }
511
512 /**
513 * interface with html tidy, used if $wgUseTidy = true
514 *
515 * @access public
516 * @static
517 */
518 function tidy ( $text ) {
519 global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
520 global $wgInputEncoding, $wgOutputEncoding;
521 $fname = 'Parser::tidy';
522 wfProfileIn( $fname );
523
524 $cleansource = '';
525 $opts = '';
526 switch(strtoupper($wgOutputEncoding)) {
527 case 'ISO-8859-1':
528 $opts .= ($wgInputEncoding == $wgOutputEncoding)? ' -latin1':' -raw';
529 break;
530 case 'UTF-8':
531 $opts .= ($wgInputEncoding == $wgOutputEncoding)? ' -utf8':' -raw';
532 break;
533 default:
534 $opts .= ' -raw';
535 }
536
537 $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'.
538 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'.
539 '<head><title>test</title></head><body>'.$text.'</body></html>';
540 $descriptorspec = array(
541 0 => array('pipe', 'r'),
542 1 => array('pipe', 'w'),
543 2 => array('file', '/dev/null', 'a')
544 );
545 $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
546 if (is_resource($process)) {
547 fwrite($pipes[0], $wrappedtext);
548 fclose($pipes[0]);
549 while (!feof($pipes[1])) {
550 $cleansource .= fgets($pipes[1], 1024);
551 }
552 fclose($pipes[1]);
553 $return_value = proc_close($process);
554 }
555
556 wfProfileOut( $fname );
557
558 if( $cleansource == '' && $text != '') {
559 wfDebug( "Tidy error detected!\n" );
560 return $text . "\n<!-- Tidy found serious XHTML errors -->\n";
561 } else {
562 return $cleansource;
563 }
564 }
565
566 /**
567 * parse the wiki syntax used to render tables
568 *
569 * @access private
570 */
571 function doTableStuff ( $t ) {
572 $fname = 'Parser::doTableStuff';
573 wfProfileIn( $fname );
574
575 $t = explode ( "\n" , $t ) ;
576 $td = array () ; # Is currently a td tag open?
577 $ltd = array () ; # Was it TD or TH?
578 $tr = array () ; # Is currently a tr tag open?
579 $ltr = array () ; # tr attributes
580 $indent_level = 0; # indent level of the table
581 foreach ( $t AS $k => $x )
582 {
583 $x = trim ( $x ) ;
584 $fc = substr ( $x , 0 , 1 ) ;
585 if ( preg_match( '/^(:*)\{\|(.*)$/', $x, $matches ) ) {
586 $indent_level = strlen( $matches[1] );
587 $t[$k] = "\n" .
588 str_repeat( '<dl><dd>', $indent_level ) .
589 '<table ' . $this->fixTagAttributes ( $matches[2] ) . '>' ;
590 array_push ( $td , false ) ;
591 array_push ( $ltd , '' ) ;
592 array_push ( $tr , false ) ;
593 array_push ( $ltr , '' ) ;
594 }
595 else if ( count ( $td ) == 0 ) { } # Don't do any of the following
596 else if ( '|}' == substr ( $x , 0 , 2 ) ) {
597 $z = "</table>\n" ;
598 $l = array_pop ( $ltd ) ;
599 if ( array_pop ( $tr ) ) $z = '</tr>' . $z ;
600 if ( array_pop ( $td ) ) $z = '</'.$l.'>' . $z ;
601 array_pop ( $ltr ) ;
602 $t[$k] = $z . str_repeat( '</dd></dl>', $indent_level );
603 }
604 else if ( '|-' == substr ( $x , 0 , 2 ) ) { # Allows for |---------------
605 $x = substr ( $x , 1 ) ;
606 while ( $x != '' && substr ( $x , 0 , 1 ) == '-' ) $x = substr ( $x , 1 ) ;
607 $z = '' ;
608 $l = array_pop ( $ltd ) ;
609 if ( array_pop ( $tr ) ) $z = '</tr>' . $z ;
610 if ( array_pop ( $td ) ) $z = '</'.$l.'>' . $z ;
611 array_pop ( $ltr ) ;
612 $t[$k] = $z ;
613 array_push ( $tr , false ) ;
614 array_push ( $td , false ) ;
615 array_push ( $ltd , '' ) ;
616 array_push ( $ltr , $this->fixTagAttributes ( $x ) ) ;
617 }
618 else if ( '|' == $fc || '!' == $fc || '|+' == substr ( $x , 0 , 2 ) ) { # Caption
619 # $x is a table row
620 if ( '|+' == substr ( $x , 0 , 2 ) ) {
621 $fc = '+' ;
622 $x = substr ( $x , 1 ) ;
623 }
624 $after = substr ( $x , 1 ) ;
625 if ( $fc == '!' ) $after = str_replace ( '!!' , '||' , $after ) ;
626 $after = explode ( '||' , $after ) ;
627 $t[$k] = '' ;
628
629 # Loop through each table cell
630 foreach ( $after AS $theline )
631 {
632 $z = '' ;
633 if ( $fc != '+' )
634 {
635 $tra = array_pop ( $ltr ) ;
636 if ( !array_pop ( $tr ) ) $z = '<tr '.$tra.">\n" ;
637 array_push ( $tr , true ) ;
638 array_push ( $ltr , '' ) ;
639 }
640
641 $l = array_pop ( $ltd ) ;
642 if ( array_pop ( $td ) ) $z = '</'.$l.'>' . $z ;
643 if ( $fc == '|' ) $l = 'td' ;
644 else if ( $fc == '!' ) $l = 'th' ;
645 else if ( $fc == '+' ) $l = 'caption' ;
646 else $l = '' ;
647 array_push ( $ltd , $l ) ;
648
649 # Cell parameters
650 $y = explode ( '|' , $theline , 2 ) ;
651 # Note that a '|' inside an invalid link should not
652 # be mistaken as delimiting cell parameters
653 if ( strpos( $y[0], '[[' ) !== false ) {
654 $y = array ($theline);
655 }
656 if ( count ( $y ) == 1 )
657 $y = "{$z}<{$l}>{$y[0]}" ;
658 else $y = $y = "{$z}<{$l} ".$this->fixTagAttributes($y[0]).">{$y[1]}" ;
659 $t[$k] .= $y ;
660 array_push ( $td , true ) ;
661 }
662 }
663 }
664
665 # Closing open td, tr && table
666 while ( count ( $td ) > 0 )
667 {
668 if ( array_pop ( $td ) ) $t[] = '</td>' ;
669 if ( array_pop ( $tr ) ) $t[] = '</tr>' ;
670 $t[] = '</table>' ;
671 }
672
673 $t = implode ( "\n" , $t ) ;
674 # $t = $this->removeHTMLtags( $t );
675 wfProfileOut( $fname );
676 return $t ;
677 }
678
679 /**
680 * Helper function for parse() that transforms wiki markup into
681 * HTML. Only called for $mOutputType == OT_HTML.
682 *
683 * @access private
684 */
685 function internalParse( $text, $linestart, $args = array(), $isMain=true ) {
686 global $wgContLang;
687
688 $fname = 'Parser::internalParse';
689 wfProfileIn( $fname );
690
691 $text = $this->removeHTMLtags( $text );
692 $text = $this->replaceVariables( $text, $args );
693
694 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
695
696 $text = $this->doHeadings( $text );
697 if($this->mOptions->getUseDynamicDates()) {
698 global $wgDateFormatter;
699 $text = $wgDateFormatter->reformat( $this->mOptions->getDateFormat(), $text );
700 }
701 $text = $this->doAllQuotes( $text );
702 $text = $this->replaceInternalLinks( $text );
703 $text = $this->replaceExternalLinks( $text );
704
705 # replaceInternalLinks may sometimes leave behind
706 # absolute URLs, which have to be masked to hide them from replaceExternalLinks
707 $text = str_replace("http-noparse://","http://",$text);
708
709 $text = $this->doMagicLinks( $text );
710 $text = $this->doTableStuff( $text );
711 $text = $this->formatHeadings( $text, $isMain );
712 $sk =& $this->mOptions->getSkin();
713 $text = $sk->transformContent( $text );
714
715 wfProfileOut( $fname );
716 return $text;
717 }
718
719 /**
720 * Replace special strings like "ISBN xxx" and "RFC xxx" with
721 * magic external links.
722 *
723 * @access private
724 */
725 function &doMagicLinks( &$text ) {
726 global $wgUseGeoMode;
727 $text = $this->magicISBN( $text );
728 if ( isset( $wgUseGeoMode ) && $wgUseGeoMode ) {
729 $text = $this->magicGEO( $text );
730 }
731 $text = $this->magicRFC( $text, 'RFC ', 'rfcurl' );
732 $text = $this->magicRFC( $text, 'PMID ', 'pubmedurl' );
733 return $text;
734 }
735
736 /**
737 * Parse ^^ tokens and return html
738 *
739 * @access private
740 */
741 function doExponent( $text ) {
742 $fname = 'Parser::doExponent';
743 wfProfileIn( $fname );
744 $text = preg_replace('/\^\^(.*)\^\^/','<small><sup>\\1</sup></small>', $text);
745 wfProfileOut( $fname );
746 return $text;
747 }
748
749 /**
750 * Parse headers and return html
751 *
752 * @access private
753 */
754 function doHeadings( $text ) {
755 $fname = 'Parser::doHeadings';
756 wfProfileIn( $fname );
757 for ( $i = 6; $i >= 1; --$i ) {
758 $h = substr( '======', 0, $i );
759 $text = preg_replace( "/^{$h}(.+){$h}(\\s|$)/m",
760 "<h{$i}>\\1</h{$i}>\\2", $text );
761 }
762 wfProfileOut( $fname );
763 return $text;
764 }
765
766 /**
767 * Replace single quotes with HTML markup
768 * @access private
769 * @return string the altered text
770 */
771 function doAllQuotes( $text ) {
772 $fname = 'Parser::doAllQuotes';
773 wfProfileIn( $fname );
774 $outtext = '';
775 $lines = explode( "\n", $text );
776 foreach ( $lines as $line ) {
777 $outtext .= $this->doQuotes ( $line ) . "\n";
778 }
779 $outtext = substr($outtext, 0,-1);
780 wfProfileOut( $fname );
781 return $outtext;
782 }
783
784 /**
785 * Helper function for doAllQuotes()
786 * @access private
787 */
788 function doQuotes( $text ) {
789 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
790 if ( count( $arr ) == 1 )
791 return $text;
792 else
793 {
794 # First, do some preliminary work. This may shift some apostrophes from
795 # being mark-up to being text. It also counts the number of occurrences
796 # of bold and italics mark-ups.
797 $i = 0;
798 $numbold = 0;
799 $numitalics = 0;
800 foreach ( $arr as $r )
801 {
802 if ( ( $i % 2 ) == 1 )
803 {
804 # If there are ever four apostrophes, assume the first is supposed to
805 # be text, and the remaining three constitute mark-up for bold text.
806 if ( strlen( $arr[$i] ) == 4 )
807 {
808 $arr[$i-1] .= "'";
809 $arr[$i] = "'''";
810 }
811 # If there are more than 5 apostrophes in a row, assume they're all
812 # text except for the last 5.
813 else if ( strlen( $arr[$i] ) > 5 )
814 {
815 $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 );
816 $arr[$i] = "'''''";
817 }
818 # Count the number of occurrences of bold and italics mark-ups.
819 # We are not counting sequences of five apostrophes.
820 if ( strlen( $arr[$i] ) == 2 ) $numitalics++; else
821 if ( strlen( $arr[$i] ) == 3 ) $numbold++; else
822 if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; }
823 }
824 $i++;
825 }
826
827 # If there is an odd number of both bold and italics, it is likely
828 # that one of the bold ones was meant to be an apostrophe followed
829 # by italics. Which one we cannot know for certain, but it is more
830 # likely to be one that has a single-letter word before it.
831 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) )
832 {
833 $i = 0;
834 $firstsingleletterword = -1;
835 $firstmultiletterword = -1;
836 $firstspace = -1;
837 foreach ( $arr as $r )
838 {
839 if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) )
840 {
841 $x1 = substr ($arr[$i-1], -1);
842 $x2 = substr ($arr[$i-1], -2, 1);
843 if ($x1 == ' ') {
844 if ($firstspace == -1) $firstspace = $i;
845 } else if ($x2 == ' ') {
846 if ($firstsingleletterword == -1) $firstsingleletterword = $i;
847 } else {
848 if ($firstmultiletterword == -1) $firstmultiletterword = $i;
849 }
850 }
851 $i++;
852 }
853
854 # If there is a single-letter word, use it!
855 if ($firstsingleletterword > -1)
856 {
857 $arr [ $firstsingleletterword ] = "''";
858 $arr [ $firstsingleletterword-1 ] .= "'";
859 }
860 # If not, but there's a multi-letter word, use that one.
861 else if ($firstmultiletterword > -1)
862 {
863 $arr [ $firstmultiletterword ] = "''";
864 $arr [ $firstmultiletterword-1 ] .= "'";
865 }
866 # ... otherwise use the first one that has neither.
867 # (notice that it is possible for all three to be -1 if, for example,
868 # there is only one pentuple-apostrophe in the line)
869 else if ($firstspace > -1)
870 {
871 $arr [ $firstspace ] = "''";
872 $arr [ $firstspace-1 ] .= "'";
873 }
874 }
875
876 # Now let's actually convert our apostrophic mush to HTML!
877 $output = '';
878 $buffer = '';
879 $state = '';
880 $i = 0;
881 foreach ($arr as $r)
882 {
883 if (($i % 2) == 0)
884 {
885 if ($state == 'both')
886 $buffer .= $r;
887 else
888 $output .= $r;
889 }
890 else
891 {
892 if (strlen ($r) == 2)
893 {
894 if ($state == 'i')
895 { $output .= '</i>'; $state = ''; }
896 else if ($state == 'bi')
897 { $output .= '</i>'; $state = 'b'; }
898 else if ($state == 'ib')
899 { $output .= '</b></i><b>'; $state = 'b'; }
900 else if ($state == 'both')
901 { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; }
902 else # $state can be 'b' or ''
903 { $output .= '<i>'; $state .= 'i'; }
904 }
905 else if (strlen ($r) == 3)
906 {
907 if ($state == 'b')
908 { $output .= '</b>'; $state = ''; }
909 else if ($state == 'bi')
910 { $output .= '</i></b><i>'; $state = 'i'; }
911 else if ($state == 'ib')
912 { $output .= '</b>'; $state = 'i'; }
913 else if ($state == 'both')
914 { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; }
915 else # $state can be 'i' or ''
916 { $output .= '<b>'; $state .= 'b'; }
917 }
918 else if (strlen ($r) == 5)
919 {
920 if ($state == 'b')
921 { $output .= '</b><i>'; $state = 'i'; }
922 else if ($state == 'i')
923 { $output .= '</i><b>'; $state = 'b'; }
924 else if ($state == 'bi')
925 { $output .= '</i></b>'; $state = ''; }
926 else if ($state == 'ib')
927 { $output .= '</b></i>'; $state = ''; }
928 else if ($state == 'both')
929 { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; }
930 else # ($state == '')
931 { $buffer = ''; $state = 'both'; }
932 }
933 }
934 $i++;
935 }
936 # Now close all remaining tags. Notice that the order is important.
937 if ($state == 'b' || $state == 'ib')
938 $output .= '</b>';
939 if ($state == 'i' || $state == 'bi' || $state == 'ib')
940 $output .= '</i>';
941 if ($state == 'bi')
942 $output .= '</b>';
943 if ($state == 'both')
944 $output .= '<b><i>'.$buffer.'</i></b>';
945 return $output;
946 }
947 }
948
949 /**
950 * Replace external links
951 *
952 * Note: this is all very hackish and the order of execution matters a lot.
953 * Make sure to run maintenance/parserTests.php if you change this code.
954 *
955 * @access private
956 */
957 function replaceExternalLinks( $text ) {
958 $fname = 'Parser::replaceExternalLinks';
959 wfProfileIn( $fname );
960
961 $sk =& $this->mOptions->getSkin();
962 $linktrail = wfMsgForContent('linktrail');
963 $bits = preg_split( EXT_LINK_BRACKETED, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
964
965 $s = $this->replaceFreeExternalLinks( array_shift( $bits ) );
966
967 $i = 0;
968 while ( $i<count( $bits ) ) {
969 $url = $bits[$i++];
970 $protocol = $bits[$i++];
971 $text = $bits[$i++];
972 $trail = $bits[$i++];
973
974 # The characters '<' and '>' (which were escaped by
975 # removeHTMLtags()) should not be included in
976 # URLs, per RFC 2396.
977 if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
978 $text = substr($url, $m2[0][1]) . ' ' . $text;
979 $url = substr($url, 0, $m2[0][1]);
980 }
981
982 # If the link text is an image URL, replace it with an <img> tag
983 # This happened by accident in the original parser, but some people used it extensively
984 $img = $this->maybeMakeImageLink( $text );
985 if ( $img !== false ) {
986 $text = $img;
987 }
988
989 $dtrail = '';
990
991 # No link text, e.g. [http://domain.tld/some.link]
992 if ( $text == '' ) {
993 # Autonumber if allowed
994 if ( strpos( HTTP_PROTOCOLS, $protocol ) !== false ) {
995 $text = '[' . ++$this->mAutonumber . ']';
996 } else {
997 # Otherwise just use the URL
998 $text = htmlspecialchars( $url );
999 }
1000 } else {
1001 # Have link text, e.g. [http://domain.tld/some.link text]s
1002 # Check for trail
1003 if ( preg_match( $linktrail, $trail, $m2 ) ) {
1004 $dtrail = $m2[1];
1005 $trail = $m2[2];
1006 }
1007 }
1008
1009 $encUrl = htmlspecialchars( $url );
1010 # Bit in parentheses showing the URL for the printable version
1011 if( $url == $text || preg_match( "!$protocol://" . preg_quote( $text, '/' ) . "/?$!", $url ) ) {
1012 $paren = '';
1013 } else {
1014 # Expand the URL for printable version
1015 if ( ! $sk->suppressUrlExpansion() ) {
1016 $paren = "<span class='urlexpansion'> (<i>" . htmlspecialchars ( $encUrl ) . "</i>)</span>";
1017 } else {
1018 $paren = '';
1019 }
1020 }
1021
1022 # Process the trail (i.e. everything after this link up until start of the next link),
1023 # replacing any non-bracketed links
1024 $trail = $this->replaceFreeExternalLinks( $trail );
1025
1026 # Use the encoded URL
1027 # This means that users can paste URLs directly into the text
1028 # Funny characters like &ouml; aren't valid in URLs anyway
1029 # This was changed in August 2004
1030 $s .= $sk->makeExternalLink( $url, $text, false ) . $dtrail. $paren . $trail;
1031 }
1032
1033 wfProfileOut( $fname );
1034 return $s;
1035 }
1036
1037 /**
1038 * Replace anything that looks like a URL with a link
1039 * @access private
1040 */
1041 function replaceFreeExternalLinks( $text ) {
1042 $fname = 'Parser::replaceFreeExternalLinks';
1043 wfProfileIn( $fname );
1044
1045 $bits = preg_split( '/((?:'.URL_PROTOCOLS.'):)/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1046 $s = array_shift( $bits );
1047 $i = 0;
1048
1049 $sk =& $this->mOptions->getSkin();
1050
1051 while ( $i < count( $bits ) ){
1052 $protocol = $bits[$i++];
1053 $remainder = $bits[$i++];
1054
1055 if ( preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) {
1056 # Found some characters after the protocol that look promising
1057 $url = $protocol . $m[1];
1058 $trail = $m[2];
1059
1060 # The characters '<' and '>' (which were escaped by
1061 # removeHTMLtags()) should not be included in
1062 # URLs, per RFC 2396.
1063 if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
1064 $trail = substr($url, $m2[0][1]) . $trail;
1065 $url = substr($url, 0, $m2[0][1]);
1066 }
1067
1068 # Move trailing punctuation to $trail
1069 $sep = ',;\.:!?';
1070 # If there is no left bracket, then consider right brackets fair game too
1071 if ( strpos( $url, '(' ) === false ) {
1072 $sep .= ')';
1073 }
1074
1075 $numSepChars = strspn( strrev( $url ), $sep );
1076 if ( $numSepChars ) {
1077 $trail = substr( $url, -$numSepChars ) . $trail;
1078 $url = substr( $url, 0, -$numSepChars );
1079 }
1080
1081 # Replace &amp; from obsolete syntax with &.
1082 # All HTML entities will be escaped by makeExternalLink()
1083 # or maybeMakeImageLink()
1084 $url = str_replace( '&amp;', '&', $url );
1085
1086 # Is this an external image?
1087 $text = $this->maybeMakeImageLink( $url );
1088 if ( $text === false ) {
1089 # Not an image, make a link
1090 $text = $sk->makeExternalLink( $url, $url );
1091 }
1092 $s .= $text . $trail;
1093 } else {
1094 $s .= $protocol . $remainder;
1095 }
1096 }
1097 wfProfileOut();
1098 return $s;
1099 }
1100
1101 /**
1102 * make an image if it's allowed
1103 * @access private
1104 */
1105 function maybeMakeImageLink( $url ) {
1106 $sk =& $this->mOptions->getSkin();
1107 $text = false;
1108 if ( $this->mOptions->getAllowExternalImages() ) {
1109 if ( preg_match( EXT_IMAGE_REGEX, $url ) ) {
1110 # Image found
1111 $text = $sk->makeImage( htmlspecialchars( $url ) );
1112 }
1113 }
1114 return $text;
1115 }
1116
1117 /**
1118 * Process [[ ]] wikilinks
1119 *
1120 * @access private
1121 */
1122
1123 function replaceInternalLinks( $s ) {
1124 global $wgLang, $wgContLang, $wgLinkCache;
1125 global $wgDisableLangConversion;
1126 static $fname = 'Parser::replaceInternalLinks' ;
1127
1128 wfProfileIn( $fname );
1129
1130 wfProfileIn( $fname.'-setup' );
1131 static $tc = FALSE;
1132 # the % is needed to support urlencoded titles as well
1133 if ( !$tc ) { $tc = Title::legalChars() . '#%'; }
1134
1135 $sk =& $this->mOptions->getSkin();
1136 global $wgUseOldExistenceCheck;
1137 # "Post-parse link colour check" works only on wiki text since it's now
1138 # in Parser. Enable it, then disable it when we're done.
1139 $saveParseColour = $sk->postParseLinkColour( !$wgUseOldExistenceCheck );
1140
1141 $redirect = MagicWord::get ( MAG_REDIRECT ) ;
1142
1143 #split the entire text string on occurences of [[
1144 $a = explode( '[[', ' ' . $s );
1145 #get the first element (all text up to first [[), and remove the space we added
1146 $s = array_shift( $a );
1147 $s = substr( $s, 1 );
1148
1149 # Match a link having the form [[namespace:link|alternate]]trail
1150 static $e1 = FALSE;
1151 if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|([^]]+))?]](.*)\$/sD"; }
1152 # Match cases where there is no "]]", which might still be images
1153 static $e1_img = FALSE;
1154 if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; }
1155 # Match the end of a line for a word that's not followed by whitespace,
1156 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
1157 static $e2 = '/^(.*?)([a-zA-Z\x80-\xff]+)$/sD';
1158
1159 $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
1160
1161 $nottalk = !Namespace::isTalk( $this->mTitle->getNamespace() );
1162
1163 if ( $useLinkPrefixExtension ) {
1164 if ( preg_match( $e2, $s, $m ) ) {
1165 $first_prefix = $m[2];
1166 $s = $m[1];
1167 } else {
1168 $first_prefix = false;
1169 }
1170 } else {
1171 $prefix = '';
1172 }
1173
1174 $selflink = $this->mTitle->getPrefixedText();
1175 wfProfileOut( $fname.'-setup' );
1176
1177 $checkVariantLink = sizeof($wgContLang->getVariants())>1;
1178 $useSubpages = $this->areSubpagesAllowed();
1179
1180 # Loop for each link
1181 for ($k = 0; isset( $a[$k] ); $k++) {
1182 $line = $a[$k];
1183 if ( $useLinkPrefixExtension ) {
1184 wfProfileIn( $fname.'-prefixhandling' );
1185 if ( preg_match( $e2, $s, $m ) ) {
1186 $prefix = $m[2];
1187 $s = $m[1];
1188 } else {
1189 $prefix='';
1190 }
1191 # first link
1192 if($first_prefix) {
1193 $prefix = $first_prefix;
1194 $first_prefix = false;
1195 }
1196 wfProfileOut( $fname.'-prefixhandling' );
1197 }
1198
1199 $might_be_img = false;
1200
1201 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
1202 $text = $m[2];
1203 # fix up urlencoded title texts
1204 if(preg_match('/%/', $m[1] )) $m[1] = urldecode($m[1]);
1205 $trail = $m[3];
1206 } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption
1207 $might_be_img = true;
1208 $text = $m[2];
1209 if(preg_match('/%/', $m[1] )) $m[1] = urldecode($m[1]);
1210 $trail = "";
1211 } else { # Invalid form; output directly
1212 $s .= $prefix . '[[' . $line ;
1213 continue;
1214 }
1215
1216 # Don't allow internal links to pages containing
1217 # PROTO: where PROTO is a valid URL protocol; these
1218 # should be external links.
1219 if (preg_match('/^((?:'.URL_PROTOCOLS.'):)/', $m[1])) {
1220 $s .= $prefix . '[[' . $line ;
1221 continue;
1222 }
1223
1224 # Make subpage if necessary
1225 if( $useSubpages ) {
1226 $link = $this->maybeDoSubpageLink( $m[1], $text );
1227 } else {
1228 $link = $m[1];
1229 }
1230
1231 $noforce = (substr($m[1], 0, 1) != ':');
1232 if (!$noforce) {
1233 # Strip off leading ':'
1234 $link = substr($link, 1);
1235 }
1236
1237 $nt =& Title::newFromText( $this->unstripNoWiki($link, $this->mStripState) );
1238 if( !$nt ) {
1239 $s .= $prefix . '[[' . $line;
1240 continue;
1241 }
1242
1243 #check other language variants of the link
1244 #if the article does not exist
1245 if( $checkVariantLink
1246 && $nt->getArticleID() == 0 ) {
1247 $wgContLang->findVariantLink($link, $nt);
1248 }
1249
1250 $ns = $nt->getNamespace();
1251 $iw = $nt->getInterWiki();
1252
1253 if ($might_be_img) { # if this is actually an invalid link
1254 if ($ns == NS_IMAGE && $noforce) { #but might be an image
1255 $found = false;
1256 while (isset ($a[$k+1]) ) {
1257 #look at the next 'line' to see if we can close it there
1258 $next_line = array_shift(array_splice( $a, $k + 1, 1) );
1259 if( preg_match("/^(.*?]].*?)]](.*)$/sD", $next_line, $m) ) {
1260 # the first ]] closes the inner link, the second the image
1261 $found = true;
1262 $text .= '[[' . $m[1];
1263 $trail = $m[2];
1264 break;
1265 } elseif( preg_match("/^.*?]].*$/sD", $next_line, $m) ) {
1266 #if there's exactly one ]] that's fine, we'll keep looking
1267 $text .= '[[' . $m[0];
1268 } else {
1269 #if $next_line is invalid too, we need look no further
1270 $text .= '[[' . $next_line;
1271 break;
1272 }
1273 }
1274 if ( !$found ) {
1275 # we couldn't find the end of this imageLink, so output it raw
1276 #but don't ignore what might be perfectly normal links in the text we've examined
1277 $text = $this->replaceInternalLinks($text);
1278 $s .= $prefix . '[[' . $link . '|' . $text;
1279 # note: no $trail, because without an end, there *is* no trail
1280 continue;
1281 }
1282 } else { #it's not an image, so output it raw
1283 $s .= $prefix . '[[' . $link . '|' . $text;
1284 # note: no $trail, because without an end, there *is* no trail
1285 continue;
1286 }
1287 }
1288
1289 $wasblank = ( '' == $text );
1290 if( $wasblank ) $text = $link;
1291
1292
1293 # Link not escaped by : , create the various objects
1294 if( $noforce ) {
1295
1296 # Interwikis
1297 if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
1298 array_push( $this->mOutput->mLanguageLinks, $nt->getFullText() );
1299 $tmp = $prefix . $trail ;
1300 $s .= (trim($tmp) == '')? '': $tmp;
1301 continue;
1302 }
1303
1304 if ( $ns == NS_IMAGE ) {
1305 wfProfileIn( "$fname-image" );
1306
1307 # recursively parse links inside the image caption
1308 # actually, this will parse them in any other parameters, too,
1309 # but it might be hard to fix that, and it doesn't matter ATM
1310 $text = $this->replaceExternalLinks($text);
1311 $text = $this->replaceInternalLinks($text);
1312
1313 # replace the image with a link-holder so that replaceExternalLinks() can't mess with it
1314 $s .= $prefix . $this->insertStripItem( $sk->makeImageLinkObj( $nt, $text ), $this->mStripState ) . $trail;
1315 $wgLinkCache->addImageLinkObj( $nt );
1316
1317 wfProfileOut( "$fname-image" );
1318 continue;
1319 }
1320
1321 if ( $ns == NS_CATEGORY ) {
1322 wfProfileIn( "$fname-category" );
1323 $t = $nt->getText();
1324
1325 $wgLinkCache->suspend(); # Don't save in links/brokenlinks
1326 $pPLC=$sk->postParseLinkColour();
1327 $sk->postParseLinkColour( false );
1328 $t = $sk->makeLinkObj( $nt, $t, '', '' , $prefix );
1329 $sk->postParseLinkColour( $pPLC );
1330 $wgLinkCache->resume();
1331
1332 if ( $wasblank ) {
1333 if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
1334 $sortkey = $this->mTitle->getText();
1335 } else {
1336 $sortkey = $this->mTitle->getPrefixedText();
1337 }
1338 } else {
1339 $sortkey = $text;
1340 }
1341 $wgLinkCache->addCategoryLinkObj( $nt, $sortkey );
1342 $this->mOutput->addCategoryLink( $t );
1343 $s .= $prefix . $trail ;
1344
1345 wfProfileOut( "$fname-category" );
1346 continue;
1347 }
1348 }
1349
1350 if( ( $nt->getPrefixedText() === $selflink ) &&
1351 ( $nt->getFragment() === '' ) ) {
1352 # Self-links are handled specially; generally de-link and change to bold.
1353 $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
1354 continue;
1355 }
1356
1357 # Special and Media are pseudo-namespaces; no pages actually exist in them
1358 if( $ns == NS_MEDIA ) {
1359 $s .= $prefix . $sk->makeMediaLinkObj( $nt, $text, true ) . $trail;
1360 $wgLinkCache->addImageLinkObj( $nt );
1361 continue;
1362 } elseif( $ns == NS_SPECIAL ) {
1363 $s .= $prefix . $sk->makeKnownLinkObj( $nt, $text, '', $trail );
1364 continue;
1365 }
1366 $s .= $sk->makeLinkObj( $nt, $text, '', $trail, $prefix );
1367 }
1368 $sk->postParseLinkColour( $saveParseColour );
1369 wfProfileOut( $fname );
1370 return $s;
1371 }
1372
1373 /**
1374 * Return true if subpage links should be expanded on this page.
1375 * @return bool
1376 */
1377 function areSubpagesAllowed() {
1378 # Some namespaces don't allow subpages
1379 global $wgNamespacesWithSubpages;
1380 return !empty($wgNamespacesWithSubpages[$this->mTitle->getNamespace()]);
1381 }
1382
1383 /**
1384 * Handle link to subpage if necessary
1385 * @param string $target the source of the link
1386 * @param string &$text the link text, modified as necessary
1387 * @return string the full name of the link
1388 * @access private
1389 */
1390 function maybeDoSubpageLink($target, &$text) {
1391 # Valid link forms:
1392 # Foobar -- normal
1393 # :Foobar -- override special treatment of prefix (images, language links)
1394 # /Foobar -- convert to CurrentPage/Foobar
1395 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
1396
1397 $fname = 'Parser::maybeDoSubpageLink';
1398 wfProfileIn( $fname );
1399 # Look at the first character
1400 if( $target != '' && $target{0} == '/' ) {
1401 # / at end means we don't want the slash to be shown
1402 if(substr($target,-1,1)=='/') {
1403 $target=substr($target,1,-1);
1404 $noslash=$target;
1405 } else {
1406 $noslash=substr($target,1);
1407 }
1408
1409 # Some namespaces don't allow subpages
1410 if( $this->areSubpagesAllowed() ) {
1411 # subpages allowed here
1412 $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash);
1413 if( '' === $text ) {
1414 $text = $target;
1415 } # this might be changed for ugliness reasons
1416 } else {
1417 # no subpage allowed, use standard link
1418 $ret = $target;
1419 }
1420 } else {
1421 # no subpage
1422 $ret = $target;
1423 }
1424
1425 wfProfileOut( $fname );
1426 return $ret;
1427 }
1428
1429 /**#@+
1430 * Used by doBlockLevels()
1431 * @access private
1432 */
1433 /* private */ function closeParagraph() {
1434 $result = '';
1435 if ( '' != $this->mLastSection ) {
1436 $result = '</' . $this->mLastSection . ">\n";
1437 }
1438 $this->mInPre = false;
1439 $this->mLastSection = '';
1440 return $result;
1441 }
1442 # getCommon() returns the length of the longest common substring
1443 # of both arguments, starting at the beginning of both.
1444 #
1445 /* private */ function getCommon( $st1, $st2 ) {
1446 $fl = strlen( $st1 );
1447 $shorter = strlen( $st2 );
1448 if ( $fl < $shorter ) { $shorter = $fl; }
1449
1450 for ( $i = 0; $i < $shorter; ++$i ) {
1451 if ( $st1{$i} != $st2{$i} ) { break; }
1452 }
1453 return $i;
1454 }
1455 # These next three functions open, continue, and close the list
1456 # element appropriate to the prefix character passed into them.
1457 #
1458 /* private */ function openList( $char ) {
1459 $result = $this->closeParagraph();
1460
1461 if ( '*' == $char ) { $result .= '<ul><li>'; }
1462 else if ( '#' == $char ) { $result .= '<ol><li>'; }
1463 else if ( ':' == $char ) { $result .= '<dl><dd>'; }
1464 else if ( ';' == $char ) {
1465 $result .= '<dl><dt>';
1466 $this->mDTopen = true;
1467 }
1468 else { $result = '<!-- ERR 1 -->'; }
1469
1470 return $result;
1471 }
1472
1473 /* private */ function nextItem( $char ) {
1474 if ( '*' == $char || '#' == $char ) { return '</li><li>'; }
1475 else if ( ':' == $char || ';' == $char ) {
1476 $close = '</dd>';
1477 if ( $this->mDTopen ) { $close = '</dt>'; }
1478 if ( ';' == $char ) {
1479 $this->mDTopen = true;
1480 return $close . '<dt>';
1481 } else {
1482 $this->mDTopen = false;
1483 return $close . '<dd>';
1484 }
1485 }
1486 return '<!-- ERR 2 -->';
1487 }
1488
1489 /* private */ function closeList( $char ) {
1490 if ( '*' == $char ) { $text = '</li></ul>'; }
1491 else if ( '#' == $char ) { $text = '</li></ol>'; }
1492 else if ( ':' == $char ) {
1493 if ( $this->mDTopen ) {
1494 $this->mDTopen = false;
1495 $text = '</dt></dl>';
1496 } else {
1497 $text = '</dd></dl>';
1498 }
1499 }
1500 else { return '<!-- ERR 3 -->'; }
1501 return $text."\n";
1502 }
1503 /**#@-*/
1504
1505 /**
1506 * Make lists from lines starting with ':', '*', '#', etc.
1507 *
1508 * @access private
1509 * @return string the lists rendered as HTML
1510 */
1511 function doBlockLevels( $text, $linestart ) {
1512 $fname = 'Parser::doBlockLevels';
1513 wfProfileIn( $fname );
1514
1515 # Parsing through the text line by line. The main thing
1516 # happening here is handling of block-level elements p, pre,
1517 # and making lists from lines starting with * # : etc.
1518 #
1519 $textLines = explode( "\n", $text );
1520
1521 $lastPrefix = $output = $lastLine = '';
1522 $this->mDTopen = $inBlockElem = false;
1523 $prefixLength = 0;
1524 $paragraphStack = false;
1525
1526 if ( !$linestart ) {
1527 $output .= array_shift( $textLines );
1528 }
1529 foreach ( $textLines as $oLine ) {
1530 $lastPrefixLength = strlen( $lastPrefix );
1531 $preCloseMatch = preg_match('/<\\/pre/i', $oLine );
1532 $preOpenMatch = preg_match('/<pre/i', $oLine );
1533 if ( !$this->mInPre ) {
1534 # Multiple prefixes may abut each other for nested lists.
1535 $prefixLength = strspn( $oLine, '*#:;' );
1536 $pref = substr( $oLine, 0, $prefixLength );
1537
1538 # eh?
1539 $pref2 = str_replace( ';', ':', $pref );
1540 $t = substr( $oLine, $prefixLength );
1541 $this->mInPre = !empty($preOpenMatch);
1542 } else {
1543 # Don't interpret any other prefixes in preformatted text
1544 $prefixLength = 0;
1545 $pref = $pref2 = '';
1546 $t = $oLine;
1547 }
1548
1549 # List generation
1550 if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) {
1551 # Same as the last item, so no need to deal with nesting or opening stuff
1552 $output .= $this->nextItem( substr( $pref, -1 ) );
1553 $paragraphStack = false;
1554
1555 if ( substr( $pref, -1 ) == ';') {
1556 # The one nasty exception: definition lists work like this:
1557 # ; title : definition text
1558 # So we check for : in the remainder text to split up the
1559 # title and definition, without b0rking links.
1560 if ($this->findColonNoLinks($t, $term, $t2) !== false) {
1561 $t = $t2;
1562 $output .= $term . $this->nextItem( ':' );
1563 }
1564 }
1565 } elseif( $prefixLength || $lastPrefixLength ) {
1566 # Either open or close a level...
1567 $commonPrefixLength = $this->getCommon( $pref, $lastPrefix );
1568 $paragraphStack = false;
1569
1570 while( $commonPrefixLength < $lastPrefixLength ) {
1571 $output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} );
1572 --$lastPrefixLength;
1573 }
1574 if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
1575 $output .= $this->nextItem( $pref{$commonPrefixLength-1} );
1576 }
1577 while ( $prefixLength > $commonPrefixLength ) {
1578 $char = substr( $pref, $commonPrefixLength, 1 );
1579 $output .= $this->openList( $char );
1580
1581 if ( ';' == $char ) {
1582 # FIXME: This is dupe of code above
1583 if ($this->findColonNoLinks($t, $term, $t2) !== false) {
1584 $t = $t2;
1585 $output .= $term . $this->nextItem( ':' );
1586 }
1587 }
1588 ++$commonPrefixLength;
1589 }
1590 $lastPrefix = $pref2;
1591 }
1592 if( 0 == $prefixLength ) {
1593 wfProfileIn( "$fname-paragraph" );
1594 # No prefix (not in list)--go to paragraph mode
1595 $uniq_prefix = UNIQ_PREFIX;
1596 // XXX: use a stack for nestable elements like span, table and div
1597 $openmatch = preg_match('/(<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
1598 $closematch = preg_match(
1599 '/(<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
1600 '<td|<th|<div|<\\/div|<hr|<\\/pre|<\\/p|'.$uniq_prefix.'-pre|<\\/li|<\\/ul)/iS', $t );
1601 if ( $openmatch or $closematch ) {
1602 $paragraphStack = false;
1603 $output .= $this->closeParagraph();
1604 if($preOpenMatch and !$preCloseMatch) {
1605 $this->mInPre = true;
1606 }
1607 if ( $closematch ) {
1608 $inBlockElem = false;
1609 } else {
1610 $inBlockElem = true;
1611 }
1612 } else if ( !$inBlockElem && !$this->mInPre ) {
1613 if ( ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) {
1614 // pre
1615 if ($this->mLastSection != 'pre') {
1616 $paragraphStack = false;
1617 $output .= $this->closeParagraph().'<pre>';
1618 $this->mLastSection = 'pre';
1619 }
1620 $t = substr( $t, 1 );
1621 } else {
1622 // paragraph
1623 if ( '' == trim($t) ) {
1624 if ( $paragraphStack ) {
1625 $output .= $paragraphStack.'<br />';
1626 $paragraphStack = false;
1627 $this->mLastSection = 'p';
1628 } else {
1629 if ($this->mLastSection != 'p' ) {
1630 $output .= $this->closeParagraph();
1631 $this->mLastSection = '';
1632 $paragraphStack = '<p>';
1633 } else {
1634 $paragraphStack = '</p><p>';
1635 }
1636 }
1637 } else {
1638 if ( $paragraphStack ) {
1639 $output .= $paragraphStack;
1640 $paragraphStack = false;
1641 $this->mLastSection = 'p';
1642 } else if ($this->mLastSection != 'p') {
1643 $output .= $this->closeParagraph().'<p>';
1644 $this->mLastSection = 'p';
1645 }
1646 }
1647 }
1648 }
1649 wfProfileOut( "$fname-paragraph" );
1650 }
1651 if ($paragraphStack === false) {
1652 $output .= $t."\n";
1653 }
1654 }
1655 while ( $prefixLength ) {
1656 $output .= $this->closeList( $pref2{$prefixLength-1} );
1657 --$prefixLength;
1658 }
1659 if ( '' != $this->mLastSection ) {
1660 $output .= '</' . $this->mLastSection . '>';
1661 $this->mLastSection = '';
1662 }
1663
1664 wfProfileOut( $fname );
1665 return $output;
1666 }
1667
1668 /**
1669 * Split up a string on ':', ignoring any occurences inside
1670 * <a>..</a> or <span>...</span>
1671 * @param string $str the string to split
1672 * @param string &$before set to everything before the ':'
1673 * @param string &$after set to everything after the ':'
1674 * return string the position of the ':', or false if none found
1675 */
1676 function findColonNoLinks($str, &$before, &$after) {
1677 # I wonder if we should make this count all tags, not just <a>
1678 # and <span>. That would prevent us from matching a ':' that
1679 # comes in the middle of italics other such formatting....
1680 # -- Wil
1681 $fname = 'Parser::findColonNoLinks';
1682 wfProfileIn( $fname );
1683 $pos = 0;
1684 do {
1685 $colon = strpos($str, ':', $pos);
1686
1687 if ($colon !== false) {
1688 $before = substr($str, 0, $colon);
1689 $after = substr($str, $colon + 1);
1690
1691 # Skip any ':' within <a> or <span> pairs
1692 $a = substr_count($before, '<a');
1693 $s = substr_count($before, '<span');
1694 $ca = substr_count($before, '</a>');
1695 $cs = substr_count($before, '</span>');
1696
1697 if ($a <= $ca and $s <= $cs) {
1698 # Tags are balanced before ':'; ok
1699 break;
1700 }
1701 $pos = $colon + 1;
1702 }
1703 } while ($colon !== false);
1704 wfProfileOut( $fname );
1705 return $colon;
1706 }
1707
1708 /**
1709 * Return value of a magic variable (like PAGENAME)
1710 *
1711 * @access private
1712 */
1713 function getVariableValue( $index ) {
1714 global $wgContLang, $wgSitename, $wgServer;
1715
1716 /**
1717 * Some of these require message or data lookups and can be
1718 * expensive to check many times.
1719 */
1720 static $varCache = array();
1721 if( isset( $varCache[$index] ) ) return $varCache[$index];
1722
1723 switch ( $index ) {
1724 case MAG_CURRENTMONTH:
1725 return $varCache[$index] = $wgContLang->formatNum( date( 'm' ) );
1726 case MAG_CURRENTMONTHNAME:
1727 return $varCache[$index] = $wgContLang->getMonthName( date('n') );
1728 case MAG_CURRENTMONTHNAMEGEN:
1729 return $varCache[$index] = $wgContLang->getMonthNameGen( date('n') );
1730 case MAG_CURRENTDAY:
1731 return $varCache[$index] = $wgContLang->formatNum( date('j') );
1732 case MAG_PAGENAME:
1733 return $this->mTitle->getText();
1734 case MAG_PAGENAMEE:
1735 return $this->mTitle->getPartialURL();
1736 case MAG_NAMESPACE:
1737 # return Namespace::getCanonicalName($this->mTitle->getNamespace());
1738 return $wgContLang->getNsText($this->mTitle->getNamespace()); # Patch by Dori
1739 case MAG_CURRENTDAYNAME:
1740 return $varCache[$index] = $wgContLang->getWeekdayName( date('w')+1 );
1741 case MAG_CURRENTYEAR:
1742 return $varCache[$index] = $wgContLang->formatNum( date( 'Y' ) );
1743 case MAG_CURRENTTIME:
1744 return $varCache[$index] = $wgContLang->time( wfTimestampNow(), false );
1745 case MAG_NUMBEROFARTICLES:
1746 return $varCache[$index] = $wgContLang->formatNum( wfNumberOfArticles() );
1747 case MAG_SITENAME:
1748 return $wgSitename;
1749 case MAG_SERVER:
1750 return $wgServer;
1751 default:
1752 return NULL;
1753 }
1754 }
1755
1756 /**
1757 * initialise the magic variables (like CURRENTMONTHNAME)
1758 *
1759 * @access private
1760 */
1761 function initialiseVariables() {
1762 $fname = 'Parser::initialiseVariables';
1763 wfProfileIn( $fname );
1764 global $wgVariableIDs;
1765 $this->mVariables = array();
1766 foreach ( $wgVariableIDs as $id ) {
1767 $mw =& MagicWord::get( $id );
1768 $mw->addToArray( $this->mVariables, $this->getVariableValue( $id ) );
1769 }
1770 wfProfileOut( $fname );
1771 }
1772
1773 /**
1774 * Replace magic variables, templates, and template arguments
1775 * with the appropriate text. Templates are substituted recursively,
1776 * taking care to avoid infinite loops.
1777 *
1778 * Note that the substitution depends on value of $mOutputType:
1779 * OT_WIKI: only {{subst:}} templates
1780 * OT_MSG: only magic variables
1781 * OT_HTML: all templates and magic variables
1782 *
1783 * @param string $tex The text to transform
1784 * @param array $args Key-value pairs representing template parameters to substitute
1785 * @access private
1786 */
1787 function replaceVariables( $text, $args = array() ) {
1788 global $wgLang, $wgScript, $wgArticlePath;
1789
1790 # Prevent too big inclusions
1791 if( strlen( $text ) > MAX_INCLUDE_SIZE ) {
1792 return $text;
1793 }
1794
1795 $fname = 'Parser::replaceVariables';
1796 wfProfileIn( $fname );
1797
1798 $titleChars = Title::legalChars();
1799
1800 # This function is called recursively. To keep track of arguments we need a stack:
1801 array_push( $this->mArgStack, $args );
1802
1803 # Variable substitution
1804 $text = preg_replace_callback( "/{{([$titleChars]*?)}}/", array( &$this, 'variableSubstitution' ), $text );
1805
1806 if ( $this->mOutputType == OT_HTML || $this->mOutputType == OT_WIKI ) {
1807 # Argument substitution
1808 $text = preg_replace_callback( "/{{{([$titleChars]*?)}}}/", array( &$this, 'argSubstitution' ), $text );
1809 }
1810 # Template substitution
1811 $regex = '/(\\n|{)?{{(['.$titleChars.']*)(\\|.*?|)}}/s';
1812 $text = preg_replace_callback( $regex, array( &$this, 'braceSubstitution' ), $text );
1813
1814 array_pop( $this->mArgStack );
1815
1816 wfProfileOut( $fname );
1817 return $text;
1818 }
1819
1820 /**
1821 * Replace magic variables
1822 * @access private
1823 */
1824 function variableSubstitution( $matches ) {
1825 $fname = 'parser::variableSubstitution';
1826 wfProfileIn( $fname );
1827 if ( !$this->mVariables ) {
1828 $this->initialiseVariables();
1829 }
1830 $skip = false;
1831 if ( $this->mOutputType == OT_WIKI ) {
1832 # Do only magic variables prefixed by SUBST
1833 $mwSubst =& MagicWord::get( MAG_SUBST );
1834 if (!$mwSubst->matchStartAndRemove( $matches[1] ))
1835 $skip = true;
1836 # Note that if we don't substitute the variable below,
1837 # we don't remove the {{subst:}} magic word, in case
1838 # it is a template rather than a magic variable.
1839 }
1840 if ( !$skip && array_key_exists( $matches[1], $this->mVariables ) ) {
1841 $text = $this->mVariables[$matches[1]];
1842 $this->mOutput->mContainsOldMagic = true;
1843 } else {
1844 $text = $matches[0];
1845 }
1846 wfProfileOut( $fname );
1847 return $text;
1848 }
1849
1850 # Split template arguments
1851 function getTemplateArgs( $argsString ) {
1852 if ( $argsString === '' ) {
1853 return array();
1854 }
1855
1856 $args = explode( '|', substr( $argsString, 1 ) );
1857
1858 # If any of the arguments contains a '[[' but no ']]', it needs to be
1859 # merged with the next arg because the '|' character between belongs
1860 # to the link syntax and not the template parameter syntax.
1861 $argc = count($args);
1862 $i = 0;
1863 for ( $i = 0; $i < $argc-1; $i++ ) {
1864 if ( substr_count ( $args[$i], '[[' ) != substr_count ( $args[$i], ']]' ) ) {
1865 $args[$i] .= '|'.$args[$i+1];
1866 array_splice($args, $i+1, 1);
1867 $i--;
1868 $argc--;
1869 }
1870 }
1871
1872 return $args;
1873 }
1874
1875 /**
1876 * Return the text of a template, after recursively
1877 * replacing any variables or templates within the template.
1878 *
1879 * @param array $matches The parts of the template
1880 * $matches[1]: the title, i.e. the part before the |
1881 * $matches[2]: the parameters (including a leading |), if any
1882 * @return string the text of the template
1883 * @access private
1884 */
1885 function braceSubstitution( $matches ) {
1886 global $wgLinkCache, $wgContLang;
1887 $fname = 'Parser::braceSubstitution';
1888 wfProfileIn( $fname );
1889
1890 $found = false;
1891 $nowiki = false;
1892 $noparse = false;
1893
1894 $title = NULL;
1895
1896 # Need to know if the template comes at the start of a line,
1897 # to treat the beginning of the template like the beginning
1898 # of a line for tables and block-level elements.
1899 $linestart = $matches[1];
1900
1901 # $part1 is the bit before the first |, and must contain only title characters
1902 # $args is a list of arguments, starting from index 0, not including $part1
1903
1904 $part1 = $matches[2];
1905 # If the third subpattern matched anything, it will start with |
1906
1907 $args = $this->getTemplateArgs($matches[3]);
1908 $argc = count( $args );
1909
1910 # Don't parse {{{}}} because that's only for template arguments
1911 if ( $linestart === '{' ) {
1912 $text = $matches[0];
1913 $found = true;
1914 $noparse = true;
1915 }
1916
1917 # SUBST
1918 if ( !$found ) {
1919 $mwSubst =& MagicWord::get( MAG_SUBST );
1920 if ( $mwSubst->matchStartAndRemove( $part1 ) xor ($this->mOutputType == OT_WIKI) ) {
1921 # One of two possibilities is true:
1922 # 1) Found SUBST but not in the PST phase
1923 # 2) Didn't find SUBST and in the PST phase
1924 # In either case, return without further processing
1925 $text = $matches[0];
1926 $found = true;
1927 $noparse = true;
1928 }
1929 }
1930
1931 # MSG, MSGNW and INT
1932 if ( !$found ) {
1933 # Check for MSGNW:
1934 $mwMsgnw =& MagicWord::get( MAG_MSGNW );
1935 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
1936 $nowiki = true;
1937 } else {
1938 # Remove obsolete MSG:
1939 $mwMsg =& MagicWord::get( MAG_MSG );
1940 $mwMsg->matchStartAndRemove( $part1 );
1941 }
1942
1943 # Check if it is an internal message
1944 $mwInt =& MagicWord::get( MAG_INT );
1945 if ( $mwInt->matchStartAndRemove( $part1 ) ) {
1946 if ( $this->incrementIncludeCount( 'int:'.$part1 ) ) {
1947 $text = $linestart . wfMsgReal( $part1, $args, true );
1948 $found = true;
1949 }
1950 }
1951 }
1952
1953 # NS
1954 if ( !$found ) {
1955 # Check for NS: (namespace expansion)
1956 $mwNs = MagicWord::get( MAG_NS );
1957 if ( $mwNs->matchStartAndRemove( $part1 ) ) {
1958 if ( intval( $part1 ) ) {
1959 $text = $linestart . $wgContLang->getNsText( intval( $part1 ) );
1960 $found = true;
1961 } else {
1962 $index = Namespace::getCanonicalIndex( strtolower( $part1 ) );
1963 if ( !is_null( $index ) ) {
1964 $text = $linestart . $wgContLang->getNsText( $index );
1965 $found = true;
1966 }
1967 }
1968 }
1969 }
1970
1971 # LOCALURL and LOCALURLE
1972 if ( !$found ) {
1973 $mwLocal = MagicWord::get( MAG_LOCALURL );
1974 $mwLocalE = MagicWord::get( MAG_LOCALURLE );
1975
1976 if ( $mwLocal->matchStartAndRemove( $part1 ) ) {
1977 $func = 'getLocalURL';
1978 } elseif ( $mwLocalE->matchStartAndRemove( $part1 ) ) {
1979 $func = 'escapeLocalURL';
1980 } else {
1981 $func = '';
1982 }
1983
1984 if ( $func !== '' ) {
1985 $title = Title::newFromText( $part1 );
1986 if ( !is_null( $title ) ) {
1987 if ( $argc > 0 ) {
1988 $text = $linestart . $title->$func( $args[0] );
1989 } else {
1990 $text = $linestart . $title->$func();
1991 }
1992 $found = true;
1993 }
1994 }
1995 }
1996
1997 # GRAMMAR
1998 if ( !$found && $argc == 1 ) {
1999 $mwGrammar =& MagicWord::get( MAG_GRAMMAR );
2000 if ( $mwGrammar->matchStartAndRemove( $part1 ) ) {
2001 $text = $linestart . $wgContLang->convertGrammar( $args[0], $part1 );
2002 $found = true;
2003 }
2004 }
2005
2006 # Template table test
2007
2008 # Did we encounter this template already? If yes, it is in the cache
2009 # and we need to check for loops.
2010 if ( !$found && isset( $this->mTemplates[$part1] ) ) {
2011 # set $text to cached message.
2012 $text = $linestart . $this->mTemplates[$part1];
2013 $found = true;
2014
2015 # Infinite loop test
2016 if ( isset( $this->mTemplatePath[$part1] ) ) {
2017 $noparse = true;
2018 $found = true;
2019 $text .= '<!-- WARNING: template loop detected -->';
2020 }
2021 }
2022
2023 # Load from database
2024 $itcamefromthedatabase = false;
2025 if ( !$found ) {
2026 $ns = NS_TEMPLATE;
2027 $part1 = $this->maybeDoSubpageLink( $part1, $subpage='' );
2028 if ($subpage !== '') {
2029 $ns = $this->mTitle->getNamespace();
2030 }
2031 $title = Title::newFromText( $part1, $ns );
2032 if ( !is_null( $title ) && !$title->isExternal() ) {
2033 # Check for excessive inclusion
2034 $dbk = $title->getPrefixedDBkey();
2035 if ( $this->incrementIncludeCount( $dbk ) ) {
2036 # This should never be reached.
2037 $article = new Article( $title );
2038 $articleContent = $article->getContentWithoutUsingSoManyDamnGlobals();
2039 if ( $articleContent !== false ) {
2040 $found = true;
2041 $text = $linestart . $articleContent;
2042 $itcamefromthedatabase = true;
2043 }
2044 }
2045
2046 # If the title is valid but undisplayable, make a link to it
2047 if ( $this->mOutputType == OT_HTML && !$found ) {
2048 $text = $linestart . '[['.$title->getPrefixedText().']]';
2049 $found = true;
2050 }
2051
2052 # Template cache array insertion
2053 $this->mTemplates[$part1] = $text;
2054 }
2055 }
2056
2057 # Recursive parsing, escaping and link table handling
2058 # Only for HTML output
2059 if ( $nowiki && $found && $this->mOutputType == OT_HTML ) {
2060 $text = wfEscapeWikiText( $text );
2061 } elseif ( ($this->mOutputType == OT_HTML || $this->mOutputType == OT_WIKI) && $found && !$noparse) {
2062 # Clean up argument array
2063 $assocArgs = array();
2064 $index = 1;
2065 foreach( $args as $arg ) {
2066 $eqpos = strpos( $arg, '=' );
2067 if ( $eqpos === false ) {
2068 $assocArgs[$index++] = $arg;
2069 } else {
2070 $name = trim( substr( $arg, 0, $eqpos ) );
2071 $value = trim( substr( $arg, $eqpos+1 ) );
2072 if ( $value === false ) {
2073 $value = '';
2074 }
2075 if ( $name !== false ) {
2076 $assocArgs[$name] = $value;
2077 }
2078 }
2079 }
2080
2081 # Add a new element to the templace recursion path
2082 $this->mTemplatePath[$part1] = 1;
2083
2084 $text = $this->strip( $text, $this->mStripState );
2085 $text = $this->removeHTMLtags( $text );
2086 $text = $this->replaceVariables( $text, $assocArgs );
2087
2088 # Resume the link cache and register the inclusion as a link
2089 if ( $this->mOutputType == OT_HTML && !is_null( $title ) ) {
2090 $wgLinkCache->addLinkObj( $title );
2091 }
2092
2093 # If the template begins with a table or block-level
2094 # element, it should be treated as beginning a new line.
2095 if ($linestart !== '\n' && preg_match('/^({\\||:|;|#|\*)/', $text)) {
2096 $text = "\n" . $text;
2097 }
2098 }
2099
2100 # Empties the template path
2101 $this->mTemplatePath = array();
2102 if ( !$found ) {
2103 wfProfileOut( $fname );
2104 return $matches[0];
2105 } else {
2106 # replace ==section headers==
2107 # XXX this needs to go away once we have a better parser.
2108 if ( $this->mOutputType != OT_WIKI && $itcamefromthedatabase ) {
2109 if( !is_null( $title ) )
2110 $encodedname = base64_encode($title->getPrefixedDBkey());
2111 else
2112 $encodedname = base64_encode("");
2113 $m = preg_split('/(^={1,6}.*?={1,6}\s*?$)/m', $text, -1,
2114 PREG_SPLIT_DELIM_CAPTURE);
2115 $text = '';
2116 $nsec = 0;
2117 for( $i = 0; $i < count($m); $i += 2 ) {
2118 $text .= $m[$i];
2119 if (!isset($m[$i + 1]) || $m[$i + 1] == "") continue;
2120 $hl = $m[$i + 1];
2121 if( strstr($hl, "<!--MWTEMPLATESECTION") ) {
2122 $text .= $hl;
2123 continue;
2124 }
2125 preg_match('/^(={1,6})(.*?)(={1,6})\s*?$/m', $hl, $m2);
2126 $text .= $m2[1] . $m2[2] . "<!--MWTEMPLATESECTION="
2127 . $encodedname . "&" . base64_encode("$nsec") . "-->" . $m2[3];
2128
2129 $nsec++;
2130 }
2131 }
2132 }
2133
2134 # Empties the template path
2135 $this->mTemplatePath = array();
2136
2137 if ( !$found ) {
2138 wfProfileOut( $fname );
2139 return $matches[0];
2140 } else {
2141 wfProfileOut( $fname );
2142 return $text;
2143 }
2144 }
2145
2146 /**
2147 * Triple brace replacement -- used for template arguments
2148 * @access private
2149 */
2150 function argSubstitution( $matches ) {
2151 $arg = trim( $matches[1] );
2152 $text = $matches[0];
2153 $inputArgs = end( $this->mArgStack );
2154
2155 if ( array_key_exists( $arg, $inputArgs ) ) {
2156 $text = $inputArgs[$arg];
2157 }
2158
2159 return $text;
2160 }
2161
2162 /**
2163 * Returns true if the function is allowed to include this entity
2164 * @access private
2165 */
2166 function incrementIncludeCount( $dbk ) {
2167 if ( !array_key_exists( $dbk, $this->mIncludeCount ) ) {
2168 $this->mIncludeCount[$dbk] = 0;
2169 }
2170 if ( ++$this->mIncludeCount[$dbk] <= MAX_INCLUDE_REPEAT ) {
2171 return true;
2172 } else {
2173 return false;
2174 }
2175 }
2176
2177
2178 /**
2179 * Cleans up HTML, removes dangerous tags and attributes, and
2180 * removes HTML comments
2181 * @access private
2182 */
2183 function removeHTMLtags( $text ) {
2184 global $wgUseTidy, $wgUserHtml;
2185 $fname = 'Parser::removeHTMLtags';
2186 wfProfileIn( $fname );
2187
2188 if( $wgUserHtml ) {
2189 $htmlpairs = array( # Tags that must be closed
2190 'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1',
2191 'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's',
2192 'strike', 'strong', 'tt', 'var', 'div', 'center',
2193 'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre',
2194 'ruby', 'rt' , 'rb' , 'rp', 'p'
2195 );
2196 $htmlsingle = array(
2197 'br', 'hr', 'li', 'dt', 'dd'
2198 );
2199 $htmlnest = array( # Tags that can be nested--??
2200 'table', 'tr', 'td', 'th', 'div', 'blockquote', 'ol', 'ul',
2201 'dl', 'font', 'big', 'small', 'sub', 'sup'
2202 );
2203 $tabletags = array( # Can only appear inside table
2204 'td', 'th', 'tr'
2205 );
2206 } else {
2207 $htmlpairs = array();
2208 $htmlsingle = array();
2209 $htmlnest = array();
2210 $tabletags = array();
2211 }
2212
2213 $htmlsingle = array_merge( $tabletags, $htmlsingle );
2214 $htmlelements = array_merge( $htmlsingle, $htmlpairs );
2215
2216 $htmlattrs = $this->getHTMLattrs () ;
2217
2218 # Remove HTML comments
2219 $text = $this->removeHTMLcomments( $text );
2220
2221 $bits = explode( '<', $text );
2222 $text = array_shift( $bits );
2223 if(!$wgUseTidy) {
2224 $tagstack = array(); $tablestack = array();
2225 foreach ( $bits as $x ) {
2226 $prev = error_reporting( E_ALL & ~( E_NOTICE | E_WARNING ) );
2227 preg_match( '/^(\\/?)(\\w+)([^>]*)(\\/{0,1}>)([^<]*)$/',
2228 $x, $regs );
2229 list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs;
2230 error_reporting( $prev );
2231
2232 $badtag = 0 ;
2233 if ( in_array( $t = strtolower( $t ), $htmlelements ) ) {
2234 # Check our stack
2235 if ( $slash ) {
2236 # Closing a tag...
2237 if ( ! in_array( $t, $htmlsingle ) &&
2238 ( $ot = @array_pop( $tagstack ) ) != $t ) {
2239 @array_push( $tagstack, $ot );
2240 $badtag = 1;
2241 } else {
2242 if ( $t == 'table' ) {
2243 $tagstack = array_pop( $tablestack );
2244 }
2245 $newparams = '';
2246 }
2247 } else {
2248 # Keep track for later
2249 if ( in_array( $t, $tabletags ) &&
2250 ! in_array( 'table', $tagstack ) ) {
2251 $badtag = 1;
2252 } else if ( in_array( $t, $tagstack ) &&
2253 ! in_array ( $t , $htmlnest ) ) {
2254 $badtag = 1 ;
2255 } else if ( ! in_array( $t, $htmlsingle ) ) {
2256 if ( $t == 'table' ) {
2257 array_push( $tablestack, $tagstack );
2258 $tagstack = array();
2259 }
2260 array_push( $tagstack, $t );
2261 }
2262 # Strip non-approved attributes from the tag
2263 $newparams = $this->fixTagAttributes($params);
2264
2265 }
2266 if ( ! $badtag ) {
2267 $rest = str_replace( '>', '&gt;', $rest );
2268 $text .= "<$slash$t $newparams$brace$rest";
2269 continue;
2270 }
2271 }
2272 $text .= '&lt;' . str_replace( '>', '&gt;', $x);
2273 }
2274 # Close off any remaining tags
2275 while ( is_array( $tagstack ) && ($t = array_pop( $tagstack )) ) {
2276 $text .= "</$t>\n";
2277 if ( $t == 'table' ) { $tagstack = array_pop( $tablestack ); }
2278 }
2279 } else {
2280 # this might be possible using tidy itself
2281 foreach ( $bits as $x ) {
2282 preg_match( '/^(\\/?)(\\w+)([^>]*)(\\/{0,1}>)([^<]*)$/',
2283 $x, $regs );
2284 @list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs;
2285 if ( in_array( $t = strtolower( $t ), $htmlelements ) ) {
2286 $newparams = $this->fixTagAttributes($params);
2287 $rest = str_replace( '>', '&gt;', $rest );
2288 $text .= "<$slash$t $newparams$brace$rest";
2289 } else {
2290 $text .= '&lt;' . str_replace( '>', '&gt;', $x);
2291 }
2292 }
2293 }
2294 wfProfileOut( $fname );
2295 return $text;
2296 }
2297
2298 /**
2299 * Remove '<!--', '-->', and everything between.
2300 * To avoid leaving blank lines, when a comment is both preceded
2301 * and followed by a newline (ignoring spaces), trim leading and
2302 * trailing spaces and one of the newlines.
2303 *
2304 * @access private
2305 */
2306 function removeHTMLcomments( $text ) {
2307 $fname='Parser::removeHTMLcomments';
2308 wfProfileIn( $fname );
2309 while (($start = strpos($text, '<!--')) !== false) {
2310 $end = strpos($text, '-->', $start + 4);
2311 if ($end === false) {
2312 # Unterminated comment; bail out
2313 break;
2314 }
2315
2316 $end += 3;
2317
2318 # Trim space and newline if the comment is both
2319 # preceded and followed by a newline
2320 $spaceStart = max($start - 1, 0);
2321 $spaceLen = $end - $spaceStart;
2322 while (substr($text, $spaceStart, 1) === ' ' && $spaceStart > 0) {
2323 $spaceStart--;
2324 $spaceLen++;
2325 }
2326 while (substr($text, $spaceStart + $spaceLen, 1) === ' ')
2327 $spaceLen++;
2328 if (substr($text, $spaceStart, 1) === "\n" and substr($text, $spaceStart + $spaceLen, 1) === "\n") {
2329 # Remove the comment, leading and trailing
2330 # spaces, and leave only one newline.
2331 $text = substr_replace($text, "\n", $spaceStart, $spaceLen + 1);
2332 }
2333 else {
2334 # Remove just the comment.
2335 $text = substr_replace($text, '', $start, $end - $start);
2336 }
2337 }
2338 wfProfileOut( $fname );
2339 return $text;
2340 }
2341
2342 /**
2343 * This function accomplishes several tasks:
2344 * 1) Auto-number headings if that option is enabled
2345 * 2) Add an [edit] link to sections for logged in users who have enabled the option
2346 * 3) Add a Table of contents on the top for users who have enabled the option
2347 * 4) Auto-anchor headings
2348 *
2349 * It loops through all headlines, collects the necessary data, then splits up the
2350 * string and re-inserts the newly formatted headlines.
2351 * @access private
2352 */
2353 /* private */ function formatHeadings( $text, $isMain=true ) {
2354 global $wgInputEncoding, $wgMaxTocLevel, $wgContLang, $wgLinkHolders;
2355
2356 $doNumberHeadings = $this->mOptions->getNumberHeadings();
2357 $doShowToc = $this->mOptions->getShowToc();
2358 $forceTocHere = false;
2359 if( !$this->mTitle->userCanEdit() ) {
2360 $showEditLink = 0;
2361 $rightClickHack = 0;
2362 } else {
2363 $showEditLink = $this->mOptions->getEditSection();
2364 $rightClickHack = $this->mOptions->getEditSectionOnRightClick();
2365 }
2366
2367 # Inhibit editsection links if requested in the page
2368 $esw =& MagicWord::get( MAG_NOEDITSECTION );
2369 if( $esw->matchAndRemove( $text ) ) {
2370 $showEditLink = 0;
2371 }
2372 # if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
2373 # do not add TOC
2374 $mw =& MagicWord::get( MAG_NOTOC );
2375 if( $mw->matchAndRemove( $text ) ) {
2376 $doShowToc = 0;
2377 }
2378
2379 # never add the TOC to the Main Page. This is an entry page that should not
2380 # be more than 1-2 screens large anyway
2381 if( $this->mTitle->getPrefixedText() == wfMsg('mainpage') ) {
2382 $doShowToc = 0;
2383 }
2384
2385 # Get all headlines for numbering them and adding funky stuff like [edit]
2386 # links - this is for later, but we need the number of headlines right now
2387 $numMatches = preg_match_all( '/<H([1-6])(.*?' . '>)(.*?)<\/H[1-6]>/i', $text, $matches );
2388
2389 # if there are fewer than 4 headlines in the article, do not show TOC
2390 if( $numMatches < 4 ) {
2391 $doShowToc = 0;
2392 }
2393
2394 # if the string __TOC__ (not case-sensitive) occurs in the HTML,
2395 # override above conditions and always show TOC at that place
2396 $mw =& MagicWord::get( MAG_TOC );
2397 if ($mw->match( $text ) ) {
2398 $doShowToc = 1;
2399 $forceTocHere = true;
2400 } else {
2401 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
2402 # override above conditions and always show TOC above first header
2403 $mw =& MagicWord::get( MAG_FORCETOC );
2404 if ($mw->matchAndRemove( $text ) ) {
2405 $doShowToc = 1;
2406 }
2407 }
2408
2409
2410
2411 # We need this to perform operations on the HTML
2412 $sk =& $this->mOptions->getSkin();
2413
2414 # headline counter
2415 $headlineCount = 0;
2416 $sectionCount = 0; # headlineCount excluding template sections
2417
2418 # Ugh .. the TOC should have neat indentation levels which can be
2419 # passed to the skin functions. These are determined here
2420 $toclevel = 0;
2421 $toc = '';
2422 $full = '';
2423 $head = array();
2424 $sublevelCount = array();
2425 $level = 0;
2426 $prevlevel = 0;
2427 foreach( $matches[3] as $headline ) {
2428 $istemplate = 0;
2429 $templatetitle = "";
2430 $templatesection = 0;
2431
2432 if (preg_match("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", $headline, $mat)) {
2433 $istemplate = 1;
2434 $templatetitle = base64_decode($mat[1]);
2435 $templatesection = 1 + (int)base64_decode($mat[2]);
2436 $headline = preg_replace("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", "", $headline);
2437 }
2438
2439 $numbering = '';
2440 if( $level ) {
2441 $prevlevel = $level;
2442 }
2443 $level = $matches[1][$headlineCount];
2444 if( ( $doNumberHeadings || $doShowToc ) && $prevlevel && $level > $prevlevel ) {
2445 # reset when we enter a new level
2446 $sublevelCount[$level] = 0;
2447 $toc .= $sk->tocIndent( $level - $prevlevel );
2448 $toclevel += $level - $prevlevel;
2449 }
2450 if( ( $doNumberHeadings || $doShowToc ) && $level < $prevlevel ) {
2451 # reset when we step back a level
2452 $sublevelCount[$level+1]=0;
2453 $toc .= $sk->tocUnindent( $prevlevel - $level );
2454 $toclevel -= $prevlevel - $level;
2455 }
2456 # count number of headlines for each level
2457 @$sublevelCount[$level]++;
2458 if( $doNumberHeadings || $doShowToc ) {
2459 $dot = 0;
2460 for( $i = 1; $i <= $level; $i++ ) {
2461 if( !empty( $sublevelCount[$i] ) ) {
2462 if( $dot ) {
2463 $numbering .= '.';
2464 }
2465 $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
2466 $dot = 1;
2467 }
2468 }
2469 }
2470
2471 # The canonized header is a version of the header text safe to use for links
2472 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
2473 $canonized_headline = $this->unstrip( $headline, $this->mStripState );
2474 $canonized_headline = $this->unstripNoWiki( $headline, $this->mStripState );
2475
2476 # Remove link placeholders by the link text.
2477 # <!--LINK number-->
2478 # turns into
2479 # link text with suffix
2480 $canonized_headline = preg_replace( '/<!--LINK ([0-9]*)-->/e',
2481 "\$wgLinkHolders['texts'][\$1]",
2482 $canonized_headline );
2483
2484 # strip out HTML
2485 $canonized_headline = preg_replace( '/<.*?' . '>/','',$canonized_headline );
2486 $tocline = trim( $canonized_headline );
2487 $canonized_headline = urlencode( do_html_entity_decode( str_replace(' ', '_', $tocline), ENT_COMPAT, $wgInputEncoding ) );
2488 $replacearray = array(
2489 '%3A' => ':',
2490 '%' => '.'
2491 );
2492 $canonized_headline = str_replace(array_keys($replacearray),array_values($replacearray),$canonized_headline);
2493 $refer[$headlineCount] = $canonized_headline;
2494
2495 # count how many in assoc. array so we can track dupes in anchors
2496 @$refers[$canonized_headline]++;
2497 $refcount[$headlineCount]=$refers[$canonized_headline];
2498
2499 # Prepend the number to the heading text
2500
2501 if( $doNumberHeadings || $doShowToc ) {
2502 $tocline = $numbering . ' ' . $tocline;
2503
2504 # Don't number the heading if it is the only one (looks silly)
2505 if( $doNumberHeadings && count( $matches[3] ) > 1) {
2506 # the two are different if the line contains a link
2507 $headline=$numbering . ' ' . $headline;
2508 }
2509 }
2510
2511 # Create the anchor for linking from the TOC to the section
2512 $anchor = $canonized_headline;
2513 if($refcount[$headlineCount] > 1 ) {
2514 $anchor .= '_' . $refcount[$headlineCount];
2515 }
2516 if( $doShowToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
2517 $toc .= $sk->tocLine($anchor,$tocline,$toclevel);
2518 }
2519 if( $showEditLink && ( !$istemplate || $templatetitle !== "" ) ) {
2520 if ( empty( $head[$headlineCount] ) ) {
2521 $head[$headlineCount] = '';
2522 }
2523 if( $istemplate )
2524 $head[$headlineCount] .= $sk->editSectionLinkForOther($templatetitle, $templatesection);
2525 else
2526 $head[$headlineCount] .= $sk->editSectionLink($this->mTitle, $sectionCount+1);
2527 }
2528
2529 # Add the edit section span
2530 if( $rightClickHack ) {
2531 if( $istemplate )
2532 $headline = $sk->editSectionScriptForOther($templatetitle, $templatesection, $headline);
2533 else
2534 $headline = $sk->editSectionScript($this->mTitle, $sectionCount+1,$headline);
2535 }
2536
2537 # give headline the correct <h#> tag
2538 @$head[$headlineCount] .= "<a name=\"$anchor\"></a><h".$level.$matches[2][$headlineCount] .$headline.'</h'.$level.'>';
2539
2540 $headlineCount++;
2541 if( !$istemplate )
2542 $sectionCount++;
2543 }
2544
2545 if( $doShowToc ) {
2546 $toclines = $headlineCount;
2547 $toc .= $sk->tocUnindent( $toclevel );
2548 $toc = $sk->tocTable( $toc );
2549 }
2550
2551 # split up and insert constructed headlines
2552
2553 $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
2554 $i = 0;
2555
2556 foreach( $blocks as $block ) {
2557 if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) {
2558 # This is the [edit] link that appears for the top block of text when
2559 # section editing is enabled
2560
2561 # Disabled because it broke block formatting
2562 # For example, a bullet point in the top line
2563 # $full .= $sk->editSectionLink(0);
2564 }
2565 $full .= $block;
2566 if( $doShowToc && !$i && $isMain && !$forceTocHere) {
2567 # Top anchor now in skin
2568 $full = $full.$toc;
2569 }
2570
2571 if( !empty( $head[$i] ) ) {
2572 $full .= $head[$i];
2573 }
2574 $i++;
2575 }
2576 if($forceTocHere) {
2577 $mw =& MagicWord::get( MAG_TOC );
2578 return $mw->replace( $toc, $full );
2579 } else {
2580 return $full;
2581 }
2582 }
2583
2584 /**
2585 * Return an HTML link for the "ISBN 123456" text
2586 * @access private
2587 */
2588 function magicISBN( $text ) {
2589 global $wgLang;
2590 $fname = 'Parser::magicISBN';
2591 wfProfileIn( $fname );
2592
2593 $a = split( 'ISBN ', ' '.$text );
2594 if ( count ( $a ) < 2 ) {
2595 wfProfileOut( $fname );
2596 return $text;
2597 }
2598 $text = substr( array_shift( $a ), 1);
2599 $valid = '0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ';
2600
2601 foreach ( $a as $x ) {
2602 $isbn = $blank = '' ;
2603 while ( ' ' == $x{0} ) {
2604 $blank .= ' ';
2605 $x = substr( $x, 1 );
2606 }
2607 if ( $x == '' ) { # blank isbn
2608 $text .= "ISBN $blank";
2609 continue;
2610 }
2611 while ( strstr( $valid, $x{0} ) != false ) {
2612 $isbn .= $x{0};
2613 $x = substr( $x, 1 );
2614 }
2615 $num = str_replace( '-', '', $isbn );
2616 $num = str_replace( ' ', '', $num );
2617
2618 if ( '' == $num ) {
2619 $text .= "ISBN $blank$x";
2620 } else {
2621 $titleObj = Title::makeTitle( NS_SPECIAL, 'Booksources' );
2622 $text .= '<a href="' .
2623 $titleObj->escapeLocalUrl( 'isbn='.$num ) .
2624 "\" class=\"internal\">ISBN $isbn</a>";
2625 $text .= $x;
2626 }
2627 }
2628 wfProfileOut( $fname );
2629 return $text;
2630 }
2631
2632 /**
2633 * Return an HTML link for the "GEO ..." text
2634 * @access private
2635 */
2636 function magicGEO( $text ) {
2637 global $wgLang, $wgUseGeoMode;
2638 $fname = 'Parser::magicGEO';
2639 wfProfileIn( $fname );
2640
2641 # These next five lines are only for the ~35000 U.S. Census Rambot pages...
2642 $directions = array ( 'N' => 'North' , 'S' => 'South' , 'E' => 'East' , 'W' => 'West' ) ;
2643 $text = preg_replace ( "/(\d+)&deg;(\d+)'(\d+)\" {$directions['N']}, (\d+)&deg;(\d+)'(\d+)\" {$directions['W']}/" , "(GEO +\$1.\$2.\$3:-\$4.\$5.\$6)" , $text ) ;
2644 $text = preg_replace ( "/(\d+)&deg;(\d+)'(\d+)\" {$directions['N']}, (\d+)&deg;(\d+)'(\d+)\" {$directions['E']}/" , "(GEO +\$1.\$2.\$3:+\$4.\$5.\$6)" , $text ) ;
2645 $text = preg_replace ( "/(\d+)&deg;(\d+)'(\d+)\" {$directions['S']}, (\d+)&deg;(\d+)'(\d+)\" {$directions['W']}/" , "(GEO +\$1.\$2.\$3:-\$4.\$5.\$6)" , $text ) ;
2646 $text = preg_replace ( "/(\d+)&deg;(\d+)'(\d+)\" {$directions['S']}, (\d+)&deg;(\d+)'(\d+)\" {$directions['E']}/" , "(GEO +\$1.\$2.\$3:+\$4.\$5.\$6)" , $text ) ;
2647
2648 $a = split( 'GEO ', ' '.$text );
2649 if ( count ( $a ) < 2 ) {
2650 wfProfileOut( $fname );
2651 return $text;
2652 }
2653 $text = substr( array_shift( $a ), 1);
2654 $valid = '0123456789.+-:';
2655
2656 foreach ( $a as $x ) {
2657 $geo = $blank = '' ;
2658 while ( ' ' == $x{0} ) {
2659 $blank .= ' ';
2660 $x = substr( $x, 1 );
2661 }
2662 while ( strstr( $valid, $x{0} ) != false ) {
2663 $geo .= $x{0};
2664 $x = substr( $x, 1 );
2665 }
2666 $num = str_replace( '+', '', $geo );
2667 $num = str_replace( ' ', '', $num );
2668
2669 if ( '' == $num || count ( explode ( ':' , $num , 3 ) ) < 2 ) {
2670 $text .= "GEO $blank$x";
2671 } else {
2672 $titleObj = Title::makeTitle( NS_SPECIAL, 'Geo' );
2673 $text .= '<a href="' .
2674 $titleObj->escapeLocalUrl( 'coordinates='.$num ) .
2675 "\" class=\"internal\">GEO $geo</a>";
2676 $text .= $x;
2677 }
2678 }
2679 wfProfileOut( $fname );
2680 return $text;
2681 }
2682
2683 /**
2684 * Return an HTML link for the "RFC 1234" text
2685 * @access private
2686 * @param string $text text to be processed
2687 */
2688 function magicRFC( $text, $keyword='RFC ', $urlmsg='rfcurl' ) {
2689 global $wgLang;
2690
2691 $valid = '0123456789';
2692 $internal = false;
2693
2694 $a = split( $keyword, ' '.$text );
2695 if ( count ( $a ) < 2 ) {
2696 return $text;
2697 }
2698 $text = substr( array_shift( $a ), 1);
2699
2700 /* Check if keyword is preceed by [[.
2701 * This test is made here cause of the array_shift above
2702 * that prevent the test to be done in the foreach.
2703 */
2704 if ( substr( $text, -2 ) == '[[' ) {
2705 $internal = true;
2706 }
2707
2708 foreach ( $a as $x ) {
2709 /* token might be empty if we have RFC RFC 1234 */
2710 if ( $x=='' ) {
2711 $text.=$keyword;
2712 continue;
2713 }
2714
2715 $id = $blank = '' ;
2716
2717 /** remove and save whitespaces in $blank */
2718 while ( $x{0} == ' ' ) {
2719 $blank .= ' ';
2720 $x = substr( $x, 1 );
2721 }
2722
2723 /** remove and save the rfc number in $id */
2724 while ( strstr( $valid, $x{0} ) != false ) {
2725 $id .= $x{0};
2726 $x = substr( $x, 1 );
2727 }
2728
2729 if ( $id == '' ) {
2730 /* call back stripped spaces*/
2731 $text .= $keyword.$blank.$x;
2732 } elseif( $internal ) {
2733 /* normal link */
2734 $text .= $keyword.$id.$x;
2735 } else {
2736 /* build the external link*/
2737 $url = wfmsg( $urlmsg );
2738 $url = str_replace( '$1', $id, $url);
2739 $sk =& $this->mOptions->getSkin();
2740 $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
2741 $text .= "<a href='{$url}'{$la}>{$keyword}{$id}</a>{$x}";
2742 }
2743
2744 /* Check if the next RFC keyword is preceed by [[ */
2745 $internal = ( substr($x,-2) == '[[' );
2746 }
2747 return $text;
2748 }
2749
2750 /**
2751 * Transform wiki markup when saving a page by doing \r\n -> \n
2752 * conversion, substitting signatures, {{subst:}} templates, etc.
2753 *
2754 * @param string $text the text to transform
2755 * @param Title &$title the Title object for the current article
2756 * @param User &$user the User object describing the current user
2757 * @param ParserOptions $options parsing options
2758 * @param bool $clearState whether to clear the parser state first
2759 * @return string the altered wiki markup
2760 * @access public
2761 */
2762 function preSaveTransform( $text, &$title, &$user, $options, $clearState = true ) {
2763 $this->mOptions = $options;
2764 $this->mTitle =& $title;
2765 $this->mOutputType = OT_WIKI;
2766
2767 if ( $clearState ) {
2768 $this->clearState();
2769 }
2770
2771 $stripState = false;
2772 $pairs = array(
2773 "\r\n" => "\n",
2774 );
2775 $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
2776 $text = $this->strip( $text, $stripState, false );
2777 $text = $this->pstPass2( $text, $user );
2778 $text = $this->unstrip( $text, $stripState );
2779 $text = $this->unstripNoWiki( $text, $stripState );
2780 return $text;
2781 }
2782
2783 /**
2784 * Pre-save transform helper function
2785 * @access private
2786 */
2787 function pstPass2( $text, &$user ) {
2788 global $wgLang, $wgContLang, $wgLocaltimezone;
2789
2790 # Variable replacement
2791 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
2792 $text = $this->replaceVariables( $text );
2793
2794 # Signatures
2795 #
2796 $n = $user->getName();
2797 $k = $user->getOption( 'nickname' );
2798 if ( '' == $k ) { $k = $n; }
2799 if ( isset( $wgLocaltimezone ) ) {
2800 $oldtz = getenv( 'TZ' );
2801 putenv( 'TZ='.$wgLocaltimezone );
2802 }
2803 /* Note: this is an ugly timezone hack for the European wikis */
2804 $d = $wgContLang->timeanddate( date( 'YmdHis' ), false ) .
2805 ' (' . date( 'T' ) . ')';
2806 if ( isset( $wgLocaltimezone ) ) {
2807 putenv( 'TZ='.$oldtzs );
2808 }
2809
2810 $text = preg_replace( '/~~~~~~/', $d, $text );
2811 $text = preg_replace( '/~~~~/', '[[' . $wgContLang->getNsText( NS_USER ) . ":$n|$k]] $d", $text );
2812 $text = preg_replace( '/~~~/', '[[' . $wgContLang->getNsText( NS_USER ) . ":$n|$k]]", $text );
2813
2814 # Context links: [[|name]] and [[name (context)|]]
2815 #
2816 $tc = "[&;%\\-,.\\(\\)' _0-9A-Za-z\\/:\\x80-\\xff]";
2817 $np = "[&;%\\-,.' _0-9A-Za-z\\/:\\x80-\\xff]"; # No parens
2818 $namespacechar = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii!
2819 $conpat = "/^({$np}+) \\(({$tc}+)\\)$/";
2820
2821 $p1 = "/\[\[({$np}+) \\(({$np}+)\\)\\|]]/"; # [[page (context)|]]
2822 $p2 = "/\[\[\\|({$tc}+)]]/"; # [[|page]]
2823 $p3 = "/\[\[(:*$namespacechar+):({$np}+)\\|]]/"; # [[namespace:page|]] and [[:namespace:page|]]
2824 $p4 = "/\[\[(:*$namespacechar+):({$np}+) \\(({$np}+)\\)\\|]]/"; # [[ns:page (cont)|]] and [[:ns:page (cont)|]]
2825 $context = '';
2826 $t = $this->mTitle->getText();
2827 if ( preg_match( $conpat, $t, $m ) ) {
2828 $context = $m[2];
2829 }
2830 $text = preg_replace( $p4, '[[\\1:\\2 (\\3)|\\2]]', $text );
2831 $text = preg_replace( $p1, '[[\\1 (\\2)|\\1]]', $text );
2832 $text = preg_replace( $p3, '[[\\1:\\2|\\2]]', $text );
2833
2834 if ( '' == $context ) {
2835 $text = preg_replace( $p2, '[[\\1]]', $text );
2836 } else {
2837 $text = preg_replace( $p2, "[[\\1 ({$context})|\\1]]", $text );
2838 }
2839
2840 # Trim trailing whitespace
2841 # MAG_END (__END__) tag allows for trailing
2842 # whitespace to be deliberately included
2843 $text = rtrim( $text );
2844 $mw =& MagicWord::get( MAG_END );
2845 $mw->matchAndRemove( $text );
2846
2847 return $text;
2848 }
2849
2850 /**
2851 * Set up some variables which are usually set up in parse()
2852 * so that an external function can call some class members with confidence
2853 * @access public
2854 */
2855 function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
2856 $this->mTitle =& $title;
2857 $this->mOptions = $options;
2858 $this->mOutputType = $outputType;
2859 if ( $clearState ) {
2860 $this->clearState();
2861 }
2862 }
2863
2864 /**
2865 * Transform a MediaWiki message by replacing magic variables.
2866 *
2867 * @param string $text the text to transform
2868 * @param ParserOptions $options options
2869 * @return string the text with variables substituted
2870 * @access public
2871 */
2872 function transformMsg( $text, $options ) {
2873 global $wgTitle;
2874 static $executing = false;
2875
2876 # Guard against infinite recursion
2877 if ( $executing ) {
2878 return $text;
2879 }
2880 $executing = true;
2881
2882 $this->mTitle = $wgTitle;
2883 $this->mOptions = $options;
2884 $this->mOutputType = OT_MSG;
2885 $this->clearState();
2886 $text = $this->replaceVariables( $text );
2887
2888 $executing = false;
2889 return $text;
2890 }
2891
2892 /**
2893 * Create an HTML-style tag, e.g. <yourtag>special text</yourtag>
2894 * Callback will be called with the text within
2895 * Transform and return the text within
2896 * @access public
2897 */
2898 function setHook( $tag, $callback ) {
2899 $oldVal = @$this->mTagHooks[$tag];
2900 $this->mTagHooks[$tag] = $callback;
2901 return $oldVal;
2902 }
2903
2904 /**
2905 * Replace <!--LINK--> link placeholders with actual links, in the buffer
2906 * Placeholders created in Skin::makeLinkObj()
2907 * Returns an array of links found, indexed by PDBK:
2908 * 0 - broken
2909 * 1 - normal link
2910 * 2 - stub
2911 * $options is a bit field, RLH_FOR_UPDATE to select for update
2912 */
2913 function replaceLinkHolders( &$text, $options = 0 ) {
2914 global $wgUser, $wgLinkCache, $wgUseOldExistenceCheck, $wgLinkHolders;
2915 global $wgInterwikiLinkHolders;
2916 global $outputReplace;
2917
2918 if ( $wgUseOldExistenceCheck ) {
2919 return array();
2920 }
2921
2922 $fname = 'Parser::replaceLinkHolders';
2923 wfProfileIn( $fname );
2924
2925 $pdbks = array();
2926 $colours = array();
2927
2928 #if ( !empty( $tmpLinks[0] ) ) { #TODO
2929 if ( !empty( $wgLinkHolders['namespaces'] ) ) {
2930 wfProfileIn( $fname.'-check' );
2931 $dbr =& wfGetDB( DB_SLAVE );
2932 $cur = $dbr->tableName( 'cur' );
2933 $sk = $wgUser->getSkin();
2934 $threshold = $wgUser->getOption('stubthreshold');
2935
2936 # Sort by namespace
2937 asort( $wgLinkHolders['namespaces'] );
2938
2939 # Generate query
2940 $query = false;
2941 foreach ( $wgLinkHolders['namespaces'] as $key => $val ) {
2942 # Make title object
2943 $title = $wgLinkHolders['titles'][$key];
2944
2945 # Skip invalid entries.
2946 # Result will be ugly, but prevents crash.
2947 if ( is_null( $title ) ) {
2948 continue;
2949 }
2950 $pdbk = $pdbks[$key] = $title->getPrefixedDBkey();
2951
2952 # Check if it's in the link cache already
2953 if ( $wgLinkCache->getGoodLinkID( $pdbk ) ) {
2954 $colours[$pdbk] = 1;
2955 } elseif ( $wgLinkCache->isBadLink( $pdbk ) ) {
2956 $colours[$pdbk] = 0;
2957 } else {
2958 # Not in the link cache, add it to the query
2959 if ( !isset( $current ) ) {
2960 $current = $val;
2961 $query = "SELECT cur_id, cur_namespace, cur_title";
2962 if ( $threshold > 0 ) {
2963 $query .= ", LENGTH(cur_text) AS cur_len, cur_is_redirect";
2964 }
2965 $query .= " FROM $cur WHERE (cur_namespace=$val AND cur_title IN(";
2966 } elseif ( $current != $val ) {
2967 $current = $val;
2968 $query .= ")) OR (cur_namespace=$val AND cur_title IN(";
2969 } else {
2970 $query .= ', ';
2971 }
2972
2973 $query .= $dbr->addQuotes( $wgLinkHolders['dbkeys'][$key] );
2974 }
2975 }
2976 if ( $query ) {
2977 $query .= '))';
2978 if ( $options & RLH_FOR_UPDATE ) {
2979 $query .= ' FOR UPDATE';
2980 }
2981
2982 $res = $dbr->query( $query, $fname );
2983
2984 # Fetch data and form into an associative array
2985 # non-existent = broken
2986 # 1 = known
2987 # 2 = stub
2988 while ( $s = $dbr->fetchObject($res) ) {
2989 $title = Title::makeTitle( $s->cur_namespace, $s->cur_title );
2990 $pdbk = $title->getPrefixedDBkey();
2991 $wgLinkCache->addGoodLink( $s->cur_id, $pdbk );
2992
2993 if ( $threshold > 0 ) {
2994 $size = $s->cur_len;
2995 if ( $s->cur_is_redirect || $s->cur_namespace != 0 || $length < $threshold ) {
2996 $colours[$pdbk] = 1;
2997 } else {
2998 $colours[$pdbk] = 2;
2999 }
3000 } else {
3001 $colours[$pdbk] = 1;
3002 }
3003 }
3004 }
3005 wfProfileOut( $fname.'-check' );
3006
3007 # Construct search and replace arrays
3008 wfProfileIn( $fname.'-construct' );
3009 $outputReplace = array();
3010 foreach ( $wgLinkHolders['namespaces'] as $key => $ns ) {
3011 $pdbk = $pdbks[$key];
3012 $searchkey = '<!--LINK '.$key.'-->';
3013 $title = $wgLinkHolders['titles'][$key];
3014 if ( empty( $colours[$pdbk] ) ) {
3015 $wgLinkCache->addBadLink( $pdbk );
3016 $colours[$pdbk] = 0;
3017 $outputReplace[$searchkey] = $sk->makeBrokenLinkObj( $title,
3018 $wgLinkHolders['texts'][$key],
3019 $wgLinkHolders['queries'][$key] );
3020 } elseif ( $colours[$pdbk] == 1 ) {
3021 $outputReplace[$searchkey] = $sk->makeKnownLinkObj( $title,
3022 $wgLinkHolders['texts'][$key],
3023 $wgLinkHolders['queries'][$key] );
3024 } elseif ( $colours[$pdbk] == 2 ) {
3025 $outputReplace[$searchkey] = $sk->makeStubLinkObj( $title,
3026 $wgLinkHolders['texts'][$key],
3027 $wgLinkHolders['queries'][$key] );
3028 }
3029 }
3030 wfProfileOut( $fname.'-construct' );
3031
3032 # Do the thing
3033 wfProfileIn( $fname.'-replace' );
3034
3035 $text = preg_replace_callback(
3036 '/(<!--LINK .*?-->)/',
3037 "outputReplaceMatches",
3038 $text);
3039 wfProfileOut( $fname.'-replace' );
3040 }
3041
3042 if ( !empty( $wgInterwikiLinkHolders ) ) {
3043 wfProfileIn( $fname.'-interwiki' );
3044 $outputReplace = $wgInterwikiLinkHolders;
3045 $text = preg_replace_callback(
3046 '/<!--IWLINK (.*?)-->/',
3047 "outputReplaceMatches",
3048 $text );
3049 wfProfileOut( $fname.'-interwiki' );
3050 }
3051
3052 wfProfileOut( $fname );
3053 return $colours;
3054 }
3055
3056 /**
3057 * Renders an image gallery from a text with one line per image.
3058 * text labels may be given by using |-style alternative text. E.g.
3059 * Image:one.jpg|The number "1"
3060 * Image:tree.jpg|A tree
3061 * given as text will return the HTML of a gallery with two images,
3062 * labeled 'The number "1"' and
3063 * 'A tree'.
3064 */
3065 function renderImageGallery( $text ) {
3066 global $wgLinkCache;
3067 $ig = new ImageGallery();
3068 $ig->setShowBytes( false );
3069 $ig->setShowFilename( false );
3070 $lines = explode( "\n", $text );
3071
3072 foreach ( $lines as $line ) {
3073 # match lines like these:
3074 # Image:someimage.jpg|This is some image
3075 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
3076 # Skip empty lines
3077 if ( count( $matches ) == 0 ) {
3078 continue;
3079 }
3080 $nt = Title::newFromURL( $matches[1] );
3081 if ( isset( $matches[3] ) ) {
3082 $label = $matches[3];
3083 } else {
3084 $label = '';
3085 }
3086 $ig->add( Image::newFromTitle( $nt ), $label );
3087 $wgLinkCache->addImageLinkObj( $nt );
3088 }
3089 return $ig->toHTML();
3090 }
3091 }
3092
3093 /**
3094 * @todo document
3095 * @package MediaWiki
3096 */
3097 class ParserOutput
3098 {
3099 var $mText, $mLanguageLinks, $mCategoryLinks, $mContainsOldMagic;
3100 var $mCacheTime; # Used in ParserCache
3101 var $mVersion; # Compatibility check
3102
3103 function ParserOutput( $text = '', $languageLinks = array(), $categoryLinks = array(),
3104 $containsOldMagic = false )
3105 {
3106 $this->mText = $text;
3107 $this->mLanguageLinks = $languageLinks;
3108 $this->mCategoryLinks = $categoryLinks;
3109 $this->mContainsOldMagic = $containsOldMagic;
3110 $this->mCacheTime = '';
3111 $this->mVersion = MW_PARSER_VERSION;
3112 }
3113
3114 function getText() { return $this->mText; }
3115 function getLanguageLinks() { return $this->mLanguageLinks; }
3116 function getCategoryLinks() { return array_keys( $this->mCategoryLinks ); }
3117 function getCacheTime() { return $this->mCacheTime; }
3118 function containsOldMagic() { return $this->mContainsOldMagic; }
3119 function setText( $text ) { return wfSetVar( $this->mText, $text ); }
3120 function setLanguageLinks( $ll ) { return wfSetVar( $this->mLanguageLinks, $ll ); }
3121 function setCategoryLinks( $cl ) { return wfSetVar( $this->mCategoryLinks, $cl ); }
3122 function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); }
3123 function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); }
3124 function addCategoryLink( $c ) { $this->mCategoryLinks[$c] = 1; }
3125
3126 function merge( $other ) {
3127 $this->mLanguageLinks = array_merge( $this->mLanguageLinks, $other->mLanguageLinks );
3128 $this->mCategoryLinks = array_merge( $this->mCategoryLinks, $this->mLanguageLinks );
3129 $this->mContainsOldMagic = $this->mContainsOldMagic || $other->mContainsOldMagic;
3130 }
3131
3132 /**
3133 * Return true if this cached output object predates the global or
3134 * per-article cache invalidation timestamps, or if it comes from
3135 * an incompatible older version.
3136 *
3137 * @param string $touched the affected article's last touched timestamp
3138 * @return bool
3139 * @access public
3140 */
3141 function expired( $touched ) {
3142 global $wgCacheEpoch;
3143 return $this->getCacheTime() <= $touched ||
3144 $this->getCacheTime() <= $wgCacheEpoch ||
3145 !isset( $this->mVersion ) ||
3146 version_compare( $this->mVersion, MW_PARSER_VERSION, "lt" );
3147 }
3148 }
3149
3150 /**
3151 * Set options of the Parser
3152 * @todo document
3153 * @package MediaWiki
3154 */
3155 class ParserOptions
3156 {
3157 # All variables are private
3158 var $mUseTeX; # Use texvc to expand <math> tags
3159 var $mUseDynamicDates; # Use $wgDateFormatter to format dates
3160 var $mInterwikiMagic; # Interlanguage links are removed and returned in an array
3161 var $mAllowExternalImages; # Allow external images inline
3162 var $mSkin; # Reference to the preferred skin
3163 var $mDateFormat; # Date format index
3164 var $mEditSection; # Create "edit section" links
3165 var $mEditSectionOnRightClick; # Generate JavaScript to edit section on right click
3166 var $mNumberHeadings; # Automatically number headings
3167 var $mShowToc; # Show table of contents
3168
3169 function getUseTeX() { return $this->mUseTeX; }
3170 function getUseDynamicDates() { return $this->mUseDynamicDates; }
3171 function getInterwikiMagic() { return $this->mInterwikiMagic; }
3172 function getAllowExternalImages() { return $this->mAllowExternalImages; }
3173 function getSkin() { return $this->mSkin; }
3174 function getDateFormat() { return $this->mDateFormat; }
3175 function getEditSection() { return $this->mEditSection; }
3176 function getEditSectionOnRightClick() { return $this->mEditSectionOnRightClick; }
3177 function getNumberHeadings() { return $this->mNumberHeadings; }
3178 function getShowToc() { return $this->mShowToc; }
3179
3180 function setUseTeX( $x ) { return wfSetVar( $this->mUseTeX, $x ); }
3181 function setUseDynamicDates( $x ) { return wfSetVar( $this->mUseDynamicDates, $x ); }
3182 function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); }
3183 function setAllowExternalImages( $x ) { return wfSetVar( $this->mAllowExternalImages, $x ); }
3184 function setDateFormat( $x ) { return wfSetVar( $this->mDateFormat, $x ); }
3185 function setEditSection( $x ) { return wfSetVar( $this->mEditSection, $x ); }
3186 function setEditSectionOnRightClick( $x ) { return wfSetVar( $this->mEditSectionOnRightClick, $x ); }
3187 function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); }
3188 function setShowToc( $x ) { return wfSetVar( $this->mShowToc, $x ); }
3189
3190 function setSkin( &$x ) { $this->mSkin =& $x; }
3191
3192 # Get parser options
3193 /* static */ function newFromUser( &$user ) {
3194 $popts = new ParserOptions;
3195 $popts->initialiseFromUser( $user );
3196 return $popts;
3197 }
3198
3199 # Get user options
3200 function initialiseFromUser( &$userInput ) {
3201 global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages;
3202 $fname = 'ParserOptions::initialiseFromUser';
3203 wfProfileIn( $fname );
3204 if ( !$userInput ) {
3205 $user = new User;
3206 $user->setLoaded( true );
3207 } else {
3208 $user =& $userInput;
3209 }
3210
3211 $this->mUseTeX = $wgUseTeX;
3212 $this->mUseDynamicDates = $wgUseDynamicDates;
3213 $this->mInterwikiMagic = $wgInterwikiMagic;
3214 $this->mAllowExternalImages = $wgAllowExternalImages;
3215 wfProfileIn( $fname.'-skin' );
3216 $this->mSkin =& $user->getSkin();
3217 wfProfileOut( $fname.'-skin' );
3218 $this->mDateFormat = $user->getOption( 'date' );
3219 $this->mEditSection = $user->getOption( 'editsection' );
3220 $this->mEditSectionOnRightClick = $user->getOption( 'editsectiononrightclick' );
3221 $this->mNumberHeadings = $user->getOption( 'numberheadings' );
3222 $this->mShowToc = $user->getOption( 'showtoc' );
3223 wfProfileOut( $fname );
3224 }
3225
3226
3227 }
3228
3229 /**
3230 * Callback function used by Parser::replaceLinkHolders()
3231 * to substitute link placeholders.
3232 */
3233 function &outputReplaceMatches( $matches ) {
3234 global $outputReplace;
3235 return $outputReplace[$matches[1]];
3236 }
3237
3238 /**
3239 * Return the total number of articles
3240 */
3241 function wfNumberOfArticles() {
3242 global $wgNumberOfArticles;
3243
3244 wfLoadSiteStats();
3245 return $wgNumberOfArticles;
3246 }
3247
3248 /**
3249 * Get various statistics from the database
3250 * @private
3251 */
3252 function wfLoadSiteStats() {
3253 global $wgNumberOfArticles, $wgTotalViews, $wgTotalEdits;
3254 $fname = 'wfLoadSiteStats';
3255
3256 if ( -1 != $wgNumberOfArticles ) return;
3257 $dbr =& wfGetDB( DB_SLAVE );
3258 $s = $dbr->selectRow( 'site_stats',
3259 array( 'ss_total_views', 'ss_total_edits', 'ss_good_articles' ),
3260 array( 'ss_row_id' => 1 ), $fname
3261 );
3262
3263 if ( $s === false ) {
3264 return;
3265 } else {
3266 $wgTotalViews = $s->ss_total_views;
3267 $wgTotalEdits = $s->ss_total_edits;
3268 $wgNumberOfArticles = $s->ss_good_articles;
3269 }
3270 }
3271
3272 function wfEscapeHTMLTagsOnly( $in ) {
3273 return str_replace(
3274 array( '"', '>', '<' ),
3275 array( '&quot;', '&gt;', '&lt;' ),
3276 $in );
3277 }
3278
3279 ?>