Move sendCacheControl in checkLastMod to only go when we're chickening out on a 304
[lhc/web/wiklou.git] / includes / OutputPage.php
1 <?
2 # See design.doc
3
4 if($wgUseTeX) include_once( "Math.php" );
5
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;
12
13 var $mDTopen, $mLastSection; # Used for processing DL, PRE
14 var $mLanguageLinks, $mSupressQuickbar;
15 var $mOnloadHandler;
16 var $mDoNothing;
17 var $mContainsOldMagic, $mContainsNewMagic;
18 var $mIsArticleRelated;
19
20 function OutputPage()
21 {
22 $this->mHeaders = $this->mCookies = $this->mMetatags =
23 $this->mKeywords = $this->mLinktags = array();
24 $this->mHTMLtitle = $this->mPagetitle = $this->mBodytext =
25 $this->mLastSection = $this->mRedirect = $this->mLastModified =
26 $this->mSubtitle = $this->mDebugtext = $this->mRobotpolicy =
27 $this->mOnloadHandler = "";
28 $this->mIsArticleRelated = $this->mIsarticle = $this->mPrintable = true;
29 $this->mSupressQuickbar = $this->mDTopen = $this->mPrintable = false;
30 $this->mLanguageLinks = array();
31 $this->mCategoryLinks = array() ;
32 $this->mAutonumber = 0;
33 $this->mDoNothing = false;
34 $this->mContainsOldMagic = $this->mContainsNewMagic = 0;
35 }
36
37 function addHeader( $name, $val ) { array_push( $this->mHeaders, "$name: $val" ) ; }
38 function addCookie( $name, $val ) { array_push( $this->mCookies, array( $name, $val ) ); }
39 function redirect( $url ) { $this->mRedirect = $url; }
40
41 # To add an http-equiv meta tag, precede the name with "http:"
42 function addMeta( $name, $val ) { array_push( $this->mMetatags, array( $name, $val ) ); }
43 function addKeyword( $text ) { array_push( $this->mKeywords, $text ); }
44 function addLink( $rel, $rev, $target ) { array_push( $this->mLinktags, array( $rel, $rev, $target ) ); }
45
46 # checkLastModified tells the client to use the client-cached page if
47 # possible. If sucessful, the OutputPage is disabled so that
48 # any future call to OutputPage->output() have no effect. The method
49 # returns true iff cache-ok headers was sent.
50 function checkLastModified ( $timestamp )
51 {
52 global $wgLang, $wgCachePages, $wgUser;
53 if( !$wgCachePages ) {
54 wfDebug( "CACHE DISABLED\n", false );
55 return;
56 }
57 if( preg_match( '/MSIE ([1-4]|5\.0)/', $_SERVER["HTTP_USER_AGENT"] ) ) {
58 # IE 5.0 has probs with our caching
59 wfDebug( "-- bad client, not caching\n", false );
60 return;
61 }
62 if( $wgUser->getOption( "nocache" ) ) {
63 wfDebug( "USER DISABLED CACHE\n", false );
64 return;
65 }
66
67 $lastmod = gmdate( "D, j M Y H:i:s", wfTimestamp2Unix(
68 max( $timestamp, $wgUser->mTouched ) ) ) . " GMT";
69
70 if( !empty( $_SERVER["HTTP_IF_MODIFIED_SINCE"] ) ) {
71 # IE sends sizes after the date like this:
72 # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
73 # this breaks strtotime().
74 $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] );
75 $ismodsince = wfUnix2Timestamp( strtotime( $modsince ) );
76 wfDebug( "-- client send If-Modified-Since: " . $modsince . "\n", false );
77 wfDebug( "-- we might send Last-Modified : $lastmod\n", false );
78
79 if( ($ismodsince >= $timestamp ) and $wgUser->validateCache( $ismodsince ) ) {
80 # Make sure you're in a place you can leave when you call us!
81 header( "HTTP/1.0 304 Not Modified" );
82 $this->sendCacheControl();
83 wfDebug( "CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp\n", false );
84 $this->disable();
85 return true;
86 } else {
87 wfDebug( "READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp\n", false );
88 $this->mLastModified = $lastmod;
89 }
90 } else {
91 wfDebug( "We're confused.\n", false );
92 $this->mLastModified = $lastmod;
93 }
94 }
95
96 function setRobotpolicy( $str ) { $this->mRobotpolicy = $str; }
97 function setHTMLtitle( $name ) { $this->mHTMLtitle = $name; }
98 function setPageTitle( $name ) { $this->mPagetitle = $name; }
99 function getPageTitle() { return $this->mPagetitle; }
100 function setSubtitle( $str ) { $this->mSubtitle = $str; }
101 function getSubtitle() { return $this->mSubtitle; }
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; }
108
109 function setArticleRelated( $v )
110 {
111 $this->mIsArticleRelated = $v;
112 if ( !$v ) {
113 $this->mIsarticle = false;
114 }
115 }
116 function setArticleFlag( $v ) {
117 $this->mIsarticle = $v;
118 if ( $v ) {
119 $this->mIsArticleRelated = $v;
120 }
121 }
122
123 function isArticleRelated()
124 {
125 return $this->mIsArticleRelated;
126 }
127
128 function getLanguageLinks() {
129 global $wgTitle, $wgLanguageCode;
130 global $wgDBconnection, $wgDBname;
131 return $this->mLanguageLinks;
132 }
133 function supressQuickbar() { $this->mSupressQuickbar = true; }
134 function isQuickbarSupressed() { return $this->mSupressQuickbar; }
135
136 function addHTML( $text ) { $this->mBodytext .= $text; }
137 function addHeadtext( $text ) { $this->mHeadtext .= $text; }
138 function debug( $text ) { $this->mDebugtext .= $text; }
139
140 # First pass--just handle <nowiki> sections, pass the rest off
141 # to doWikiPass2() which does all the real work.
142 #
143 function addWikiText( $text, $linestart = true )
144 {
145 global $wgUseTeX, $wgArticle, $wgUser, $action;
146 $fname = "OutputPage::addWikiText";
147 wfProfileIn( $fname );
148 $unique = "3iyZiyA7iMwg5rhxP0Dcc9oTnj8qD1jm1Sfv4";
149 $unique2 = "4LIQ9nXtiYFPCSfitVwDw7EYwQlL4GeeQ7qSO";
150 $unique3 = "fPaA8gDfdLBqzj68Yjg9Hil3qEF8JGO0uszIp";
151 $nwlist = array();
152 $nwsecs = 0;
153 $mathlist = array();
154 $mathsecs = 0;
155 $prelist = array ();
156 $presecs = 0;
157 $stripped = "";
158 $stripped2 = "";
159 $stripped3 = "";
160
161 # Replace any instances of the placeholders
162 $text = str_replace( $unique, wfHtmlEscapeFirst( $unique ), $text );
163 $text = str_replace( $unique2, wfHtmlEscapeFirst( $unique2 ), $text );
164 $text = str_replace( $unique3, wfHtmlEscapeFirst( $unique3 ), $text );
165
166 global $wgEnableParserCache;
167 $use_parser_cache =
168 $wgEnableParserCache && $action == "view" &&
169 intval($wgUser->getOption( "stubthreshold" )) == 0 &&
170 isset($wgArticle) && $wgArticle->getID() > 0;
171
172 if( $use_parser_cache ){
173 if( $this->fillFromParserCache() ){
174 wfProfileOut( $fname );
175 return;
176 }
177 }
178
179 while ( "" != $text ) {
180 $p = preg_split( "/<\\s*nowiki\\s*>/i", $text, 2 );
181 $stripped .= $p[0];
182 if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { $text = ""; }
183 else {
184 $q = preg_split( "/<\\/\\s*nowiki\\s*>/i", $p[1], 2 );
185 ++$nwsecs;
186 $nwlist[$nwsecs] = wfEscapeHTMLTagsOnly($q[0]);
187 $stripped .= $unique . $nwsecs . "s";
188 $text = $q[1];
189 }
190 }
191
192 if( $wgUseTeX ) {
193 while ( "" != $stripped ) {
194 $p = preg_split( "/<\\s*math\\s*>/i", $stripped, 2 );
195 $stripped2 .= $p[0];
196 if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { $stripped = ""; }
197 else {
198 $q = preg_split( "/<\\/\\s*math\\s*>/i", $p[1], 2 );
199 ++$mathsecs;
200 $mathlist[$mathsecs] = renderMath($q[0]);
201 $stripped2 .= $unique2 . $mathsecs . "s";
202 $stripped = $q[1];
203 }
204 }
205 } else {
206 $stripped2 = $stripped;
207 }
208
209 while ( "" != $stripped2 ) {
210 $p = preg_split( "/<\\s*pre\\s*>/i", $stripped2, 2 );
211 $stripped3 .= $p[0];
212 if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { $stripped2 = ""; }
213 else {
214 $q = preg_split( "/<\\/\\s*pre\\s*>/i", $p[1], 2 );
215 ++$presecs;
216 $prelist[$presecs] = "<pre>". wfEscapeHTMLTagsOnly($q[0]). "</pre>\n";
217 $stripped3 .= $unique3 . $presecs . "s";
218 $stripped2 = $q[1];
219 }
220 }
221
222 $text = $this->doWikiPass2( $stripped3, $linestart );
223
224 $specialChars = array("\\", "$");
225 $escapedChars = array("\\\\", "\\$");
226 for ( $i = 1; $i <= $presecs; ++$i ) {
227 $text = preg_replace( "/{$unique3}{$i}s/", str_replace( $specialChars,
228 $escapedChars, $prelist[$i] ), $text );
229 }
230
231 for ( $i = 1; $i <= $mathsecs; ++$i ) {
232 $text = preg_replace( "/{$unique2}{$i}s/", str_replace( $specialChars,
233 $escapedChars, $mathlist[$i] ), $text );
234 }
235
236 for ( $i = 1; $i <= $nwsecs; ++$i ) {
237 $text = preg_replace( "/{$unique}{$i}s/", str_replace( $specialChars,
238 $escapedChars, $nwlist[$i] ), $text );
239 }
240 $this->addHTML( $text );
241
242 if($use_parser_cache ){
243 $this->saveParserCache( $text );
244 }
245 wfProfileOut( $fname );
246 }
247
248 function sendCacheControl() {
249 global $wgUseSquid, $wgUseESI;
250 # FIXME: This header may cause trouble with some versions of Internet Explorer
251 header( "Vary: Accept-Encoding, Cookie" );
252 if( $this->mLastModified != "" ) {
253 if( $wgUseSquid && ! isset( $_COOKIE[ini_get( "session.name") ] ) ) {
254 if ( $wgUseESI ) {
255 # We'll purge the proxy cache explicitly, but require end user agents
256 # to revalidate against the proxy on each visit.
257 # Surrogate-Control controls our Squid, Cache-Control downstream caches
258 wfDebug( "** proxy caching with ESI; {$this->mLastModified} **\n", false );
259 # start with a shorter timeout for initial testing
260 # header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"');
261 header( 'Surrogate-Control: max-age=18000+18000, content="ESI/1.0"');
262 header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' );
263 } else {
264 # We'll purge the proxy cache for anons explicitly, but require end user agents
265 # to revalidate against the proxy on each visit.
266 # The Squid need to replace the Cache-Control header with
267 # Cache-Control: s-maxage=0, must-revalidate, max-age=0
268 wfDebug( "** local proxy caching; {$this->mLastModified} **\n", false );
269 # start with a shorter timeout for initial testing
270 # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
271 header( "Cache-Control: s-maxage=18000, must-revalidate, max-age=0" );
272 }
273 } else {
274 # We do want clients to cache if they can, but they *must* check for updates
275 # on revisiting the page.
276 wfDebug( "** private caching; {$this->mLastModified} **\n", false );
277 header( "Expires: -1" );
278 header( "Cache-Control: private, must-revalidate, max-age=0" );
279 }
280 header( "Last-modified: {$this->mLastModified}" );
281 } else {
282 wfDebug( "** no caching **\n", false );
283 header( "Expires: -1" );
284 header( "Cache-Control: no-cache" );
285 header( "Pragma: no-cache" );
286 header( "Last-modified: " . gmdate( "D, j M Y H:i:s" ) . " GMT" );
287 }
288 }
289
290 # Finally, all the text has been munged and accumulated into
291 # the object, let's actually output it:
292 #
293 function output()
294 {
295 global $wgUser, $wgLang, $wgDebugComments, $wgCookieExpiration;
296 global $wgInputEncoding, $wgOutputEncoding, $wgLanguageCode;
297 if( $this->mDoNothing ){
298 return;
299 }
300 $fname = "OutputPage::output";
301 wfProfileIn( $fname );
302
303 $sk = $wgUser->getSkin();
304
305 $this->sendCacheControl();
306
307 header( "Content-type: text/html; charset={$wgOutputEncoding}" );
308 header( "Content-language: {$wgLanguageCode}" );
309
310 if ( "" != $this->mRedirect ) {
311 if( substr( $this->mRedirect, 0, 4 ) != "http" ) {
312 # Standards require redirect URLs to be absolute
313 global $wgServer;
314 $this->mRedirect = $wgServer . $this->mRedirect;
315 }
316 header( "Location: {$this->mRedirect}" );
317 return;
318 }
319
320 $exp = time() + $wgCookieExpiration;
321 foreach( $this->mCookies as $name => $val ) {
322 setcookie( $name, $val, $exp, "/" );
323 }
324
325 $sk->outputPage( $this );
326 # flush();
327 }
328
329 function out( $ins )
330 {
331 global $wgInputEncoding, $wgOutputEncoding, $wgLang;
332 if ( 0 == strcmp( $wgInputEncoding, $wgOutputEncoding ) ) {
333 $outs = $ins;
334 } else {
335 $outs = $wgLang->iconv( $wgInputEncoding, $wgOutputEncoding, $ins );
336 if ( false === $outs ) { $outs = $ins; }
337 }
338 print $outs;
339 }
340
341 function setEncodings()
342 {
343 global $wgInputEncoding, $wgOutputEncoding;
344 global $wgUser, $wgLang;
345
346 $wgInputEncoding = strtolower( $wgInputEncoding );
347
348 if( $wgUser->getOption( 'altencoding' ) ) {
349 $wgLang->setAltEncoding();
350 return;
351 }
352
353 if ( empty( $_SERVER['HTTP_ACCEPT_CHARSET'] ) ) {
354 $wgOutputEncoding = strtolower( $wgOutputEncoding );
355 return;
356 }
357
358 /*
359 # This code is unused anyway!
360 # Commenting out. --bv 2003-11-15
361
362 $a = explode( ",", $_SERVER['HTTP_ACCEPT_CHARSET'] );
363 $best = 0.0;
364 $bestset = "*";
365
366 foreach ( $a as $s ) {
367 if ( preg_match( "/(.*);q=(.*)/", $s, $m ) ) {
368 $set = $m[1];
369 $q = (float)($m[2]);
370 } else {
371 $set = $s;
372 $q = 1.0;
373 }
374 if ( $q > $best ) {
375 $bestset = $set;
376 $best = $q;
377 }
378 }
379 #if ( "*" == $bestset ) { $bestset = "iso-8859-1"; }
380 if ( "*" == $bestset ) { $bestset = $wgOutputEncoding; }
381 $wgOutputEncoding = strtolower( $bestset );
382
383 # Disable for now
384 #
385 */
386 $wgOutputEncoding = $wgInputEncoding;
387 }
388
389 # Returns a HTML comment with the elapsed time since request.
390 # This method has no side effects.
391 function reportTime()
392 {
393 global $wgRequestTime;
394
395 list( $usec, $sec ) = explode( " ", microtime() );
396 $now = (float)$sec + (float)$usec;
397
398 list( $usec, $sec ) = explode( " ", $wgRequestTime );
399 $start = (float)$sec + (float)$usec;
400 $elapsed = $now - $start;
401 $com = sprintf( "<!-- Time since request: %01.2f secs. -->",
402 $elapsed );
403 return $com;
404 }
405
406 # Note: these arguments are keys into wfMsg(), not text!
407 #
408 function errorpage( $title, $msg )
409 {
410 global $wgTitle;
411
412 $this->mDebugtext .= "Original title: " .
413 $wgTitle->getPrefixedText() . "\n";
414 $this->setHTMLTitle( wfMsg( "errorpagetitle" ) );
415 $this->setPageTitle( wfMsg( $title ) );
416 $this->setRobotpolicy( "noindex,nofollow" );
417 $this->setArticleRelated( false );
418
419 $this->mBodytext = "";
420 $this->addHTML( "<p>" . wfMsg( $msg ) . "\n" );
421 $this->returnToMain( false );
422
423 $this->output();
424 wfAbruptExit();
425 }
426
427 function sysopRequired()
428 {
429 global $wgUser;
430
431 $this->setHTMLTitle( wfMsg( "errorpagetitle" ) );
432 $this->setPageTitle( wfMsg( "sysoptitle" ) );
433 $this->setRobotpolicy( "noindex,nofollow" );
434 $this->setArticleRelated( false );
435 $this->mBodytext = "";
436
437 $sk = $wgUser->getSkin();
438 $ap = $sk->makeKnownLink( wfMsg( "administrators" ), "" );
439 $this->addHTML( wfMsg( "sysoptext", $ap ) );
440 $this->returnToMain();
441 }
442
443 function developerRequired()
444 {
445 global $wgUser;
446
447 $this->setHTMLTitle( wfMsg( "errorpagetitle" ) );
448 $this->setPageTitle( wfMsg( "developertitle" ) );
449 $this->setRobotpolicy( "noindex,nofollow" );
450 $this->setArticleRelated( false );
451 $this->mBodytext = "";
452
453 $sk = $wgUser->getSkin();
454 $ap = $sk->makeKnownLink( wfMsg( "administrators" ), "" );
455 $this->addHTML( wfMsg( "developertext", $ap ) );
456 $this->returnToMain();
457 }
458
459 function databaseError( $fname )
460 {
461 global $wgUser, $wgCommandLineMode;
462
463 $this->setPageTitle( wfMsgNoDB( "databaseerror" ) );
464 $this->setRobotpolicy( "noindex,nofollow" );
465 $this->setArticleRelated( false );
466
467 if ( $wgCommandLineMode ) {
468 $msg = wfMsgNoDB( "dberrortextcl" );
469 } else {
470 $msg = wfMsgNoDB( "dberrortext" );
471 }
472
473 $msg = str_replace( "$1", htmlspecialchars( wfLastDBquery() ), $msg );
474 $msg = str_replace( "$2", htmlspecialchars( $fname ), $msg );
475 $msg = str_replace( "$3", wfLastErrno(), $msg );
476 $msg = str_replace( "$4", htmlspecialchars( wfLastError() ), $msg );
477
478 if ( $wgCommandLineMode || !is_object( $wgUser )) {
479 print "$msg\n";
480 wfAbruptExit();
481 }
482 $sk = $wgUser->getSkin();
483 $shlink = $sk->makeKnownLink( wfMsgNoDB( "searchhelppage" ),
484 wfMsgNoDB( "searchingwikipedia" ) );
485 $msg = str_replace( "$5", $shlink, $msg );
486 $this->mBodytext = $msg;
487 $this->output();
488 wfAbruptExit();
489 }
490
491 function readOnlyPage( $source = "", $protected = false )
492 {
493 global $wgUser, $wgReadOnlyFile;
494
495 $this->setRobotpolicy( "noindex,nofollow" );
496 $this->setArticleRelated( false );
497
498 if( $protected ) {
499 $this->setPageTitle( wfMsg( "viewsource" ) );
500 $this->addWikiText( wfMsg( "protectedtext" ) );
501 } else {
502 $this->setPageTitle( wfMsg( "readonly" ) );
503 $reason = file_get_contents( $wgReadOnlyFile );
504 $this->addHTML( wfMsg( "readonlytext", $reason ) );
505 }
506
507 if($source) {
508 $rows = $wgUser->getOption( "rows" );
509 $cols = $wgUser->getOption( "cols" );
510 $text .= "</p>\n<textarea cols='$cols' rows='$rows' readonly>" .
511 htmlspecialchars( $source ) . "\n</textarea>";
512 $this->addHTML( $text );
513 }
514
515 $this->returnToMain( false );
516 }
517
518 function fatalError( $message )
519 {
520 $this->setPageTitle( wfMsg( "internalerror" ) );
521 $this->setRobotpolicy( "noindex,nofollow" );
522 $this->setArticleRelated( false );
523
524 $this->mBodytext = $message;
525 $this->output();
526 wfAbruptExit();
527 }
528
529 function unexpectedValueError( $name, $val )
530 {
531 $this->fatalError( wfMsg( "unexpected", $name, $val ) );
532 }
533
534 function fileCopyError( $old, $new )
535 {
536 $this->fatalError( wfMsg( "filecopyerror", $old, $new ) );
537 }
538
539 function fileRenameError( $old, $new )
540 {
541 $this->fatalError( wfMsg( "filerenameerror", $old, $new ) );
542 }
543
544 function fileDeleteError( $name )
545 {
546 $this->fatalError( wfMsg( "filedeleteerror", $name ) );
547 }
548
549 function fileNotFoundError( $name )
550 {
551 $this->fatalError( wfMsg( "filenotfound", $name ) );
552 }
553
554 function returnToMain( $auto = true )
555 {
556 global $wgUser, $wgOut, $returnto;
557
558 $sk = $wgUser->getSkin();
559 if ( "" == $returnto ) {
560 $returnto = wfMsg( "mainpage" );
561 }
562 $link = $sk->makeKnownLink( $returnto, "" );
563
564 $r = wfMsg( "returnto", $link );
565 if ( $auto ) {
566 $wgOut->addMeta( "http:Refresh", "10;url=" .
567 wfLocalUrlE( wfUrlencode( $returnto ) ) );
568 }
569 $wgOut->addHTML( "\n<p>$r\n" );
570 }
571
572
573 function categoryMagic ()
574 {
575 global $wgTitle , $wgUseCategoryMagic ;
576 if ( !isset ( $wgUseCategoryMagic ) || !$wgUseCategoryMagic ) return ;
577 $id = $wgTitle->getArticleID() ;
578 $cat = ucfirst ( wfMsg ( "category" ) ) ;
579 $ti = $wgTitle->getText() ;
580 $ti = explode ( ":" , $ti , 2 ) ;
581 if ( $cat != $ti[0] ) return "" ;
582 $r = "<br break=all>\n" ;
583
584 $articles = array() ;
585 $parents = array () ;
586 $children = array() ;
587
588
589 global $wgUser ;
590 $sk = $wgUser->getSkin() ;
591 $sql = "SELECT l_from FROM links WHERE l_to={$id}" ;
592 $res = wfQuery ( $sql, DB_READ ) ;
593 while ( $x = wfFetchObject ( $res ) )
594 {
595 # $t = new Title ;
596 # $t->newFromDBkey ( $x->l_from ) ;
597 # $t = $t->getText() ;
598 $t = $x->l_from ;
599 $y = explode ( ":" , $t , 2 ) ;
600 if ( count ( $y ) == 2 && $y[0] == $cat ) {
601 array_push ( $children , $sk->makeLink ( $t , $y[1] ) ) ;
602 } else {
603 array_push ( $articles , $sk->makeLink ( $t ) ) ;
604 }
605 }
606 wfFreeResult ( $res ) ;
607
608 # Children
609 if ( count ( $children ) > 0 )
610 {
611 asort ( $children ) ;
612 $r .= "<h2>".wfMsg("subcategories")."</h2>\n" ;
613 $r .= implode ( ", " , $children ) ;
614 }
615
616 # Articles
617 if ( count ( $articles ) > 0 )
618 {
619 asort ( $articles ) ;
620 $h = wfMsg( "category_header", $ti[1] );
621 $r .= "<h2>{$h}</h2>\n" ;
622 $r .= implode ( ", " , $articles ) ;
623 }
624
625
626 return $r ;
627 }
628
629 function getHTMLattrs ()
630 {
631 $htmlattrs = array( # Allowed attributes--no scripting, etc.
632 "title", "align", "lang", "dir", "width", "height",
633 "bgcolor", "clear", /* BR */ "noshade", /* HR */
634 "cite", /* BLOCKQUOTE, Q */ "size", "face", "color",
635 /* FONT */ "type", "start", "value", "compact",
636 /* For various lists, mostly deprecated but safe */
637 "summary", "width", "border", "frame", "rules",
638 "cellspacing", "cellpadding", "valign", "char",
639 "charoff", "colgroup", "col", "span", "abbr", "axis",
640 "headers", "scope", "rowspan", "colspan", /* Tables */
641 "id", "class", "name", "style" /* For CSS */
642 );
643 return $htmlattrs ;
644 }
645
646 function fixTagAttributes ( $t )
647 {
648 if ( trim ( $t ) == "" ) return "" ; # Saves runtime ;-)
649 $htmlattrs = $this->getHTMLattrs() ;
650
651 # Strip non-approved attributes from the tag
652 $t = preg_replace(
653 "/(\\w+)(\\s*=\\s*([^\\s\">]+|\"[^\">]*\"))?/e",
654 "(in_array(strtolower(\"\$1\"),\$htmlattrs)?(\"\$1\".((\"x\$3\" != \"x\")?\"=\$3\":'')):'')",
655 $t);
656 # Strip javascript "expression" from stylesheets. Brute force approach:
657 # If anythin offensive is found, all attributes of the HTML tag are dropped
658
659 if( preg_match(
660 "/style\\s*=.*(expression|tps*:\/\/|url\\s*\().*/is",
661 wfMungeToUtf8( $t ) ) )
662 {
663 $t="";
664 }
665
666 return trim ( $t ) ;
667 }
668
669 function doTableStuff ( $t )
670 {
671 $t = explode ( "\n" , $t ) ;
672 $td = array () ; # Is currently a td tag open?
673 $ltd = array () ; # Was it TD or TH?
674 $tr = array () ; # Is currently a tr tag open?
675 $ltr = array () ; # tr attributes
676 foreach ( $t AS $k => $x )
677 {
678 $x = rtrim ( $x ) ;
679 $fc = substr ( $x , 0 , 1 ) ;
680 if ( "{|" == substr ( $x , 0 , 2 ) )
681 {
682 $t[$k] = "<table " . $this->fixTagAttributes ( substr ( $x , 3 ) ) . ">" ;
683 array_push ( $td , false ) ;
684 array_push ( $ltd , "" ) ;
685 array_push ( $tr , false ) ;
686 array_push ( $ltr , "" ) ;
687 }
688 else if ( count ( $td ) == 0 ) { } # Don't do any of the following
689 else if ( "|}" == substr ( $x , 0 , 2 ) )
690 {
691 $z = "</table>\n" ;
692 $l = array_pop ( $ltd ) ;
693 if ( array_pop ( $tr ) ) $z = "</tr>" . $z ;
694 if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
695 array_pop ( $ltr ) ;
696 $t[$k] = $z ;
697 }
698 /* else if ( "|_" == substr ( $x , 0 , 2 ) ) # Caption
699 {
700 $z = trim ( substr ( $x , 2 ) ) ;
701 $t[$k] = "<caption>{$z}</caption>\n" ;
702 }*/
703 else if ( "|-" == substr ( $x , 0 , 2 ) ) # Allows for |---------------
704 {
705 $x = substr ( $x , 1 ) ;
706 while ( $x != "" && substr ( $x , 0 , 1 ) == '-' ) $x = substr ( $x , 1 ) ;
707 $z = "" ;
708 $l = array_pop ( $ltd ) ;
709 if ( array_pop ( $tr ) ) $z = "</tr>" . $z ;
710 if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
711 array_pop ( $ltr ) ;
712 $t[$k] = $z ;
713 array_push ( $tr , false ) ;
714 array_push ( $td , false ) ;
715 array_push ( $ltd , "" ) ;
716 array_push ( $ltr , $this->fixTagAttributes ( $x ) ) ;
717 }
718 else if ( "|" == $fc || "!" == $fc || "|+" == substr ( $x , 0 , 2 ) ) # Caption
719 {
720 if ( "|+" == substr ( $x , 0 , 2 ) )
721 {
722 $fc = "+" ;
723 $x = substr ( $x , 1 ) ;
724 }
725 $after = substr ( $x , 1 ) ;
726 if ( $fc == "!" ) $after = str_replace ( "!!" , "||" , $after ) ;
727 $after = explode ( "||" , $after ) ;
728 $t[$k] = "" ;
729 foreach ( $after AS $theline )
730 {
731 $z = "" ;
732 $tra = array_pop ( $ltr ) ;
733 if ( !array_pop ( $tr ) ) $z = "<tr {$tra}>\n" ;
734 array_push ( $tr , true ) ;
735 array_push ( $ltr , "" ) ;
736
737 $l = array_pop ( $ltd ) ;
738 if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
739 if ( $fc == "|" ) $l = "TD" ;
740 else if ( $fc == "!" ) $l = "TH" ;
741 else if ( $fc == "+" ) $l = "CAPTION" ;
742 else $l = "" ;
743 array_push ( $ltd , $l ) ;
744 $y = explode ( "|" , $theline , 2 ) ;
745 if ( count ( $y ) == 1 ) $y = "{$z}<{$l}>{$y[0]}" ;
746 else $y = $y = "{$z}<{$l} ".$this->fixTagAttributes($y[0]).">{$y[1]}" ;
747 $t[$k] .= $y ;
748 array_push ( $td , true ) ;
749 }
750 }
751 }
752
753 # Closing open td, tr && table
754 while ( count ( $td ) > 0 )
755 {
756 if ( array_pop ( $td ) ) $t[] = "</td>" ;
757 if ( array_pop ( $tr ) ) $t[] = "</tr>" ;
758 $t[] = "</table>" ;
759 }
760
761 $t = implode ( "\n" , $t ) ;
762 # $t = $this->removeHTMLtags( $t );
763 return $t ;
764 }
765
766 # Well, OK, it's actually about 14 passes. But since all the
767 # hard lifting is done inside PHP's regex code, it probably
768 # wouldn't speed things up much to add a real parser.
769 #
770 function doWikiPass2( $text, $linestart )
771 {
772 global $wgUser, $wgLang, $wgUseDynamicDates;
773 $fname = "OutputPage::doWikiPass2";
774 wfProfileIn( $fname );
775
776 $text = $this->removeHTMLtags( $text );
777 $text = $this->replaceVariables( $text );
778
779 $text = preg_replace( "/(^|\n)-----*/", "\\1<hr>", $text );
780 $text = str_replace ( "<HR>", "<hr>", $text );
781
782 $text = $this->doAllQuotes( $text );
783 $text = $this->doHeadings( $text );
784 $text = $this->doBlockLevels( $text, $linestart );
785
786 if($wgUseDynamicDates) {
787 global $wgDateFormatter;
788 $text = $wgDateFormatter->reformat( $wgUser->getOption("date"), $text );
789 }
790
791 $text = $this->replaceExternalLinks( $text );
792 $text = $this->replaceInternalLinks ( $text );
793 $text = $this->doTableStuff ( $text ) ;
794
795 $text = $this->magicISBN( $text );
796 $text = $this->magicRFC( $text );
797 $text = $this->formatHeadings( $text );
798
799 $sk = $wgUser->getSkin();
800 $text = $sk->transformContent( $text );
801 $text .= $this->categoryMagic () ;
802
803 wfProfileOut( $fname );
804 return $text;
805 }
806
807 /* private */ function doAllQuotes( $text )
808 {
809 $outtext = "";
810 $lines = explode( "\r\n", $text );
811 foreach ( $lines as $line ) {
812 $outtext .= $this->doQuotes ( "", $line, "" ) . "\r\n";
813 }
814 return $outtext;
815 }
816
817 /* private */ function doQuotes( $pre, $text, $mode )
818 {
819 if ( preg_match( "/^(.*)''(.*)$/sU", $text, $m ) ) {
820 $m1_strong = ($m[1] == "") ? "" : "<strong>{$m[1]}</strong>";
821 $m1_em = ($m[1] == "") ? "" : "<em>{$m[1]}</em>";
822 if ( substr ($m[2], 0, 1) == "'" ) {
823 $m[2] = substr ($m[2], 1);
824 if ($mode == "em") {
825 return $this->doQuotes ( $m[1], $m[2], ($m[1] == "") ? "both" : "emstrong" );
826 } else if ($mode == "strong") {
827 return $m1_strong . $this->doQuotes ( "", $m[2], "" );
828 } else if (($mode == "emstrong") || ($mode == "both")) {
829 return $this->doQuotes ( "", $pre.$m1_strong.$m[2], "em" );
830 } else if ($mode == "strongem") {
831 return "<strong>{$pre}{$m1_em}</strong>" . $this->doQuotes ( "", $m[2], "em" );
832 } else {
833 return $m[1] . $this->doQuotes ( "", $m[2], "strong" );
834 }
835 } else {
836 if ($mode == "strong") {
837 return $this->doQuotes ( $m[1], $m[2], ($m[1] == "") ? "both" : "strongem" );
838 } else if ($mode == "em") {
839 return $m1_em . $this->doQuotes ( "", $m[2], "" );
840 } else if ($mode == "emstrong") {
841 return "<em>{$pre}{$m1_strong}</em>" . $this->doQuotes ( "", $m[2], "strong" );
842 } else if (($mode == "strongem") || ($mode == "both")) {
843 return $this->doQuotes ( "", $pre.$m1_em.$m[2], "strong" );
844 } else {
845 return $m[1] . $this->doQuotes ( "", $m[2], "em" );
846 }
847 }
848 } else {
849 $text_strong = ($text == "") ? "" : "<strong>{$text}</strong>";
850 $text_em = ($text == "") ? "" : "<em>{$text}</em>";
851 if ($mode == "") {
852 return $pre . $text;
853 } else if ($mode == "em") {
854 return $pre . $text_em;
855 } else if ($mode == "strong") {
856 return $pre . $text_strong;
857 } else if ($mode == "strongem") {
858 return (($pre == "") && ($text == "")) ? "" : "<strong>{$pre}{$text_em}</strong>";
859 } else {
860 return (($pre == "") && ($text == "")) ? "" : "<em>{$pre}{$text_strong}</em>";
861 }
862 }
863 }
864
865 /* private */ function doHeadings( $text )
866 {
867 for ( $i = 6; $i >= 1; --$i ) {
868 $h = substr( "======", 0, $i );
869 $text = preg_replace( "/^{$h}([^=]+){$h}(\\s|$)/m",
870 "<h{$i}>\\1</h{$i}>\\2", $text );
871 }
872 return $text;
873 }
874
875 # Note: we have to do external links before the internal ones,
876 # and otherwise take great care in the order of things here, so
877 # that we don't end up interpreting some URLs twice.
878
879 /* private */ function replaceExternalLinks( $text )
880 {
881 $fname = "OutputPage::replaceExternalLinks";
882 wfProfileIn( $fname );
883 $text = $this->subReplaceExternalLinks( $text, "http", true );
884 $text = $this->subReplaceExternalLinks( $text, "https", true );
885 $text = $this->subReplaceExternalLinks( $text, "ftp", false );
886 $text = $this->subReplaceExternalLinks( $text, "irc", false );
887 $text = $this->subReplaceExternalLinks( $text, "gopher", false );
888 $text = $this->subReplaceExternalLinks( $text, "news", false );
889 $text = $this->subReplaceExternalLinks( $text, "mailto", false );
890 wfProfileOut( $fname );
891 return $text;
892 }
893
894 /* private */ function subReplaceExternalLinks( $s, $protocol, $autonumber )
895 {
896 global $wgUser, $printable;
897 global $wgAllowExternalImages;
898
899
900 $unique = "4jzAfzB8hNvf4sqyO9Edd8pSmk9rE2in0Tgw3";
901 $uc = "A-Za-z0-9_\\/~%\\-+&*#?!=()@\\x80-\\xFF";
902
903 # this is the list of separators that should be ignored if they
904 # are the last character of an URL but that should be included
905 # if they occur within the URL, e.g. "go to www.foo.com, where .."
906 # in this case, the last comma should not become part of the URL,
907 # but in "www.foo.com/123,2342,32.htm" it should.
908 $sep = ",;\.:";
909 $fnc = "A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF";
910 $images = "gif|png|jpg|jpeg";
911
912 # PLEASE NOTE: The curly braces { } are not part of the regex,
913 # they are interpreted as part of the string (used to tell PHP
914 # that the content of the string should be inserted there).
915 $e1 = "/(^|[^\\[])({$protocol}:)([{$uc}{$sep}]+)\\/([{$fnc}]+)\\." .
916 "((?i){$images})([^{$uc}]|$)/";
917
918 $e2 = "/(^|[^\\[])({$protocol}:)(([".$uc."]|[".$sep."][".$uc."])+)([^". $uc . $sep. "]|[".$sep."]|$)/";
919 $sk = $wgUser->getSkin();
920
921 if ( $autonumber and $wgAllowExternalImages) { # Use img tags only for HTTP urls
922 $s = preg_replace( $e1, "\\1" . $sk->makeImage( "{$unique}:\\3" .
923 "/\\4.\\5", "\\4.\\5" ) . "\\6", $s );
924 }
925 $s = preg_replace( $e2, "\\1" . "<a href=\"{$unique}:\\3\"" .
926 $sk->getExternalLinkAttributes( "{$unique}:\\3", wfEscapeHTML(
927 "{$unique}:\\3" ) ) . ">" . wfEscapeHTML( "{$unique}:\\3" ) .
928 "</a>\\5", $s );
929 $s = str_replace( $unique, $protocol, $s );
930
931 $a = explode( "[{$protocol}:", " " . $s );
932 $s = array_shift( $a );
933 $s = substr( $s, 1 );
934
935 $e1 = "/^([{$uc}"."{$sep}]+)](.*)\$/sD";
936 $e2 = "/^([{$uc}"."{$sep}]+)\\s+([^\\]]+)](.*)\$/sD";
937
938 foreach ( $a as $line ) {
939 if ( preg_match( $e1, $line, $m ) ) {
940 $link = "{$protocol}:{$m[1]}";
941 $trail = $m[2];
942 if ( $autonumber ) { $text = "[" . ++$this->mAutonumber . "]"; }
943 else { $text = wfEscapeHTML( $link ); }
944 } else if ( preg_match( $e2, $line, $m ) ) {
945 $link = "{$protocol}:{$m[1]}";
946 $text = $m[2];
947 $trail = $m[3];
948 } else {
949 $s .= "[{$protocol}:" . $line;
950 continue;
951 }
952 if ( $printable == "yes") $paren = " (<i>" . htmlspecialchars ( $link ) . "</i>)";
953 else $paren = "";
954 $la = $sk->getExternalLinkAttributes( $link, $text );
955 $s .= "<a href='{$link}'{$la}>{$text}</a>{$paren}{$trail}";
956
957 }
958 return $s;
959 }
960
961 /* private */ function replaceInternalLinks( $s )
962 {
963 global $wgTitle, $wgUser, $wgLang;
964 global $wgLinkCache, $wgInterwikiMagic, $wgUseCategoryMagic;
965 global $wgNamespacesWithSubpages, $wgLanguageCode;
966 wfProfileIn( $fname = "OutputPage::replaceInternalLinks" );
967
968 wfProfileIn( "$fname-setup" );
969 $tc = Title::legalChars() . "#";
970 $sk = $wgUser->getSkin();
971
972 $a = explode( "[[", " " . $s );
973 $s = array_shift( $a );
974 $s = substr( $s, 1 );
975
976 $e1 = "/^([{$tc}]+)(?:\\|([^]]+))?]](.*)\$/sD";
977
978 # Special and Media are pseudo-namespaces; no pages actually exist in them
979 $image = Namespace::getImage();
980 $special = Namespace::getSpecial();
981 $media = Namespace::getMedia();
982 $nottalk = !Namespace::isTalk( $wgTitle->getNamespace() );
983 wfProfileOut( "$fname-setup" );
984
985 foreach ( $a as $line ) {
986 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
987 $text = $m[2];
988 $trail = $m[3];
989 } else { # Invalid form; output directly
990 $s .= "[[" . $line ;
991 continue;
992 }
993
994 /* Valid link forms:
995 Foobar -- normal
996 :Foobar -- override special treatment of prefix (images, language links)
997 /Foobar -- convert to CurrentPage/Foobar
998 /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
999 */
1000 $c = substr($m[1],0,1);
1001 $noforce = ($c != ":");
1002 if( $c == "/" ) { # subpage
1003 if(substr($m[1],-1,1)=="/") { # / at end means we don't want the slash to be shown
1004 $m[1]=substr($m[1],1,strlen($m[1])-2);
1005 $noslash=$m[1];
1006 } else {
1007 $noslash=substr($m[1],1);
1008 }
1009 if($wgNamespacesWithSubpages[$wgTitle->getNamespace()]) { # subpages allowed here
1010 $link = $wgTitle->getPrefixedText(). "/" . trim($noslash);
1011 if( "" == $text ) {
1012 $text= $m[1];
1013 } # this might be changed for ugliness reasons
1014 } else {
1015 $link = $noslash; # no subpage allowed, use standard link
1016 }
1017 } elseif( $noforce ) { # no subpage
1018 $link = $m[1];
1019 } else {
1020 $link = substr( $m[1], 1 );
1021 }
1022 if( "" == $text )
1023 $text = $link;
1024
1025 $nt = Title::newFromText( $link );
1026 if( !$nt ) {
1027 $s .= "[[" . $line;
1028 continue;
1029 }
1030 $ns = $nt->getNamespace();
1031 $iw = $nt->getInterWiki();
1032 if( $noforce ) {
1033 if( $iw && $wgInterwikiMagic && $nottalk && $wgLang->getLanguageName( $iw ) ) {
1034 array_push( $this->mLanguageLinks, $nt->getPrefixedText() );
1035 $s .= $trail;
1036 /* CHECK MERGE @@@
1037 } else if ( "media" == $pre ) {
1038 $nt = Title::newFromText( $suf );
1039 $name = $nt->getDBkey();
1040 if ( "" == $text ) { $text = $nt->GetText(); }
1041
1042 $wgLinkCache->addImageLink( $name );
1043 $s .= $sk->makeMediaLink( $name,
1044 wfImageUrl( $name ), $text );
1045 $s .= $trail;
1046 } else if ( isset($wgUseCategoryMagic) && $wgUseCategoryMagic && $pre == wfMsg ( "category" ) ) {
1047 $l = $sk->makeLink ( $pre.":".ucfirst( $m[2] ), ucfirst ( $m[2] ) ) ;
1048 array_push ( $this->mCategoryLinks , $l ) ;
1049 $s .= $trail ;
1050 } else {
1051 $l = $wgLang->getLanguageName( $pre );
1052 if ( "" == $l or !$wgInterwikiMagic or Namespace::isTalk( $wgTitle->getNamespace() ) ) {
1053 if ( "" == $text ) {
1054 $text = $link;
1055 }
1056 $s .= $sk->makeLink( $link, $text, "", $trail );
1057 } else if ( $pre != $wgLanguageCode ) {
1058 array_push( $this->mLanguageLinks, "$pre:$suf" );
1059 $s .= $trail;
1060 }
1061 */
1062 continue;
1063 }
1064 if( $ns == $image ) {
1065 $s .= $sk->makeImageLinkObj( $nt, $text ) . $trail;
1066 $wgLinkCache->addImageLinkObj( $nt );
1067 continue;
1068 }
1069 /* CHECK MERGE @@@
1070 # } else if ( 0 == strcmp( "##", substr( $link, 0, 2 ) ) ) {
1071 # $link = substr( $link, 2 );
1072 # $s .= "<a name=\"{$link}\">{$text}</a>{$trail}";
1073 } else {
1074 if ( "" == $text ) { $text = $link; }
1075 # Hotspot:
1076 $s .= $sk->makeLink( $link, $text, "", $trail );
1077 */
1078 }
1079 if( $ns == $media ) {
1080 $s .= $sk->makeMediaLinkObj( $nt, $text ) . $trail;
1081 $wgLinkCache->addImageLinkObj( $nt );
1082 continue;
1083 } elseif( $ns == $special ) {
1084 $s .= $sk->makeKnownLinkObj( $nt, $text, "", $trail );
1085 continue;
1086 }
1087 $s .= $sk->makeLinkObj( $nt, $text, "", $trail );
1088 }
1089 wfProfileOut( $fname );
1090 return $s;
1091 }
1092
1093 # Some functions here used by doBlockLevels()
1094 #
1095 /* private */ function closeParagraph()
1096 {
1097 $result = "";
1098 if ( 0 != strcmp( "p", $this->mLastSection ) &&
1099 0 != strcmp( "", $this->mLastSection ) ) {
1100 $result = "</" . $this->mLastSection . ">";
1101 }
1102 $this->mLastSection = "";
1103 return $result."\n";
1104 }
1105 # getCommon() returns the length of the longest common substring
1106 # of both arguments, starting at the beginning of both.
1107 #
1108 /* private */ function getCommon( $st1, $st2 )
1109 {
1110 $fl = strlen( $st1 );
1111 $shorter = strlen( $st2 );
1112 if ( $fl < $shorter ) { $shorter = $fl; }
1113
1114 for ( $i = 0; $i < $shorter; ++$i ) {
1115 if ( $st1{$i} != $st2{$i} ) { break; }
1116 }
1117 return $i;
1118 }
1119 # These next three functions open, continue, and close the list
1120 # element appropriate to the prefix character passed into them.
1121 #
1122 /* private */ function openList( $char )
1123 {
1124 $result = $this->closeParagraph();
1125
1126 if ( "*" == $char ) { $result .= "<ul><li>"; }
1127 else if ( "#" == $char ) { $result .= "<ol><li>"; }
1128 else if ( ":" == $char ) { $result .= "<dl><dd>"; }
1129 else if ( ";" == $char ) {
1130 $result .= "<dl><dt>";
1131 $this->mDTopen = true;
1132 }
1133 else { $result = "<!-- ERR 1 -->"; }
1134
1135 return $result;
1136 }
1137
1138 /* private */ function nextItem( $char )
1139 {
1140 if ( "*" == $char || "#" == $char ) { return "</li><li>"; }
1141 else if ( ":" == $char || ";" == $char ) {
1142 $close = "</dd>";
1143 if ( $this->mDTopen ) { $close = "</dt>"; }
1144 if ( ";" == $char ) {
1145 $this->mDTopen = true;
1146 return $close . "<dt>";
1147 } else {
1148 $this->mDTopen = false;
1149 return $close . "<dd>";
1150 }
1151 }
1152 return "<!-- ERR 2 -->";
1153 }
1154
1155 /* private */function closeList( $char )
1156 {
1157 if ( "*" == $char ) { $text = "</li></ul>"; }
1158 else if ( "#" == $char ) { $text = "</li></ol>"; }
1159 else if ( ":" == $char ) {
1160 if ( $this->mDTopen ) {
1161 $this->mDTopen = false;
1162 $text = "</dt></dl>";
1163 } else {
1164 $text = "</dd></dl>";
1165 }
1166 }
1167 else { return "<!-- ERR 3 -->"; }
1168 return $text."\n";
1169 }
1170
1171 /* private */ function doBlockLevels( $text, $linestart )
1172 {
1173 $fname = "OutputPage::doBlockLevels";
1174 wfProfileIn( $fname );
1175 # Parsing through the text line by line. The main thing
1176 # happening here is handling of block-level elements p, pre,
1177 # and making lists from lines starting with * # : etc.
1178 #
1179 $a = explode( "\n", $text );
1180 $text = $lastPref = "";
1181 $this->mDTopen = $inBlockElem = false;
1182
1183 if ( ! $linestart ) { $text .= array_shift( $a ); }
1184 foreach ( $a as $t ) {
1185 if ( "" != $text ) { $text .= "\n"; }
1186
1187 $oLine = $t;
1188 $opl = strlen( $lastPref );
1189 $npl = strspn( $t, "*#:;" );
1190 $pref = substr( $t, 0, $npl );
1191 $pref2 = str_replace( ";", ":", $pref );
1192 $t = substr( $t, $npl );
1193
1194 if ( 0 != $npl && 0 == strcmp( $lastPref, $pref2 ) ) {
1195 $text .= $this->nextItem( substr( $pref, -1 ) );
1196
1197 if ( ";" == substr( $pref, -1 ) ) {
1198 $cpos = strpos( $t, ":" );
1199 if ( ! ( false === $cpos ) ) {
1200 $term = substr( $t, 0, $cpos );
1201 $text .= $term . $this->nextItem( ":" );
1202 $t = substr( $t, $cpos + 1 );
1203 }
1204 }
1205 } else if (0 != $npl || 0 != $opl) {
1206 $cpl = $this->getCommon( $pref, $lastPref );
1207
1208 while ( $cpl < $opl ) {
1209 $text .= $this->closeList( $lastPref{$opl-1} );
1210 --$opl;
1211 }
1212 if ( $npl <= $cpl && $cpl > 0 ) {
1213 $text .= $this->nextItem( $pref{$cpl-1} );
1214 }
1215 while ( $npl > $cpl ) {
1216 $char = substr( $pref, $cpl, 1 );
1217 $text .= $this->openList( $char );
1218
1219 if ( ";" == $char ) {
1220 $cpos = strpos( $t, ":" );
1221 if ( ! ( false === $cpos ) ) {
1222 $term = substr( $t, 0, $cpos );
1223 $text .= $term . $this->nextItem( ":" );
1224 $t = substr( $t, $cpos + 1 );
1225 }
1226 }
1227 ++$cpl;
1228 }
1229 $lastPref = $pref2;
1230 }
1231 if ( 0 == $npl ) { # No prefix--go to paragraph mode
1232 if ( preg_match(
1233 "/(<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6)/i", $t ) ) {
1234 $text .= $this->closeParagraph();
1235 $inBlockElem = true;
1236 }
1237 if ( ! $inBlockElem ) {
1238 if ( " " == $t{0} ) {
1239 $newSection = "pre";
1240 # $t = wfEscapeHTML( $t );
1241 }
1242 else { $newSection = "p"; }
1243
1244 if ( 0 == strcmp( "", trim( $oLine ) ) ) {
1245 $text .= $this->closeParagraph();
1246 $text .= "<" . $newSection . ">";
1247 } else if ( 0 != strcmp( $this->mLastSection,
1248 $newSection ) ) {
1249 $text .= $this->closeParagraph();
1250 if ( 0 != strcmp( "p", $newSection ) ) {
1251 $text .= "<" . $newSection . ">";
1252 }
1253 }
1254 $this->mLastSection = $newSection;
1255 }
1256 if ( $inBlockElem &&
1257 preg_match( "/(<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6)/i", $t ) ) {
1258 $inBlockElem = false;
1259 }
1260 }
1261 $text .= $t;
1262 }
1263 while ( $npl ) {
1264 $text .= $this->closeList( $pref2{$npl-1} );
1265 --$npl;
1266 }
1267 if ( "" != $this->mLastSection ) {
1268 if ( "p" != $this->mLastSection ) {
1269 $text .= "</" . $this->mLastSection . ">";
1270 }
1271 $this->mLastSection = "";
1272 }
1273 wfProfileOut( $fname );
1274 return $text;
1275 }
1276
1277 /* private */ function replaceVariables( $text )
1278 {
1279 global $wgLang, $wgCurOut;
1280 $fname = "OutputPage::replaceVariables";
1281 wfProfileIn( $fname );
1282
1283 $magic = array();
1284
1285 # Basic variables
1286 # See Language.php for the definition of each magic word
1287 # As with sigs, this uses the server's local time -- ensure
1288 # this is appropriate for your audience!
1289
1290 $magic[MAG_CURRENTMONTH] = date( "m" );
1291 $magic[MAG_CURRENTMONTHNAME] = $wgLang->getMonthName( date("n") );
1292 $magic[MAG_CURRENTMONTHNAMEGEN] = $wgLang->getMonthNameGen( date("n") );
1293 $magic[MAG_CURRENTDAY] = date("j");
1294 $magic[MAG_CURRENTDAYNAME] = $wgLang->getWeekdayName( date("w")+1 );
1295 $magic[MAG_CURRENTYEAR] = date( "Y" );
1296 $magic[MAG_CURRENTTIME] = $wgLang->time( wfTimestampNow(), false );
1297
1298 $this->mContainsOldMagic += MagicWord::replaceMultiple($magic, $text, $text);
1299
1300 $mw =& MagicWord::get( MAG_NUMBEROFARTICLES );
1301 if ( $mw->match( $text ) ) {
1302 $v = wfNumberOfArticles();
1303 $text = $mw->replace( $v, $text );
1304 if( $mw->getWasModified() ) { $this->mContainsOldMagic++; }
1305 }
1306
1307 # "Variables" with an additional parameter e.g. {{MSG:wikipedia}}
1308 # The callbacks are at the bottom of this file
1309 $wgCurOut = $this;
1310 $mw =& MagicWord::get( MAG_MSG );
1311 $text = $mw->substituteCallback( $text, "wfReplaceMsgVar" );
1312 if( $mw->getWasModified() ) { $this->mContainsNewMagic++; }
1313
1314 $mw =& MagicWord::get( MAG_MSGNW );
1315 $text = $mw->substituteCallback( $text, "wfReplaceMsgnwVar" );
1316 if( $mw->getWasModified() ) { $this->mContainsNewMagic++; }
1317
1318 wfProfileOut( $fname );
1319 return $text;
1320 }
1321
1322 # Cleans up HTML, removes dangerous tags and attributes
1323 /* private */ function removeHTMLtags( $text )
1324 {
1325 $fname = "OutputPage::removeHTMLtags";
1326 wfProfileIn( $fname );
1327 $htmlpairs = array( # Tags that must be closed
1328 "b", "i", "u", "font", "big", "small", "sub", "sup", "h1",
1329 "h2", "h3", "h4", "h5", "h6", "cite", "code", "em", "s",
1330 "strike", "strong", "tt", "var", "div", "center",
1331 "blockquote", "ol", "ul", "dl", "table", "caption", "pre",
1332 "ruby", "rt" , "rb" , "rp"
1333 );
1334 $htmlsingle = array(
1335 "br", "p", "hr", "li", "dt", "dd"
1336 );
1337 $htmlnest = array( # Tags that can be nested--??
1338 "table", "tr", "td", "th", "div", "blockquote", "ol", "ul",
1339 "dl", "font", "big", "small", "sub", "sup"
1340 );
1341 $tabletags = array( # Can only appear inside table
1342 "td", "th", "tr"
1343 );
1344
1345 $htmlsingle = array_merge( $tabletags, $htmlsingle );
1346 $htmlelements = array_merge( $htmlsingle, $htmlpairs );
1347
1348 $htmlattrs = $this->getHTMLattrs () ;
1349
1350 # Remove HTML comments
1351 $text = preg_replace( "/<!--.*-->/sU", "", $text );
1352
1353 $bits = explode( "<", $text );
1354 $text = array_shift( $bits );
1355 $tagstack = array(); $tablestack = array();
1356
1357 foreach ( $bits as $x ) {
1358 $prev = error_reporting( E_ALL & ~( E_NOTICE | E_WARNING ) );
1359 preg_match( "/^(\\/?)(\\w+)([^>]*)(\\/{0,1}>)([^<]*)$/",
1360 $x, $regs );
1361 list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs;
1362 error_reporting( $prev );
1363
1364 $badtag = 0 ;
1365 if ( in_array( $t = strtolower( $t ), $htmlelements ) ) {
1366 # Check our stack
1367 if ( $slash ) {
1368 # Closing a tag...
1369 if ( ! in_array( $t, $htmlsingle ) &&
1370 ( $ot = array_pop( $tagstack ) ) != $t ) {
1371 array_push( $tagstack, $ot );
1372 $badtag = 1;
1373 } else {
1374 if ( $t == "table" ) {
1375 $tagstack = array_pop( $tablestack );
1376 }
1377 $newparams = "";
1378 }
1379 } else {
1380 # Keep track for later
1381 if ( in_array( $t, $tabletags ) &&
1382 ! in_array( "table", $tagstack ) ) {
1383 $badtag = 1;
1384 } else if ( in_array( $t, $tagstack ) &&
1385 ! in_array ( $t , $htmlnest ) ) {
1386 $badtag = 1 ;
1387 } else if ( ! in_array( $t, $htmlsingle ) ) {
1388 if ( $t == "table" ) {
1389 array_push( $tablestack, $tagstack );
1390 $tagstack = array();
1391 }
1392 array_push( $tagstack, $t );
1393 }
1394 # Strip non-approved attributes from the tag
1395 $newparams = $this->fixTagAttributes($params);
1396
1397 }
1398 if ( ! $badtag ) {
1399 $rest = str_replace( ">", "&gt;", $rest );
1400 $text .= "<$slash$t $newparams$brace$rest";
1401 continue;
1402 }
1403 }
1404 $text .= "&lt;" . str_replace( ">", "&gt;", $x);
1405 }
1406 # Close off any remaining tags
1407 while ( $t = array_pop( $tagstack ) ) {
1408 $text .= "</$t>\n";
1409 if ( $t == "table" ) { $tagstack = array_pop( $tablestack ); }
1410 }
1411 wfProfileOut( $fname );
1412 return $text;
1413 }
1414
1415 /*
1416 *
1417 * This function accomplishes several tasks:
1418 * 1) Auto-number headings if that option is enabled
1419 * 2) Add an [edit] link to sections for logged in users who have enabled the option
1420 * 3) Add a Table of contents on the top for users who have enabled the option
1421 * 4) Auto-anchor headings
1422 *
1423 * It loops through all headlines, collects the necessary data, then splits up the
1424 * string and re-inserts the newly formatted headlines.
1425 *
1426 * */
1427 /* private */ function formatHeadings( $text )
1428 {
1429 global $wgUser,$wgArticle,$wgTitle,$wpPreview;
1430 $nh=$wgUser->getOption( "numberheadings" );
1431 $st=$wgUser->getOption( "showtoc" );
1432 if(!$wgTitle->userCanEdit()) {
1433 $es=0;
1434 $esr=0;
1435 } else {
1436 $es=$wgUser->getID() && $wgUser->getOption( "editsection" );
1437 $esr=$wgUser->getID() && $wgUser->getOption( "editsectiononrightclick" );
1438 }
1439
1440 # Inhibit editsection links if requested in the page
1441 $esw =& MagicWord::get( MAG_NOEDITSECTION );
1442 if ($esw->matchAndRemove( $text )) {
1443 $es=0;
1444 }
1445 # if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
1446 # do not add TOC
1447 $mw =& MagicWord::get( MAG_NOTOC );
1448 if ($mw->matchAndRemove( $text ))
1449 {
1450 $st = 0;
1451 }
1452
1453 # never add the TOC to the Main Page. This is an entry page that should not
1454 # be more than 1-2 screens large anyway
1455 if($wgTitle->getPrefixedText()==wfMsg("mainpage")) {$st=0;}
1456
1457 # We need this to perform operations on the HTML
1458 $sk=$wgUser->getSkin();
1459
1460 # Get all headlines for numbering them and adding funky stuff like [edit]
1461 # links
1462 preg_match_all("/<H([1-6])(.*?>)(.*?)<\/H[1-6]>/i",$text,$matches);
1463
1464 # headline counter
1465 $c=0;
1466
1467 # Ugh .. the TOC should have neat indentation levels which can be
1468 # passed to the skin functions. These are determined here
1469 foreach($matches[3] as $headline) {
1470 if($level) { $prevlevel=$level;}
1471 $level=$matches[1][$c];
1472 if(($nh||$st) && $prevlevel && $level>$prevlevel) {
1473
1474 $h[$level]=0; // reset when we enter a new level
1475 $toc.=$sk->tocIndent($level-$prevlevel);
1476 $toclevel+=$level-$prevlevel;
1477
1478 }
1479 if(($nh||$st) && $level<$prevlevel) {
1480 $h[$level+1]=0; // reset when we step back a level
1481 $toc.=$sk->tocUnindent($prevlevel-$level);
1482 $toclevel-=$prevlevel-$level;
1483
1484 }
1485 $h[$level]++; // count number of headlines for each level
1486
1487 if($nh||$st) {
1488 for($i=1;$i<=$level;$i++) {
1489 if($h[$i]) {
1490 if($dot) {$numbering.=".";}
1491 $numbering.=$h[$i];
1492 $dot=1;
1493 }
1494 }
1495 }
1496
1497 // The canonized header is a version of the header text safe to use for links
1498
1499 $canonized_headline=preg_replace("/<.*?>/","",$headline); // strip out HTML
1500 $tocline = trim( $canonized_headline );
1501 $canonized_headline=str_replace('"',"",$canonized_headline);
1502 $canonized_headline=str_replace(" ","_",trim($canonized_headline));
1503 $refer[$c]=$canonized_headline;
1504 $refers[$canonized_headline]++; // count how many in assoc. array so we can track dupes in anchors
1505 $refcount[$c]=$refers[$canonized_headline];
1506
1507 // Prepend the number to the heading text
1508
1509 if($nh||$st) {
1510 $tocline=$numbering ." ". $tocline;
1511
1512 // Don't number the heading if it is the only one (looks silly)
1513 if($nh && count($matches[3]) > 1) {
1514 $headline=$numbering . " " . $headline; // the two are different if the line contains a link
1515 }
1516 }
1517
1518 // Create the anchor for linking from the TOC to the section
1519
1520 $anchor=$canonized_headline;
1521 if($refcount[$c]>1) {$anchor.="_".$refcount[$c];}
1522 if($st) {
1523 $toc.=$sk->tocLine($anchor,$tocline,$toclevel);
1524 }
1525 if($es && !isset($wpPreview)) {
1526 $head[$c].=$sk->editSectionLink($c+1);
1527 }
1528
1529 // Put it all together
1530
1531 $head[$c].="<h".$level.$matches[2][$c]
1532 ."<a name=\"".$anchor."\">"
1533 .$headline
1534 ."</a>"
1535 ."</h".$level.">";
1536
1537 // Add the edit section link
1538
1539 if($esr && !isset($wpPreview)) {
1540 $head[$c]=$sk->editSectionScript($c+1,$head[$c]);
1541 }
1542
1543 $numbering="";
1544 $c++;
1545 $dot=0;
1546 }
1547
1548 if($st) {
1549 $toclines=$c;
1550 $toc.=$sk->tocUnindent($toclevel);
1551 $toc=$sk->tocTable($toc);
1552 }
1553
1554 // split up and insert constructed headlines
1555
1556 $blocks=preg_split("/<H[1-6].*?>.*?<\/H[1-6]>/i",$text);
1557 $i=0;
1558
1559 foreach($blocks as $block) {
1560 if(($es) && !isset($wpPreview) && $c>0 && $i==0) {
1561 # This is the [edit] link that appears for the top block of text when
1562 # section editing is enabled
1563 $full.=$sk->editSectionLink(0);
1564 }
1565 $full.=$block;
1566 if($st && $toclines>3 && !$i) {
1567 # Let's add a top anchor just in case we want to link to the top of the page
1568 $full="<a name=\"top\"></a>".$full.$toc;
1569 }
1570
1571 $full.=$head[$i];
1572 $i++;
1573 }
1574
1575 return $full;
1576 }
1577
1578 /* private */ function magicISBN( $text )
1579 {
1580 global $wgLang;
1581
1582 $a = split( "ISBN ", " $text" );
1583 if ( count ( $a ) < 2 ) return $text;
1584 $text = substr( array_shift( $a ), 1);
1585 $valid = "0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1586
1587 foreach ( $a as $x ) {
1588 $isbn = $blank = "" ;
1589 while ( " " == $x{0} ) {
1590 $blank .= " ";
1591 $x = substr( $x, 1 );
1592 }
1593 while ( strstr( $valid, $x{0} ) != false ) {
1594 $isbn .= $x{0};
1595 $x = substr( $x, 1 );
1596 }
1597 $num = str_replace( "-", "", $isbn );
1598 $num = str_replace( " ", "", $num );
1599
1600 if ( "" == $num ) {
1601 $text .= "ISBN $blank$x";
1602 } else {
1603 $text .= "<a href=\"" . wfLocalUrlE( $wgLang->specialPage(
1604 "Booksources"), "isbn={$num}" ) . "\" class=\"internal\">ISBN $isbn</a>";
1605 $text .= $x;
1606 }
1607 }
1608 return $text;
1609 }
1610
1611 /* private */ function magicRFC( $text )
1612 {
1613 return $text;
1614 }
1615
1616 /* private */ function headElement()
1617 {
1618 global $wgDocType, $wgDTD, $wgUser, $wgLanguageCode, $wgOutputEncoding, $wgLang;
1619
1620 $ret = "<!DOCTYPE HTML PUBLIC \"$wgDocType\"\n \"$wgDTD\">\n";
1621
1622 if ( "" == $this->mHTMLtitle ) {
1623 $this->mHTMLtitle = $this->mPagetitle;
1624 }
1625 $rtl = $wgLang->isRTL() ? " dir='RTL'" : "";
1626 $ret .= "<html lang=\"$wgLanguageCode\"$rtl><head><title>{$this->mHTMLtitle}</title>\n";
1627 array_push( $this->mMetatags, array( "http:Content-type", "text/html; charset={$wgOutputEncoding}" ) );
1628 foreach ( $this->mMetatags as $tag ) {
1629 if ( 0 == strcasecmp( "http:", substr( $tag[0], 0, 5 ) ) ) {
1630 $a = "http-equiv";
1631 $tag[0] = substr( $tag[0], 5 );
1632 } else {
1633 $a = "name";
1634 }
1635 $ret .= "<meta $a=\"{$tag[0]}\" content=\"{$tag[1]}\">\n";
1636 }
1637 $p = $this->mRobotpolicy;
1638 if ( "" == $p ) { $p = "index,follow"; }
1639 $ret .= "<meta name=\"robots\" content=\"$p\">\n";
1640
1641 if ( count( $this->mKeywords ) > 0 ) {
1642 $ret .= "<meta name=\"keywords\" content=\"" .
1643 implode( ",", $this->mKeywords ) . "\">\n";
1644 }
1645 foreach ( $this->mLinktags as $tag ) {
1646 $ret .= "<link ";
1647 if ( "" != $tag[0] ) { $ret .= "rel=\"{$tag[0]}\" "; }
1648 if ( "" != $tag[1] ) { $ret .= "rev=\"{$tag[1]}\" "; }
1649 $ret .= "href=\"{$tag[2]}\">\n";
1650 }
1651 $sk = $wgUser->getSkin();
1652 $ret .= $sk->getHeadScripts();
1653 $ret .= $sk->getUserStyles();
1654
1655 $ret .= "</head>\n";
1656 return $ret;
1657 }
1658
1659 /* private */ function fillFromParserCache(){
1660 global $wgUser, $wgArticle;
1661 $hash = $wgUser->getPageRenderingHash();
1662 $pageid = intval( $wgArticle->getID() );
1663 $res = wfQuery("SELECT pc_data FROM parsercache WHERE pc_pageid = {$pageid} ".
1664 " AND pc_prefhash = '{$hash}' AND pc_expire > NOW()", DB_WRITE);
1665 $row = wfFetchObject ( $res );
1666 if( $row ){
1667 $data = unserialize( gzuncompress($row->pc_data) );
1668 $this->addHTML( $data['html'] );
1669 $this->mLanguageLinks = $data['mLanguageLinks'];
1670 $this->mCategoryLinks = $data['mCategoryLinks'];
1671 wfProfileOut( $fname );
1672 return true;
1673 } else {
1674 return false;
1675 }
1676 }
1677
1678 /* private */ function saveParserCache( $text ){
1679 global $wgUser, $wgArticle;
1680 $hash = $wgUser->getPageRenderingHash();
1681 $pageid = intval( $wgArticle->getID() );
1682 $title = wfStrencode( $wgArticle->mTitle->getPrefixedDBKey() );
1683 $data = array();
1684 $data['html'] = $text;
1685 $data['mLanguageLinks'] = $this->mLanguageLinks;
1686 $data['mCategoryLinks'] = $this->mCategoryLinks;
1687 $ser = addslashes( gzcompress( serialize( $data ) ) );
1688 if( $this->mContainsOldMagic ){
1689 $expire = "1 HOUR";
1690 } else if( $this->mContainsNewMagic ){
1691 $expire = "1 DAY";
1692 } else {
1693 $expire = "7 DAY";
1694 }
1695
1696 wfQuery("REPLACE INTO parsercache (pc_prefhash,pc_pageid,pc_title,pc_data, pc_expire) ".
1697 "VALUES('{$hash}', {$pageid}, '{$title}', '{$ser}', ".
1698 "DATE_ADD(NOW(), INTERVAL {$expire}))", DB_WRITE);
1699
1700 if( rand() % 50 == 0 ){ // more efficient to just do it sometimes
1701 $this->purgeParserCache();
1702 }
1703 }
1704
1705 /* static private */ function purgeParserCache(){
1706 wfQuery("DELETE FROM parsercache WHERE pc_expire < NOW() LIMIT 250", DB_WRITE);
1707 }
1708
1709 /* static */ function parsercacheClearLinksTo( $pid ){
1710 $pid = intval( $pid );
1711 wfQuery("DELETE parsercache FROM parsercache,links ".
1712 "WHERE pc_title=links.l_from AND l_to={$pid}", DB_WRITE);
1713 wfQuery("DELETE FROM parsercache WHERE pc_pageid='{$pid}'", DB_WRITE);
1714 }
1715
1716 # $title is a prefixed db title, for example like Title->getPrefixedDBkey() returns.
1717 /* static */ function parsercacheClearBrokenLinksTo( $title ){
1718 $title = wfStrencode( $title );
1719 wfQuery("DELETE parsercache FROM parsercache,brokenlinks ".
1720 "WHERE pc_pageid=bl_from AND bl_to='{$title}'", DB_WRITE);
1721 }
1722
1723 # $pid is a page id
1724 /* static */ function parsercacheClearPage( $pid ){
1725 $pid = intval( $pid );
1726 wfQuery("DELETE FROM parsercache WHERE pc_pageid='{$pid}'", DB_WRITE);
1727 }
1728 }
1729
1730 # Regex callbacks, used in OutputPage::replaceVariables
1731
1732 # Just get rid of the dangerous stuff
1733 # Necessary because replaceVariables is called after removeHTMLtags,
1734 # and message text can come from any user
1735 function wfReplaceMsgVar( $matches ) {
1736 global $wgCurOut, $wgLinkCache;
1737 $text = $wgCurOut->removeHTMLtags( wfMsg( $matches[1] ) );
1738 $wgLinkCache->suspend();
1739 $text = $wgCurOut->replaceInternalLinks( $text );
1740 $wgLinkCache->resume();
1741 $wgLinkCache->addLinkObj( Title::makeTitle( NS_MEDIAWIKI, $matches[1] ) );
1742 return $text;
1743 }
1744
1745 # Effective <nowiki></nowiki>
1746 # Not real <nowiki> because this is called after nowiki sections are processed
1747 function wfReplaceMsgnwVar( $matches ) {
1748 global $wgCurOut, $wgLinkCache;
1749 $text = wfEscapeWikiText( wfMsg( $matches[1] ) );
1750 $wgLinkCache->addLinkObj( Title::makeTitle( NS_MEDIAWIKI, $matches[1] ) );
1751 return $text;
1752 }
1753
1754 ?>