[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Preparation for the final page rendering. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 */ 22 23 /** 24 * This class should be covered by a general architecture document which does 25 * not exist as of January 2011. This is one of the Core classes and should 26 * be read at least once by any new developers. 27 * 28 * This class is used to prepare the final rendering. A skin is then 29 * applied to the output parameters (links, javascript, html, categories ...). 30 * 31 * @todo FIXME: Another class handles sending the whole page to the client. 32 * 33 * Some comments comes from a pairing session between Zak Greant and Antoine Musso 34 * in November 2010. 35 * 36 * @todo document 37 */ 38 class OutputPage extends ContextSource { 39 /** @var array Should be private. Used with addMeta() which adds "<meta>" */ 40 protected $mMetatags = array(); 41 42 /** @var array */ 43 protected $mLinktags = array(); 44 45 /** @var bool */ 46 protected $mCanonicalUrl = false; 47 48 /** 49 * @var array Additional stylesheets. Looks like this is for extensions. 50 * Might be replaced by resource loader. 51 */ 52 protected $mExtStyles = array(); 53 54 /** 55 * @var string Should be private - has getter and setter. Contains 56 * the HTML title */ 57 public $mPagetitle = ''; 58 59 /** 60 * @var string Contains all of the "<body>" content. Should be private we 61 * got set/get accessors and the append() method. 62 */ 63 public $mBodytext = ''; 64 65 /** 66 * Holds the debug lines that will be output as comments in page source if 67 * $wgDebugComments is enabled. See also $wgShowDebug. 68 * @deprecated since 1.20; use MWDebug class instead. 69 */ 70 public $mDebugtext = ''; 71 72 /** @var string Stores contents of "<title>" tag */ 73 private $mHTMLtitle = ''; 74 75 /** 76 * @var bool Is the displayed content related to the source of the 77 * corresponding wiki article. 78 */ 79 private $mIsarticle = false; 80 81 /** @var bool Stores "article flag" toggle. */ 82 private $mIsArticleRelated = true; 83 84 /** 85 * @var bool We have to set isPrintable(). Some pages should 86 * never be printed (ex: redirections). 87 */ 88 private $mPrintable = false; 89 90 /** 91 * @var array Contains the page subtitle. Special pages usually have some 92 * links here. Don't confuse with site subtitle added by skins. 93 */ 94 private $mSubtitle = array(); 95 96 /** @var string */ 97 public $mRedirect = ''; 98 99 /** @var int */ 100 protected $mStatusCode; 101 102 /** 103 * @var string Variable mLastModified and mEtag are used for sending cache control. 104 * The whole caching system should probably be moved into its own class. 105 */ 106 protected $mLastModified = ''; 107 108 /** 109 * Contains an HTTP Entity Tags (see RFC 2616 section 3.13) which is used 110 * as a unique identifier for the content. It is later used by the client 111 * to compare its cached version with the server version. Client sends 112 * headers If-Match and If-None-Match containing its locally cached ETAG value. 113 * 114 * To get more information, you will have to look at HTTP/1.1 protocol which 115 * is properly described in RFC 2616 : http://tools.ietf.org/html/rfc2616 116 */ 117 private $mETag = false; 118 119 /** @var array */ 120 protected $mCategoryLinks = array(); 121 122 /** @var array */ 123 protected $mCategories = array(); 124 125 /** @var array Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page') */ 126 private $mLanguageLinks = array(); 127 128 /** 129 * Used for JavaScript (pre resource loader) 130 * @todo We should split JS / CSS. 131 * mScripts content is inserted as is in "<head>" by Skin. This might 132 * contain either a link to a stylesheet or inline CSS. 133 */ 134 private $mScripts = ''; 135 136 /** @var string Inline CSS styles. Use addInlineStyle() sparingly */ 137 protected $mInlineStyles = ''; 138 139 /** @todo Unused? */ 140 private $mLinkColours; 141 142 /** 143 * @var string Used by skin template. 144 * Example: $tpl->set( 'displaytitle', $out->mPageLinkTitle ); 145 */ 146 public $mPageLinkTitle = ''; 147 148 /** @var array Array of elements in "<head>". Parser might add its own headers! */ 149 protected $mHeadItems = array(); 150 151 // @todo FIXME: Next 5 variables probably come from the resource loader 152 153 /** @var array */ 154 protected $mModules = array(); 155 156 /** @var array */ 157 protected $mModuleScripts = array(); 158 159 /** @var array */ 160 protected $mModuleStyles = array(); 161 162 /** @var array */ 163 protected $mModuleMessages = array(); 164 165 /** @var ResourceLoader */ 166 protected $mResourceLoader; 167 168 /** @var array */ 169 protected $mJsConfigVars = array(); 170 171 /** @var array */ 172 protected $mTemplateIds = array(); 173 174 /** @var array */ 175 protected $mImageTimeKeys = array(); 176 177 /** @var string */ 178 public $mRedirectCode = ''; 179 180 protected $mFeedLinksAppendQuery = null; 181 182 /** @var array 183 * What level of 'untrustworthiness' is allowed in CSS/JS modules loaded on this page? 184 * @see ResourceLoaderModule::$origin 185 * ResourceLoaderModule::ORIGIN_ALL is assumed unless overridden; 186 */ 187 protected $mAllowedModules = array( 188 ResourceLoaderModule::TYPE_COMBINED => ResourceLoaderModule::ORIGIN_ALL, 189 ); 190 191 /** @var bool Whether output is disabled. If this is true, the 'output' method will do nothing. */ 192 protected $mDoNothing = false; 193 194 // Parser related. 195 196 /** 197 * @var int 198 * @todo Unused? 199 */ 200 private $mContainsOldMagic = 0; 201 202 /** @var int */ 203 protected $mContainsNewMagic = 0; 204 205 /** 206 * lazy initialised, use parserOptions() 207 * @var ParserOptions 208 */ 209 protected $mParserOptions = null; 210 211 /** 212 * Handles the Atom / RSS links. 213 * We probably only support Atom in 2011. 214 * @see $wgAdvertisedFeedTypes 215 */ 216 private $mFeedLinks = array(); 217 218 // Gwicke work on squid caching? Roughly from 2003. 219 protected $mEnableClientCache = true; 220 221 /** @var bool Flag if output should only contain the body of the article. */ 222 private $mArticleBodyOnly = false; 223 224 /** @var bool */ 225 protected $mNewSectionLink = false; 226 227 /** @var bool */ 228 protected $mHideNewSectionLink = false; 229 230 /** 231 * @var bool Comes from the parser. This was probably made to load CSS/JS 232 * only if we had "<gallery>". Used directly in CategoryPage.php. 233 * Looks like resource loader can replace this. 234 */ 235 public $mNoGallery = false; 236 237 /** @var string */ 238 private $mPageTitleActionText = ''; 239 240 /** @var array */ 241 private $mParseWarnings = array(); 242 243 /** @var int Cache stuff. Looks like mEnableClientCache */ 244 protected $mSquidMaxage = 0; 245 246 /** 247 * @var bool 248 * @todo Document 249 */ 250 protected $mPreventClickjacking = true; 251 252 /** @var int To include the variable {{REVISIONID}} */ 253 private $mRevisionId = null; 254 255 /** @var string */ 256 private $mRevisionTimestamp = null; 257 258 /** @var array */ 259 protected $mFileVersion = null; 260 261 /** 262 * @var array An array of stylesheet filenames (relative from skins path), 263 * with options for CSS media, IE conditions, and RTL/LTR direction. 264 * For internal use; add settings in the skin via $this->addStyle() 265 * 266 * Style again! This seems like a code duplication since we already have 267 * mStyles. This is what makes Open Source amazing. 268 */ 269 protected $styles = array(); 270 271 /** 272 * Whether jQuery is already handled. 273 */ 274 protected $mJQueryDone = false; 275 276 private $mIndexPolicy = 'index'; 277 private $mFollowPolicy = 'follow'; 278 private $mVaryHeader = array( 279 'Accept-Encoding' => array( 'list-contains=gzip' ), 280 ); 281 282 /** 283 * If the current page was reached through a redirect, $mRedirectedFrom contains the Title 284 * of the redirect. 285 * 286 * @var Title 287 */ 288 private $mRedirectedFrom = null; 289 290 /** 291 * Additional key => value data 292 */ 293 private $mProperties = array(); 294 295 /** 296 * @var string|null ResourceLoader target for load.php links. If null, will be omitted 297 */ 298 private $mTarget = null; 299 300 /** 301 * @var bool Whether parser output should contain table of contents 302 */ 303 private $mEnableTOC = true; 304 305 /** 306 * @var bool Whether parser output should contain section edit links 307 */ 308 private $mEnableSectionEditLinks = true; 309 310 /** 311 * Constructor for OutputPage. This should not be called directly. 312 * Instead a new RequestContext should be created and it will implicitly create 313 * a OutputPage tied to that context. 314 * @param IContextSource|null $context 315 */ 316 function __construct( IContextSource $context = null ) { 317 if ( $context === null ) { 318 # Extensions should use `new RequestContext` instead of `new OutputPage` now. 319 wfDeprecated( __METHOD__, '1.18' ); 320 } else { 321 $this->setContext( $context ); 322 } 323 } 324 325 /** 326 * Redirect to $url rather than displaying the normal page 327 * 328 * @param string $url URL 329 * @param string $responsecode HTTP status code 330 */ 331 public function redirect( $url, $responsecode = '302' ) { 332 # Strip newlines as a paranoia check for header injection in PHP<5.1.2 333 $this->mRedirect = str_replace( "\n", '', $url ); 334 $this->mRedirectCode = $responsecode; 335 } 336 337 /** 338 * Get the URL to redirect to, or an empty string if not redirect URL set 339 * 340 * @return string 341 */ 342 public function getRedirect() { 343 return $this->mRedirect; 344 } 345 346 /** 347 * Set the HTTP status code to send with the output. 348 * 349 * @param int $statusCode 350 */ 351 public function setStatusCode( $statusCode ) { 352 $this->mStatusCode = $statusCode; 353 } 354 355 /** 356 * Add a new "<meta>" tag 357 * To add an http-equiv meta tag, precede the name with "http:" 358 * 359 * @param string $name Tag name 360 * @param string $val Tag value 361 */ 362 function addMeta( $name, $val ) { 363 array_push( $this->mMetatags, array( $name, $val ) ); 364 } 365 366 /** 367 * Add a new \<link\> tag to the page header. 368 * 369 * Note: use setCanonicalUrl() for rel=canonical. 370 * 371 * @param array $linkarr Associative array of attributes. 372 */ 373 function addLink( array $linkarr ) { 374 array_push( $this->mLinktags, $linkarr ); 375 } 376 377 /** 378 * Add a new \<link\> with "rel" attribute set to "meta" 379 * 380 * @param array $linkarr Associative array mapping attribute names to their 381 * values, both keys and values will be escaped, and the 382 * "rel" attribute will be automatically added 383 */ 384 function addMetadataLink( array $linkarr ) { 385 $linkarr['rel'] = $this->getMetadataAttribute(); 386 $this->addLink( $linkarr ); 387 } 388 389 /** 390 * Set the URL to be used for the <link rel=canonical>. This should be used 391 * in preference to addLink(), to avoid duplicate link tags. 392 * @param string $url 393 */ 394 function setCanonicalUrl( $url ) { 395 $this->mCanonicalUrl = $url; 396 } 397 398 /** 399 * Get the value of the "rel" attribute for metadata links 400 * 401 * @return string 402 */ 403 public function getMetadataAttribute() { 404 # note: buggy CC software only reads first "meta" link 405 static $haveMeta = false; 406 if ( $haveMeta ) { 407 return 'alternate meta'; 408 } else { 409 $haveMeta = true; 410 return 'meta'; 411 } 412 } 413 414 /** 415 * Add raw HTML to the list of scripts (including \<script\> tag, etc.) 416 * 417 * @param string $script Raw HTML 418 */ 419 function addScript( $script ) { 420 $this->mScripts .= $script . "\n"; 421 } 422 423 /** 424 * Register and add a stylesheet from an extension directory. 425 * 426 * @param string $url Path to sheet. Provide either a full url (beginning 427 * with 'http', etc) or a relative path from the document root 428 * (beginning with '/'). Otherwise it behaves identically to 429 * addStyle() and draws from the /skins folder. 430 */ 431 public function addExtensionStyle( $url ) { 432 array_push( $this->mExtStyles, $url ); 433 } 434 435 /** 436 * Get all styles added by extensions 437 * 438 * @return array 439 */ 440 function getExtStyle() { 441 return $this->mExtStyles; 442 } 443 444 /** 445 * Add a JavaScript file out of skins/common, or a given relative path. 446 * 447 * @param string $file Filename in skins/common or complete on-server path 448 * (/foo/bar.js) 449 * @param string $version Style version of the file. Defaults to $wgStyleVersion 450 */ 451 public function addScriptFile( $file, $version = null ) { 452 // See if $file parameter is an absolute URL or begins with a slash 453 if ( substr( $file, 0, 1 ) == '/' || preg_match( '#^[a-z]*://#i', $file ) ) { 454 $path = $file; 455 } else { 456 $path = $this->getConfig()->get( 'StylePath' ) . "/common/{$file}"; 457 } 458 if ( is_null( $version ) ) { 459 $version = $this->getConfig()->get( 'StyleVersion' ); 460 } 461 $this->addScript( Html::linkedScript( wfAppendQuery( $path, $version ) ) ); 462 } 463 464 /** 465 * Add a self-contained script tag with the given contents 466 * 467 * @param string $script JavaScript text, no "<script>" tags 468 */ 469 public function addInlineScript( $script ) { 470 $this->mScripts .= Html::inlineScript( "\n$script\n" ) . "\n"; 471 } 472 473 /** 474 * Get all registered JS and CSS tags for the header. 475 * 476 * @return string 477 * @deprecated since 1.24 Use OutputPage::headElement to build the full header. 478 */ 479 function getScript() { 480 wfDeprecated( __METHOD__, '1.24' ); 481 return $this->mScripts . $this->getHeadItems(); 482 } 483 484 /** 485 * Filter an array of modules to remove insufficiently trustworthy members, and modules 486 * which are no longer registered (eg a page is cached before an extension is disabled) 487 * @param array $modules 488 * @param string|null $position If not null, only return modules with this position 489 * @param string $type 490 * @return array 491 */ 492 protected function filterModules( array $modules, $position = null, 493 $type = ResourceLoaderModule::TYPE_COMBINED 494 ) { 495 $resourceLoader = $this->getResourceLoader(); 496 $filteredModules = array(); 497 foreach ( $modules as $val ) { 498 $module = $resourceLoader->getModule( $val ); 499 if ( $module instanceof ResourceLoaderModule 500 && $module->getOrigin() <= $this->getAllowedModules( $type ) 501 && ( is_null( $position ) || $module->getPosition() == $position ) 502 && ( !$this->mTarget || in_array( $this->mTarget, $module->getTargets() ) ) 503 ) { 504 $filteredModules[] = $val; 505 } 506 } 507 return $filteredModules; 508 } 509 510 /** 511 * Get the list of modules to include on this page 512 * 513 * @param bool $filter Whether to filter out insufficiently trustworthy modules 514 * @param string|null $position If not null, only return modules with this position 515 * @param string $param 516 * @return array Array of module names 517 */ 518 public function getModules( $filter = false, $position = null, $param = 'mModules' ) { 519 $modules = array_values( array_unique( $this->$param ) ); 520 return $filter 521 ? $this->filterModules( $modules, $position ) 522 : $modules; 523 } 524 525 /** 526 * Add one or more modules recognized by the resource loader. Modules added 527 * through this function will be loaded by the resource loader when the 528 * page loads. 529 * 530 * @param string|array $modules Module name (string) or array of module names 531 */ 532 public function addModules( $modules ) { 533 $this->mModules = array_merge( $this->mModules, (array)$modules ); 534 } 535 536 /** 537 * Get the list of module JS to include on this page 538 * 539 * @param bool $filter 540 * @param string|null $position 541 * 542 * @return array Array of module names 543 */ 544 public function getModuleScripts( $filter = false, $position = null ) { 545 return $this->getModules( $filter, $position, 'mModuleScripts' ); 546 } 547 548 /** 549 * Add only JS of one or more modules recognized by the resource loader. Module 550 * scripts added through this function will be loaded by the resource loader when 551 * the page loads. 552 * 553 * @param string|array $modules Module name (string) or array of module names 554 */ 555 public function addModuleScripts( $modules ) { 556 $this->mModuleScripts = array_merge( $this->mModuleScripts, (array)$modules ); 557 } 558 559 /** 560 * Get the list of module CSS to include on this page 561 * 562 * @param bool $filter 563 * @param string|null $position 564 * 565 * @return array Array of module names 566 */ 567 public function getModuleStyles( $filter = false, $position = null ) { 568 return $this->getModules( $filter, $position, 'mModuleStyles' ); 569 } 570 571 /** 572 * Add only CSS of one or more modules recognized by the resource loader. 573 * 574 * Module styles added through this function will be added using standard link CSS 575 * tags, rather than as a combined Javascript and CSS package. Thus, they will 576 * load when JavaScript is disabled (unless CSS also happens to be disabled). 577 * 578 * @param string|array $modules Module name (string) or array of module names 579 */ 580 public function addModuleStyles( $modules ) { 581 $this->mModuleStyles = array_merge( $this->mModuleStyles, (array)$modules ); 582 } 583 584 /** 585 * Get the list of module messages to include on this page 586 * 587 * @param bool $filter 588 * @param string|null $position 589 * 590 * @return array Array of module names 591 */ 592 public function getModuleMessages( $filter = false, $position = null ) { 593 return $this->getModules( $filter, $position, 'mModuleMessages' ); 594 } 595 596 /** 597 * Add only messages of one or more modules recognized by the resource loader. 598 * Module messages added through this function will be loaded by the resource 599 * loader when the page loads. 600 * 601 * @param string|array $modules Module name (string) or array of module names 602 */ 603 public function addModuleMessages( $modules ) { 604 $this->mModuleMessages = array_merge( $this->mModuleMessages, (array)$modules ); 605 } 606 607 /** 608 * @return null|string ResourceLoader target 609 */ 610 public function getTarget() { 611 return $this->mTarget; 612 } 613 614 /** 615 * Sets ResourceLoader target for load.php links. If null, will be omitted 616 * 617 * @param string|null $target 618 */ 619 public function setTarget( $target ) { 620 $this->mTarget = $target; 621 } 622 623 /** 624 * Get an array of head items 625 * 626 * @return array 627 */ 628 function getHeadItemsArray() { 629 return $this->mHeadItems; 630 } 631 632 /** 633 * Get all header items in a string 634 * 635 * @return string 636 * @deprecated since 1.24 Use OutputPage::headElement or 637 * if absolutely necessary use OutputPage::getHeadItemsArray 638 */ 639 function getHeadItems() { 640 wfDeprecated( __METHOD__, '1.24' ); 641 $s = ''; 642 foreach ( $this->mHeadItems as $item ) { 643 $s .= $item; 644 } 645 return $s; 646 } 647 648 /** 649 * Add or replace an header item to the output 650 * 651 * @param string $name Item name 652 * @param string $value Raw HTML 653 */ 654 public function addHeadItem( $name, $value ) { 655 $this->mHeadItems[$name] = $value; 656 } 657 658 /** 659 * Check if the header item $name is already set 660 * 661 * @param string $name Item name 662 * @return bool 663 */ 664 public function hasHeadItem( $name ) { 665 return isset( $this->mHeadItems[$name] ); 666 } 667 668 /** 669 * Set the value of the ETag HTTP header, only used if $wgUseETag is true 670 * 671 * @param string $tag Value of "ETag" header 672 */ 673 function setETag( $tag ) { 674 $this->mETag = $tag; 675 } 676 677 /** 678 * Set whether the output should only contain the body of the article, 679 * without any skin, sidebar, etc. 680 * Used e.g. when calling with "action=render". 681 * 682 * @param bool $only Whether to output only the body of the article 683 */ 684 public function setArticleBodyOnly( $only ) { 685 $this->mArticleBodyOnly = $only; 686 } 687 688 /** 689 * Return whether the output will contain only the body of the article 690 * 691 * @return bool 692 */ 693 public function getArticleBodyOnly() { 694 return $this->mArticleBodyOnly; 695 } 696 697 /** 698 * Set an additional output property 699 * @since 1.21 700 * 701 * @param string $name 702 * @param mixed $value 703 */ 704 public function setProperty( $name, $value ) { 705 $this->mProperties[$name] = $value; 706 } 707 708 /** 709 * Get an additional output property 710 * @since 1.21 711 * 712 * @param string $name 713 * @return mixed Property value or null if not found 714 */ 715 public function getProperty( $name ) { 716 if ( isset( $this->mProperties[$name] ) ) { 717 return $this->mProperties[$name]; 718 } else { 719 return null; 720 } 721 } 722 723 /** 724 * checkLastModified tells the client to use the client-cached page if 725 * possible. If successful, the OutputPage is disabled so that 726 * any future call to OutputPage->output() have no effect. 727 * 728 * Side effect: sets mLastModified for Last-Modified header 729 * 730 * @param string $timestamp 731 * 732 * @return bool True if cache-ok headers was sent. 733 */ 734 public function checkLastModified( $timestamp ) { 735 if ( !$timestamp || $timestamp == '19700101000000' ) { 736 wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" ); 737 return false; 738 } 739 $config = $this->getConfig(); 740 if ( !$config->get( 'CachePages' ) ) { 741 wfDebug( __METHOD__ . ": CACHE DISABLED\n" ); 742 return false; 743 } 744 745 $timestamp = wfTimestamp( TS_MW, $timestamp ); 746 $modifiedTimes = array( 747 'page' => $timestamp, 748 'user' => $this->getUser()->getTouched(), 749 'epoch' => $config->get( 'CacheEpoch' ) 750 ); 751 if ( $config->get( 'UseSquid' ) ) { 752 // bug 44570: the core page itself may not change, but resources might 753 $modifiedTimes['sepoch'] = wfTimestamp( TS_MW, time() - $config->get( 'SquidMaxage' ) ); 754 } 755 wfRunHooks( 'OutputPageCheckLastModified', array( &$modifiedTimes ) ); 756 757 $maxModified = max( $modifiedTimes ); 758 $this->mLastModified = wfTimestamp( TS_RFC2822, $maxModified ); 759 760 $clientHeader = $this->getRequest()->getHeader( 'If-Modified-Since' ); 761 if ( $clientHeader === false ) { 762 wfDebug( __METHOD__ . ": client did not send If-Modified-Since header\n", 'log' ); 763 return false; 764 } 765 766 # IE sends sizes after the date like this: 767 # Wed, 20 Aug 2003 06:51:19 GMT; length=5202 768 # this breaks strtotime(). 769 $clientHeader = preg_replace( '/;.*$/', '', $clientHeader ); 770 771 wfSuppressWarnings(); // E_STRICT system time bitching 772 $clientHeaderTime = strtotime( $clientHeader ); 773 wfRestoreWarnings(); 774 if ( !$clientHeaderTime ) { 775 wfDebug( __METHOD__ 776 . ": unable to parse the client's If-Modified-Since header: $clientHeader\n" ); 777 return false; 778 } 779 $clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime ); 780 781 # Make debug info 782 $info = ''; 783 foreach ( $modifiedTimes as $name => $value ) { 784 if ( $info !== '' ) { 785 $info .= ', '; 786 } 787 $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value ); 788 } 789 790 wfDebug( __METHOD__ . ": client sent If-Modified-Since: " . 791 wfTimestamp( TS_ISO_8601, $clientHeaderTime ) . "\n", 'log' ); 792 wfDebug( __METHOD__ . ": effective Last-Modified: " . 793 wfTimestamp( TS_ISO_8601, $maxModified ) . "\n", 'log' ); 794 if ( $clientHeaderTime < $maxModified ) { 795 wfDebug( __METHOD__ . ": STALE, $info\n", 'log' ); 796 return false; 797 } 798 799 # Not modified 800 # Give a 304 response code and disable body output 801 wfDebug( __METHOD__ . ": NOT MODIFIED, $info\n", 'log' ); 802 ini_set( 'zlib.output_compression', 0 ); 803 $this->getRequest()->response()->header( "HTTP/1.1 304 Not Modified" ); 804 $this->sendCacheControl(); 805 $this->disable(); 806 807 // Don't output a compressed blob when using ob_gzhandler; 808 // it's technically against HTTP spec and seems to confuse 809 // Firefox when the response gets split over two packets. 810 wfClearOutputBuffers(); 811 812 return true; 813 } 814 815 /** 816 * Override the last modified timestamp 817 * 818 * @param string $timestamp New timestamp, in a format readable by 819 * wfTimestamp() 820 */ 821 public function setLastModified( $timestamp ) { 822 $this->mLastModified = wfTimestamp( TS_RFC2822, $timestamp ); 823 } 824 825 /** 826 * Set the robot policy for the page: <http://www.robotstxt.org/meta.html> 827 * 828 * @param string $policy The literal string to output as the contents of 829 * the meta tag. Will be parsed according to the spec and output in 830 * standardized form. 831 * @return null 832 */ 833 public function setRobotPolicy( $policy ) { 834 $policy = Article::formatRobotPolicy( $policy ); 835 836 if ( isset( $policy['index'] ) ) { 837 $this->setIndexPolicy( $policy['index'] ); 838 } 839 if ( isset( $policy['follow'] ) ) { 840 $this->setFollowPolicy( $policy['follow'] ); 841 } 842 } 843 844 /** 845 * Set the index policy for the page, but leave the follow policy un- 846 * touched. 847 * 848 * @param string $policy Either 'index' or 'noindex'. 849 * @return null 850 */ 851 public function setIndexPolicy( $policy ) { 852 $policy = trim( $policy ); 853 if ( in_array( $policy, array( 'index', 'noindex' ) ) ) { 854 $this->mIndexPolicy = $policy; 855 } 856 } 857 858 /** 859 * Set the follow policy for the page, but leave the index policy un- 860 * touched. 861 * 862 * @param string $policy Either 'follow' or 'nofollow'. 863 * @return null 864 */ 865 public function setFollowPolicy( $policy ) { 866 $policy = trim( $policy ); 867 if ( in_array( $policy, array( 'follow', 'nofollow' ) ) ) { 868 $this->mFollowPolicy = $policy; 869 } 870 } 871 872 /** 873 * Set the new value of the "action text", this will be added to the 874 * "HTML title", separated from it with " - ". 875 * 876 * @param string $text New value of the "action text" 877 */ 878 public function setPageTitleActionText( $text ) { 879 $this->mPageTitleActionText = $text; 880 } 881 882 /** 883 * Get the value of the "action text" 884 * 885 * @return string 886 */ 887 public function getPageTitleActionText() { 888 return $this->mPageTitleActionText; 889 } 890 891 /** 892 * "HTML title" means the contents of "<title>". 893 * It is stored as plain, unescaped text and will be run through htmlspecialchars in the skin file. 894 * 895 * @param string|Message $name 896 */ 897 public function setHTMLTitle( $name ) { 898 if ( $name instanceof Message ) { 899 $this->mHTMLtitle = $name->setContext( $this->getContext() )->text(); 900 } else { 901 $this->mHTMLtitle = $name; 902 } 903 } 904 905 /** 906 * Return the "HTML title", i.e. the content of the "<title>" tag. 907 * 908 * @return string 909 */ 910 public function getHTMLTitle() { 911 return $this->mHTMLtitle; 912 } 913 914 /** 915 * Set $mRedirectedFrom, the Title of the page which redirected us to the current page. 916 * 917 * @param Title $t 918 */ 919 public function setRedirectedFrom( $t ) { 920 $this->mRedirectedFrom = $t; 921 } 922 923 /** 924 * "Page title" means the contents of \<h1\>. It is stored as a valid HTML 925 * fragment. This function allows good tags like \<sup\> in the \<h1\> tag, 926 * but not bad tags like \<script\>. This function automatically sets 927 * \<title\> to the same content as \<h1\> but with all tags removed. Bad 928 * tags that were escaped in \<h1\> will still be escaped in \<title\>, and 929 * good tags like \<i\> will be dropped entirely. 930 * 931 * @param string|Message $name 932 */ 933 public function setPageTitle( $name ) { 934 if ( $name instanceof Message ) { 935 $name = $name->setContext( $this->getContext() )->text(); 936 } 937 938 # change "<script>foo&bar</script>" to "<script>foo&bar</script>" 939 # but leave "<i>foobar</i>" alone 940 $nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) ); 941 $this->mPagetitle = $nameWithTags; 942 943 # change "<i>foo&bar</i>" to "foo&bar" 944 $this->setHTMLTitle( 945 $this->msg( 'pagetitle' )->rawParams( Sanitizer::stripAllTags( $nameWithTags ) ) 946 ->inContentLanguage() 947 ); 948 } 949 950 /** 951 * Return the "page title", i.e. the content of the \<h1\> tag. 952 * 953 * @return string 954 */ 955 public function getPageTitle() { 956 return $this->mPagetitle; 957 } 958 959 /** 960 * Set the Title object to use 961 * 962 * @param Title $t 963 */ 964 public function setTitle( Title $t ) { 965 $this->getContext()->setTitle( $t ); 966 } 967 968 /** 969 * Replace the subtitle with $str 970 * 971 * @param string|Message $str New value of the subtitle. String should be safe HTML. 972 */ 973 public function setSubtitle( $str ) { 974 $this->clearSubtitle(); 975 $this->addSubtitle( $str ); 976 } 977 978 /** 979 * Add $str to the subtitle 980 * 981 * @deprecated since 1.19; use addSubtitle() instead 982 * @param string|Message $str String or Message to add to the subtitle 983 */ 984 public function appendSubtitle( $str ) { 985 $this->addSubtitle( $str ); 986 } 987 988 /** 989 * Add $str to the subtitle 990 * 991 * @param string|Message $str String or Message to add to the subtitle. String should be safe HTML. 992 */ 993 public function addSubtitle( $str ) { 994 if ( $str instanceof Message ) { 995 $this->mSubtitle[] = $str->setContext( $this->getContext() )->parse(); 996 } else { 997 $this->mSubtitle[] = $str; 998 } 999 } 1000 1001 /** 1002 * Add a subtitle containing a backlink to a page 1003 * 1004 * @param Title $title Title to link to 1005 * @param array $query Array of additional parameters to include in the link 1006 */ 1007 public function addBacklinkSubtitle( Title $title, $query = array() ) { 1008 if ( $title->isRedirect() ) { 1009 $query['redirect'] = 'no'; 1010 } 1011 $this->addSubtitle( $this->msg( 'backlinksubtitle' ) 1012 ->rawParams( Linker::link( $title, null, array(), $query ) ) ); 1013 } 1014 1015 /** 1016 * Clear the subtitles 1017 */ 1018 public function clearSubtitle() { 1019 $this->mSubtitle = array(); 1020 } 1021 1022 /** 1023 * Get the subtitle 1024 * 1025 * @return string 1026 */ 1027 public function getSubtitle() { 1028 return implode( "<br />\n\t\t\t\t", $this->mSubtitle ); 1029 } 1030 1031 /** 1032 * Set the page as printable, i.e. it'll be displayed with with all 1033 * print styles included 1034 */ 1035 public function setPrintable() { 1036 $this->mPrintable = true; 1037 } 1038 1039 /** 1040 * Return whether the page is "printable" 1041 * 1042 * @return bool 1043 */ 1044 public function isPrintable() { 1045 return $this->mPrintable; 1046 } 1047 1048 /** 1049 * Disable output completely, i.e. calling output() will have no effect 1050 */ 1051 public function disable() { 1052 $this->mDoNothing = true; 1053 } 1054 1055 /** 1056 * Return whether the output will be completely disabled 1057 * 1058 * @return bool 1059 */ 1060 public function isDisabled() { 1061 return $this->mDoNothing; 1062 } 1063 1064 /** 1065 * Show an "add new section" link? 1066 * 1067 * @return bool 1068 */ 1069 public function showNewSectionLink() { 1070 return $this->mNewSectionLink; 1071 } 1072 1073 /** 1074 * Forcibly hide the new section link? 1075 * 1076 * @return bool 1077 */ 1078 public function forceHideNewSectionLink() { 1079 return $this->mHideNewSectionLink; 1080 } 1081 1082 /** 1083 * Add or remove feed links in the page header 1084 * This is mainly kept for backward compatibility, see OutputPage::addFeedLink() 1085 * for the new version 1086 * @see addFeedLink() 1087 * 1088 * @param bool $show True: add default feeds, false: remove all feeds 1089 */ 1090 public function setSyndicated( $show = true ) { 1091 if ( $show ) { 1092 $this->setFeedAppendQuery( false ); 1093 } else { 1094 $this->mFeedLinks = array(); 1095 } 1096 } 1097 1098 /** 1099 * Add default feeds to the page header 1100 * This is mainly kept for backward compatibility, see OutputPage::addFeedLink() 1101 * for the new version 1102 * @see addFeedLink() 1103 * 1104 * @param string $val Query to append to feed links or false to output 1105 * default links 1106 */ 1107 public function setFeedAppendQuery( $val ) { 1108 $this->mFeedLinks = array(); 1109 1110 foreach ( $this->getConfig()->get( 'AdvertisedFeedTypes' ) as $type ) { 1111 $query = "feed=$type"; 1112 if ( is_string( $val ) ) { 1113 $query .= '&' . $val; 1114 } 1115 $this->mFeedLinks[$type] = $this->getTitle()->getLocalURL( $query ); 1116 } 1117 } 1118 1119 /** 1120 * Add a feed link to the page header 1121 * 1122 * @param string $format Feed type, should be a key of $wgFeedClasses 1123 * @param string $href URL 1124 */ 1125 public function addFeedLink( $format, $href ) { 1126 if ( in_array( $format, $this->getConfig()->get( 'AdvertisedFeedTypes' ) ) ) { 1127 $this->mFeedLinks[$format] = $href; 1128 } 1129 } 1130 1131 /** 1132 * Should we output feed links for this page? 1133 * @return bool 1134 */ 1135 public function isSyndicated() { 1136 return count( $this->mFeedLinks ) > 0; 1137 } 1138 1139 /** 1140 * Return URLs for each supported syndication format for this page. 1141 * @return array Associating format keys with URLs 1142 */ 1143 public function getSyndicationLinks() { 1144 return $this->mFeedLinks; 1145 } 1146 1147 /** 1148 * Will currently always return null 1149 * 1150 * @return null 1151 */ 1152 public function getFeedAppendQuery() { 1153 return $this->mFeedLinksAppendQuery; 1154 } 1155 1156 /** 1157 * Set whether the displayed content is related to the source of the 1158 * corresponding article on the wiki 1159 * Setting true will cause the change "article related" toggle to true 1160 * 1161 * @param bool $v 1162 */ 1163 public function setArticleFlag( $v ) { 1164 $this->mIsarticle = $v; 1165 if ( $v ) { 1166 $this->mIsArticleRelated = $v; 1167 } 1168 } 1169 1170 /** 1171 * Return whether the content displayed page is related to the source of 1172 * the corresponding article on the wiki 1173 * 1174 * @return bool 1175 */ 1176 public function isArticle() { 1177 return $this->mIsarticle; 1178 } 1179 1180 /** 1181 * Set whether this page is related an article on the wiki 1182 * Setting false will cause the change of "article flag" toggle to false 1183 * 1184 * @param bool $v 1185 */ 1186 public function setArticleRelated( $v ) { 1187 $this->mIsArticleRelated = $v; 1188 if ( !$v ) { 1189 $this->mIsarticle = false; 1190 } 1191 } 1192 1193 /** 1194 * Return whether this page is related an article on the wiki 1195 * 1196 * @return bool 1197 */ 1198 public function isArticleRelated() { 1199 return $this->mIsArticleRelated; 1200 } 1201 1202 /** 1203 * Add new language links 1204 * 1205 * @param array $newLinkArray Associative array mapping language code to the page 1206 * name 1207 */ 1208 public function addLanguageLinks( array $newLinkArray ) { 1209 $this->mLanguageLinks += $newLinkArray; 1210 } 1211 1212 /** 1213 * Reset the language links and add new language links 1214 * 1215 * @param array $newLinkArray Associative array mapping language code to the page 1216 * name 1217 */ 1218 public function setLanguageLinks( array $newLinkArray ) { 1219 $this->mLanguageLinks = $newLinkArray; 1220 } 1221 1222 /** 1223 * Get the list of language links 1224 * 1225 * @return array Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page') 1226 */ 1227 public function getLanguageLinks() { 1228 return $this->mLanguageLinks; 1229 } 1230 1231 /** 1232 * Add an array of categories, with names in the keys 1233 * 1234 * @param array $categories Mapping category name => sort key 1235 */ 1236 public function addCategoryLinks( array $categories ) { 1237 global $wgContLang; 1238 1239 if ( !is_array( $categories ) || count( $categories ) == 0 ) { 1240 return; 1241 } 1242 1243 # Add the links to a LinkBatch 1244 $arr = array( NS_CATEGORY => $categories ); 1245 $lb = new LinkBatch; 1246 $lb->setArray( $arr ); 1247 1248 # Fetch existence plus the hiddencat property 1249 $dbr = wfGetDB( DB_SLAVE ); 1250 $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_len', 1251 'page_is_redirect', 'page_latest', 'pp_value' ); 1252 1253 if ( $this->getConfig()->get( 'ContentHandlerUseDB' ) ) { 1254 $fields[] = 'page_content_model'; 1255 } 1256 1257 $res = $dbr->select( array( 'page', 'page_props' ), 1258 $fields, 1259 $lb->constructSet( 'page', $dbr ), 1260 __METHOD__, 1261 array(), 1262 array( 'page_props' => array( 'LEFT JOIN', array( 1263 'pp_propname' => 'hiddencat', 1264 'pp_page = page_id' 1265 ) ) ) 1266 ); 1267 1268 # Add the results to the link cache 1269 $lb->addResultToCache( LinkCache::singleton(), $res ); 1270 1271 # Set all the values to 'normal'. 1272 $categories = array_fill_keys( array_keys( $categories ), 'normal' ); 1273 1274 # Mark hidden categories 1275 foreach ( $res as $row ) { 1276 if ( isset( $row->pp_value ) ) { 1277 $categories[$row->page_title] = 'hidden'; 1278 } 1279 } 1280 1281 # Add the remaining categories to the skin 1282 if ( wfRunHooks( 1283 'OutputPageMakeCategoryLinks', 1284 array( &$this, $categories, &$this->mCategoryLinks ) ) 1285 ) { 1286 foreach ( $categories as $category => $type ) { 1287 $origcategory = $category; 1288 $title = Title::makeTitleSafe( NS_CATEGORY, $category ); 1289 if ( !$title ) { 1290 continue; 1291 } 1292 $wgContLang->findVariantLink( $category, $title, true ); 1293 if ( $category != $origcategory && array_key_exists( $category, $categories ) ) { 1294 continue; 1295 } 1296 $text = $wgContLang->convertHtml( $title->getText() ); 1297 $this->mCategories[] = $title->getText(); 1298 $this->mCategoryLinks[$type][] = Linker::link( $title, $text ); 1299 } 1300 } 1301 } 1302 1303 /** 1304 * Reset the category links (but not the category list) and add $categories 1305 * 1306 * @param array $categories Mapping category name => sort key 1307 */ 1308 public function setCategoryLinks( array $categories ) { 1309 $this->mCategoryLinks = array(); 1310 $this->addCategoryLinks( $categories ); 1311 } 1312 1313 /** 1314 * Get the list of category links, in a 2-D array with the following format: 1315 * $arr[$type][] = $link, where $type is either "normal" or "hidden" (for 1316 * hidden categories) and $link a HTML fragment with a link to the category 1317 * page 1318 * 1319 * @return array 1320 */ 1321 public function getCategoryLinks() { 1322 return $this->mCategoryLinks; 1323 } 1324 1325 /** 1326 * Get the list of category names this page belongs to 1327 * 1328 * @return array Array of strings 1329 */ 1330 public function getCategories() { 1331 return $this->mCategories; 1332 } 1333 1334 /** 1335 * Do not allow scripts which can be modified by wiki users to load on this page; 1336 * only allow scripts bundled with, or generated by, the software. 1337 * Site-wide styles are controlled by a config setting, since they can be 1338 * used to create a custom skin/theme, but not user-specific ones. 1339 * 1340 * @todo this should be given a more accurate name 1341 */ 1342 public function disallowUserJs() { 1343 $this->reduceAllowedModules( 1344 ResourceLoaderModule::TYPE_SCRIPTS, 1345 ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL 1346 ); 1347 1348 // Site-wide styles are controlled by a config setting, see bug 71621 1349 // for background on why. User styles are never allowed. 1350 if ( $this->getConfig()->get( 'AllowSiteCSSOnRestrictedPages' ) ) { 1351 $styleOrigin = ResourceLoaderModule::ORIGIN_USER_SITEWIDE; 1352 } else { 1353 $styleOrigin = ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL; 1354 } 1355 $this->reduceAllowedModules( 1356 ResourceLoaderModule::TYPE_STYLES, 1357 $styleOrigin 1358 ); 1359 } 1360 1361 /** 1362 * Show what level of JavaScript / CSS untrustworthiness is allowed on this page 1363 * @see ResourceLoaderModule::$origin 1364 * @param string $type ResourceLoaderModule TYPE_ constant 1365 * @return int ResourceLoaderModule ORIGIN_ class constant 1366 */ 1367 public function getAllowedModules( $type ) { 1368 if ( $type == ResourceLoaderModule::TYPE_COMBINED ) { 1369 return min( array_values( $this->mAllowedModules ) ); 1370 } else { 1371 return isset( $this->mAllowedModules[$type] ) 1372 ? $this->mAllowedModules[$type] 1373 : ResourceLoaderModule::ORIGIN_ALL; 1374 } 1375 } 1376 1377 /** 1378 * Set the highest level of CSS/JS untrustworthiness allowed 1379 * 1380 * @deprecated since 1.24 Raising level of allowed untrusted content is no longer supported. 1381 * Use reduceAllowedModules() instead 1382 * @param string $type ResourceLoaderModule TYPE_ constant 1383 * @param int $level ResourceLoaderModule class constant 1384 */ 1385 public function setAllowedModules( $type, $level ) { 1386 wfDeprecated( __METHOD__, '1.24' ); 1387 $this->reduceAllowedModules( $type, $level ); 1388 } 1389 1390 /** 1391 * Limit the highest level of CSS/JS untrustworthiness allowed. 1392 * 1393 * If passed the same or a higher level than the current level of untrustworthiness set, the 1394 * level will remain unchanged. 1395 * 1396 * @param string $type 1397 * @param int $level ResourceLoaderModule class constant 1398 */ 1399 public function reduceAllowedModules( $type, $level ) { 1400 $this->mAllowedModules[$type] = min( $this->getAllowedModules( $type ), $level ); 1401 } 1402 1403 /** 1404 * Prepend $text to the body HTML 1405 * 1406 * @param string $text HTML 1407 */ 1408 public function prependHTML( $text ) { 1409 $this->mBodytext = $text . $this->mBodytext; 1410 } 1411 1412 /** 1413 * Append $text to the body HTML 1414 * 1415 * @param string $text HTML 1416 */ 1417 public function addHTML( $text ) { 1418 $this->mBodytext .= $text; 1419 } 1420 1421 /** 1422 * Shortcut for adding an Html::element via addHTML. 1423 * 1424 * @since 1.19 1425 * 1426 * @param string $element 1427 * @param array $attribs 1428 * @param string $contents 1429 */ 1430 public function addElement( $element, array $attribs = array(), $contents = '' ) { 1431 $this->addHTML( Html::element( $element, $attribs, $contents ) ); 1432 } 1433 1434 /** 1435 * Clear the body HTML 1436 */ 1437 public function clearHTML() { 1438 $this->mBodytext = ''; 1439 } 1440 1441 /** 1442 * Get the body HTML 1443 * 1444 * @return string HTML 1445 */ 1446 public function getHTML() { 1447 return $this->mBodytext; 1448 } 1449 1450 /** 1451 * Get/set the ParserOptions object to use for wikitext parsing 1452 * 1453 * @param ParserOptions|null $options Either the ParserOption to use or null to only get the 1454 * current ParserOption object 1455 * @return ParserOptions 1456 */ 1457 public function parserOptions( $options = null ) { 1458 if ( !$this->mParserOptions ) { 1459 $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() ); 1460 $this->mParserOptions->setEditSection( false ); 1461 } 1462 return wfSetVar( $this->mParserOptions, $options ); 1463 } 1464 1465 /** 1466 * Set the revision ID which will be seen by the wiki text parser 1467 * for things such as embedded {{REVISIONID}} variable use. 1468 * 1469 * @param int|null $revid An positive integer, or null 1470 * @return mixed Previous value 1471 */ 1472 public function setRevisionId( $revid ) { 1473 $val = is_null( $revid ) ? null : intval( $revid ); 1474 return wfSetVar( $this->mRevisionId, $val ); 1475 } 1476 1477 /** 1478 * Get the displayed revision ID 1479 * 1480 * @return int 1481 */ 1482 public function getRevisionId() { 1483 return $this->mRevisionId; 1484 } 1485 1486 /** 1487 * Set the timestamp of the revision which will be displayed. This is used 1488 * to avoid a extra DB call in Skin::lastModified(). 1489 * 1490 * @param string|null $timestamp 1491 * @return mixed Previous value 1492 */ 1493 public function setRevisionTimestamp( $timestamp ) { 1494 return wfSetVar( $this->mRevisionTimestamp, $timestamp ); 1495 } 1496 1497 /** 1498 * Get the timestamp of displayed revision. 1499 * This will be null if not filled by setRevisionTimestamp(). 1500 * 1501 * @return string|null 1502 */ 1503 public function getRevisionTimestamp() { 1504 return $this->mRevisionTimestamp; 1505 } 1506 1507 /** 1508 * Set the displayed file version 1509 * 1510 * @param File|bool $file 1511 * @return mixed Previous value 1512 */ 1513 public function setFileVersion( $file ) { 1514 $val = null; 1515 if ( $file instanceof File && $file->exists() ) { 1516 $val = array( 'time' => $file->getTimestamp(), 'sha1' => $file->getSha1() ); 1517 } 1518 return wfSetVar( $this->mFileVersion, $val, true ); 1519 } 1520 1521 /** 1522 * Get the displayed file version 1523 * 1524 * @return array|null ('time' => MW timestamp, 'sha1' => sha1) 1525 */ 1526 public function getFileVersion() { 1527 return $this->mFileVersion; 1528 } 1529 1530 /** 1531 * Get the templates used on this page 1532 * 1533 * @return array (namespace => dbKey => revId) 1534 * @since 1.18 1535 */ 1536 public function getTemplateIds() { 1537 return $this->mTemplateIds; 1538 } 1539 1540 /** 1541 * Get the files used on this page 1542 * 1543 * @return array (dbKey => array('time' => MW timestamp or null, 'sha1' => sha1 or '')) 1544 * @since 1.18 1545 */ 1546 public function getFileSearchOptions() { 1547 return $this->mImageTimeKeys; 1548 } 1549 1550 /** 1551 * Convert wikitext to HTML and add it to the buffer 1552 * Default assumes that the current page title will be used. 1553 * 1554 * @param string $text 1555 * @param bool $linestart Is this the start of a line? 1556 * @param bool $interface Is this text in the user interface language? 1557 */ 1558 public function addWikiText( $text, $linestart = true, $interface = true ) { 1559 $title = $this->getTitle(); // Work around E_STRICT 1560 if ( !$title ) { 1561 throw new MWException( 'Title is null' ); 1562 } 1563 $this->addWikiTextTitle( $text, $title, $linestart, /*tidy*/false, $interface ); 1564 } 1565 1566 /** 1567 * Add wikitext with a custom Title object 1568 * 1569 * @param string $text Wikitext 1570 * @param Title $title 1571 * @param bool $linestart Is this the start of a line? 1572 */ 1573 public function addWikiTextWithTitle( $text, &$title, $linestart = true ) { 1574 $this->addWikiTextTitle( $text, $title, $linestart ); 1575 } 1576 1577 /** 1578 * Add wikitext with a custom Title object and tidy enabled. 1579 * 1580 * @param string $text Wikitext 1581 * @param Title $title 1582 * @param bool $linestart Is this the start of a line? 1583 */ 1584 function addWikiTextTitleTidy( $text, &$title, $linestart = true ) { 1585 $this->addWikiTextTitle( $text, $title, $linestart, true ); 1586 } 1587 1588 /** 1589 * Add wikitext with tidy enabled 1590 * 1591 * @param string $text Wikitext 1592 * @param bool $linestart Is this the start of a line? 1593 */ 1594 public function addWikiTextTidy( $text, $linestart = true ) { 1595 $title = $this->getTitle(); 1596 $this->addWikiTextTitleTidy( $text, $title, $linestart ); 1597 } 1598 1599 /** 1600 * Add wikitext with a custom Title object 1601 * 1602 * @param string $text Wikitext 1603 * @param Title $title 1604 * @param bool $linestart Is this the start of a line? 1605 * @param bool $tidy Whether to use tidy 1606 * @param bool $interface Whether it is an interface message 1607 * (for example disables conversion) 1608 */ 1609 public function addWikiTextTitle( $text, Title $title, $linestart, 1610 $tidy = false, $interface = false 1611 ) { 1612 global $wgParser; 1613 1614 wfProfileIn( __METHOD__ ); 1615 1616 $popts = $this->parserOptions(); 1617 $oldTidy = $popts->setTidy( $tidy ); 1618 $popts->setInterfaceMessage( (bool)$interface ); 1619 1620 $parserOutput = $wgParser->getFreshParser()->parse( 1621 $text, $title, $popts, 1622 $linestart, true, $this->mRevisionId 1623 ); 1624 1625 $popts->setTidy( $oldTidy ); 1626 1627 $this->addParserOutput( $parserOutput ); 1628 1629 wfProfileOut( __METHOD__ ); 1630 } 1631 1632 /** 1633 * Add a ParserOutput object, but without Html. 1634 * 1635 * @deprecated since 1.24, use addParserOutputMetadata() instead. 1636 * @param ParserOutput $parserOutput 1637 */ 1638 public function addParserOutputNoText( $parserOutput ) { 1639 $this->addParserOutputMetadata( $parserOutput ); 1640 } 1641 1642 /** 1643 * Add all metadata associated with a ParserOutput object, but without the actual HTML. This 1644 * includes categories, language links, ResourceLoader modules, effects of certain magic words, 1645 * and so on. 1646 * 1647 * @since 1.24 1648 * @param ParserOutput $parserOutput 1649 */ 1650 public function addParserOutputMetadata( $parserOutput ) { 1651 $this->mLanguageLinks += $parserOutput->getLanguageLinks(); 1652 $this->addCategoryLinks( $parserOutput->getCategories() ); 1653 $this->mNewSectionLink = $parserOutput->getNewSection(); 1654 $this->mHideNewSectionLink = $parserOutput->getHideNewSection(); 1655 1656 $this->mParseWarnings = $parserOutput->getWarnings(); 1657 if ( !$parserOutput->isCacheable() ) { 1658 $this->enableClientCache( false ); 1659 } 1660 $this->mNoGallery = $parserOutput->getNoGallery(); 1661 $this->mHeadItems = array_merge( $this->mHeadItems, $parserOutput->getHeadItems() ); 1662 $this->addModules( $parserOutput->getModules() ); 1663 $this->addModuleScripts( $parserOutput->getModuleScripts() ); 1664 $this->addModuleStyles( $parserOutput->getModuleStyles() ); 1665 $this->addModuleMessages( $parserOutput->getModuleMessages() ); 1666 $this->addJsConfigVars( $parserOutput->getJsConfigVars() ); 1667 $this->mPreventClickjacking = $this->mPreventClickjacking 1668 || $parserOutput->preventClickjacking(); 1669 1670 // Template versioning... 1671 foreach ( (array)$parserOutput->getTemplateIds() as $ns => $dbks ) { 1672 if ( isset( $this->mTemplateIds[$ns] ) ) { 1673 $this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns]; 1674 } else { 1675 $this->mTemplateIds[$ns] = $dbks; 1676 } 1677 } 1678 // File versioning... 1679 foreach ( (array)$parserOutput->getFileSearchOptions() as $dbk => $data ) { 1680 $this->mImageTimeKeys[$dbk] = $data; 1681 } 1682 1683 // Hooks registered in the object 1684 $parserOutputHooks = $this->getConfig()->get( 'ParserOutputHooks' ); 1685 foreach ( $parserOutput->getOutputHooks() as $hookInfo ) { 1686 list( $hookName, $data ) = $hookInfo; 1687 if ( isset( $parserOutputHooks[$hookName] ) ) { 1688 call_user_func( $parserOutputHooks[$hookName], $this, $parserOutput, $data ); 1689 } 1690 } 1691 1692 // Link flags are ignored for now, but may in the future be 1693 // used to mark individual language links. 1694 $linkFlags = array(); 1695 wfRunHooks( 'LanguageLinks', array( $this->getTitle(), &$this->mLanguageLinks, &$linkFlags ) ); 1696 wfRunHooks( 'OutputPageParserOutput', array( &$this, $parserOutput ) ); 1697 } 1698 1699 /** 1700 * Add the HTML and enhancements for it (like ResourceLoader modules) associated with a 1701 * ParserOutput object, without any other metadata. 1702 * 1703 * @since 1.24 1704 * @param ParserOutput $parserOutput 1705 */ 1706 public function addParserOutputContent( $parserOutput ) { 1707 $this->addParserOutputText( $parserOutput ); 1708 1709 $this->addModules( $parserOutput->getModules() ); 1710 $this->addModuleScripts( $parserOutput->getModuleScripts() ); 1711 $this->addModuleStyles( $parserOutput->getModuleStyles() ); 1712 $this->addModuleMessages( $parserOutput->getModuleMessages() ); 1713 1714 $this->addJsConfigVars( $parserOutput->getJsConfigVars() ); 1715 } 1716 1717 /** 1718 * Add the HTML associated with a ParserOutput object, without any metadata. 1719 * 1720 * @since 1.24 1721 * @param ParserOutput $parserOutput 1722 */ 1723 public function addParserOutputText( $parserOutput ) { 1724 $text = $parserOutput->getText(); 1725 wfRunHooks( 'OutputPageBeforeHTML', array( &$this, &$text ) ); 1726 $this->addHTML( $text ); 1727 } 1728 1729 /** 1730 * Add everything from a ParserOutput object. 1731 * 1732 * @param ParserOutput $parserOutput 1733 */ 1734 function addParserOutput( $parserOutput ) { 1735 $this->addParserOutputMetadata( $parserOutput ); 1736 $parserOutput->setTOCEnabled( $this->mEnableTOC ); 1737 1738 // Touch section edit links only if not previously disabled 1739 if ( $parserOutput->getEditSectionTokens() ) { 1740 $parserOutput->setEditSectionTokens( $this->mEnableSectionEditLinks ); 1741 } 1742 1743 $this->addParserOutputText( $parserOutput ); 1744 } 1745 1746 /** 1747 * Add the output of a QuickTemplate to the output buffer 1748 * 1749 * @param QuickTemplate $template 1750 */ 1751 public function addTemplate( &$template ) { 1752 $this->addHTML( $template->getHTML() ); 1753 } 1754 1755 /** 1756 * Parse wikitext and return the HTML. 1757 * 1758 * @param string $text 1759 * @param bool $linestart Is this the start of a line? 1760 * @param bool $interface Use interface language ($wgLang instead of 1761 * $wgContLang) while parsing language sensitive magic words like GRAMMAR and PLURAL. 1762 * This also disables LanguageConverter. 1763 * @param Language $language Target language object, will override $interface 1764 * @throws MWException 1765 * @return string HTML 1766 */ 1767 public function parse( $text, $linestart = true, $interface = false, $language = null ) { 1768 global $wgParser; 1769 1770 if ( is_null( $this->getTitle() ) ) { 1771 throw new MWException( 'Empty $mTitle in ' . __METHOD__ ); 1772 } 1773 1774 $popts = $this->parserOptions(); 1775 if ( $interface ) { 1776 $popts->setInterfaceMessage( true ); 1777 } 1778 if ( $language !== null ) { 1779 $oldLang = $popts->setTargetLanguage( $language ); 1780 } 1781 1782 $parserOutput = $wgParser->getFreshParser()->parse( 1783 $text, $this->getTitle(), $popts, 1784 $linestart, true, $this->mRevisionId 1785 ); 1786 1787 if ( $interface ) { 1788 $popts->setInterfaceMessage( false ); 1789 } 1790 if ( $language !== null ) { 1791 $popts->setTargetLanguage( $oldLang ); 1792 } 1793 1794 return $parserOutput->getText(); 1795 } 1796 1797 /** 1798 * Parse wikitext, strip paragraphs, and return the HTML. 1799 * 1800 * @param string $text 1801 * @param bool $linestart Is this the start of a line? 1802 * @param bool $interface Use interface language ($wgLang instead of 1803 * $wgContLang) while parsing language sensitive magic 1804 * words like GRAMMAR and PLURAL 1805 * @return string HTML 1806 */ 1807 public function parseInline( $text, $linestart = true, $interface = false ) { 1808 $parsed = $this->parse( $text, $linestart, $interface ); 1809 return Parser::stripOuterParagraph( $parsed ); 1810 } 1811 1812 /** 1813 * Set the value of the "s-maxage" part of the "Cache-control" HTTP header 1814 * 1815 * @param int $maxage Maximum cache time on the Squid, in seconds. 1816 */ 1817 public function setSquidMaxage( $maxage ) { 1818 $this->mSquidMaxage = $maxage; 1819 } 1820 1821 /** 1822 * Use enableClientCache(false) to force it to send nocache headers 1823 * 1824 * @param bool $state 1825 * 1826 * @return bool 1827 */ 1828 public function enableClientCache( $state ) { 1829 return wfSetVar( $this->mEnableClientCache, $state ); 1830 } 1831 1832 /** 1833 * Get the list of cookies that will influence on the cache 1834 * 1835 * @return array 1836 */ 1837 function getCacheVaryCookies() { 1838 static $cookies; 1839 if ( $cookies === null ) { 1840 $config = $this->getConfig(); 1841 $cookies = array_merge( 1842 array( 1843 $config->get( 'CookiePrefix' ) . 'Token', 1844 $config->get( 'CookiePrefix' ) . 'LoggedOut', 1845 "forceHTTPS", 1846 session_name() 1847 ), 1848 $config->get( 'CacheVaryCookies' ) 1849 ); 1850 wfRunHooks( 'GetCacheVaryCookies', array( $this, &$cookies ) ); 1851 } 1852 return $cookies; 1853 } 1854 1855 /** 1856 * Check if the request has a cache-varying cookie header 1857 * If it does, it's very important that we don't allow public caching 1858 * 1859 * @return bool 1860 */ 1861 function haveCacheVaryCookies() { 1862 $cookieHeader = $this->getRequest()->getHeader( 'cookie' ); 1863 if ( $cookieHeader === false ) { 1864 return false; 1865 } 1866 $cvCookies = $this->getCacheVaryCookies(); 1867 foreach ( $cvCookies as $cookieName ) { 1868 # Check for a simple string match, like the way squid does it 1869 if ( strpos( $cookieHeader, $cookieName ) !== false ) { 1870 wfDebug( __METHOD__ . ": found $cookieName\n" ); 1871 return true; 1872 } 1873 } 1874 wfDebug( __METHOD__ . ": no cache-varying cookies found\n" ); 1875 return false; 1876 } 1877 1878 /** 1879 * Add an HTTP header that will influence on the cache 1880 * 1881 * @param string $header Header name 1882 * @param array|null $option 1883 * @todo FIXME: Document the $option parameter; it appears to be for 1884 * X-Vary-Options but what format is acceptable? 1885 */ 1886 public function addVaryHeader( $header, $option = null ) { 1887 if ( !array_key_exists( $header, $this->mVaryHeader ) ) { 1888 $this->mVaryHeader[$header] = (array)$option; 1889 } elseif ( is_array( $option ) ) { 1890 if ( is_array( $this->mVaryHeader[$header] ) ) { 1891 $this->mVaryHeader[$header] = array_merge( $this->mVaryHeader[$header], $option ); 1892 } else { 1893 $this->mVaryHeader[$header] = $option; 1894 } 1895 } 1896 $this->mVaryHeader[$header] = array_unique( (array)$this->mVaryHeader[$header] ); 1897 } 1898 1899 /** 1900 * Return a Vary: header on which to vary caches. Based on the keys of $mVaryHeader, 1901 * such as Accept-Encoding or Cookie 1902 * 1903 * @return string 1904 */ 1905 public function getVaryHeader() { 1906 return 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) ); 1907 } 1908 1909 /** 1910 * Get a complete X-Vary-Options header 1911 * 1912 * @return string 1913 */ 1914 public function getXVO() { 1915 $cvCookies = $this->getCacheVaryCookies(); 1916 1917 $cookiesOption = array(); 1918 foreach ( $cvCookies as $cookieName ) { 1919 $cookiesOption[] = 'string-contains=' . $cookieName; 1920 } 1921 $this->addVaryHeader( 'Cookie', $cookiesOption ); 1922 1923 $headers = array(); 1924 foreach ( $this->mVaryHeader as $header => $option ) { 1925 $newheader = $header; 1926 if ( is_array( $option ) && count( $option ) > 0 ) { 1927 $newheader .= ';' . implode( ';', $option ); 1928 } 1929 $headers[] = $newheader; 1930 } 1931 $xvo = 'X-Vary-Options: ' . implode( ',', $headers ); 1932 1933 return $xvo; 1934 } 1935 1936 /** 1937 * bug 21672: Add Accept-Language to Vary and XVO headers 1938 * if there's no 'variant' parameter existed in GET. 1939 * 1940 * For example: 1941 * /w/index.php?title=Main_page should always be served; but 1942 * /w/index.php?title=Main_page&variant=zh-cn should never be served. 1943 */ 1944 function addAcceptLanguage() { 1945 $title = $this->getTitle(); 1946 if ( !$title instanceof Title ) { 1947 return; 1948 } 1949 1950 $lang = $title->getPageLanguage(); 1951 if ( !$this->getRequest()->getCheck( 'variant' ) && $lang->hasVariants() ) { 1952 $variants = $lang->getVariants(); 1953 $aloption = array(); 1954 foreach ( $variants as $variant ) { 1955 if ( $variant === $lang->getCode() ) { 1956 continue; 1957 } else { 1958 $aloption[] = 'string-contains=' . $variant; 1959 1960 // IE and some other browsers use BCP 47 standards in 1961 // their Accept-Language header, like "zh-CN" or "zh-Hant". 1962 // We should handle these too. 1963 $variantBCP47 = wfBCP47( $variant ); 1964 if ( $variantBCP47 !== $variant ) { 1965 $aloption[] = 'string-contains=' . $variantBCP47; 1966 } 1967 } 1968 } 1969 $this->addVaryHeader( 'Accept-Language', $aloption ); 1970 } 1971 } 1972 1973 /** 1974 * Set a flag which will cause an X-Frame-Options header appropriate for 1975 * edit pages to be sent. The header value is controlled by 1976 * $wgEditPageFrameOptions. 1977 * 1978 * This is the default for special pages. If you display a CSRF-protected 1979 * form on an ordinary view page, then you need to call this function. 1980 * 1981 * @param bool $enable 1982 */ 1983 public function preventClickjacking( $enable = true ) { 1984 $this->mPreventClickjacking = $enable; 1985 } 1986 1987 /** 1988 * Turn off frame-breaking. Alias for $this->preventClickjacking(false). 1989 * This can be called from pages which do not contain any CSRF-protected 1990 * HTML form. 1991 */ 1992 public function allowClickjacking() { 1993 $this->mPreventClickjacking = false; 1994 } 1995 1996 /** 1997 * Get the prevent-clickjacking flag 1998 * 1999 * @since 1.24 2000 * @return bool 2001 */ 2002 public function getPreventClickjacking() { 2003 return $this->mPreventClickjacking; 2004 } 2005 2006 /** 2007 * Get the X-Frame-Options header value (without the name part), or false 2008 * if there isn't one. This is used by Skin to determine whether to enable 2009 * JavaScript frame-breaking, for clients that don't support X-Frame-Options. 2010 * 2011 * @return string 2012 */ 2013 public function getFrameOptions() { 2014 $config = $this->getConfig(); 2015 if ( $config->get( 'BreakFrames' ) ) { 2016 return 'DENY'; 2017 } elseif ( $this->mPreventClickjacking && $config->get( 'EditPageFrameOptions' ) ) { 2018 return $config->get( 'EditPageFrameOptions' ); 2019 } 2020 return false; 2021 } 2022 2023 /** 2024 * Send cache control HTTP headers 2025 */ 2026 public function sendCacheControl() { 2027 $response = $this->getRequest()->response(); 2028 $config = $this->getConfig(); 2029 if ( $config->get( 'UseETag' ) && $this->mETag ) { 2030 $response->header( "ETag: $this->mETag" ); 2031 } 2032 2033 $this->addVaryHeader( 'Cookie' ); 2034 $this->addAcceptLanguage(); 2035 2036 # don't serve compressed data to clients who can't handle it 2037 # maintain different caches for logged-in users and non-logged in ones 2038 $response->header( $this->getVaryHeader() ); 2039 2040 if ( $config->get( 'UseXVO' ) ) { 2041 # Add an X-Vary-Options header for Squid with Wikimedia patches 2042 $response->header( $this->getXVO() ); 2043 } 2044 2045 if ( $this->mEnableClientCache ) { 2046 if ( 2047 $config->get( 'UseSquid' ) && session_id() == '' && !$this->isPrintable() && 2048 $this->mSquidMaxage != 0 && !$this->haveCacheVaryCookies() 2049 ) { 2050 if ( $config->get( 'UseESI' ) ) { 2051 # We'll purge the proxy cache explicitly, but require end user agents 2052 # to revalidate against the proxy on each visit. 2053 # Surrogate-Control controls our Squid, Cache-Control downstream caches 2054 wfDebug( __METHOD__ . ": proxy caching with ESI; {$this->mLastModified} **\n", 'log' ); 2055 # start with a shorter timeout for initial testing 2056 # header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"'); 2057 $response->header( 'Surrogate-Control: max-age=' . $config->get( 'SquidMaxage' ) 2058 . '+' . $this->mSquidMaxage . ', content="ESI/1.0"' ); 2059 $response->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); 2060 } else { 2061 # We'll purge the proxy cache for anons explicitly, but require end user agents 2062 # to revalidate against the proxy on each visit. 2063 # IMPORTANT! The Squid needs to replace the Cache-Control header with 2064 # Cache-Control: s-maxage=0, must-revalidate, max-age=0 2065 wfDebug( __METHOD__ . ": local proxy caching; {$this->mLastModified} **\n", 'log' ); 2066 # start with a shorter timeout for initial testing 2067 # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" ); 2068 $response->header( 'Cache-Control: s-maxage=' . $this->mSquidMaxage 2069 . ', must-revalidate, max-age=0' ); 2070 } 2071 } else { 2072 # We do want clients to cache if they can, but they *must* check for updates 2073 # on revisiting the page. 2074 wfDebug( __METHOD__ . ": private caching; {$this->mLastModified} **\n", 'log' ); 2075 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); 2076 $response->header( "Cache-Control: private, must-revalidate, max-age=0" ); 2077 } 2078 if ( $this->mLastModified ) { 2079 $response->header( "Last-Modified: {$this->mLastModified}" ); 2080 } 2081 } else { 2082 wfDebug( __METHOD__ . ": no caching **\n", 'log' ); 2083 2084 # In general, the absence of a last modified header should be enough to prevent 2085 # the client from using its cache. We send a few other things just to make sure. 2086 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); 2087 $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); 2088 $response->header( 'Pragma: no-cache' ); 2089 } 2090 } 2091 2092 /** 2093 * Finally, all the text has been munged and accumulated into 2094 * the object, let's actually output it: 2095 */ 2096 public function output() { 2097 global $wgLanguageCode; 2098 2099 if ( $this->mDoNothing ) { 2100 return; 2101 } 2102 2103 wfProfileIn( __METHOD__ ); 2104 2105 $response = $this->getRequest()->response(); 2106 $config = $this->getConfig(); 2107 2108 if ( $this->mRedirect != '' ) { 2109 # Standards require redirect URLs to be absolute 2110 $this->mRedirect = wfExpandUrl( $this->mRedirect, PROTO_CURRENT ); 2111 2112 $redirect = $this->mRedirect; 2113 $code = $this->mRedirectCode; 2114 2115 if ( wfRunHooks( "BeforePageRedirect", array( $this, &$redirect, &$code ) ) ) { 2116 if ( $code == '301' || $code == '303' ) { 2117 if ( !$config->get( 'DebugRedirects' ) ) { 2118 $message = HttpStatus::getMessage( $code ); 2119 $response->header( "HTTP/1.1 $code $message" ); 2120 } 2121 $this->mLastModified = wfTimestamp( TS_RFC2822 ); 2122 } 2123 if ( $config->get( 'VaryOnXFP' ) ) { 2124 $this->addVaryHeader( 'X-Forwarded-Proto' ); 2125 } 2126 $this->sendCacheControl(); 2127 2128 $response->header( "Content-Type: text/html; charset=utf-8" ); 2129 if ( $config->get( 'DebugRedirects' ) ) { 2130 $url = htmlspecialchars( $redirect ); 2131 print "<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n"; 2132 print "<p>Location: <a href=\"$url\">$url</a></p>\n"; 2133 print "</body>\n</html>\n"; 2134 } else { 2135 $response->header( 'Location: ' . $redirect ); 2136 } 2137 } 2138 2139 wfProfileOut( __METHOD__ ); 2140 return; 2141 } elseif ( $this->mStatusCode ) { 2142 $message = HttpStatus::getMessage( $this->mStatusCode ); 2143 if ( $message ) { 2144 $response->header( 'HTTP/1.1 ' . $this->mStatusCode . ' ' . $message ); 2145 } 2146 } 2147 2148 # Buffer output; final headers may depend on later processing 2149 ob_start(); 2150 2151 $response->header( 'Content-type: ' . $config->get( 'MimeType' ) . '; charset=UTF-8' ); 2152 $response->header( 'Content-language: ' . $wgLanguageCode ); 2153 2154 // Avoid Internet Explorer "compatibility view" in IE 8-10, so that 2155 // jQuery etc. can work correctly. 2156 $response->header( 'X-UA-Compatible: IE=Edge' ); 2157 2158 // Prevent framing, if requested 2159 $frameOptions = $this->getFrameOptions(); 2160 if ( $frameOptions ) { 2161 $response->header( "X-Frame-Options: $frameOptions" ); 2162 } 2163 2164 if ( $this->mArticleBodyOnly ) { 2165 echo $this->mBodytext; 2166 } else { 2167 2168 $sk = $this->getSkin(); 2169 // add skin specific modules 2170 $modules = $sk->getDefaultModules(); 2171 2172 // enforce various default modules for all skins 2173 $coreModules = array( 2174 // keep this list as small as possible 2175 'mediawiki.page.startup', 2176 'mediawiki.user', 2177 ); 2178 2179 // Support for high-density display images if enabled 2180 if ( $config->get( 'ResponsiveImages' ) ) { 2181 $coreModules[] = 'mediawiki.hidpi'; 2182 } 2183 2184 $this->addModules( $coreModules ); 2185 foreach ( $modules as $group ) { 2186 $this->addModules( $group ); 2187 } 2188 MWDebug::addModules( $this ); 2189 2190 // Hook that allows last minute changes to the output page, e.g. 2191 // adding of CSS or Javascript by extensions. 2192 wfRunHooks( 'BeforePageDisplay', array( &$this, &$sk ) ); 2193 2194 wfProfileIn( 'Output-skin' ); 2195 $sk->outputPage(); 2196 wfProfileOut( 'Output-skin' ); 2197 } 2198 2199 // This hook allows last minute changes to final overall output by modifying output buffer 2200 wfRunHooks( 'AfterFinalPageOutput', array( $this ) ); 2201 2202 $this->sendCacheControl(); 2203 2204 ob_end_flush(); 2205 2206 wfProfileOut( __METHOD__ ); 2207 } 2208 2209 /** 2210 * Actually output something with print. 2211 * 2212 * @param string $ins The string to output 2213 * @deprecated since 1.22 Use echo yourself. 2214 */ 2215 public function out( $ins ) { 2216 wfDeprecated( __METHOD__, '1.22' ); 2217 print $ins; 2218 } 2219 2220 /** 2221 * Produce a "user is blocked" page. 2222 * @deprecated since 1.18 2223 */ 2224 function blockedPage() { 2225 throw new UserBlockedError( $this->getUser()->mBlock ); 2226 } 2227 2228 /** 2229 * Prepare this object to display an error page; disable caching and 2230 * indexing, clear the current text and redirect, set the page's title 2231 * and optionally an custom HTML title (content of the "<title>" tag). 2232 * 2233 * @param string|Message $pageTitle Will be passed directly to setPageTitle() 2234 * @param string|Message $htmlTitle Will be passed directly to setHTMLTitle(); 2235 * optional, if not passed the "<title>" attribute will be 2236 * based on $pageTitle 2237 */ 2238 public function prepareErrorPage( $pageTitle, $htmlTitle = false ) { 2239 $this->setPageTitle( $pageTitle ); 2240 if ( $htmlTitle !== false ) { 2241 $this->setHTMLTitle( $htmlTitle ); 2242 } 2243 $this->setRobotPolicy( 'noindex,nofollow' ); 2244 $this->setArticleRelated( false ); 2245 $this->enableClientCache( false ); 2246 $this->mRedirect = ''; 2247 $this->clearSubtitle(); 2248 $this->clearHTML(); 2249 } 2250 2251 /** 2252 * Output a standard error page 2253 * 2254 * showErrorPage( 'titlemsg', 'pagetextmsg' ); 2255 * showErrorPage( 'titlemsg', 'pagetextmsg', array( 'param1', 'param2' ) ); 2256 * showErrorPage( 'titlemsg', $messageObject ); 2257 * showErrorPage( $titleMessageObject, $messageObject ); 2258 * 2259 * @param string|Message $title Message key (string) for page title, or a Message object 2260 * @param string|Message $msg Message key (string) for page text, or a Message object 2261 * @param array $params Message parameters; ignored if $msg is a Message object 2262 */ 2263 public function showErrorPage( $title, $msg, $params = array() ) { 2264 if ( !$title instanceof Message ) { 2265 $title = $this->msg( $title ); 2266 } 2267 2268 $this->prepareErrorPage( $title ); 2269 2270 if ( $msg instanceof Message ) { 2271 if ( $params !== array() ) { 2272 trigger_error( 'Argument ignored: $params. The message parameters argument ' 2273 . 'is discarded when the $msg argument is a Message object instead of ' 2274 . 'a string.', E_USER_NOTICE ); 2275 } 2276 $this->addHTML( $msg->parseAsBlock() ); 2277 } else { 2278 $this->addWikiMsgArray( $msg, $params ); 2279 } 2280 2281 $this->returnToMain(); 2282 } 2283 2284 /** 2285 * Output a standard permission error page 2286 * 2287 * @param array $errors Error message keys 2288 * @param string $action Action that was denied or null if unknown 2289 */ 2290 public function showPermissionsErrorPage( array $errors, $action = null ) { 2291 // For some action (read, edit, create and upload), display a "login to do this action" 2292 // error if all of the following conditions are met: 2293 // 1. the user is not logged in 2294 // 2. the only error is insufficient permissions (i.e. no block or something else) 2295 // 3. the error can be avoided simply by logging in 2296 if ( in_array( $action, array( 'read', 'edit', 'createpage', 'createtalk', 'upload' ) ) 2297 && $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] ) 2298 && ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' ) 2299 && ( User::groupHasPermission( 'user', $action ) 2300 || User::groupHasPermission( 'autoconfirmed', $action ) ) 2301 ) { 2302 $displayReturnto = null; 2303 2304 # Due to bug 32276, if a user does not have read permissions, 2305 # $this->getTitle() will just give Special:Badtitle, which is 2306 # not especially useful as a returnto parameter. Use the title 2307 # from the request instead, if there was one. 2308 $request = $this->getRequest(); 2309 $returnto = Title::newFromURL( $request->getVal( 'title', '' ) ); 2310 if ( $action == 'edit' ) { 2311 $msg = 'whitelistedittext'; 2312 $displayReturnto = $returnto; 2313 } elseif ( $action == 'createpage' || $action == 'createtalk' ) { 2314 $msg = 'nocreatetext'; 2315 } elseif ( $action == 'upload' ) { 2316 $msg = 'uploadnologintext'; 2317 } else { # Read 2318 $msg = 'loginreqpagetext'; 2319 $displayReturnto = Title::newMainPage(); 2320 } 2321 2322 $query = array(); 2323 2324 if ( $returnto ) { 2325 $query['returnto'] = $returnto->getPrefixedText(); 2326 2327 if ( !$request->wasPosted() ) { 2328 $returntoquery = $request->getValues(); 2329 unset( $returntoquery['title'] ); 2330 unset( $returntoquery['returnto'] ); 2331 unset( $returntoquery['returntoquery'] ); 2332 $query['returntoquery'] = wfArrayToCgi( $returntoquery ); 2333 } 2334 } 2335 $loginLink = Linker::linkKnown( 2336 SpecialPage::getTitleFor( 'Userlogin' ), 2337 $this->msg( 'loginreqlink' )->escaped(), 2338 array(), 2339 $query 2340 ); 2341 2342 $this->prepareErrorPage( $this->msg( 'loginreqtitle' ) ); 2343 $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->parse() ); 2344 2345 # Don't return to a page the user can't read otherwise 2346 # we'll end up in a pointless loop 2347 if ( $displayReturnto && $displayReturnto->userCan( 'read', $this->getUser() ) ) { 2348 $this->returnToMain( null, $displayReturnto ); 2349 } 2350 } else { 2351 $this->prepareErrorPage( $this->msg( 'permissionserrors' ) ); 2352 $this->addWikiText( $this->formatPermissionsErrorMessage( $errors, $action ) ); 2353 } 2354 } 2355 2356 /** 2357 * Display an error page indicating that a given version of MediaWiki is 2358 * required to use it 2359 * 2360 * @param mixed $version The version of MediaWiki needed to use the page 2361 */ 2362 public function versionRequired( $version ) { 2363 $this->prepareErrorPage( $this->msg( 'versionrequired', $version ) ); 2364 2365 $this->addWikiMsg( 'versionrequiredtext', $version ); 2366 $this->returnToMain(); 2367 } 2368 2369 /** 2370 * Display an error page noting that a given permission bit is required. 2371 * @deprecated since 1.18, just throw the exception directly 2372 * @param string $permission Key required 2373 * @throws PermissionsError 2374 */ 2375 public function permissionRequired( $permission ) { 2376 throw new PermissionsError( $permission ); 2377 } 2378 2379 /** 2380 * Produce the stock "please login to use the wiki" page 2381 * 2382 * @deprecated since 1.19; throw the exception directly 2383 */ 2384 public function loginToUse() { 2385 throw new PermissionsError( 'read' ); 2386 } 2387 2388 /** 2389 * Format a list of error messages 2390 * 2391 * @param array $errors Array of arrays returned by Title::getUserPermissionsErrors 2392 * @param string $action Action that was denied or null if unknown 2393 * @return string The wikitext error-messages, formatted into a list. 2394 */ 2395 public function formatPermissionsErrorMessage( array $errors, $action = null ) { 2396 if ( $action == null ) { 2397 $text = $this->msg( 'permissionserrorstext', count( $errors ) )->plain() . "\n\n"; 2398 } else { 2399 $action_desc = $this->msg( "action-$action" )->plain(); 2400 $text = $this->msg( 2401 'permissionserrorstext-withaction', 2402 count( $errors ), 2403 $action_desc 2404 )->plain() . "\n\n"; 2405 } 2406 2407 if ( count( $errors ) > 1 ) { 2408 $text .= '<ul class="permissions-errors">' . "\n"; 2409 2410 foreach ( $errors as $error ) { 2411 $text .= '<li>'; 2412 $text .= call_user_func_array( array( $this, 'msg' ), $error )->plain(); 2413 $text .= "</li>\n"; 2414 } 2415 $text .= '</ul>'; 2416 } else { 2417 $text .= "<div class=\"permissions-errors\">\n" . 2418 call_user_func_array( array( $this, 'msg' ), reset( $errors ) )->plain() . 2419 "\n</div>"; 2420 } 2421 2422 return $text; 2423 } 2424 2425 /** 2426 * Display a page stating that the Wiki is in read-only mode, 2427 * and optionally show the source of the page that the user 2428 * was trying to edit. Should only be called (for this 2429 * purpose) after wfReadOnly() has returned true. 2430 * 2431 * For historical reasons, this function is _also_ used to 2432 * show the error message when a user tries to edit a page 2433 * they are not allowed to edit. (Unless it's because they're 2434 * blocked, then we show blockedPage() instead.) In this 2435 * case, the second parameter should be set to true and a list 2436 * of reasons supplied as the third parameter. 2437 * 2438 * @todo Needs to be split into multiple functions. 2439 * 2440 * @param string $source Source code to show (or null). 2441 * @param bool $protected Is this a permissions error? 2442 * @param array $reasons List of reasons for this error, as returned by 2443 * Title::getUserPermissionsErrors(). 2444 * @param string $action Action that was denied or null if unknown 2445 * @throws ReadOnlyError 2446 */ 2447 public function readOnlyPage( $source = null, $protected = false, 2448 array $reasons = array(), $action = null 2449 ) { 2450 $this->setRobotPolicy( 'noindex,nofollow' ); 2451 $this->setArticleRelated( false ); 2452 2453 // If no reason is given, just supply a default "I can't let you do 2454 // that, Dave" message. Should only occur if called by legacy code. 2455 if ( $protected && empty( $reasons ) ) { 2456 $reasons[] = array( 'badaccess-group0' ); 2457 } 2458 2459 if ( !empty( $reasons ) ) { 2460 // Permissions error 2461 if ( $source ) { 2462 $this->setPageTitle( $this->msg( 'viewsource-title', $this->getTitle()->getPrefixedText() ) ); 2463 $this->addBacklinkSubtitle( $this->getTitle() ); 2464 } else { 2465 $this->setPageTitle( $this->msg( 'badaccess' ) ); 2466 } 2467 $this->addWikiText( $this->formatPermissionsErrorMessage( $reasons, $action ) ); 2468 } else { 2469 // Wiki is read only 2470 throw new ReadOnlyError; 2471 } 2472 2473 // Show source, if supplied 2474 if ( is_string( $source ) ) { 2475 $this->addWikiMsg( 'viewsourcetext' ); 2476 2477 $pageLang = $this->getTitle()->getPageLanguage(); 2478 $params = array( 2479 'id' => 'wpTextbox1', 2480 'name' => 'wpTextbox1', 2481 'cols' => $this->getUser()->getOption( 'cols' ), 2482 'rows' => $this->getUser()->getOption( 'rows' ), 2483 'readonly' => 'readonly', 2484 'lang' => $pageLang->getHtmlCode(), 2485 'dir' => $pageLang->getDir(), 2486 ); 2487 $this->addHTML( Html::element( 'textarea', $params, $source ) ); 2488 2489 // Show templates used by this article 2490 $templates = Linker::formatTemplates( $this->getTitle()->getTemplateLinksFrom() ); 2491 $this->addHTML( "<div class='templatesUsed'> 2492 $templates 2493 </div> 2494 " ); 2495 } 2496 2497 # If the title doesn't exist, it's fairly pointless to print a return 2498 # link to it. After all, you just tried editing it and couldn't, so 2499 # what's there to do there? 2500 if ( $this->getTitle()->exists() ) { 2501 $this->returnToMain( null, $this->getTitle() ); 2502 } 2503 } 2504 2505 /** 2506 * Turn off regular page output and return an error response 2507 * for when rate limiting has triggered. 2508 */ 2509 public function rateLimited() { 2510 throw new ThrottledError; 2511 } 2512 2513 /** 2514 * Show a warning about slave lag 2515 * 2516 * If the lag is higher than $wgSlaveLagCritical seconds, 2517 * then the warning is a bit more obvious. If the lag is 2518 * lower than $wgSlaveLagWarning, then no warning is shown. 2519 * 2520 * @param int $lag Slave lag 2521 */ 2522 public function showLagWarning( $lag ) { 2523 $config = $this->getConfig(); 2524 if ( $lag >= $config->get( 'SlaveLagWarning' ) ) { 2525 $message = $lag < $config->get( 'SlaveLagCritical' ) 2526 ? 'lag-warn-normal' 2527 : 'lag-warn-high'; 2528 $wrap = Html::rawElement( 'div', array( 'class' => "mw-{$message}" ), "\n$1\n" ); 2529 $this->wrapWikiMsg( "$wrap\n", array( $message, $this->getLanguage()->formatNum( $lag ) ) ); 2530 } 2531 } 2532 2533 public function showFatalError( $message ) { 2534 $this->prepareErrorPage( $this->msg( 'internalerror' ) ); 2535 2536 $this->addHTML( $message ); 2537 } 2538 2539 public function showUnexpectedValueError( $name, $val ) { 2540 $this->showFatalError( $this->msg( 'unexpected', $name, $val )->text() ); 2541 } 2542 2543 public function showFileCopyError( $old, $new ) { 2544 $this->showFatalError( $this->msg( 'filecopyerror', $old, $new )->text() ); 2545 } 2546 2547 public function showFileRenameError( $old, $new ) { 2548 $this->showFatalError( $this->msg( 'filerenameerror', $old, $new )->text() ); 2549 } 2550 2551 public function showFileDeleteError( $name ) { 2552 $this->showFatalError( $this->msg( 'filedeleteerror', $name )->text() ); 2553 } 2554 2555 public function showFileNotFoundError( $name ) { 2556 $this->showFatalError( $this->msg( 'filenotfound', $name )->text() ); 2557 } 2558 2559 /** 2560 * Add a "return to" link pointing to a specified title 2561 * 2562 * @param Title $title Title to link 2563 * @param array $query Query string parameters 2564 * @param string $text Text of the link (input is not escaped) 2565 * @param array $options Options array to pass to Linker 2566 */ 2567 public function addReturnTo( $title, array $query = array(), $text = null, $options = array() ) { 2568 $link = $this->msg( 'returnto' )->rawParams( 2569 Linker::link( $title, $text, array(), $query, $options ) )->escaped(); 2570 $this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" ); 2571 } 2572 2573 /** 2574 * Add a "return to" link pointing to a specified title, 2575 * or the title indicated in the request, or else the main page 2576 * 2577 * @param mixed $unused 2578 * @param Title|string $returnto Title or String to return to 2579 * @param string $returntoquery Query string for the return to link 2580 */ 2581 public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) { 2582 if ( $returnto == null ) { 2583 $returnto = $this->getRequest()->getText( 'returnto' ); 2584 } 2585 2586 if ( $returntoquery == null ) { 2587 $returntoquery = $this->getRequest()->getText( 'returntoquery' ); 2588 } 2589 2590 if ( $returnto === '' ) { 2591 $returnto = Title::newMainPage(); 2592 } 2593 2594 if ( is_object( $returnto ) ) { 2595 $titleObj = $returnto; 2596 } else { 2597 $titleObj = Title::newFromText( $returnto ); 2598 } 2599 if ( !is_object( $titleObj ) ) { 2600 $titleObj = Title::newMainPage(); 2601 } 2602 2603 $this->addReturnTo( $titleObj, wfCgiToArray( $returntoquery ) ); 2604 } 2605 2606 /** 2607 * @param Skin $sk The given Skin 2608 * @param bool $includeStyle Unused 2609 * @return string The doctype, opening "<html>", and head element. 2610 */ 2611 public function headElement( Skin $sk, $includeStyle = true ) { 2612 global $wgContLang; 2613 2614 $userdir = $this->getLanguage()->getDir(); 2615 $sitedir = $wgContLang->getDir(); 2616 2617 $ret = Html::htmlHeader( $sk->getHtmlElementAttributes() ); 2618 2619 if ( $this->getHTMLTitle() == '' ) { 2620 $this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() )->inContentLanguage() ); 2621 } 2622 2623 $openHead = Html::openElement( 'head' ); 2624 if ( $openHead ) { 2625 # Don't bother with the newline if $head == '' 2626 $ret .= "$openHead\n"; 2627 } 2628 2629 if ( !Html::isXmlMimeType( $this->getConfig()->get( 'MimeType' ) ) ) { 2630 // Add <meta charset="UTF-8"> 2631 // This should be before <title> since it defines the charset used by 2632 // text including the text inside <title>. 2633 // The spec recommends defining XHTML5's charset using the XML declaration 2634 // instead of meta. 2635 // Our XML declaration is output by Html::htmlHeader. 2636 // http://www.whatwg.org/html/semantics.html#attr-meta-http-equiv-content-type 2637 // http://www.whatwg.org/html/semantics.html#charset 2638 $ret .= Html::element( 'meta', array( 'charset' => 'UTF-8' ) ) . "\n"; 2639 } 2640 2641 $ret .= Html::element( 'title', null, $this->getHTMLTitle() ) . "\n"; 2642 2643 foreach ( $this->getHeadLinksArray() as $item ) { 2644 $ret .= $item . "\n"; 2645 } 2646 2647 // No newline after buildCssLinks since makeResourceLoaderLink did that already 2648 $ret .= $this->buildCssLinks(); 2649 2650 $ret .= $this->getHeadScripts() . "\n"; 2651 2652 foreach ( $this->mHeadItems as $item ) { 2653 $ret .= $item . "\n"; 2654 } 2655 2656 $closeHead = Html::closeElement( 'head' ); 2657 if ( $closeHead ) { 2658 $ret .= "$closeHead\n"; 2659 } 2660 2661 $bodyClasses = array(); 2662 $bodyClasses[] = 'mediawiki'; 2663 2664 # Classes for LTR/RTL directionality support 2665 $bodyClasses[] = $userdir; 2666 $bodyClasses[] = "sitedir-$sitedir"; 2667 2668 if ( $this->getLanguage()->capitalizeAllNouns() ) { 2669 # A <body> class is probably not the best way to do this . . . 2670 $bodyClasses[] = 'capitalize-all-nouns'; 2671 } 2672 2673 $bodyClasses[] = $sk->getPageClasses( $this->getTitle() ); 2674 $bodyClasses[] = 'skin-' . Sanitizer::escapeClass( $sk->getSkinName() ); 2675 $bodyClasses[] = 2676 'action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) ); 2677 2678 $bodyAttrs = array(); 2679 // While the implode() is not strictly needed, it's used for backwards compatibility 2680 // (this used to be built as a string and hooks likely still expect that). 2681 $bodyAttrs['class'] = implode( ' ', $bodyClasses ); 2682 2683 // Allow skins and extensions to add body attributes they need 2684 $sk->addToBodyAttributes( $this, $bodyAttrs ); 2685 wfRunHooks( 'OutputPageBodyAttributes', array( $this, $sk, &$bodyAttrs ) ); 2686 2687 $ret .= Html::openElement( 'body', $bodyAttrs ) . "\n"; 2688 2689 return $ret; 2690 } 2691 2692 /** 2693 * Get a ResourceLoader object associated with this OutputPage 2694 * 2695 * @return ResourceLoader 2696 */ 2697 public function getResourceLoader() { 2698 if ( is_null( $this->mResourceLoader ) ) { 2699 $this->mResourceLoader = new ResourceLoader( $this->getConfig() ); 2700 } 2701 return $this->mResourceLoader; 2702 } 2703 2704 /** 2705 * @todo Document 2706 * @param array|string $modules One or more module names 2707 * @param string $only ResourceLoaderModule TYPE_ class constant 2708 * @param bool $useESI 2709 * @param array $extraQuery Array with extra query parameters to add to each 2710 * request. array( param => value ). 2711 * @param bool $loadCall If true, output an (asynchronous) mw.loader.load() 2712 * call rather than a "<script src='...'>" tag. 2713 * @return string The html "<script>", "<link>" and "<style>" tags 2714 */ 2715 protected function makeResourceLoaderLink( $modules, $only, $useESI = false, 2716 array $extraQuery = array(), $loadCall = false 2717 ) { 2718 $modules = (array)$modules; 2719 2720 $links = array( 2721 'html' => '', 2722 'states' => array(), 2723 ); 2724 2725 if ( !count( $modules ) ) { 2726 return $links; 2727 } 2728 2729 if ( count( $modules ) > 1 ) { 2730 // Remove duplicate module requests 2731 $modules = array_unique( $modules ); 2732 // Sort module names so requests are more uniform 2733 sort( $modules ); 2734 2735 if ( ResourceLoader::inDebugMode() ) { 2736 // Recursively call us for every item 2737 foreach ( $modules as $name ) { 2738 $link = $this->makeResourceLoaderLink( $name, $only, $useESI ); 2739 $links['html'] .= $link['html']; 2740 $links['states'] += $link['states']; 2741 } 2742 return $links; 2743 } 2744 } 2745 2746 if ( !is_null( $this->mTarget ) ) { 2747 $extraQuery['target'] = $this->mTarget; 2748 } 2749 2750 // Create keyed-by-source and then keyed-by-group list of module objects from modules list 2751 $sortedModules = array(); 2752 $resourceLoader = $this->getResourceLoader(); 2753 $resourceLoaderUseESI = $this->getConfig()->get( 'ResourceLoaderUseESI' ); 2754 foreach ( $modules as $name ) { 2755 $module = $resourceLoader->getModule( $name ); 2756 # Check that we're allowed to include this module on this page 2757 if ( !$module 2758 || ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS ) 2759 && $only == ResourceLoaderModule::TYPE_SCRIPTS ) 2760 || ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_STYLES ) 2761 && $only == ResourceLoaderModule::TYPE_STYLES ) 2762 || ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_COMBINED ) 2763 && $only == ResourceLoaderModule::TYPE_COMBINED ) 2764 || ( $this->mTarget && !in_array( $this->mTarget, $module->getTargets() ) ) 2765 ) { 2766 continue; 2767 } 2768 2769 $sortedModules[$module->getSource()][$module->getGroup()][$name] = $module; 2770 } 2771 2772 foreach ( $sortedModules as $source => $groups ) { 2773 foreach ( $groups as $group => $grpModules ) { 2774 // Special handling for user-specific groups 2775 $user = null; 2776 if ( ( $group === 'user' || $group === 'private' ) && $this->getUser()->isLoggedIn() ) { 2777 $user = $this->getUser()->getName(); 2778 } 2779 2780 // Create a fake request based on the one we are about to make so modules return 2781 // correct timestamp and emptiness data 2782 $query = ResourceLoader::makeLoaderQuery( 2783 array(), // modules; not determined yet 2784 $this->getLanguage()->getCode(), 2785 $this->getSkin()->getSkinName(), 2786 $user, 2787 null, // version; not determined yet 2788 ResourceLoader::inDebugMode(), 2789 $only === ResourceLoaderModule::TYPE_COMBINED ? null : $only, 2790 $this->isPrintable(), 2791 $this->getRequest()->getBool( 'handheld' ), 2792 $extraQuery 2793 ); 2794 $context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) ); 2795 2796 2797 // Extract modules that know they're empty and see if we have one or more 2798 // raw modules 2799 $isRaw = false; 2800 foreach ( $grpModules as $key => $module ) { 2801 // Inline empty modules: since they're empty, just mark them as 'ready' (bug 46857) 2802 // If we're only getting the styles, we don't need to do anything for empty modules. 2803 if ( $module->isKnownEmpty( $context ) ) { 2804 unset( $grpModules[$key] ); 2805 if ( $only !== ResourceLoaderModule::TYPE_STYLES ) { 2806 $links['states'][$key] = 'ready'; 2807 } 2808 } 2809 2810 $isRaw |= $module->isRaw(); 2811 } 2812 2813 // If there are no non-empty modules, skip this group 2814 if ( count( $grpModules ) === 0 ) { 2815 continue; 2816 } 2817 2818 // Inline private modules. These can't be loaded through load.php for security 2819 // reasons, see bug 34907. Note that these modules should be loaded from 2820 // getHeadScripts() before the first loader call. Otherwise other modules can't 2821 // properly use them as dependencies (bug 30914) 2822 if ( $group === 'private' ) { 2823 if ( $only == ResourceLoaderModule::TYPE_STYLES ) { 2824 $links['html'] .= Html::inlineStyle( 2825 $resourceLoader->makeModuleResponse( $context, $grpModules ) 2826 ); 2827 } else { 2828 $links['html'] .= Html::inlineScript( 2829 ResourceLoader::makeLoaderConditionalScript( 2830 $resourceLoader->makeModuleResponse( $context, $grpModules ) 2831 ) 2832 ); 2833 } 2834 $links['html'] .= "\n"; 2835 continue; 2836 } 2837 2838 // Special handling for the user group; because users might change their stuff 2839 // on-wiki like user pages, or user preferences; we need to find the highest 2840 // timestamp of these user-changeable modules so we can ensure cache misses on change 2841 // This should NOT be done for the site group (bug 27564) because anons get that too 2842 // and we shouldn't be putting timestamps in Squid-cached HTML 2843 $version = null; 2844 if ( $group === 'user' ) { 2845 // Get the maximum timestamp 2846 $timestamp = 1; 2847 foreach ( $grpModules as $module ) { 2848 $timestamp = max( $timestamp, $module->getModifiedTime( $context ) ); 2849 } 2850 // Add a version parameter so cache will break when things change 2851 $query['version'] = wfTimestamp( TS_ISO_8601_BASIC, $timestamp ); 2852 } 2853 2854 $query['modules'] = ResourceLoader::makePackedModulesString( array_keys( $grpModules ) ); 2855 $moduleContext = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) ); 2856 $url = $resourceLoader->createLoaderURL( $source, $moduleContext, $extraQuery ); 2857 2858 if ( $useESI && $resourceLoaderUseESI ) { 2859 $esi = Xml::element( 'esi:include', array( 'src' => $url ) ); 2860 if ( $only == ResourceLoaderModule::TYPE_STYLES ) { 2861 $link = Html::inlineStyle( $esi ); 2862 } else { 2863 $link = Html::inlineScript( $esi ); 2864 } 2865 } else { 2866 // Automatically select style/script elements 2867 if ( $only === ResourceLoaderModule::TYPE_STYLES ) { 2868 $link = Html::linkedStyle( $url ); 2869 } elseif ( $loadCall ) { 2870 $link = Html::inlineScript( 2871 ResourceLoader::makeLoaderConditionalScript( 2872 Xml::encodeJsCall( 'mw.loader.load', array( $url, 'text/javascript', true ) ) 2873 ) 2874 ); 2875 } else { 2876 $link = Html::linkedScript( $url ); 2877 if ( $context->getOnly() === 'scripts' && !$context->getRaw() && !$isRaw ) { 2878 // Wrap only=script requests in a conditional as browsers not supported 2879 // by the startup module would unconditionally execute this module. 2880 // Otherwise users will get "ReferenceError: mw is undefined" or 2881 // "jQuery is undefined" from e.g. a "site" module. 2882 $link = Html::inlineScript( 2883 ResourceLoader::makeLoaderConditionalScript( 2884 Xml::encodeJsCall( 'document.write', array( $link ) ) 2885 ) 2886 ); 2887 } 2888 2889 // For modules requested directly in the html via <link> or <script>, 2890 // tell mw.loader they are being loading to prevent duplicate requests. 2891 foreach ( $grpModules as $key => $module ) { 2892 // Don't output state=loading for the startup module.. 2893 if ( $key !== 'startup' ) { 2894 $links['states'][$key] = 'loading'; 2895 } 2896 } 2897 } 2898 } 2899 2900 if ( $group == 'noscript' ) { 2901 $links['html'] .= Html::rawElement( 'noscript', array(), $link ) . "\n"; 2902 } else { 2903 $links['html'] .= $link . "\n"; 2904 } 2905 } 2906 } 2907 2908 return $links; 2909 } 2910 2911 /** 2912 * Build html output from an array of links from makeResourceLoaderLink. 2913 * @param array $links 2914 * @return string HTML 2915 */ 2916 protected static function getHtmlFromLoaderLinks( array $links ) { 2917 $html = ''; 2918 $states = array(); 2919 foreach ( $links as $link ) { 2920 if ( !is_array( $link ) ) { 2921 $html .= $link; 2922 } else { 2923 $html .= $link['html']; 2924 $states += $link['states']; 2925 } 2926 } 2927 2928 if ( count( $states ) ) { 2929 $html = Html::inlineScript( 2930 ResourceLoader::makeLoaderConditionalScript( 2931 ResourceLoader::makeLoaderStateScript( $states ) 2932 ) 2933 ) . "\n" . $html; 2934 } 2935 2936 return $html; 2937 } 2938 2939 /** 2940 * JS stuff to put in the "<head>". This is the startup module, config 2941 * vars and modules marked with position 'top' 2942 * 2943 * @return string HTML fragment 2944 */ 2945 function getHeadScripts() { 2946 // Startup - this will immediately load jquery and mediawiki modules 2947 $links = array(); 2948 $links[] = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true ); 2949 2950 // Load config before anything else 2951 $links[] = Html::inlineScript( 2952 ResourceLoader::makeLoaderConditionalScript( 2953 ResourceLoader::makeConfigSetScript( $this->getJSVars() ) 2954 ) 2955 ); 2956 2957 // Load embeddable private modules before any loader links 2958 // This needs to be TYPE_COMBINED so these modules are properly wrapped 2959 // in mw.loader.implement() calls and deferred until mw.user is available 2960 $embedScripts = array( 'user.options', 'user.tokens' ); 2961 $links[] = $this->makeResourceLoaderLink( $embedScripts, ResourceLoaderModule::TYPE_COMBINED ); 2962 2963 // Scripts and messages "only" requests marked for top inclusion 2964 // Messages should go first 2965 $links[] = $this->makeResourceLoaderLink( 2966 $this->getModuleMessages( true, 'top' ), 2967 ResourceLoaderModule::TYPE_MESSAGES 2968 ); 2969 $links[] = $this->makeResourceLoaderLink( 2970 $this->getModuleScripts( true, 'top' ), 2971 ResourceLoaderModule::TYPE_SCRIPTS 2972 ); 2973 2974 // Modules requests - let the client calculate dependencies and batch requests as it likes 2975 // Only load modules that have marked themselves for loading at the top 2976 $modules = $this->getModules( true, 'top' ); 2977 if ( $modules ) { 2978 $links[] = Html::inlineScript( 2979 ResourceLoader::makeLoaderConditionalScript( 2980 Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) 2981 ) 2982 ); 2983 } 2984 2985 if ( $this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) { 2986 $links[] = $this->getScriptsForBottomQueue( true ); 2987 } 2988 2989 return self::getHtmlFromLoaderLinks( $links ); 2990 } 2991 2992 /** 2993 * JS stuff to put at the 'bottom', which can either be the bottom of the 2994 * "<body>" or the bottom of the "<head>" depending on 2995 * $wgResourceLoaderExperimentalAsyncLoading: modules marked with position 2996 * 'bottom', legacy scripts ($this->mScripts), user preferences, site JS 2997 * and user JS. 2998 * 2999 * @param bool $inHead If true, this HTML goes into the "<head>", 3000 * if false it goes into the "<body>". 3001 * @return string 3002 */ 3003 function getScriptsForBottomQueue( $inHead ) { 3004 // Scripts and messages "only" requests marked for bottom inclusion 3005 // If we're in the <head>, use load() calls rather than <script src="..."> tags 3006 // Messages should go first 3007 $links = array(); 3008 $links[] = $this->makeResourceLoaderLink( $this->getModuleMessages( true, 'bottom' ), 3009 ResourceLoaderModule::TYPE_MESSAGES, /* $useESI = */ false, /* $extraQuery = */ array(), 3010 /* $loadCall = */ $inHead 3011 ); 3012 $links[] = $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'bottom' ), 3013 ResourceLoaderModule::TYPE_SCRIPTS, /* $useESI = */ false, /* $extraQuery = */ array(), 3014 /* $loadCall = */ $inHead 3015 ); 3016 3017 // Modules requests - let the client calculate dependencies and batch requests as it likes 3018 // Only load modules that have marked themselves for loading at the bottom 3019 $modules = $this->getModules( true, 'bottom' ); 3020 if ( $modules ) { 3021 $links[] = Html::inlineScript( 3022 ResourceLoader::makeLoaderConditionalScript( 3023 Xml::encodeJsCall( 'mw.loader.load', array( $modules, null, true ) ) 3024 ) 3025 ); 3026 } 3027 3028 // Legacy Scripts 3029 $links[] = "\n" . $this->mScripts; 3030 3031 // Add site JS if enabled 3032 $links[] = $this->makeResourceLoaderLink( 'site', ResourceLoaderModule::TYPE_SCRIPTS, 3033 /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead 3034 ); 3035 3036 // Add user JS if enabled 3037 if ( $this->getConfig()->get( 'AllowUserJs' ) 3038 && $this->getUser()->isLoggedIn() 3039 && $this->getTitle() 3040 && $this->getTitle()->isJsSubpage() 3041 && $this->userCanPreview() 3042 ) { 3043 # XXX: additional security check/prompt? 3044 // We're on a preview of a JS subpage 3045 // Exclude this page from the user module in case it's in there (bug 26283) 3046 $links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false, 3047 array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead 3048 ); 3049 // Load the previewed JS 3050 $links[] = Html::inlineScript( "\n" 3051 . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n"; 3052 3053 // FIXME: If the user is previewing, say, ./vector.js, his ./common.js will be loaded 3054 // asynchronously and may arrive *after* the inline script here. So the previewed code 3055 // may execute before ./common.js runs. Normally, ./common.js runs before ./vector.js... 3056 } else { 3057 // Include the user module normally, i.e., raw to avoid it being wrapped in a closure. 3058 $links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, 3059 /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead 3060 ); 3061 } 3062 3063 // Group JS is only enabled if site JS is enabled. 3064 $links[] = $this->makeResourceLoaderLink( 'user.groups', ResourceLoaderModule::TYPE_COMBINED, 3065 /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead 3066 ); 3067 3068 return self::getHtmlFromLoaderLinks( $links ); 3069 } 3070 3071 /** 3072 * JS stuff to put at the bottom of the "<body>" 3073 * @return string 3074 */ 3075 function getBottomScripts() { 3076 // Optimise jQuery ready event cross-browser. 3077 // This also enforces $.isReady to be true at </body> which fixes the 3078 // mw.loader bug in Firefox with using document.write between </body> 3079 // and the DOMContentReady event (bug 47457). 3080 $html = Html::inlineScript( 'window.jQuery && jQuery.ready();' ); 3081 3082 if ( !$this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) { 3083 $html .= $this->getScriptsForBottomQueue( false ); 3084 } 3085 3086 return $html; 3087 } 3088 3089 /** 3090 * Get the javascript config vars to include on this page 3091 * 3092 * @return array Array of javascript config vars 3093 * @since 1.23 3094 */ 3095 public function getJsConfigVars() { 3096 return $this->mJsConfigVars; 3097 } 3098 3099 /** 3100 * Add one or more variables to be set in mw.config in JavaScript 3101 * 3102 * @param string|array $keys Key or array of key/value pairs 3103 * @param mixed $value [optional] Value of the configuration variable 3104 */ 3105 public function addJsConfigVars( $keys, $value = null ) { 3106 if ( is_array( $keys ) ) { 3107 foreach ( $keys as $key => $value ) { 3108 $this->mJsConfigVars[$key] = $value; 3109 } 3110 return; 3111 } 3112 3113 $this->mJsConfigVars[$keys] = $value; 3114 } 3115 3116 /** 3117 * Get an array containing the variables to be set in mw.config in JavaScript. 3118 * 3119 * Do not add things here which can be evaluated in ResourceLoaderStartUpModule 3120 * - in other words, page-independent/site-wide variables (without state). 3121 * You will only be adding bloat to the html page and causing page caches to 3122 * have to be purged on configuration changes. 3123 * @return array 3124 */ 3125 private function getJSVars() { 3126 global $wgContLang; 3127 3128 $curRevisionId = 0; 3129 $articleId = 0; 3130 $canonicalSpecialPageName = false; # bug 21115 3131 3132 $title = $this->getTitle(); 3133 $ns = $title->getNamespace(); 3134 $canonicalNamespace = MWNamespace::exists( $ns ) 3135 ? MWNamespace::getCanonicalName( $ns ) 3136 : $title->getNsText(); 3137 3138 $sk = $this->getSkin(); 3139 // Get the relevant title so that AJAX features can use the correct page name 3140 // when making API requests from certain special pages (bug 34972). 3141 $relevantTitle = $sk->getRelevantTitle(); 3142 $relevantUser = $sk->getRelevantUser(); 3143 3144 if ( $ns == NS_SPECIAL ) { 3145 list( $canonicalSpecialPageName, /*...*/ ) = 3146 SpecialPageFactory::resolveAlias( $title->getDBkey() ); 3147 } elseif ( $this->canUseWikiPage() ) { 3148 $wikiPage = $this->getWikiPage(); 3149 $curRevisionId = $wikiPage->getLatest(); 3150 $articleId = $wikiPage->getId(); 3151 } 3152 3153 $lang = $title->getPageLanguage(); 3154 3155 // Pre-process information 3156 $separatorTransTable = $lang->separatorTransformTable(); 3157 $separatorTransTable = $separatorTransTable ? $separatorTransTable : array(); 3158 $compactSeparatorTransTable = array( 3159 implode( "\t", array_keys( $separatorTransTable ) ), 3160 implode( "\t", $separatorTransTable ), 3161 ); 3162 $digitTransTable = $lang->digitTransformTable(); 3163 $digitTransTable = $digitTransTable ? $digitTransTable : array(); 3164 $compactDigitTransTable = array( 3165 implode( "\t", array_keys( $digitTransTable ) ), 3166 implode( "\t", $digitTransTable ), 3167 ); 3168 3169 $user = $this->getUser(); 3170 3171 $vars = array( 3172 'wgCanonicalNamespace' => $canonicalNamespace, 3173 'wgCanonicalSpecialPageName' => $canonicalSpecialPageName, 3174 'wgNamespaceNumber' => $title->getNamespace(), 3175 'wgPageName' => $title->getPrefixedDBkey(), 3176 'wgTitle' => $title->getText(), 3177 'wgCurRevisionId' => $curRevisionId, 3178 'wgRevisionId' => (int)$this->getRevisionId(), 3179 'wgArticleId' => $articleId, 3180 'wgIsArticle' => $this->isArticle(), 3181 'wgIsRedirect' => $title->isRedirect(), 3182 'wgAction' => Action::getActionName( $this->getContext() ), 3183 'wgUserName' => $user->isAnon() ? null : $user->getName(), 3184 'wgUserGroups' => $user->getEffectiveGroups(), 3185 'wgCategories' => $this->getCategories(), 3186 'wgBreakFrames' => $this->getFrameOptions() == 'DENY', 3187 'wgPageContentLanguage' => $lang->getCode(), 3188 'wgPageContentModel' => $title->getContentModel(), 3189 'wgSeparatorTransformTable' => $compactSeparatorTransTable, 3190 'wgDigitTransformTable' => $compactDigitTransTable, 3191 'wgDefaultDateFormat' => $lang->getDefaultDateFormat(), 3192 'wgMonthNames' => $lang->getMonthNamesArray(), 3193 'wgMonthNamesShort' => $lang->getMonthAbbreviationsArray(), 3194 'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(), 3195 ); 3196 3197 if ( $user->isLoggedIn() ) { 3198 $vars['wgUserId'] = $user->getId(); 3199 $vars['wgUserEditCount'] = $user->getEditCount(); 3200 $userReg = wfTimestampOrNull( TS_UNIX, $user->getRegistration() ); 3201 $vars['wgUserRegistration'] = $userReg !== null ? ( $userReg * 1000 ) : null; 3202 // Get the revision ID of the oldest new message on the user's talk 3203 // page. This can be used for constructing new message alerts on 3204 // the client side. 3205 $vars['wgUserNewMsgRevisionId'] = $user->getNewMessageRevisionId(); 3206 } 3207 3208 if ( $wgContLang->hasVariants() ) { 3209 $vars['wgUserVariant'] = $wgContLang->getPreferredVariant(); 3210 } 3211 // Same test as SkinTemplate 3212 $vars['wgIsProbablyEditable'] = $title->quickUserCan( 'edit', $user ) 3213 && ( $title->exists() || $title->quickUserCan( 'create', $user ) ); 3214 3215 foreach ( $title->getRestrictionTypes() as $type ) { 3216 $vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type ); 3217 } 3218 3219 if ( $title->isMainPage() ) { 3220 $vars['wgIsMainPage'] = true; 3221 } 3222 3223 if ( $this->mRedirectedFrom ) { 3224 $vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBkey(); 3225 } 3226 3227 if ( $relevantUser ) { 3228 $vars['wgRelevantUserName'] = $relevantUser->getName(); 3229 } 3230 3231 // Allow extensions to add their custom variables to the mw.config map. 3232 // Use the 'ResourceLoaderGetConfigVars' hook if the variable is not 3233 // page-dependant but site-wide (without state). 3234 // Alternatively, you may want to use OutputPage->addJsConfigVars() instead. 3235 wfRunHooks( 'MakeGlobalVariablesScript', array( &$vars, $this ) ); 3236 3237 // Merge in variables from addJsConfigVars last 3238 return array_merge( $vars, $this->getJsConfigVars() ); 3239 } 3240 3241 /** 3242 * To make it harder for someone to slip a user a fake 3243 * user-JavaScript or user-CSS preview, a random token 3244 * is associated with the login session. If it's not 3245 * passed back with the preview request, we won't render 3246 * the code. 3247 * 3248 * @return bool 3249 */ 3250 public function userCanPreview() { 3251 if ( $this->getRequest()->getVal( 'action' ) != 'submit' 3252 || !$this->getRequest()->wasPosted() 3253 || !$this->getUser()->matchEditToken( 3254 $this->getRequest()->getVal( 'wpEditToken' ) ) 3255 ) { 3256 return false; 3257 } 3258 if ( !$this->getTitle()->isJsSubpage() && !$this->getTitle()->isCssSubpage() ) { 3259 return false; 3260 } 3261 3262 return !count( $this->getTitle()->getUserPermissionsErrors( 'edit', $this->getUser() ) ); 3263 } 3264 3265 /** 3266 * @return array Array in format "link name or number => 'link html'". 3267 */ 3268 public function getHeadLinksArray() { 3269 global $wgVersion; 3270 3271 $tags = array(); 3272 $config = $this->getConfig(); 3273 3274 $canonicalUrl = $this->mCanonicalUrl; 3275 3276 $tags['meta-generator'] = Html::element( 'meta', array( 3277 'name' => 'generator', 3278 'content' => "MediaWiki $wgVersion", 3279 ) ); 3280 3281 $p = "{$this->mIndexPolicy},{$this->mFollowPolicy}"; 3282 if ( $p !== 'index,follow' ) { 3283 // http://www.robotstxt.org/wc/meta-user.html 3284 // Only show if it's different from the default robots policy 3285 $tags['meta-robots'] = Html::element( 'meta', array( 3286 'name' => 'robots', 3287 'content' => $p, 3288 ) ); 3289 } 3290 3291 foreach ( $this->mMetatags as $tag ) { 3292 if ( 0 == strcasecmp( 'http:', substr( $tag[0], 0, 5 ) ) ) { 3293 $a = 'http-equiv'; 3294 $tag[0] = substr( $tag[0], 5 ); 3295 } else { 3296 $a = 'name'; 3297 } 3298 $tagName = "meta-{$tag[0]}"; 3299 if ( isset( $tags[$tagName] ) ) { 3300 $tagName .= $tag[1]; 3301 } 3302 $tags[$tagName] = Html::element( 'meta', 3303 array( 3304 $a => $tag[0], 3305 'content' => $tag[1] 3306 ) 3307 ); 3308 } 3309 3310 foreach ( $this->mLinktags as $tag ) { 3311 $tags[] = Html::element( 'link', $tag ); 3312 } 3313 3314 # Universal edit button 3315 if ( $config->get( 'UniversalEditButton' ) && $this->isArticleRelated() ) { 3316 $user = $this->getUser(); 3317 if ( $this->getTitle()->quickUserCan( 'edit', $user ) 3318 && ( $this->getTitle()->exists() || $this->getTitle()->quickUserCan( 'create', $user ) ) ) { 3319 // Original UniversalEditButton 3320 $msg = $this->msg( 'edit' )->text(); 3321 $tags['universal-edit-button'] = Html::element( 'link', array( 3322 'rel' => 'alternate', 3323 'type' => 'application/x-wiki', 3324 'title' => $msg, 3325 'href' => $this->getTitle()->getEditURL(), 3326 ) ); 3327 // Alternate edit link 3328 $tags['alternative-edit'] = Html::element( 'link', array( 3329 'rel' => 'edit', 3330 'title' => $msg, 3331 'href' => $this->getTitle()->getEditURL(), 3332 ) ); 3333 } 3334 } 3335 3336 # Generally the order of the favicon and apple-touch-icon links 3337 # should not matter, but Konqueror (3.5.9 at least) incorrectly 3338 # uses whichever one appears later in the HTML source. Make sure 3339 # apple-touch-icon is specified first to avoid this. 3340 if ( $config->get( 'AppleTouchIcon' ) !== false ) { 3341 $tags['apple-touch-icon'] = Html::element( 'link', array( 3342 'rel' => 'apple-touch-icon', 3343 'href' => $config->get( 'AppleTouchIcon' ) 3344 ) ); 3345 } 3346 3347 if ( $config->get( 'Favicon' ) !== false ) { 3348 $tags['favicon'] = Html::element( 'link', array( 3349 'rel' => 'shortcut icon', 3350 'href' => $config->get( 'Favicon' ) 3351 ) ); 3352 } 3353 3354 # OpenSearch description link 3355 $tags['opensearch'] = Html::element( 'link', array( 3356 'rel' => 'search', 3357 'type' => 'application/opensearchdescription+xml', 3358 'href' => wfScript( 'opensearch_desc' ), 3359 'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(), 3360 ) ); 3361 3362 if ( $config->get( 'EnableAPI' ) ) { 3363 # Real Simple Discovery link, provides auto-discovery information 3364 # for the MediaWiki API (and potentially additional custom API 3365 # support such as WordPress or Twitter-compatible APIs for a 3366 # blogging extension, etc) 3367 $tags['rsd'] = Html::element( 'link', array( 3368 'rel' => 'EditURI', 3369 'type' => 'application/rsd+xml', 3370 // Output a protocol-relative URL here if $wgServer is protocol-relative 3371 // Whether RSD accepts relative or protocol-relative URLs is completely undocumented, though 3372 'href' => wfExpandUrl( wfAppendQuery( 3373 wfScript( 'api' ), 3374 array( 'action' => 'rsd' ) ), 3375 PROTO_RELATIVE 3376 ), 3377 ) ); 3378 } 3379 3380 # Language variants 3381 if ( !$config->get( 'DisableLangConversion' ) ) { 3382 $lang = $this->getTitle()->getPageLanguage(); 3383 if ( $lang->hasVariants() ) { 3384 $variants = $lang->getVariants(); 3385 foreach ( $variants as $_v ) { 3386 $tags["variant-$_v"] = Html::element( 'link', array( 3387 'rel' => 'alternate', 3388 'hreflang' => wfBCP47( $_v ), 3389 'href' => $this->getTitle()->getLocalURL( array( 'variant' => $_v ) ) ) 3390 ); 3391 } 3392 } 3393 # x-default link per https://support.google.com/webmasters/answer/189077?hl=en 3394 $tags["variant-x-default"] = Html::element( 'link', array( 3395 'rel' => 'alternate', 3396 'hreflang' => 'x-default', 3397 'href' => $this->getTitle()->getLocalURL() ) ); 3398 } 3399 3400 # Copyright 3401 $copyright = ''; 3402 if ( $config->get( 'RightsPage' ) ) { 3403 $copy = Title::newFromText( $config->get( 'RightsPage' ) ); 3404 3405 if ( $copy ) { 3406 $copyright = $copy->getLocalURL(); 3407 } 3408 } 3409 3410 if ( !$copyright && $config->get( 'RightsUrl' ) ) { 3411 $copyright = $config->get( 'RightsUrl' ); 3412 } 3413 3414 if ( $copyright ) { 3415 $tags['copyright'] = Html::element( 'link', array( 3416 'rel' => 'copyright', 3417 'href' => $copyright ) 3418 ); 3419 } 3420 3421 # Feeds 3422 if ( $config->get( 'Feed' ) ) { 3423 foreach ( $this->getSyndicationLinks() as $format => $link ) { 3424 # Use the page name for the title. In principle, this could 3425 # lead to issues with having the same name for different feeds 3426 # corresponding to the same page, but we can't avoid that at 3427 # this low a level. 3428 3429 $tags[] = $this->feedLink( 3430 $format, 3431 $link, 3432 # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep) 3433 $this->msg( "page-{$format}-feed", $this->getTitle()->getPrefixedText() )->text() 3434 ); 3435 } 3436 3437 # Recent changes feed should appear on every page (except recentchanges, 3438 # that would be redundant). Put it after the per-page feed to avoid 3439 # changing existing behavior. It's still available, probably via a 3440 # menu in your browser. Some sites might have a different feed they'd 3441 # like to promote instead of the RC feed (maybe like a "Recent New Articles" 3442 # or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined. 3443 # If so, use it instead. 3444 $sitename = $config->get( 'Sitename' ); 3445 if ( $config->get( 'OverrideSiteFeed' ) ) { 3446 foreach ( $config->get( 'OverrideSiteFeed' ) as $type => $feedUrl ) { 3447 // Note, this->feedLink escapes the url. 3448 $tags[] = $this->feedLink( 3449 $type, 3450 $feedUrl, 3451 $this->msg( "site-{$type}-feed", $sitename )->text() 3452 ); 3453 } 3454 } elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) { 3455 $rctitle = SpecialPage::getTitleFor( 'Recentchanges' ); 3456 foreach ( $config->get( 'AdvertisedFeedTypes' ) as $format ) { 3457 $tags[] = $this->feedLink( 3458 $format, 3459 $rctitle->getLocalURL( array( 'feed' => $format ) ), 3460 # For grep: 'site-rss-feed', 'site-atom-feed' 3461 $this->msg( "site-{$format}-feed", $sitename )->text() 3462 ); 3463 } 3464 } 3465 } 3466 3467 # Canonical URL 3468 if ( $config->get( 'EnableCanonicalServerLink' ) ) { 3469 if ( $canonicalUrl !== false ) { 3470 $canonicalUrl = wfExpandUrl( $canonicalUrl, PROTO_CANONICAL ); 3471 } else { 3472 $reqUrl = $this->getRequest()->getRequestURL(); 3473 $canonicalUrl = wfExpandUrl( $reqUrl, PROTO_CANONICAL ); 3474 } 3475 } 3476 if ( $canonicalUrl !== false ) { 3477 $tags[] = Html::element( 'link', array( 3478 'rel' => 'canonical', 3479 'href' => $canonicalUrl 3480 ) ); 3481 } 3482 3483 return $tags; 3484 } 3485 3486 /** 3487 * @return string HTML tag links to be put in the header. 3488 * @deprecated since 1.24 Use OutputPage::headElement or if you have to, 3489 * OutputPage::getHeadLinksArray directly. 3490 */ 3491 public function getHeadLinks() { 3492 wfDeprecated( __METHOD__, '1.24' ); 3493 return implode( "\n", $this->getHeadLinksArray() ); 3494 } 3495 3496 /** 3497 * Generate a "<link rel/>" for a feed. 3498 * 3499 * @param string $type Feed type 3500 * @param string $url URL to the feed 3501 * @param string $text Value of the "title" attribute 3502 * @return string HTML fragment 3503 */ 3504 private function feedLink( $type, $url, $text ) { 3505 return Html::element( 'link', array( 3506 'rel' => 'alternate', 3507 'type' => "application/$type+xml", 3508 'title' => $text, 3509 'href' => $url ) 3510 ); 3511 } 3512 3513 /** 3514 * Add a local or specified stylesheet, with the given media options. 3515 * Meant primarily for internal use... 3516 * 3517 * @param string $style URL to the file 3518 * @param string $media To specify a media type, 'screen', 'printable', 'handheld' or any. 3519 * @param string $condition For IE conditional comments, specifying an IE version 3520 * @param string $dir Set to 'rtl' or 'ltr' for direction-specific sheets 3521 */ 3522 public function addStyle( $style, $media = '', $condition = '', $dir = '' ) { 3523 $options = array(); 3524 // Even though we expect the media type to be lowercase, but here we 3525 // force it to lowercase to be safe. 3526 if ( $media ) { 3527 $options['media'] = $media; 3528 } 3529 if ( $condition ) { 3530 $options['condition'] = $condition; 3531 } 3532 if ( $dir ) { 3533 $options['dir'] = $dir; 3534 } 3535 $this->styles[$style] = $options; 3536 } 3537 3538 /** 3539 * Adds inline CSS styles 3540 * @param mixed $style_css Inline CSS 3541 * @param string $flip Set to 'flip' to flip the CSS if needed 3542 */ 3543 public function addInlineStyle( $style_css, $flip = 'noflip' ) { 3544 if ( $flip === 'flip' && $this->getLanguage()->isRTL() ) { 3545 # If wanted, and the interface is right-to-left, flip the CSS 3546 $style_css = CSSJanus::transform( $style_css, true, false ); 3547 } 3548 $this->mInlineStyles .= Html::inlineStyle( $style_css ) . "\n"; 3549 } 3550 3551 /** 3552 * Build a set of "<link>" elements for the stylesheets specified in the $this->styles array. 3553 * These will be applied to various media & IE conditionals. 3554 * 3555 * @return string 3556 */ 3557 public function buildCssLinks() { 3558 global $wgContLang; 3559 3560 $this->getSkin()->setupSkinUserCss( $this ); 3561 3562 // Add ResourceLoader styles 3563 // Split the styles into these groups 3564 $styles = array( 3565 'other' => array(), 3566 'user' => array(), 3567 'site' => array(), 3568 'private' => array(), 3569 'noscript' => array() 3570 ); 3571 $links = array(); 3572 $otherTags = ''; // Tags to append after the normal <link> tags 3573 $resourceLoader = $this->getResourceLoader(); 3574 3575 $moduleStyles = $this->getModuleStyles(); 3576 3577 // Per-site custom styles 3578 $moduleStyles[] = 'site'; 3579 $moduleStyles[] = 'noscript'; 3580 $moduleStyles[] = 'user.groups'; 3581 3582 // Per-user custom styles 3583 if ( $this->getConfig()->get( 'AllowUserCss' ) && $this->getTitle()->isCssSubpage() && $this->userCanPreview() ) { 3584 // We're on a preview of a CSS subpage 3585 // Exclude this page from the user module in case it's in there (bug 26283) 3586 $link = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_STYLES, false, 3587 array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ) 3588 ); 3589 $otherTags .= $link['html']; 3590 3591 // Load the previewed CSS 3592 // If needed, Janus it first. This is user-supplied CSS, so it's 3593 // assumed to be right for the content language directionality. 3594 $previewedCSS = $this->getRequest()->getText( 'wpTextbox1' ); 3595 if ( $this->getLanguage()->getDir() !== $wgContLang->getDir() ) { 3596 $previewedCSS = CSSJanus::transform( $previewedCSS, true, false ); 3597 } 3598 $otherTags .= Html::inlineStyle( $previewedCSS ) . "\n"; 3599 } else { 3600 // Load the user styles normally 3601 $moduleStyles[] = 'user'; 3602 } 3603 3604 // Per-user preference styles 3605 $moduleStyles[] = 'user.cssprefs'; 3606 3607 foreach ( $moduleStyles as $name ) { 3608 $module = $resourceLoader->getModule( $name ); 3609 if ( !$module ) { 3610 continue; 3611 } 3612 $group = $module->getGroup(); 3613 // Modules in groups different than the ones listed on top (see $styles assignment) 3614 // will be placed in the "other" group 3615 $styles[isset( $styles[$group] ) ? $group : 'other'][] = $name; 3616 } 3617 3618 // We want site, private and user styles to override dynamically added 3619 // styles from modules, but we want dynamically added styles to override 3620 // statically added styles from other modules. So the order has to be 3621 // other, dynamic, site, private, user. Add statically added styles for 3622 // other modules 3623 $links[] = $this->makeResourceLoaderLink( $styles['other'], ResourceLoaderModule::TYPE_STYLES ); 3624 // Add normal styles added through addStyle()/addInlineStyle() here 3625 $links[] = implode( "\n", $this->buildCssLinksArray() ) . $this->mInlineStyles; 3626 // Add marker tag to mark the place where the client-side loader should inject dynamic styles 3627 // We use a <meta> tag with a made-up name for this because that's valid HTML 3628 $links[] = Html::element( 3629 'meta', 3630 array( 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ) 3631 ) . "\n"; 3632 3633 // Add site, private and user styles 3634 // 'private' at present only contains user.options, so put that before 'user' 3635 // Any future private modules will likely have a similar user-specific character 3636 foreach ( array( 'site', 'noscript', 'private', 'user' ) as $group ) { 3637 $links[] = $this->makeResourceLoaderLink( $styles[$group], 3638 ResourceLoaderModule::TYPE_STYLES 3639 ); 3640 } 3641 3642 // Add stuff in $otherTags (previewed user CSS if applicable) 3643 return self::getHtmlFromLoaderLinks( $links ) . $otherTags; 3644 } 3645 3646 /** 3647 * @return array 3648 */ 3649 public function buildCssLinksArray() { 3650 $links = array(); 3651 3652 // Add any extension CSS 3653 foreach ( $this->mExtStyles as $url ) { 3654 $this->addStyle( $url ); 3655 } 3656 $this->mExtStyles = array(); 3657 3658 foreach ( $this->styles as $file => $options ) { 3659 $link = $this->styleLink( $file, $options ); 3660 if ( $link ) { 3661 $links[$file] = $link; 3662 } 3663 } 3664 return $links; 3665 } 3666 3667 /** 3668 * Generate \<link\> tags for stylesheets 3669 * 3670 * @param string $style URL to the file 3671 * @param array $options Option, can contain 'condition', 'dir', 'media' keys 3672 * @return string HTML fragment 3673 */ 3674 protected function styleLink( $style, array $options ) { 3675 if ( isset( $options['dir'] ) ) { 3676 if ( $this->getLanguage()->getDir() != $options['dir'] ) { 3677 return ''; 3678 } 3679 } 3680 3681 if ( isset( $options['media'] ) ) { 3682 $media = self::transformCssMedia( $options['media'] ); 3683 if ( is_null( $media ) ) { 3684 return ''; 3685 } 3686 } else { 3687 $media = 'all'; 3688 } 3689 3690 if ( substr( $style, 0, 1 ) == '/' || 3691 substr( $style, 0, 5 ) == 'http:' || 3692 substr( $style, 0, 6 ) == 'https:' ) { 3693 $url = $style; 3694 } else { 3695 $config = $this->getConfig(); 3696 $url = $config->get( 'StylePath' ) . '/' . $style . '?' . $config->get( 'StyleVersion' ); 3697 } 3698 3699 $link = Html::linkedStyle( $url, $media ); 3700 3701 if ( isset( $options['condition'] ) ) { 3702 $condition = htmlspecialchars( $options['condition'] ); 3703 $link = "<!--[if $condition]>$link<![endif]-->"; 3704 } 3705 return $link; 3706 } 3707 3708 /** 3709 * Transform "media" attribute based on request parameters 3710 * 3711 * @param string $media Current value of the "media" attribute 3712 * @return string Modified value of the "media" attribute, or null to skip 3713 * this stylesheet 3714 */ 3715 public static function transformCssMedia( $media ) { 3716 global $wgRequest; 3717 3718 // http://www.w3.org/TR/css3-mediaqueries/#syntax 3719 $screenMediaQueryRegex = '/^(?:only\s+)?screen\b/i'; 3720 3721 // Switch in on-screen display for media testing 3722 $switches = array( 3723 'printable' => 'print', 3724 'handheld' => 'handheld', 3725 ); 3726 foreach ( $switches as $switch => $targetMedia ) { 3727 if ( $wgRequest->getBool( $switch ) ) { 3728 if ( $media == $targetMedia ) { 3729 $media = ''; 3730 } elseif ( preg_match( $screenMediaQueryRegex, $media ) === 1 ) { 3731 // This regex will not attempt to understand a comma-separated media_query_list 3732 // 3733 // Example supported values for $media: 3734 // 'screen', 'only screen', 'screen and (min-width: 982px)' ), 3735 // Example NOT supported value for $media: 3736 // '3d-glasses, screen, print and resolution > 90dpi' 3737 // 3738 // If it's a print request, we never want any kind of screen stylesheets 3739 // If it's a handheld request (currently the only other choice with a switch), 3740 // we don't want simple 'screen' but we might want screen queries that 3741 // have a max-width or something, so we'll pass all others on and let the 3742 // client do the query. 3743 if ( $targetMedia == 'print' || $media == 'screen' ) { 3744 return null; 3745 } 3746 } 3747 } 3748 } 3749 3750 return $media; 3751 } 3752 3753 /** 3754 * Add a wikitext-formatted message to the output. 3755 * This is equivalent to: 3756 * 3757 * $wgOut->addWikiText( wfMessage( ... )->plain() ) 3758 */ 3759 public function addWikiMsg( /*...*/ ) { 3760 $args = func_get_args(); 3761 $name = array_shift( $args ); 3762 $this->addWikiMsgArray( $name, $args ); 3763 } 3764 3765 /** 3766 * Add a wikitext-formatted message to the output. 3767 * Like addWikiMsg() except the parameters are taken as an array 3768 * instead of a variable argument list. 3769 * 3770 * @param string $name 3771 * @param array $args 3772 */ 3773 public function addWikiMsgArray( $name, $args ) { 3774 $this->addHTML( $this->msg( $name, $args )->parseAsBlock() ); 3775 } 3776 3777 /** 3778 * This function takes a number of message/argument specifications, wraps them in 3779 * some overall structure, and then parses the result and adds it to the output. 3780 * 3781 * In the $wrap, $1 is replaced with the first message, $2 with the second, and so 3782 * on. The subsequent arguments may either be strings, in which case they are the 3783 * message names, or arrays, in which case the first element is the message name, 3784 * and subsequent elements are the parameters to that message. 3785 * 3786 * Don't use this for messages that are not in users interface language. 3787 * 3788 * For example: 3789 * 3790 * $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>", 'some-error' ); 3791 * 3792 * Is equivalent to: 3793 * 3794 * $wgOut->addWikiText( "<div class='error'>\n" 3795 * . wfMessage( 'some-error' )->plain() . "\n</div>" ); 3796 * 3797 * The newline after opening div is needed in some wikitext. See bug 19226. 3798 * 3799 * @param string $wrap 3800 */ 3801 public function wrapWikiMsg( $wrap /*, ...*/ ) { 3802 $msgSpecs = func_get_args(); 3803 array_shift( $msgSpecs ); 3804 $msgSpecs = array_values( $msgSpecs ); 3805 $s = $wrap; 3806 foreach ( $msgSpecs as $n => $spec ) { 3807 if ( is_array( $spec ) ) { 3808 $args = $spec; 3809 $name = array_shift( $args ); 3810 if ( isset( $args['options'] ) ) { 3811 unset( $args['options'] ); 3812 wfDeprecated( 3813 'Adding "options" to ' . __METHOD__ . ' is no longer supported', 3814 '1.20' 3815 ); 3816 } 3817 } else { 3818 $args = array(); 3819 $name = $spec; 3820 } 3821 $s = str_replace( '$' . ( $n + 1 ), $this->msg( $name, $args )->plain(), $s ); 3822 } 3823 $this->addWikiText( $s ); 3824 } 3825 3826 /** 3827 * Include jQuery core. Use this to avoid loading it multiple times 3828 * before we get a usable script loader. 3829 * 3830 * @param array $modules List of jQuery modules which should be loaded 3831 * @return array The list of modules which were not loaded. 3832 * @since 1.16 3833 * @deprecated since 1.17 3834 */ 3835 public function includeJQuery( array $modules = array() ) { 3836 return array(); 3837 } 3838 3839 /** 3840 * Enables/disables TOC, doesn't override __NOTOC__ 3841 * @param bool $flag 3842 * @since 1.22 3843 */ 3844 public function enableTOC( $flag = true ) { 3845 $this->mEnableTOC = $flag; 3846 } 3847 3848 /** 3849 * @return bool 3850 * @since 1.22 3851 */ 3852 public function isTOCEnabled() { 3853 return $this->mEnableTOC; 3854 } 3855 3856 /** 3857 * Enables/disables section edit links, doesn't override __NOEDITSECTION__ 3858 * @param bool $flag 3859 * @since 1.23 3860 */ 3861 public function enableSectionEditLinks( $flag = true ) { 3862 $this->mEnableSectionEditLinks = $flag; 3863 } 3864 3865 /** 3866 * @return bool 3867 * @since 1.23 3868 */ 3869 public function sectionEditLinksEnabled() { 3870 return $this->mEnableSectionEditLinks; 3871 } 3872 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |