Removed warning for invalid links since it scares users needlessly.
[mediawiki.git] / includes / OutputPage.php
blob307148a389d461976b1e32cd16dca7987a90cfad
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;
17 var $mContainsOldMagic, $mContainsNewMagic;
19 function OutputPage()
21 $this->mHeaders = $this->mCookies = $this->mMetatags =
22 $this->mKeywords = $this->mLinktags = array();
23 $this->mHTMLtitle = $this->mPagetitle = $this->mBodytext =
24 $this->mLastSection = $this->mRedirect = $this->mLastModified =
25 $this->mSubtitle = $this->mDebugtext = $this->mRobotpolicy =
26 $this->mOnloadHandler = "";
27 $this->mIsarticle = $this->mPrintable = true;
28 $this->mSupressQuickbar = $this->mDTopen = $this->mPrintable = false;
29 $this->mLanguageLinks = array();
30 $this->mCategoryLinks = array() ;
31 $this->mAutonumber = 0;
32 $this->mDoNothing = false;
33 $this->mContainsOldMagic = $this->mContainsNewMagic = 0;
36 function addHeader( $name, $val ) { array_push( $this->mHeaders, "$name: $val" ) ; }
37 function addCookie( $name, $val ) { array_push( $this->mCookies, array( $name, $val ) ); }
38 function redirect( $url ) { $this->mRedirect = $url; }
40 # To add an http-equiv meta tag, precede the name with "http:"
41 function addMeta( $name, $val ) { array_push( $this->mMetatags, array( $name, $val ) ); }
42 function addKeyword( $text ) { array_push( $this->mKeywords, $text ); }
43 function addLink( $rel, $rev, $target ) { array_push( $this->mLinktags, array( $rel, $rev, $target ) ); }
45 # checkLastModified tells the client to use the client-cached page if
46 # possible. If sucessful, the OutputPage is disabled so that
47 # any future call to OutputPage->output() have no effect. The method
48 # returns true iff cache-ok headers was sent.
49 function checkLastModified ( $timestamp )
51 global $wgLang, $wgCachePages, $wgUser;
52 if( !$wgCachePages ) {
53 wfDebug( "CACHE DISABLED\n", false );
54 return;
56 if( preg_match( '/MSIE ([1-4]|5\.0)/', $_SERVER["HTTP_USER_AGENT"] ) ) {
57 # IE 5.0 has probs with our caching
58 wfDebug( "-- bad client, not caching\n", false );
59 return;
61 if( $wgUser->getOption( "nocache" ) ) {
62 wfDebug( "USER DISABLED CACHE\n", false );
63 return;
66 $lastmod = gmdate( "D, j M Y H:i:s", wfTimestamp2Unix(
67 max( $timestamp, $wgUser->mTouched ) ) ) . " GMT";
69 if( !empty( $_SERVER["HTTP_IF_MODIFIED_SINCE"] ) ) {
70 # IE sends sizes after the date like this:
71 # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
72 # this breaks strtotime().
73 $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] );
74 $ismodsince = wfUnix2Timestamp( strtotime( $modsince ) );
75 wfDebug( "-- client send If-Modified-Since: " . $modsince . "\n", false );
76 wfDebug( "-- we might send Last-Modified : $lastmod\n", false );
78 if( ($ismodsince >= $timestamp ) and $wgUser->validateCache( $ismodsince ) ) {
79 # Make sure you're in a place you can leave when you call us!
80 header( "HTTP/1.0 304 Not Modified" );
81 header( "Expires: Mon, 15 Jan 2001 00:00:00 GMT" ); # Cachers always validate the page!
82 header( "Cache-Control: private, must-revalidate, max-age=0" );
83 header( "Last-Modified: {$lastmod}" );
84 wfDebug( "CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp\n", false );
85 $this->disable();
86 return true;
87 } else {
88 wfDebug( "READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp\n", false );
89 $this->mLastModified = $lastmod;
91 } else {
92 wfDebug( "We're confused.\n", false );
93 $this->mLastModified = $lastmod;
97 function setRobotpolicy( $str ) { $this->mRobotpolicy = $str; }
98 function setHTMLtitle( $name ) { $this->mHTMLtitle = $name; }
99 function setPageTitle( $name ) { $this->mPagetitle = $name; }
100 function getPageTitle() { return $this->mPagetitle; }
101 function setSubtitle( $str ) { $this->mSubtitle = $str; }
102 function getSubtitle() { return $this->mSubtitle; }
103 function setArticleFlag( $v ) { $this->mIsarticle = $v; }
104 function isArticle() { return $this->mIsarticle; }
105 function setPrintable() { $this->mPrintable = true; }
106 function isPrintable() { return $this->mPrintable; }
107 function setOnloadHandler( $js ) { $this->mOnloadHandler = $js; }
108 function getOnloadHandler() { return $this->mOnloadHandler; }
109 function disable() { $this->mDoNothing = true; }
111 function getLanguageLinks() {
112 global $wgTitle, $wgLanguageCode;
113 global $wgDBconnection, $wgDBname;
114 return $this->mLanguageLinks;
116 function supressQuickbar() { $this->mSupressQuickbar = true; }
117 function isQuickbarSupressed() { return $this->mSupressQuickbar; }
119 function addHTML( $text ) { $this->mBodytext .= $text; }
120 function addHeadtext( $text ) { $this->mHeadtext .= $text; }
121 function debug( $text ) { $this->mDebugtext .= $text; }
123 # First pass--just handle <nowiki> sections, pass the rest off
124 # to doWikiPass2() which does all the real work.
126 function addWikiText( $text, $linestart = true )
128 global $wgUseTeX, $wgArticle, $wgUser, $action;
129 $fname = "OutputPage::addWikiText";
130 wfProfileIn( $fname );
131 $unique = "3iyZiyA7iMwg5rhxP0Dcc9oTnj8qD1jm1Sfv4";
132 $unique2 = "4LIQ9nXtiYFPCSfitVwDw7EYwQlL4GeeQ7qSO";
133 $unique3 = "fPaA8gDfdLBqzj68Yjg9Hil3qEF8JGO0uszIp";
134 $nwlist = array();
135 $nwsecs = 0;
136 $mathlist = array();
137 $mathsecs = 0;
138 $prelist = array ();
139 $presecs = 0;
140 $stripped = "";
141 $stripped2 = "";
142 $stripped3 = "";
144 # Replace any instances of the placeholders
145 $text = str_replace( $unique, wfHtmlEscapeFirst( $unique ), $text );
146 $text = str_replace( $unique2, wfHtmlEscapeFirst( $unique2 ), $text );
147 $text = str_replace( $unique3, wfHtmlEscapeFirst( $unique3 ), $text );
149 global $wgEnableParserCache;
150 $use_parser_cache =
151 $wgEnableParserCache && $action == "view" &&
152 intval($wgUser->getOption( "stubthreshold" )) == 0 &&
153 isset($wgArticle) && $wgArticle->getID() > 0;
155 if( $use_parser_cache ){
156 if( $this->fillFromParserCache() ){
157 wfProfileOut( $fname );
158 return;
162 while ( "" != $text ) {
163 $p = preg_split( "/<\\s*nowiki\\s*>/i", $text, 2 );
164 $stripped .= $p[0];
165 if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { $text = ""; }
166 else {
167 $q = preg_split( "/<\\/\\s*nowiki\\s*>/i", $p[1], 2 );
168 ++$nwsecs;
169 $nwlist[$nwsecs] = wfEscapeHTMLTagsOnly($q[0]);
170 $stripped .= $unique . $nwsecs . "s";
171 $text = $q[1];
175 if( $wgUseTeX ) {
176 while ( "" != $stripped ) {
177 $p = preg_split( "/<\\s*math\\s*>/i", $stripped, 2 );
178 $stripped2 .= $p[0];
179 if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { $stripped = ""; }
180 else {
181 $q = preg_split( "/<\\/\\s*math\\s*>/i", $p[1], 2 );
182 ++$mathsecs;
183 $mathlist[$mathsecs] = renderMath($q[0]);
184 $stripped2 .= $unique2 . $mathsecs . "s";
185 $stripped = $q[1];
188 } else {
189 $stripped2 = $stripped;
192 while ( "" != $stripped2 ) {
193 $p = preg_split( "/<\\s*pre\\s*>/i", $stripped2, 2 );
194 $stripped3 .= $p[0];
195 if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { $stripped2 = ""; }
196 else {
197 $q = preg_split( "/<\\/\\s*pre\\s*>/i", $p[1], 2 );
198 ++$presecs;
199 $prelist[$presecs] = "<pre>". wfEscapeHTMLTagsOnly($q[0]). "</pre>\n";
200 $stripped3 .= $unique3 . $presecs . "s";
201 $stripped2 = $q[1];
205 $text = $this->doWikiPass2( $stripped3, $linestart );
207 $specialChars = array("\\", "$");
208 $escapedChars = array("\\\\", "\\$");
209 for ( $i = 1; $i <= $presecs; ++$i ) {
210 $text = preg_replace( "/{$unique3}{$i}s/", str_replace( $specialChars,
211 $escapedChars, $prelist[$i] ), $text );
214 for ( $i = 1; $i <= $mathsecs; ++$i ) {
215 $text = preg_replace( "/{$unique2}{$i}s/", str_replace( $specialChars,
216 $escapedChars, $mathlist[$i] ), $text );
219 for ( $i = 1; $i <= $nwsecs; ++$i ) {
220 $text = preg_replace( "/{$unique}{$i}s/", str_replace( $specialChars,
221 $escapedChars, $nwlist[$i] ), $text );
223 $this->addHTML( $text );
225 if($use_parser_cache ){
226 $this->saveParserCache( $text );
228 wfProfileOut( $fname );
231 function sendCacheControl() {
232 global $wgUseGzip;
233 if( $this->mLastModified != "" ) {
234 wfDebug( "** private caching; {$this->mLastModified} **\n", false );
235 header( "Cache-Control: private, must-revalidate, max-age=0" );
236 header( "Last-modified: {$this->mLastModified}" );
237 if( $wgUseGzip ) {
238 # We should put in Accept-Encoding, but IE chokes on anything but
239 # User-Agent in a Vary: header (at least through 6.0)
240 header( "Vary: User-Agent" );
242 } else {
243 wfDebug( "** no caching **\n", false );
244 header( "Cache-Control: no-cache" ); # Experimental - see below
245 header( "Pragma: no-cache" );
246 header( "Last-modified: " . gmdate( "D, j M Y H:i:s" ) . " GMT" );
248 header( "Expires: Mon, 15 Jan 2001 00:00:00 GMT" ); # Cachers always validate the page!
251 # Finally, all the text has been munged and accumulated into
252 # the object, let's actually output it:
254 function output()
256 global $wgUser, $wgLang, $wgDebugComments, $wgCookieExpiration;
257 global $wgInputEncoding, $wgOutputEncoding, $wgLanguageCode;
258 if( $this->mDoNothing ){
259 return;
261 $fname = "OutputPage::output";
262 wfProfileIn( $fname );
264 $sk = $wgUser->getSkin();
266 $this->sendCacheControl();
268 header( "Content-type: text/html; charset={$wgOutputEncoding}" );
269 header( "Content-language: {$wgLanguageCode}" );
271 if ( "" != $this->mRedirect ) {
272 if( substr( $this->mRedirect, 0, 4 ) != "http" ) {
273 # Standards require redirect URLs to be absolute
274 global $wgServer;
275 $this->mRedirect = $wgServer . $this->mRedirect;
277 header( "Location: {$this->mRedirect}" );
278 return;
281 $exp = time() + $wgCookieExpiration;
282 foreach( $this->mCookies as $name => $val ) {
283 setcookie( $name, $val, $exp, "/" );
286 $sk->outputPage( $this );
287 # flush();
290 function out( $ins )
292 global $wgInputEncoding, $wgOutputEncoding, $wgLang;
293 if ( 0 == strcmp( $wgInputEncoding, $wgOutputEncoding ) ) {
294 $outs = $ins;
295 } else {
296 $outs = $wgLang->iconv( $wgInputEncoding, $wgOutputEncoding, $ins );
297 if ( false === $outs ) { $outs = $ins; }
299 print $outs;
302 function setEncodings()
304 global $wgInputEncoding, $wgOutputEncoding;
305 global $wgUser, $wgLang;
307 $wgInputEncoding = strtolower( $wgInputEncoding );
309 if( $wgUser->getOption( 'altencoding' ) ) {
310 $wgLang->setAltEncoding();
311 return;
314 if ( empty( $_SERVER['HTTP_ACCEPT_CHARSET'] ) ) {
315 $wgOutputEncoding = strtolower( $wgOutputEncoding );
316 return;
320 # This code is unused anyway!
321 # Commenting out. --bv 2003-11-15
323 $a = explode( ",", $_SERVER['HTTP_ACCEPT_CHARSET'] );
324 $best = 0.0;
325 $bestset = "*";
327 foreach ( $a as $s ) {
328 if ( preg_match( "/(.*);q=(.*)/", $s, $m ) ) {
329 $set = $m[1];
330 $q = (float)($m[2]);
331 } else {
332 $set = $s;
333 $q = 1.0;
335 if ( $q > $best ) {
336 $bestset = $set;
337 $best = $q;
340 #if ( "*" == $bestset ) { $bestset = "iso-8859-1"; }
341 if ( "*" == $bestset ) { $bestset = $wgOutputEncoding; }
342 $wgOutputEncoding = strtolower( $bestset );
344 # Disable for now
347 $wgOutputEncoding = $wgInputEncoding;
350 # Returns a HTML comment with the elapsed time since request.
351 # This method has no side effects.
352 function reportTime()
354 global $wgRequestTime;
356 list( $usec, $sec ) = explode( " ", microtime() );
357 $now = (float)$sec + (float)$usec;
359 list( $usec, $sec ) = explode( " ", $wgRequestTime );
360 $start = (float)$sec + (float)$usec;
361 $elapsed = $now - $start;
362 $com = sprintf( "<!-- Time since request: %01.2f secs. -->",
363 $elapsed );
364 return $com;
367 # Note: these arguments are keys into wfMsg(), not text!
369 function errorpage( $title, $msg )
371 global $wgTitle;
373 $this->mDebugtext .= "Original title: " .
374 $wgTitle->getPrefixedText() . "\n";
375 $this->setHTMLTitle( wfMsg( "errorpagetitle" ) );
376 $this->setPageTitle( wfMsg( $title ) );
377 $this->setRobotpolicy( "noindex,nofollow" );
378 $this->setArticleFlag( false );
380 $this->mBodytext = "";
381 $this->addHTML( "<p>" . wfMsg( $msg ) . "\n" );
382 $this->returnToMain( false );
384 $this->output();
385 wfAbruptExit();
388 function sysopRequired()
390 global $wgUser;
392 $this->setHTMLTitle( wfMsg( "errorpagetitle" ) );
393 $this->setPageTitle( wfMsg( "sysoptitle" ) );
394 $this->setRobotpolicy( "noindex,nofollow" );
395 $this->setArticleFlag( false );
396 $this->mBodytext = "";
398 $sk = $wgUser->getSkin();
399 $ap = $sk->makeKnownLink( wfMsg( "administrators" ), "" );
400 $this->addHTML( wfMsg( "sysoptext", $ap ) );
401 $this->returnToMain();
404 function developerRequired()
406 global $wgUser;
408 $this->setHTMLTitle( wfMsg( "errorpagetitle" ) );
409 $this->setPageTitle( wfMsg( "developertitle" ) );
410 $this->setRobotpolicy( "noindex,nofollow" );
411 $this->setArticleFlag( false );
412 $this->mBodytext = "";
414 $sk = $wgUser->getSkin();
415 $ap = $sk->makeKnownLink( wfMsg( "administrators" ), "" );
416 $this->addHTML( wfMsg( "developertext", $ap ) );
417 $this->returnToMain();
420 function databaseError( $fname )
422 global $wgUser, $wgCommandLineMode;
424 $this->setPageTitle( wfMsgNoDB( "databaseerror" ) );
425 $this->setRobotpolicy( "noindex,nofollow" );
426 $this->setArticleFlag( false );
428 if ( $wgCommandLineMode ) {
429 $msg = wfMsgNoDB( "dberrortextcl" );
430 } else {
431 $msg = wfMsgNoDB( "dberrortext" );
434 $msg = str_replace( "$1", htmlspecialchars( wfLastDBquery() ), $msg );
435 $msg = str_replace( "$2", htmlspecialchars( $fname ), $msg );
436 $msg = str_replace( "$3", wfLastErrno(), $msg );
437 $msg = str_replace( "$4", htmlspecialchars( wfLastError() ), $msg );
439 if ( $wgCommandLineMode ) {
440 print "$msg\n";
441 wfAbruptExit();
443 $sk = $wgUser->getSkin();
444 $shlink = $sk->makeKnownLink( wfMsgNoDB( "searchhelppage" ),
445 wfMsgNoDB( "searchingwikipedia" ) );
446 $msg = str_replace( "$5", $shlink, $msg );
448 $this->mBodytext = $msg;
449 $this->output();
450 wfAbruptExit();
453 function readOnlyPage( $source = "", $protected = false )
455 global $wgUser, $wgReadOnlyFile;
457 $this->setRobotpolicy( "noindex,nofollow" );
458 $this->setArticleFlag( false );
460 if( $protected ) {
461 $this->setPageTitle( wfMsg( "viewsource" ) );
462 $this->addWikiText( wfMsg( "protectedtext" ) );
463 } else {
464 $this->setPageTitle( wfMsg( "readonly" ) );
465 $reason = file_get_contents( $wgReadOnlyFile );
466 $this->addHTML( wfMsg( "readonlytext", $reason ) );
469 if($source) {
470 $rows = $wgUser->getOption( "rows" );
471 $cols = $wgUser->getOption( "cols" );
472 $text .= "</p>\n<textarea cols='$cols' rows='$rows' readonly>" .
473 htmlspecialchars( $source ) . "\n</textarea>";
474 $this->addHTML( $text );
477 $this->returnToMain( false );
480 function fatalError( $message )
482 $this->setPageTitle( wfMsg( "internalerror" ) );
483 $this->setRobotpolicy( "noindex,nofollow" );
484 $this->setArticleFlag( false );
486 $this->mBodytext = $message;
487 $this->output();
488 wfAbruptExit();
491 function unexpectedValueError( $name, $val )
493 $this->fatalError( wfMsg( "unexpected", $name, $val ) );
496 function fileCopyError( $old, $new )
498 $this->fatalError( wfMsg( "filecopyerror", $old, $new ) );
501 function fileRenameError( $old, $new )
503 $this->fatalError( wfMsg( "filerenameerror", $old, $new ) );
506 function fileDeleteError( $name )
508 $this->fatalError( wfMsg( "filedeleteerror", $name ) );
511 function fileNotFoundError( $name )
513 $this->fatalError( wfMsg( "filenotfound", $name ) );
516 function returnToMain( $auto = true )
518 global $wgUser, $wgOut, $returnto;
520 $sk = $wgUser->getSkin();
521 if ( "" == $returnto ) {
522 $returnto = wfMsg( "mainpage" );
524 $link = $sk->makeKnownLink( $returnto, "" );
526 $r = wfMsg( "returnto", $link );
527 if ( $auto ) {
528 $wgOut->addMeta( "http:Refresh", "10;url=" .
529 wfLocalUrlE( wfUrlencode( $returnto ) ) );
531 $wgOut->addHTML( "\n<p>$r\n" );
535 function categoryMagic ()
537 global $wgTitle , $wgUseCategoryMagic ;
538 if ( !isset ( $wgUseCategoryMagic ) || !$wgUseCategoryMagic ) return ;
539 $id = $wgTitle->getArticleID() ;
540 $cat = ucfirst ( wfMsg ( "category" ) ) ;
541 $ti = $wgTitle->getText() ;
542 $ti = explode ( ":" , $ti , 2 ) ;
543 if ( $cat != $ti[0] ) return "" ;
544 $r = "<br break=all>\n" ;
546 $articles = array() ;
547 $parents = array () ;
548 $children = array() ;
551 global $wgUser ;
552 $sk = $wgUser->getSkin() ;
553 $sql = "SELECT l_from FROM links WHERE l_to={$id}" ;
554 $res = wfQuery ( $sql, DB_READ ) ;
555 while ( $x = wfFetchObject ( $res ) )
557 # $t = new Title ;
558 # $t->newFromDBkey ( $x->l_from ) ;
559 # $t = $t->getText() ;
560 $t = $x->l_from ;
561 $y = explode ( ":" , $t , 2 ) ;
562 if ( count ( $y ) == 2 && $y[0] == $cat ) {
563 array_push ( $children , $sk->makeLink ( $t , $y[1] ) ) ;
564 } else {
565 array_push ( $articles , $sk->makeLink ( $t ) ) ;
568 wfFreeResult ( $res ) ;
570 # Children
571 if ( count ( $children ) > 0 )
573 asort ( $children ) ;
574 $r .= "<h2>".wfMsg("subcategories")."</h2>\n" ;
575 $r .= implode ( ", " , $children ) ;
578 # Articles
579 if ( count ( $articles ) > 0 )
581 asort ( $articles ) ;
582 $h = wfMsg( "category_header", $ti[1] );
583 $r .= "<h2>{$h}</h2>\n" ;
584 $r .= implode ( ", " , $articles ) ;
588 return $r ;
591 function getHTMLattrs ()
593 $htmlattrs = array( # Allowed attributes--no scripting, etc.
594 "title", "align", "lang", "dir", "width", "height",
595 "bgcolor", "clear", /* BR */ "noshade", /* HR */
596 "cite", /* BLOCKQUOTE, Q */ "size", "face", "color",
597 /* FONT */ "type", "start", "value", "compact",
598 /* For various lists, mostly deprecated but safe */
599 "summary", "width", "border", "frame", "rules",
600 "cellspacing", "cellpadding", "valign", "char",
601 "charoff", "colgroup", "col", "span", "abbr", "axis",
602 "headers", "scope", "rowspan", "colspan", /* Tables */
603 "id", "class", "name", "style" /* For CSS */
605 return $htmlattrs ;
608 function fixTableTags ( $t )
610 if ( trim ( $t ) == "" ) return "" ; # Saves runtime ;-)
611 $htmlattrs = $this->getHTMLattrs() ;
613 # Strip non-approved attributes from the tag
614 $t = preg_replace(
615 "/(\\w+)(\\s*=\\s*([^\\s\">]+|\"[^\">]*\"))?/e",
616 "(in_array(strtolower(\"\$1\"),\$htmlattrs)?(\"\$1\".((\"x\$3\" != \"x\")?\"=\$3\":'')):'')",
617 $t);
619 return trim ( $t ) ;
622 function doTableStuff ( $t )
624 $t = explode ( "\n" , $t ) ;
625 $td = array () ; # Is currently a td tag open?
626 $ltd = array () ; # Was it TD or TH?
627 $tr = array () ; # Is currently a tr tag open?
628 $ltr = array () ; # tr attributes
629 foreach ( $t AS $k => $x )
631 $x = rtrim ( $x ) ;
632 $fc = substr ( $x , 0 , 1 ) ;
633 if ( "{|" == substr ( $x , 0 , 2 ) )
635 $t[$k] = "<table " . $this->fixTableTags ( substr ( $x , 3 ) ) . ">" ;
636 array_push ( $td , false ) ;
637 array_push ( $ltd , "" ) ;
638 array_push ( $tr , false ) ;
639 array_push ( $ltr , "" ) ;
641 else if ( count ( $td ) == 0 ) { } # Don't do any of the following
642 else if ( "|}" == substr ( $x , 0 , 2 ) )
644 $z = "</table>\n" ;
645 $l = array_pop ( $ltd ) ;
646 if ( array_pop ( $tr ) ) $z = "</tr>" . $z ;
647 if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
648 array_pop ( $ltr ) ;
649 $t[$k] = $z ;
651 /* else if ( "|_" == substr ( $x , 0 , 2 ) ) # Caption
653 $z = trim ( substr ( $x , 2 ) ) ;
654 $t[$k] = "<caption>{$z}</caption>\n" ;
656 else if ( "|-" == substr ( $x , 0 , 2 ) ) # Allows for |---------------
658 $x = substr ( $x , 1 ) ;
659 while ( $x != "" && substr ( $x , 0 , 1 ) == '-' ) $x = substr ( $x , 1 ) ;
660 $z = "" ;
661 $l = array_pop ( $ltd ) ;
662 if ( array_pop ( $tr ) ) $z = "</tr>" . $z ;
663 if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
664 array_pop ( $ltr ) ;
665 $t[$k] = $z ;
666 array_push ( $tr , false ) ;
667 array_push ( $td , false ) ;
668 array_push ( $ltd , "" ) ;
669 array_push ( $ltr , $this->fixTableTags ( $x ) ) ;
671 else if ( "|" == $fc || "!" == $fc || "|+" == substr ( $x , 0 , 2 ) ) # Caption
673 if ( "|+" == substr ( $x , 0 , 2 ) )
675 $fc = "+" ;
676 $x = substr ( $x , 1 ) ;
678 $after = substr ( $x , 1 ) ;
679 if ( $fc == "!" ) $after = str_replace ( "!!" , "||" , $after ) ;
680 $after = explode ( "||" , $after ) ;
681 $t[$k] = "" ;
682 foreach ( $after AS $theline )
684 $z = "" ;
685 $tra = array_pop ( $ltr ) ;
686 if ( !array_pop ( $tr ) ) $z = "<tr {$tra}>\n" ;
687 array_push ( $tr , true ) ;
688 array_push ( $ltr , "" ) ;
690 $l = array_pop ( $ltd ) ;
691 if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
692 if ( $fc == "|" ) $l = "TD" ;
693 else if ( $fc == "!" ) $l = "TH" ;
694 else if ( $fc == "+" ) $l = "CAPTION" ;
695 else $l = "" ;
696 array_push ( $ltd , $l ) ;
697 $y = explode ( "|" , $theline , 2 ) ;
698 if ( count ( $y ) == 1 ) $y = "{$z}<{$l}>{$y[0]}" ;
699 else $y = $y = "{$z}<{$l} ".$this->fixTableTags($y[0]).">{$y[1]}" ;
700 $t[$k] .= $y ;
701 array_push ( $td , true ) ;
706 # Closing open td, tr && table
707 while ( count ( $td ) > 0 )
709 if ( array_pop ( $td ) ) $t[] = "</td>" ;
710 if ( array_pop ( $tr ) ) $t[] = "</tr>" ;
711 $t[] = "</table>" ;
714 $t = implode ( "\n" , $t ) ;
715 # $t = $this->removeHTMLtags( $t );
716 return $t ;
719 # Well, OK, it's actually about 14 passes. But since all the
720 # hard lifting is done inside PHP's regex code, it probably
721 # wouldn't speed things up much to add a real parser.
723 function doWikiPass2( $text, $linestart )
725 global $wgUser, $wgLang, $wgUseDynamicDates;
726 $fname = "OutputPage::doWikiPass2";
727 wfProfileIn( $fname );
729 $text = $this->removeHTMLtags( $text );
730 $text = $this->replaceVariables( $text );
732 $text = preg_replace( "/(^|\n)-----*/", "\\1<hr>", $text );
733 $text = str_replace ( "<HR>", "<hr>", $text );
735 $text = $this->doAllQuotes( $text );
736 $text = $this->doHeadings( $text );
737 $text = $this->doBlockLevels( $text, $linestart );
739 if($wgUseDynamicDates) {
740 global $wgDateFormatter;
741 $text = $wgDateFormatter->reformat( $wgUser->getOption("date"), $text );
744 $text = $this->replaceExternalLinks( $text );
745 $text = $this->replaceInternalLinks ( $text );
746 $text = $this->doTableStuff ( $text ) ;
748 $text = $this->magicISBN( $text );
749 $text = $this->magicRFC( $text );
750 $text = $this->formatHeadings( $text );
752 $sk = $wgUser->getSkin();
753 $text = $sk->transformContent( $text );
754 $text .= $this->categoryMagic () ;
756 wfProfileOut( $fname );
757 return $text;
760 /* private */ function doAllQuotes( $text )
762 $outtext = "";
763 $lines = explode( "\r\n", $text );
764 foreach ( $lines as $line ) {
765 $outtext .= $this->doQuotes ( "", $line, "" ) . "\r\n";
767 return $outtext;
770 /* private */ function doQuotes( $pre, $text, $mode )
772 if ( preg_match( "/^(.*)''(.*)$/sU", $text, $m ) ) {
773 $m1_strong = ($m[1] == "") ? "" : "<strong>{$m[1]}</strong>";
774 $m1_em = ($m[1] == "") ? "" : "<em>{$m[1]}</em>";
775 if ( substr ($m[2], 0, 1) == "'" ) {
776 $m[2] = substr ($m[2], 1);
777 if ($mode == "em") {
778 return $this->doQuotes ( $m[1], $m[2], ($m[1] == "") ? "both" : "emstrong" );
779 } else if ($mode == "strong") {
780 return $m1_strong . $this->doQuotes ( "", $m[2], "" );
781 } else if (($mode == "emstrong") || ($mode == "both")) {
782 return $this->doQuotes ( "", $pre.$m1_strong.$m[2], "em" );
783 } else if ($mode == "strongem") {
784 return "<strong>{$pre}{$m1_em}</strong>" . $this->doQuotes ( "", $m[2], "em" );
785 } else {
786 return $m[1] . $this->doQuotes ( "", $m[2], "strong" );
788 } else {
789 if ($mode == "strong") {
790 return $this->doQuotes ( $m[1], $m[2], ($m[1] == "") ? "both" : "strongem" );
791 } else if ($mode == "em") {
792 return $m1_em . $this->doQuotes ( "", $m[2], "" );
793 } else if ($mode == "emstrong") {
794 return "<em>{$pre}{$m1_strong}</em>" . $this->doQuotes ( "", $m[2], "strong" );
795 } else if (($mode == "strongem") || ($mode == "both")) {
796 return $this->doQuotes ( "", $pre.$m1_em.$m[2], "strong" );
797 } else {
798 return $m[1] . $this->doQuotes ( "", $m[2], "em" );
801 } else {
802 $text_strong = ($text == "") ? "" : "<strong>{$text}</strong>";
803 $text_em = ($text == "") ? "" : "<em>{$text}</em>";
804 if ($mode == "") {
805 return $pre . $text;
806 } else if ($mode == "em") {
807 return $pre . $text_em;
808 } else if ($mode == "strong") {
809 return $pre . $text_strong;
810 } else if ($mode == "strongem") {
811 return (($pre == "") && ($text == "")) ? "" : "<strong>{$pre}{$text_em}</strong>";
812 } else {
813 return (($pre == "") && ($text == "")) ? "" : "<em>{$pre}{$text_strong}</em>";
818 /* private */ function doHeadings( $text )
820 for ( $i = 6; $i >= 1; --$i ) {
821 $h = substr( "======", 0, $i );
822 $text = preg_replace( "/^{$h}([^=]+){$h}(\\s|$)/m",
823 "<h{$i}>\\1</h{$i}>\\2", $text );
825 return $text;
828 # Note: we have to do external links before the internal ones,
829 # and otherwise take great care in the order of things here, so
830 # that we don't end up interpreting some URLs twice.
832 /* private */ function replaceExternalLinks( $text )
834 $fname = "OutputPage::replaceExternalLinks";
835 wfProfileIn( $fname );
836 $text = $this->subReplaceExternalLinks( $text, "http", true );
837 $text = $this->subReplaceExternalLinks( $text, "https", true );
838 $text = $this->subReplaceExternalLinks( $text, "ftp", false );
839 $text = $this->subReplaceExternalLinks( $text, "gopher", false );
840 $text = $this->subReplaceExternalLinks( $text, "news", false );
841 $text = $this->subReplaceExternalLinks( $text, "mailto", false );
842 wfProfileOut( $fname );
843 return $text;
846 /* private */ function subReplaceExternalLinks( $s, $protocol, $autonumber )
848 global $wgUser, $printable;
849 global $wgAllowExternalImages;
852 $unique = "4jzAfzB8hNvf4sqyO9Edd8pSmk9rE2in0Tgw3";
853 $uc = "A-Za-z0-9_\\/~%\\-+&*#?!=()@\\x80-\\xFF";
855 # this is the list of separators that should be ignored if they
856 # are the last character of an URL but that should be included
857 # if they occur within the URL, e.g. "go to www.foo.com, where .."
858 # in this case, the last comma should not become part of the URL,
859 # but in "www.foo.com/123,2342,32.htm" it should.
860 $sep = ",;\.:";
861 $fnc = "A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF";
862 $images = "gif|png|jpg|jpeg";
864 # PLEASE NOTE: The curly braces { } are not part of the regex,
865 # they are interpreted as part of the string (used to tell PHP
866 # that the content of the string should be inserted there).
867 $e1 = "/(^|[^\\[])({$protocol}:)([{$uc}{$sep}]+)\\/([{$fnc}]+)\\." .
868 "((?i){$images})([^{$uc}]|$)/";
870 $e2 = "/(^|[^\\[])({$protocol}:)(([".$uc."]|[".$sep."][".$uc."])+)([^". $uc . $sep. "]|[".$sep."]|$)/";
871 $sk = $wgUser->getSkin();
873 if ( $autonumber and $wgAllowExternalImages) { # Use img tags only for HTTP urls
874 $s = preg_replace( $e1, "\\1" . $sk->makeImage( "{$unique}:\\3" .
875 "/\\4.\\5", "\\4.\\5" ) . "\\6", $s );
877 $s = preg_replace( $e2, "\\1" . "<a href=\"{$unique}:\\3\"" .
878 $sk->getExternalLinkAttributes( "{$unique}:\\3", wfEscapeHTML(
879 "{$unique}:\\3" ) ) . ">" . wfEscapeHTML( "{$unique}:\\3" ) .
880 "</a>\\5", $s );
881 $s = str_replace( $unique, $protocol, $s );
883 $a = explode( "[{$protocol}:", " " . $s );
884 $s = array_shift( $a );
885 $s = substr( $s, 1 );
887 $e1 = "/^([{$uc}"."{$sep}]+)](.*)\$/sD";
888 $e2 = "/^([{$uc}"."{$sep}]+)\\s+([^\\]]+)](.*)\$/sD";
890 foreach ( $a as $line ) {
891 if ( preg_match( $e1, $line, $m ) ) {
892 $link = "{$protocol}:{$m[1]}";
893 $trail = $m[2];
894 if ( $autonumber ) { $text = "[" . ++$this->mAutonumber . "]"; }
895 else { $text = wfEscapeHTML( $link ); }
896 } else if ( preg_match( $e2, $line, $m ) ) {
897 $link = "{$protocol}:{$m[1]}";
898 $text = $m[2];
899 $trail = $m[3];
900 } else {
901 $s .= "[{$protocol}:" . $line;
902 continue;
904 if ( $printable == "yes") $paren = " (<i>" . htmlspecialchars ( $link ) . "</i>)";
905 else $paren = "";
906 $la = $sk->getExternalLinkAttributes( $link, $text );
907 $s .= "<a href='{$link}'{$la}>{$text}</a>{$paren}{$trail}";
910 return $s;
913 /* private */ function replaceInternalLinks( $s )
915 global $wgTitle, $wgUser, $wgLang;
916 global $wgLinkCache, $wgInterwikiMagic, $wgUseCategoryMagic;
917 global $wgNamespacesWithSubpages, $wgLanguageCode;
918 wfProfileIn( $fname = "OutputPage::replaceInternalLinks" );
920 wfProfileIn( "$fname-setup" );
921 $tc = Title::legalChars() . "#";
922 $sk = $wgUser->getSkin();
924 $a = explode( "[[", " " . $s );
925 $s = array_shift( $a );
926 $s = substr( $s, 1 );
928 $e1 = "/^([{$tc}]+)(?:\\|([^]]+))?]](.*)\$/sD";
930 # Special and Media are pseudo-namespaces; no pages actually exist in them
931 $image = Namespace::getImage();
932 $special = Namespace::getSpecial();
933 $media = Namespace::getMedia();
934 $nottalk = !Namespace::isTalk( $wgTitle->getNamespace() );
935 wfProfileOut( "$fname-setup" );
937 foreach ( $a as $line ) {
938 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
939 $text = $m[2];
940 $trail = $m[3];
941 } else { # Invalid form; output directly
942 $s .= "[[" . $line ;
943 continue;
946 /* Valid link forms:
947 Foobar -- normal
948 :Foobar -- override special treatment of prefix (images, language links)
949 /Foobar -- convert to CurrentPage/Foobar
950 /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
952 $c = substr($m[1],0,1);
953 $noforce = ($c != ":");
954 if( $c == "/" ) { # subpage
955 if(substr($m[1],-1,1)=="/") { # / at end means we don't want the slash to be shown
956 $m[1]=substr($m[1],1,strlen($m[1])-2);
957 $noslash=$m[1];
958 } else {
959 $noslash=substr($m[1],1);
961 if($wgNamespacesWithSubpages[$wgTitle->getNamespace()]) { # subpages allowed here
962 $link = $wgTitle->getPrefixedText(). "/" . trim($noslash);
963 if( "" == $text ) {
964 $text= $m[1];
965 } # this might be changed for ugliness reasons
966 } else {
967 $link = $noslash; # no subpage allowed, use standard link
969 } elseif( $noforce ) { # no subpage
970 $link = $m[1];
971 } else {
972 $link = substr( $m[1], 1 );
974 if( "" == $text )
975 $text = $link;
977 $nt = Title::newFromText( $link );
978 if( !$nt ) {
979 $s .= "[[" . $line;
980 continue;
982 $ns = $nt->getNamespace();
983 $iw = $nt->getInterWiki();
984 if( $noforce ) {
985 if( $iw && $wgInterwikiMagic && $nottalk && $wgLang->getLanguageName( $iw ) ) {
986 array_push( $this->mLanguageLinks, $nt->getPrefixedText() );
987 $s .= $trail;
988 /* CHECK MERGE @@@
989 } else if ( "media" == $pre ) {
990 $nt = Title::newFromText( $suf );
991 $name = $nt->getDBkey();
992 if ( "" == $text ) { $text = $nt->GetText(); }
994 $wgLinkCache->addImageLink( $name );
995 $s .= $sk->makeMediaLink( $name,
996 wfImageUrl( $name ), $text );
997 $s .= $trail;
998 } else if ( isset($wgUseCategoryMagic) && $wgUseCategoryMagic && $pre == wfMsg ( "category" ) ) {
999 $l = $sk->makeLink ( $pre.":".ucfirst( $m[2] ), ucfirst ( $m[2] ) ) ;
1000 array_push ( $this->mCategoryLinks , $l ) ;
1001 $s .= $trail ;
1002 } else {
1003 $l = $wgLang->getLanguageName( $pre );
1004 if ( "" == $l or !$wgInterwikiMagic or Namespace::isTalk( $wgTitle->getNamespace() ) ) {
1005 if ( "" == $text ) {
1006 $text = $link;
1008 $s .= $sk->makeLink( $link, $text, "", $trail );
1009 } else if ( $pre != $wgLanguageCode ) {
1010 array_push( $this->mLanguageLinks, "$pre:$suf" );
1011 $s .= $trail;
1014 continue;
1016 if( $ns == $image ) {
1017 $s .= $sk->makeImageLinkObj( $nt, $text ) . $trail;
1018 $wgLinkCache->addImageLinkObj( $nt );
1019 continue;
1021 /* CHECK MERGE @@@
1022 # } else if ( 0 == strcmp( "##", substr( $link, 0, 2 ) ) ) {
1023 # $link = substr( $link, 2 );
1024 # $s .= "<a name=\"{$link}\">{$text}</a>{$trail}";
1025 } else {
1026 if ( "" == $text ) { $text = $link; }
1027 # Hotspot:
1028 $s .= $sk->makeLink( $link, $text, "", $trail );
1031 if( $ns == $media ) {
1032 $s .= $sk->makeMediaLinkObj( $nt, $text ) . $trail;
1033 $wgLinkCache->addImageLinkObj( $nt );
1034 continue;
1035 } elseif( $ns == $special ) {
1036 $s .= $sk->makeKnownLinkObj( $nt, $text, "", $trail );
1037 continue;
1039 $s .= $sk->makeLinkObj( $nt, $text, "", $trail );
1041 wfProfileOut( $fname );
1042 return $s;
1045 # Some functions here used by doBlockLevels()
1047 /* private */ function closeParagraph()
1049 $result = "";
1050 if ( 0 != strcmp( "p", $this->mLastSection ) &&
1051 0 != strcmp( "", $this->mLastSection ) ) {
1052 $result = "</" . $this->mLastSection . ">";
1054 $this->mLastSection = "";
1055 return $result."\n";
1057 # getCommon() returns the length of the longest common substring
1058 # of both arguments, starting at the beginning of both.
1060 /* private */ function getCommon( $st1, $st2 )
1062 $fl = strlen( $st1 );
1063 $shorter = strlen( $st2 );
1064 if ( $fl < $shorter ) { $shorter = $fl; }
1066 for ( $i = 0; $i < $shorter; ++$i ) {
1067 if ( $st1{$i} != $st2{$i} ) { break; }
1069 return $i;
1071 # These next three functions open, continue, and close the list
1072 # element appropriate to the prefix character passed into them.
1074 /* private */ function openList( $char )
1076 $result = $this->closeParagraph();
1078 if ( "*" == $char ) { $result .= "<ul><li>"; }
1079 else if ( "#" == $char ) { $result .= "<ol><li>"; }
1080 else if ( ":" == $char ) { $result .= "<dl><dd>"; }
1081 else if ( ";" == $char ) {
1082 $result .= "<dl><dt>";
1083 $this->mDTopen = true;
1085 else { $result = "<!-- ERR 1 -->"; }
1087 return $result;
1090 /* private */ function nextItem( $char )
1092 if ( "*" == $char || "#" == $char ) { return "</li><li>"; }
1093 else if ( ":" == $char || ";" == $char ) {
1094 $close = "</dd>";
1095 if ( $this->mDTopen ) { $close = "</dt>"; }
1096 if ( ";" == $char ) {
1097 $this->mDTopen = true;
1098 return $close . "<dt>";
1099 } else {
1100 $this->mDTopen = false;
1101 return $close . "<dd>";
1104 return "<!-- ERR 2 -->";
1107 /* private */function closeList( $char )
1109 if ( "*" == $char ) { $text = "</li></ul>"; }
1110 else if ( "#" == $char ) { $text = "</li></ol>"; }
1111 else if ( ":" == $char ) {
1112 if ( $this->mDTopen ) {
1113 $this->mDTopen = false;
1114 $text = "</dt></dl>";
1115 } else {
1116 $text = "</dd></dl>";
1119 else { return "<!-- ERR 3 -->"; }
1120 return $text."\n";
1123 /* private */ function doBlockLevels( $text, $linestart )
1125 $fname = "OutputPage::doBlockLevels";
1126 wfProfileIn( $fname );
1127 # Parsing through the text line by line. The main thing
1128 # happening here is handling of block-level elements p, pre,
1129 # and making lists from lines starting with * # : etc.
1131 $a = explode( "\n", $text );
1132 $text = $lastPref = "";
1133 $this->mDTopen = $inBlockElem = false;
1135 if ( ! $linestart ) { $text .= array_shift( $a ); }
1136 foreach ( $a as $t ) {
1137 if ( "" != $text ) { $text .= "\n"; }
1139 $oLine = $t;
1140 $opl = strlen( $lastPref );
1141 $npl = strspn( $t, "*#:;" );
1142 $pref = substr( $t, 0, $npl );
1143 $pref2 = str_replace( ";", ":", $pref );
1144 $t = substr( $t, $npl );
1146 if ( 0 != $npl && 0 == strcmp( $lastPref, $pref2 ) ) {
1147 $text .= $this->nextItem( substr( $pref, -1 ) );
1149 if ( ";" == substr( $pref, -1 ) ) {
1150 $cpos = strpos( $t, ":" );
1151 if ( ! ( false === $cpos ) ) {
1152 $term = substr( $t, 0, $cpos );
1153 $text .= $term . $this->nextItem( ":" );
1154 $t = substr( $t, $cpos + 1 );
1157 } else if (0 != $npl || 0 != $opl) {
1158 $cpl = $this->getCommon( $pref, $lastPref );
1160 while ( $cpl < $opl ) {
1161 $text .= $this->closeList( $lastPref{$opl-1} );
1162 --$opl;
1164 if ( $npl <= $cpl && $cpl > 0 ) {
1165 $text .= $this->nextItem( $pref{$cpl-1} );
1167 while ( $npl > $cpl ) {
1168 $char = substr( $pref, $cpl, 1 );
1169 $text .= $this->openList( $char );
1171 if ( ";" == $char ) {
1172 $cpos = strpos( $t, ":" );
1173 if ( ! ( false === $cpos ) ) {
1174 $term = substr( $t, 0, $cpos );
1175 $text .= $term . $this->nextItem( ":" );
1176 $t = substr( $t, $cpos + 1 );
1179 ++$cpl;
1181 $lastPref = $pref2;
1183 if ( 0 == $npl ) { # No prefix--go to paragraph mode
1184 if ( preg_match(
1185 "/(<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6)/i", $t ) ) {
1186 $text .= $this->closeParagraph();
1187 $inBlockElem = true;
1189 if ( ! $inBlockElem ) {
1190 if ( " " == $t{0} ) {
1191 $newSection = "pre";
1192 # $t = wfEscapeHTML( $t );
1194 else { $newSection = "p"; }
1196 if ( 0 == strcmp( "", trim( $oLine ) ) ) {
1197 $text .= $this->closeParagraph();
1198 $text .= "<" . $newSection . ">";
1199 } else if ( 0 != strcmp( $this->mLastSection,
1200 $newSection ) ) {
1201 $text .= $this->closeParagraph();
1202 if ( 0 != strcmp( "p", $newSection ) ) {
1203 $text .= "<" . $newSection . ">";
1206 $this->mLastSection = $newSection;
1208 if ( $inBlockElem &&
1209 preg_match( "/(<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6)/i", $t ) ) {
1210 $inBlockElem = false;
1213 $text .= $t;
1215 while ( $npl ) {
1216 $text .= $this->closeList( $pref2{$npl-1} );
1217 --$npl;
1219 if ( "" != $this->mLastSection ) {
1220 if ( "p" != $this->mLastSection ) {
1221 $text .= "</" . $this->mLastSection . ">";
1223 $this->mLastSection = "";
1225 wfProfileOut( $fname );
1226 return $text;
1229 /* private */ function replaceVariables( $text )
1231 global $wgLang;
1232 $fname = "OutputPage::replaceVariables";
1233 wfProfileIn( $fname );
1236 # Basic variables
1237 # See Language.php for the definition of each magic word
1239 # As with sigs, this uses the server's local time -- ensure
1240 # this is appropriate for your audience!
1241 $v = date( "m" );
1242 $mw =& MagicWord::get( MAG_CURRENTMONTH );
1243 $text = $mw->replace( $v, $text );
1244 if( $mw->getWasModified() ) { $this->mContainsOldMagic++; }
1246 $v = $wgLang->getMonthName( date( "n" ) );
1247 $mw =& MagicWord::get( MAG_CURRENTMONTHNAME );
1248 $text = $mw->replace( $v, $text );
1249 if( $mw->getWasModified() ) { $this->mContainsOldMagic++; }
1251 $v = $wgLang->getMonthNameGen( date( "n" ) );
1252 $mw =& MagicWord::get( MAG_CURRENTMONTHNAMEGEN );
1253 $text = $mw->replace( $v, $text );
1254 if( $mw->getWasModified() ) { $this->mContainsOldMagic++; }
1256 $v = date( "j" );
1257 $mw = MagicWord::get( MAG_CURRENTDAY );
1258 $text = $mw->replace( $v, $text );
1259 if( $mw->getWasModified() ) { $this->mContainsOldMagic++; }
1261 $v = $wgLang->getWeekdayName( date( "w" )+1 );
1262 $mw =& MagicWord::get( MAG_CURRENTDAYNAME );
1263 $text = $mw->replace( $v, $text );
1264 if( $mw->getWasModified() ) { $this->mContainsOldMagic++; }
1266 $v = date( "Y" );
1267 $mw =& MagicWord::get( MAG_CURRENTYEAR );
1268 $text = $mw->replace( $v, $text );
1269 if( $mw->getWasModified() ) { $this->mContainsOldMagic++; }
1271 $v = $wgLang->time( wfTimestampNow(), false );
1272 $mw =& MagicWord::get( MAG_CURRENTTIME );
1273 $text = $mw->replace( $v, $text );
1274 if( $mw->getWasModified() ) { $this->mContainsOldMagic++; }
1276 $mw =& MagicWord::get( MAG_NUMBEROFARTICLES );
1277 if ( $mw->match( $text ) ) {
1278 $v = wfNumberOfArticles();
1279 $text = $mw->replace( $v, $text );
1280 if( $mw->getWasModified() ) { $this->mContainsOldMagic++; }
1283 # "Variables" with an additional parameter e.g. {{MSG:wikipedia}}
1284 # The callbacks are at the bottom of this file
1285 $mw =& MagicWord::get( MAG_MSG );
1286 $text = $mw->substituteCallback( $text, "wfReplaceMsgVar" );
1287 if( $mw->getWasModified() ) { $this->mContainsNewMagic++; }
1289 $mw =& MagicWord::get( MAG_MSGNW );
1290 $text = $mw->substituteCallback( $text, "wfReplaceMsgnwVar" );
1291 if( $mw->getWasModified() ) { $this->mContainsNewMagic++; }
1293 wfProfileOut( $fname );
1294 return $text;
1297 # Cleans up HTML, removes dangerous tags and attributes
1298 /* private */ function removeHTMLtags( $text )
1300 $fname = "OutputPage::removeHTMLtags";
1301 wfProfileIn( $fname );
1302 $htmlpairs = array( # Tags that must be closed
1303 "b", "i", "u", "font", "big", "small", "sub", "sup", "h1",
1304 "h2", "h3", "h4", "h5", "h6", "cite", "code", "em", "s",
1305 "strike", "strong", "tt", "var", "div", "center",
1306 "blockquote", "ol", "ul", "dl", "table", "caption", "pre",
1307 "ruby", "rt" , "rb" , "rp"
1309 $htmlsingle = array(
1310 "br", "p", "hr", "li", "dt", "dd"
1312 $htmlnest = array( # Tags that can be nested--??
1313 "table", "tr", "td", "th", "div", "blockquote", "ol", "ul",
1314 "dl", "font", "big", "small", "sub", "sup"
1316 $tabletags = array( # Can only appear inside table
1317 "td", "th", "tr"
1320 $htmlsingle = array_merge( $tabletags, $htmlsingle );
1321 $htmlelements = array_merge( $htmlsingle, $htmlpairs );
1323 $htmlattrs = $this->getHTMLattrs () ;
1325 # Remove HTML comments
1326 $text = preg_replace( "/<!--.*-->/sU", "", $text );
1328 $bits = explode( "<", $text );
1329 $text = array_shift( $bits );
1330 $tagstack = array(); $tablestack = array();
1332 foreach ( $bits as $x ) {
1333 $prev = error_reporting( E_ALL & ~( E_NOTICE | E_WARNING ) );
1334 preg_match( "/^(\\/?)(\\w+)([^>]*)(\\/{0,1}>)([^<]*)$/",
1335 $x, $regs );
1336 list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs;
1337 error_reporting( $prev );
1339 $badtag = 0 ;
1340 if ( in_array( $t = strtolower( $t ), $htmlelements ) ) {
1341 # Check our stack
1342 if ( $slash ) {
1343 # Closing a tag...
1344 if ( ! in_array( $t, $htmlsingle ) &&
1345 ( $ot = array_pop( $tagstack ) ) != $t ) {
1346 array_push( $tagstack, $ot );
1347 $badtag = 1;
1348 } else {
1349 if ( $t == "table" ) {
1350 $tagstack = array_pop( $tablestack );
1352 $newparams = "";
1354 } else {
1355 # Keep track for later
1356 if ( in_array( $t, $tabletags ) &&
1357 ! in_array( "table", $tagstack ) ) {
1358 $badtag = 1;
1359 } else if ( in_array( $t, $tagstack ) &&
1360 ! in_array ( $t , $htmlnest ) ) {
1361 $badtag = 1 ;
1362 } else if ( ! in_array( $t, $htmlsingle ) ) {
1363 if ( $t == "table" ) {
1364 array_push( $tablestack, $tagstack );
1365 $tagstack = array();
1367 array_push( $tagstack, $t );
1369 # Strip non-approved attributes from the tag
1370 $newparams = preg_replace(
1371 "/(\\w+)(\\s*=\\s*([^\\s\">]+|\"[^\">]*\"))?/e",
1372 "(in_array(strtolower(\"\$1\"),\$htmlattrs)?(\"\$1\".((\"x\$3\" != \"x\")?\"=\$3\":'')):'')",
1373 $params);
1375 if ( ! $badtag ) {
1376 $rest = str_replace( ">", "&gt;", $rest );
1377 $text .= "<$slash$t$newparams$brace$rest";
1378 continue;
1381 $text .= "&lt;" . str_replace( ">", "&gt;", $x);
1383 # Close off any remaining tags
1384 while ( $t = array_pop( $tagstack ) ) {
1385 $text .= "</$t>\n";
1386 if ( $t == "table" ) { $tagstack = array_pop( $tablestack ); }
1388 wfProfileOut( $fname );
1389 return $text;
1394 * This function accomplishes several tasks:
1395 * 1) Auto-number headings if that option is enabled
1396 * 2) Add an [edit] link to sections for logged in users who have enabled the option
1397 * 3) Add a Table of contents on the top for users who have enabled the option
1398 * 4) Auto-anchor headings
1400 * It loops through all headlines, collects the necessary data, then splits up the
1401 * string and re-inserts the newly formatted headlines.
1403 * */
1404 /* private */ function formatHeadings( $text )
1406 global $wgUser,$wgArticle,$wgTitle,$wpPreview;
1407 $nh=$wgUser->getOption( "numberheadings" );
1408 $st=$wgUser->getOption( "showtoc" );
1409 if(!$wgTitle->userCanEdit()) {
1410 $es=0;
1411 $esr=0;
1412 } else {
1413 $es=$wgUser->getID() && $wgUser->getOption( "editsection" );
1414 $esr=$wgUser->getID() && $wgUser->getOption( "editsectiononrightclick" );
1417 # Inhibit editsection links if requested in the page
1418 $esw =& MagicWord::get( MAG_NOEDITSECTION );
1419 if ($esw->matchAndRemove( $text )) {
1420 $es=0;
1422 # if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
1423 # do not add TOC
1424 $mw =& MagicWord::get( MAG_NOTOC );
1425 if ($mw->matchAndRemove( $text ))
1427 $st = 0;
1430 # never add the TOC to the Main Page. This is an entry page that should not
1431 # be more than 1-2 screens large anyway
1432 if($wgTitle->getPrefixedText()==wfMsg("mainpage")) {$st=0;}
1434 # We need this to perform operations on the HTML
1435 $sk=$wgUser->getSkin();
1437 # Get all headlines for numbering them and adding funky stuff like [edit]
1438 # links
1439 preg_match_all("/<H([1-6])(.*?>)(.*?)<\/H[1-6]>/i",$text,$matches);
1441 # headline counter
1442 $c=0;
1444 # Ugh .. the TOC should have neat indentation levels which can be
1445 # passed to the skin functions. These are determined here
1446 foreach($matches[3] as $headline) {
1447 if($level) { $prevlevel=$level;}
1448 $level=$matches[1][$c];
1449 if(($nh||$st) && $prevlevel && $level>$prevlevel) {
1451 $h[$level]=0; // reset when we enter a new level
1452 $toc.=$sk->tocIndent($level-$prevlevel);
1453 $toclevel+=$level-$prevlevel;
1456 if(($nh||$st) && $level<$prevlevel) {
1457 $h[$level+1]=0; // reset when we step back a level
1458 $toc.=$sk->tocUnindent($prevlevel-$level);
1459 $toclevel-=$prevlevel-$level;
1462 $h[$level]++; // count number of headlines for each level
1464 if($nh||$st) {
1465 for($i=1;$i<=$level;$i++) {
1466 if($h[$i]) {
1467 if($dot) {$numbering.=".";}
1468 $numbering.=$h[$i];
1469 $dot=1;
1474 // The canonized header is a version of the header text safe to use for links
1476 $canonized_headline=preg_replace("/<.*?>/","",$headline); // strip out HTML
1477 $tocline = trim( $canonized_headline );
1478 $canonized_headline=str_replace('"',"",$canonized_headline);
1479 $canonized_headline=str_replace(" ","_",trim($canonized_headline));
1480 $refer[$c]=$canonized_headline;
1481 $refers[$canonized_headline]++; // count how many in assoc. array so we can track dupes in anchors
1482 $refcount[$c]=$refers[$canonized_headline];
1484 // Prepend the number to the heading text
1486 if($nh||$st) {
1487 $tocline=$numbering ." ". $tocline;
1489 // Don't number the heading if it is the only one (looks silly)
1490 if($nh && count($matches[3]) > 1) {
1491 $headline=$numbering . " " . $headline; // the two are different if the line contains a link
1495 // Create the anchor for linking from the TOC to the section
1497 $anchor=$canonized_headline;
1498 if($refcount[$c]>1) {$anchor.="_".$refcount[$c];}
1499 if($st) {
1500 $toc.=$sk->tocLine($anchor,$tocline,$toclevel);
1502 if($es && !isset($wpPreview)) {
1503 $head[$c].=$sk->editSectionLink($c+1);
1506 // Put it all together
1508 $head[$c].="<h".$level.$matches[2][$c]
1509 ."<a name=\"".$anchor."\">"
1510 .$headline
1511 ."</a>"
1512 ."</h".$level.">";
1514 // Add the edit section link
1516 if($esr && !isset($wpPreview)) {
1517 $head[$c]=$sk->editSectionScript($c+1,$head[$c]);
1520 $numbering="";
1521 $c++;
1522 $dot=0;
1525 if($st) {
1526 $toclines=$c;
1527 $toc.=$sk->tocUnindent($toclevel);
1528 $toc=$sk->tocTable($toc);
1531 // split up and insert constructed headlines
1533 $blocks=preg_split("/<H[1-6].*?>.*?<\/H[1-6]>/i",$text);
1534 $i=0;
1536 foreach($blocks as $block) {
1537 if(($es) && !isset($wpPreview) && $c>0 && $i==0) {
1538 # This is the [edit] link that appears for the top block of text when
1539 # section editing is enabled
1540 $full.=$sk->editSectionLink(0);
1542 $full.=$block;
1543 if($st && $toclines>3 && !$i) {
1544 # Let's add a top anchor just in case we want to link to the top of the page
1545 $full="<a name=\"top\"></a>".$full.$toc;
1548 $full.=$head[$i];
1549 $i++;
1552 return $full;
1555 /* private */ function magicISBN( $text )
1557 global $wgLang;
1559 $a = split( "ISBN ", " $text" );
1560 if ( count ( $a ) < 2 ) return $text;
1561 $text = substr( array_shift( $a ), 1);
1562 $valid = "0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1564 foreach ( $a as $x ) {
1565 $isbn = $blank = "" ;
1566 while ( " " == $x{0} ) {
1567 $blank .= " ";
1568 $x = substr( $x, 1 );
1570 while ( strstr( $valid, $x{0} ) != false ) {
1571 $isbn .= $x{0};
1572 $x = substr( $x, 1 );
1574 $num = str_replace( "-", "", $isbn );
1575 $num = str_replace( " ", "", $num );
1577 if ( "" == $num ) {
1578 $text .= "ISBN $blank$x";
1579 } else {
1580 $text .= "<a href=\"" . wfLocalUrlE( $wgLang->specialPage(
1581 "Booksources"), "isbn={$num}" ) . "\" class=\"internal\">ISBN $isbn</a>";
1582 $text .= $x;
1585 return $text;
1588 /* private */ function magicRFC( $text )
1590 return $text;
1593 /* private */ function headElement()
1595 global $wgDocType, $wgDTD, $wgUser, $wgLanguageCode, $wgOutputEncoding, $wgLang;
1597 $ret = "<!DOCTYPE HTML PUBLIC \"$wgDocType\"\n \"$wgDTD\">\n";
1599 if ( "" == $this->mHTMLtitle ) {
1600 $this->mHTMLtitle = $this->mPagetitle;
1602 $rtl = $wgLang->isRTL() ? " dir='RTL'" : "";
1603 $ret .= "<html lang=\"$wgLanguageCode\"$rtl><head><title>{$this->mHTMLtitle}</title>\n";
1604 array_push( $this->mMetatags, array( "http:Content-type", "text/html; charset={$wgOutputEncoding}" ) );
1605 foreach ( $this->mMetatags as $tag ) {
1606 if ( 0 == strcasecmp( "http:", substr( $tag[0], 0, 5 ) ) ) {
1607 $a = "http-equiv";
1608 $tag[0] = substr( $tag[0], 5 );
1609 } else {
1610 $a = "name";
1612 $ret .= "<meta $a=\"{$tag[0]}\" content=\"{$tag[1]}\">\n";
1614 $p = $this->mRobotpolicy;
1615 if ( "" == $p ) { $p = "index,follow"; }
1616 $ret .= "<meta name=\"robots\" content=\"$p\">\n";
1618 if ( count( $this->mKeywords ) > 0 ) {
1619 $ret .= "<meta name=\"keywords\" content=\"" .
1620 implode( ",", $this->mKeywords ) . "\">\n";
1622 foreach ( $this->mLinktags as $tag ) {
1623 $ret .= "<link ";
1624 if ( "" != $tag[0] ) { $ret .= "rel=\"{$tag[0]}\" "; }
1625 if ( "" != $tag[1] ) { $ret .= "rev=\"{$tag[1]}\" "; }
1626 $ret .= "href=\"{$tag[2]}\">\n";
1628 $sk = $wgUser->getSkin();
1629 $ret .= $sk->getHeadScripts();
1630 $ret .= $sk->getUserStyles();
1632 $ret .= "</head>\n";
1633 return $ret;
1636 /* private */ function fillFromParserCache(){
1637 global $wgUser, $wgArticle;
1638 $hash = $wgUser->getPageRenderingHash();
1639 $pageid = intval( $wgArticle->getID() );
1640 $res = wfQuery("SELECT pc_data FROM parsercache WHERE pc_pageid = {$pageid} ".
1641 " AND pc_prefhash = '{$hash}' AND pc_expire > NOW()", DB_WRITE);
1642 $row = wfFetchObject ( $res );
1643 if( $row ){
1644 $data = unserialize( gzuncompress($row->pc_data) );
1645 $this->addHTML( $data['html'] );
1646 $this->mLanguageLinks = $data['mLanguageLinks'];
1647 $this->mCategoryLinks = $data['mCategoryLinks'];
1648 wfProfileOut( $fname );
1649 return true;
1650 } else {
1651 return false;
1655 /* private */ function saveParserCache( $text ){
1656 global $wgUser, $wgArticle;
1657 $hash = $wgUser->getPageRenderingHash();
1658 $pageid = intval( $wgArticle->getID() );
1659 $title = wfStrencode( $wgArticle->mTitle->getPrefixedDBKey() );
1660 $data = array();
1661 $data['html'] = $text;
1662 $data['mLanguageLinks'] = $this->mLanguageLinks;
1663 $data['mCategoryLinks'] = $this->mCategoryLinks;
1664 $ser = addslashes( gzcompress( serialize( $data ) ) );
1665 if( $this->mContainsOldMagic ){
1666 $expire = "1 HOUR";
1667 } else if( $this->mContainsNewMagic ){
1668 $expire = "1 DAY";
1669 } else {
1670 $expire = "7 DAY";
1673 wfQuery("REPLACE INTO parsercache (pc_prefhash,pc_pageid,pc_title,pc_data, pc_expire) ".
1674 "VALUES('{$hash}', {$pageid}, '{$title}', '{$ser}', ".
1675 "DATE_ADD(NOW(), INTERVAL {$expire}))", DB_WRITE);
1677 if( rand() % 50 == 0 ){ // more efficient to just do it sometimes
1678 $this->purgeParserCache();
1682 /* static private */ function purgeParserCache(){
1683 wfQuery("DELETE FROM parsercache WHERE pc_expire < NOW() LIMIT 250", DB_WRITE);
1686 /* static */ function parsercacheClearLinksTo( $pid ){
1687 $pid = intval( $pid );
1688 wfQuery("DELETE parsercache FROM parsercache,links ".
1689 "WHERE pc_title=links.l_from AND l_to={$pid}", DB_WRITE);
1690 wfQuery("DELETE FROM parsercache WHERE pc_pageid='{$pid}'", DB_WRITE);
1693 # $title is a prefixed db title, for example like Title->getPrefixedDBkey() returns.
1694 /* static */ function parsercacheClearBrokenLinksTo( $title ){
1695 $title = wfStrencode( $title );
1696 wfQuery("DELETE parsercache FROM parsercache,brokenlinks ".
1697 "WHERE pc_pageid=bl_from AND bl_to='{$title}'", DB_WRITE);
1700 # $pid is a page id
1701 /* static */ function parsercacheClearPage( $pid ){
1702 $pid = intval( $pid );
1703 wfQuery("DELETE FROM parsercache WHERE pc_pageid='{$pid}'", DB_WRITE);
1707 # Regex callbacks, used in OutputPage::replaceVariables
1709 # Just get rid of the dangerous stuff
1710 # Necessary because replaceVariables is called after removeHTMLtags,
1711 # and message text can come from any user
1712 function wfReplaceMsgVar( $matches ) {
1713 global $wgOut;
1714 $text = $wgOut->removeHTMLtags( wfMsg( $matches[1] ) );
1715 return $text;
1718 # Effective <nowiki></nowiki>
1719 # Not real <nowiki> because this is called after nowiki sections are processed
1720 function wfReplaceMsgnwVar( $matches ) {
1721 $text = wfEscapeWikiText( wfMsg( $matches[1] ) );
1722 return $text;