diff options
Diffstat (limited to 'includes/OutputPage.php')
-rw-r--r-- | includes/OutputPage.php | 847 |
1 files changed, 579 insertions, 268 deletions
diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 68c771bc..a91d5465 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -13,7 +13,7 @@ if ( !defined( 'MEDIAWIKI' ) ) { * * @todo FIXME: Another class handles sending the whole page to the client. * - * Some comments comes from a pairing session between Zak Greant and Ashar Voultoiz + * Some comments comes from a pairing session between Zak Greant and Antoine Musso * in November 2010. * * @todo document @@ -47,7 +47,13 @@ class OutputPage extends ContextSource { var $mHTMLtitle = ''; /// Should be private. Is the displayed content related to the source of the corresponding wiki article. - var $mIsarticle = true; + var $mIsarticle = false; + + /** + * Should be private. Has get/set methods properly documented. + * Stores "article flag" toggle. + */ + var $mIsArticleRelated = true; /** * Should be private. We have to set isPrintable(). Some pages should @@ -61,7 +67,7 @@ class OutputPage extends ContextSource { * Contains the page subtitle. Special pages usually have some links here. * Don't confuse with site subtitle added by skins. */ - var $mSubtitle = ''; + private $mSubtitle = array(); var $mRedirect = ''; var $mStatusCode; @@ -126,9 +132,9 @@ class OutputPage extends ContextSource { var $mTemplateIds = array(); var $mImageTimeKeys = array(); - var $mRedirectCode = ''; + var $mRedirectCode = ''; - var $mFeedLinksAppendQuery = null; + var $mFeedLinksAppendQuery = null; # What level of 'untrustworthiness' is allowed in CSS/JS modules loaded on this page? # @see ResourceLoaderModule::$origin @@ -147,12 +153,9 @@ class OutputPage extends ContextSource { var $mContainsOldMagic = 0, $mContainsNewMagic = 0; /** - * Should be private. Has get/set methods properly documented. - * Stores "article flag" toggle. + * lazy initialised, use parserOptions() + * @var ParserOptions */ - var $mIsArticleRelated = true; - - /// lazy initialised, use parserOptions() protected $mParserOptions = null; /** @@ -194,6 +197,7 @@ class OutputPage extends ContextSource { /// should be private. To include the variable {{REVISIONID}} var $mRevisionId = null; + private $mRevisionTimestamp = null; var $mFileVersion = null; @@ -220,12 +224,25 @@ class OutputPage extends ContextSource { ); /** + * If the current page was reached through a redirect, $mRedirectedFrom contains the Title + * of the redirect. + * + * @var Title + */ + private $mRedirectedFrom = null; + + /** * Constructor for OutputPage. This should not be called directly. * Instead a new RequestContext should be created and it will implicitly create * a OutputPage tied to that context. */ function __construct( IContextSource $context = null ) { - $this->setContext( $context ); + if ( $context === null ) { + # Extensions should use `new RequestContext` instead of `new OutputPage` now. + wfDeprecated( __METHOD__ ); + } else { + $this->setContext( $context ); + } } /** @@ -464,7 +481,7 @@ class OutputPage extends ContextSource { * * @param $filter * @param $position - * + * * @return Array of module names */ public function getModuleStyles( $filter = false, $position = null ) { @@ -506,6 +523,15 @@ class OutputPage extends ContextSource { } /** + * Get an array of head items + * + * @return Array + */ + function getHeadItemsArray() { + return $this->mHeadItems; + } + + /** * Get all header items in a string * * @return String @@ -743,7 +769,11 @@ class OutputPage extends ContextSource { * @param $name string */ public function setHTMLTitle( $name ) { - $this->mHTMLtitle = $name; + if ( $name instanceof Message ) { + $this->mHTMLtitle = $name->setContext( $this->getContext() )->text(); + } else { + $this->mHTMLtitle = $name; + } } /** @@ -756,21 +786,34 @@ class OutputPage extends ContextSource { } /** + * Set $mRedirectedFrom, the Title of the page which redirected us to the current page. + * + * param @t Title + */ + public function setRedirectedFrom( $t ) { + $this->mRedirectedFrom = $t; + } + + /** * "Page title" means the contents of \<h1\>. It is stored as a valid HTML fragment. * This function allows good tags like \<sup\> in the \<h1\> tag, but not bad tags like \<script\>. * This function automatically sets \<title\> to the same content as \<h1\> but with all tags removed. * Bad tags that were escaped in \<h1\> will still be escaped in \<title\>, and good tags like \<i\> will be dropped entirely. * - * @param $name string + * @param $name string|Message */ public function setPageTitle( $name ) { + if ( $name instanceof Message ) { + $name = $name->setContext( $this->getContext() )->text(); + } + # change "<script>foo&bar</script>" to "<script>foo&bar</script>" # but leave "<i>foobar</i>" alone $nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) ); $this->mPagetitle = $nameWithTags; # change "<i>foo&bar</i>" to "foo&bar" - $this->setHTMLTitle( wfMsg( 'pagetitle', Sanitizer::stripAllTags( $nameWithTags ) ) ); + $this->setHTMLTitle( $this->msg( 'pagetitle' )->rawParams( Sanitizer::stripAllTags( $nameWithTags ) ) ); } /** @@ -787,7 +830,7 @@ class OutputPage extends ContextSource { * * @param $t Title object */ - public function setTitle( $t ) { + public function setTitle( Title $t ) { $this->getContext()->setTitle( $t ); } @@ -795,19 +838,54 @@ class OutputPage extends ContextSource { /** * Replace the subtile with $str * - * @param $str String: new value of the subtitle + * @param $str String|Message: new value of the subtitle */ public function setSubtitle( $str ) { - $this->mSubtitle = /*$this->parse(*/ $str /*)*/; // @bug 2514 + $this->clearSubtitle(); + $this->addSubtitle( $str ); } /** * Add $str to the subtitle * - * @param $str String to add to the subtitle + * @deprecated in 1.19; use addSubtitle() instead + * @param $str String|Message to add to the subtitle */ public function appendSubtitle( $str ) { - $this->mSubtitle .= /*$this->parse(*/ $str /*)*/; // @bug 2514 + $this->addSubtitle( $str ); + } + + /** + * Add $str to the subtitle + * + * @param $str String|Message to add to the subtitle + */ + public function addSubtitle( $str ) { + if ( $str instanceof Message ) { + $this->mSubtitle[] = $str->setContext( $this->getContext() )->parse(); + } else { + $this->mSubtitle[] = $str; + } + } + + /** + * Add a subtitle containing a backlink to a page + * + * @param $title Title to link to + */ + public function addBacklinkSubtitle( Title $title ) { + $query = array(); + if ( $title->isRedirect() ) { + $query['redirect'] = 'no'; + } + $this->addSubtitle( $this->msg( 'backlinksubtitle' )->rawParams( Linker::link( $title, null, array(), $query ) ) ); + } + + /** + * Clear the subtitles + */ + public function clearSubtitle() { + $this->mSubtitle = array(); } /** @@ -816,7 +894,7 @@ class OutputPage extends ContextSource { * @return String */ public function getSubtitle() { - return $this->mSubtitle; + return implode( "<br />\n\t\t\t\t", $this->mSubtitle ); } /** @@ -1130,9 +1208,11 @@ class OutputPage extends ContextSource { * Return whether user JavaScript is allowed for this page * @deprecated since 1.18 Load modules with ResourceLoader, and origin and * trustworthiness is identified and enforced automagically. + * Will be removed in 1.20. * @return Boolean */ public function isUserJsAllowed() { + wfDeprecated( __METHOD__, '1.18' ); return $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS ) >= ResourceLoaderModule::ORIGIN_USER_INDIVIDUAL; } @@ -1189,6 +1269,19 @@ class OutputPage extends ContextSource { } /** + * Shortcut for adding an Html::element via addHTML. + * + * @since 1.19 + * + * @param $element string + * @param $attribs array + * @param $contents string + */ + public function addElement( $element, $attribs = array(), $contents = '' ) { + $this->addHTML( Html::element( $element, $attribs, $contents ) ); + } + + /** * Clear the body HTML */ public function clearHTML() { @@ -1222,7 +1315,7 @@ class OutputPage extends ContextSource { */ public function parserOptions( $options = null ) { if ( !$this->mParserOptions ) { - $this->mParserOptions = new ParserOptions; + $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() ); $this->mParserOptions->setEditSection( false ); } return wfSetVar( $this->mParserOptions, $options ); @@ -1250,6 +1343,27 @@ class OutputPage extends ContextSource { } /** + * Set the timestamp of the revision which will be displayed. This is used + * to avoid a extra DB call in Skin::lastModified(). + * + * @param $revid Mixed: string, or null + * @return Mixed: previous value + */ + public function setRevisionTimestamp( $timestmap ) { + return wfSetVar( $this->mRevisionTimestamp, $timestmap ); + } + + /** + * Get the timestamp of displayed revision. + * This will be null if not filled by setRevisionTimestamp(). + * + * @return String or null + */ + public function getRevisionTimestamp() { + return $this->mRevisionTimestamp; + } + + /** * Set the displayed file version * * @param $file File|false @@ -1288,7 +1402,7 @@ class OutputPage extends ContextSource { * @return Array (dbKey => array('time' => MW timestamp or null, 'sha1' => sha1 or '')) * @since 1.18 */ - public function getImageTimeKeys() { + public function getFileSearchOptions() { return $this->mImageTimeKeys; } @@ -1298,10 +1412,11 @@ class OutputPage extends ContextSource { * * @param $text String * @param $linestart Boolean: is this the start of a line? + * @param $interface Boolean: is this text in the user interface language? */ - public function addWikiText( $text, $linestart = true ) { + public function addWikiText( $text, $linestart = true, $interface = true ) { $title = $this->getTitle(); // Work arround E_STRICT - $this->addWikiTextTitle( $text, $title, $linestart ); + $this->addWikiTextTitle( $text, $title, $linestart, /*tidy*/false, $interface ); } /** @@ -1344,16 +1459,17 @@ class OutputPage extends ContextSource { * @param $title Title object * @param $linestart Boolean: is this the start of a line? * @param $tidy Boolean: whether to use tidy + * @param $interface Boolean: whether it is an interface message + * (for example disables conversion) */ - public function addWikiTextTitle( $text, &$title, $linestart, $tidy = false ) { + public function addWikiTextTitle( $text, &$title, $linestart, $tidy = false, $interface = false ) { global $wgParser; wfProfileIn( __METHOD__ ); - wfIncrStats( 'pcache_not_possible' ); - $popts = $this->parserOptions(); $oldTidy = $popts->setTidy( $tidy ); + $popts->setInterfaceMessage( (bool) $interface ); $parserOutput = $wgParser->parse( $text, $title, $popts, @@ -1398,7 +1514,7 @@ class OutputPage extends ContextSource { } } // File versioning... - foreach ( (array)$parserOutput->getImageTimeKeys() as $dbk => $data ) { + foreach ( (array)$parserOutput->getFileSearchOptions() as $dbk => $data ) { $this->mImageTimeKeys[$dbk] = $data; } @@ -1446,7 +1562,8 @@ class OutputPage extends ContextSource { * @param $linestart Boolean: is this the start of a line? * @param $interface Boolean: use interface language ($wgLang instead of * $wgContLang) while parsing language sensitive magic - * words like GRAMMAR and PLURAL + * words like GRAMMAR and PLURAL. This also disables + * LanguageConverter. * @param $language Language object: target language object, will override * $interface * @return String: HTML @@ -1636,12 +1753,12 @@ class OutputPage extends ContextSource { * /w/index.php?title=Main_page&variant=zh-cn should never be served. */ function addAcceptLanguage() { - global $wgContLang; - if( !$this->getRequest()->getCheck( 'variant' ) && $wgContLang->hasVariants() ) { - $variants = $wgContLang->getVariants(); + $lang = $this->getTitle()->getPageLanguage(); + if( !$this->getRequest()->getCheck( 'variant' ) && $lang->hasVariants() ) { + $variants = $lang->getVariants(); $aloption = array(); foreach ( $variants as $variant ) { - if( $variant === $wgContLang->getCode() ) { + if( $variant === $lang->getCode() ) { continue; } else { $aloption[] = 'string-contains=' . $variant; @@ -1698,6 +1815,7 @@ class OutputPage extends ContextSource { } elseif ( $this->mPreventClickjacking && $wgEditPageFrameOptions ) { return $wgEditPageFrameOptions; } + return false; } /** @@ -1773,7 +1891,7 @@ class OutputPage extends ContextSource { * * @param $code Integer: status code * @return String or null: message or null if $code is not in the list of - * messages + * messages * * @deprecated since 1.18 Use HttpStatus::getMessage() instead. */ @@ -1861,7 +1979,7 @@ class OutputPage extends ContextSource { wfRunHooks( 'BeforePageDisplay', array( &$this, &$sk ) ); wfProfileIn( 'Output-skin' ); - $sk->outputPage( $this ); + $sk->outputPage(); wfProfileOut( 'Output-skin' ); } @@ -1888,26 +2006,44 @@ class OutputPage extends ContextSource { } /** - * Output a standard error page + * Prepare this object to display an error page; disable caching and + * indexing, clear the current text and redirect, set the page's title + * and optionally an custom HTML title (content of the <title> tag). * - * showErrorPage( 'titlemsg', 'pagetextmsg', array( 'param1', 'param2' ) ); - * showErrorPage( 'titlemsg', $messageObject ); - * - * @param $title String: message key for page title - * @param $msg Mixed: message key (string) for page text, or a Message object - * @param $params Array: message parameters; ignored if $msg is a Message object + * @param $pageTitle String|Message will be passed directly to setPageTitle() + * @param $htmlTitle String|Message will be passed directly to setHTMLTitle(); + * optional, if not passed the <title> attribute will be + * based on $pageTitle */ - public function showErrorPage( $title, $msg, $params = array() ) { + public function prepareErrorPage( $pageTitle, $htmlTitle = false ) { if ( $this->getTitle() ) { $this->mDebugtext .= 'Original title: ' . $this->getTitle()->getPrefixedText() . "\n"; } - $this->setPageTitle( wfMsg( $title ) ); - $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); + + $this->setPageTitle( $pageTitle ); + if ( $htmlTitle !== false ) { + $this->setHTMLTitle( $htmlTitle ); + } $this->setRobotPolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); $this->enableClientCache( false ); $this->mRedirect = ''; - $this->mBodytext = ''; + $this->clearSubtitle(); + $this->clearHTML(); + } + + /** + * Output a standard error page + * + * showErrorPage( 'titlemsg', 'pagetextmsg', array( 'param1', 'param2' ) ); + * showErrorPage( 'titlemsg', $messageObject ); + * + * @param $title String: message key for page title + * @param $msg Mixed: message key (string) for page text, or a Message object + * @param $params Array: message parameters; ignored if $msg is a Message object + */ + public function showErrorPage( $title, $msg, $params = array() ) { + $this->prepareErrorPage( $this->msg( $title ), $this->msg( 'errorpagetitle' ) ); if ( $msg instanceof Message ){ $this->addHTML( $msg->parse() ); @@ -1925,16 +2061,71 @@ class OutputPage extends ContextSource { * @param $action String: action that was denied or null if unknown */ public function showPermissionsErrorPage( $errors, $action = null ) { - $this->mDebugtext .= 'Original title: ' . - $this->getTitle()->getPrefixedText() . "\n"; - $this->setPageTitle( wfMsg( 'permissionserrors' ) ); - $this->setHTMLTitle( wfMsg( 'permissionserrors' ) ); - $this->setRobotPolicy( 'noindex,nofollow' ); - $this->setArticleRelated( false ); - $this->enableClientCache( false ); - $this->mRedirect = ''; - $this->mBodytext = ''; - $this->addWikiText( $this->formatPermissionsErrorMessage( $errors, $action ) ); + global $wgGroupPermissions; + + // For some action (read, edit, create and upload), display a "login to do this action" + // error if all of the following conditions are met: + // 1. the user is not logged in + // 2. the only error is insufficient permissions (i.e. no block or something else) + // 3. the error can be avoided simply by logging in + if ( in_array( $action, array( 'read', 'edit', 'createpage', 'createtalk', 'upload' ) ) + && $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] ) + && ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' ) + && ( ( isset( $wgGroupPermissions['user'][$action] ) && $wgGroupPermissions['user'][$action] ) + || ( isset( $wgGroupPermissions['autoconfirmed'][$action] ) && $wgGroupPermissions['autoconfirmed'][$action] ) ) + ) { + $displayReturnto = null; + + # Due to bug 32276, if a user does not have read permissions, + # $this->getTitle() will just give Special:Badtitle, which is + # not especially useful as a returnto parameter. Use the title + # from the request instead, if there was one. + $request = $this->getRequest(); + $returnto = Title::newFromURL( $request->getVal( 'title', '' ) ); + if ( $action == 'edit' ) { + $msg = 'whitelistedittext'; + $displayReturnto = $returnto; + } elseif ( $action == 'createpage' || $action == 'createtalk' ) { + $msg = 'nocreatetext'; + } elseif ( $action == 'upload' ) { + $msg = 'uploadnologintext'; + } else { # Read + $msg = 'loginreqpagetext'; + $displayReturnto = Title::newMainPage(); + } + + $query = array(); + + if ( $returnto ) { + $query['returnto'] = $returnto->getPrefixedText(); + + if ( !$request->wasPosted() ) { + $returntoquery = $request->getValues(); + unset( $returntoquery['title'] ); + unset( $returntoquery['returnto'] ); + unset( $returntoquery['returntoquery'] ); + $query['returntoquery'] = wfArrayToCGI( $returntoquery ); + } + } + $loginLink = Linker::linkKnown( + SpecialPage::getTitleFor( 'Userlogin' ), + $this->msg( 'loginreqlink' )->escaped(), + array(), + $query + ); + + $this->prepareErrorPage( $this->msg( 'loginreqtitle' ) ); + $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->parse() ); + + # Don't return to a page the user can't read otherwise + # we'll end up in a pointless loop + if ( $displayReturnto && $displayReturnto->userCan( 'read', $this->getUser() ) ) { + $this->returnToMain( null, $displayReturnto ); + } + } else { + $this->prepareErrorPage( $this->msg( 'permissionserrors' ) ); + $this->addWikiText( $this->formatPermissionsErrorMessage( $errors, $action ) ); + } } /** @@ -1944,11 +2135,7 @@ class OutputPage extends ContextSource { * @param $version Mixed: the version of MediaWiki needed to use the page */ public function versionRequired( $version ) { - $this->setPageTitle( wfMsg( 'versionrequired', $version ) ); - $this->setHTMLTitle( wfMsg( 'versionrequired', $version ) ); - $this->setRobotPolicy( 'noindex,nofollow' ); - $this->setArticleRelated( false ); - $this->mBodytext = ''; + $this->prepareErrorPage( $this->msg( 'versionrequired', $version ) ); $this->addWikiMsg( 'versionrequiredtext', $version ); $this->returnToMain(); @@ -1965,41 +2152,11 @@ class OutputPage extends ContextSource { /** * Produce the stock "please login to use the wiki" page + * + * @deprecated in 1.19; throw the exception directly */ public function loginToUse() { - global $wgRequest; - - if( $this->getUser()->isLoggedIn() ) { - throw new PermissionsError( 'read' ); - } - - $this->setPageTitle( wfMsg( 'loginreqtitle' ) ); - $this->setHtmlTitle( wfMsg( 'errorpagetitle' ) ); - $this->setRobotPolicy( 'noindex,nofollow' ); - $this->setArticleRelated( false ); - - $returnto = Title::newFromURL( $wgRequest->getVal( 'title', '' ) ); - $returntoquery = array(); - if( $returnto ) { - $returntoquery = array( 'returnto' => $returnto->getPrefixedText() ); - } - - $loginTitle = SpecialPage::getTitleFor( 'Userlogin' ); - $loginLink = Linker::linkKnown( - $loginTitle, - wfMsgHtml( 'loginreqlink' ), - array(), - $returntoquery - ); - $this->addHTML( wfMessage( 'loginreqpagetext' )->rawParams( $loginLink )->parse() . - "\n<!--" . $this->getTitle()->getPrefixedUrl() . '-->' ); - - # Don't return to the main page if the user can't read it - # otherwise we'll end up in a pointless loop - $mainPage = Title::newMainPage(); - if( $mainPage->userCanRead() ) { - $this->returnToMain( null, $mainPage ); - } + throw new PermissionsError( 'read' ); } /** @@ -2011,14 +2168,14 @@ class OutputPage extends ContextSource { */ public function formatPermissionsErrorMessage( $errors, $action = null ) { if ( $action == null ) { - $text = wfMsgNoTrans( 'permissionserrorstext', count( $errors ) ) . "\n\n"; + $text = $this->msg( 'permissionserrorstext', count( $errors ) )->plain() . "\n\n"; } else { - $action_desc = wfMsgNoTrans( "action-$action" ); - $text = wfMsgNoTrans( + $action_desc = $this->msg( "action-$action" )->plain(); + $text = $this->msg( 'permissionserrorstext-withaction', count( $errors ), $action_desc - ) . "\n\n"; + )->plain() . "\n\n"; } if ( count( $errors ) > 1 ) { @@ -2026,13 +2183,13 @@ class OutputPage extends ContextSource { foreach( $errors as $error ) { $text .= '<li>'; - $text .= call_user_func_array( 'wfMsgNoTrans', $error ); + $text .= call_user_func_array( array( $this, 'msg' ), $error )->plain(); $text .= "</li>\n"; } $text .= '</ul>'; } else { $text .= "<div class=\"permissions-errors\">\n" . - call_user_func_array( 'wfMsgNoTrans', reset( $errors ) ) . + call_user_func_array( array( $this, 'msg' ), reset( $errors ) )->plain() . "\n</div>"; } @@ -2072,12 +2229,10 @@ class OutputPage extends ContextSource { if ( !empty( $reasons ) ) { // Permissions error if( $source ) { - $this->setPageTitle( wfMsg( 'viewsource' ) ); - $this->setSubtitle( - wfMsg( 'viewsourcefor', Linker::linkKnown( $this->getTitle() ) ) - ); + $this->setPageTitle( $this->msg( 'viewsource-title', $this->getTitle()->getPrefixedText() ) ); + $this->addBacklinkSubtitle( $this->getTitle() ); } else { - $this->setPageTitle( wfMsg( 'badaccess' ) ); + $this->setPageTitle( $this->msg( 'badaccess' ) ); } $this->addWikiText( $this->formatPermissionsErrorMessage( $reasons, $action ) ); } else { @@ -2089,18 +2244,20 @@ class OutputPage extends ContextSource { if( is_string( $source ) ) { $this->addWikiMsg( 'viewsourcetext' ); + $pageLang = $this->getTitle()->getPageLanguage(); $params = array( 'id' => 'wpTextbox1', 'name' => 'wpTextbox1', 'cols' => $this->getUser()->getOption( 'cols' ), 'rows' => $this->getUser()->getOption( 'rows' ), - 'readonly' => 'readonly' + 'readonly' => 'readonly', + 'lang' => $pageLang->getHtmlCode(), + 'dir' => $pageLang->getDir(), ); $this->addHTML( Html::element( 'textarea', $params, $source ) ); // Show templates used by this article - $article = new Article( $this->getTitle() ); - $templates = Linker::formatTemplates( $article->getUsedTemplates() ); + $templates = Linker::formatTemplates( $this->getTitle()->getTemplateLinksFrom() ); $this->addHTML( "<div class='templatesUsed'> $templates </div> @@ -2139,37 +2296,34 @@ $templates ? 'lag-warn-normal' : 'lag-warn-high'; $wrap = Html::rawElement( 'div', array( 'class' => "mw-{$message}" ), "\n$1\n" ); - $this->wrapWikiMsg( "$wrap\n", array( $message, $this->getContext()->getLang()->formatNum( $lag ) ) ); + $this->wrapWikiMsg( "$wrap\n", array( $message, $this->getLanguage()->formatNum( $lag ) ) ); } } public function showFatalError( $message ) { - $this->setPageTitle( wfMsg( 'internalerror' ) ); - $this->setRobotPolicy( 'noindex,nofollow' ); - $this->setArticleRelated( false ); - $this->enableClientCache( false ); - $this->mRedirect = ''; - $this->mBodytext = $message; + $this->prepareErrorPage( $this->msg( 'internalerror' ) ); + + $this->addHTML( $message ); } public function showUnexpectedValueError( $name, $val ) { - $this->showFatalError( wfMsg( 'unexpected', $name, $val ) ); + $this->showFatalError( $this->msg( 'unexpected', $name, $val )->text() ); } public function showFileCopyError( $old, $new ) { - $this->showFatalError( wfMsg( 'filecopyerror', $old, $new ) ); + $this->showFatalError( $this->msg( 'filecopyerror', $old, $new )->text() ); } public function showFileRenameError( $old, $new ) { - $this->showFatalError( wfMsg( 'filerenameerror', $old, $new ) ); + $this->showFatalError( $this->msg( 'filerenameerror', $old, $new )->text() ); } public function showFileDeleteError( $name ) { - $this->showFatalError( wfMsg( 'filedeleteerror', $name ) ); + $this->showFatalError( $this->msg( 'filedeleteerror', $name )->text() ); } public function showFileNotFoundError( $name ) { - $this->showFatalError( wfMsg( 'filenotfound', $name ) ); + $this->showFatalError( $this->msg( 'filenotfound', $name )->text() ); } /** @@ -2181,10 +2335,8 @@ $templates */ public function addReturnTo( $title, $query = array(), $text = null ) { $this->addLink( array( 'rel' => 'next', 'href' => $title->getFullURL() ) ); - $link = wfMsgHtml( - 'returnto', - Linker::link( $title, $text, array(), $query ) - ); + $link = $this->msg( 'returnto' )->rawParams( + Linker::link( $title, $text, array(), $query ) )->escaped(); $this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" ); } @@ -2227,19 +2379,19 @@ $templates * @return String: The doctype, opening <html>, and head element. */ public function headElement( Skin $sk, $includeStyle = true ) { - global $wgContLang, $wgUseTrackbacks; - $userdir = $this->getLang()->getDir(); + global $wgContLang; + + $userdir = $this->getLanguage()->getDir(); $sitedir = $wgContLang->getDir(); if ( $sk->commonPrintStylesheet() ) { $this->addModuleStyles( 'mediawiki.legacy.wikiprintable' ); } - $sk->setupUserCss( $this ); - $ret = Html::htmlHeader( array( 'lang' => $this->getLang()->getCode(), 'dir' => $userdir, 'class' => 'client-nojs' ) ); + $ret = Html::htmlHeader( array( 'lang' => $this->getLanguage()->getHtmlCode(), 'dir' => $userdir, 'class' => 'client-nojs' ) ); if ( $this->getHTMLTitle() == '' ) { - $this->setHTMLTitle( wfMsg( 'pagetitle', $this->getPageTitle() ) ); + $this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() ) ); } $openHead = Html::openElement( 'head' ); @@ -2251,16 +2403,12 @@ $templates $ret .= Html::element( 'title', null, $this->getHTMLTitle() ) . "\n"; $ret .= implode( "\n", array( - $this->getHeadLinks( $sk, true ), - $this->buildCssLinks( $sk ), - $this->getHeadScripts( $sk ), + $this->getHeadLinks( null, true ), + $this->buildCssLinks(), + $this->getHeadScripts(), $this->getHeadItems() ) ); - if ( $wgUseTrackbacks && $this->isArticleRelated() ) { - $ret .= $this->getTitle()->trackbackRDF(); - } - $closeHead = Html::closeElement( 'head' ); if ( $closeHead ) { $ret .= "$closeHead\n"; @@ -2268,29 +2416,16 @@ $templates $bodyAttrs = array(); - # Crazy edit-on-double-click stuff - $action = $this->getRequest()->getVal( 'action', 'view' ); - - if ( - $this->getTitle()->getNamespace() != NS_SPECIAL && - in_array( $action, array( 'view', 'purge' ) ) && - $this->getUser()->getOption( 'editondblclick' ) - ) - { - $editUrl = $this->getTitle()->getLocalUrl( $sk->editUrlOptions() ); - $bodyAttrs['ondblclick'] = "document.location = '" . - Xml::escapeJsString( $editUrl ) . "'"; - } - # Classes for LTR/RTL directionality support $bodyAttrs['class'] = "mediawiki $userdir sitedir-$sitedir"; - if ( $this->getContext()->getLang()->capitalizeAllNouns() ) { + if ( $this->getLanguage()->capitalizeAllNouns() ) { # A <body> class is probably not the best way to do this . . . $bodyAttrs['class'] .= ' capitalize-all-nouns'; } $bodyAttrs['class'] .= ' ' . $sk->getPageClasses( $this->getTitle() ); $bodyAttrs['class'] .= ' skin-' . Sanitizer::escapeClass( $sk->getSkinName() ); + $bodyAttrs['class'] .= ' action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) ); $sk->addToBodyAttributes( $this, $bodyAttrs ); // Allow skins to add body attributes they need wfRunHooks( 'OutputPageBodyAttributes', array( $this, $sk, &$bodyAttrs ) ); @@ -2304,13 +2439,12 @@ $templates * Add the default ResourceLoader modules to this object */ private function addDefaultModules() { - global $wgIncludeLegacyJavaScript, $wgUseAjax, + global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil, $wgUseAjax, $wgAjaxWatch, $wgEnableMWSuggest; // Add base resources $this->addModules( array( 'mediawiki.user', - 'mediawiki.util', 'mediawiki.page.startup', 'mediawiki.page.ready', ) ); @@ -2318,6 +2452,12 @@ $templates $this->addModules( 'mediawiki.legacy.wikibits' ); } + if ( $wgPreloadJavaScriptMwUtil ) { + $this->addModules( 'mediawiki.util' ); + } + + MWDebug::addModules( $this ); + // Add various resources if required if ( $wgUseAjax ) { $this->addModules( 'mediawiki.legacy.ajax' ); @@ -2336,6 +2476,11 @@ $templates if ( $this->getUser()->getBoolOption( 'editsectiononrightclick' ) ) { $this->addModules( 'mediawiki.action.view.rightClickEdit' ); } + + # Crazy edit-on-double-click stuff + if ( $this->isArticle() && $this->getUser()->getOption( 'editondblclick' ) ) { + $this->addModules( 'mediawiki.action.view.dblClickEdit' ); + } } /** @@ -2352,29 +2497,15 @@ $templates /** * TODO: Document - * @param $skin Skin - * @param $modules Array/string with the module name + * @param $modules Array/string with the module name(s) * @param $only String ResourceLoaderModule TYPE_ class constant * @param $useESI boolean + * @param $extraQuery Array with extra query parameters to add to each request. array( param => value ) + * @param $loadCall boolean If true, output an (asynchronous) mw.loader.load() call rather than a <script src="..."> tag * @return string html <script> and <style> tags */ - protected function makeResourceLoaderLink( Skin $skin, $modules, $only, $useESI = false ) { - global $wgLoadScript, $wgResourceLoaderUseESI; - // Lazy-load ResourceLoader - // TODO: Should this be a static function of ResourceLoader instead? - $baseQuery = array( - 'lang' => $this->getContext()->getLang()->getCode(), - 'debug' => ResourceLoader::inDebugMode() ? 'true' : 'false', - 'skin' => $skin->getSkinName(), - 'only' => $only, - ); - // Propagate printable and handheld parameters if present - if ( $this->isPrintable() ) { - $baseQuery['printable'] = 1; - } - if ( $this->getRequest()->getBool( 'handheld' ) ) { - $baseQuery['handheld'] = 1; - } + protected function makeResourceLoaderLink( $modules, $only, $useESI = false, array $extraQuery = array(), $loadCall = false ) { + global $wgResourceLoaderUseESI; if ( !count( $modules ) ) { return ''; @@ -2390,7 +2521,7 @@ $templates // Recursively call us for every item $links = ''; foreach ( $modules as $name ) { - $links .= $this->makeResourceLoaderLink( $skin, $name, $only, $useESI ); + $links .= $this->makeResourceLoaderLink( $name, $only, $useESI ); } return $links; } @@ -2421,14 +2552,26 @@ $templates $links = ''; foreach ( $groups as $group => $modules ) { - $query = $baseQuery; // Special handling for user-specific groups + $user = null; if ( ( $group === 'user' || $group === 'private' ) && $this->getUser()->isLoggedIn() ) { - $query['user'] = $this->getUser()->getName(); + $user = $this->getUser()->getName(); } // Create a fake request based on the one we are about to make so modules return // correct timestamp and emptiness data + $query = ResourceLoader::makeLoaderQuery( + array(), // modules; not determined yet + $this->getLanguage()->getCode(), + $this->getSkin()->getSkinName(), + $user, + null, // version; not determined yet + ResourceLoader::inDebugMode(), + $only === ResourceLoaderModule::TYPE_COMBINED ? null : $only, + $this->isPrintable(), + $this->getRequest()->getBool( 'handheld' ), + $extraQuery + ); $context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) ); // Drop modules that know they're empty foreach ( $modules as $key => $module ) { @@ -2441,8 +2584,6 @@ $templates continue; } - $query['modules'] = ResourceLoader::makePackedModulesString( array_keys( $modules ) ); - // Inline private modules. These can't be loaded through load.php for security // reasons, see bug 34907. Note that these modules should be loaded from // getHeadScripts() before the first loader call. Otherwise other modules can't @@ -2459,6 +2600,7 @@ $templates ) ); } + $links .= "\n"; continue; } // Special handling for the user group; because users might change their stuff @@ -2466,6 +2608,7 @@ $templates // timestamp of these user-changable modules so we can ensure cache misses on change // This should NOT be done for the site group (bug 27564) because anons get that too // and we shouldn't be putting timestamps in Squid-cached HTML + $version = null; if ( $group === 'user' ) { // Get the maximum timestamp $timestamp = 1; @@ -2473,15 +2616,21 @@ $templates $timestamp = max( $timestamp, $module->getModifiedTime( $context ) ); } // Add a version parameter so cache will break when things change - $query['version'] = wfTimestamp( TS_ISO_8601_BASIC, $timestamp ); + $version = wfTimestamp( TS_ISO_8601_BASIC, $timestamp ); } - // Make queries uniform in order - ksort( $query ); - $url = wfAppendQuery( $wgLoadScript, $query ); - // Prevent the IE6 extension check from being triggered (bug 28840) - // by appending a character that's invalid in Windows extensions ('*') - $url .= '&*'; + $url = ResourceLoader::makeLoaderURL( + array_keys( $modules ), + $this->getLanguage()->getCode(), + $this->getSkin()->getSkinName(), + $user, + $version, + ResourceLoader::inDebugMode(), + $only === ResourceLoaderModule::TYPE_COMBINED ? null : $only, + $this->isPrintable(), + $this->getRequest()->getBool( 'handheld' ), + $extraQuery + ); if ( $useESI && $wgResourceLoaderUseESI ) { $esi = Xml::element( 'esi:include', array( 'src' => $url ) ); if ( $only == ResourceLoaderModule::TYPE_STYLES ) { @@ -2493,6 +2642,12 @@ $templates // Automatically select style/script elements if ( $only === ResourceLoaderModule::TYPE_STYLES ) { $link = Html::linkedStyle( $url ); + } else if ( $loadCall ) { + $link = Html::inlineScript( + ResourceLoader::makeLoaderConditionalScript( + Xml::encodeJsCall( 'mw.loader.load', array( $url, 'text/javascript', true ) ) + ) + ); } else { $link = Html::linkedScript( $url ); } @@ -2511,12 +2666,13 @@ $templates * JS stuff to put in the <head>. This is the startup module, config * vars and modules marked with position 'top' * - * @param $sk Skin object to use * @return String: HTML fragment */ - function getHeadScripts( Skin $sk ) { + function getHeadScripts() { + global $wgResourceLoaderExperimentalAsyncLoading; + // Startup - this will immediately load jquery and mediawiki modules - $scripts = $this->makeResourceLoaderLink( $sk, 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true ); + $scripts = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true ); // Load config before anything else $scripts .= Html::inlineScript( @@ -2525,10 +2681,16 @@ $templates ) ); + // Load embeddable private modules before any loader links + // This needs to be TYPE_COMBINED so these modules are properly wrapped + // in mw.loader.implement() calls and deferred until mw.user is available + $embedScripts = array( 'user.options', 'user.tokens' ); + $scripts .= $this->makeResourceLoaderLink( $embedScripts, ResourceLoaderModule::TYPE_COMBINED ); + // Script and Messages "only" requests marked for top inclusion // Messages should go first - $scripts .= $this->makeResourceLoaderLink( $sk, $this->getModuleMessages( true, 'top' ), ResourceLoaderModule::TYPE_MESSAGES ); - $scripts .= $this->makeResourceLoaderLink( $sk, $this->getModuleScripts( true, 'top' ), ResourceLoaderModule::TYPE_SCRIPTS ); + $scripts .= $this->makeResourceLoaderLink( $this->getModuleMessages( true, 'top' ), ResourceLoaderModule::TYPE_MESSAGES ); + $scripts .= $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'top' ), ResourceLoaderModule::TYPE_SCRIPTS ); // Modules requests - let the client calculate dependencies and batch requests as it likes // Only load modules that have marked themselves for loading at the top @@ -2540,25 +2702,37 @@ $templates ) ); } + + if ( $wgResourceLoaderExperimentalAsyncLoading ) { + $scripts .= $this->getScriptsForBottomQueue( true ); + } return $scripts; } /** - * JS stuff to put at the bottom of the <body>: modules marked with position 'bottom', - * legacy scripts ($this->mScripts), user preferences, site JS and user JS - * - * @param $sk Skin + * JS stuff to put at the 'bottom', which can either be the bottom of the <body> + * or the bottom of the <head> depending on $wgResourceLoaderExperimentalAsyncLoading: + * modules marked with position 'bottom', legacy scripts ($this->mScripts), + * user preferences, site JS and user JS * + * @param $inHead boolean If true, this HTML goes into the <head>, if false it goes into the <body> * @return string */ - function getBottomScripts( Skin $sk ) { + function getScriptsForBottomQueue( $inHead ) { global $wgUseSiteJs, $wgAllowUserJs; // Script and Messages "only" requests marked for bottom inclusion + // If we're in the <head>, use load() calls rather than <script src="..."> tags // Messages should go first - $scripts = $this->makeResourceLoaderLink( $sk, $this->getModuleMessages( true, 'bottom' ), ResourceLoaderModule::TYPE_MESSAGES ); - $scripts .= $this->makeResourceLoaderLink( $sk, $this->getModuleScripts( true, 'bottom' ), ResourceLoaderModule::TYPE_SCRIPTS ); + $scripts = $this->makeResourceLoaderLink( $this->getModuleMessages( true, 'bottom' ), + ResourceLoaderModule::TYPE_MESSAGES, /* $useESI = */ false, /* $extraQuery = */ array(), + /* $loadCall = */ $inHead + ); + $scripts .= $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'bottom' ), + ResourceLoaderModule::TYPE_SCRIPTS, /* $useESI = */ false, /* $extraQuery = */ array(), + /* $loadCall = */ $inHead + ); // Modules requests - let the client calculate dependencies and batch requests as it likes // Only load modules that have marked themselves for loading at the bottom @@ -2566,7 +2740,7 @@ $templates if ( $modules ) { $scripts .= Html::inlineScript( ResourceLoader::makeLoaderConditionalScript( - Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) + Xml::encodeJsCall( 'mw.loader.load', array( $modules, null, true ) ) ) ); } @@ -2574,11 +2748,13 @@ $templates // Legacy Scripts $scripts .= "\n" . $this->mScripts; - $userScripts = array( 'user.options', 'user.tokens' ); + $userScripts = array(); // Add site JS if enabled if ( $wgUseSiteJs ) { - $scripts .= $this->makeResourceLoaderLink( $sk, 'site', ResourceLoaderModule::TYPE_SCRIPTS ); + $scripts .= $this->makeResourceLoaderLink( 'site', ResourceLoaderModule::TYPE_SCRIPTS, + /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead + ); if( $this->getUser()->isLoggedIn() ){ $userScripts[] = 'user.groups'; } @@ -2586,28 +2762,50 @@ $templates // Add user JS if enabled if ( $wgAllowUserJs && $this->getUser()->isLoggedIn() ) { - $action = $this->getRequest()->getVal( 'action', 'view' ); - if( $this->getTitle() && $this->getTitle()->isJsSubpage() && $sk->userCanPreview( $action ) ) { + if( $this->getTitle() && $this->getTitle()->isJsSubpage() && $this->userCanPreview() ) { # XXX: additional security check/prompt? + // We're on a preview of a JS subpage + // Exclude this page from the user module in case it's in there (bug 26283) + $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false, + array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead + ); + // Load the previewed JS $scripts .= Html::inlineScript( "\n" . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n"; } else { - # @todo FIXME: This means that User:Me/Common.js doesn't load when previewing - # User:Me/Vector.js, and vice versa (bug26283) - $userScripts[] = 'user'; + // Include the user module normally + // We can't do $userScripts[] = 'user'; because the user module would end up + // being wrapped in a closure, so load it raw like 'site' + $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, + /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead + ); } } - $scripts .= $this->makeResourceLoaderLink( $sk, $userScripts, ResourceLoaderModule::TYPE_SCRIPTS ); + $scripts .= $this->makeResourceLoaderLink( $userScripts, ResourceLoaderModule::TYPE_COMBINED, + /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead + ); return $scripts; } /** + * JS stuff to put at the bottom of the <body> + */ + function getBottomScripts() { + global $wgResourceLoaderExperimentalAsyncLoading; + if ( !$wgResourceLoaderExperimentalAsyncLoading ) { + return $this->getScriptsForBottomQueue( false ); + } else { + return ''; + } + } + + /** * Add one or more variables to be set in mw.config in JavaScript. * * @param $key {String|Array} Key or array of key/value pars. - * @param $value {Mixed} Value of the configuration variable. + * @param $value {Mixed} [optional] Value of the configuration variable. */ - public function addJsConfigVars( $keys, $value ) { + public function addJsConfigVars( $keys, $value = null ) { if ( is_array( $keys ) ) { foreach ( $keys as $key => $value ) { $this->mJsConfigVars[$key] = $value; @@ -2626,40 +2824,72 @@ $templates * This is only public until that function is removed. You have been warned. * * Do not add things here which can be evaluated in ResourceLoaderStartupScript - * - in other words, page-indendent/site-wide variables (without state). + * - in other words, page-independent/site-wide variables (without state). * You will only be adding bloat to the html page and causing page caches to * have to be purged on configuration changes. + * @return array */ public function getJSVars() { - global $wgUseAjax, $wgEnableMWSuggest, $wgContLang; + global $wgUseAjax, $wgEnableMWSuggest; + + $latestRevID = 0; + $pageID = 0; + $canonicalName = false; # bug 21115 $title = $this->getTitle(); $ns = $title->getNamespace(); $nsname = MWNamespace::exists( $ns ) ? MWNamespace::getCanonicalName( $ns ) : $title->getNsText(); + + // Get the relevant title so that AJAX features can use the correct page name + // when making API requests from certain special pages (bug 34972). + $relevantTitle = $this->getSkin()->getRelevantTitle(); + if ( $ns == NS_SPECIAL ) { list( $canonicalName, /*...*/ ) = SpecialPageFactory::resolveAlias( $title->getDBkey() ); - } else { - $canonicalName = false; # bug 21115 + } elseif ( $this->canUseWikiPage() ) { + $wikiPage = $this->getWikiPage(); + $latestRevID = $wikiPage->getLatest(); + $pageID = $wikiPage->getId(); } + $lang = $title->getPageLanguage(); + + // Pre-process information + $separatorTransTable = $lang->separatorTransformTable(); + $separatorTransTable = $separatorTransTable ? $separatorTransTable : array(); + $compactSeparatorTransTable = array( + implode( "\t", array_keys( $separatorTransTable ) ), + implode( "\t", $separatorTransTable ), + ); + $digitTransTable = $lang->digitTransformTable(); + $digitTransTable = $digitTransTable ? $digitTransTable : array(); + $compactDigitTransTable = array( + implode( "\t", array_keys( $digitTransTable ) ), + implode( "\t", $digitTransTable ), + ); + $vars = array( 'wgCanonicalNamespace' => $nsname, 'wgCanonicalSpecialPageName' => $canonicalName, 'wgNamespaceNumber' => $title->getNamespace(), 'wgPageName' => $title->getPrefixedDBKey(), 'wgTitle' => $title->getText(), - 'wgCurRevisionId' => $title->getLatestRevID(), - 'wgArticleId' => $title->getArticleId(), + 'wgCurRevisionId' => $latestRevID, + 'wgArticleId' => $pageID, 'wgIsArticle' => $this->isArticle(), - 'wgAction' => $this->getRequest()->getText( 'action', 'view' ), + 'wgAction' => Action::getActionName( $this->getContext() ), 'wgUserName' => $this->getUser()->isAnon() ? null : $this->getUser()->getName(), 'wgUserGroups' => $this->getUser()->getEffectiveGroups(), 'wgCategories' => $this->getCategories(), 'wgBreakFrames' => $this->getFrameOptions() == 'DENY', + 'wgPageContentLanguage' => $lang->getCode(), + 'wgSeparatorTransformTable' => $compactSeparatorTransTable, + 'wgDigitTransformTable' => $compactDigitTransTable, + 'wgRelevantPageName' => $relevantTitle->getPrefixedDBKey(), ); - if ( $wgContLang->hasVariants() ) { - $vars['wgUserVariant'] = $wgContLang->getPreferredVariant(); - } + if ( $lang->hasVariants() ) { + $vars['wgUserVariant'] = $lang->getPreferredVariant(); + } foreach ( $title->getRestrictionTypes() as $type ) { $vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type ); } @@ -2669,28 +2899,55 @@ $templates if ( $title->isMainPage() ) { $vars['wgIsMainPage'] = true; } + if ( $this->mRedirectedFrom ) { + $vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBKey(); + } // Allow extensions to add their custom variables to the mw.config map. // Use the 'ResourceLoaderGetConfigVars' hook if the variable is not // page-dependant but site-wide (without state). // Alternatively, you may want to use OutputPage->addJsConfigVars() instead. - wfRunHooks( 'MakeGlobalVariablesScript', array( &$vars ) ); + wfRunHooks( 'MakeGlobalVariablesScript', array( &$vars, $this ) ); // Merge in variables from addJsConfigVars last return array_merge( $vars, $this->mJsConfigVars ); } /** - * @param $sk Skin + * To make it harder for someone to slip a user a fake + * user-JavaScript or user-CSS preview, a random token + * is associated with the login session. If it's not + * passed back with the preview request, we won't render + * the code. + * + * @return bool + */ + public function userCanPreview() { + if ( $this->getRequest()->getVal( 'action' ) != 'submit' + || !$this->getRequest()->wasPosted() + || !$this->getUser()->matchEditToken( + $this->getRequest()->getVal( 'wpEditToken' ) ) + ) { + return false; + } + if ( !$this->getTitle()->isJsSubpage() && !$this->getTitle()->isCssSubpage() ) { + return false; + } + + return !count( $this->getTitle()->getUserPermissionsErrors( 'edit', $this->getUser() ) ); + } + + /** + * @param $unused Unused * @param $addContentType bool * * @return string HTML tag links to be put in the header. */ - public function getHeadLinks( Skin $sk, $addContentType = false ) { + public function getHeadLinks( $unused = null, $addContentType = false ) { global $wgUniversalEditButton, $wgFavicon, $wgAppleTouchIcon, $wgEnableAPI, $wgSitename, $wgVersion, $wgHtml5, $wgMimeType, $wgFeed, $wgOverrideSiteFeed, $wgAdvertisedFeedTypes, - $wgDisableLangConversion, $wgCanonicalLanguageLinks, $wgContLang, + $wgDisableLangConversion, $wgCanonicalLanguageLinks, $wgRightsPage, $wgRightsUrl; $tags = array(); @@ -2762,11 +3019,12 @@ $templates } # Universal edit button - if ( $wgUniversalEditButton ) { - if ( $this->isArticleRelated() && $this->getTitle() && $this->getTitle()->quickUserCan( 'edit' ) - && ( $this->getTitle()->exists() || $this->getTitle()->quickUserCan( 'create' ) ) ) { + if ( $wgUniversalEditButton && $this->isArticleRelated() ) { + $user = $this->getUser(); + if ( $this->getTitle()->quickUserCan( 'edit', $user ) + && ( $this->getTitle()->exists() || $this->getTitle()->quickUserCan( 'create', $user ) ) ) { // Original UniversalEditButton - $msg = wfMsg( 'edit' ); + $msg = $this->msg( 'edit' )->text(); $tags[] = Html::element( 'link', array( 'rel' => 'alternate', 'type' => 'application/x-wiki', @@ -2799,7 +3057,7 @@ $templates 'rel' => 'search', 'type' => 'application/opensearchdescription+xml', 'href' => wfScript( 'opensearch_desc' ), - 'title' => wfMsgForContent( 'opensearch-desc' ), + 'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(), ) ); if ( $wgEnableAPI ) { @@ -2816,26 +3074,29 @@ $templates ) ); } - # Language variants - if ( !$wgDisableLangConversion && $wgCanonicalLanguageLinks - && $wgContLang->hasVariants() ) { - - $urlvar = $wgContLang->getURLVariant(); - if ( !$urlvar ) { - $variants = $wgContLang->getVariants(); - foreach ( $variants as $_v ) { + # Language variants + if ( !$wgDisableLangConversion && $wgCanonicalLanguageLinks ) { + $lang = $this->getTitle()->getPageLanguage(); + if ( $lang->hasVariants() ) { + + $urlvar = $lang->getURLVariant(); + + if ( !$urlvar ) { + $variants = $lang->getVariants(); + foreach ( $variants as $_v ) { + $tags[] = Html::element( 'link', array( + 'rel' => 'alternate', + 'hreflang' => $_v, + 'href' => $this->getTitle()->getLocalURL( array( 'variant' => $_v ) ) ) + ); + } + } else { $tags[] = Html::element( 'link', array( - 'rel' => 'alternate', - 'hreflang' => $_v, - 'href' => $this->getTitle()->getLocalURL( '', $_v ) ) - ); + 'rel' => 'canonical', + 'href' => $this->getTitle()->getCanonicalUrl() + ) ); } - } else { - $tags[] = Html::element( 'link', array( - 'rel' => 'canonical', - 'href' => $this->getTitle()->getCanonicalUrl() - ) ); } } @@ -2872,7 +3133,7 @@ $templates $format, $link, # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep) - wfMsg( "page-{$format}-feed", $this->getTitle()->getPrefixedText() ) + $this->msg( "page-{$format}-feed", $this->getTitle()->getPrefixedText() )->text() ); } @@ -2883,24 +3144,22 @@ $templates # like to promote instead of the RC feed (maybe like a "Recent New Articles" # or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined. # If so, use it instead. - - $rctitle = SpecialPage::getTitleFor( 'Recentchanges' ); - if ( $wgOverrideSiteFeed ) { foreach ( $wgOverrideSiteFeed as $type => $feedUrl ) { // Note, this->feedLink escapes the url. $tags[] = $this->feedLink( $type, $feedUrl, - wfMsg( "site-{$type}-feed", $wgSitename ) + $this->msg( "site-{$type}-feed", $wgSitename )->text() ); } - } elseif ( $this->getTitle()->getPrefixedText() != $rctitle->getPrefixedText() ) { + } elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) { + $rctitle = SpecialPage::getTitleFor( 'Recentchanges' ); foreach ( $wgAdvertisedFeedTypes as $format ) { $tags[] = $this->feedLink( $format, $rctitle->getLocalURL( "feed={$format}" ), - wfMsg( "site-{$format}-feed", $wgSitename ) # For grep: 'site-rss-feed', 'site-atom-feed'. + $this->msg( "site-{$format}-feed", $wgSitename )->text() # For grep: 'site-rss-feed', 'site-atom-feed'. ); } } @@ -2953,10 +3212,10 @@ $templates /** * Adds inline CSS styles * @param $style_css Mixed: inline CSS - * @param $flip False or String: Set to 'flip' to flip the CSS if needed + * @param $flip String: Set to 'flip' to flip the CSS if needed */ - public function addInlineStyle( $style_css, $flip = false ) { - if( $flip === 'flip' && $this->getLang()->isRTL() ) { + public function addInlineStyle( $style_css, $flip = 'noflip' ) { + if( $flip === 'flip' && $this->getLanguage()->isRTL() ) { # If wanted, and the interface is right-to-left, flip the CSS $style_css = CSSJanus::transform( $style_css, true, false ); } @@ -2966,17 +3225,61 @@ $templates /** * Build a set of <link>s for the stylesheets specified in the $this->styles array. * These will be applied to various media & IE conditionals. - * @param $sk Skin object * * @return string */ - public function buildCssLinks( $sk ) { - $ret = ''; + public function buildCssLinks() { + global $wgUseSiteCss, $wgAllowUserCss, $wgAllowUserCssPrefs, + $wgLang, $wgContLang; + + $this->getSkin()->setupSkinUserCss( $this ); + // Add ResourceLoader styles // Split the styles into four groups $styles = array( 'other' => array(), 'user' => array(), 'site' => array(), 'private' => array(), 'noscript' => array() ); + $otherTags = ''; // Tags to append after the normal <link> tags $resourceLoader = $this->getResourceLoader(); - foreach ( $this->getModuleStyles() as $name ) { + + $moduleStyles = $this->getModuleStyles(); + + // Per-site custom styles + if ( $wgUseSiteCss ) { + $moduleStyles[] = 'site'; + $moduleStyles[] = 'noscript'; + if( $this->getUser()->isLoggedIn() ){ + $moduleStyles[] = 'user.groups'; + } + } + + // Per-user custom styles + if ( $wgAllowUserCss ) { + if ( $this->getTitle()->isCssSubpage() && $this->userCanPreview() ) { + // We're on a preview of a CSS subpage + // Exclude this page from the user module in case it's in there (bug 26283) + $otherTags .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_STYLES, false, + array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ) + ); + + // Load the previewed CSS + // If needed, Janus it first. This is user-supplied CSS, so it's + // assumed to be right for the content language directionality. + $previewedCSS = $this->getRequest()->getText( 'wpTextbox1' ); + if ( $wgLang->getDir() !== $wgContLang->getDir() ) { + $previewedCSS = CSSJanus::transform( $previewedCSS, true, false ); + } + $otherTags .= Html::inlineStyle( $previewedCSS ); + } else { + // Load the user styles normally + $moduleStyles[] = 'user'; + } + } + + // Per-user preference styles + if ( $wgAllowUserCssPrefs ) { + $moduleStyles[] = 'user.cssprefs'; + } + + foreach ( $moduleStyles as $name ) { $module = $resourceLoader->getModule( $name ); if ( !$module ) { continue; @@ -2991,7 +3294,7 @@ $templates // dynamically added styles to override statically added styles from other modules. So the order // has to be other, dynamic, site, private, user // Add statically added styles for other modules - $ret .= $this->makeResourceLoaderLink( $sk, $styles['other'], ResourceLoaderModule::TYPE_STYLES ); + $ret = $this->makeResourceLoaderLink( $styles['other'], ResourceLoaderModule::TYPE_STYLES ); // Add normal styles added through addStyle()/addInlineStyle() here $ret .= implode( "\n", $this->buildCssLinksArray() ) . $this->mInlineStyles; // Add marker tag to mark the place where the client-side loader should inject dynamic styles @@ -3002,15 +3305,28 @@ $templates // 'private' at present only contains user.options, so put that before 'user' // Any future private modules will likely have a similar user-specific character foreach ( array( 'site', 'noscript', 'private', 'user' ) as $group ) { - $ret .= $this->makeResourceLoaderLink( $sk, $styles[$group], + $ret .= $this->makeResourceLoaderLink( $styles[$group], ResourceLoaderModule::TYPE_STYLES ); } + + // Add stuff in $otherTags (previewed user CSS if applicable) + $ret .= $otherTags; return $ret; } + /** + * @return Array + */ public function buildCssLinksArray() { $links = array(); + + // Add any extension CSS + foreach ( $this->mExtStyles as $url ) { + $this->addStyle( $url ); + } + $this->mExtStyles = array(); + foreach( $this->styles as $file => $options ) { $link = $this->styleLink( $file, $options ); if( $link ) { @@ -3030,7 +3346,7 @@ $templates */ protected function styleLink( $style, $options ) { if( isset( $options['dir'] ) ) { - if( $this->getLang()->getDir() != $options['dir'] ) { + if( $this->getLanguage()->getDir() != $options['dir'] ) { return ''; } } @@ -3118,16 +3434,11 @@ $templates * Like addWikiMsg() except the parameters are taken as an array * instead of a variable argument list. * - * $options is passed through to wfMsgExt(), see that function for details. - * * @param $name string * @param $args array - * @param $options array */ - public function addWikiMsgArray( $name, $args, $options = array() ) { - $options[] = 'parse'; - $text = wfMsgExt( $name, $options, $args ); - $this->addHTML( $text ); + public function addWikiMsgArray( $name, $args ) { + $this->addHTML( $this->msg( $name, $args )->parseAsBlock() ); } /** |