PageRenderTime 82ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/includes/OutputPage.php

https://bitbucket.org/brunodefraine/mediawiki
PHP | 3506 lines | 1809 code | 399 blank | 1298 comment | 309 complexity | bead357dfa806c0fa23a5034c56272ad MD5 | raw file
Possible License(s): GPL-2.0, Apache-2.0, LGPL-3.0
  1. <?php
  2. if ( !defined( 'MEDIAWIKI' ) ) {
  3. die( 1 );
  4. }
  5. /**
  6. * This class should be covered by a general architecture document which does
  7. * not exist as of January 2011. This is one of the Core classes and should
  8. * be read at least once by any new developers.
  9. *
  10. * This class is used to prepare the final rendering. A skin is then
  11. * applied to the output parameters (links, javascript, html, categories ...).
  12. *
  13. * @todo FIXME: Another class handles sending the whole page to the client.
  14. *
  15. * Some comments comes from a pairing session between Zak Greant and Antoine Musso
  16. * in November 2010.
  17. *
  18. * @todo document
  19. */
  20. class OutputPage extends ContextSource {
  21. /// Should be private. Used with addMeta() which adds <meta>
  22. var $mMetatags = array();
  23. /// <meta keyworkds="stuff"> most of the time the first 10 links to an article
  24. var $mKeywords = array();
  25. var $mLinktags = array();
  26. /// Additional stylesheets. Looks like this is for extensions. Might be replaced by resource loader.
  27. var $mExtStyles = array();
  28. /// Should be private - has getter and setter. Contains the HTML title
  29. var $mPagetitle = '';
  30. /// Contains all of the <body> content. Should be private we got set/get accessors and the append() method.
  31. var $mBodytext = '';
  32. /**
  33. * Holds the debug lines that will be output as comments in page source if
  34. * $wgDebugComments is enabled. See also $wgShowDebug.
  35. * TODO: make a getter method for this
  36. */
  37. public $mDebugtext = ''; // TODO: we might want to replace it by wfDebug() wfDebugLog()
  38. /// Should be private. Stores contents of <title> tag
  39. var $mHTMLtitle = '';
  40. /// Should be private. Is the displayed content related to the source of the corresponding wiki article.
  41. var $mIsarticle = false;
  42. /**
  43. * Should be private. Has get/set methods properly documented.
  44. * Stores "article flag" toggle.
  45. */
  46. var $mIsArticleRelated = true;
  47. /**
  48. * Should be private. We have to set isPrintable(). Some pages should
  49. * never be printed (ex: redirections).
  50. */
  51. var $mPrintable = false;
  52. /**
  53. * Should be private. We have set/get/append methods.
  54. *
  55. * Contains the page subtitle. Special pages usually have some links here.
  56. * Don't confuse with site subtitle added by skins.
  57. */
  58. private $mSubtitle = array();
  59. var $mRedirect = '';
  60. var $mStatusCode;
  61. /**
  62. * mLastModified and mEtag are used for sending cache control.
  63. * The whole caching system should probably be moved into its own class.
  64. */
  65. var $mLastModified = '';
  66. /**
  67. * Should be private. No getter but used in sendCacheControl();
  68. * Contains an HTTP Entity Tags (see RFC 2616 section 3.13) which is used
  69. * as a unique identifier for the content. It is later used by the client
  70. * to compare its cached version with the server version. Client sends
  71. * headers If-Match and If-None-Match containing its locally cached ETAG value.
  72. *
  73. * To get more information, you will have to look at HTTP/1.1 protocol which
  74. * is properly described in RFC 2616 : http://tools.ietf.org/html/rfc2616
  75. */
  76. var $mETag = false;
  77. var $mCategoryLinks = array();
  78. var $mCategories = array();
  79. /// Should be private. Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page')
  80. var $mLanguageLinks = array();
  81. /**
  82. * Should be private. Used for JavaScript (pre resource loader)
  83. * We should split js / css.
  84. * mScripts content is inserted as is in <head> by Skin. This might contains
  85. * either a link to a stylesheet or inline css.
  86. */
  87. var $mScripts = '';
  88. /**
  89. * Inline CSS styles. Use addInlineStyle() sparsingly
  90. */
  91. var $mInlineStyles = '';
  92. //
  93. var $mLinkColours;
  94. /**
  95. * Used by skin template.
  96. * Example: $tpl->set( 'displaytitle', $out->mPageLinkTitle );
  97. */
  98. var $mPageLinkTitle = '';
  99. /// Array of elements in <head>. Parser might add its own headers!
  100. var $mHeadItems = array();
  101. // @todo FIXME: Next variables probably comes from the resource loader
  102. var $mModules = array(), $mModuleScripts = array(), $mModuleStyles = array(), $mModuleMessages = array();
  103. var $mResourceLoader;
  104. var $mJsConfigVars = array();
  105. /** @todo FIXME: Is this still used ?*/
  106. var $mInlineMsg = array();
  107. var $mTemplateIds = array();
  108. var $mImageTimeKeys = array();
  109. var $mRedirectCode = '';
  110. var $mFeedLinksAppendQuery = null;
  111. # What level of 'untrustworthiness' is allowed in CSS/JS modules loaded on this page?
  112. # @see ResourceLoaderModule::$origin
  113. # ResourceLoaderModule::ORIGIN_ALL is assumed unless overridden;
  114. protected $mAllowedModules = array(
  115. ResourceLoaderModule::TYPE_COMBINED => ResourceLoaderModule::ORIGIN_ALL,
  116. );
  117. /**
  118. * @EasterEgg I just love the name for this self documenting variable.
  119. * @todo document
  120. */
  121. var $mDoNothing = false;
  122. // Parser related.
  123. var $mContainsOldMagic = 0, $mContainsNewMagic = 0;
  124. /**
  125. * lazy initialised, use parserOptions()
  126. * @var ParserOptions
  127. */
  128. protected $mParserOptions = null;
  129. /**
  130. * Handles the atom / rss links.
  131. * We probably only support atom in 2011.
  132. * Looks like a private variable.
  133. * @see $wgAdvertisedFeedTypes
  134. */
  135. var $mFeedLinks = array();
  136. // Gwicke work on squid caching? Roughly from 2003.
  137. var $mEnableClientCache = true;
  138. /**
  139. * Flag if output should only contain the body of the article.
  140. * Should be private.
  141. */
  142. var $mArticleBodyOnly = false;
  143. var $mNewSectionLink = false;
  144. var $mHideNewSectionLink = false;
  145. /**
  146. * Comes from the parser. This was probably made to load CSS/JS only
  147. * if we had <gallery>. Used directly in CategoryPage.php
  148. * Looks like resource loader can replace this.
  149. */
  150. var $mNoGallery = false;
  151. // should be private.
  152. var $mPageTitleActionText = '';
  153. var $mParseWarnings = array();
  154. // Cache stuff. Looks like mEnableClientCache
  155. var $mSquidMaxage = 0;
  156. // @todo document
  157. var $mPreventClickjacking = true;
  158. /// should be private. To include the variable {{REVISIONID}}
  159. var $mRevisionId = null;
  160. private $mRevisionTimestamp = null;
  161. var $mFileVersion = null;
  162. /**
  163. * An array of stylesheet filenames (relative from skins path), with options
  164. * for CSS media, IE conditions, and RTL/LTR direction.
  165. * For internal use; add settings in the skin via $this->addStyle()
  166. *
  167. * Style again! This seems like a code duplication since we already have
  168. * mStyles. This is what makes OpenSource amazing.
  169. */
  170. var $styles = array();
  171. /**
  172. * Whether jQuery is already handled.
  173. */
  174. protected $mJQueryDone = false;
  175. private $mIndexPolicy = 'index';
  176. private $mFollowPolicy = 'follow';
  177. private $mVaryHeader = array(
  178. 'Accept-Encoding' => array( 'list-contains=gzip' ),
  179. 'Cookie' => null
  180. );
  181. /**
  182. * If the current page was reached through a redirect, $mRedirectedFrom contains the Title
  183. * of the redirect.
  184. *
  185. * @var Title
  186. */
  187. private $mRedirectedFrom = null;
  188. /**
  189. * Constructor for OutputPage. This should not be called directly.
  190. * Instead a new RequestContext should be created and it will implicitly create
  191. * a OutputPage tied to that context.
  192. */
  193. function __construct( IContextSource $context = null ) {
  194. if ( $context === null ) {
  195. # Extensions should use `new RequestContext` instead of `new OutputPage` now.
  196. wfDeprecated( __METHOD__ );
  197. } else {
  198. $this->setContext( $context );
  199. }
  200. }
  201. /**
  202. * Redirect to $url rather than displaying the normal page
  203. *
  204. * @param $url String: URL
  205. * @param $responsecode String: HTTP status code
  206. */
  207. public function redirect( $url, $responsecode = '302' ) {
  208. # Strip newlines as a paranoia check for header injection in PHP<5.1.2
  209. $this->mRedirect = str_replace( "\n", '', $url );
  210. $this->mRedirectCode = $responsecode;
  211. }
  212. /**
  213. * Get the URL to redirect to, or an empty string if not redirect URL set
  214. *
  215. * @return String
  216. */
  217. public function getRedirect() {
  218. return $this->mRedirect;
  219. }
  220. /**
  221. * Set the HTTP status code to send with the output.
  222. *
  223. * @param $statusCode Integer
  224. */
  225. public function setStatusCode( $statusCode ) {
  226. $this->mStatusCode = $statusCode;
  227. }
  228. /**
  229. * Add a new <meta> tag
  230. * To add an http-equiv meta tag, precede the name with "http:"
  231. *
  232. * @param $name String tag name
  233. * @param $val String tag value
  234. */
  235. function addMeta( $name, $val ) {
  236. array_push( $this->mMetatags, array( $name, $val ) );
  237. }
  238. /**
  239. * Add a keyword or a list of keywords in the page header
  240. *
  241. * @param $text String or array of strings
  242. */
  243. function addKeyword( $text ) {
  244. if( is_array( $text ) ) {
  245. $this->mKeywords = array_merge( $this->mKeywords, $text );
  246. } else {
  247. array_push( $this->mKeywords, $text );
  248. }
  249. }
  250. /**
  251. * Add a new \<link\> tag to the page header
  252. *
  253. * @param $linkarr Array: associative array of attributes.
  254. */
  255. function addLink( $linkarr ) {
  256. array_push( $this->mLinktags, $linkarr );
  257. }
  258. /**
  259. * Add a new \<link\> with "rel" attribute set to "meta"
  260. *
  261. * @param $linkarr Array: associative array mapping attribute names to their
  262. * values, both keys and values will be escaped, and the
  263. * "rel" attribute will be automatically added
  264. */
  265. function addMetadataLink( $linkarr ) {
  266. $linkarr['rel'] = $this->getMetadataAttribute();
  267. $this->addLink( $linkarr );
  268. }
  269. /**
  270. * Get the value of the "rel" attribute for metadata links
  271. *
  272. * @return String
  273. */
  274. public function getMetadataAttribute() {
  275. # note: buggy CC software only reads first "meta" link
  276. static $haveMeta = false;
  277. if ( $haveMeta ) {
  278. return 'alternate meta';
  279. } else {
  280. $haveMeta = true;
  281. return 'meta';
  282. }
  283. }
  284. /**
  285. * Add raw HTML to the list of scripts (including \<script\> tag, etc.)
  286. *
  287. * @param $script String: raw HTML
  288. */
  289. function addScript( $script ) {
  290. $this->mScripts .= $script . "\n";
  291. }
  292. /**
  293. * Register and add a stylesheet from an extension directory.
  294. *
  295. * @param $url String path to sheet. Provide either a full url (beginning
  296. * with 'http', etc) or a relative path from the document root
  297. * (beginning with '/'). Otherwise it behaves identically to
  298. * addStyle() and draws from the /skins folder.
  299. */
  300. public function addExtensionStyle( $url ) {
  301. array_push( $this->mExtStyles, $url );
  302. }
  303. /**
  304. * Get all styles added by extensions
  305. *
  306. * @return Array
  307. */
  308. function getExtStyle() {
  309. return $this->mExtStyles;
  310. }
  311. /**
  312. * Add a JavaScript file out of skins/common, or a given relative path.
  313. *
  314. * @param $file String: filename in skins/common or complete on-server path
  315. * (/foo/bar.js)
  316. * @param $version String: style version of the file. Defaults to $wgStyleVersion
  317. */
  318. public function addScriptFile( $file, $version = null ) {
  319. global $wgStylePath, $wgStyleVersion;
  320. // See if $file parameter is an absolute URL or begins with a slash
  321. if( substr( $file, 0, 1 ) == '/' || preg_match( '#^[a-z]*://#i', $file ) ) {
  322. $path = $file;
  323. } else {
  324. $path = "{$wgStylePath}/common/{$file}";
  325. }
  326. if ( is_null( $version ) )
  327. $version = $wgStyleVersion;
  328. $this->addScript( Html::linkedScript( wfAppendQuery( $path, $version ) ) );
  329. }
  330. /**
  331. * Add a self-contained script tag with the given contents
  332. *
  333. * @param $script String: JavaScript text, no <script> tags
  334. */
  335. public function addInlineScript( $script ) {
  336. $this->mScripts .= Html::inlineScript( "\n$script\n" ) . "\n";
  337. }
  338. /**
  339. * Get all registered JS and CSS tags for the header.
  340. *
  341. * @return String
  342. */
  343. function getScript() {
  344. return $this->mScripts . $this->getHeadItems();
  345. }
  346. /**
  347. * Filter an array of modules to remove insufficiently trustworthy members, and modules
  348. * which are no longer registered (eg a page is cached before an extension is disabled)
  349. * @param $modules Array
  350. * @param $position String if not null, only return modules with this position
  351. * @param $type string
  352. * @return Array
  353. */
  354. protected function filterModules( $modules, $position = null, $type = ResourceLoaderModule::TYPE_COMBINED ){
  355. $resourceLoader = $this->getResourceLoader();
  356. $filteredModules = array();
  357. foreach( $modules as $val ){
  358. $module = $resourceLoader->getModule( $val );
  359. if( $module instanceof ResourceLoaderModule
  360. && $module->getOrigin() <= $this->getAllowedModules( $type )
  361. && ( is_null( $position ) || $module->getPosition() == $position ) )
  362. {
  363. $filteredModules[] = $val;
  364. }
  365. }
  366. return $filteredModules;
  367. }
  368. /**
  369. * Get the list of modules to include on this page
  370. *
  371. * @param $filter Bool whether to filter out insufficiently trustworthy modules
  372. * @param $position String if not null, only return modules with this position
  373. * @param $param string
  374. * @return Array of module names
  375. */
  376. public function getModules( $filter = false, $position = null, $param = 'mModules' ) {
  377. $modules = array_values( array_unique( $this->$param ) );
  378. return $filter
  379. ? $this->filterModules( $modules, $position )
  380. : $modules;
  381. }
  382. /**
  383. * Add one or more modules recognized by the resource loader. Modules added
  384. * through this function will be loaded by the resource loader when the
  385. * page loads.
  386. *
  387. * @param $modules Mixed: module name (string) or array of module names
  388. */
  389. public function addModules( $modules ) {
  390. $this->mModules = array_merge( $this->mModules, (array)$modules );
  391. }
  392. /**
  393. * Get the list of module JS to include on this page
  394. *
  395. * @param $filter
  396. * @param $position
  397. *
  398. * @return array of module names
  399. */
  400. public function getModuleScripts( $filter = false, $position = null ) {
  401. return $this->getModules( $filter, $position, 'mModuleScripts' );
  402. }
  403. /**
  404. * Add only JS of one or more modules recognized by the resource loader. Module
  405. * scripts added through this function will be loaded by the resource loader when
  406. * the page loads.
  407. *
  408. * @param $modules Mixed: module name (string) or array of module names
  409. */
  410. public function addModuleScripts( $modules ) {
  411. $this->mModuleScripts = array_merge( $this->mModuleScripts, (array)$modules );
  412. }
  413. /**
  414. * Get the list of module CSS to include on this page
  415. *
  416. * @param $filter
  417. * @param $position
  418. *
  419. * @return Array of module names
  420. */
  421. public function getModuleStyles( $filter = false, $position = null ) {
  422. return $this->getModules( $filter, $position, 'mModuleStyles' );
  423. }
  424. /**
  425. * Add only CSS of one or more modules recognized by the resource loader. Module
  426. * styles added through this function will be loaded by the resource loader when
  427. * the page loads.
  428. *
  429. * @param $modules Mixed: module name (string) or array of module names
  430. */
  431. public function addModuleStyles( $modules ) {
  432. $this->mModuleStyles = array_merge( $this->mModuleStyles, (array)$modules );
  433. }
  434. /**
  435. * Get the list of module messages to include on this page
  436. *
  437. * @param $filter
  438. * @param $position
  439. *
  440. * @return Array of module names
  441. */
  442. public function getModuleMessages( $filter = false, $position = null ) {
  443. return $this->getModules( $filter, $position, 'mModuleMessages' );
  444. }
  445. /**
  446. * Add only messages of one or more modules recognized by the resource loader.
  447. * Module messages added through this function will be loaded by the resource
  448. * loader when the page loads.
  449. *
  450. * @param $modules Mixed: module name (string) or array of module names
  451. */
  452. public function addModuleMessages( $modules ) {
  453. $this->mModuleMessages = array_merge( $this->mModuleMessages, (array)$modules );
  454. }
  455. /**
  456. * Get an array of head items
  457. *
  458. * @return Array
  459. */
  460. function getHeadItemsArray() {
  461. return $this->mHeadItems;
  462. }
  463. /**
  464. * Get all header items in a string
  465. *
  466. * @return String
  467. */
  468. function getHeadItems() {
  469. $s = '';
  470. foreach ( $this->mHeadItems as $item ) {
  471. $s .= $item;
  472. }
  473. return $s;
  474. }
  475. /**
  476. * Add or replace an header item to the output
  477. *
  478. * @param $name String: item name
  479. * @param $value String: raw HTML
  480. */
  481. public function addHeadItem( $name, $value ) {
  482. $this->mHeadItems[$name] = $value;
  483. }
  484. /**
  485. * Check if the header item $name is already set
  486. *
  487. * @param $name String: item name
  488. * @return Boolean
  489. */
  490. public function hasHeadItem( $name ) {
  491. return isset( $this->mHeadItems[$name] );
  492. }
  493. /**
  494. * Set the value of the ETag HTTP header, only used if $wgUseETag is true
  495. *
  496. * @param $tag String: value of "ETag" header
  497. */
  498. function setETag( $tag ) {
  499. $this->mETag = $tag;
  500. }
  501. /**
  502. * Set whether the output should only contain the body of the article,
  503. * without any skin, sidebar, etc.
  504. * Used e.g. when calling with "action=render".
  505. *
  506. * @param $only Boolean: whether to output only the body of the article
  507. */
  508. public function setArticleBodyOnly( $only ) {
  509. $this->mArticleBodyOnly = $only;
  510. }
  511. /**
  512. * Return whether the output will contain only the body of the article
  513. *
  514. * @return Boolean
  515. */
  516. public function getArticleBodyOnly() {
  517. return $this->mArticleBodyOnly;
  518. }
  519. /**
  520. * checkLastModified tells the client to use the client-cached page if
  521. * possible. If sucessful, the OutputPage is disabled so that
  522. * any future call to OutputPage->output() have no effect.
  523. *
  524. * Side effect: sets mLastModified for Last-Modified header
  525. *
  526. * @param $timestamp string
  527. *
  528. * @return Boolean: true iff cache-ok headers was sent.
  529. */
  530. public function checkLastModified( $timestamp ) {
  531. global $wgCachePages, $wgCacheEpoch;
  532. if ( !$timestamp || $timestamp == '19700101000000' ) {
  533. wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" );
  534. return false;
  535. }
  536. if( !$wgCachePages ) {
  537. wfDebug( __METHOD__ . ": CACHE DISABLED\n", false );
  538. return false;
  539. }
  540. if( $this->getUser()->getOption( 'nocache' ) ) {
  541. wfDebug( __METHOD__ . ": USER DISABLED CACHE\n", false );
  542. return false;
  543. }
  544. $timestamp = wfTimestamp( TS_MW, $timestamp );
  545. $modifiedTimes = array(
  546. 'page' => $timestamp,
  547. 'user' => $this->getUser()->getTouched(),
  548. 'epoch' => $wgCacheEpoch
  549. );
  550. wfRunHooks( 'OutputPageCheckLastModified', array( &$modifiedTimes ) );
  551. $maxModified = max( $modifiedTimes );
  552. $this->mLastModified = wfTimestamp( TS_RFC2822, $maxModified );
  553. if( empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
  554. wfDebug( __METHOD__ . ": client did not send If-Modified-Since header\n", false );
  555. return false;
  556. }
  557. # Make debug info
  558. $info = '';
  559. foreach ( $modifiedTimes as $name => $value ) {
  560. if ( $info !== '' ) {
  561. $info .= ', ';
  562. }
  563. $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value );
  564. }
  565. # IE sends sizes after the date like this:
  566. # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
  567. # this breaks strtotime().
  568. $clientHeader = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] );
  569. wfSuppressWarnings(); // E_STRICT system time bitching
  570. $clientHeaderTime = strtotime( $clientHeader );
  571. wfRestoreWarnings();
  572. if ( !$clientHeaderTime ) {
  573. wfDebug( __METHOD__ . ": unable to parse the client's If-Modified-Since header: $clientHeader\n" );
  574. return false;
  575. }
  576. $clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime );
  577. wfDebug( __METHOD__ . ": client sent If-Modified-Since: " .
  578. wfTimestamp( TS_ISO_8601, $clientHeaderTime ) . "\n", false );
  579. wfDebug( __METHOD__ . ": effective Last-Modified: " .
  580. wfTimestamp( TS_ISO_8601, $maxModified ) . "\n", false );
  581. if( $clientHeaderTime < $maxModified ) {
  582. wfDebug( __METHOD__ . ": STALE, $info\n", false );
  583. return false;
  584. }
  585. # Not modified
  586. # Give a 304 response code and disable body output
  587. wfDebug( __METHOD__ . ": NOT MODIFIED, $info\n", false );
  588. ini_set( 'zlib.output_compression', 0 );
  589. $this->getRequest()->response()->header( "HTTP/1.1 304 Not Modified" );
  590. $this->sendCacheControl();
  591. $this->disable();
  592. // Don't output a compressed blob when using ob_gzhandler;
  593. // it's technically against HTTP spec and seems to confuse
  594. // Firefox when the response gets split over two packets.
  595. wfClearOutputBuffers();
  596. return true;
  597. }
  598. /**
  599. * Override the last modified timestamp
  600. *
  601. * @param $timestamp String: new timestamp, in a format readable by
  602. * wfTimestamp()
  603. */
  604. public function setLastModified( $timestamp ) {
  605. $this->mLastModified = wfTimestamp( TS_RFC2822, $timestamp );
  606. }
  607. /**
  608. * Set the robot policy for the page: <http://www.robotstxt.org/meta.html>
  609. *
  610. * @param $policy String: the literal string to output as the contents of
  611. * the meta tag. Will be parsed according to the spec and output in
  612. * standardized form.
  613. * @return null
  614. */
  615. public function setRobotPolicy( $policy ) {
  616. $policy = Article::formatRobotPolicy( $policy );
  617. if( isset( $policy['index'] ) ) {
  618. $this->setIndexPolicy( $policy['index'] );
  619. }
  620. if( isset( $policy['follow'] ) ) {
  621. $this->setFollowPolicy( $policy['follow'] );
  622. }
  623. }
  624. /**
  625. * Set the index policy for the page, but leave the follow policy un-
  626. * touched.
  627. *
  628. * @param $policy string Either 'index' or 'noindex'.
  629. * @return null
  630. */
  631. public function setIndexPolicy( $policy ) {
  632. $policy = trim( $policy );
  633. if( in_array( $policy, array( 'index', 'noindex' ) ) ) {
  634. $this->mIndexPolicy = $policy;
  635. }
  636. }
  637. /**
  638. * Set the follow policy for the page, but leave the index policy un-
  639. * touched.
  640. *
  641. * @param $policy String: either 'follow' or 'nofollow'.
  642. * @return null
  643. */
  644. public function setFollowPolicy( $policy ) {
  645. $policy = trim( $policy );
  646. if( in_array( $policy, array( 'follow', 'nofollow' ) ) ) {
  647. $this->mFollowPolicy = $policy;
  648. }
  649. }
  650. /**
  651. * Set the new value of the "action text", this will be added to the
  652. * "HTML title", separated from it with " - ".
  653. *
  654. * @param $text String: new value of the "action text"
  655. */
  656. public function setPageTitleActionText( $text ) {
  657. $this->mPageTitleActionText = $text;
  658. }
  659. /**
  660. * Get the value of the "action text"
  661. *
  662. * @return String
  663. */
  664. public function getPageTitleActionText() {
  665. if ( isset( $this->mPageTitleActionText ) ) {
  666. return $this->mPageTitleActionText;
  667. }
  668. }
  669. /**
  670. * "HTML title" means the contents of <title>.
  671. * It is stored as plain, unescaped text and will be run through htmlspecialchars in the skin file.
  672. *
  673. * @param $name string
  674. */
  675. public function setHTMLTitle( $name ) {
  676. if ( $name instanceof Message ) {
  677. $this->mHTMLtitle = $name->setContext( $this->getContext() )->text();
  678. } else {
  679. $this->mHTMLtitle = $name;
  680. }
  681. }
  682. /**
  683. * Return the "HTML title", i.e. the content of the <title> tag.
  684. *
  685. * @return String
  686. */
  687. public function getHTMLTitle() {
  688. return $this->mHTMLtitle;
  689. }
  690. /**
  691. * Set $mRedirectedFrom, the Title of the page which redirected us to the current page.
  692. *
  693. * param @t Title
  694. */
  695. public function setRedirectedFrom( $t ) {
  696. $this->mRedirectedFrom = $t;
  697. }
  698. /**
  699. * "Page title" means the contents of \<h1\>. It is stored as a valid HTML fragment.
  700. * This function allows good tags like \<sup\> in the \<h1\> tag, but not bad tags like \<script\>.
  701. * This function automatically sets \<title\> to the same content as \<h1\> but with all tags removed.
  702. * Bad tags that were escaped in \<h1\> will still be escaped in \<title\>, and good tags like \<i\> will be dropped entirely.
  703. *
  704. * @param $name string|Message
  705. */
  706. public function setPageTitle( $name ) {
  707. if ( $name instanceof Message ) {
  708. $name = $name->setContext( $this->getContext() )->text();
  709. }
  710. # change "<script>foo&bar</script>" to "&lt;script&gt;foo&amp;bar&lt;/script&gt;"
  711. # but leave "<i>foobar</i>" alone
  712. $nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) );
  713. $this->mPagetitle = $nameWithTags;
  714. # change "<i>foo&amp;bar</i>" to "foo&bar"
  715. $this->setHTMLTitle( $this->msg( 'pagetitle' )->rawParams( Sanitizer::stripAllTags( $nameWithTags ) ) );
  716. }
  717. /**
  718. * Return the "page title", i.e. the content of the \<h1\> tag.
  719. *
  720. * @return String
  721. */
  722. public function getPageTitle() {
  723. return $this->mPagetitle;
  724. }
  725. /**
  726. * Set the Title object to use
  727. *
  728. * @param $t Title object
  729. */
  730. public function setTitle( Title $t ) {
  731. $this->getContext()->setTitle( $t );
  732. }
  733. /**
  734. * Replace the subtile with $str
  735. *
  736. * @param $str String|Message: new value of the subtitle
  737. */
  738. public function setSubtitle( $str ) {
  739. $this->clearSubtitle();
  740. $this->addSubtitle( $str );
  741. }
  742. /**
  743. * Add $str to the subtitle
  744. *
  745. * @deprecated in 1.19; use addSubtitle() instead
  746. * @param $str String|Message to add to the subtitle
  747. */
  748. public function appendSubtitle( $str ) {
  749. $this->addSubtitle( $str );
  750. }
  751. /**
  752. * Add $str to the subtitle
  753. *
  754. * @param $str String|Message to add to the subtitle
  755. */
  756. public function addSubtitle( $str ) {
  757. if ( $str instanceof Message ) {
  758. $this->mSubtitle[] = $str->setContext( $this->getContext() )->parse();
  759. } else {
  760. $this->mSubtitle[] = $str;
  761. }
  762. }
  763. /**
  764. * Add a subtitle containing a backlink to a page
  765. *
  766. * @param $title Title to link to
  767. */
  768. public function addBacklinkSubtitle( Title $title ) {
  769. $query = array();
  770. if ( $title->isRedirect() ) {
  771. $query['redirect'] = 'no';
  772. }
  773. $this->addSubtitle( $this->msg( 'backlinksubtitle' )->rawParams( Linker::link( $title, null, array(), $query ) ) );
  774. }
  775. /**
  776. * Clear the subtitles
  777. */
  778. public function clearSubtitle() {
  779. $this->mSubtitle = array();
  780. }
  781. /**
  782. * Get the subtitle
  783. *
  784. * @return String
  785. */
  786. public function getSubtitle() {
  787. return implode( "<br />\n\t\t\t\t", $this->mSubtitle );
  788. }
  789. /**
  790. * Set the page as printable, i.e. it'll be displayed with with all
  791. * print styles included
  792. */
  793. public function setPrintable() {
  794. $this->mPrintable = true;
  795. }
  796. /**
  797. * Return whether the page is "printable"
  798. *
  799. * @return Boolean
  800. */
  801. public function isPrintable() {
  802. return $this->mPrintable;
  803. }
  804. /**
  805. * Disable output completely, i.e. calling output() will have no effect
  806. */
  807. public function disable() {
  808. $this->mDoNothing = true;
  809. }
  810. /**
  811. * Return whether the output will be completely disabled
  812. *
  813. * @return Boolean
  814. */
  815. public function isDisabled() {
  816. return $this->mDoNothing;
  817. }
  818. /**
  819. * Show an "add new section" link?
  820. *
  821. * @return Boolean
  822. */
  823. public function showNewSectionLink() {
  824. return $this->mNewSectionLink;
  825. }
  826. /**
  827. * Forcibly hide the new section link?
  828. *
  829. * @return Boolean
  830. */
  831. public function forceHideNewSectionLink() {
  832. return $this->mHideNewSectionLink;
  833. }
  834. /**
  835. * Add or remove feed links in the page header
  836. * This is mainly kept for backward compatibility, see OutputPage::addFeedLink()
  837. * for the new version
  838. * @see addFeedLink()
  839. *
  840. * @param $show Boolean: true: add default feeds, false: remove all feeds
  841. */
  842. public function setSyndicated( $show = true ) {
  843. if ( $show ) {
  844. $this->setFeedAppendQuery( false );
  845. } else {
  846. $this->mFeedLinks = array();
  847. }
  848. }
  849. /**
  850. * Add default feeds to the page header
  851. * This is mainly kept for backward compatibility, see OutputPage::addFeedLink()
  852. * for the new version
  853. * @see addFeedLink()
  854. *
  855. * @param $val String: query to append to feed links or false to output
  856. * default links
  857. */
  858. public function setFeedAppendQuery( $val ) {
  859. global $wgAdvertisedFeedTypes;
  860. $this->mFeedLinks = array();
  861. foreach ( $wgAdvertisedFeedTypes as $type ) {
  862. $query = "feed=$type";
  863. if ( is_string( $val ) ) {
  864. $query .= '&' . $val;
  865. }
  866. $this->mFeedLinks[$type] = $this->getTitle()->getLocalURL( $query );
  867. }
  868. }
  869. /**
  870. * Add a feed link to the page header
  871. *
  872. * @param $format String: feed type, should be a key of $wgFeedClasses
  873. * @param $href String: URL
  874. */
  875. public function addFeedLink( $format, $href ) {
  876. global $wgAdvertisedFeedTypes;
  877. if ( in_array( $format, $wgAdvertisedFeedTypes ) ) {
  878. $this->mFeedLinks[$format] = $href;
  879. }
  880. }
  881. /**
  882. * Should we output feed links for this page?
  883. * @return Boolean
  884. */
  885. public function isSyndicated() {
  886. return count( $this->mFeedLinks ) > 0;
  887. }
  888. /**
  889. * Return URLs for each supported syndication format for this page.
  890. * @return array associating format keys with URLs
  891. */
  892. public function getSyndicationLinks() {
  893. return $this->mFeedLinks;
  894. }
  895. /**
  896. * Will currently always return null
  897. *
  898. * @return null
  899. */
  900. public function getFeedAppendQuery() {
  901. return $this->mFeedLinksAppendQuery;
  902. }
  903. /**
  904. * Set whether the displayed content is related to the source of the
  905. * corresponding article on the wiki
  906. * Setting true will cause the change "article related" toggle to true
  907. *
  908. * @param $v Boolean
  909. */
  910. public function setArticleFlag( $v ) {
  911. $this->mIsarticle = $v;
  912. if ( $v ) {
  913. $this->mIsArticleRelated = $v;
  914. }
  915. }
  916. /**
  917. * Return whether the content displayed page is related to the source of
  918. * the corresponding article on the wiki
  919. *
  920. * @return Boolean
  921. */
  922. public function isArticle() {
  923. return $this->mIsarticle;
  924. }
  925. /**
  926. * Set whether this page is related an article on the wiki
  927. * Setting false will cause the change of "article flag" toggle to false
  928. *
  929. * @param $v Boolean
  930. */
  931. public function setArticleRelated( $v ) {
  932. $this->mIsArticleRelated = $v;
  933. if ( !$v ) {
  934. $this->mIsarticle = false;
  935. }
  936. }
  937. /**
  938. * Return whether this page is related an article on the wiki
  939. *
  940. * @return Boolean
  941. */
  942. public function isArticleRelated() {
  943. return $this->mIsArticleRelated;
  944. }
  945. /**
  946. * Add new language links
  947. *
  948. * @param $newLinkArray Associative array mapping language code to the page
  949. * name
  950. */
  951. public function addLanguageLinks( $newLinkArray ) {
  952. $this->mLanguageLinks += $newLinkArray;
  953. }
  954. /**
  955. * Reset the language links and add new language links
  956. *
  957. * @param $newLinkArray Associative array mapping language code to the page
  958. * name
  959. */
  960. public function setLanguageLinks( $newLinkArray ) {
  961. $this->mLanguageLinks = $newLinkArray;
  962. }
  963. /**
  964. * Get the list of language links
  965. *
  966. * @return Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page')
  967. */
  968. public function getLanguageLinks() {
  969. return $this->mLanguageLinks;
  970. }
  971. /**
  972. * Add an array of categories, with names in the keys
  973. *
  974. * @param $categories Array mapping category name => sort key
  975. */
  976. public function addCategoryLinks( $categories ) {
  977. global $wgContLang;
  978. if ( !is_array( $categories ) || count( $categories ) == 0 ) {
  979. return;
  980. }
  981. # Add the links to a LinkBatch
  982. $arr = array( NS_CATEGORY => $categories );
  983. $lb = new LinkBatch;
  984. $lb->setArray( $arr );
  985. # Fetch existence plus the hiddencat property
  986. $dbr = wfGetDB( DB_SLAVE );
  987. $res = $dbr->select( array( 'page', 'page_props' ),
  988. array( 'page_id', 'page_namespace', 'page_title', 'page_len', 'page_is_redirect', 'page_latest', 'pp_value' ),
  989. $lb->constructSet( 'page', $dbr ),
  990. __METHOD__,
  991. array(),
  992. array( 'page_props' => array( 'LEFT JOIN', array( 'pp_propname' => 'hiddencat', 'pp_page = page_id' ) ) )
  993. );
  994. # Add the results to the link cache
  995. $lb->addResultToCache( LinkCache::singleton(), $res );
  996. # Set all the values to 'normal'. This can be done with array_fill_keys in PHP 5.2.0+
  997. $categories = array_combine(
  998. array_keys( $categories ),
  999. array_fill( 0, count( $categories ), 'normal' )
  1000. );
  1001. # Mark hidden categories
  1002. foreach ( $res as $row ) {
  1003. if ( isset( $row->pp_value ) ) {
  1004. $categories[$row->page_title] = 'hidden';
  1005. }
  1006. }
  1007. # Add the remaining categories to the skin
  1008. if ( wfRunHooks( 'OutputPageMakeCategoryLinks', array( &$this, $categories, &$this->mCategoryLinks ) ) ) {
  1009. foreach ( $categories as $category => $type ) {
  1010. $origcategory = $category;
  1011. $title = Title::makeTitleSafe( NS_CATEGORY, $category );
  1012. $wgContLang->findVariantLink( $category, $title, true );
  1013. if ( $category != $origcategory ) {
  1014. if ( array_key_exists( $category, $categories ) ) {
  1015. continue;
  1016. }
  1017. }
  1018. $text = $wgContLang->convertHtml( $title->getText() );
  1019. $this->mCategories[] = $title->getText();
  1020. $this->mCategoryLinks[$type][] = Linker::link( $title, $text );
  1021. }
  1022. }
  1023. }
  1024. /**
  1025. * Reset the category links (but not the category list) and add $categories
  1026. *
  1027. * @param $categories Array mapping category name => sort key
  1028. */
  1029. public function setCategoryLinks( $categories ) {
  1030. $this->mCategoryLinks = array();
  1031. $this->addCategoryLinks( $categories );
  1032. }
  1033. /**
  1034. * Get the list of category links, in a 2-D array with the following format:
  1035. * $arr[$type][] = $link, where $type is either "normal" or "hidden" (for
  1036. * hidden categories) and $link a HTML fragment with a link to the category
  1037. * page
  1038. *
  1039. * @return Array
  1040. */
  1041. public function getCategoryLinks() {
  1042. return $this->mCategoryLinks;
  1043. }
  1044. /**
  1045. * Get the list of category names this page belongs to
  1046. *
  1047. * @return Array of strings
  1048. */
  1049. public function getCategories() {
  1050. return $this->mCategories;
  1051. }
  1052. /**
  1053. * Do not allow scripts which can be modified by wiki users to load on this page;
  1054. * only allow scripts bundled with, or generated by, the software.
  1055. */
  1056. public function disallowUserJs() {
  1057. $this->reduceAllowedModules(
  1058. ResourceLoaderModule::TYPE_SCRIPTS,
  1059. ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
  1060. );
  1061. }
  1062. /**
  1063. * Return whether user JavaScript is allowed for this page
  1064. * @deprecated since 1.18 Load modules with ResourceLoader, and origin and
  1065. * trustworthiness is identified and enforced automagically.
  1066. * Will be removed in 1.20.
  1067. * @return Boolean
  1068. */
  1069. public function isUserJsAllowed() {
  1070. wfDeprecated( __METHOD__, '1.18' );
  1071. return $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS ) >= ResourceLoaderModule::ORIGIN_USER_INDIVIDUAL;
  1072. }
  1073. /**
  1074. * Show what level of JavaScript / CSS untrustworthiness is allowed on this page
  1075. * @see ResourceLoaderModule::$origin
  1076. * @param $type String ResourceLoaderModule TYPE_ constant
  1077. * @return Int ResourceLoaderModule ORIGIN_ class constant
  1078. */
  1079. public function getAllowedModules( $type ){
  1080. if( $type == ResourceLoaderModule::TYPE_COMBINED ){
  1081. return min( array_values( $this->mAllowedModules ) );
  1082. } else {
  1083. return isset( $this->mAllowedModules[$type] )
  1084. ? $this->mAllowedModules[$type]
  1085. : ResourceLoaderModule::ORIGIN_ALL;
  1086. }
  1087. }
  1088. /**
  1089. * Set the highest level of CSS/JS untrustworthiness allowed
  1090. * @param $type String ResourceLoaderModule TYPE_ constant
  1091. * @param $level Int ResourceLoaderModule class constant
  1092. */
  1093. public function setAllowedModules( $type, $level ){
  1094. $this->mAllowedModules[$type] = $level;
  1095. }
  1096. /**
  1097. * As for setAllowedModules(), but don't inadvertantly make the page more accessible
  1098. * @param $type String
  1099. * @param $level Int ResourceLoaderModule class constant
  1100. */
  1101. public function reduceAllowedModules( $type, $level ){
  1102. $this->mAllowedModules[$type] = min( $this->getAllowedModules($type), $level );
  1103. }
  1104. /**
  1105. * Prepend $text to the body HTML
  1106. *
  1107. * @param $text String: HTML
  1108. */
  1109. public function prependHTML( $text ) {
  1110. $this->mBodytext = $text . $this->mBodytext;
  1111. }
  1112. /**
  1113. * Append $text to the body HTML
  1114. *
  1115. * @param $text String: HTML
  1116. */
  1117. public function addHTML( $text ) {
  1118. $this->mBodytext .= $text;
  1119. }
  1120. /**
  1121. * Shortcut for adding an Html::element via addHTML.
  1122. *
  1123. * @since 1.19
  1124. *
  1125. * @param $element string
  1126. * @param $attribs array
  1127. * @param $contents string
  1128. */
  1129. public function addElement( $element, $attribs = array(), $contents = '' ) {
  1130. $this->addHTML( Html::element( $element, $attribs, $contents ) );
  1131. }
  1132. /**
  1133. * Clear the body HTML
  1134. */
  1135. public function clearHTML() {
  1136. $this->mBodytext = '';
  1137. }
  1138. /**
  1139. * Get the body HTML
  1140. *
  1141. * @return String: HTML
  1142. */
  1143. public function getHTML() {
  1144. return $this->mBodytext;
  1145. }
  1146. /**
  1147. * Add $text to the debug output
  1148. *
  1149. * @param $text String: debug text
  1150. */
  1151. public function debug( $text ) {
  1152. $this->mDebugtext .= $text;
  1153. }
  1154. /**
  1155. * Get/set the ParserOptions object to use for wikitext parsing
  1156. *
  1157. * @param $options either the ParserOption to use or null to only get the
  1158. * current ParserOption object
  1159. * @return ParserOptions object
  1160. */
  1161. public function parserOptions( $options = null ) {
  1162. if ( !$this->mParserOptions ) {
  1163. $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() );
  1164. $this->mParserOptions->setEditSection( false );
  1165. }
  1166. return wfSetVar( $this->mParserOptions, $options );
  1167. }
  1168. /**
  1169. * Set the revision ID which will be seen by the wiki text parser
  1170. * for things such as embedded {{REVISIONID}} variable use.
  1171. *
  1172. * @param $revid Mixed: an positive integer, or null
  1173. * @return Mixed: previous value
  1174. */
  1175. public function setRevisionId( $revid ) {
  1176. $val = is_null( $revid ) ? null : intval( $revid );
  1177. return wfSetVar( $this->mRevisionId, $val );
  1178. }
  1179. /**
  1180. * Get the displayed revision ID
  1181. *
  1182. * @return Integer
  1183. */
  1184. public function getRevisionId() {
  1185. return $this->mRevisionId;
  1186. }
  1187. /**
  1188. * Set the timestamp of the revision which will be displayed. This is used
  1189. * to avoid a extra DB call in Skin::lastModified().
  1190. *
  1191. * @param $revid Mixed: string, or null
  1192. * @return Mixed: previous value
  1193. */
  1194. public function setRevisionTimestamp( $timestmap ) {
  1195. return wfSetVar( $this->mRevisionTimestamp, $timestmap );
  1196. }
  1197. /**
  1198. * Get the timestamp of displayed revision.
  1199. * This will be null if not filled by setRevisionTimestamp().
  1200. *
  1201. * @return String or null
  1202. */
  1203. public function getRevisionTimestamp() {
  1204. return $this->mRevisionTimestamp;
  1205. }
  1206. /**
  1207. * Set the displayed file version
  1208. *
  1209. * @param $file File|false
  1210. * @return Mixed: previous value
  1211. */
  1212. public function setFileVersion( $file ) {
  1213. $val = null;
  1214. if ( $file instanceof File && $file->exists() ) {
  1215. $val = array( 'time' => $file->getTimestamp(), 'sha1' => $file->getSha1() );
  1216. }
  1217. return wfSetVar( $this->mFileVersion, $val, true );
  1218. }
  1219. /**
  1220. * Get the displayed file version
  1221. *
  1222. * @return Array|null ('time' => MW timestamp, 'sha1' => sha1)
  1223. */
  1224. public function getFileVersion() {
  1225. return $this->mFileVersion;
  1226. }
  1227. /**
  1228. * Get the templates used on this page
  1229. *
  1230. * @return Array (namespace => dbKey => revId)
  1231. * @since 1.18
  1232. */
  1233. public function getTemplateIds() {
  1234. return $this->mTemplateIds;
  1235. }
  1236. /**
  1237. * Get the files used on this page
  1238. *
  1239. * @return Array (dbKey => array('time' => MW timestamp or null, 'sha1' => sha1 or ''))
  1240. * @since 1.18
  1241. */
  1242. public function getFileSearchOptions() {
  1243. return $this->mImageTimeKeys;
  1244. }
  1245. /**
  1246. * Convert wikitext to HTML and add it to the buffer
  1247. * Default assumes that the current page title will be used.
  1248. *
  1249. * @param $text String
  1250. * @param $linestart Boolean: is this the start of a line?
  1251. * @param $interface Boolean: is this text in the user interface language?
  1252. */
  1253. public function addWikiText( $text, $linestart = true, $interface = true ) {
  1254. $title = $this->getTitle(); // Work arround E_STRICT
  1255. $this->addWikiTextTitle( $text, $title, $linestart, /*tidy*/false, $interface );
  1256. }
  1257. /**
  1258. * Add wikitext with a custom Title object
  1259. *
  1260. * @param $text String: wikitext
  1261. * @param $title Title object
  1262. * @param $linestart Boolean: is this the start of a line?
  1263. */
  1264. public function addWikiTextWithTitle( $text, &$title, $linestart = true ) {
  1265. $this->addWikiTextTitle( $text, $title, $linestart );
  1266. }
  1267. /**
  1268. * Add wikitext with a custom Title object and tidy enabled.
  1269. *
  1270. * @param $text String: wikitext
  1271. * @param $title Title object
  1272. * @param $linestart Boolean: is this the start of a line?
  1273. */
  1274. function addWikiTextTitleTidy( $text, &$title, $linestart = true ) {
  1275. $this->addWikiTextTitle( $text, $title, $linestart, true );
  1276. }
  1277. /**
  1278. * Add wikitext with tidy enabled
  1279. *
  1280. * @param $text String: wikitext
  1281. * @param $linestart Boolean: is this the start of a line?
  1282. */
  1283. public function addWikiTextTidy( $text, $linestart = true ) {
  1284. $title = $this->getTitle();
  1285. $this->addWikiTextTitleTidy( $text, $title, $linestart );
  1286. }
  1287. /**
  1288. * Add wikitext with a custom Title object
  1289. *
  1290. * @param $text String: wikitext
  1291. * @param $title Title object
  1292. * @param $linestart Boolean: is this the start of a line?
  1293. * @param $tidy Boolean: whether to use tidy
  1294. * @param $interface Boolean: whether it is an interface message
  1295. * (for example disables conversion)
  1296. */
  1297. public function addWikiTextTitle( $text, &$title, $linestart, $tidy = false, $interface = false ) {
  1298. global $wgParser;
  1299. wfProfileIn( __METHOD__ );
  1300. $popts = $this->parserOptions();
  1301. $oldTidy = $popts->setTidy( $tidy );
  1302. $popts->setInterfaceMessage( (bool) $interface );
  1303. $parserOutput = $wgParser->parse(
  1304. $text, $title, $popts,
  1305. $linestart, true, $this->mRevisionId
  1306. );
  1307. $popts->setTidy( $oldTidy );
  1308. $this->addParserOutput( $parserOutput );
  1309. wfProfileOut( __METHOD__ );
  1310. }
  1311. /**
  1312. * Add a ParserOutput object, but without Html
  1313. *
  1314. * @param $parserOutput ParserOutput object
  1315. */
  1316. public function addParserOutputNoText( &$parserOutput ) {
  1317. $this->mLanguageLinks += $parserOutput->getLanguageLinks();
  1318. $this->addCategoryLinks( $parserOutput->getCategories() );
  1319. $this->mNewSectionLink = $parserOutput->getNewSection();
  1320. $this->mHideNewSectionLink = $parserOutput->getHideNewSection();
  1321. $this->mParseWarnings = $parserOutput->getWarnings();
  1322. if ( !$parserOutput->isCacheable() ) {
  1323. $this->enableClientCache( false );
  1324. }
  1325. $this->mNoGallery = $parserOutput->getNoGallery();
  1326. $this->mHeadItems = array_merge( $this->mHeadItems, $parserOutput->getHeadItems() );
  1327. $this->addModules( $parserOutput->getModules() );
  1328. $this->addModuleScripts( $parserOutput->getModuleScripts() );
  1329. $this->addModuleStyles( $parserOutput->getModuleStyles() );
  1330. $this->addModuleMessages( $parserOutput->getModuleMessages() );
  1331. // Template versioning...
  1332. foreach ( (array)$parserOutput->getTemplateIds() as $ns => $dbks ) {
  1333. if ( isset( $this->mTemplateIds[$ns] ) ) {
  1334. $this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns];
  1335. } else {
  1336. $this->mTemplateIds[$ns] = $dbks;
  1337. }
  1338. }
  1339. // File versioning...
  1340. foreach ( (array)$parserOutput->getFileSearchOptions() as $dbk => $data ) {
  1341. $this->mImageTimeKeys[$dbk] = $data;
  1342. }
  1343. // Hooks registered in the object
  1344. global $wgParserOutputHooks;
  1345. foreach ( $parserOutput->getOutputHooks() as $hookInfo ) {
  1346. list( $hookName, $data ) = $hookInfo;
  1347. if ( isset( $wgParserOutputHooks[$hookName] ) ) {
  1348. call_user_func( $wgParserOutputHooks[$hookName], $this, $parserOutput, $data );
  1349. }
  1350. }
  1351. wfRunHooks( 'OutputPageParserOutput', array( &$this, $parserOutput ) );
  1352. }
  1353. /**
  1354. * Add a ParserOutput object
  1355. *
  1356. * @param $parserOutput ParserOutput
  1357. */
  1358. function addParserOutput( &$parserOutput ) {
  1359. $this->addParserOutputNoText( $parserOutput );
  1360. $text = $parserOutput->getText();
  1361. wfRunHooks( 'OutputPageBeforeHTML', array( &$this, &$text ) );
  1362. $this->addHTML( $text );
  1363. }
  1364. /**
  1365. * Add the output of a QuickTemplate to the output buffer
  1366. *
  1367. * @param $template QuickTemplate
  1368. */
  1369. public function addTemplate( &$template ) {
  1370. ob_start();
  1371. $template->execute();
  1372. $this->addHTML( ob_get_contents() );
  1373. ob_end_clean();
  1374. }
  1375. /**
  1376. * Parse wikitext and return the HTML.
  1377. *
  1378. * @param $text String
  1379. * @param $linestart Boolean: is this the start of a line?
  1380. * @param $interface Boolean: use interface language ($wgLang instead of
  1381. * $wgContLang) while parsing language sensitive magic
  1382. * words like GRAMMAR and PLURAL. This also disables
  1383. * LanguageConverter.
  1384. * @param $language Language object: target language object, will override
  1385. * $interface
  1386. * @return String: HTML
  1387. */
  1388. public function parse( $text, $linestart = true, $interface = false, $language = null ) {
  1389. global $wgParser;
  1390. if( is_null( $this->getTitle() ) ) {
  1391. throw new MWException( 'Empty $mTitle in ' . __METHOD__ );
  1392. }
  1393. $popts = $this->parserOptions();
  1394. if ( $interface ) {
  1395. $popts->setInterfaceMessage( true );
  1396. }
  1397. if ( $language !== null ) {
  1398. $oldLang = $popts->setTargetLanguage( $language );
  1399. }
  1400. $parserOutput = $wgParser->parse(
  1401. $text, $this->getTitle(), $popts,
  1402. $linestart, true, $this->mRevisionId
  1403. );
  1404. if ( $interface ) {
  1405. $popts->setInterfaceMessage( false );
  1406. }
  1407. if ( $language !== null ) {
  1408. $popts->setTargetLanguage( $oldLang );
  1409. }
  1410. return $parserOutput->getText();
  1411. }
  1412. /**
  1413. * Parse wikitext, strip paragraphs, and return the HTML.
  1414. *
  1415. * @param $text String
  1416. * @param $linestart Boolean: is this the start of a line?
  1417. * @param $interface Boolean: use interface language ($wgLang instead of
  1418. * $wgContLang) while parsing language sensitive magic
  1419. * words like GRAMMAR and PLURAL
  1420. * @return String: HTML
  1421. */
  1422. public function parseInline( $text, $linestart = true, $interface = false ) {
  1423. $parsed = $this->parse( $text, $linestart, $interface );
  1424. $m = array();
  1425. if ( preg_match( '/^<p>(.*)\n?<\/p>\n?/sU', $parsed, $m ) ) {
  1426. $parsed = $m[1];
  1427. }
  1428. return $parsed;
  1429. }
  1430. /**
  1431. * Set the value of the "s-maxage" part of the "Cache-control" HTTP header
  1432. *
  1433. * @param $maxage Integer: maximum cache time on the Squid, in seconds.
  1434. */
  1435. public function setSquidMaxage( $maxage ) {
  1436. $this->mSquidMaxage = $maxage;
  1437. }
  1438. /**
  1439. * Use enableClientCache(false) to force it to send nocache headers
  1440. *
  1441. * @param $state bool
  1442. *
  1443. * @return bool
  1444. */
  1445. public function enableClientCache( $state ) {
  1446. return wfSetVar( $this->mEnableClientCache, $state );
  1447. }
  1448. /**
  1449. * Get the list of cookies that will influence on the cache
  1450. *
  1451. * @return Array
  1452. */
  1453. function getCacheVaryCookies() {
  1454. global $wgCookiePrefix, $wgCacheVaryCookies;
  1455. static $cookies;
  1456. if ( $cookies === null ) {
  1457. $cookies = array_merge(
  1458. array(
  1459. "{$wgCookiePrefix}Token",
  1460. "{$wgCookiePrefix}LoggedOut",
  1461. session_name()
  1462. ),
  1463. $wgCacheVaryCookies
  1464. );
  1465. wfRunHooks( 'GetCacheVaryCookies', array( $this, &$cookies ) );
  1466. }
  1467. return $cookies;
  1468. }
  1469. /**
  1470. * Return whether this page is not cacheable because "useskin" or "uselang"
  1471. * URL parameters were passed.
  1472. *
  1473. * @return Boolean
  1474. */
  1475. function uncacheableBecauseRequestVars() {
  1476. $request = $this->getRequest();
  1477. return $request->getText( 'useskin', false ) === false
  1478. && $request->getText( 'uselang', false ) === false;
  1479. }
  1480. /**
  1481. * Check if the request has a cache-varying cookie header
  1482. * If it does, it's very important that we don't allow public caching
  1483. *
  1484. * @return Boolean
  1485. */
  1486. function haveCacheVaryCookies() {
  1487. $cookieHeader = $this->getRequest()->getHeader( 'cookie' );
  1488. if ( $cookieHeader === false ) {
  1489. return false;
  1490. }
  1491. $cvCookies = $this->getCacheVaryCookies();
  1492. foreach ( $cvCookies as $cookieName ) {
  1493. # Check for a simple string match, like the way squid does it
  1494. if ( strpos( $cookieHeader, $cookieName ) !== false ) {
  1495. wfDebug( __METHOD__ . ": found $cookieName\n" );
  1496. return true;
  1497. }
  1498. }
  1499. wfDebug( __METHOD__ . ": no cache-varying cookies found\n" );
  1500. return false;
  1501. }
  1502. /**
  1503. * Add an HTTP header that will influence on the cache
  1504. *
  1505. * @param $header String: header name
  1506. * @param $option Array|null
  1507. * @todo FIXME: Document the $option parameter; it appears to be for
  1508. * X-Vary-Options but what format is acceptable?
  1509. */
  1510. public function addVaryHeader( $header, $option = null ) {
  1511. if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
  1512. $this->mVaryHeader[$header] = (array)$option;
  1513. } elseif( is_array( $option ) ) {
  1514. if( is_array( $this->mVaryHeader[$header] ) ) {
  1515. $this->mVaryHeader[$header] = array_merge( $this->mVaryHeader[$header], $option );
  1516. } else {
  1517. $this->mVaryHeader[$header] = $option;
  1518. }
  1519. }
  1520. $this->mVaryHeader[$header] = array_unique( (array)$this->mVaryHeader[$header] );
  1521. }
  1522. /**
  1523. * Get a complete X-Vary-Options header
  1524. *
  1525. * @return String
  1526. */
  1527. public function getXVO() {
  1528. $cvCookies = $this->getCacheVaryCookies();
  1529. $cookiesOption = array();
  1530. foreach ( $cvCookies as $cookieName ) {
  1531. $cookiesOption[] = 'string-contains=' . $cookieName;
  1532. }
  1533. $this->addVaryHeader( 'Cookie', $cookiesOption );
  1534. $headers = array();
  1535. foreach( $this->mVaryHeader as $header => $option ) {
  1536. $newheader = $header;
  1537. if( is_array( $option ) ) {
  1538. $newheader .= ';' . implode( ';', $option );
  1539. }
  1540. $headers[] = $newheader;
  1541. }
  1542. $xvo = 'X-Vary-Options: ' . implode( ',', $headers );
  1543. return $xvo;
  1544. }
  1545. /**
  1546. * bug 21672: Add Accept-Language to Vary and XVO headers
  1547. * if there's no 'variant' parameter existed in GET.
  1548. *
  1549. * For example:
  1550. * /w/index.php?title=Main_page should always be served; but
  1551. * /w/index.php?title=Main_page&variant=zh-cn should never be served.
  1552. */
  1553. function addAcceptLanguage() {
  1554. $lang = $this->getTitle()->getPageLanguage();
  1555. if( !$this->getRequest()->getCheck( 'variant' ) && $lang->hasVariants() ) {
  1556. $variants = $lang->getVariants();
  1557. $aloption = array();
  1558. foreach ( $variants as $variant ) {
  1559. if( $variant === $lang->getCode() ) {
  1560. continue;
  1561. } else {
  1562. $aloption[] = 'string-contains=' . $variant;
  1563. // IE and some other browsers use another form of language code
  1564. // in their Accept-Language header, like "zh-CN" or "zh-TW".
  1565. // We should handle these too.
  1566. $ievariant = explode( '-', $variant );
  1567. if ( count( $ievariant ) == 2 ) {
  1568. $ievariant[1] = strtoupper( $ievariant[1] );
  1569. $ievariant = implode( '-', $ievariant );
  1570. $aloption[] = 'string-contains=' . $ievariant;
  1571. }
  1572. }
  1573. }
  1574. $this->addVaryHeader( 'Accept-Language', $aloption );
  1575. }
  1576. }
  1577. /**
  1578. * Set a flag which will cause an X-Frame-Options header appropriate for
  1579. * edit pages to be sent. The header value is controlled by
  1580. * $wgEditPageFrameOptions.
  1581. *
  1582. * This is the default for special pages. If you display a CSRF-protected
  1583. * form on an ordinary view page, then you need to call this function.
  1584. *
  1585. * @param $enable bool
  1586. */
  1587. public function preventClickjacking( $enable = true ) {
  1588. $this->mPreventClickjacking = $enable;
  1589. }
  1590. /**
  1591. * Turn off frame-breaking. Alias for $this->preventClickjacking(false).
  1592. * This can be called from pages which do not contain any CSRF-protected
  1593. * HTML form.
  1594. */
  1595. public function allowClickjacking() {
  1596. $this->mPreventClickjacking = false;
  1597. }
  1598. /**
  1599. * Get the X-Frame-Options header value (without the name part), or false
  1600. * if there isn't one. This is used by Skin to determine whether to enable
  1601. * JavaScript frame-breaking, for clients that don't support X-Frame-Options.
  1602. *
  1603. * @return string
  1604. */
  1605. public function getFrameOptions() {
  1606. global $wgBreakFrames, $wgEditPageFrameOptions;
  1607. if ( $wgBreakFrames ) {
  1608. return 'DENY';
  1609. } elseif ( $this->mPreventClickjacking && $wgEditPageFrameOptions ) {
  1610. return $wgEditPageFrameOptions;
  1611. }
  1612. return false;
  1613. }
  1614. /**
  1615. * Send cache control HTTP headers
  1616. */
  1617. public function sendCacheControl() {
  1618. global $wgUseSquid, $wgUseESI, $wgUseETag, $wgSquidMaxage, $wgUseXVO;
  1619. $response = $this->getRequest()->response();
  1620. if ( $wgUseETag && $this->mETag ) {
  1621. $response->header( "ETag: $this->mETag" );
  1622. }
  1623. $this->addAcceptLanguage();
  1624. # don't serve compressed data to clients who can't handle it
  1625. # maintain different caches for logged-in users and non-logged in ones
  1626. $response->header( 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) ) );
  1627. if ( $wgUseXVO ) {
  1628. # Add an X-Vary-Options header for Squid with Wikimedia patches
  1629. $response->header( $this->getXVO() );
  1630. }
  1631. if( !$this->uncacheableBecauseRequestVars() && $this->mEnableClientCache ) {
  1632. if(
  1633. $wgUseSquid && session_id() == '' && !$this->isPrintable() &&
  1634. $this->mSquidMaxage != 0 && !$this->haveCacheVaryCookies()
  1635. )
  1636. {
  1637. if ( $wgUseESI ) {
  1638. # We'll purge the proxy cache explicitly, but require end user agents
  1639. # to revalidate against the proxy on each visit.
  1640. # Surrogate-Control controls our Squid, Cache-Control downstream caches
  1641. wfDebug( __METHOD__ . ": proxy caching with ESI; {$this->mLastModified} **\n", false );
  1642. # start with a shorter timeout for initial testing
  1643. # header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"');
  1644. $response->header( 'Surrogate-Control: max-age='.$wgSquidMaxage.'+'.$this->mSquidMaxage.', content="ESI/1.0"');
  1645. $response->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' );
  1646. } else {
  1647. # We'll purge the proxy cache for anons explicitly, but require end user agents
  1648. # to revalidate against the proxy on each visit.
  1649. # IMPORTANT! The Squid needs to replace the Cache-Control header with
  1650. # Cache-Control: s-maxage=0, must-revalidate, max-age=0
  1651. wfDebug( __METHOD__ . ": local proxy caching; {$this->mLastModified} **\n", false );
  1652. # start with a shorter timeout for initial testing
  1653. # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
  1654. $response->header( 'Cache-Control: s-maxage='.$this->mSquidMaxage.', must-revalidate, max-age=0' );
  1655. }
  1656. } else {
  1657. # We do want clients to cache if they can, but they *must* check for updates
  1658. # on revisiting the page.
  1659. wfDebug( __METHOD__ . ": private caching; {$this->mLastModified} **\n", false );
  1660. $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
  1661. $response->header( "Cache-Control: private, must-revalidate, max-age=0" );
  1662. }
  1663. if($this->mLastModified) {
  1664. $response->header( "Last-Modified: {$this->mLastModified}" );
  1665. }
  1666. } else {
  1667. wfDebug( __METHOD__ . ": no caching **\n", false );
  1668. # In general, the absence of a last modified header should be enough to prevent
  1669. # the client from using its cache. We send a few other things just to make sure.
  1670. $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
  1671. $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
  1672. $response->header( 'Pragma: no-cache' );
  1673. }
  1674. }
  1675. /**
  1676. * Get the message associed with the HTTP response code $code
  1677. *
  1678. * @param $code Integer: status code
  1679. * @return String or null: message or null if $code is not in the list of
  1680. * messages
  1681. *
  1682. * @deprecated since 1.18 Use HttpStatus::getMessage() instead.
  1683. */
  1684. public static function getStatusMessage( $code ) {
  1685. wfDeprecated( __METHOD__ );
  1686. return HttpStatus::getMessage( $code );
  1687. }
  1688. /**
  1689. * Finally, all the text has been munged and accumulated into
  1690. * the object, let's actually output it:
  1691. */
  1692. public function output() {
  1693. global $wgLanguageCode, $wgDebugRedirects, $wgMimeType, $wgVaryOnXFP;
  1694. if( $this->mDoNothing ) {
  1695. return;
  1696. }
  1697. wfProfileIn( __METHOD__ );
  1698. $response = $this->getRequest()->response();
  1699. if ( $this->mRedirect != '' ) {
  1700. # Standards require redirect URLs to be absolute
  1701. $this->mRedirect = wfExpandUrl( $this->mRedirect, PROTO_CURRENT );
  1702. $redirect = $this->mRedirect;
  1703. $code = $this->mRedirectCode;
  1704. if( wfRunHooks( "BeforePageRedirect", array( $this, &$redirect, &$code ) ) ) {
  1705. if( $code == '301' || $code == '303' ) {
  1706. if( !$wgDebugRedirects ) {
  1707. $message = HttpStatus::getMessage( $code );
  1708. $response->header( "HTTP/1.1 $code $message" );
  1709. }
  1710. $this->mLastModified = wfTimestamp( TS_RFC2822 );
  1711. }
  1712. if ( $wgVaryOnXFP ) {
  1713. $this->addVaryHeader( 'X-Forwarded-Proto' );
  1714. }
  1715. $this->sendCacheControl();
  1716. $response->header( "Content-Type: text/html; charset=utf-8" );
  1717. if( $wgDebugRedirects ) {
  1718. $url = htmlspecialchars( $redirect );
  1719. print "<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
  1720. print "<p>Location: <a href=\"$url\">$url</a></p>\n";
  1721. print "</body>\n</html>\n";
  1722. } else {
  1723. $response->header( 'Location: ' . $redirect );
  1724. }
  1725. }
  1726. wfProfileOut( __METHOD__ );
  1727. return;
  1728. } elseif ( $this->mStatusCode ) {
  1729. $message = HttpStatus::getMessage( $this->mStatusCode );
  1730. if ( $message ) {
  1731. $response->header( 'HTTP/1.1 ' . $this->mStatusCode . ' ' . $message );
  1732. }
  1733. }
  1734. # Buffer output; final headers may depend on later processing
  1735. ob_start();
  1736. $response->header( "Content-type: $wgMimeType; charset=UTF-8" );
  1737. $response->header( 'Content-language: ' . $wgLanguageCode );
  1738. // Prevent framing, if requested
  1739. $frameOptions = $this->getFrameOptions();
  1740. if ( $frameOptions ) {
  1741. $response->header( "X-Frame-Options: $frameOptions" );
  1742. }
  1743. if ( $this->mArticleBodyOnly ) {
  1744. $this->out( $this->mBodytext );
  1745. } else {
  1746. $this->addDefaultModules();
  1747. $sk = $this->getSkin();
  1748. // Hook that allows last minute changes to the output page, e.g.
  1749. // adding of CSS or Javascript by extensions.
  1750. wfRunHooks( 'BeforePageDisplay', array( &$this, &$sk ) );
  1751. wfProfileIn( 'Output-skin' );
  1752. $sk->outputPage();
  1753. wfProfileOut( 'Output-skin' );
  1754. }
  1755. $this->sendCacheControl();
  1756. ob_end_flush();
  1757. wfProfileOut( __METHOD__ );
  1758. }
  1759. /**
  1760. * Actually output something with print().
  1761. *
  1762. * @param $ins String: the string to output
  1763. */
  1764. public function out( $ins ) {
  1765. print $ins;
  1766. }
  1767. /**
  1768. * Produce a "user is blocked" page.
  1769. * @deprecated since 1.18
  1770. */
  1771. function blockedPage() {
  1772. throw new UserBlockedError( $this->getUser()->mBlock );
  1773. }
  1774. /**
  1775. * Prepare this object to display an error page; disable caching and
  1776. * indexing, clear the current text and redirect, set the page's title
  1777. * and optionally an custom HTML title (content of the <title> tag).
  1778. *
  1779. * @param $pageTitle String|Message will be passed directly to setPageTitle()
  1780. * @param $htmlTitle String|Message will be passed directly to setHTMLTitle();
  1781. * optional, if not passed the <title> attribute will be
  1782. * based on $pageTitle
  1783. */
  1784. public function prepareErrorPage( $pageTitle, $htmlTitle = false ) {
  1785. if ( $this->getTitle() ) {
  1786. $this->mDebugtext .= 'Original title: ' . $this->getTitle()->getPrefixedText() . "\n";
  1787. }
  1788. $this->setPageTitle( $pageTitle );
  1789. if ( $htmlTitle !== false ) {
  1790. $this->setHTMLTitle( $htmlTitle );
  1791. }
  1792. $this->setRobotPolicy( 'noindex,nofollow' );
  1793. $this->setArticleRelated( false );
  1794. $this->enableClientCache( false );
  1795. $this->mRedirect = '';
  1796. $this->clearSubtitle();
  1797. $this->clearHTML();
  1798. }
  1799. /**
  1800. * Output a standard error page
  1801. *
  1802. * showErrorPage( 'titlemsg', 'pagetextmsg', array( 'param1', 'param2' ) );
  1803. * showErrorPage( 'titlemsg', $messageObject );
  1804. *
  1805. * @param $title String: message key for page title
  1806. * @param $msg Mixed: message key (string) for page text, or a Message object
  1807. * @param $params Array: message parameters; ignored if $msg is a Message object
  1808. */
  1809. public function showErrorPage( $title, $msg, $params = array() ) {
  1810. $this->prepareErrorPage( $this->msg( $title ), $this->msg( 'errorpagetitle' ) );
  1811. if ( $msg instanceof Message ){
  1812. $this->addHTML( $msg->parse() );
  1813. } else {
  1814. $this->addWikiMsgArray( $msg, $params );
  1815. }
  1816. $this->returnToMain();
  1817. }
  1818. /**
  1819. * Output a standard permission error page
  1820. *
  1821. * @param $errors Array: error message keys
  1822. * @param $action String: action that was denied or null if unknown
  1823. */
  1824. public function showPermissionsErrorPage( $errors, $action = null ) {
  1825. global $wgGroupPermissions;
  1826. // For some action (read, edit, create and upload), display a "login to do this action"
  1827. // error if all of the following conditions are met:
  1828. // 1. the user is not logged in
  1829. // 2. the only error is insufficient permissions (i.e. no block or something else)
  1830. // 3. the error can be avoided simply by logging in
  1831. if ( in_array( $action, array( 'read', 'edit', 'createpage', 'createtalk', 'upload' ) )
  1832. && $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] )
  1833. && ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' )
  1834. && ( ( isset( $wgGroupPermissions['user'][$action] ) && $wgGroupPermissions['user'][$action] )
  1835. || ( isset( $wgGroupPermissions['autoconfirmed'][$action] ) && $wgGroupPermissions['autoconfirmed'][$action] ) )
  1836. ) {
  1837. $displayReturnto = null;
  1838. # Due to bug 32276, if a user does not have read permissions,
  1839. # $this->getTitle() will just give Special:Badtitle, which is
  1840. # not especially useful as a returnto parameter. Use the title
  1841. # from the request instead, if there was one.
  1842. $request = $this->getRequest();
  1843. $returnto = Title::newFromURL( $request->getVal( 'title', '' ) );
  1844. if ( $action == 'edit' ) {
  1845. $msg = 'whitelistedittext';
  1846. $displayReturnto = $returnto;
  1847. } elseif ( $action == 'createpage' || $action == 'createtalk' ) {
  1848. $msg = 'nocreatetext';
  1849. } elseif ( $action == 'upload' ) {
  1850. $msg = 'uploadnologintext';
  1851. } else { # Read
  1852. $msg = 'loginreqpagetext';
  1853. $displayReturnto = Title::newMainPage();
  1854. }
  1855. $query = array();
  1856. if ( $returnto ) {
  1857. $query['returnto'] = $returnto->getPrefixedText();
  1858. if ( !$request->wasPosted() ) {
  1859. $returntoquery = $request->getValues();
  1860. unset( $returntoquery['title'] );
  1861. unset( $returntoquery['returnto'] );
  1862. unset( $returntoquery['returntoquery'] );
  1863. $query['returntoquery'] = wfArrayToCGI( $returntoquery );
  1864. }
  1865. }
  1866. $loginLink = Linker::linkKnown(
  1867. SpecialPage::getTitleFor( 'Userlogin' ),
  1868. $this->msg( 'loginreqlink' )->escaped(),
  1869. array(),
  1870. $query
  1871. );
  1872. $this->prepareErrorPage( $this->msg( 'loginreqtitle' ) );
  1873. $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->parse() );
  1874. # Don't return to a page the user can't read otherwise
  1875. # we'll end up in a pointless loop
  1876. if ( $displayReturnto && $displayReturnto->userCan( 'read', $this->getUser() ) ) {
  1877. $this->returnToMain( null, $displayReturnto );
  1878. }
  1879. } else {
  1880. $this->prepareErrorPage( $this->msg( 'permissionserrors' ) );
  1881. $this->addWikiText( $this->formatPermissionsErrorMessage( $errors, $action ) );
  1882. }
  1883. }
  1884. /**
  1885. * Display an error page indicating that a given version of MediaWiki is
  1886. * required to use it
  1887. *
  1888. * @param $version Mixed: the version of MediaWiki needed to use the page
  1889. */
  1890. public function versionRequired( $version ) {
  1891. $this->prepareErrorPage( $this->msg( 'versionrequired', $version ) );
  1892. $this->addWikiMsg( 'versionrequiredtext', $version );
  1893. $this->returnToMain();
  1894. }
  1895. /**
  1896. * Display an error page noting that a given permission bit is required.
  1897. * @deprecated since 1.18, just throw the exception directly
  1898. * @param $permission String: key required
  1899. */
  1900. public function permissionRequired( $permission ) {
  1901. throw new PermissionsError( $permission );
  1902. }
  1903. /**
  1904. * Produce the stock "please login to use the wiki" page
  1905. *
  1906. * @deprecated in 1.19; throw the exception directly
  1907. */
  1908. public function loginToUse() {
  1909. throw new PermissionsError( 'read' );
  1910. }
  1911. /**
  1912. * Format a list of error messages
  1913. *
  1914. * @param $errors Array of arrays returned by Title::getUserPermissionsErrors
  1915. * @param $action String: action that was denied or null if unknown
  1916. * @return String: the wikitext error-messages, formatted into a list.
  1917. */
  1918. public function formatPermissionsErrorMessage( $errors, $action = null ) {
  1919. if ( $action == null ) {
  1920. $text = $this->msg( 'permissionserrorstext', count( $errors ) )->plain() . "\n\n";
  1921. } else {
  1922. $action_desc = $this->msg( "action-$action" )->plain();
  1923. $text = $this->msg(
  1924. 'permissionserrorstext-withaction',
  1925. count( $errors ),
  1926. $action_desc
  1927. )->plain() . "\n\n";
  1928. }
  1929. if ( count( $errors ) > 1 ) {
  1930. $text .= '<ul class="permissions-errors">' . "\n";
  1931. foreach( $errors as $error ) {
  1932. $text .= '<li>';
  1933. $text .= call_user_func_array( array( $this, 'msg' ), $error )->plain();
  1934. $text .= "</li>\n";
  1935. }
  1936. $text .= '</ul>';
  1937. } else {
  1938. $text .= "<div class=\"permissions-errors\">\n" .
  1939. call_user_func_array( array( $this, 'msg' ), reset( $errors ) )->plain() .
  1940. "\n</div>";
  1941. }
  1942. return $text;
  1943. }
  1944. /**
  1945. * Display a page stating that the Wiki is in read-only mode,
  1946. * and optionally show the source of the page that the user
  1947. * was trying to edit. Should only be called (for this
  1948. * purpose) after wfReadOnly() has returned true.
  1949. *
  1950. * For historical reasons, this function is _also_ used to
  1951. * show the error message when a user tries to edit a page
  1952. * they are not allowed to edit. (Unless it's because they're
  1953. * blocked, then we show blockedPage() instead.) In this
  1954. * case, the second parameter should be set to true and a list
  1955. * of reasons supplied as the third parameter.
  1956. *
  1957. * @todo Needs to be split into multiple functions.
  1958. *
  1959. * @param $source String: source code to show (or null).
  1960. * @param $protected Boolean: is this a permissions error?
  1961. * @param $reasons Array: list of reasons for this error, as returned by Title::getUserPermissionsErrors().
  1962. * @param $action String: action that was denied or null if unknown
  1963. */
  1964. public function readOnlyPage( $source = null, $protected = false, $reasons = array(), $action = null ) {
  1965. $this->setRobotPolicy( 'noindex,nofollow' );
  1966. $this->setArticleRelated( false );
  1967. // If no reason is given, just supply a default "I can't let you do
  1968. // that, Dave" message. Should only occur if called by legacy code.
  1969. if ( $protected && empty( $reasons ) ) {
  1970. $reasons[] = array( 'badaccess-group0' );
  1971. }
  1972. if ( !empty( $reasons ) ) {
  1973. // Permissions error
  1974. if( $source ) {
  1975. $this->setPageTitle( $this->msg( 'viewsource-title', $this->getTitle()->getPrefixedText() ) );
  1976. $this->addBacklinkSubtitle( $this->getTitle() );
  1977. } else {
  1978. $this->setPageTitle( $this->msg( 'badaccess' ) );
  1979. }
  1980. $this->addWikiText( $this->formatPermissionsErrorMessage( $reasons, $action ) );
  1981. } else {
  1982. // Wiki is read only
  1983. throw new ReadOnlyError;
  1984. }
  1985. // Show source, if supplied
  1986. if( is_string( $source ) ) {
  1987. $this->addWikiMsg( 'viewsourcetext' );
  1988. $pageLang = $this->getTitle()->getPageLanguage();
  1989. $params = array(
  1990. 'id' => 'wpTextbox1',
  1991. 'name' => 'wpTextbox1',
  1992. 'cols' => $this->getUser()->getOption( 'cols' ),
  1993. 'rows' => $this->getUser()->getOption( 'rows' ),
  1994. 'readonly' => 'readonly',
  1995. 'lang' => $pageLang->getHtmlCode(),
  1996. 'dir' => $pageLang->getDir(),
  1997. );
  1998. $this->addHTML( Html::element( 'textarea', $params, $source ) );
  1999. // Show templates used by this article
  2000. $templates = Linker::formatTemplates( $this->getTitle()->getTemplateLinksFrom() );
  2001. $this->addHTML( "<div class='templatesUsed'>
  2002. $templates
  2003. </div>
  2004. " );
  2005. }
  2006. # If the title doesn't exist, it's fairly pointless to print a return
  2007. # link to it. After all, you just tried editing it and couldn't, so
  2008. # what's there to do there?
  2009. if( $this->getTitle()->exists() ) {
  2010. $this->returnToMain( null, $this->getTitle() );
  2011. }
  2012. }
  2013. /**
  2014. * Turn off regular page output and return an error reponse
  2015. * for when rate limiting has triggered.
  2016. */
  2017. public function rateLimited() {
  2018. throw new ThrottledError;
  2019. }
  2020. /**
  2021. * Show a warning about slave lag
  2022. *
  2023. * If the lag is higher than $wgSlaveLagCritical seconds,
  2024. * then the warning is a bit more obvious. If the lag is
  2025. * lower than $wgSlaveLagWarning, then no warning is shown.
  2026. *
  2027. * @param $lag Integer: slave lag
  2028. */
  2029. public function showLagWarning( $lag ) {
  2030. global $wgSlaveLagWarning, $wgSlaveLagCritical;
  2031. if( $lag >= $wgSlaveLagWarning ) {
  2032. $message = $lag < $wgSlaveLagCritical
  2033. ? 'lag-warn-normal'
  2034. : 'lag-warn-high';
  2035. $wrap = Html::rawElement( 'div', array( 'class' => "mw-{$message}" ), "\n$1\n" );
  2036. $this->wrapWikiMsg( "$wrap\n", array( $message, $this->getLanguage()->formatNum( $lag ) ) );
  2037. }
  2038. }
  2039. public function showFatalError( $message ) {
  2040. $this->prepareErrorPage( $this->msg( 'internalerror' ) );
  2041. $this->addHTML( $message );
  2042. }
  2043. public function showUnexpectedValueError( $name, $val ) {
  2044. $this->showFatalError( $this->msg( 'unexpected', $name, $val )->text() );
  2045. }
  2046. public function showFileCopyError( $old, $new ) {
  2047. $this->showFatalError( $this->msg( 'filecopyerror', $old, $new )->text() );
  2048. }
  2049. public function showFileRenameError( $old, $new ) {
  2050. $this->showFatalError( $this->msg( 'filerenameerror', $old, $new )->text() );
  2051. }
  2052. public function showFileDeleteError( $name ) {
  2053. $this->showFatalError( $this->msg( 'filedeleteerror', $name )->text() );
  2054. }
  2055. public function showFileNotFoundError( $name ) {
  2056. $this->showFatalError( $this->msg( 'filenotfound', $name )->text() );
  2057. }
  2058. /**
  2059. * Add a "return to" link pointing to a specified title
  2060. *
  2061. * @param $title Title to link
  2062. * @param $query String query string
  2063. * @param $text String text of the link (input is not escaped)
  2064. */
  2065. public function addReturnTo( $title, $query = array(), $text = null ) {
  2066. $this->addLink( array( 'rel' => 'next', 'href' => $title->getFullURL() ) );
  2067. $link = $this->msg( 'returnto' )->rawParams(
  2068. Linker::link( $title, $text, array(), $query ) )->escaped();
  2069. $this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" );
  2070. }
  2071. /**
  2072. * Add a "return to" link pointing to a specified title,
  2073. * or the title indicated in the request, or else the main page
  2074. *
  2075. * @param $unused No longer used
  2076. * @param $returnto Title or String to return to
  2077. * @param $returntoquery String: query string for the return to link
  2078. */
  2079. public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) {
  2080. if ( $returnto == null ) {
  2081. $returnto = $this->getRequest()->getText( 'returnto' );
  2082. }
  2083. if ( $returntoquery == null ) {
  2084. $returntoquery = $this->getRequest()->getText( 'returntoquery' );
  2085. }
  2086. if ( $returnto === '' ) {
  2087. $returnto = Title::newMainPage();
  2088. }
  2089. if ( is_object( $returnto ) ) {
  2090. $titleObj = $returnto;
  2091. } else {
  2092. $titleObj = Title::newFromText( $returnto );
  2093. }
  2094. if ( !is_object( $titleObj ) ) {
  2095. $titleObj = Title::newMainPage();
  2096. }
  2097. $this->addReturnTo( $titleObj, $returntoquery );
  2098. }
  2099. /**
  2100. * @param $sk Skin The given Skin
  2101. * @param $includeStyle Boolean: unused
  2102. * @return String: The doctype, opening <html>, and head element.
  2103. */
  2104. public function headElement( Skin $sk, $includeStyle = true ) {
  2105. global $wgContLang;
  2106. $userdir = $this->getLanguage()->getDir();
  2107. $sitedir = $wgContLang->getDir();
  2108. if ( $sk->commonPrintStylesheet() ) {
  2109. $this->addModuleStyles( 'mediawiki.legacy.wikiprintable' );
  2110. }
  2111. $ret = Html::htmlHeader( array( 'lang' => $this->getLanguage()->getHtmlCode(), 'dir' => $userdir, 'class' => 'client-nojs' ) );
  2112. if ( $this->getHTMLTitle() == '' ) {
  2113. $this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() ) );
  2114. }
  2115. $openHead = Html::openElement( 'head' );
  2116. if ( $openHead ) {
  2117. # Don't bother with the newline if $head == ''
  2118. $ret .= "$openHead\n";
  2119. }
  2120. $ret .= Html::element( 'title', null, $this->getHTMLTitle() ) . "\n";
  2121. $ret .= implode( "\n", array(
  2122. $this->getHeadLinks( null, true ),
  2123. $this->buildCssLinks(),
  2124. $this->getHeadScripts(),
  2125. $this->getHeadItems()
  2126. ) );
  2127. $closeHead = Html::closeElement( 'head' );
  2128. if ( $closeHead ) {
  2129. $ret .= "$closeHead\n";
  2130. }
  2131. $bodyAttrs = array();
  2132. # Classes for LTR/RTL directionality support
  2133. $bodyAttrs['class'] = "mediawiki $userdir sitedir-$sitedir";
  2134. if ( $this->getLanguage()->capitalizeAllNouns() ) {
  2135. # A <body> class is probably not the best way to do this . . .
  2136. $bodyAttrs['class'] .= ' capitalize-all-nouns';
  2137. }
  2138. $bodyAttrs['class'] .= ' ' . $sk->getPageClasses( $this->getTitle() );
  2139. $bodyAttrs['class'] .= ' skin-' . Sanitizer::escapeClass( $sk->getSkinName() );
  2140. $bodyAttrs['class'] .= ' action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) );
  2141. $sk->addToBodyAttributes( $this, $bodyAttrs ); // Allow skins to add body attributes they need
  2142. wfRunHooks( 'OutputPageBodyAttributes', array( $this, $sk, &$bodyAttrs ) );
  2143. $ret .= Html::openElement( 'body', $bodyAttrs ) . "\n";
  2144. return $ret;
  2145. }
  2146. /**
  2147. * Add the default ResourceLoader modules to this object
  2148. */
  2149. private function addDefaultModules() {
  2150. global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil, $wgUseAjax,
  2151. $wgAjaxWatch, $wgEnableMWSuggest;
  2152. // Add base resources
  2153. $this->addModules( array(
  2154. 'mediawiki.user',
  2155. 'mediawiki.page.startup',
  2156. 'mediawiki.page.ready',
  2157. ) );
  2158. if ( $wgIncludeLegacyJavaScript ){
  2159. $this->addModules( 'mediawiki.legacy.wikibits' );
  2160. }
  2161. if ( $wgPreloadJavaScriptMwUtil ) {
  2162. $this->addModules( 'mediawiki.util' );
  2163. }
  2164. MWDebug::addModules( $this );
  2165. // Add various resources if required
  2166. if ( $wgUseAjax ) {
  2167. $this->addModules( 'mediawiki.legacy.ajax' );
  2168. wfRunHooks( 'AjaxAddScript', array( &$this ) );
  2169. if( $wgAjaxWatch && $this->getUser()->isLoggedIn() ) {
  2170. $this->addModules( 'mediawiki.action.watch.ajax' );
  2171. }
  2172. if ( $wgEnableMWSuggest && !$this->getUser()->getOption( 'disablesuggest', false ) ) {
  2173. $this->addModules( 'mediawiki.legacy.mwsuggest' );
  2174. }
  2175. }
  2176. if ( $this->getUser()->getBoolOption( 'editsectiononrightclick' ) ) {
  2177. $this->addModules( 'mediawiki.action.view.rightClickEdit' );
  2178. }
  2179. # Crazy edit-on-double-click stuff
  2180. if ( $this->isArticle() && $this->getUser()->getOption( 'editondblclick' ) ) {
  2181. $this->addModules( 'mediawiki.action.view.dblClickEdit' );
  2182. }
  2183. }
  2184. /**
  2185. * Get a ResourceLoader object associated with this OutputPage
  2186. *
  2187. * @return ResourceLoader
  2188. */
  2189. public function getResourceLoader() {
  2190. if ( is_null( $this->mResourceLoader ) ) {
  2191. $this->mResourceLoader = new ResourceLoader();
  2192. }
  2193. return $this->mResourceLoader;
  2194. }
  2195. /**
  2196. * TODO: Document
  2197. * @param $modules Array/string with the module name(s)
  2198. * @param $only String ResourceLoaderModule TYPE_ class constant
  2199. * @param $useESI boolean
  2200. * @param $extraQuery Array with extra query parameters to add to each request. array( param => value )
  2201. * @param $loadCall boolean If true, output an (asynchronous) mw.loader.load() call rather than a <script src="..."> tag
  2202. * @return string html <script> and <style> tags
  2203. */
  2204. protected function makeResourceLoaderLink( $modules, $only, $useESI = false, array $extraQuery = array(), $loadCall = false ) {
  2205. global $wgResourceLoaderUseESI;
  2206. if ( !count( $modules ) ) {
  2207. return '';
  2208. }
  2209. if ( count( $modules ) > 1 ) {
  2210. // Remove duplicate module requests
  2211. $modules = array_unique( (array) $modules );
  2212. // Sort module names so requests are more uniform
  2213. sort( $modules );
  2214. if ( ResourceLoader::inDebugMode() ) {
  2215. // Recursively call us for every item
  2216. $links = '';
  2217. foreach ( $modules as $name ) {
  2218. $links .= $this->makeResourceLoaderLink( $name, $only, $useESI );
  2219. }
  2220. return $links;
  2221. }
  2222. }
  2223. // Create keyed-by-group list of module objects from modules list
  2224. $groups = array();
  2225. $resourceLoader = $this->getResourceLoader();
  2226. foreach ( (array) $modules as $name ) {
  2227. $module = $resourceLoader->getModule( $name );
  2228. # Check that we're allowed to include this module on this page
  2229. if ( !$module
  2230. || ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS )
  2231. && $only == ResourceLoaderModule::TYPE_SCRIPTS )
  2232. || ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_STYLES )
  2233. && $only == ResourceLoaderModule::TYPE_STYLES )
  2234. )
  2235. {
  2236. continue;
  2237. }
  2238. $group = $module->getGroup();
  2239. if ( !isset( $groups[$group] ) ) {
  2240. $groups[$group] = array();
  2241. }
  2242. $groups[$group][$name] = $module;
  2243. }
  2244. $links = '';
  2245. foreach ( $groups as $group => $modules ) {
  2246. // Special handling for user-specific groups
  2247. $user = null;
  2248. if ( ( $group === 'user' || $group === 'private' ) && $this->getUser()->isLoggedIn() ) {
  2249. $user = $this->getUser()->getName();
  2250. }
  2251. // Create a fake request based on the one we are about to make so modules return
  2252. // correct timestamp and emptiness data
  2253. $query = ResourceLoader::makeLoaderQuery(
  2254. array(), // modules; not determined yet
  2255. $this->getLanguage()->getCode(),
  2256. $this->getSkin()->getSkinName(),
  2257. $user,
  2258. null, // version; not determined yet
  2259. ResourceLoader::inDebugMode(),
  2260. $only === ResourceLoaderModule::TYPE_COMBINED ? null : $only,
  2261. $this->isPrintable(),
  2262. $this->getRequest()->getBool( 'handheld' ),
  2263. $extraQuery
  2264. );
  2265. $context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
  2266. // Drop modules that know they're empty
  2267. foreach ( $modules as $key => $module ) {
  2268. if ( $module->isKnownEmpty( $context ) ) {
  2269. unset( $modules[$key] );
  2270. }
  2271. }
  2272. // If there are no modules left, skip this group
  2273. if ( $modules === array() ) {
  2274. continue;
  2275. }
  2276. // Inline private modules. These can't be loaded through load.php for security
  2277. // reasons, see bug 34907. Note that these modules should be loaded from
  2278. // getHeadScripts() before the first loader call. Otherwise other modules can't
  2279. // properly use them as dependencies (bug 30914)
  2280. if ( $group === 'private' ) {
  2281. if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
  2282. $links .= Html::inlineStyle(
  2283. $resourceLoader->makeModuleResponse( $context, $modules )
  2284. );
  2285. } else {
  2286. $links .= Html::inlineScript(
  2287. ResourceLoader::makeLoaderConditionalScript(
  2288. $resourceLoader->makeModuleResponse( $context, $modules )
  2289. )
  2290. );
  2291. }
  2292. $links .= "\n";
  2293. continue;
  2294. }
  2295. // Special handling for the user group; because users might change their stuff
  2296. // on-wiki like user pages, or user preferences; we need to find the highest
  2297. // timestamp of these user-changable modules so we can ensure cache misses on change
  2298. // This should NOT be done for the site group (bug 27564) because anons get that too
  2299. // and we shouldn't be putting timestamps in Squid-cached HTML
  2300. $version = null;
  2301. if ( $group === 'user' ) {
  2302. // Get the maximum timestamp
  2303. $timestamp = 1;
  2304. foreach ( $modules as $module ) {
  2305. $timestamp = max( $timestamp, $module->getModifiedTime( $context ) );
  2306. }
  2307. // Add a version parameter so cache will break when things change
  2308. $version = wfTimestamp( TS_ISO_8601_BASIC, $timestamp );
  2309. }
  2310. $url = ResourceLoader::makeLoaderURL(
  2311. array_keys( $modules ),
  2312. $this->getLanguage()->getCode(),
  2313. $this->getSkin()->getSkinName(),
  2314. $user,
  2315. $version,
  2316. ResourceLoader::inDebugMode(),
  2317. $only === ResourceLoaderModule::TYPE_COMBINED ? null : $only,
  2318. $this->isPrintable(),
  2319. $this->getRequest()->getBool( 'handheld' ),
  2320. $extraQuery
  2321. );
  2322. if ( $useESI && $wgResourceLoaderUseESI ) {
  2323. $esi = Xml::element( 'esi:include', array( 'src' => $url ) );
  2324. if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
  2325. $link = Html::inlineStyle( $esi );
  2326. } else {
  2327. $link = Html::inlineScript( $esi );
  2328. }
  2329. } else {
  2330. // Automatically select style/script elements
  2331. if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
  2332. $link = Html::linkedStyle( $url );
  2333. } else if ( $loadCall ) {
  2334. $link = Html::inlineScript(
  2335. ResourceLoader::makeLoaderConditionalScript(
  2336. Xml::encodeJsCall( 'mw.loader.load', array( $url, 'text/javascript', true ) )
  2337. )
  2338. );
  2339. } else {
  2340. $link = Html::linkedScript( $url );
  2341. }
  2342. }
  2343. if( $group == 'noscript' ){
  2344. $links .= Html::rawElement( 'noscript', array(), $link ) . "\n";
  2345. } else {
  2346. $links .= $link . "\n";
  2347. }
  2348. }
  2349. return $links;
  2350. }
  2351. /**
  2352. * JS stuff to put in the <head>. This is the startup module, config
  2353. * vars and modules marked with position 'top'
  2354. *
  2355. * @return String: HTML fragment
  2356. */
  2357. function getHeadScripts() {
  2358. global $wgResourceLoaderExperimentalAsyncLoading;
  2359. // Startup - this will immediately load jquery and mediawiki modules
  2360. $scripts = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true );
  2361. // Load config before anything else
  2362. $scripts .= Html::inlineScript(
  2363. ResourceLoader::makeLoaderConditionalScript(
  2364. ResourceLoader::makeConfigSetScript( $this->getJSVars() )
  2365. )
  2366. );
  2367. // Load embeddable private modules before any loader links
  2368. // This needs to be TYPE_COMBINED so these modules are properly wrapped
  2369. // in mw.loader.implement() calls and deferred until mw.user is available
  2370. $embedScripts = array( 'user.options', 'user.tokens' );
  2371. $scripts .= $this->makeResourceLoaderLink( $embedScripts, ResourceLoaderModule::TYPE_COMBINED );
  2372. // Script and Messages "only" requests marked for top inclusion
  2373. // Messages should go first
  2374. $scripts .= $this->makeResourceLoaderLink( $this->getModuleMessages( true, 'top' ), ResourceLoaderModule::TYPE_MESSAGES );
  2375. $scripts .= $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'top' ), ResourceLoaderModule::TYPE_SCRIPTS );
  2376. // Modules requests - let the client calculate dependencies and batch requests as it likes
  2377. // Only load modules that have marked themselves for loading at the top
  2378. $modules = $this->getModules( true, 'top' );
  2379. if ( $modules ) {
  2380. $scripts .= Html::inlineScript(
  2381. ResourceLoader::makeLoaderConditionalScript(
  2382. Xml::encodeJsCall( 'mw.loader.load', array( $modules ) )
  2383. )
  2384. );
  2385. }
  2386. if ( $wgResourceLoaderExperimentalAsyncLoading ) {
  2387. $scripts .= $this->getScriptsForBottomQueue( true );
  2388. }
  2389. return $scripts;
  2390. }
  2391. /**
  2392. * JS stuff to put at the 'bottom', which can either be the bottom of the <body>
  2393. * or the bottom of the <head> depending on $wgResourceLoaderExperimentalAsyncLoading:
  2394. * modules marked with position 'bottom', legacy scripts ($this->mScripts),
  2395. * user preferences, site JS and user JS
  2396. *
  2397. * @param $inHead boolean If true, this HTML goes into the <head>, if false it goes into the <body>
  2398. * @return string
  2399. */
  2400. function getScriptsForBottomQueue( $inHead ) {
  2401. global $wgUseSiteJs, $wgAllowUserJs;
  2402. // Script and Messages "only" requests marked for bottom inclusion
  2403. // If we're in the <head>, use load() calls rather than <script src="..."> tags
  2404. // Messages should go first
  2405. $scripts = $this->makeResourceLoaderLink( $this->getModuleMessages( true, 'bottom' ),
  2406. ResourceLoaderModule::TYPE_MESSAGES, /* $useESI = */ false, /* $extraQuery = */ array(),
  2407. /* $loadCall = */ $inHead
  2408. );
  2409. $scripts .= $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'bottom' ),
  2410. ResourceLoaderModule::TYPE_SCRIPTS, /* $useESI = */ false, /* $extraQuery = */ array(),
  2411. /* $loadCall = */ $inHead
  2412. );
  2413. // Modules requests - let the client calculate dependencies and batch requests as it likes
  2414. // Only load modules that have marked themselves for loading at the bottom
  2415. $modules = $this->getModules( true, 'bottom' );
  2416. if ( $modules ) {
  2417. $scripts .= Html::inlineScript(
  2418. ResourceLoader::makeLoaderConditionalScript(
  2419. Xml::encodeJsCall( 'mw.loader.load', array( $modules, null, true ) )
  2420. )
  2421. );
  2422. }
  2423. // Legacy Scripts
  2424. $scripts .= "\n" . $this->mScripts;
  2425. $userScripts = array();
  2426. // Add site JS if enabled
  2427. if ( $wgUseSiteJs ) {
  2428. $scripts .= $this->makeResourceLoaderLink( 'site', ResourceLoaderModule::TYPE_SCRIPTS,
  2429. /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
  2430. );
  2431. if( $this->getUser()->isLoggedIn() ){
  2432. $userScripts[] = 'user.groups';
  2433. }
  2434. }
  2435. // Add user JS if enabled
  2436. if ( $wgAllowUserJs && $this->getUser()->isLoggedIn() ) {
  2437. if( $this->getTitle() && $this->getTitle()->isJsSubpage() && $this->userCanPreview() ) {
  2438. # XXX: additional security check/prompt?
  2439. // We're on a preview of a JS subpage
  2440. // Exclude this page from the user module in case it's in there (bug 26283)
  2441. $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false,
  2442. array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead
  2443. );
  2444. // Load the previewed JS
  2445. $scripts .= Html::inlineScript( "\n" . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n";
  2446. } else {
  2447. // Include the user module normally
  2448. // We can't do $userScripts[] = 'user'; because the user module would end up
  2449. // being wrapped in a closure, so load it raw like 'site'
  2450. $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS,
  2451. /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
  2452. );
  2453. }
  2454. }
  2455. $scripts .= $this->makeResourceLoaderLink( $userScripts, ResourceLoaderModule::TYPE_COMBINED,
  2456. /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
  2457. );
  2458. return $scripts;
  2459. }
  2460. /**
  2461. * JS stuff to put at the bottom of the <body>
  2462. */
  2463. function getBottomScripts() {
  2464. global $wgResourceLoaderExperimentalAsyncLoading;
  2465. if ( !$wgResourceLoaderExperimentalAsyncLoading ) {
  2466. return $this->getScriptsForBottomQueue( false );
  2467. } else {
  2468. return '';
  2469. }
  2470. }
  2471. /**
  2472. * Add one or more variables to be set in mw.config in JavaScript.
  2473. *
  2474. * @param $key {String|Array} Key or array of key/value pars.
  2475. * @param $value {Mixed} [optional] Value of the configuration variable.
  2476. */
  2477. public function addJsConfigVars( $keys, $value = null ) {
  2478. if ( is_array( $keys ) ) {
  2479. foreach ( $keys as $key => $value ) {
  2480. $this->mJsConfigVars[$key] = $value;
  2481. }
  2482. return;
  2483. }
  2484. $this->mJsConfigVars[$keys] = $value;
  2485. }
  2486. /**
  2487. * Get an array containing the variables to be set in mw.config in JavaScript.
  2488. *
  2489. * DO NOT CALL THIS FROM OUTSIDE OF THIS CLASS OR Skin::makeGlobalVariablesScript().
  2490. * This is only public until that function is removed. You have been warned.
  2491. *
  2492. * Do not add things here which can be evaluated in ResourceLoaderStartupScript
  2493. * - in other words, page-independent/site-wide variables (without state).
  2494. * You will only be adding bloat to the html page and causing page caches to
  2495. * have to be purged on configuration changes.
  2496. * @return array
  2497. */
  2498. public function getJSVars() {
  2499. global $wgUseAjax, $wgEnableMWSuggest;
  2500. $latestRevID = 0;
  2501. $pageID = 0;
  2502. $canonicalName = false; # bug 21115
  2503. $title = $this->getTitle();
  2504. $ns = $title->getNamespace();
  2505. $nsname = MWNamespace::exists( $ns ) ? MWNamespace::getCanonicalName( $ns ) : $title->getNsText();
  2506. // Get the relevant title so that AJAX features can use the correct page name
  2507. // when making API requests from certain special pages (bug 34972).
  2508. $relevantTitle = $this->getSkin()->getRelevantTitle();
  2509. if ( $ns == NS_SPECIAL ) {
  2510. list( $canonicalName, /*...*/ ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
  2511. } elseif ( $this->canUseWikiPage() ) {
  2512. $wikiPage = $this->getWikiPage();
  2513. $latestRevID = $wikiPage->getLatest();
  2514. $pageID = $wikiPage->getId();
  2515. }
  2516. $lang = $title->getPageLanguage();
  2517. // Pre-process information
  2518. $separatorTransTable = $lang->separatorTransformTable();
  2519. $separatorTransTable = $separatorTransTable ? $separatorTransTable : array();
  2520. $compactSeparatorTransTable = array(
  2521. implode( "\t", array_keys( $separatorTransTable ) ),
  2522. implode( "\t", $separatorTransTable ),
  2523. );
  2524. $digitTransTable = $lang->digitTransformTable();
  2525. $digitTransTable = $digitTransTable ? $digitTransTable : array();
  2526. $compactDigitTransTable = array(
  2527. implode( "\t", array_keys( $digitTransTable ) ),
  2528. implode( "\t", $digitTransTable ),
  2529. );
  2530. $vars = array(
  2531. 'wgCanonicalNamespace' => $nsname,
  2532. 'wgCanonicalSpecialPageName' => $canonicalName,
  2533. 'wgNamespaceNumber' => $title->getNamespace(),
  2534. 'wgPageName' => $title->getPrefixedDBKey(),
  2535. 'wgTitle' => $title->getText(),
  2536. 'wgCurRevisionId' => $latestRevID,
  2537. 'wgArticleId' => $pageID,
  2538. 'wgIsArticle' => $this->isArticle(),
  2539. 'wgAction' => Action::getActionName( $this->getContext() ),
  2540. 'wgUserName' => $this->getUser()->isAnon() ? null : $this->getUser()->getName(),
  2541. 'wgUserGroups' => $this->getUser()->getEffectiveGroups(),
  2542. 'wgCategories' => $this->getCategories(),
  2543. 'wgBreakFrames' => $this->getFrameOptions() == 'DENY',
  2544. 'wgPageContentLanguage' => $lang->getCode(),
  2545. 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
  2546. 'wgDigitTransformTable' => $compactDigitTransTable,
  2547. 'wgRelevantPageName' => $relevantTitle->getPrefixedDBKey(),
  2548. );
  2549. if ( $lang->hasVariants() ) {
  2550. $vars['wgUserVariant'] = $lang->getPreferredVariant();
  2551. }
  2552. foreach ( $title->getRestrictionTypes() as $type ) {
  2553. $vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
  2554. }
  2555. if ( $wgUseAjax && $wgEnableMWSuggest && !$this->getUser()->getOption( 'disablesuggest', false ) ) {
  2556. $vars['wgSearchNamespaces'] = SearchEngine::userNamespaces( $this->getUser() );
  2557. }
  2558. if ( $title->isMainPage() ) {
  2559. $vars['wgIsMainPage'] = true;
  2560. }
  2561. if ( $this->mRedirectedFrom ) {
  2562. $vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBKey();
  2563. }
  2564. // Allow extensions to add their custom variables to the mw.config map.
  2565. // Use the 'ResourceLoaderGetConfigVars' hook if the variable is not
  2566. // page-dependant but site-wide (without state).
  2567. // Alternatively, you may want to use OutputPage->addJsConfigVars() instead.
  2568. wfRunHooks( 'MakeGlobalVariablesScript', array( &$vars, $this ) );
  2569. // Merge in variables from addJsConfigVars last
  2570. return array_merge( $vars, $this->mJsConfigVars );
  2571. }
  2572. /**
  2573. * To make it harder for someone to slip a user a fake
  2574. * user-JavaScript or user-CSS preview, a random token
  2575. * is associated with the login session. If it's not
  2576. * passed back with the preview request, we won't render
  2577. * the code.
  2578. *
  2579. * @return bool
  2580. */
  2581. public function userCanPreview() {
  2582. if ( $this->getRequest()->getVal( 'action' ) != 'submit'
  2583. || !$this->getRequest()->wasPosted()
  2584. || !$this->getUser()->matchEditToken(
  2585. $this->getRequest()->getVal( 'wpEditToken' ) )
  2586. ) {
  2587. return false;
  2588. }
  2589. if ( !$this->getTitle()->isJsSubpage() && !$this->getTitle()->isCssSubpage() ) {
  2590. return false;
  2591. }
  2592. return !count( $this->getTitle()->getUserPermissionsErrors( 'edit', $this->getUser() ) );
  2593. }
  2594. /**
  2595. * @param $unused Unused
  2596. * @param $addContentType bool
  2597. *
  2598. * @return string HTML tag links to be put in the header.
  2599. */
  2600. public function getHeadLinks( $unused = null, $addContentType = false ) {
  2601. global $wgUniversalEditButton, $wgFavicon, $wgAppleTouchIcon, $wgEnableAPI,
  2602. $wgSitename, $wgVersion, $wgHtml5, $wgMimeType,
  2603. $wgFeed, $wgOverrideSiteFeed, $wgAdvertisedFeedTypes,
  2604. $wgDisableLangConversion, $wgCanonicalLanguageLinks,
  2605. $wgRightsPage, $wgRightsUrl;
  2606. $tags = array();
  2607. if ( $addContentType ) {
  2608. if ( $wgHtml5 ) {
  2609. # More succinct than <meta http-equiv=Content-Type>, has the
  2610. # same effect
  2611. $tags[] = Html::element( 'meta', array( 'charset' => 'UTF-8' ) );
  2612. } else {
  2613. $tags[] = Html::element( 'meta', array(
  2614. 'http-equiv' => 'Content-Type',
  2615. 'content' => "$wgMimeType; charset=UTF-8"
  2616. ) );
  2617. $tags[] = Html::element( 'meta', array( // bug 15835
  2618. 'http-equiv' => 'Content-Style-Type',
  2619. 'content' => 'text/css'
  2620. ) );
  2621. }
  2622. }
  2623. $tags[] = Html::element( 'meta', array(
  2624. 'name' => 'generator',
  2625. 'content' => "MediaWiki $wgVersion",
  2626. ) );
  2627. $p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
  2628. if( $p !== 'index,follow' ) {
  2629. // http://www.robotstxt.org/wc/meta-user.html
  2630. // Only show if it's different from the default robots policy
  2631. $tags[] = Html::element( 'meta', array(
  2632. 'name' => 'robots',
  2633. 'content' => $p,
  2634. ) );
  2635. }
  2636. if ( count( $this->mKeywords ) > 0 ) {
  2637. $strip = array(
  2638. "/<.*?" . ">/" => '',
  2639. "/_/" => ' '
  2640. );
  2641. $tags[] = Html::element( 'meta', array(
  2642. 'name' => 'keywords',
  2643. 'content' => preg_replace(
  2644. array_keys( $strip ),
  2645. array_values( $strip ),
  2646. implode( ',', $this->mKeywords )
  2647. )
  2648. ) );
  2649. }
  2650. foreach ( $this->mMetatags as $tag ) {
  2651. if ( 0 == strcasecmp( 'http:', substr( $tag[0], 0, 5 ) ) ) {
  2652. $a = 'http-equiv';
  2653. $tag[0] = substr( $tag[0], 5 );
  2654. } else {
  2655. $a = 'name';
  2656. }
  2657. $tags[] = Html::element( 'meta',
  2658. array(
  2659. $a => $tag[0],
  2660. 'content' => $tag[1]
  2661. )
  2662. );
  2663. }
  2664. foreach ( $this->mLinktags as $tag ) {
  2665. $tags[] = Html::element( 'link', $tag );
  2666. }
  2667. # Universal edit button
  2668. if ( $wgUniversalEditButton && $this->isArticleRelated() ) {
  2669. $user = $this->getUser();
  2670. if ( $this->getTitle()->quickUserCan( 'edit', $user )
  2671. && ( $this->getTitle()->exists() || $this->getTitle()->quickUserCan( 'create', $user ) ) ) {
  2672. // Original UniversalEditButton
  2673. $msg = $this->msg( 'edit' )->text();
  2674. $tags[] = Html::element( 'link', array(
  2675. 'rel' => 'alternate',
  2676. 'type' => 'application/x-wiki',
  2677. 'title' => $msg,
  2678. 'href' => $this->getTitle()->getLocalURL( 'action=edit' )
  2679. ) );
  2680. // Alternate edit link
  2681. $tags[] = Html::element( 'link', array(
  2682. 'rel' => 'edit',
  2683. 'title' => $msg,
  2684. 'href' => $this->getTitle()->getLocalURL( 'action=edit' )
  2685. ) );
  2686. }
  2687. }
  2688. # Generally the order of the favicon and apple-touch-icon links
  2689. # should not matter, but Konqueror (3.5.9 at least) incorrectly
  2690. # uses whichever one appears later in the HTML source. Make sure
  2691. # apple-touch-icon is specified first to avoid this.
  2692. if ( $wgAppleTouchIcon !== false ) {
  2693. $tags[] = Html::element( 'link', array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) );
  2694. }
  2695. if ( $wgFavicon !== false ) {
  2696. $tags[] = Html::element( 'link', array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) );
  2697. }
  2698. # OpenSearch description link
  2699. $tags[] = Html::element( 'link', array(
  2700. 'rel' => 'search',
  2701. 'type' => 'application/opensearchdescription+xml',
  2702. 'href' => wfScript( 'opensearch_desc' ),
  2703. 'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(),
  2704. ) );
  2705. if ( $wgEnableAPI ) {
  2706. # Real Simple Discovery link, provides auto-discovery information
  2707. # for the MediaWiki API (and potentially additional custom API
  2708. # support such as WordPress or Twitter-compatible APIs for a
  2709. # blogging extension, etc)
  2710. $tags[] = Html::element( 'link', array(
  2711. 'rel' => 'EditURI',
  2712. 'type' => 'application/rsd+xml',
  2713. // Output a protocol-relative URL here if $wgServer is protocol-relative
  2714. // Whether RSD accepts relative or protocol-relative URLs is completely undocumented, though
  2715. 'href' => wfExpandUrl( wfAppendQuery( wfScript( 'api' ), array( 'action' => 'rsd' ) ), PROTO_RELATIVE ),
  2716. ) );
  2717. }
  2718. # Language variants
  2719. if ( !$wgDisableLangConversion && $wgCanonicalLanguageLinks ) {
  2720. $lang = $this->getTitle()->getPageLanguage();
  2721. if ( $lang->hasVariants() ) {
  2722. $urlvar = $lang->getURLVariant();
  2723. if ( !$urlvar ) {
  2724. $variants = $lang->getVariants();
  2725. foreach ( $variants as $_v ) {
  2726. $tags[] = Html::element( 'link', array(
  2727. 'rel' => 'alternate',
  2728. 'hreflang' => $_v,
  2729. 'href' => $this->getTitle()->getLocalURL( array( 'variant' => $_v ) ) )
  2730. );
  2731. }
  2732. } else {
  2733. $tags[] = Html::element( 'link', array(
  2734. 'rel' => 'canonical',
  2735. 'href' => $this->getTitle()->getCanonicalUrl()
  2736. ) );
  2737. }
  2738. }
  2739. }
  2740. # Copyright
  2741. $copyright = '';
  2742. if ( $wgRightsPage ) {
  2743. $copy = Title::newFromText( $wgRightsPage );
  2744. if ( $copy ) {
  2745. $copyright = $copy->getLocalURL();
  2746. }
  2747. }
  2748. if ( !$copyright && $wgRightsUrl ) {
  2749. $copyright = $wgRightsUrl;
  2750. }
  2751. if ( $copyright ) {
  2752. $tags[] = Html::element( 'link', array(
  2753. 'rel' => 'copyright',
  2754. 'href' => $copyright )
  2755. );
  2756. }
  2757. # Feeds
  2758. if ( $wgFeed ) {
  2759. foreach( $this->getSyndicationLinks() as $format => $link ) {
  2760. # Use the page name for the title. In principle, this could
  2761. # lead to issues with having the same name for different feeds
  2762. # corresponding to the same page, but we can't avoid that at
  2763. # this low a level.
  2764. $tags[] = $this->feedLink(
  2765. $format,
  2766. $link,
  2767. # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep)
  2768. $this->msg( "page-{$format}-feed", $this->getTitle()->getPrefixedText() )->text()
  2769. );
  2770. }
  2771. # Recent changes feed should appear on every page (except recentchanges,
  2772. # that would be redundant). Put it after the per-page feed to avoid
  2773. # changing existing behavior. It's still available, probably via a
  2774. # menu in your browser. Some sites might have a different feed they'd
  2775. # like to promote instead of the RC feed (maybe like a "Recent New Articles"
  2776. # or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined.
  2777. # If so, use it instead.
  2778. if ( $wgOverrideSiteFeed ) {
  2779. foreach ( $wgOverrideSiteFeed as $type => $feedUrl ) {
  2780. // Note, this->feedLink escapes the url.
  2781. $tags[] = $this->feedLink(
  2782. $type,
  2783. $feedUrl,
  2784. $this->msg( "site-{$type}-feed", $wgSitename )->text()
  2785. );
  2786. }
  2787. } elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) {
  2788. $rctitle = SpecialPage::getTitleFor( 'Recentchanges' );
  2789. foreach ( $wgAdvertisedFeedTypes as $format ) {
  2790. $tags[] = $this->feedLink(
  2791. $format,
  2792. $rctitle->getLocalURL( "feed={$format}" ),
  2793. $this->msg( "site-{$format}-feed", $wgSitename )->text() # For grep: 'site-rss-feed', 'site-atom-feed'.
  2794. );
  2795. }
  2796. }
  2797. }
  2798. return implode( "\n", $tags );
  2799. }
  2800. /**
  2801. * Generate a <link rel/> for a feed.
  2802. *
  2803. * @param $type String: feed type
  2804. * @param $url String: URL to the feed
  2805. * @param $text String: value of the "title" attribute
  2806. * @return String: HTML fragment
  2807. */
  2808. private function feedLink( $type, $url, $text ) {
  2809. return Html::element( 'link', array(
  2810. 'rel' => 'alternate',
  2811. 'type' => "application/$type+xml",
  2812. 'title' => $text,
  2813. 'href' => $url )
  2814. );
  2815. }
  2816. /**
  2817. * Add a local or specified stylesheet, with the given media options.
  2818. * Meant primarily for internal use...
  2819. *
  2820. * @param $style String: URL to the file
  2821. * @param $media String: to specify a media type, 'screen', 'printable', 'handheld' or any.
  2822. * @param $condition String: for IE conditional comments, specifying an IE version
  2823. * @param $dir String: set to 'rtl' or 'ltr' for direction-specific sheets
  2824. */
  2825. public function addStyle( $style, $media = '', $condition = '', $dir = '' ) {
  2826. $options = array();
  2827. // Even though we expect the media type to be lowercase, but here we
  2828. // force it to lowercase to be safe.
  2829. if( $media ) {
  2830. $options['media'] = $media;
  2831. }
  2832. if( $condition ) {
  2833. $options['condition'] = $condition;
  2834. }
  2835. if( $dir ) {
  2836. $options['dir'] = $dir;
  2837. }
  2838. $this->styles[$style] = $options;
  2839. }
  2840. /**
  2841. * Adds inline CSS styles
  2842. * @param $style_css Mixed: inline CSS
  2843. * @param $flip String: Set to 'flip' to flip the CSS if needed
  2844. */
  2845. public function addInlineStyle( $style_css, $flip = 'noflip' ) {
  2846. if( $flip === 'flip' && $this->getLanguage()->isRTL() ) {
  2847. # If wanted, and the interface is right-to-left, flip the CSS
  2848. $style_css = CSSJanus::transform( $style_css, true, false );
  2849. }
  2850. $this->mInlineStyles .= Html::inlineStyle( $style_css );
  2851. }
  2852. /**
  2853. * Build a set of <link>s for the stylesheets specified in the $this->styles array.
  2854. * These will be applied to various media & IE conditionals.
  2855. *
  2856. * @return string
  2857. */
  2858. public function buildCssLinks() {
  2859. global $wgUseSiteCss, $wgAllowUserCss, $wgAllowUserCssPrefs,
  2860. $wgLang, $wgContLang;
  2861. $this->getSkin()->setupSkinUserCss( $this );
  2862. // Add ResourceLoader styles
  2863. // Split the styles into four groups
  2864. $styles = array( 'other' => array(), 'user' => array(), 'site' => array(), 'private' => array(), 'noscript' => array() );
  2865. $otherTags = ''; // Tags to append after the normal <link> tags
  2866. $resourceLoader = $this->getResourceLoader();
  2867. $moduleStyles = $this->getModuleStyles();
  2868. // Per-site custom styles
  2869. if ( $wgUseSiteCss ) {
  2870. $moduleStyles[] = 'site';
  2871. $moduleStyles[] = 'noscript';
  2872. if( $this->getUser()->isLoggedIn() ){
  2873. $moduleStyles[] = 'user.groups';
  2874. }
  2875. }
  2876. // Per-user custom styles
  2877. if ( $wgAllowUserCss ) {
  2878. if ( $this->getTitle()->isCssSubpage() && $this->userCanPreview() ) {
  2879. // We're on a preview of a CSS subpage
  2880. // Exclude this page from the user module in case it's in there (bug 26283)
  2881. $otherTags .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_STYLES, false,
  2882. array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() )
  2883. );
  2884. // Load the previewed CSS
  2885. // If needed, Janus it first. This is user-supplied CSS, so it's
  2886. // assumed to be right for the content language directionality.
  2887. $previewedCSS = $this->getRequest()->getText( 'wpTextbox1' );
  2888. if ( $wgLang->getDir() !== $wgContLang->getDir() ) {
  2889. $previewedCSS = CSSJanus::transform( $previewedCSS, true, false );
  2890. }
  2891. $otherTags .= Html::inlineStyle( $previewedCSS );
  2892. } else {
  2893. // Load the user styles normally
  2894. $moduleStyles[] = 'user';
  2895. }
  2896. }
  2897. // Per-user preference styles
  2898. if ( $wgAllowUserCssPrefs ) {
  2899. $moduleStyles[] = 'user.cssprefs';
  2900. }
  2901. foreach ( $moduleStyles as $name ) {
  2902. $module = $resourceLoader->getModule( $name );
  2903. if ( !$module ) {
  2904. continue;
  2905. }
  2906. $group = $module->getGroup();
  2907. // Modules in groups named "other" or anything different than "user", "site" or "private"
  2908. // will be placed in the "other" group
  2909. $styles[isset( $styles[$group] ) ? $group : 'other'][] = $name;
  2910. }
  2911. // We want site, private and user styles to override dynamically added styles from modules, but we want
  2912. // dynamically added styles to override statically added styles from other modules. So the order
  2913. // has to be other, dynamic, site, private, user
  2914. // Add statically added styles for other modules
  2915. $ret = $this->makeResourceLoaderLink( $styles['other'], ResourceLoaderModule::TYPE_STYLES );
  2916. // Add normal styles added through addStyle()/addInlineStyle() here
  2917. $ret .= implode( "\n", $this->buildCssLinksArray() ) . $this->mInlineStyles;
  2918. // Add marker tag to mark the place where the client-side loader should inject dynamic styles
  2919. // We use a <meta> tag with a made-up name for this because that's valid HTML
  2920. $ret .= Html::element( 'meta', array( 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ) ) . "\n";
  2921. // Add site, private and user styles
  2922. // 'private' at present only contains user.options, so put that before 'user'
  2923. // Any future private modules will likely have a similar user-specific character
  2924. foreach ( array( 'site', 'noscript', 'private', 'user' ) as $group ) {
  2925. $ret .= $this->makeResourceLoaderLink( $styles[$group],
  2926. ResourceLoaderModule::TYPE_STYLES
  2927. );
  2928. }
  2929. // Add stuff in $otherTags (previewed user CSS if applicable)
  2930. $ret .= $otherTags;
  2931. return $ret;
  2932. }
  2933. /**
  2934. * @return Array
  2935. */
  2936. public function buildCssLinksArray() {
  2937. $links = array();
  2938. // Add any extension CSS
  2939. foreach ( $this->mExtStyles as $url ) {
  2940. $this->addStyle( $url );
  2941. }
  2942. $this->mExtStyles = array();
  2943. foreach( $this->styles as $file => $options ) {
  2944. $link = $this->styleLink( $file, $options );
  2945. if( $link ) {
  2946. $links[$file] = $link;
  2947. }
  2948. }
  2949. return $links;
  2950. }
  2951. /**
  2952. * Generate \<link\> tags for stylesheets
  2953. *
  2954. * @param $style String: URL to the file
  2955. * @param $options Array: option, can contain 'condition', 'dir', 'media'
  2956. * keys
  2957. * @return String: HTML fragment
  2958. */
  2959. protected function styleLink( $style, $options ) {
  2960. if( isset( $options['dir'] ) ) {
  2961. if( $this->getLanguage()->getDir() != $options['dir'] ) {
  2962. return '';
  2963. }
  2964. }
  2965. if( isset( $options['media'] ) ) {
  2966. $media = self::transformCssMedia( $options['media'] );
  2967. if( is_null( $media ) ) {
  2968. return '';
  2969. }
  2970. } else {
  2971. $media = 'all';
  2972. }
  2973. if( substr( $style, 0, 1 ) == '/' ||
  2974. substr( $style, 0, 5 ) == 'http:' ||
  2975. substr( $style, 0, 6 ) == 'https:' ) {
  2976. $url = $style;
  2977. } else {
  2978. global $wgStylePath, $wgStyleVersion;
  2979. $url = $wgStylePath . '/' . $style . '?' . $wgStyleVersion;
  2980. }
  2981. $link = Html::linkedStyle( $url, $media );
  2982. if( isset( $options['condition'] ) ) {
  2983. $condition = htmlspecialchars( $options['condition'] );
  2984. $link = "<!--[if $condition]>$link<![endif]-->";
  2985. }
  2986. return $link;
  2987. }
  2988. /**
  2989. * Transform "media" attribute based on request parameters
  2990. *
  2991. * @param $media String: current value of the "media" attribute
  2992. * @return String: modified value of the "media" attribute
  2993. */
  2994. public static function transformCssMedia( $media ) {
  2995. global $wgRequest, $wgHandheldForIPhone;
  2996. // Switch in on-screen display for media testing
  2997. $switches = array(
  2998. 'printable' => 'print',
  2999. 'handheld' => 'handheld',
  3000. );
  3001. foreach( $switches as $switch => $targetMedia ) {
  3002. if( $wgRequest->getBool( $switch ) ) {
  3003. if( $media == $targetMedia ) {
  3004. $media = '';
  3005. } elseif( $media == 'screen' ) {
  3006. return null;
  3007. }
  3008. }
  3009. }
  3010. // Expand longer media queries as iPhone doesn't grok 'handheld'
  3011. if( $wgHandheldForIPhone ) {
  3012. $mediaAliases = array(
  3013. 'screen' => 'screen and (min-device-width: 481px)',
  3014. 'handheld' => 'handheld, only screen and (max-device-width: 480px)',
  3015. );
  3016. if( isset( $mediaAliases[$media] ) ) {
  3017. $media = $mediaAliases[$media];
  3018. }
  3019. }
  3020. return $media;
  3021. }
  3022. /**
  3023. * Add a wikitext-formatted message to the output.
  3024. * This is equivalent to:
  3025. *
  3026. * $wgOut->addWikiText( wfMsgNoTrans( ... ) )
  3027. */
  3028. public function addWikiMsg( /*...*/ ) {
  3029. $args = func_get_args();
  3030. $name = array_shift( $args );
  3031. $this->addWikiMsgArray( $name, $args );
  3032. }
  3033. /**
  3034. * Add a wikitext-formatted message to the output.
  3035. * Like addWikiMsg() except the parameters are taken as an array
  3036. * instead of a variable argument list.
  3037. *
  3038. * @param $name string
  3039. * @param $args array
  3040. */
  3041. public function addWikiMsgArray( $name, $args ) {
  3042. $this->addHTML( $this->msg( $name, $args )->parseAsBlock() );
  3043. }
  3044. /**
  3045. * This function takes a number of message/argument specifications, wraps them in
  3046. * some overall structure, and then parses the result and adds it to the output.
  3047. *
  3048. * In the $wrap, $1 is replaced with the first message, $2 with the second, and so
  3049. * on. The subsequent arguments may either be strings, in which case they are the
  3050. * message names, or arrays, in which case the first element is the message name,
  3051. * and subsequent elements are the parameters to that message.
  3052. *
  3053. * The special named parameter 'options' in a message specification array is passed
  3054. * through to the $options parameter of wfMsgExt().
  3055. *
  3056. * Don't use this for messages that are not in users interface language.
  3057. *
  3058. * For example:
  3059. *
  3060. * $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>", 'some-error' );
  3061. *
  3062. * Is equivalent to:
  3063. *
  3064. * $wgOut->addWikiText( "<div class='error'>\n" . wfMsgNoTrans( 'some-error' ) . "\n</div>" );
  3065. *
  3066. * The newline after opening div is needed in some wikitext. See bug 19226.
  3067. *
  3068. * @param $wrap string
  3069. */
  3070. public function wrapWikiMsg( $wrap /*, ...*/ ) {
  3071. $msgSpecs = func_get_args();
  3072. array_shift( $msgSpecs );
  3073. $msgSpecs = array_values( $msgSpecs );
  3074. $s = $wrap;
  3075. foreach ( $msgSpecs as $n => $spec ) {
  3076. $options = array();
  3077. if ( is_array( $spec ) ) {
  3078. $args = $spec;
  3079. $name = array_shift( $args );
  3080. if ( isset( $args['options'] ) ) {
  3081. $options = $args['options'];
  3082. unset( $args['options'] );
  3083. }
  3084. } else {
  3085. $args = array();
  3086. $name = $spec;
  3087. }
  3088. $s = str_replace( '$' . ( $n + 1 ), wfMsgExt( $name, $options, $args ), $s );
  3089. }
  3090. $this->addWikiText( $s );
  3091. }
  3092. /**
  3093. * Include jQuery core. Use this to avoid loading it multiple times
  3094. * before we get a usable script loader.
  3095. *
  3096. * @param $modules Array: list of jQuery modules which should be loaded
  3097. * @return Array: the list of modules which were not loaded.
  3098. * @since 1.16
  3099. * @deprecated since 1.17
  3100. */
  3101. public function includeJQuery( $modules = array() ) {
  3102. return array();
  3103. }
  3104. }