Updated fix for bug 869949 'Lists inside tables' as suggested by TomK32
[mediawiki.git] / includes / OutputPage.php
blob7c5c92752743acc4ee36cb53dce0f68779d100e7
1 <?
2 # See design.doc
4 if($wgUseTeX) include_once( "Math.php" );
6 class OutputPage {
7 var $mHeaders, $mCookies, $mMetatags, $mKeywords;
8 var $mLinktags, $mPagetitle, $mBodytext, $mDebugtext;
9 var $mHTMLtitle, $mRobotpolicy, $mIsarticle, $mPrintable;
10 var $mSubtitle, $mRedirect, $mAutonumber, $mHeadtext;
11 var $mLastModified, $mCategoryLinks;
13 var $mDTopen, $mLastSection; # Used for processing DL, PRE
14 var $mLanguageLinks, $mSupressQuickbar;
15 var $mOnloadHandler;
16 var $mDoNothing;
18 function OutputPage()
20 $this->mHeaders = $this->mCookies = $this->mMetatags =
21 $this->mKeywords = $this->mLinktags = array();
22 $this->mHTMLtitle = $this->mPagetitle = $this->mBodytext =
23 $this->mLastSection = $this->mRedirect = $this->mLastModified =
24 $this->mSubtitle = $this->mDebugtext = $this->mRobotpolicy =
25 $this->mOnloadHandler = "";
26 $this->mIsarticle = $this->mPrintable = true;
27 $this->mSupressQuickbar = $this->mDTopen = $this->mPrintable = false;
28 $this->mLanguageLinks = array();
29 $this->mCategoryLinks = array() ;
30 $this->mAutonumber = 0;
31 $this->mDoNothing = false;
34 function addHeader( $name, $val ) { array_push( $this->mHeaders, "$name: $val" ) ; }
35 function addCookie( $name, $val ) { array_push( $this->mCookies, array( $name, $val ) ); }
36 function redirect( $url ) { $this->mRedirect = $url; }
38 # To add an http-equiv meta tag, precede the name with "http:"
39 function addMeta( $name, $val ) { array_push( $this->mMetatags, array( $name, $val ) ); }
40 function addKeyword( $text ) { array_push( $this->mKeywords, $text ); }
41 function addLink( $rel, $rev, $target ) { array_push( $this->mLinktags, array( $rel, $rev, $target ) ); }
43 # checkLastModified tells the client to use the client-cached page if
44 # possible. If sucessful, the OutputPage is disabled so that
45 # any future call to OutputPage->output() have no effect. The method
46 # returns true iff cache-ok headers was sent.
47 function checkLastModified ( $timestamp )
49 global $wgLang, $wgCachePages, $wgUser;
50 if( !$wgCachePages ) {
51 wfDebug( "CACHE DISABLED\n", false );
52 return;
54 if( preg_match( '/MSIE ([1-4]|5\.0)/', $_SERVER["HTTP_USER_AGENT"] ) ) {
55 # IE 5.0 has probs with our caching
56 wfDebug( "-- bad client, not caching\n", false );
57 return;
59 if( $wgUser->getOption( "nocache" ) ) {
60 wfDebug( "USER DISABLED CACHE\n", false );
61 return;
64 $lastmod = gmdate( "D, j M Y H:i:s", wfTimestamp2Unix(
65 max( $timestamp, $wgUser->mTouched ) ) ) . " GMT";
67 if( !empty( $_SERVER["HTTP_IF_MODIFIED_SINCE"] ) ) {
68 # IE sends sizes after the date like this:
69 # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
70 # this breaks strtotime().
71 $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] );
72 $ismodsince = wfUnix2Timestamp( strtotime( $modsince ) );
73 wfDebug( "-- client send If-Modified-Since: " . $modsince . "\n", false );
74 wfDebug( "-- we might send Last-Modified : $lastmod\n", false );
76 if( ($ismodsince >= $timestamp ) and $wgUser->validateCache( $ismodsince ) ) {
77 # Make sure you're in a place you can leave when you call us!
78 header( "HTTP/1.0 304 Not Modified" );
79 header( "Expires: Mon, 15 Jan 2001 00:00:00 GMT" ); # Cachers always validate the page!
80 header( "Cache-Control: private, must-revalidate, max-age=0" );
81 header( "Last-Modified: {$lastmod}" );
82 wfDebug( "CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp\n", false );
83 $this->disable();
84 return true;
85 } else {
86 wfDebug( "READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp\n", false );
87 $this->mLastModified = $lastmod;
89 } else {
90 wfDebug( "We're confused.\n", false );
91 $this->mLastModified = $lastmod;
95 function setRobotpolicy( $str ) { $this->mRobotpolicy = $str; }
96 function setHTMLtitle( $name ) { $this->mHTMLtitle = $name; }
97 function setPageTitle( $name ) { $this->mPagetitle = $name; }
98 function getPageTitle() { return $this->mPagetitle; }
99 function setSubtitle( $str ) { $this->mSubtitle = $str; }
100 function getSubtitle() { return $this->mSubtitle; }
101 function setArticleFlag( $v ) { $this->mIsarticle = $v; }
102 function isArticle() { return $this->mIsarticle; }
103 function setPrintable() { $this->mPrintable = true; }
104 function isPrintable() { return $this->mPrintable; }
105 function setOnloadHandler( $js ) { $this->mOnloadHandler = $js; }
106 function getOnloadHandler() { return $this->mOnloadHandler; }
107 function disable() { $this->mDoNothing = true; }
109 function getLanguageLinks() {
110 global $wgTitle, $wgLanguageCode;
111 global $wgDBconnection, $wgDBname;
112 return $this->mLanguageLinks;
114 function supressQuickbar() { $this->mSupressQuickbar = true; }
115 function isQuickbarSupressed() { return $this->mSupressQuickbar; }
117 function addHTML( $text ) { $this->mBodytext .= $text; }
118 function addHeadtext( $text ) { $this->mHeadtext .= $text; }
119 function debug( $text ) { $this->mDebugtext .= $text; }
121 # First pass--just handle <nowiki> sections, pass the rest off
122 # to doWikiPass2() which does all the real work.
125 function addWikiText( $text, $linestart = true )
127 global $wgUseTeX;
128 $fname = "OutputPage::addWikiText";
129 wfProfileIn( $fname );
130 $unique = "3iyZiyA7iMwg5rhxP0Dcc9oTnj8qD1jm1Sfv4";
131 $unique2 = "4LIQ9nXtiYFPCSfitVwDw7EYwQlL4GeeQ7qSO";
132 $unique3 = "fPaA8gDfdLBqzj68Yjg9Hil3qEF8JGO0uszIp";
133 $nwlist = array();
134 $nwsecs = 0;
135 $mathlist = array();
136 $mathsecs = 0;
137 $prelist = array ();
138 $presecs = 0;
139 $stripped = "";
140 $stripped2 = "";
141 $stripped3 = "";
143 while ( "" != $text ) {
144 $p = preg_split( "/<\\s*nowiki\\s*>/i", $text, 2 );
145 $stripped .= $p[0];
146 if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { $text = ""; }
147 else {
148 $q = preg_split( "/<\\/\\s*nowiki\\s*>/i", $p[1], 2 );
149 ++$nwsecs;
150 $nwlist[$nwsecs] = wfEscapeHTMLTagsOnly($q[0]);
151 $stripped .= $unique . $nwsecs . "s";
152 $text = $q[1];
156 if( $wgUseTeX ) {
157 while ( "" != $stripped ) {
158 $p = preg_split( "/<\\s*math\\s*>/i", $stripped, 2 );
159 $stripped2 .= $p[0];
160 if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { $stripped = ""; }
161 else {
162 $q = preg_split( "/<\\/\\s*math\\s*>/i", $p[1], 2 );
163 ++$mathsecs;
164 $mathlist[$mathsecs] = renderMath($q[0]);
165 $stripped2 .= $unique2 . $mathsecs . "s";
166 $stripped = $q[1];
169 } else {
170 $stripped2 = $stripped;
173 while ( "" != $stripped2 ) {
174 $p = preg_split( "/<\\s*pre\\s*>/i", $stripped2, 2 );
175 $stripped3 .= $p[0];
176 if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { $stripped2 = ""; }
177 else {
178 $q = preg_split( "/<\\/\\s*pre\\s*>/i", $p[1], 2 );
179 ++$presecs;
180 $prelist[$presecs] = "<pre>". wfEscapeHTMLTagsOnly($q[0]). "</pre>";
181 $stripped3 .= $unique3 . $presecs . "s";
182 $stripped2 = $q[1];
186 $text = $this->doWikiPass2( $stripped3, $linestart );
188 $specialChars = array("\\", "$");
189 $escapedChars = array("\\\\", "\\$");
190 for ( $i = 1; $i <= $presecs; ++$i ) {
191 $text = preg_replace( "/{$unique3}{$i}s/", str_replace( $specialChars,
192 $escapedChars, $prelist[$i] ), $text );
195 for ( $i = 1; $i <= $mathsecs; ++$i ) {
196 $text = preg_replace( "/{$unique2}{$i}s/", str_replace( $specialChars,
197 $escapedChars, $mathlist[$i] ), $text );
200 for ( $i = 1; $i <= $nwsecs; ++$i ) {
201 $text = preg_replace( "/{$unique}{$i}s/", str_replace( $specialChars,
202 $escapedChars, $nwlist[$i] ), $text );
204 $this->addHTML( $text );
205 wfProfileOut( $fname );
208 function sendCacheControl() {
209 global $wgUseGzip;
210 if( $this->mLastModified != "" ) {
211 wfDebug( "** private caching; {$this->mLastModified} **\n", false );
212 header( "Cache-Control: private, must-revalidate, max-age=0" );
213 header( "Last-modified: {$this->mLastModified}" );
214 if( $wgUseGzip ) {
215 # We should put in Accept-Encoding, but IE chokes on anything but
216 # User-Agent in a Vary: header (at least through 6.0)
217 header( "Vary: User-Agent" );
219 } else {
220 wfDebug( "** no caching **\n", false );
221 header( "Cache-Control: no-cache" ); # Experimental - see below
222 header( "Pragma: no-cache" );
223 header( "Last-modified: " . gmdate( "D, j M Y H:i:s" ) . " GMT" );
225 header( "Expires: Mon, 15 Jan 2001 00:00:00 GMT" ); # Cachers always validate the page!
228 # Finally, all the text has been munged and accumulated into
229 # the object, let's actually output it:
231 function output()
233 global $wgUser, $wgLang, $wgDebugComments, $wgCookieExpiration;
234 global $wgInputEncoding, $wgOutputEncoding, $wgLanguageCode;
235 if( $this->mDoNothing ){
236 return;
238 $fname = "OutputPage::output";
239 wfProfileIn( $fname );
241 $sk = $wgUser->getSkin();
243 $this->sendCacheControl();
245 header( "Content-type: text/html; charset={$wgOutputEncoding}" );
246 header( "Content-language: {$wgLanguageCode}" );
248 if ( "" != $this->mRedirect ) {
249 if( substr( $this->mRedirect, 0, 4 ) != "http" ) {
250 # Standards require redirect URLs to be absolute
251 global $wgServer;
252 $this->mRedirect = $wgServer . $this->mRedirect;
254 header( "Location: {$this->mRedirect}" );
255 return;
258 $exp = time() + $wgCookieExpiration;
259 foreach( $this->mCookies as $name => $val ) {
260 setcookie( $name, $val, $exp, "/" );
263 $sk->outputPage( $this );
264 flush();
267 function out( $ins )
269 global $wgInputEncoding, $wgOutputEncoding, $wgLang;
270 if ( 0 == strcmp( $wgInputEncoding, $wgOutputEncoding ) ) {
271 $outs = $ins;
272 } else {
273 $outs = $wgLang->iconv( $wgInputEncoding, $wgOutputEncoding, $ins );
274 if ( false === $outs ) { $outs = $ins; }
276 print $outs;
279 function setEncodings()
281 global $wgInputEncoding, $wgOutputEncoding;
282 global $wgUser, $wgLang;
284 $wgInputEncoding = strtolower( $wgInputEncoding );
286 if( $wgUser->getOption( 'altencoding' ) ) {
287 $wgLang->setAltEncoding();
288 return;
291 if ( empty( $_SERVER['HTTP_ACCEPT_CHARSET'] ) ) {
292 $wgOutputEncoding = strtolower( $wgOutputEncoding );
293 return;
297 # This code is unused anyway!
298 # Commenting out. --bv 2003-11-15
300 $a = explode( ",", $_SERVER['HTTP_ACCEPT_CHARSET'] );
301 $best = 0.0;
302 $bestset = "*";
304 foreach ( $a as $s ) {
305 if ( preg_match( "/(.*);q=(.*)/", $s, $m ) ) {
306 $set = $m[1];
307 $q = (float)($m[2]);
308 } else {
309 $set = $s;
310 $q = 1.0;
312 if ( $q > $best ) {
313 $bestset = $set;
314 $best = $q;
317 #if ( "*" == $bestset ) { $bestset = "iso-8859-1"; }
318 if ( "*" == $bestset ) { $bestset = $wgOutputEncoding; }
319 $wgOutputEncoding = strtolower( $bestset );
321 # Disable for now
324 $wgOutputEncoding = $wgInputEncoding;
327 # Returns a HTML comment with the elapsed time since request.
328 # This method has no side effects.
329 function reportTime()
331 global $wgRequestTime;
333 list( $usec, $sec ) = explode( " ", microtime() );
334 $now = (float)$sec + (float)$usec;
336 list( $usec, $sec ) = explode( " ", $wgRequestTime );
337 $start = (float)$sec + (float)$usec;
338 $elapsed = $now - $start;
339 $com = sprintf( "<!-- Time since request: %01.2f secs. -->",
340 $elapsed );
341 return $com;
344 # Note: these arguments are keys into wfMsg(), not text!
346 function errorpage( $title, $msg )
348 global $wgTitle;
350 $this->mDebugtext .= "Original title: " .
351 $wgTitle->getPrefixedText() . "\n";
352 $this->setHTMLTitle( wfMsg( "errorpagetitle" ) );
353 $this->setPageTitle( wfMsg( $title ) );
354 $this->setRobotpolicy( "noindex,nofollow" );
355 $this->setArticleFlag( false );
357 $this->mBodytext = "";
358 $this->addHTML( "<p>" . wfMsg( $msg ) . "\n" );
359 $this->returnToMain( false );
361 $this->output();
362 wfAbruptExit();
365 function sysopRequired()
367 global $wgUser;
369 $this->setHTMLTitle( wfMsg( "errorpagetitle" ) );
370 $this->setPageTitle( wfMsg( "sysoptitle" ) );
371 $this->setRobotpolicy( "noindex,nofollow" );
372 $this->setArticleFlag( false );
373 $this->mBodytext = "";
375 $sk = $wgUser->getSkin();
376 $ap = $sk->makeKnownLink( wfMsg( "administrators" ), "" );
377 $this->addHTML( wfMsg( "sysoptext", $ap ) );
378 $this->returnToMain();
381 function developerRequired()
383 global $wgUser;
385 $this->setHTMLTitle( wfMsg( "errorpagetitle" ) );
386 $this->setPageTitle( wfMsg( "developertitle" ) );
387 $this->setRobotpolicy( "noindex,nofollow" );
388 $this->setArticleFlag( false );
389 $this->mBodytext = "";
391 $sk = $wgUser->getSkin();
392 $ap = $sk->makeKnownLink( wfMsg( "administrators" ), "" );
393 $this->addHTML( wfMsg( "developertext", $ap ) );
394 $this->returnToMain();
397 function databaseError( $fname )
399 global $wgUser, $wgCommandLineMode;
401 $this->setPageTitle( wfMsgNoDB( "databaseerror" ) );
402 $this->setRobotpolicy( "noindex,nofollow" );
403 $this->setArticleFlag( false );
405 if ( $wgCommandLineMode ) {
406 $msg = wfMsgNoDB( "dberrortextcl" );
407 } else {
408 $msg = wfMsgNoDB( "dberrortext" );
411 $msg = str_replace( "$1", htmlspecialchars( wfLastDBquery() ), $msg );
412 $msg = str_replace( "$2", htmlspecialchars( $fname ), $msg );
413 $msg = str_replace( "$3", wfLastErrno(), $msg );
414 $msg = str_replace( "$4", htmlspecialchars( wfLastError() ), $msg );
416 if ( $wgCommandLineMode ) {
417 print "$msg\n";
418 wfAbruptExit();
420 $sk = $wgUser->getSkin();
421 $shlink = $sk->makeKnownLink( wfMsgNoDB( "searchhelppage" ),
422 wfMsgNoDB( "searchingwikipedia" ) );
423 $msg = str_replace( "$5", $shlink, $msg );
425 $this->mBodytext = $msg;
426 $this->output();
427 wfAbruptExit();
430 function readOnlyPage( $source = "", $protected = false )
432 global $wgUser, $wgReadOnlyFile;
434 $this->setRobotpolicy( "noindex,nofollow" );
435 $this->setArticleFlag( false );
437 if( $protected ) {
438 $this->setPageTitle( wfMsg( "viewsource" ) );
439 $this->addWikiText( wfMsg( "protectedtext" ) );
440 } else {
441 $this->setPageTitle( wfMsg( "readonly" ) );
442 $reason = file_get_contents( $wgReadOnlyFile );
443 $this->addHTML( wfMsg( "readonlytext", $reason ) );
446 if($source) {
447 $rows = $wgUser->getOption( "rows" );
448 $cols = $wgUser->getOption( "cols" );
449 $text .= "</p>\n<textarea cols='$cols' rows='$rows' readonly>" .
450 htmlspecialchars( $source ) . "\n</textarea>";
451 $this->addHTML( $text );
454 $this->returnToMain( false );
457 function fatalError( $message )
459 $this->setPageTitle( wfMsg( "internalerror" ) );
460 $this->setRobotpolicy( "noindex,nofollow" );
461 $this->setArticleFlag( false );
463 $this->mBodytext = $message;
464 $this->output();
465 wfAbruptExit();
468 function unexpectedValueError( $name, $val )
470 $this->fatalError( wfMsg( "unexpected", $name, $val ) );
473 function fileCopyError( $old, $new )
475 $this->fatalError( wfMsg( "filecopyerror", $old, $new ) );
478 function fileRenameError( $old, $new )
480 $this->fatalError( wfMsg( "filerenameerror", $old, $new ) );
483 function fileDeleteError( $name )
485 $this->fatalError( wfMsg( "filedeleteerror", $name ) );
488 function fileNotFoundError( $name )
490 $this->fatalError( wfMsg( "filenotfound", $name ) );
493 function returnToMain( $auto = true )
495 global $wgUser, $wgOut, $returnto;
497 $sk = $wgUser->getSkin();
498 if ( "" == $returnto ) {
499 $returnto = wfMsg( "mainpage" );
501 $link = $sk->makeKnownLink( $returnto, "" );
503 $r = wfMsg( "returnto", $link );
504 if ( $auto ) {
505 $wgOut->addMeta( "http:Refresh", "10;url=" .
506 wfLocalUrlE( wfUrlencode( $returnto ) ) );
508 $wgOut->addHTML( "\n<p>$r\n" );
512 function categoryMagic ()
514 global $wgTitle , $wgUseCategoryMagic ;
515 if ( !isset ( $wgUseCategoryMagic ) || !$wgUseCategoryMagic ) return ;
516 $id = $wgTitle->getArticleID() ;
517 $cat = ucfirst ( wfMsg ( "category" ) ) ;
518 $ti = $wgTitle->getText() ;
519 $ti = explode ( ":" , $ti , 2 ) ;
520 if ( $cat != $ti[0] ) return "" ;
521 $r = "<br break=all>\n" ;
523 $articles = array() ;
524 $parents = array () ;
525 $children = array() ;
528 global $wgUser ;
529 $sk = $wgUser->getSkin() ;
530 $sql = "SELECT l_from FROM links WHERE l_to={$id}" ;
531 $res = wfQuery ( $sql, DB_READ ) ;
532 while ( $x = wfFetchObject ( $res ) )
534 # $t = new Title ;
535 # $t->newFromDBkey ( $x->l_from ) ;
536 # $t = $t->getText() ;
537 $t = $x->l_from ;
538 $y = explode ( ":" , $t , 2 ) ;
539 if ( count ( $y ) == 2 && $y[0] == $cat ) {
540 array_push ( $children , $sk->makeLink ( $t , $y[1] ) ) ;
541 } else {
542 array_push ( $articles , $sk->makeLink ( $t ) ) ;
545 wfFreeResult ( $res ) ;
547 # Children
548 if ( count ( $children ) > 0 )
550 asort ( $children ) ;
551 $r .= "<h2>".wfMsg("subcategories")."</h2>\n" ;
552 $r .= implode ( ", " , $children ) ;
555 # Articles
556 if ( count ( $articles ) > 0 )
558 asort ( $articles ) ;
559 $h = wfMsg( "category_header", $ti[1] );
560 $r .= "<h2>{$h}</h2>\n" ;
561 $r .= implode ( ", " , $articles ) ;
565 return $r ;
568 function getHTMLattrs ()
570 $htmlattrs = array( # Allowed attributes--no scripting, etc.
571 "title", "align", "lang", "dir", "width", "height",
572 "bgcolor", "clear", /* BR */ "noshade", /* HR */
573 "cite", /* BLOCKQUOTE, Q */ "size", "face", "color",
574 /* FONT */ "type", "start", "value", "compact",
575 /* For various lists, mostly deprecated but safe */
576 "summary", "width", "border", "frame", "rules",
577 "cellspacing", "cellpadding", "valign", "char",
578 "charoff", "colgroup", "col", "span", "abbr", "axis",
579 "headers", "scope", "rowspan", "colspan", /* Tables */
580 "id", "class", "name", "style" /* For CSS */
582 return $htmlattrs ;
585 function fixTableTags ( $t )
587 if ( trim ( $t ) == "" ) return "" ; # Saves runtime ;-)
588 $htmlattrs = $this->getHTMLattrs() ;
590 # Strip non-approved attributes from the tag
591 $t = preg_replace(
592 "/(\\w+)(\\s*=\\s*([^\\s\">]+|\"[^\">]*\"))?/e",
593 "(in_array(strtolower(\"\$1\"),\$htmlattrs)?(\"\$1\".((\"x\$3\" != \"x\")?\"=\$3\":'')):'')",
594 $t);
596 return trim ( $t ) ;
599 function doTableStuff ( $t )
601 $t = explode ( "\n" , $t ) ;
602 $td = array () ; # Is currently a td tag open?
603 $ltd = array () ; # Was it TD or TH?
604 $tr = array () ; # Is currently a tr tag open?
605 $ltr = array () ; # tr attributes
606 foreach ( $t AS $k => $x )
608 $x = rtrim ( $x ) ;
609 $fc = substr ( $x , 0 , 1 ) ;
610 if ( "{|" == substr ( $x , 0 , 2 ) )
612 $t[$k] = "<table " . $this->fixTableTags ( substr ( $x , 3 ) ) . ">" ;
613 array_push ( $td , false ) ;
614 array_push ( $ltd , "" ) ;
615 array_push ( $tr , false ) ;
616 array_push ( $ltr , "" ) ;
618 else if ( count ( $td ) == 0 ) { } # Don't do any of the following
619 else if ( "|}" == substr ( $x , 0 , 2 ) )
621 $z = "</table>\n" ;
622 $l = array_pop ( $ltd ) ;
623 if ( array_pop ( $tr ) ) $z = "</tr>" . $z ;
624 if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
625 array_pop ( $ltr ) ;
626 $t[$k] = $z ;
628 /* else if ( "|_" == substr ( $x , 0 , 2 ) ) # Caption
630 $z = trim ( substr ( $x , 2 ) ) ;
631 $t[$k] = "<caption>{$z}</caption>\n" ;
633 else if ( "|-" == substr ( $x , 0 , 2 ) ) # Allows for |---------------
635 $x = substr ( $x , 1 ) ;
636 while ( $x != "" && substr ( $x , 0 , 1 ) == '-' ) $x = substr ( $x , 1 ) ;
637 $z = "" ;
638 $l = array_pop ( $ltd ) ;
639 if ( array_pop ( $tr ) ) $z = "</tr>" . $z ;
640 if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
641 array_pop ( $ltr ) ;
642 $t[$k] = $z ;
643 array_push ( $tr , false ) ;
644 array_push ( $td , false ) ;
645 array_push ( $ltd , "" ) ;
646 array_push ( $ltr , $this->fixTableTags ( $x ) ) ;
648 else if ( "|" == $fc || "!" == $fc || "|+" == substr ( $x , 0 , 2 ) ) # Caption
650 if ( "|+" == substr ( $x , 0 , 2 ) )
652 $fc = "+" ;
653 $x = substr ( $x , 1 ) ;
655 $after = substr ( $x , 1 ) ;
656 if ( $fc == "!" ) $after = str_replace ( "!!" , "||" , $after ) ;
657 $after = explode ( "||" , $after ) ;
658 $t[$k] = "" ;
659 foreach ( $after AS $theline )
661 $z = "" ;
662 $tra = array_pop ( $ltr ) ;
663 if ( !array_pop ( $tr ) ) $z = "<tr {$tra}>\n" ;
664 array_push ( $tr , true ) ;
665 array_push ( $ltr , "" ) ;
667 $l = array_pop ( $ltd ) ;
668 if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
669 if ( $fc == "|" ) $l = "TD" ;
670 else if ( $fc == "!" ) $l = "TH" ;
671 else if ( $fc == "+" ) $l = "CAPTION" ;
672 else $l = "" ;
673 array_push ( $ltd , $l ) ;
674 $y = explode ( "|" , $theline , 2 ) ;
675 if ( count ( $y ) == 1 ) $y = "{$z}<{$l}>{$y[0]}" ;
676 else $y = $y = "{$z}<{$l} ".$this->fixTableTags($y[0]).">{$y[1]}" ;
677 $t[$k] .= $y ;
678 array_push ( $td , true ) ;
683 # Closing open td, tr && table
684 while ( count ( $td ) > 0 )
686 if ( array_pop ( $td ) ) $t[] = "</td>" ;
687 if ( array_pop ( $tr ) ) $t[] = "</tr>" ;
688 $t[] = "</table>" ;
691 $t = implode ( "\n" , $t ) ;
692 # $t = $this->removeHTMLtags( $t );
693 return $t ;
696 # Well, OK, it's actually about 14 passes. But since all the
697 # hard lifting is done inside PHP's regex code, it probably
698 # wouldn't speed things up much to add a real parser.
700 function doWikiPass2( $text, $linestart )
702 global $wgUser, $wgLang, $wgUseDynamicDates;
703 $fname = "OutputPage::doWikiPass2";
704 wfProfileIn( $fname );
706 $text = $this->removeHTMLtags( $text );
707 $text = $this->replaceVariables( $text );
709 $text = preg_replace( "/(^|\n)-----*/", "\\1<hr>", $text );
710 $text = str_replace ( "<HR>", "<hr>", $text );
712 $text = $this->doAllQuotes( $text );
713 $text = $this->doHeadings( $text );
715 if($wgUseDynamicDates) {
716 global $wgDateFormatter;
717 $text = $wgDateFormatter->reformat( $wgUser->getOption("date"), $text );
720 $text = $this->replaceExternalLinks( $text );
721 $text = $this->replaceInternalLinks ( $text );
723 $text = $this->doTableStuff ( $text ) ;
724 $text = $this->doBlockLevels( $text, $linestart );
726 $text = $this->magicISBN( $text );
727 $text = $this->magicRFC( $text );
728 $text = $this->formatHeadings( $text );
730 $sk = $wgUser->getSkin();
731 $text = $sk->transformContent( $text );
732 $text .= $this->categoryMagic () ;
734 wfProfileOut( $fname );
735 return $text;
738 /* private */ function doAllQuotes( $text )
740 $outtext = "";
741 $lines = explode( "\r\n", $text );
742 foreach ( $lines as $line ) {
743 $outtext .= $this->doQuotes ( "", $line, "" ) . "\r\n";
745 return $outtext;
748 /* private */ function doQuotes( $pre, $text, $mode )
750 if ( preg_match( "/^(.*)''(.*)$/sU", $text, $m ) ) {
751 $m1_strong = ($m[1] == "") ? "" : "<strong>{$m[1]}</strong>";
752 $m1_em = ($m[1] == "") ? "" : "<em>{$m[1]}</em>";
753 if ( substr ($m[2], 0, 1) == "'" ) {
754 $m[2] = substr ($m[2], 1);
755 if ($mode == "em") {
756 return $this->doQuotes ( $m[1], $m[2], ($m[1] == "") ? "both" : "emstrong" );
757 } else if ($mode == "strong") {
758 return $m1_strong . $this->doQuotes ( "", $m[2], "" );
759 } else if (($mode == "emstrong") || ($mode == "both")) {
760 return $this->doQuotes ( "", $pre.$m1_strong.$m[2], "em" );
761 } else if ($mode == "strongem") {
762 return "<strong>{$pre}{$m1_em}</strong>" . $this->doQuotes ( "", $m[2], "em" );
763 } else {
764 return $m[1] . $this->doQuotes ( "", $m[2], "strong" );
766 } else {
767 if ($mode == "strong") {
768 return $this->doQuotes ( $m[1], $m[2], ($m[1] == "") ? "both" : "strongem" );
769 } else if ($mode == "em") {
770 return $m1_em . $this->doQuotes ( "", $m[2], "" );
771 } else if ($mode == "emstrong") {
772 return "<em>{$pre}{$m1_strong}</em>" . $this->doQuotes ( "", $m[2], "strong" );
773 } else if (($mode == "strongem") || ($mode == "both")) {
774 return $this->doQuotes ( "", $pre.$m1_em.$m[2], "strong" );
775 } else {
776 return $m[1] . $this->doQuotes ( "", $m[2], "em" );
779 } else {
780 $text_strong = ($text == "") ? "" : "<strong>{$text}</strong>";
781 $text_em = ($text == "") ? "" : "<em>{$text}</em>";
782 if ($mode == "") {
783 return $pre . $text;
784 } else if ($mode == "em") {
785 return $pre . $text_em;
786 } else if ($mode == "strong") {
787 return $pre . $text_strong;
788 } else if ($mode == "strongem") {
789 return (($pre == "") && ($text == "")) ? "" : "<strong>{$pre}{$text_em}</strong>";
790 } else {
791 return (($pre == "") && ($text == "")) ? "" : "<em>{$pre}{$text_strong}</em>";
796 /* private */ function doHeadings( $text )
798 for ( $i = 6; $i >= 1; --$i ) {
799 $h = substr( "======", 0, $i );
800 $text = preg_replace( "/^{$h}([^=]+){$h}(\\s|$)/m",
801 "<h{$i}>\\1</h{$i}>\\2", $text );
803 return $text;
806 # Note: we have to do external links before the internal ones,
807 # and otherwise take great care in the order of things here, so
808 # that we don't end up interpreting some URLs twice.
810 /* private */ function replaceExternalLinks( $text )
812 $fname = "OutputPage::replaceExternalLinks";
813 wfProfileIn( $fname );
814 $text = $this->subReplaceExternalLinks( $text, "http", true );
815 $text = $this->subReplaceExternalLinks( $text, "https", true );
816 $text = $this->subReplaceExternalLinks( $text, "ftp", false );
817 $text = $this->subReplaceExternalLinks( $text, "gopher", false );
818 $text = $this->subReplaceExternalLinks( $text, "news", false );
819 $text = $this->subReplaceExternalLinks( $text, "mailto", false );
820 wfProfileOut( $fname );
821 return $text;
824 /* private */ function subReplaceExternalLinks( $s, $protocol, $autonumber )
826 global $wgUser, $printable;
827 global $wgAllowExternalImages;
830 $unique = "4jzAfzB8hNvf4sqyO9Edd8pSmk9rE2in0Tgw3";
831 $uc = "A-Za-z0-9_\\/~%\\-+&*#?!=()@\\x80-\\xFF";
833 # this is the list of separators that should be ignored if they
834 # are the last character of an URL but that should be included
835 # if they occur within the URL, e.g. "go to www.foo.com, where .."
836 # in this case, the last comma should not become part of the URL,
837 # but in "www.foo.com/123,2342,32.htm" it should.
838 $sep = ",;\.:";
839 $fnc = "A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF";
840 $images = "gif|png|jpg|jpeg";
842 # PLEASE NOTE: The curly braces { } are not part of the regex,
843 # they are interpreted as part of the string (used to tell PHP
844 # that the content of the string should be inserted there).
845 $e1 = "/(^|[^\\[])({$protocol}:)([{$uc}{$sep}]+)\\/([{$fnc}]+)\\." .
846 "((?i){$images})([^{$uc}]|$)/";
848 $e2 = "/(^|[^\\[])({$protocol}:)(([".$uc."]|[".$sep."][".$uc."])+)([^". $uc . $sep. "]|[".$sep."]|$)/";
849 $sk = $wgUser->getSkin();
851 if ( $autonumber and $wgAllowExternalImages) { # Use img tags only for HTTP urls
852 $s = preg_replace( $e1, "\\1" . $sk->makeImage( "{$unique}:\\3" .
853 "/\\4.\\5", "\\4.\\5" ) . "\\6", $s );
855 $s = preg_replace( $e2, "\\1" . "<a href=\"{$unique}:\\3\"" .
856 $sk->getExternalLinkAttributes( "{$unique}:\\3", wfEscapeHTML(
857 "{$unique}:\\3" ) ) . ">" . wfEscapeHTML( "{$unique}:\\3" ) .
858 "</a>\\5", $s );
859 $s = str_replace( $unique, $protocol, $s );
861 $a = explode( "[{$protocol}:", " " . $s );
862 $s = array_shift( $a );
863 $s = substr( $s, 1 );
865 $e1 = "/^([{$uc}"."{$sep}]+)](.*)\$/sD";
866 $e2 = "/^([{$uc}"."{$sep}]+)\\s+([^\\]]+)](.*)\$/sD";
868 foreach ( $a as $line ) {
869 if ( preg_match( $e1, $line, $m ) ) {
870 $link = "{$protocol}:{$m[1]}";
871 $trail = $m[2];
872 if ( $autonumber ) { $text = "[" . ++$this->mAutonumber . "]"; }
873 else { $text = wfEscapeHTML( $link ); }
874 } else if ( preg_match( $e2, $line, $m ) ) {
875 $link = "{$protocol}:{$m[1]}";
876 $text = $m[2];
877 $trail = $m[3];
878 } else {
879 $s .= "[{$protocol}:" . $line;
880 continue;
882 if ( $printable == "yes") $paren = " (<i>" . htmlspecialchars ( $link ) . "</i>)";
883 else $paren = "";
884 $la = $sk->getExternalLinkAttributes( $link, $text );
885 $s .= "<a href='{$link}'{$la}>{$text}</a>{$paren}{$trail}";
888 return $s;
891 /* private */ function replaceInternalLinks( $s )
893 global $wgTitle, $wgUser, $wgLang;
894 global $wgLinkCache, $wgInterwikiMagic, $wgUseCategoryMagic;
895 global $wgNamespacesWithSubpages, $wgLanguageCode;
896 wfProfileIn( $fname = "OutputPage::replaceInternalLinks" );
898 wfProfileIn( "$fname-setup" );
899 $tc = Title::legalChars() . "#";
900 $sk = $wgUser->getSkin();
902 $a = explode( "[[", " " . $s );
903 $s = array_shift( $a );
904 $s = substr( $s, 1 );
906 $e1 = "/^([{$tc}]+)(?:\\|([^]]+))?]](.*)\$/sD";
908 # Special and Media are pseudo-namespaces; no pages actually exist in them
909 $image = Namespace::getImage();
910 $special = Namespace::getSpecial();
911 $media = Namespace::getMedia();
912 $nottalk = !Namespace::isTalk( $wgTitle->getNamespace() );
913 wfProfileOut( "$fname-setup" );
915 foreach ( $a as $line ) {
916 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
917 $text = $m[2];
918 $trail = $m[3];
919 } else { # Invalid form; output directly
920 $s .= "[[" . $line ;
921 continue;
924 /* Valid link forms:
925 Foobar -- normal
926 :Foobar -- override special treatment of prefix (images, language links)
927 /Foobar -- convert to CurrentPage/Foobar
928 /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
930 $c = substr($m[1],0,1);
931 $noforce = ($c != ":");
932 if( $c == "/" ) { # subpage
933 if(substr($m[1],-1,1)=="/") { # / at end means we don't want the slash to be shown
934 $m[1]=substr($m[1],1,strlen($m[1])-2);
935 $noslash=$m[1];
936 } else {
937 $noslash=substr($m[1],1);
939 if($wgNamespacesWithSubpages[$wgTitle->getNamespace()]) { # subpages allowed here
940 $link = $wgTitle->getPrefixedText(). "/" . trim($noslash);
941 if( "" == $text ) {
942 $text= $m[1];
943 } # this might be changed for ugliness reasons
944 } else {
945 $link = $noslash; # no subpage allowed, use standard link
947 } elseif( $noforce ) { # no subpage
948 $link = $m[1];
949 } else {
950 $link = substr( $m[1], 1 );
952 if( "" == $text )
953 $text = $link;
955 $nt = Title::newFromText( $link );
956 if( !$nt ) {
957 $s .= "[[" . $line;
958 continue;
960 $ns = $nt->getNamespace();
961 $iw = $nt->getInterWiki();
962 if( $noforce ) {
963 if( $iw && $wgInterwikiMagic && $nottalk && $wgLang->getLanguageName( $iw ) ) {
964 array_push( $this->mLanguageLinks, $nt->getPrefixedText() );
965 $s .= $trail;
966 /* CHECK MERGE @@@
967 } else if ( "media" == $pre ) {
968 $nt = Title::newFromText( $suf );
969 $name = $nt->getDBkey();
970 if ( "" == $text ) { $text = $nt->GetText(); }
972 $wgLinkCache->addImageLink( $name );
973 $s .= $sk->makeMediaLink( $name,
974 wfImageUrl( $name ), $text );
975 $s .= $trail;
976 } else if ( isset($wgUseCategoryMagic) && $wgUseCategoryMagic && $pre == wfMsg ( "category" ) ) {
977 $l = $sk->makeLink ( $pre.":".ucfirst( $m[2] ), ucfirst ( $m[2] ) ) ;
978 array_push ( $this->mCategoryLinks , $l ) ;
979 $s .= $trail ;
980 } else {
981 $l = $wgLang->getLanguageName( $pre );
982 if ( "" == $l or !$wgInterwikiMagic or Namespace::isTalk( $wgTitle->getNamespace() ) ) {
983 if ( "" == $text ) {
984 $text = $link;
986 $s .= $sk->makeLink( $link, $text, "", $trail );
987 } else if ( $pre != $wgLanguageCode ) {
988 array_push( $this->mLanguageLinks, "$pre:$suf" );
989 $s .= $trail;
992 continue;
994 if( $ns == $image ) {
995 $s .= $sk->makeImageLinkObj( $nt, $text ) . $trail;
996 $wgLinkCache->addImageLinkObj( $nt );
997 continue;
999 /* CHECK MERGE @@@
1000 # } else if ( 0 == strcmp( "##", substr( $link, 0, 2 ) ) ) {
1001 # $link = substr( $link, 2 );
1002 # $s .= "<a name=\"{$link}\">{$text}</a>{$trail}";
1003 } else {
1004 if ( "" == $text ) { $text = $link; }
1005 # Hotspot:
1006 $s .= $sk->makeLink( $link, $text, "", $trail );
1009 if( $ns == $media ) {
1010 $s .= $sk->makeMediaLinkObj( $nt, $text ) . $trail;
1011 $wgLinkCache->addImageLinkObj( $nt );
1012 continue;
1013 } elseif( $ns == $special ) {
1014 $s .= $sk->makeKnownLinkObj( $nt, $text, "", $trail );
1015 continue;
1017 $s .= $sk->makeLinkObj( $nt, $text, "", $trail );
1019 wfProfileOut( $fname );
1020 return $s;
1023 # Some functions here used by doBlockLevels()
1025 /* private */ function closeParagraph()
1027 $result = "";
1028 if ( 0 != strcmp( "p", $this->mLastSection ) &&
1029 0 != strcmp( "", $this->mLastSection ) ) {
1030 $result = "</" . $this->mLastSection . ">";
1032 $this->mLastSection = "";
1033 return $result;
1035 # getCommon() returns the length of the longest common substring
1036 # of both arguments, starting at the beginning of both.
1038 /* private */ function getCommon( $st1, $st2 )
1040 $fl = strlen( $st1 );
1041 $shorter = strlen( $st2 );
1042 if ( $fl < $shorter ) { $shorter = $fl; }
1044 for ( $i = 0; $i < $shorter; ++$i ) {
1045 if ( $st1{$i} != $st2{$i} ) { break; }
1047 return $i;
1049 # These next three functions open, continue, and close the list
1050 # element appropriate to the prefix character passed into them.
1052 /* private */ function openList( $char )
1054 $result = $this->closeParagraph();
1056 if ( "*" == $char ) { $result .= "<ul><li>"; }
1057 else if ( "#" == $char ) { $result .= "<ol><li>"; }
1058 else if ( ":" == $char ) { $result .= "<dl><dd>"; }
1059 else if ( ";" == $char ) {
1060 $result .= "<dl><dt>";
1061 $this->mDTopen = true;
1063 else { $result = "<!-- ERR 1 -->"; }
1065 return $result;
1068 /* private */ function nextItem( $char )
1070 if ( "*" == $char || "#" == $char ) { return "</li><li>"; }
1071 else if ( ":" == $char || ";" == $char ) {
1072 $close = "</dd>";
1073 if ( $this->mDTopen ) { $close = "</dt>"; }
1074 if ( ";" == $char ) {
1075 $this->mDTopen = true;
1076 return $close . "<dt>";
1077 } else {
1078 $this->mDTopen = false;
1079 return $close . "<dd>";
1082 return "<!-- ERR 2 -->";
1085 /* private */function closeList( $char )
1087 if ( "*" == $char ) { return "</li></ul>"; }
1088 else if ( "#" == $char ) { return "</li></ol>"; }
1089 else if ( ":" == $char ) {
1090 if ( $this->mDTopen ) {
1091 $this->mDTopen = false;
1092 return "</dt></dl>";
1093 } else {
1094 return "</dd></dl>";
1097 return "<!-- ERR 3 -->";
1100 /* private */ function doBlockLevels( $text, $linestart )
1102 $fname = "OutputPage::doBlockLevels";
1103 wfProfileIn( $fname );
1104 # Parsing through the text line by line. The main thing
1105 # happening here is handling of block-level elements p, pre,
1106 # and making lists from lines starting with * # : etc.
1108 $a = explode( "\n", $text );
1109 $text = $lastPref = "";
1110 $this->mDTopen = $inBlockElem = false;
1112 if ( ! $linestart ) { $text .= array_shift( $a ); }
1113 foreach ( $a as $t ) {
1114 if ( "" != $text ) { $text .= "\n"; }
1116 $oLine = $t;
1117 $opl = strlen( $lastPref );
1118 $npl = strspn( $t, "*#:;" );
1119 $pref = substr( $t, 0, $npl );
1120 $pref2 = str_replace( ";", ":", $pref );
1121 $t = substr( $t, $npl );
1123 if ( 0 != $npl && 0 == strcmp( $lastPref, $pref2 ) ) {
1124 $text .= $this->nextItem( substr( $pref, -1 ) );
1126 if ( ";" == substr( $pref, -1 ) ) {
1127 $cpos = strpos( $t, ":" );
1128 if ( ! ( false === $cpos ) ) {
1129 $term = substr( $t, 0, $cpos );
1130 $text .= $term . $this->nextItem( ":" );
1131 $t = substr( $t, $cpos + 1 );
1134 } else if (0 != $npl || 0 != $opl) {
1135 $cpl = $this->getCommon( $pref, $lastPref );
1137 while ( $cpl < $opl ) {
1138 $text .= $this->closeList( $lastPref{$opl-1} );
1139 --$opl;
1141 if ( $npl <= $cpl && $cpl > 0 ) {
1142 $text .= $this->nextItem( $pref{$cpl-1} );
1144 while ( $npl > $cpl ) {
1145 $char = substr( $pref, $cpl, 1 );
1146 $text .= $this->openList( $char );
1148 if ( ";" == $char ) {
1149 $cpos = strpos( $t, ":" );
1150 if ( ! ( false === $cpos ) ) {
1151 $term = substr( $t, 0, $cpos );
1152 $text .= $term . $this->nextItem( ":" );
1153 $t = substr( $t, $cpos + 1 );
1156 ++$cpl;
1158 $lastPref = $pref2;
1160 if ( 0 == $npl ) { # No prefix--go to paragraph mode
1161 if ( preg_match(
1162 "/(<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6)/i", $t ) ) {
1163 $text .= $this->closeParagraph();
1164 $inBlockElem = true;
1166 if ( ! $inBlockElem ) {
1167 if ( " " == $t{0} ) {
1168 $newSection = "pre";
1169 # $t = wfEscapeHTML( $t );
1171 else { $newSection = "p"; }
1173 if ( 0 == strcmp( "", trim( $oLine ) ) ) {
1174 $text .= $this->closeParagraph();
1175 $text .= "<" . $newSection . ">";
1176 } else if ( 0 != strcmp( $this->mLastSection,
1177 $newSection ) ) {
1178 $text .= $this->closeParagraph();
1179 if ( 0 != strcmp( "p", $newSection ) ) {
1180 $text .= "<" . $newSection . ">";
1183 $this->mLastSection = $newSection;
1185 if ( $inBlockElem &&
1186 preg_match( "/(<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6)/i", $t ) ) {
1187 $inBlockElem = false;
1190 $text .= $t;
1192 while ( $npl ) {
1193 $text .= $this->closeList( $pref2{$npl-1} );
1194 --$npl;
1196 if ( "" != $this->mLastSection ) {
1197 if ( "p" != $this->mLastSection ) {
1198 $text .= "</" . $this->mLastSection . ">";
1200 $this->mLastSection = "";
1202 wfProfileOut( $fname );
1203 return $text;
1206 /* private */ function replaceVariables( $text )
1208 global $wgLang;
1209 $fname = "OutputPage::replaceVariables";
1210 wfProfileIn( $fname );
1213 # Basic variables
1214 # See Language.php for the definition of each magic word
1216 # As with sigs, this uses the server's local time -- ensure
1217 # this is appropriate for your audience!
1218 $v = date( "m" );
1219 $mw =& MagicWord::get( MAG_CURRENTMONTH );
1220 $text = $mw->replace( $v, $text );
1222 $v = $wgLang->getMonthName( date( "n" ) );
1223 $mw =& MagicWord::get( MAG_CURRENTMONTHNAME );
1224 $text = $mw->replace( $v, $text );
1226 $v = $wgLang->getMonthNameGen( date( "n" ) );
1227 $mw =& MagicWord::get( MAG_CURRENTMONTHNAMEGEN );
1228 $text = $mw->replace( $v, $text );
1230 $v = date( "j" );
1231 $mw = MagicWord::get( MAG_CURRENTDAY );
1232 $text = $mw->replace( $v, $text );
1234 $v = $wgLang->getWeekdayName( date( "w" )+1 );
1235 $mw =& MagicWord::get( MAG_CURRENTDAYNAME );
1236 $text = $mw->replace( $v, $text );
1238 $v = date( "Y" );
1239 $mw =& MagicWord::get( MAG_CURRENTYEAR );
1240 $text = $mw->replace( $v, $text );
1242 $v = $wgLang->time( wfTimestampNow(), false );
1243 $mw =& MagicWord::get( MAG_CURRENTTIME );
1244 $text = $mw->replace( $v, $text );
1246 $mw =& MagicWord::get( MAG_NUMBEROFARTICLES );
1247 if ( $mw->match( $text ) ) {
1248 $v = wfNumberOfArticles();
1249 $text = $mw->replace( $v, $text );
1252 # "Variables" with an additional parameter e.g. {{MSG:wikipedia}}
1253 # The callbacks are at the bottom of this file
1254 $mw =& MagicWord::get( MAG_MSG );
1255 $text = $mw->substituteCallback( $text, "wfReplaceMsgVar" );
1257 $mw =& MagicWord::get( MAG_MSGNW );
1258 $text = $mw->substituteCallback( $text, "wfReplaceMsgnwVar" );
1260 wfProfileOut( $fname );
1261 return $text;
1264 # Cleans up HTML, removes dangerous tags and attributes
1265 /* private */ function removeHTMLtags( $text )
1267 $fname = "OutputPage::removeHTMLtags";
1268 wfProfileIn( $fname );
1269 $htmlpairs = array( # Tags that must be closed
1270 "b", "i", "u", "font", "big", "small", "sub", "sup", "h1",
1271 "h2", "h3", "h4", "h5", "h6", "cite", "code", "em", "s",
1272 "strike", "strong", "tt", "var", "div", "center",
1273 "blockquote", "ol", "ul", "dl", "table", "caption", "pre",
1274 "ruby", "rt" , "rb" , "rp"
1276 $htmlsingle = array(
1277 "br", "p", "hr", "li", "dt", "dd"
1279 $htmlnest = array( # Tags that can be nested--??
1280 "table", "tr", "td", "th", "div", "blockquote", "ol", "ul",
1281 "dl", "font", "big", "small", "sub", "sup"
1283 $tabletags = array( # Can only appear inside table
1284 "td", "th", "tr"
1287 $htmlsingle = array_merge( $tabletags, $htmlsingle );
1288 $htmlelements = array_merge( $htmlsingle, $htmlpairs );
1290 $htmlattrs = $this->getHTMLattrs () ;
1292 # Remove HTML comments
1293 $text = preg_replace( "/<!--.*-->/sU", "", $text );
1295 $bits = explode( "<", $text );
1296 $text = array_shift( $bits );
1297 $tagstack = array(); $tablestack = array();
1299 foreach ( $bits as $x ) {
1300 $prev = error_reporting( E_ALL & ~( E_NOTICE | E_WARNING ) );
1301 preg_match( "/^(\\/?)(\\w+)([^>]*)(\\/{0,1}>)([^<]*)$/",
1302 $x, $regs );
1303 list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs;
1304 error_reporting( $prev );
1306 $badtag = 0 ;
1307 if ( in_array( $t = strtolower( $t ), $htmlelements ) ) {
1308 # Check our stack
1309 if ( $slash ) {
1310 # Closing a tag...
1311 if ( ! in_array( $t, $htmlsingle ) &&
1312 ( $ot = array_pop( $tagstack ) ) != $t ) {
1313 array_push( $tagstack, $ot );
1314 $badtag = 1;
1315 } else {
1316 if ( $t == "table" ) {
1317 $tagstack = array_pop( $tablestack );
1319 $newparams = "";
1321 } else {
1322 # Keep track for later
1323 if ( in_array( $t, $tabletags ) &&
1324 ! in_array( "table", $tagstack ) ) {
1325 $badtag = 1;
1326 } else if ( in_array( $t, $tagstack ) &&
1327 ! in_array ( $t , $htmlnest ) ) {
1328 $badtag = 1 ;
1329 } else if ( ! in_array( $t, $htmlsingle ) ) {
1330 if ( $t == "table" ) {
1331 array_push( $tablestack, $tagstack );
1332 $tagstack = array();
1334 array_push( $tagstack, $t );
1336 # Strip non-approved attributes from the tag
1337 $newparams = preg_replace(
1338 "/(\\w+)(\\s*=\\s*([^\\s\">]+|\"[^\">]*\"))?/e",
1339 "(in_array(strtolower(\"\$1\"),\$htmlattrs)?(\"\$1\".((\"x\$3\" != \"x\")?\"=\$3\":'')):'')",
1340 $params);
1342 if ( ! $badtag ) {
1343 $rest = str_replace( ">", "&gt;", $rest );
1344 $text .= "<$slash$t$newparams$brace$rest";
1345 continue;
1348 $text .= "&lt;" . str_replace( ">", "&gt;", $x);
1350 # Close off any remaining tags
1351 while ( $t = array_pop( $tagstack ) ) {
1352 $text .= "</$t>\n";
1353 if ( $t == "table" ) { $tagstack = array_pop( $tablestack ); }
1355 wfProfileOut( $fname );
1356 return $text;
1362 * This function accomplishes several tasks:
1363 * 1) Auto-number headings if that option is enabled
1364 * 2) Add an [edit] link to sections for logged in users who have enabled the option
1365 * 3) Add a Table of contents on the top for users who have enabled the option
1366 * 4) Auto-anchor headings
1368 * It loops through all headlines, collects the necessary data, then splits up the
1369 * string and re-inserts the newly formatted headlines.
1371 * */
1372 /* private */ function formatHeadings( $text )
1374 global $wgUser,$wgArticle,$wgTitle,$wpPreview;
1375 $nh=$wgUser->getOption( "numberheadings" );
1376 $st=$wgUser->getOption( "showtoc" );
1377 if(!$wgTitle->userCanEdit()) {
1378 $es=0;
1379 $esr=0;
1380 } else {
1381 $es=$wgUser->getID() && $wgUser->getOption( "editsection" );
1382 $esr=$wgUser->getID() && $wgUser->getOption( "editsectiononrightclick" );
1385 # Inhibit editsection links if requested in the page
1386 if ($es) {
1387 $esw=& MagicWord::get(MAG_NOEDITSECTION);
1388 if ($esw->matchAndRemove( $text )) {
1389 $es=0;
1392 # if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
1393 # do not add TOC
1394 $mw =& MagicWord::get( MAG_NOTOC );
1395 if ($mw->matchAndRemove( $text ))
1397 $st = 0;
1400 # never add the TOC to the Main Page. This is an entry page that should not
1401 # be more than 1-2 screens large anyway
1402 if($wgTitle->getPrefixedText()==wfMsg("mainpage")) {$st=0;}
1404 # We need this to perform operations on the HTML
1405 $sk=$wgUser->getSkin();
1407 # Get all headlines for numbering them and adding funky stuff like [edit]
1408 # links
1409 preg_match_all("/<H([1-6])(.*?>)(.*?)<\/H[1-6]>/i",$text,$matches);
1411 # headline counter
1412 $c=0;
1414 # Ugh .. the TOC should have neat indentation levels which can be
1415 # passed to the skin functions. These are determined here
1416 foreach($matches[3] as $headline) {
1417 if($level) { $prevlevel=$level;}
1418 $level=$matches[1][$c];
1419 if(($nh||$st) && $prevlevel && $level>$prevlevel) {
1421 $h[$level]=0; // reset when we enter a new level
1422 $toc.=$sk->tocIndent($level-$prevlevel);
1423 $toclevel+=$level-$prevlevel;
1426 if(($nh||$st) && $level<$prevlevel) {
1427 $h[$level+1]=0; // reset when we step back a level
1428 $toc.=$sk->tocUnindent($prevlevel-$level);
1429 $toclevel-=$prevlevel-$level;
1432 $h[$level]++; // count number of headlines for each level
1434 if($nh||$st) {
1435 for($i=1;$i<=$level;$i++) {
1436 if($h[$i]) {
1437 if($dot) {$numbering.=".";}
1438 $numbering.=$h[$i];
1439 $dot=1;
1444 // The canonized header is a version of the header text safe to use for links
1446 $canonized_headline=preg_replace("/<.*?>/","",$headline); // strip out HTML
1447 $tocline = trim( $canonized_headline );
1448 $canonized_headline=str_replace('"',"",$canonized_headline);
1449 $canonized_headline=str_replace(" ","_",trim($canonized_headline));
1450 $refer[$c]=$canonized_headline;
1451 $refers[$canonized_headline]++; // count how many in assoc. array so we can track dupes in anchors
1452 $refcount[$c]=$refers[$canonized_headline];
1454 // Prepend the number to the heading text
1456 if($nh||$st) {
1457 $tocline=$numbering ." ". $tocline;
1459 // Don't number the heading if it is the only one (looks silly)
1460 if($nh && count($matches[3]) > 1) {
1461 $headline=$numbering . " " . $headline; // the two are different if the line contains a link
1465 // Create the anchor for linking from the TOC to the section
1467 $anchor=$canonized_headline;
1468 if($refcount[$c]>1) {$anchor.="_".$refcount[$c];}
1469 if($st) {
1470 $toc.=$sk->tocLine($anchor,$tocline,$toclevel);
1472 if($es && !isset($wpPreview)) {
1473 $head[$c].=$sk->editSectionLink($c+1);
1476 // Put it all together
1478 $head[$c].="<h".$level.$matches[2][$c]
1479 ."<a name=\"".$anchor."\">"
1480 .$headline
1481 ."</a>"
1482 ."</h".$level.">";
1484 // Add the edit section link
1486 if($esr && !isset($wpPreview)) {
1487 $head[$c]=$sk->editSectionScript($c+1,$head[$c]);
1490 $numbering="";
1491 $c++;
1492 $dot=0;
1495 if($st) {
1496 $toclines=$c;
1497 $toc.=$sk->tocUnindent($toclevel);
1498 $toc=$sk->tocTable($toc);
1501 // split up and insert constructed headlines
1503 $blocks=preg_split("/<H[1-6].*?>.*?<\/H[1-6]>/i",$text);
1504 $i=0;
1506 foreach($blocks as $block) {
1507 if(($es) && !isset($wpPreview) && $c>0 && $i==0) {
1508 # This is the [edit] link that appears for the top block of text when
1509 # section editing is enabled
1510 $full.=$sk->editSectionLink(0);
1512 $full.=$block;
1513 if($st && $toclines>3 && !$i) {
1514 # Let's add a top anchor just in case we want to link to the top of the page
1515 $full="<a name=\"top\"></a>".$full.$toc;
1518 $full.=$head[$i];
1519 $i++;
1522 return $full;
1525 /* private */ function magicISBN( $text )
1527 global $wgLang;
1529 $a = split( "ISBN ", " $text" );
1530 if ( count ( $a ) < 2 ) return $text;
1531 $text = substr( array_shift( $a ), 1);
1532 $valid = "0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1534 foreach ( $a as $x ) {
1535 $isbn = $blank = "" ;
1536 while ( " " == $x{0} ) {
1537 $blank .= " ";
1538 $x = substr( $x, 1 );
1540 while ( strstr( $valid, $x{0} ) != false ) {
1541 $isbn .= $x{0};
1542 $x = substr( $x, 1 );
1544 $num = str_replace( "-", "", $isbn );
1545 $num = str_replace( " ", "", $num );
1547 if ( "" == $num ) {
1548 $text .= "ISBN $blank$x";
1549 } else {
1550 $text .= "<a href=\"" . wfLocalUrlE( $wgLang->specialPage(
1551 "Booksources"), "isbn={$num}" ) . "\" class=\"internal\">ISBN $isbn</a>";
1552 $text .= $x;
1555 return $text;
1558 /* private */ function magicRFC( $text )
1560 return $text;
1563 /* private */ function headElement()
1565 global $wgDocType, $wgDTD, $wgUser, $wgLanguageCode, $wgOutputEncoding, $wgLang;
1567 $ret = "<!DOCTYPE HTML PUBLIC \"$wgDocType\"\n \"$wgDTD\">\n";
1569 if ( "" == $this->mHTMLtitle ) {
1570 $this->mHTMLtitle = $this->mPagetitle;
1572 $rtl = $wgLang->isRTL() ? " dir='RTL'" : "";
1573 $ret .= "<html lang=\"$wgLanguageCode\"$rtl><head><title>{$this->mHTMLtitle}</title>\n";
1574 array_push( $this->mMetatags, array( "http:Content-type", "text/html; charset={$wgOutputEncoding}" ) );
1575 foreach ( $this->mMetatags as $tag ) {
1576 if ( 0 == strcasecmp( "http:", substr( $tag[0], 0, 5 ) ) ) {
1577 $a = "http-equiv";
1578 $tag[0] = substr( $tag[0], 5 );
1579 } else {
1580 $a = "name";
1582 $ret .= "<meta $a=\"{$tag[0]}\" content=\"{$tag[1]}\">\n";
1584 $p = $this->mRobotpolicy;
1585 if ( "" == $p ) { $p = "index,follow"; }
1586 $ret .= "<meta name=\"robots\" content=\"$p\">\n";
1588 if ( count( $this->mKeywords ) > 0 ) {
1589 $ret .= "<meta name=\"keywords\" content=\"" .
1590 implode( ",", $this->mKeywords ) . "\">\n";
1592 foreach ( $this->mLinktags as $tag ) {
1593 $ret .= "<link ";
1594 if ( "" != $tag[0] ) { $ret .= "rel=\"{$tag[0]}\" "; }
1595 if ( "" != $tag[1] ) { $ret .= "rev=\"{$tag[1]}\" "; }
1596 $ret .= "href=\"{$tag[2]}\">\n";
1598 $sk = $wgUser->getSkin();
1599 $ret .= $sk->getHeadScripts();
1600 $ret .= $sk->getUserStyles();
1602 $ret .= "</head>\n";
1603 return $ret;
1607 # Regex callbacks, used in OutputPage::replaceVariables
1609 # Just get rid of the dangerous stuff
1610 # Necessary because replaceVariables is called after removeHTMLtags,
1611 # and message text can come from any user
1612 function wfReplaceMsgVar( $matches ) {
1613 global $wgOut;
1614 $text = $wgOut->removeHTMLtags( wfMsg( $matches[1] ) );
1615 return $text;
1618 # Effective <nowiki></nowiki>
1619 # Not real <nowiki> because this is called after nowiki sections are processed
1620 function wfReplaceMsgnwVar( $matches ) {
1621 $text = wfEscapeWikiText( wfMsg( $matches[1] ) );
1622 return $text;