diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2012-05-03 13:01:35 +0200 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2012-05-03 13:01:35 +0200 |
commit | d9022f63880ce039446fba8364f68e656b7bf4cb (patch) | |
tree | 16b40fbf17bf7c9ee6f4ead25b16dd192378050a /includes | |
parent | 27cf83d177256813e2e802241085fce5dd0f3fb9 (diff) |
Update to MediaWiki 1.19.0
Diffstat (limited to 'includes')
457 files changed, 37248 insertions, 19306 deletions
diff --git a/includes/Action.php b/includes/Action.php index d5432b23..37c48488 100644 --- a/includes/Action.php +++ b/includes/Action.php @@ -1,5 +1,9 @@ <?php /** + * @defgroup Actions Action done on pages + */ + +/** * Actions are things which can be done to pages (edit, delete, rollback, etc). They * are distinct from Special Pages because an action must apply to exactly one page. * @@ -27,7 +31,7 @@ abstract class Action { /** * Page on which we're performing the action - * @var Article + * @var Page */ protected $page; @@ -72,34 +76,82 @@ abstract class Action { /** * Get an appropriate Action subclass for the given action * @param $action String - * @param $page Article + * @param $page Page + * @param $context IContextSource * @return Action|false|null false if the action is disabled, null * if it is not recognised */ - public final static function factory( $action, Page $page ) { + public final static function factory( $action, Page $page, IContextSource $context = null ) { $class = self::getClass( $action, $page->getActionOverrides() ); if ( $class ) { - $obj = new $class( $page ); + $obj = new $class( $page, $context ); return $obj; } return $class; } /** + * Get the action that will be executed, not necessarily the one passed + * passed through the "action" request parameter. Actions disabled in + * $wgActions will be replaced by "nosuchaction". + * + * @since 1.19 + * @param $context IContextSource + * @return string: action name + */ + public final static function getActionName( IContextSource $context ) { + global $wgActions; + + $request = $context->getRequest(); + $actionName = $request->getVal( 'action', 'view' ); + + // Check for disabled actions + if ( isset( $wgActions[$actionName] ) && $wgActions[$actionName] === false ) { + $actionName = 'nosuchaction'; + } + + // Workaround for bug #20966: inability of IE to provide an action dependent + // on which submit button is clicked. + if ( $actionName === 'historysubmit' ) { + if ( $request->getBool( 'revisiondelete' ) ) { + $actionName = 'revisiondelete'; + } else { + $actionName = 'view'; + } + } elseif ( $actionName == 'editredlink' ) { + $actionName = 'edit'; + } + + // Trying to get a WikiPage for NS_SPECIAL etc. will result + // in WikiPage::factory throwing "Invalid or virtual namespace -1 given." + // For SpecialPages et al, default to action=view. + if ( !$context->canUseWikiPage() ) { + return 'view'; + } + + $action = Action::factory( $actionName, $context->getWikiPage() ); + if ( $action instanceof Action ) { + return $action->getName(); + } + + return 'nosuchaction'; + } + + /** * Check if a given action is recognised, even if it's disabled * * @param $name String: name of an action * @return Bool */ public final static function exists( $name ) { - return self::getClass( $name ) !== null; + return self::getClass( $name, array() ) !== null; } /** * Get the IContextSource in use here * @return IContextSource */ - protected final function getContext() { + public final function getContext() { if ( $this->context instanceof IContextSource ) { return $this->context; } @@ -111,7 +163,7 @@ abstract class Action { * * @return WebRequest */ - protected final function getRequest() { + public final function getRequest() { return $this->getContext()->getRequest(); } @@ -120,7 +172,7 @@ abstract class Action { * * @return OutputPage */ - protected final function getOutput() { + public final function getOutput() { return $this->getContext()->getOutput(); } @@ -129,7 +181,7 @@ abstract class Action { * * @return User */ - protected final function getUser() { + public final function getUser() { return $this->getContext()->getUser(); } @@ -138,34 +190,58 @@ abstract class Action { * * @return Skin */ - protected final function getSkin() { + public final function getSkin() { return $this->getContext()->getSkin(); } /** * Shortcut to get the user Language being used for this instance * - * @return Skin + * @return Language + */ + public final function getLanguage() { + return $this->getContext()->getLanguage(); + } + + /** + * Shortcut to get the user Language being used for this instance + * + * @deprecated 1.19 Use getLanguage instead + * @return Language */ - protected final function getLang() { - return $this->getContext()->getLang(); + public final function getLang() { + wfDeprecated( __METHOD__, '1.19' ); + return $this->getLanguage(); } /** * Shortcut to get the Title object from the page * @return Title */ - protected final function getTitle() { + public final function getTitle() { return $this->page->getTitle(); } /** + * Get a Message object with context set + * Parameters are the same as wfMessage() + * + * @return Message object + */ + public final function msg() { + $params = func_get_args(); + return call_user_func_array( array( $this->getContext(), 'msg' ), $params ); + } + + /** * Protected constructor: use Action::factory( $action, $page ) to actually build * these things in the real world - * @param Page $page + * @param $page Page + * @param $context IContextSource */ - protected function __construct( Page $page ) { + protected function __construct( Page $page, IContextSource $context = null ) { $this->page = $page; + $this->context = $context; } /** @@ -177,8 +253,11 @@ abstract class Action { /** * Get the permission required to perform this action. Often, but not always, * the same as the action name + * @return String|null */ - public abstract function getRestriction(); + public function getRestriction() { + return null; + } /** * Checks if the given user (identified by an object) can perform this action. Can be @@ -189,18 +268,25 @@ abstract class Action { * @throws ErrorPageError */ protected function checkCanExecute( User $user ) { - if ( $this->requiresWrite() && wfReadOnly() ) { - throw new ReadOnlyError(); - } - - if ( $this->getRestriction() !== null && !$user->isAllowed( $this->getRestriction() ) ) { - throw new PermissionsError( $this->getRestriction() ); + $right = $this->getRestriction(); + if ( $right !== null ) { + $errors = $this->getTitle()->getUserPermissionsErrors( $right, $user ); + if ( count( $errors ) ) { + throw new PermissionsError( $right, $errors ); + } } if ( $this->requiresUnblock() && $user->isBlocked() ) { $block = $user->mBlock; throw new UserBlockedError( $block ); } + + // This should be checked at the end so that the user won't think the + // error is only temporary when he also don't have the rights to execute + // this action + if ( $this->requiresWrite() && wfReadOnly() ) { + throw new ReadOnlyError(); + } } /** @@ -246,7 +332,7 @@ abstract class Action { * @return String */ protected function getDescription() { - return wfMsg( strtolower( $this->getName() ) ); + return wfMsgHtml( strtolower( $this->getName() ) ); } /** @@ -259,8 +345,6 @@ abstract class Action { /** * Execute the action in a silent fashion: do not display anything or release any errors. - * @param $data Array values that would normally be in the POST request - * @param $captureErrors Bool whether to catch exceptions and just return false * @return Bool whether execution was successful */ public abstract function execute(); @@ -279,6 +363,10 @@ abstract class FormAction extends Action { * @return String HTML which will be sent to $form->addPreText() */ protected function preText() { return ''; } + + /** + * @return string + */ protected function postText() { return ''; } /** diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index b9f80855..e60ca23c 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -180,11 +180,11 @@ class AjaxResponse { $this->disable(); $this->mLastModified = $lastmod; - wfDebug( "$fname: CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false ); + wfDebug( "$fname: CACHED client: $ismodsince ; user: {$wgUser->getTouched()} ; page: $timestamp ; site $wgCacheEpoch\n", false ); return true; } else { - wfDebug( "$fname: READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false ); + wfDebug( "$fname: READY client: $ismodsince ; user: {$wgUser->getTouched()} ; page: $timestamp ; site $wgCacheEpoch\n", false ); $this->mLastModified = $lastmod; } } else { @@ -193,6 +193,11 @@ class AjaxResponse { } } + /** + * @param $mckey + * @param $touched + * @return bool + */ function loadFromMemcached( $mckey, $touched ) { global $wgMemc; @@ -216,6 +221,11 @@ class AjaxResponse { return false; } + /** + * @param $mckey + * @param $expiry int + * @return bool + */ function storeInMemcached( $mckey, $expiry = 86400 ) { global $wgMemc; diff --git a/includes/Article.php b/includes/Article.php index a0cc6a95..b07f309c 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -32,6 +32,11 @@ class Article extends Page { */ protected $mPage; + /** + * @var ParserOptions: ParserOptions object for $wgUser articles + */ + public $mParserOptions; + var $mContent; // !< var $mContentLoaded = false; // !< var $mOldId; // !< @@ -69,6 +74,10 @@ class Article extends Page { $this->mPage = $this->newPage( $title ); } + /** + * @param $title Title + * @return WikiPage + */ protected function newPage( Title $title ) { return new WikiPage( $title ); } @@ -76,6 +85,7 @@ class Article extends Page { /** * Constructor from a page id * @param $id Int article ID to load + * @return Article|null */ public static function newFromID( $id ) { $t = Title::newFromID( $id ); @@ -140,6 +150,7 @@ class Article extends Page { /** * Get the title object of the article + * * @return Title object of this page */ public function getTitle() { @@ -147,9 +158,17 @@ class Article extends Page { } /** + * Get the WikiPage object of this instance + * + * @since 1.19 + * @return WikiPage + */ + public function getPage() { + return $this->mPage; + } + + /** * Clear the object - * @todo FIXME: Shouldn't this be public? - * @private */ public function clear() { $this->mContentLoaded = false; @@ -164,7 +183,7 @@ class Article extends Page { /** * Note that getContent/loadContent do not follow redirects anymore. * If you need to fetch redirectable content easily, try - * the shortcut in Article::followRedirect() + * the shortcut in WikiPage::getRedirectTarget() * * This function has side effects! Do not use this function if you * only want the real revision text if any. @@ -191,7 +210,7 @@ class Article extends Page { return $text; } else { - $this->loadContent(); + $this->fetchContent(); wfProfileOut( __METHOD__ ); return $this->mContent; @@ -220,27 +239,39 @@ class Article extends Page { $this->mRedirectUrl = false; - $oldid = $wgRequest->getVal( 'oldid' ); + $oldid = $wgRequest->getIntOrNull( 'oldid' ); - if ( isset( $oldid ) ) { - $oldid = intval( $oldid ); - if ( $wgRequest->getVal( 'direction' ) == 'next' ) { - $nextid = $this->getTitle()->getNextRevisionID( $oldid ); - if ( $nextid ) { - $oldid = $nextid; - } else { - $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' ); - } - } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) { - $previd = $this->getTitle()->getPreviousRevisionID( $oldid ); - if ( $previd ) { - $oldid = $previd; + if ( $oldid === null ) { + return 0; + } + + if ( $oldid !== 0 ) { + # Load the given revision and check whether the page is another one. + # In that case, update this instance to reflect the change. + $this->mRevision = Revision::newFromId( $oldid ); + if ( $this->mRevision !== null ) { + // Revision title doesn't match the page title given? + if ( $this->mPage->getID() != $this->mRevision->getPage() ) { + $function = array( get_class( $this->mPage ), 'newFromID' ); + $this->mPage = call_user_func( $function, $this->mRevision->getPage() ); } } } - if ( !$oldid ) { - $oldid = 0; + if ( $wgRequest->getVal( 'direction' ) == 'next' ) { + $nextid = $this->getTitle()->getNextRevisionID( $oldid ); + if ( $nextid ) { + $oldid = $nextid; + $this->mRevision = null; + } else { + $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' ); + } + } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) { + $previd = $this->getTitle()->getPreviousRevisionID( $oldid ); + if ( $previd ) { + $oldid = $previd; + $this->mRevision = null; + } } return $oldid; @@ -248,31 +279,31 @@ class Article extends Page { /** * Load the revision (including text) into this object + * + * @deprecated in 1.19; use fetchContent() */ function loadContent() { - if ( $this->mContentLoaded ) { - return; - } - - wfProfileIn( __METHOD__ ); - - $this->fetchContent( $this->getOldID() ); - - wfProfileOut( __METHOD__ ); + wfDeprecated( __METHOD__, '1.19' ); + $this->fetchContent(); } /** * Get text of an article from database * Does *NOT* follow redirects. * - * @param $oldid Int: 0 for whatever the latest revision is * @return mixed string containing article contents, or false if null */ - function fetchContent( $oldid = 0 ) { + function fetchContent() { if ( $this->mContentLoaded ) { return $this->mContent; } + wfProfileIn( __METHOD__ ); + + $this->mContentLoaded = true; + + $oldid = $this->getOldID(); + # Pre-fill content with error message so that if something # fails we'll have something telling us what we intended. $t = $this->getTitle()->getPrefixedText(); @@ -280,43 +311,39 @@ class Article extends Page { $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ; if ( $oldid ) { - $revision = Revision::newFromId( $oldid ); - if ( !$revision ) { - wfDebug( __METHOD__ . " failed to retrieve specified revision, id $oldid\n" ); - return false; - } - // Revision title doesn't match the page title given? - if ( $this->mPage->getID() != $revision->getPage() ) { - $function = array( get_class( $this->mPage ), 'newFromID' ); - $this->mPage = call_user_func( $function, $revision->getPage() ); - if ( !$this->mPage->getId() ) { - wfDebug( __METHOD__ . " failed to get page data linked to revision id $oldid\n" ); + # $this->mRevision might already be fetched by getOldIDFromRequest() + if ( !$this->mRevision ) { + $this->mRevision = Revision::newFromId( $oldid ); + if ( !$this->mRevision ) { + wfDebug( __METHOD__ . " failed to retrieve specified revision, id $oldid\n" ); + wfProfileOut( __METHOD__ ); return false; } } } else { if ( !$this->mPage->getLatest() ) { wfDebug( __METHOD__ . " failed to find page data for title " . $this->getTitle()->getPrefixedText() . "\n" ); + wfProfileOut( __METHOD__ ); return false; } - $revision = $this->mPage->getRevision(); - if ( !$revision ) { + $this->mRevision = $this->mPage->getRevision(); + if ( !$this->mRevision ) { wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" ); + wfProfileOut( __METHOD__ ); return false; } } // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks. // We should instead work with the Revision object when we need it... - $this->mContent = $revision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed - - $this->mRevIdFetched = $revision->getId(); - $this->mContentLoaded = true; - $this->mRevision =& $revision; + $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed + $this->mRevIdFetched = $this->mRevision->getId(); wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); + wfProfileOut( __METHOD__ ); + return $this->mContent; } @@ -325,7 +352,7 @@ class Article extends Page { * @deprecated since 1.18 */ public function forUpdate() { - wfDeprecated( __METHOD__ ); + wfDeprecated( __METHOD__, '1.18' ); } /** @@ -343,6 +370,19 @@ class Article extends Page { } /** + * Get the fetched Revision object depending on request parameters or null + * on failure. + * + * @since 1.19 + * @return Revision|null + */ + public function getRevisionFetched() { + $this->fetchContent(); + + return $this->mRevision; + } + + /** * Use this to fetch the rev ID used on page views * * @return int revision ID of last article revision @@ -361,14 +401,25 @@ class Article extends Page { */ public function view() { global $wgUser, $wgOut, $wgRequest, $wgParser; - global $wgUseFileCache, $wgUseETag; + global $wgUseFileCache, $wgUseETag, $wgDebugToolbar; wfProfileIn( __METHOD__ ); # Get variables from query string + # As side effect this will load the revision and update the title + # in a revision ID is passed in the request, so this should remain + # the first call of this method even if $oldid is used way below. $oldid = $this->getOldID(); - # getOldID may want us to redirect somewhere else + # Another whitelist check in case getOldID() is altering the title + $permErrors = $this->getTitle()->getUserPermissionsErrors( 'read', $wgUser ); + if ( count( $permErrors ) ) { + wfDebug( __METHOD__ . ": denied on secondary read check\n" ); + wfProfileOut( __METHOD__ ); + throw new PermissionsError( 'read', $permErrors ); + } + + # getOldID() may as well want us to redirect somewhere else if ( $this->mRedirectUrl ) { $wgOut->redirect( $this->mRedirectUrl ); wfDebug( __METHOD__ . ": redirecting due to oldid\n" ); @@ -377,10 +428,6 @@ class Article extends Page { return; } - $wgOut->setArticleFlag( true ); - # Set page title (may be overridden by DISPLAYTITLE) - $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() ); - # If we got diff in the query, we want to see a diff page instead of the article. if ( $wgRequest->getCheck( 'diff' ) ) { wfDebug( __METHOD__ . ": showing diff page\n" ); @@ -390,22 +437,26 @@ class Article extends Page { return; } + # Set page title (may be overridden by DISPLAYTITLE) + $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() ); + + $wgOut->setArticleFlag( true ); # Allow frames by default $wgOut->allowClickjacking(); $parserCache = ParserCache::singleton(); - $parserOptions = $this->mPage->getParserOptions(); + $parserOptions = $this->getParserOptions(); # Render printable version, use printable version cache if ( $wgOut->isPrintable() ) { $parserOptions->setIsPrintable( true ); $parserOptions->setEditSection( false ); - } elseif ( $wgUseETag && !$this->getTitle()->quickUserCan( 'edit' ) ) { + } elseif ( !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit' ) ) { $parserOptions->setEditSection( false ); } # Try client and file cache - if ( $oldid === 0 && $this->mPage->checkTouched() ) { + if ( !$wgDebugToolbar && $oldid === 0 && $this->mPage->checkTouched() ) { if ( $wgUseETag ) { $wgOut->setETag( $parserCache->getETag( $this, $parserOptions ) ); } @@ -421,25 +472,21 @@ class Article extends Page { wfDebug( __METHOD__ . ": done file cache\n" ); # tell wgOut that output is taken care of $wgOut->disable(); - $this->mPage->viewUpdates(); + $this->mPage->doViewUpdates( $wgUser ); wfProfileOut( __METHOD__ ); return; } } - if ( !$wgUseETag && !$this->getTitle()->quickUserCan( 'edit' ) ) { - $parserOptions->setEditSection( false ); - } - # Should the parser cache be used? - $useParserCache = $this->useParserCache( $oldid ); + $useParserCache = $this->mPage->isParserCacheUsed( $parserOptions, $oldid ); wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); if ( $wgUser->getStubThreshold() ) { wfIncrStats( 'pcache_miss_stub' ); } - $wasRedirected = $this->showRedirectedFromHeader(); + $this->showRedirectedFromHeader(); $this->showNamespaceHeader(); # Iterate through the possible ways of constructing the output text. @@ -454,45 +501,45 @@ class Article extends Page { wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$useParserCache ) ); break; case 2: + # Early abort if the page doesn't exist + if ( !$this->mPage->exists() ) { + wfDebug( __METHOD__ . ": showing missing article\n" ); + $this->showMissingArticle(); + wfProfileOut( __METHOD__ ); + return; + } + # Try the parser cache if ( $useParserCache ) { $this->mParserOutput = $parserCache->get( $this, $parserOptions ); if ( $this->mParserOutput !== false ) { - wfDebug( __METHOD__ . ": showing parser cache contents\n" ); + if ( $oldid ) { + wfDebug( __METHOD__ . ": showing parser cache contents for current rev permalink\n" ); + $this->setOldSubtitle( $oldid ); + } else { + wfDebug( __METHOD__ . ": showing parser cache contents\n" ); + } $wgOut->addParserOutput( $this->mParserOutput ); # Ensure that UI elements requiring revision ID have # the correct version information. $wgOut->setRevisionId( $this->mPage->getLatest() ); - $outputDone = true; # Preload timestamp to avoid a DB hit - if ( isset( $this->mParserOutput->mTimestamp ) ) { - $this->mPage->setTimestamp( $this->mParserOutput->mTimestamp ); + $cachedTimestamp = $this->mParserOutput->getTimestamp(); + if ( $cachedTimestamp !== null ) { + $wgOut->setRevisionTimestamp( $cachedTimestamp ); + $this->mPage->setTimestamp( $cachedTimestamp ); } + $outputDone = true; } } break; case 3: - $text = $this->getContent(); - if ( $text === false || $this->mPage->getID() == 0 ) { - wfDebug( __METHOD__ . ": showing missing article\n" ); - $this->showMissingArticle(); - wfProfileOut( __METHOD__ ); - return; - } - - # Another whitelist check in case oldid is altering the title - if ( !$this->getTitle()->userCanRead() ) { - wfDebug( __METHOD__ . ": denied on secondary read check\n" ); - $wgOut->loginToUse(); - $wgOut->output(); - $wgOut->disable(); - wfProfileOut( __METHOD__ ); - return; - } + # This will set $this->mRevision if needed + $this->fetchContent(); # Are we looking at an old revision - if ( $oldid && !is_null( $this->mRevision ) ) { + if ( $oldid && $this->mRevision ) { $this->setOldSubtitle( $oldid ); if ( !$this->showDeletedRevisionHeader() ) { @@ -500,36 +547,29 @@ class Article extends Page { wfProfileOut( __METHOD__ ); return; } - - # If this "old" version is the current, then try the parser cache... - if ( $oldid === $this->mPage->getLatest() && $this->useParserCache( false ) ) { - $this->mParserOutput = $parserCache->get( $this, $parserOptions ); - if ( $this->mParserOutput ) { - wfDebug( __METHOD__ . ": showing parser cache for current rev permalink\n" ); - $wgOut->addParserOutput( $this->mParserOutput ); - $wgOut->setRevisionId( $this->mPage->getLatest() ); - $outputDone = true; - break; - } - } } # Ensure that UI elements requiring revision ID have # the correct version information. $wgOut->setRevisionId( $this->getRevIdFetched() ); + # Preload timestamp to avoid a DB hit + $wgOut->setRevisionTimestamp( $this->getTimestamp() ); # Pages containing custom CSS or JavaScript get special treatment if ( $this->getTitle()->isCssOrJsPage() || $this->getTitle()->isCssJsSubpage() ) { wfDebug( __METHOD__ . ": showing CSS/JS source\n" ); $this->showCssOrJsPage(); $outputDone = true; + } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $wgOut ) ) ) { + # Allow extensions do their own custom view for certain pages + $outputDone = true; } else { + $text = $this->getContent(); $rt = Title::newFromRedirectArray( $text ); if ( $rt ) { wfDebug( __METHOD__ . ": showing redirect=no page\n" ); # Viewing a redirect page (e.g. with parameter redirect=no) - # Don't append the subtitle if this was an old revision - $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) ); + $wgOut->addHTML( $this->viewRedirect( $rt ) ); # Parse just to get categories, displaytitle, etc. $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions ); $wgOut->addParserOutputNoText( $this->mParserOutput ); @@ -541,16 +581,34 @@ class Article extends Page { # Run the parse, protected by a pool counter wfDebug( __METHOD__ . ": doing uncached parse\n" ); - $key = $parserCache->getKey( $this, $parserOptions ); - $poolArticleView = new PoolWorkArticleView( $this, $key, $useParserCache, $parserOptions ); + $poolArticleView = new PoolWorkArticleView( $this, $parserOptions, + $this->getRevIdFetched(), $useParserCache, $this->getContent() ); if ( !$poolArticleView->execute() ) { + $error = $poolArticleView->getError(); + if ( $error ) { + $wgOut->clearHTML(); // for release() errors + $wgOut->enableClientCache( false ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + + $errortext = $error->getWikiText( false, 'view-pool-error' ); + $wgOut->addWikiText( '<div class="errorbox">' . $errortext . '</div>' ); + } # Connection or timeout error wfProfileOut( __METHOD__ ); return; - } else { - $outputDone = true; } + + $this->mParserOutput = $poolArticleView->getParserOutput(); + $wgOut->addParserOutput( $this->mParserOutput ); + + # Don't cache a dirty ParserOutput object + if ( $poolArticleView->getIsDirty() ) { + $wgOut->setSquidMaxage( 0 ); + $wgOut->addHTML( "<!-- parser cache is expired, sending anyway due to pool overload-->\n" ); + } + + $outputDone = true; break; # Should be unreachable, but just in case... default: @@ -558,38 +616,53 @@ class Article extends Page { } } - # Adjust the title if it was set by displaytitle, -{T|}- or language conversion - if ( $this->mParserOutput ) { - $titleText = $this->mParserOutput->getTitleText(); + # Get the ParserOutput actually *displayed* here. + # Note that $this->mParserOutput is the *current* version output. + $pOutput = ( $outputDone instanceof ParserOutput ) + ? $outputDone // object fetched by hook + : $this->mParserOutput; - if ( strval( $titleText ) !== '' ) { - $wgOut->setPageTitle( $titleText ); - } + # Adjust title for main page & pages with displaytitle + if ( $pOutput ) { + $this->adjustDisplayTitle( $pOutput ); } # For the main page, overwrite the <title> element with the con- # tents of 'pagetitle-view-mainpage' instead of the default (if # that's not empty). # This message always exists because it is in the i18n files - if ( $this->getTitle()->equals( Title::newMainPage() ) ) { + if ( $this->getTitle()->isMainPage() ) { $msg = wfMessage( 'pagetitle-view-mainpage' )->inContentLanguage(); if ( !$msg->isDisabled() ) { $wgOut->setHTMLTitle( $msg->title( $this->getTitle() )->text() ); } } - # Now that we've filled $this->mParserOutput, we know whether - # there are any __NOINDEX__ tags on the page - $policy = $this->getRobotPolicy( 'view' ); + # Check for any __NOINDEX__ tags on the page using $pOutput + $policy = $this->getRobotPolicy( 'view', $pOutput ); $wgOut->setIndexPolicy( $policy['index'] ); $wgOut->setFollowPolicy( $policy['follow'] ); $this->showViewFooter(); - $this->mPage->viewUpdates(); + $this->mPage->doViewUpdates( $wgUser ); + wfProfileOut( __METHOD__ ); } /** + * Adjust title for pages with displaytitle, -{T|}- or language conversion + * @param $pOutput ParserOutput + */ + public function adjustDisplayTitle( ParserOutput $pOutput ) { + global $wgOut; + # Adjust the title if it was set by displaytitle, -{T|}- or language conversion + $titleText = $pOutput->getTitleText(); + if ( strval( $titleText ) !== '' ) { + $wgOut->setPageTitle( $titleText ); + } + } + + /** * Show a diff page according to current request variables. For use within * Article::view() only, other callers should use the DifferenceEngine class. */ @@ -603,14 +676,14 @@ class Article extends Page { $unhide = $wgRequest->getInt( 'unhide' ) == 1; $oldid = $this->getOldID(); - $de = new DifferenceEngine( $this->getTitle(), $oldid, $diff, $rcid, $purge, $unhide ); + $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide ); // DifferenceEngine directly fetched the revision: $this->mRevIdFetched = $de->mNewid; $de->showDiffPage( $diffOnly ); if ( $diff == 0 || $diff == $this->mPage->getLatest() ) { # Run view updates for current revision only - $this->mPage->viewUpdates(); + $this->mPage->doViewUpdates( $wgUser ); } } @@ -622,10 +695,10 @@ class Article extends Page { * page views. */ protected function showCssOrJsPage() { - global $wgOut, $wgLang; + global $wgOut; - $dir = $wgLang->getDir(); - $lang = $wgLang->getCode(); + $dir = $this->getContext()->getLanguage()->getDir(); + $lang = $this->getContext()->getLanguage()->getCode(); $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>", 'clearyourcache' ); @@ -644,10 +717,11 @@ class Article extends Page { /** * Get the robot policy to be used for the current view * @param $action String the action= GET parameter + * @param $pOutput ParserOutput * @return Array the policy that should be set * TODO: actions other than 'view' */ - public function getRobotPolicy( $action ) { + public function getRobotPolicy( $action, $pOutput ) { global $wgOut, $wgArticleRobotPolicies, $wgNamespaceRobotPolicies; global $wgDefaultRobotPolicy, $wgRequest; @@ -695,12 +769,12 @@ class Article extends Page { self::formatRobotPolicy( $wgNamespaceRobotPolicies[$ns] ) ); } - if ( $this->getTitle()->canUseNoindex() && is_object( $this->mParserOutput ) && $this->mParserOutput->getIndexPolicy() ) { + if ( $this->getTitle()->canUseNoindex() && is_object( $pOutput ) && $pOutput->getIndexPolicy() ) { # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates # a final sanity check that we have really got the parser output. $policy = array_merge( $policy, - array( 'index' => $this->mParserOutput->getIndexPolicy() ) + array( 'index' => $pOutput->getIndexPolicy() ) ); } @@ -760,16 +834,14 @@ class Article extends Page { // This is an internally redirected page view. // We'll need a backlink to the source page for navigation. if ( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) { - $redir = Linker::link( + $redir = Linker::linkKnown( $this->mRedirectedFrom, null, array(), - array( 'redirect' => 'no' ), - array( 'known', 'noclasses' ) + array( 'redirect' => 'no' ) ); - $s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir ); - $wgOut->setSubtitle( $s ); + $wgOut->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) ); // Set the fragment if one was specified in the redirect if ( strval( $this->getTitle()->getFragment() ) != '' ) { @@ -782,6 +854,9 @@ class Article extends Page { 'href' => $this->getTitle()->getLocalURL() ) ); + // Tell $wgOut the user arrived at this article through a redirect + $wgOut->setRedirectedFrom( $this->mRedirectedFrom ); + return true; } } elseif ( $rdfrom ) { @@ -789,8 +864,7 @@ class Article extends Page { // If it was reported from a trusted site, supply a backlink. if ( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) { $redir = Linker::makeExternalLink( $rdfrom, $rdfrom ); - $s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir ); - $wgOut->setSubtitle( $s ); + $wgOut->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) ); return true; } @@ -817,7 +891,7 @@ class Article extends Page { * Show the footer section of an ordinary page view */ public function showViewFooter() { - global $wgOut, $wgUseTrackbacks; + global $wgOut; # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page if ( $this->getTitle()->getNamespace() == NS_USER_TALK && IP::isValid( $this->getTitle()->getText() ) ) { @@ -828,11 +902,6 @@ class Article extends Page { # chance to mark this new article as patrolled. $this->showPatrolFooter(); - # Trackbacks - if ( $wgUseTrackbacks ) { - $this->addTrackbacks(); - } - wfRunHooks( 'ArticleViewFooter', array( $this ) ); } @@ -851,7 +920,7 @@ class Article extends Page { return; } - $token = $wgUser->editToken( $rcid ); + $token = $wgUser->getEditToken( $rcid ); $wgOut->preventClickjacking(); $wgOut->addHTML( @@ -879,7 +948,7 @@ class Article extends Page { * namespace, show the default message text. To be called from Article::view(). */ public function showMissingArticle() { - global $wgOut, $wgRequest, $wgUser; + global $wgOut, $wgRequest, $wgUser, $wgSend404Code; # Show info in user (talk) namespace. Does the user exist? Is he blocked? if ( $this->getTitle()->getNamespace() == NS_USER || $this->getTitle()->getNamespace() == NS_USER_TALK ) { @@ -888,7 +957,7 @@ class Article extends Page { $user = User::newFromName( $rootPart, false /* allow IP users*/ ); $ip = User::isIP( $rootPart ); - if ( !$user->isLoggedIn() && !$ip ) { # User does not exist + if ( !($user && $user->isLoggedIn()) && !$ip ) { # User does not exist $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>", array( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) ) ); } elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked @@ -919,6 +988,18 @@ class Article extends Page { 'msgKey' => array( 'moveddeleted-notice' ) ) ); + if ( !$this->mPage->hasViewableContent() && $wgSend404Code ) { + // If there's no backing content, send a 404 Not Found + // for better machine handling of broken links. + $wgRequest->response()->header( "HTTP/1.1 404 Not Found" ); + } + + $hookResult = wfRunHooks( 'BeforeDisplayNoArticleText', array( $this ) ); + + if ( ! $hookResult ) { + return; + } + # Show error message $oldid = $this->getOldID(); if ( $oldid ) { @@ -941,12 +1022,6 @@ class Article extends Page { } $text = "<div class='noarticletext'>\n$text\n</div>"; - if ( !$this->mPage->hasViewableContent() ) { - // If there's no backing content, send a 404 Not Found - // for better machine handling of broken links. - $wgRequest->response()->header( "HTTP/1.1 404 Not Found" ); - } - $wgOut->addWikiText( $text ); } @@ -992,63 +1067,126 @@ class Article extends Page { } /** - * Execute the uncached parse for action=view + * Generate the navigation links when browsing through an article revisions + * It shows the information as: + * Revision as of \<date\>; view current revision + * \<- Previous version | Next Version -\> + * + * @param $oldid String: revision ID of this article revision */ - public function doViewParse() { - global $wgOut; + public function setOldSubtitle( $oldid = 0 ) { + global $wgLang, $wgOut, $wgUser, $wgRequest; - $oldid = $this->getOldID(); - $parserOptions = $this->mPage->getParserOptions(); + if ( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) { + return; + } - # Render printable version, use printable version cache - $parserOptions->setIsPrintable( $wgOut->isPrintable() ); + $unhide = $wgRequest->getInt( 'unhide' ) == 1; - # Don't show section-edit links on old revisions... this way lies madness. - if ( !$this->isCurrent() || $wgOut->isPrintable() || !$this->getTitle()->quickUserCan( 'edit' ) ) { - $parserOptions->setEditSection( false ); + # Cascade unhide param in links for easy deletion browsing + $extraParams = array(); + if ( $wgRequest->getVal( 'unhide' ) ) { + $extraParams['unhide'] = 1; } - $useParserCache = $this->useParserCache( $oldid ); - $this->outputWikiText( $this->getContent(), $useParserCache, $parserOptions ); - - return true; - } + $revision = Revision::newFromId( $oldid ); + $timestamp = $revision->getTimestamp(); - /** - * Try to fetch an expired entry from the parser cache. If it is present, - * output it and return true. If it is not present, output nothing and - * return false. This is used as a callback function for - * PoolCounter::executeProtected(). - * - * @return boolean - */ - public function tryDirtyCache() { - global $wgOut; - $parserCache = ParserCache::singleton(); - $options = $this->mPage->getParserOptions(); + $current = ( $oldid == $this->mPage->getLatest() ); + $td = $wgLang->timeanddate( $timestamp, true ); + $tddate = $wgLang->date( $timestamp, true ); + $tdtime = $wgLang->time( $timestamp, true ); - if ( $wgOut->isPrintable() ) { - $options->setIsPrintable( true ); - $options->setEditSection( false ); - } + # Show user links if allowed to see them. If hidden, then show them only if requested... + $userlinks = Linker::revUserTools( $revision, !$unhide ); - $output = $parserCache->getDirty( $this, $options ); + $infomsg = $current && !wfMessage( 'revision-info-current' )->isDisabled() + ? 'revision-info-current' + : 'revision-info'; - if ( $output ) { - wfDebug( __METHOD__ . ": sending dirty output\n" ); - wfDebugLog( 'dirty', "dirty output " . $parserCache->getKey( $this, $options ) . "\n" ); - $wgOut->setSquidMaxage( 0 ); - $this->mParserOutput = $output; - $wgOut->addParserOutput( $output ); - $wgOut->addHTML( "<!-- parser cache is expired, sending anyway due to pool overload-->\n" ); + $wgOut->addSubtitle( "<div id=\"mw-{$infomsg}\">" . wfMessage( $infomsg, + $td )->rawParams( $userlinks )->params( $revision->getID(), $tddate, + $tdtime, $revision->getUser() )->parse() . "</div>" ); - return true; - } else { - wfDebugLog( 'dirty', "dirty missing\n" ); - wfDebug( __METHOD__ . ": no dirty cache\n" ); + $lnk = $current + ? wfMsgHtml( 'currentrevisionlink' ) + : Linker::link( + $this->getTitle(), + wfMsgHtml( 'currentrevisionlink' ), + array(), + $extraParams, + array( 'known', 'noclasses' ) + ); + $curdiff = $current + ? wfMsgHtml( 'diff' ) + : Linker::link( + $this->getTitle(), + wfMsgHtml( 'diff' ), + array(), + array( + 'diff' => 'cur', + 'oldid' => $oldid + ) + $extraParams, + array( 'known', 'noclasses' ) + ); + $prev = $this->getTitle()->getPreviousRevisionID( $oldid ) ; + $prevlink = $prev + ? Linker::link( + $this->getTitle(), + wfMsgHtml( 'previousrevision' ), + array(), + array( + 'direction' => 'prev', + 'oldid' => $oldid + ) + $extraParams, + array( 'known', 'noclasses' ) + ) + : wfMsgHtml( 'previousrevision' ); + $prevdiff = $prev + ? Linker::link( + $this->getTitle(), + wfMsgHtml( 'diff' ), + array(), + array( + 'diff' => 'prev', + 'oldid' => $oldid + ) + $extraParams, + array( 'known', 'noclasses' ) + ) + : wfMsgHtml( 'diff' ); + $nextlink = $current + ? wfMsgHtml( 'nextrevision' ) + : Linker::link( + $this->getTitle(), + wfMsgHtml( 'nextrevision' ), + array(), + array( + 'direction' => 'next', + 'oldid' => $oldid + ) + $extraParams, + array( 'known', 'noclasses' ) + ); + $nextdiff = $current + ? wfMsgHtml( 'diff' ) + : Linker::link( + $this->getTitle(), + wfMsgHtml( 'diff' ), + array(), + array( + 'diff' => 'next', + 'oldid' => $oldid + ) + $extraParams, + array( 'known', 'noclasses' ) + ); - return false; + $cdel = Linker::getRevDeleteLink( $wgUser, $revision, $this->getTitle() ); + if ( $cdel !== '' ) { + $cdel .= ' '; } + + $wgOut->addSubtitle( "<div id=\"mw-revision-nav\">" . $cdel . + wfMsgExt( 'revision-nav', array( 'escapenoentities', 'parsemag', 'replaceafter' ), + $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>" ); } /** @@ -1060,19 +1198,24 @@ class Article extends Page { * @return string containing HMTL with redirect link */ public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) { - global $wgOut, $wgLang, $wgStylePath; + global $wgOut, $wgStylePath; if ( !is_array( $target ) ) { $target = array( $target ); } - $imageDir = $wgLang->getDir(); + $lang = $this->getTitle()->getPageLanguage(); + $imageDir = $lang->getDir(); if ( $appendSubtitle ) { $wgOut->appendSubtitle( wfMsgHtml( 'redirectpagesub' ) ); } // the loop prepends the arrow image before the link, so the first case needs to be outside + + /** + * @var $title Title + */ $title = array_shift( $target ); if ( $forceKnown ) { @@ -1082,7 +1225,7 @@ class Article extends Page { } $nextRedirect = $wgStylePath . '/common/images/nextredirect' . $imageDir . '.png'; - $alt = $wgLang->isRTL() ? '←' : '→'; + $alt = $lang->isRTL() ? '←' : '→'; // Automatically append redirect=no to each link, since most of them are redirect pages themselves. foreach ( $target as $rt ) { $link .= Html::element( 'img', array( 'src' => $nextRedirect, 'alt' => $alt ) ); @@ -1100,57 +1243,8 @@ class Article extends Page { } /** - * Builds trackback links for article display if $wgUseTrackbacks is set to true - */ - public function addTrackbacks() { - global $wgOut; - - $dbr = wfGetDB( DB_SLAVE ); - $tbs = $dbr->select( 'trackbacks', - array( 'tb_id', 'tb_title', 'tb_url', 'tb_ex', 'tb_name' ), - array( 'tb_page' => $this->mPage->getID() ) - ); - - if ( !$dbr->numRows( $tbs ) ) { - return; - } - - $wgOut->preventClickjacking(); - - $tbtext = ""; - foreach ( $tbs as $o ) { - $rmvtxt = ""; - - if ( $this->getContext()->getUser()->isAllowed( 'trackback' ) ) { - $delurl = $this->getTitle()->getFullURL( "action=deletetrackback&tbid=" . - $o->tb_id . "&token=" . urlencode( $this->getContext()->getUser()->editToken() ) ); - $rmvtxt = wfMsg( 'trackbackremove', htmlspecialchars( $delurl ) ); - } - - $tbtext .= "\n"; - $tbtext .= wfMsgNoTrans( strlen( $o->tb_ex ) ? 'trackbackexcerpt' : 'trackback', - $o->tb_title, - $o->tb_url, - $o->tb_ex, - $o->tb_name, - $rmvtxt ); - } - - $wgOut->wrapWikiMsg( "<div id='mw_trackbacks'>\n$1\n</div>\n", array( 'trackbackbox', $tbtext ) ); - } - - /** - * Removes trackback record for current article from trackbacks table - * @deprecated since 1.18 - */ - public function deletetrackback() { - return Action::factory( 'deletetrackback', $this )->show(); - } - - /** * Handle action=render */ - public function render() { global $wgOut; @@ -1159,62 +1253,6 @@ class Article extends Page { } /** - * Handle action=purge - */ - public function purge() { - return Action::factory( 'purge', $this )->show(); - } - - /** - * Mark this particular edit/page as patrolled - * @deprecated since 1.18 - */ - public function markpatrolled() { - Action::factory( 'markpatrolled', $this )->show(); - } - - /** - * User-interface handler for the "watch" action. - * Requires Request to pass a token as of 1.18. - * @deprecated since 1.18 - */ - public function watch() { - Action::factory( 'watch', $this )->show(); - } - - /** - * Add this page to $wgUser's watchlist - * - * This is safe to be called multiple times - * - * @return bool true on successful watch operation - * @deprecated since 1.18 - */ - public function doWatch() { - global $wgUser; - return WatchAction::doWatch( $this->getTitle(), $wgUser ); - } - - /** - * User interface handler for the "unwatch" action. - * Requires Request to pass a token as of 1.18. - * @deprecated since 1.18 - */ - public function unwatch() { - Action::factory( 'unwatch', $this )->show(); - } - - /** - * Stop watching a page - * @return bool true on successful unwatch - * @deprecated since 1.18 - */ - public function doUnwatch() { - global $wgUser; - return WatchAction::doUnwatch( $this->getTitle(), $wgUser ); - } - - /** * action=protect handler */ public function protect() { @@ -1230,136 +1268,69 @@ class Article extends Page { } /** - * Info about this page - * Called for ?action=info when $wgAllowPageInfo is on. - */ - public function info() { - Action::factory( 'info', $this )->show(); - } - - /** - * Overriden by ImagePage class, only present here to avoid a fatal error - * Called for ?action=revert - */ - public function revert() { - Action::factory( 'revert', $this )->show(); - } - - /** - * User interface for rollback operations - */ - public function rollback() { - Action::factory( 'rollback', $this )->show(); - } - - /** - * Output a redirect back to the article. - * This is typically used after an edit. - * - * @deprecated in 1.18; call $wgOut->redirect() directly - * @param $noRedir Boolean: add redirect=no - * @param $sectionAnchor String: section to redirect to, including "#" - * @param $extraQuery String: extra query params - */ - public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) { - wfDeprecated( __METHOD__ ); - global $wgOut; - - if ( $noRedir ) { - $query = 'redirect=no'; - if ( $extraQuery ) - $query .= "&$extraQuery"; - } else { - $query = $extraQuery; - } - - $wgOut->redirect( $this->getTitle()->getFullURL( $query ) . $sectionAnchor ); - } - - /** * UI entry point for page deletion */ public function delete() { - global $wgOut, $wgRequest; + global $wgOut, $wgRequest, $wgLang; - $confirm = $wgRequest->wasPosted() && - $this->getContext()->getUser()->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ); - - $this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' ); - $this->DeleteReason = $wgRequest->getText( 'wpReason' ); + # This code desperately needs to be totally rewritten - $reason = $this->DeleteReasonList; + $title = $this->getTitle(); + $user = $this->getContext()->getUser(); - if ( $reason != 'other' && $this->DeleteReason != '' ) { - // Entry from drop down menu + additional comment - $reason .= wfMsgForContent( 'colon-separator' ) . $this->DeleteReason; - } elseif ( $reason == 'other' ) { - $reason = $this->DeleteReason; + # Check permissions + $permission_errors = $title->getUserPermissionsErrors( 'delete', $user ); + if ( count( $permission_errors ) ) { + throw new PermissionsError( 'delete', $permission_errors ); } - # Flag to hide all contents of the archived revisions - $suppress = $wgRequest->getVal( 'wpSuppress' ) && $this->getContext()->getUser()->isAllowed( 'suppressrevision' ); - - # This code desperately needs to be totally rewritten - # Read-only check... if ( wfReadOnly() ) { - $wgOut->readOnlyPage(); - - return; + throw new ReadOnlyError; } - # Check permissions - $permission_errors = $this->getTitle()->getUserPermissionsErrors( 'delete', $this->getContext()->getUser() ); - - if ( count( $permission_errors ) > 0 ) { - $wgOut->showPermissionsErrorPage( $permission_errors ); - - return; - } - - $wgOut->setPagetitle( wfMsg( 'delete-confirm', $this->getTitle()->getPrefixedText() ) ); - # Better double-check that it hasn't been deleted yet! $dbw = wfGetDB( DB_MASTER ); - $conds = $this->getTitle()->pageCond(); + $conds = $title->pageCond(); $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); if ( $latest === false ) { - $wgOut->showFatalError( - Html::rawElement( - 'div', - array( 'class' => 'error mw-error-cannotdelete' ), - wfMsgExt( 'cannotdelete', array( 'parse' ), - wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ) - ) - ); + $wgOut->setPageTitle( wfMessage( 'cannotdelete-title', $title->getPrefixedText() ) ); + $wgOut->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>", + array( 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) ) + ); $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); LogEventsList::showLogExtract( $wgOut, 'delete', - $this->getTitle()->getPrefixedText() + $title->getPrefixedText() ); return; } - # Hack for big sites - $bigHistory = $this->mPage->isBigDeletion(); - if ( $bigHistory && !$this->getTitle()->userCan( 'bigdelete' ) ) { - global $wgLang, $wgDeleteRevisionsLimit; - - $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n", - array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) ); + $deleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' ); + $deleteReason = $wgRequest->getText( 'wpReason' ); - return; + if ( $deleteReasonList == 'other' ) { + $reason = $deleteReason; + } elseif ( $deleteReason != '' ) { + // Entry from drop down menu + additional comment + $reason = $deleteReasonList . wfMsgForContent( 'colon-separator' ) . $deleteReason; + } else { + $reason = $deleteReasonList; } - if ( $confirm ) { + if ( $wgRequest->wasPosted() && $user->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), + array( 'delete', $this->getTitle()->getPrefixedText() ) ) ) + { + # Flag to hide all contents of the archived revisions + $suppress = $wgRequest->getVal( 'wpSuppress' ) && $user->isAllowed( 'suppressrevision' ); + $this->doDelete( $reason, $suppress ); - if ( $wgRequest->getCheck( 'wpWatch' ) && $this->getContext()->getUser()->isLoggedIn() ) { + if ( $wgRequest->getCheck( 'wpWatch' ) && $user->isLoggedIn() ) { $this->doWatch(); - } elseif ( $this->getTitle()->userIsWatching() ) { + } elseif ( $title->userIsWatching() ) { $this->doUnwatch(); } @@ -1373,21 +1344,19 @@ class Article extends Page { } // If the page has a history, insert a warning - if ( $hasHistory && !$confirm ) { - global $wgLang; - - $revisions = $this->mPage->estimateRevisionCount(); + if ( $hasHistory ) { + $revisions = $this->mTitle->estimateRevisionCount(); // @todo FIXME: i18n issue/patchwork message $wgOut->addHTML( '<strong class="mw-delete-warning-revisions">' . wfMsgExt( 'historywarning', array( 'parseinline' ), $wgLang->formatNum( $revisions ) ) . - wfMsgHtml( 'word-separator' ) . Linker::link( $this->getTitle(), + wfMsgHtml( 'word-separator' ) . Linker::link( $title, wfMsgHtml( 'history' ), array( 'rel' => 'archives' ), array( 'action' => 'history' ) ) . '</strong>' ); - if ( $bigHistory ) { + if ( $this->mTitle->isBigDeletion() ) { global $wgDeleteRevisionsLimit; $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n", array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) ); @@ -1407,14 +1376,16 @@ class Article extends Page { wfDebug( "Article::confirmDelete\n" ); - $deleteBackLink = Linker::linkKnown( $this->getTitle() ); - $wgOut->setSubtitle( wfMsgHtml( 'delete-backlink', $deleteBackLink ) ); + $wgOut->setPageTitle( wfMessage( 'delete-confirm', $this->getTitle()->getPrefixedText() ) ); + $wgOut->addBacklinkSubtitle( $this->getTitle() ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addWikiMsg( 'confirmdeletetext' ); wfRunHooks( 'ArticleConfirmDelete', array( $this, $wgOut, &$reason ) ); - if ( $this->getContext()->getUser()->isAllowed( 'suppressrevision' ) ) { + $user = $this->getContext()->getUser(); + + if ( $user->isAllowed( 'suppressrevision' ) ) { $suppress = "<tr id=\"wpDeleteSuppressRow\"> <td></td> <td class='mw-input'><strong>" . @@ -1425,7 +1396,7 @@ class Article extends Page { } else { $suppress = ''; } - $checkWatch = $this->getContext()->getUser()->getBoolOption( 'watchdeletion' ) || $this->getTitle()->userIsWatching(); + $checkWatch = $user->getBoolOption( 'watchdeletion' ) || $this->getTitle()->userIsWatching(); $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) . @@ -1458,7 +1429,7 @@ class Article extends Page { </tr>"; # Disallow watching if user is not logged in - if ( $this->getContext()->getUser()->isLoggedIn() ) { + if ( $user->isLoggedIn() ) { $form .= " <tr> <td></td> @@ -1480,10 +1451,10 @@ class Article extends Page { </tr>" . Xml::closeElement( 'table' ) . Xml::closeElement( 'fieldset' ) . - Html::hidden( 'wpEditToken', $this->getContext()->getUser()->editToken() ) . + Html::hidden( 'wpEditToken', $user->getEditToken( array( 'delete', $this->getTitle()->getPrefixedText() ) ) ) . Xml::closeElement( 'form' ); - if ( $this->getContext()->getUser()->isAllowed( 'editinterface' ) ) { + if ( $user->isAllowed( 'editinterface' ) ) { $title = Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' ); $link = Linker::link( $title, @@ -1503,17 +1474,17 @@ class Article extends Page { /** * Perform a deletion and output success or failure messages + * @param $reason + * @param $suppress bool */ public function doDelete( $reason, $suppress = false ) { global $wgOut; - $id = $this->getTitle()->getArticleID( Title::GAID_FOR_UPDATE ); - $error = ''; - if ( $this->mPage->doDeleteArticle( $reason, $suppress, $id, $error ) ) { + if ( $this->mPage->doDeleteArticle( $reason, $suppress, 0, true, $error ) ) { $deleted = $this->getTitle()->getPrefixedText(); - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); + $wgOut->setPageTitle( wfMessage( 'actioncomplete' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]'; @@ -1521,16 +1492,11 @@ class Article extends Page { $wgOut->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink ); $wgOut->returnToMain( false ); } else { + $wgOut->setPageTitle( wfMessage( 'cannotdelete-title', $this->getTitle()->getPrefixedText() ) ); if ( $error == '' ) { - $wgOut->showFatalError( - Html::rawElement( - 'div', - array( 'class' => 'error mw-error-cannotdelete' ), - wfMsgExt( 'cannotdelete', array( 'parse' ), - wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ) - ) + $wgOut->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>", + array( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ) ); - $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); LogEventsList::showLogExtract( @@ -1539,156 +1505,11 @@ class Article extends Page { $this->getTitle()->getPrefixedText() ); } else { - $wgOut->showFatalError( $error ); + $wgOut->addHTML( $error ); } } } - /** - * Generate the navigation links when browsing through an article revisions - * It shows the information as: - * Revision as of \<date\>; view current revision - * \<- Previous version | Next Version -\> - * - * @param $oldid String: revision ID of this article revision - */ - public function setOldSubtitle( $oldid = 0 ) { - global $wgLang, $wgOut, $wgUser, $wgRequest; - - if ( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) { - return; - } - - $unhide = $wgRequest->getInt( 'unhide' ) == 1; - - # Cascade unhide param in links for easy deletion browsing - $extraParams = array(); - if ( $wgRequest->getVal( 'unhide' ) ) { - $extraParams['unhide'] = 1; - } - - $revision = Revision::newFromId( $oldid ); - $timestamp = $revision->getTimestamp(); - - $current = ( $oldid == $this->mPage->getLatest() ); - $td = $wgLang->timeanddate( $timestamp, true ); - $tddate = $wgLang->date( $timestamp, true ); - $tdtime = $wgLang->time( $timestamp, true ); - - $lnk = $current - ? wfMsgHtml( 'currentrevisionlink' ) - : Linker::link( - $this->getTitle(), - wfMsgHtml( 'currentrevisionlink' ), - array(), - $extraParams, - array( 'known', 'noclasses' ) - ); - $curdiff = $current - ? wfMsgHtml( 'diff' ) - : Linker::link( - $this->getTitle(), - wfMsgHtml( 'diff' ), - array(), - array( - 'diff' => 'cur', - 'oldid' => $oldid - ) + $extraParams, - array( 'known', 'noclasses' ) - ); - $prev = $this->getTitle()->getPreviousRevisionID( $oldid ) ; - $prevlink = $prev - ? Linker::link( - $this->getTitle(), - wfMsgHtml( 'previousrevision' ), - array(), - array( - 'direction' => 'prev', - 'oldid' => $oldid - ) + $extraParams, - array( 'known', 'noclasses' ) - ) - : wfMsgHtml( 'previousrevision' ); - $prevdiff = $prev - ? Linker::link( - $this->getTitle(), - wfMsgHtml( 'diff' ), - array(), - array( - 'diff' => 'prev', - 'oldid' => $oldid - ) + $extraParams, - array( 'known', 'noclasses' ) - ) - : wfMsgHtml( 'diff' ); - $nextlink = $current - ? wfMsgHtml( 'nextrevision' ) - : Linker::link( - $this->getTitle(), - wfMsgHtml( 'nextrevision' ), - array(), - array( - 'direction' => 'next', - 'oldid' => $oldid - ) + $extraParams, - array( 'known', 'noclasses' ) - ); - $nextdiff = $current - ? wfMsgHtml( 'diff' ) - : Linker::link( - $this->getTitle(), - wfMsgHtml( 'diff' ), - array(), - array( - 'diff' => 'next', - 'oldid' => $oldid - ) + $extraParams, - array( 'known', 'noclasses' ) - ); - - $cdel = ''; - - // User can delete revisions or view deleted revisions... - $canHide = $wgUser->isAllowed( 'deleterevision' ); - if ( $canHide || ( $revision->getVisibility() && $wgUser->isAllowed( 'deletedhistory' ) ) ) { - if ( !$revision->userCan( Revision::DELETED_RESTRICTED ) ) { - $cdel = Linker::revDeleteLinkDisabled( $canHide ); // rev was hidden from Sysops - } else { - $query = array( - 'type' => 'revision', - 'target' => $this->getTitle()->getPrefixedDbkey(), - 'ids' => $oldid - ); - $cdel = Linker::revDeleteLink( $query, $revision->isDeleted( File::DELETED_RESTRICTED ), $canHide ); - } - $cdel .= ' '; - } - - # Show user links if allowed to see them. If hidden, then show them only if requested... - $userlinks = Linker::revUserTools( $revision, !$unhide ); - - $infomsg = $current && !wfMessage( 'revision-info-current' )->isDisabled() - ? 'revision-info-current' - : 'revision-info'; - - $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . - wfMsgExt( - $infomsg, - array( 'parseinline', 'replaceafter' ), - $td, - $userlinks, - $revision->getID(), - $tddate, - $tdtime, - $revision->getUser() - ) . - "</div>\n" . - "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsgExt( 'revision-nav', array( 'escapenoentities', 'parsemag', 'replaceafter' ), - $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t"; - - $wgOut->setSubtitle( $r ); - } - /* Caching functions */ /** @@ -1708,10 +1529,10 @@ class Article extends Page { $called = true; if ( $this->isFileCacheable() ) { - $cache = new HTMLFileCache( $this->getTitle() ); - if ( $cache->isFileCacheGood( $this->mPage->getTouched() ) ) { + $cache = HTMLFileCache::newFromTitle( $this->getTitle(), 'view' ); + if ( $cache->isCacheGood( $this->mPage->getTouched() ) ) { wfDebug( "Article::tryFileCache(): about to load file\n" ); - $cache->loadFromFileCache(); + $cache->loadFromFileCache( $this->getContext() ); return true; } else { wfDebug( "Article::tryFileCache(): starting buffer\n" ); @@ -1731,8 +1552,9 @@ class Article extends Page { public function isFileCacheable() { $cacheable = false; - if ( HTMLFileCache::useFileCache() ) { - $cacheable = $this->mPage->getID() && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect(); + if ( HTMLFileCache::useFileCache( $this->getContext() ) ) { + $cacheable = $this->mPage->getID() + && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect(); // Extension may have reason to disable file caching on some pages. if ( $cacheable ) { $cacheable = wfRunHooks( 'IsFileCacheable', array( &$this ) ); @@ -1745,25 +1567,6 @@ class Article extends Page { /**#@-*/ /** - * Add the primary page-view wikitext to the output buffer - * Saves the text into the parser cache if possible. - * Updates templatelinks if it is out of date. - * - * @param $text String - * @param $cache Boolean - * @param $parserOptions mixed ParserOptions object, or boolean false - */ - public function outputWikiText( $text, $cache = true, $parserOptions = false ) { - global $wgOut; - - $this->mParserOutput = $this->getOutputFromWikitext( $text, $cache, $parserOptions ); - - $this->doCascadeProtectionUpdates( $this->mParserOutput ); - - $wgOut->addParserOutput( $this->mParserOutput ); - } - - /** * Lightweight method to get the parser output for a page, checking the parser cache * and so on. Doesn't consider most of the stuff that WikiPage::view is forced to * consider, so it's not appropriate to use there. @@ -1775,91 +1578,25 @@ class Article extends Page { * @return ParserOutput or false if the given revsion ID is not found */ public function getParserOutput( $oldid = null, User $user = null ) { - global $wgEnableParserCache, $wgUser; - $user = is_null( $user ) ? $wgUser : $user; - - wfProfileIn( __METHOD__ ); - // Should the parser cache be used? - $useParserCache = $wgEnableParserCache && - $user->getStubThreshold() == 0 && - $this->mPage->exists() && - $oldid === null; - - wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); - - if ( $user->getStubThreshold() ) { - wfIncrStats( 'pcache_miss_stub' ); - } - - if ( $useParserCache ) { - $parserOutput = ParserCache::singleton()->get( $this, $this->mPage->getParserOptions() ); - if ( $parserOutput !== false ) { - wfProfileOut( __METHOD__ ); - return $parserOutput; - } - } + global $wgUser; - // Cache miss; parse and output it. - if ( $oldid === null ) { - $text = $this->mPage->getRawText(); - } else { - $rev = Revision::newFromTitle( $this->getTitle(), $oldid ); - if ( $rev === null ) { - wfProfileOut( __METHOD__ ); - return false; - } - $text = $rev->getText(); - } + $user = is_null( $user ) ? $wgUser : $user; + $parserOptions = $this->mPage->makeParserOptions( $user ); - wfProfileOut( __METHOD__ ); - return $this->getOutputFromWikitext( $text, $useParserCache ); + return $this->mPage->getParserOutput( $parserOptions, $oldid ); } /** - * This does all the heavy lifting for outputWikitext, except it returns the parser - * output instead of sending it straight to $wgOut. Makes things nice and simple for, - * say, embedding thread pages within a discussion system (LiquidThreads) - * - * @param $text string - * @param $cache boolean - * @param $parserOptions parsing options, defaults to false - * @return ParserOutput + * Get parser options suitable for rendering the primary article wikitext + * @return ParserOptions|false */ - public function getOutputFromWikitext( $text, $cache = true, $parserOptions = false ) { - global $wgParser, $wgEnableParserCache, $wgUseFileCache; - - if ( !$parserOptions ) { - $parserOptions = $this->mPage->getParserOptions(); - } - - $time = - wfTime(); - $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), - $parserOptions, true, true, $this->getRevIdFetched() ); - $time += wfTime(); - - # Timing hack - if ( $time > 3 ) { - wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, - $this->getTitle()->getPrefixedDBkey() ) ); - } - - if ( $wgEnableParserCache && $cache && $this->mParserOutput->isCacheable() ) { - $parserCache = ParserCache::singleton(); - $parserCache->save( $this->mParserOutput, $this, $parserOptions ); - } - - // Make sure file cache is not used on uncacheable content. - // Output that has magic words in it can still use the parser cache - // (if enabled), though it will generally expire sooner. - if ( !$this->mParserOutput->isCacheable() || $this->mParserOutput->containsOldMagic() ) { - $wgUseFileCache = false; - } - - if ( $this->isCurrent() ) { - $this->mPage->doCascadeProtectionUpdates( $this->mParserOutput ); + public function getParserOptions() { + global $wgUser; + if ( !$this->mParserOptions ) { + $this->mParserOptions = $this->mPage->makeParserOptions( $wgUser ); } - - return $this->mParserOutput; + // Clone to allow modifications of the return value without affecting cache + return clone $this->mParserOptions; } /** @@ -1888,6 +1625,119 @@ class Article extends Page { } /** + * Info about this page + * @deprecated since 1.19 + */ + public function info() { + wfDeprecated( __METHOD__, '1.19' ); + Action::factory( 'info', $this )->show(); + } + + /** + * Mark this particular edit/page as patrolled + * @deprecated since 1.18 + */ + public function markpatrolled() { + wfDeprecated( __METHOD__, '1.18' ); + Action::factory( 'markpatrolled', $this )->show(); + } + + /** + * Handle action=purge + * @deprecated since 1.19 + */ + public function purge() { + return Action::factory( 'purge', $this )->show(); + } + + /** + * Handle action=revert + * @deprecated since 1.19 + */ + public function revert() { + wfDeprecated( __METHOD__, '1.19' ); + Action::factory( 'revert', $this )->show(); + } + + /** + * Handle action=rollback + * @deprecated since 1.19 + */ + public function rollback() { + wfDeprecated( __METHOD__, '1.19' ); + Action::factory( 'rollback', $this )->show(); + } + + /** + * User-interface handler for the "watch" action. + * Requires Request to pass a token as of 1.18. + * @deprecated since 1.18 + */ + public function watch() { + wfDeprecated( __METHOD__, '1.18' ); + Action::factory( 'watch', $this )->show(); + } + + /** + * Add this page to $wgUser's watchlist + * + * This is safe to be called multiple times + * + * @return bool true on successful watch operation + * @deprecated since 1.18 + */ + public function doWatch() { + global $wgUser; + wfDeprecated( __METHOD__, '1.18' ); + return WatchAction::doWatch( $this->getTitle(), $wgUser ); + } + + /** + * User interface handler for the "unwatch" action. + * Requires Request to pass a token as of 1.18. + * @deprecated since 1.18 + */ + public function unwatch() { + wfDeprecated( __METHOD__, '1.18' ); + Action::factory( 'unwatch', $this )->show(); + } + + /** + * Stop watching a page + * @return bool true on successful unwatch + * @deprecated since 1.18 + */ + public function doUnwatch() { + global $wgUser; + wfDeprecated( __METHOD__, '1.18' ); + return WatchAction::doUnwatch( $this->getTitle(), $wgUser ); + } + + /** + * Output a redirect back to the article. + * This is typically used after an edit. + * + * @deprecated in 1.18; call $wgOut->redirect() directly + * @param $noRedir Boolean: add redirect=no + * @param $sectionAnchor String: section to redirect to, including "#" + * @param $extraQuery String: extra query params + */ + public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) { + wfDeprecated( __METHOD__, '1.18' ); + global $wgOut; + + if ( $noRedir ) { + $query = 'redirect=no'; + if ( $extraQuery ) + $query .= "&$extraQuery"; + } else { + $query = $extraQuery; + } + + $wgOut->redirect( $this->getTitle()->getFullURL( $query ) . $sectionAnchor ); + } + + /** * Use PHP's magic __get handler to handle accessing of * raw WikiPage fields for backwards compatibility. * @@ -1898,7 +1748,7 @@ class Article extends Page { #wfWarn( "Access to raw $fname field " . __CLASS__ ); return $this->mPage->$fname; } - trigger_error( 'Inaccessible property via __get(): ' . $fname, E_USER_NOTICE ); + trigger_error( 'Inaccessible property via __get(): ' . $fname, E_USER_NOTICE ); } /** @@ -1907,7 +1757,6 @@ class Article extends Page { * * @param $fname String Field name * @param $fvalue mixed New value - * @param $args Array Arguments to the method */ public function __set( $fname, $fvalue ) { if ( property_exists( $this->mPage, $fname ) ) { @@ -1917,7 +1766,7 @@ class Article extends Page { } elseif ( !in_array( $fname, array( 'mContext', 'mPage' ) ) ) { $this->mPage->$fname = $fvalue; } else { - trigger_error( 'Inaccessible property via __get(): ' . $fname, E_USER_NOTICE ); + trigger_error( 'Inaccessible property via __set(): ' . $fname, E_USER_NOTICE ); } } @@ -1933,109 +1782,121 @@ class Article extends Page { #wfWarn( "Call to " . __CLASS__ . "::$fname; please use WikiPage instead" ); return call_user_func_array( array( $this->mPage, $fname ), $args ); } - trigger_error( 'Inaccessible function via __call(): ' . $fname, E_USER_ERROR ); + trigger_error( 'Inaccessible function via __call(): ' . $fname, E_USER_ERROR ); } // ****** B/C functions to work-around PHP silliness with __call and references ****** // + + /** + * @param $limit array + * @param $expiry array + * @param $cascade bool + * @param $reason string + * @param $user User + * @return Status + */ + public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user ) { + return $this->mPage->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user ); + } + + /** + * @param $limit array + * @param $reason string + * @param $cascade int + * @param $expiry array + * @return bool + */ public function updateRestrictions( $limit = array(), $reason = '', &$cascade = 0, $expiry = array() ) { return $this->mPage->updateRestrictions( $limit, $reason, $cascade, $expiry ); } + /** + * @param $reason string + * @param $suppress bool + * @param $id int + * @param $commit bool + * @param $error string + * @return bool + */ public function doDeleteArticle( $reason, $suppress = false, $id = 0, $commit = true, &$error = '' ) { return $this->mPage->doDeleteArticle( $reason, $suppress, $id, $commit, $error ); } + /** + * @param $fromP + * @param $summary + * @param $token + * @param $bot + * @param $resultDetails + * @param $user User + * @return array + */ public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails, User $user = null ) { global $wgUser; $user = is_null( $user ) ? $wgUser : $user; return $this->mPage->doRollback( $fromP, $summary, $token, $bot, $resultDetails, $user ); } + /** + * @param $fromP + * @param $summary + * @param $bot + * @param $resultDetails + * @param $guser User + * @return array + */ public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser = null ) { global $wgUser; $guser = is_null( $guser ) ? $wgUser : $guser; return $this->mPage->commitRollback( $fromP, $summary, $bot, $resultDetails, $guser ); } + /** + * @param $hasHistory bool + * @return mixed + */ public function generateReason( &$hasHistory ) { return $this->mPage->getAutoDeleteReason( $hasHistory ); } // ****** B/C functions for static methods ( __callStatic is PHP>=5.3 ) ****** // + + /** + * @return array + */ public static function selectFields() { return WikiPage::selectFields(); } + /** + * @param $title Title + */ public static function onArticleCreate( $title ) { - return WikiPage::onArticleCreate( $title ); + WikiPage::onArticleCreate( $title ); } + /** + * @param $title Title + */ public static function onArticleDelete( $title ) { - return WikiPage::onArticleDelete( $title ); - } - - public static function onArticleEdit( $title ) { - return WikiPage::onArticleEdit( $title ); + WikiPage::onArticleDelete( $title ); } - public static function getAutosummary( $oldtext, $newtext, $flags ) { - return WikiPage::getAutosummary( $oldtext, $newtext, $flags ); - } - // ****** -} - -class PoolWorkArticleView extends PoolCounterWork { - /** - * @var Article + * @param $title Title */ - private $mArticle; - - function __construct( $article, $key, $useParserCache, $parserOptions ) { - parent::__construct( 'ArticleView', $key ); - $this->mArticle = $article; - $this->cacheable = $useParserCache; - $this->parserOptions = $parserOptions; - } - - function doWork() { - return $this->mArticle->doViewParse(); - } - - function getCachedWork() { - global $wgOut; - - $parserCache = ParserCache::singleton(); - $this->mArticle->mParserOutput = $parserCache->get( $this->mArticle, $this->parserOptions ); - - if ( $this->mArticle->mParserOutput !== false ) { - wfDebug( __METHOD__ . ": showing contents parsed by someone else\n" ); - $wgOut->addParserOutput( $this->mArticle->mParserOutput ); - # Ensure that UI elements requiring revision ID have - # the correct version information. - $wgOut->setRevisionId( $this->mArticle->getLatest() ); - return true; - } - return false; - } - - function fallback() { - return $this->mArticle->tryDirtyCache(); + public static function onArticleEdit( $title ) { + WikiPage::onArticleEdit( $title ); } /** - * @param $status Status + * @param $oldtext + * @param $newtext + * @param $flags + * @return string */ - function error( $status ) { - global $wgOut; - - $wgOut->clearHTML(); // for release() errors - $wgOut->enableClientCache( false ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - - $errortext = $status->getWikiText( false, 'view-pool-error' ); - $wgOut->addWikiText( '<div class="errorbox">' . $errortext . '</div>' ); - - return false; + public static function getAutosummary( $oldtext, $newtext, $flags ) { + return WikiPage::getAutosummary( $oldtext, $newtext, $flags ); } + // ****** } diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php index eebb52d6..2fdba797 100644 --- a/includes/AuthPlugin.php +++ b/includes/AuthPlugin.php @@ -67,7 +67,7 @@ class AuthPlugin { * Modify options in the login template. * * @param $template UserLoginTemplate object. - * @param $type String 'signup' or 'login'. + * @param $type String 'signup' or 'login'. Added in 1.16. */ public function modifyUITemplate( &$template, &$type ) { # Override this! diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index d8263ba9..93fac45f 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -24,12 +24,13 @@ $wgAutoloadLocalClasses = array( 'AuthPluginUser' => 'includes/AuthPlugin.php', 'Autopromote' => 'includes/Autopromote.php', 'BacklinkCache' => 'includes/BacklinkCache.php', + 'BadTitleError' => 'includes/Exception.php', 'BaseTemplate' => 'includes/SkinTemplate.php', 'Block' => 'includes/Block.php', 'Category' => 'includes/Category.php', 'Categoryfinder' => 'includes/Categoryfinder.php', 'CategoryPage' => 'includes/CategoryPage.php', - 'CategoryViewer' => 'includes/CategoryPage.php', + 'CategoryViewer' => 'includes/CategoryViewer.php', 'CdbFunctions' => 'includes/Cdb_PHP.php', 'CdbReader' => 'includes/Cdb.php', 'CdbReader_DBA' => 'includes/Cdb.php', @@ -46,11 +47,15 @@ $wgAutoloadLocalClasses = array( 'ConfEditor' => 'includes/ConfEditor.php', 'ConfEditorParseError' => 'includes/ConfEditor.php', 'ConfEditorToken' => 'includes/ConfEditor.php', - 'ContextSource' => 'includes/RequestContext.php', 'Cookie' => 'includes/Cookie.php', 'CookieJar' => 'includes/Cookie.php', + 'MWCryptRand' => 'includes/CryptRand.php', + 'CurlHttpRequest' => 'includes/HttpFunctions.php', + 'DeferrableUpdate' => 'includes/DeferredUpdates.php', + 'DeferredUpdates' => 'includes/DeferredUpdates.php', + 'DerivativeRequest' => 'includes/WebRequest.php', 'DiffHistoryBlob' => 'includes/HistoryBlob.php', - 'DjVuImage' => 'includes/DjVuImage.php', + 'DoubleReplacer' => 'includes/StringUtils.php', 'DummyLinker' => 'includes/Linker.php', 'Dump7ZipOutput' => 'includes/Export.php', @@ -87,13 +92,10 @@ $wgAutoloadLocalClasses = array( 'FormAction' => 'includes/Action.php', 'FormOptions' => 'includes/FormOptions.php', 'FormSpecialPage' => 'includes/SpecialPage.php', - 'GenderCache' => 'includes/GenderCache.php', 'HashtableReplacer' => 'includes/StringUtils.php', 'HistoryBlob' => 'includes/HistoryBlob.php', 'HistoryBlobCurStub' => 'includes/HistoryBlob.php', 'HistoryBlobStub' => 'includes/HistoryBlob.php', - 'HistoryPage' => 'includes/HistoryPage.php', - 'HistoryPager' => 'includes/HistoryPage.php', 'Hooks' => 'includes/Hooks.php', 'Html' => 'includes/Html.php', 'HTMLCheckField' => 'includes/HTMLForm.php', @@ -113,8 +115,8 @@ $wgAutoloadLocalClasses = array( 'HTMLTextAreaField' => 'includes/HTMLForm.php', 'HTMLTextField' => 'includes/HTMLForm.php', 'Http' => 'includes/HttpFunctions.php', + 'HttpError' => 'includes/Exception.php', 'HttpRequest' => 'includes/HttpFunctions.old.php', - 'IContextSource' => 'includes/RequestContext.php', 'IcuCollation' => 'includes/Collation.php', 'IdentityCollation' => 'includes/Collation.php', 'ImageGallery' => 'includes/ImageGallery.php', @@ -128,6 +130,7 @@ $wgAutoloadLocalClasses = array( 'IndexPager' => 'includes/Pager.php', 'Interwiki' => 'includes/interwiki/Interwiki.php', 'IP' => 'includes/IP.php', + 'LCStore_Accel' => 'includes/LocalisationCache.php', 'LCStore_CDB' => 'includes/LocalisationCache.php', 'LCStore_DB' => 'includes/LocalisationCache.php', 'LCStore_Null' => 'includes/LocalisationCache.php', @@ -139,9 +142,6 @@ $wgAutoloadLocalClasses = array( 'LinksUpdate' => 'includes/LinksUpdate.php', 'LocalisationCache' => 'includes/LocalisationCache.php', 'LocalisationCache_BulkLoad' => 'includes/LocalisationCache.php', - 'LogEventsList' => 'includes/LogEventsList.php', - 'LogPage' => 'includes/LogPage.php', - 'LogPager' => 'includes/LogEventsList.php', 'MagicWord' => 'includes/MagicWord.php', 'MagicWordArray' => 'includes/MagicWord.php', 'MailAddress' => 'includes/UserMailer.php', @@ -150,7 +150,6 @@ $wgAutoloadLocalClasses = array( 'Message' => 'includes/Message.php', 'MessageBlobStore' => 'includes/MessageBlobStore.php', 'MimeMagic' => 'includes/MimeMagic.php', - 'MWCryptRand' => 'includes/CryptRand.php', 'MWException' => 'includes/Exception.php', 'MWExceptionHandler' => 'includes/Exception.php', 'MWFunction' => 'includes/MWFunction.php', @@ -160,24 +159,23 @@ $wgAutoloadLocalClasses = array( 'OldChangesList' => 'includes/ChangesList.php', 'OutputPage' => 'includes/OutputPage.php', 'Page' => 'includes/WikiPage.php', - 'PageHistory' => 'includes/HistoryPage.php', - 'PageHistoryPager' => 'includes/HistoryPage.php', 'PageQueryPage' => 'includes/PageQueryPage.php', 'Pager' => 'includes/Pager.php', 'PasswordError' => 'includes/User.php', - 'PatrolLog' => 'includes/PatrolLog.php', + 'PathRouter' => 'includes/PathRouter.php', + 'PathRouterPatternReplacer' => 'includes/PathRouter.php', 'PermissionsError' => 'includes/Exception.php', 'PhpHttpRequest' => 'includes/HttpFunctions.php', 'PoolCounter' => 'includes/PoolCounter.php', 'PoolCounter_Stub' => 'includes/PoolCounter.php', 'PoolCounterWork' => 'includes/PoolCounter.php', + 'PoolWorkArticleView' => 'includes/WikiPage.php', 'Preferences' => 'includes/Preferences.php', 'PreferencesForm' => 'includes/Preferences.php', 'PrefixSearch' => 'includes/PrefixSearch.php', 'ProtectionForm' => 'includes/ProtectionForm.php', 'QueryPage' => 'includes/QueryPage.php', 'QuickTemplate' => 'includes/SkinTemplate.php', - 'RawPage' => 'includes/RawPage.php', 'RCCacheEntry' => 'includes/ChangesList.php', 'RdfMetaData' => 'includes/Metadata.php', 'ReadOnlyError' => 'includes/Exception.php', @@ -186,10 +184,9 @@ $wgAutoloadLocalClasses = array( 'RegexlikeReplacer' => 'includes/StringUtils.php', 'ReplacementArray' => 'includes/StringUtils.php', 'Replacer' => 'includes/StringUtils.php', - 'RequestContext' => 'includes/RequestContext.php', 'ReverseChronologicalPager' => 'includes/Pager.php', - 'Rev_Item' => 'includes/RevisionList.php', - 'Rev_List' => 'includes/RevisionList.php', + 'RevisionItemBase' => 'includes/RevisionList.php', + 'RevisionListBase' => 'includes/RevisionList.php', 'Revision' => 'includes/Revision.php', 'RevisionList' => 'includes/RevisionList.php', 'RSSFeed' => 'includes/Feed.php', @@ -214,6 +211,7 @@ $wgAutoloadLocalClasses = array( 'SquidPurgeClient' => 'includes/SquidPurgeClient.php', 'SquidPurgeClientPool' => 'includes/SquidPurgeClient.php', 'Status' => 'includes/Status.php', + 'StreamFile' => 'includes/StreamFile.php', 'StringUtils' => 'includes/StringUtils.php', 'StubContLang' => 'includes/StubObject.php', 'StubObject' => 'includes/StubObject.php', @@ -258,15 +256,26 @@ $wgAutoloadLocalClasses = array( # includes/actions 'CreditsAction' => 'includes/actions/CreditsAction.php', - 'DeletetrackbackAction' => 'includes/actions/DeletetrackbackAction.php', + 'DeleteAction' => 'includes/actions/DeleteAction.php', + 'EditAction' => 'includes/actions/EditAction.php', + 'HistoryAction' => 'includes/actions/HistoryAction.php', + 'HistoryPage' => 'includes/actions/HistoryAction.php', + 'HistoryPager' => 'includes/actions/HistoryAction.php', 'InfoAction' => 'includes/actions/InfoAction.php', 'MarkpatrolledAction' => 'includes/actions/MarkpatrolledAction.php', + 'ProtectAction' => 'includes/actions/ProtectAction.php', 'PurgeAction' => 'includes/actions/PurgeAction.php', + 'RawAction' => 'includes/actions/RawAction.php', + 'RawPage' => 'includes/actions/RawAction.php', + 'RenderAction' => 'includes/actions/RenderAction.php', 'RevertAction' => 'includes/actions/RevertAction.php', 'RevertFileAction' => 'includes/actions/RevertAction.php', 'RevisiondeleteAction' => 'includes/actions/RevisiondeleteAction.php', 'RollbackAction' => 'includes/actions/RollbackAction.php', + 'SubmitAction' => 'includes/actions/EditAction.php', + 'UnprotectAction' => 'includes/actions/ProtectAction.php', 'UnwatchAction' => 'includes/actions/WatchAction.php', + 'ViewAction' => 'includes/actions/ViewAction.php', 'WatchAction' => 'includes/actions/WatchAction.php', # includes/api @@ -359,13 +368,14 @@ $wgAutoloadLocalClasses = array( 'ApiUpload' => 'includes/api/ApiUpload.php', 'ApiUserrights' => 'includes/api/ApiUserrights.php', 'ApiWatch' => 'includes/api/ApiWatch.php', - 'UsageException' => 'includes/api/ApiMain.php', # includes/cache 'CacheDependency' => 'includes/cache/CacheDependency.php', 'ConstantDependency' => 'includes/cache/CacheDependency.php', 'DependencyWrapper' => 'includes/cache/CacheDependency.php', + 'FileCacheBase' => 'includes/cache/FileCacheBase.php', 'FileDependency' => 'includes/cache/CacheDependency.php', + 'GenderCache' => 'includes/cache/GenderCache.php', 'GlobalDependency' => 'includes/cache/CacheDependency.php', 'HTMLCacheUpdate' => 'includes/cache/HTMLCacheUpdate.php', 'HTMLCacheUpdateJob' => 'includes/cache/HTMLCacheUpdate.php', @@ -373,10 +383,20 @@ $wgAutoloadLocalClasses = array( 'LinkBatch' => 'includes/cache/LinkBatch.php', 'LinkCache' => 'includes/cache/LinkCache.php', 'MessageCache' => 'includes/cache/MessageCache.php', + 'ObjectFileCache' => 'includes/cache/ObjectFileCache.php', + 'ResourceFileCache' => 'includes/cache/ResourceFileCache.php', 'SquidUpdate' => 'includes/cache/SquidUpdate.php', 'TitleDependency' => 'includes/cache/CacheDependency.php', 'TitleListDependency' => 'includes/cache/CacheDependency.php', + 'UsageException' => 'includes/api/ApiMain.php', + + # includes/context + 'ContextSource' => 'includes/context/ContextSource.php', + 'DerivativeContext' => 'includes/context/DerivativeContext.php', + 'IContextSource' => 'includes/context/IContextSource.php', + 'RequestContext' => 'includes/context/RequestContext.php', + # includes/db 'Blob' => 'includes/db/DatabaseUtility.php', 'ChronologyProtector' => 'includes/db/LBFactory.php', @@ -419,6 +439,9 @@ $wgAutoloadLocalClasses = array( 'ResultWrapper' => 'includes/db/DatabaseUtility.php', 'SQLiteField' => 'includes/db/DatabaseSqlite.php', + # includes/debug + 'MWDebug' => 'includes/debug/Debug.php', + # includes/diff '_DiffEngine' => 'includes/diff/DairikiDiff.php', '_DiffOp' => 'includes/diff/DairikiDiff.php', @@ -444,24 +467,57 @@ $wgAutoloadLocalClasses = array( 'ExternalUser_vB' => 'includes/extauth/vB.php', # includes/filerepo - 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php', - 'File' => 'includes/filerepo/File.php', 'FileRepo' => 'includes/filerepo/FileRepo.php', 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php', - 'ForeignAPIFile' => 'includes/filerepo/ForeignAPIFile.php', 'ForeignAPIRepo' => 'includes/filerepo/ForeignAPIRepo.php', - 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php', 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php', 'ForeignDBViaLBRepo' => 'includes/filerepo/ForeignDBViaLBRepo.php', 'FSRepo' => 'includes/filerepo/FSRepo.php', - 'LocalFile' => 'includes/filerepo/LocalFile.php', - 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php', - 'LocalFileMoveBatch' => 'includes/filerepo/LocalFile.php', - 'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php', 'LocalRepo' => 'includes/filerepo/LocalRepo.php', - 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php', + 'NullRepo' => 'includes/filerepo/NullRepo.php', 'RepoGroup' => 'includes/filerepo/RepoGroup.php', - 'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php', + + # includes/filerepo/file + 'ArchivedFile' => 'includes/filerepo/file/ArchivedFile.php', + 'File' => 'includes/filerepo/file/File.php', + 'ForeignAPIFile' => 'includes/filerepo/file/ForeignAPIFile.php', + 'ForeignDBFile' => 'includes/filerepo/file/ForeignDBFile.php', + 'LocalFile' => 'includes/filerepo/file/LocalFile.php', + 'LocalFileDeleteBatch' => 'includes/filerepo/file/LocalFile.php', + 'LocalFileMoveBatch' => 'includes/filerepo/file/LocalFile.php', + 'LocalFileRestoreBatch' => 'includes/filerepo/file/LocalFile.php', + 'OldLocalFile' => 'includes/filerepo/file/OldLocalFile.php', + 'UnregisteredLocalFile' => 'includes/filerepo/file/UnregisteredLocalFile.php', + 'FSFile' => 'includes/filerepo/backend/FSFile.php', + 'TempFSFile' => 'includes/filerepo/backend/TempFSFile.php', + + # includes/filerepo/backend + 'FileBackendGroup' => 'includes/filerepo/backend/FileBackendGroup.php', + 'FileBackend' => 'includes/filerepo/backend/FileBackend.php', + 'FileBackendStore' => 'includes/filerepo/backend/FileBackend.php', + 'FileBackendMultiWrite' => 'includes/filerepo/backend/FileBackendMultiWrite.php', + 'FileBackendStoreShardListIterator' => 'includes/filerepo/backend/FileBackend.php', + 'FSFileBackend' => 'includes/filerepo/backend/FSFileBackend.php', + 'FSFileBackendFileList' => 'includes/filerepo/backend/FSFileBackend.php', + 'SwiftFileBackend' => 'includes/filerepo/backend/SwiftFileBackend.php', + 'SwiftFileBackendFileList' => 'includes/filerepo/backend/SwiftFileBackend.php', + 'LockManagerGroup' => 'includes/filerepo/backend/lockmanager/LockManagerGroup.php', + 'LockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php', + 'ScopedLock' => 'includes/filerepo/backend/lockmanager/LockManager.php', + 'FSLockManager' => 'includes/filerepo/backend/lockmanager/FSLockManager.php', + 'DBLockManager' => 'includes/filerepo/backend/lockmanager/DBLockManager.php', + 'LSLockManager' => 'includes/filerepo/backend/lockmanager/LSLockManager.php', + 'MySqlLockManager'=> 'includes/filerepo/backend/lockmanager/DBLockManager.php', + 'NullLockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php', + 'FileOp' => 'includes/filerepo/backend/FileOp.php', + 'FileOpScopedPHPTimeout' => 'includes/filerepo/backend/FileOp.php', + 'StoreFileOp' => 'includes/filerepo/backend/FileOp.php', + 'CopyFileOp' => 'includes/filerepo/backend/FileOp.php', + 'MoveFileOp' => 'includes/filerepo/backend/FileOp.php', + 'DeleteFileOp' => 'includes/filerepo/backend/FileOp.php', + 'ConcatenateFileOp' => 'includes/filerepo/backend/FileOp.php', + 'CreateFileOp' => 'includes/filerepo/backend/FileOp.php', + 'NullFileOp' => 'includes/filerepo/backend/FileOp.php', # includes/installer 'CliInstaller' => 'includes/installer/CliInstaller.php', @@ -527,13 +583,32 @@ $wgAutoloadLocalClasses = array( 'JSMinPlus' => 'includes/libs/jsminplus.php', 'JSParser' => 'includes/libs/jsminplus.php', + # includes/logging + 'DatabaseLogEntry' => 'includes/logging/LogEntry.php', + 'DeleteLogFormatter' => 'includes/logging/LogFormatter.php', + 'LegacyLogFormatter' => 'includes/logging/LogFormatter.php', + 'LogEntry' => 'includes/logging/LogEntry.php', + 'LogEventsList' => 'includes/logging/LogEventsList.php', + 'LogEntryBase' => 'includes/logging/LogEntry.php', + 'LogFormatter' => 'includes/logging/LogFormatter.php', + 'LogPage' => 'includes/logging/LogPage.php', + 'LogPager' => 'includes/logging/LogPager.php', + 'ManualLogEntry' => 'includes/logging/LogEntry.php', + 'MoveLogFormatter' => 'includes/logging/LogFormatter.php', + 'NewUsersLogFormatter' => 'includes/logging/LogFormatter.php', + 'PatrolLog' => 'includes/logging/PatrolLog.php', + 'PatrolLogFormatter' => 'includes/logging/LogFormatter.php', + 'RCDatabaseLogEntry' => 'includes/logging/LogEntry.php', + # includes/media 'BitmapHandler' => 'includes/media/Bitmap.php', 'BitmapHandler_ClientOnly' => 'includes/media/Bitmap_ClientOnly.php', 'BitmapMetadataHandler' => 'includes/media/BitmapMetadataHandler.php', 'BmpHandler' => 'includes/media/BMP.php', 'DjVuHandler' => 'includes/media/DjVu.php', + 'DjVuImage' => 'includes/media/DjVuImage.php', 'Exif' => 'includes/media/Exif.php', + 'ExifBitmapHandler' => 'includes/media/ExifBitmap.php', 'FormatExif' => 'includes/media/FormatMetadata.php', 'FormatMetadata' => 'includes/media/FormatMetadata.php', 'GIFHandler' => 'includes/media/GIF.php', @@ -542,7 +617,6 @@ $wgAutoloadLocalClasses = array( 'IPTC' => 'includes/media/IPTC.php', 'JpegHandler' => 'includes/media/Jpeg.php', 'JpegMetadataExtractor' => 'includes/media/JpegMetadataExtractor.php', - 'ExifBitmapHandler' => 'includes/media/ExifBitmap.php', 'MediaHandler' => 'includes/media/Generic.php', 'MediaTransformError' => 'includes/media/MediaTransformOutput.php', 'MediaTransformOutput' => 'includes/media/MediaTransformOutput.php', @@ -553,6 +627,7 @@ $wgAutoloadLocalClasses = array( 'ThumbnailImage' => 'includes/media/MediaTransformOutput.php', 'TiffHandler' => 'includes/media/Tiff.php', 'TransformParameterError' => 'includes/media/MediaTransformOutput.php', + 'XCFHandler' => 'includes/media/XCF.php', 'XMPInfo' => 'includes/media/XMPInfo.php', 'XMPReader' => 'includes/media/XMP.php', 'XMPValidate' => 'includes/media/XMPValidate.php', @@ -564,7 +639,6 @@ $wgAutoloadLocalClasses = array( 'APCBagOStuff' => 'includes/objectcache/APCBagOStuff.php', 'BagOStuff' => 'includes/objectcache/BagOStuff.php', 'DBABagOStuff' => 'includes/objectcache/DBABagOStuff.php', - 'eAccelBagOStuff' => 'includes/objectcache/eAccelBagOStuff.php', 'EhcacheBagOStuff' => 'includes/objectcache/EhcacheBagOStuff.php', 'EmptyBagOStuff' => 'includes/objectcache/EmptyBagOStuff.php', 'FakeMemCachedClient' => 'includes/objectcache/EmptyBagOStuff.php', @@ -648,6 +722,7 @@ $wgAutoloadLocalClasses = array( 'ResourceLoaderNoscriptModule' => 'includes/resourceloader/ResourceLoaderNoscriptModule.php', 'ResourceLoaderSiteModule' => 'includes/resourceloader/ResourceLoaderSiteModule.php', 'ResourceLoaderStartUpModule' => 'includes/resourceloader/ResourceLoaderStartUpModule.php', + 'ResourceLoaderUserCSSPrefsModule' => 'includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php', 'ResourceLoaderUserGroupsModule' => 'includes/resourceloader/ResourceLoaderUserGroupsModule.php', 'ResourceLoaderUserModule' => 'includes/resourceloader/ResourceLoaderUserModule.php', 'ResourceLoaderUserOptionsModule' => 'includes/resourceloader/ResourceLoaderUserOptionsModule.php', @@ -740,6 +815,7 @@ $wgAutoloadLocalClasses = array( 'SpecialBlockme' => 'includes/specials/SpecialBlockme.php', 'SpecialBookSources' => 'includes/specials/SpecialBooksources.php', 'SpecialCategories' => 'includes/specials/SpecialCategories.php', + 'SpecialChangeEmail' => 'includes/specials/SpecialChangeEmail.php', 'SpecialChangePassword' => 'includes/specials/SpecialChangePassword.php', 'SpecialComparePages' => 'includes/specials/SpecialComparePages.php', 'SpecialContributions' => 'includes/specials/SpecialContributions.php', @@ -748,6 +824,7 @@ $wgAutoloadLocalClasses = array( 'SpecialExport' => 'includes/specials/SpecialExport.php', 'SpecialFilepath' => 'includes/specials/SpecialFilepath.php', 'SpecialImport' => 'includes/specials/SpecialImport.php', + 'SpecialJavaScriptTest' => 'includes/specials/SpecialJavaScriptTest.php', 'SpecialListFiles' => 'includes/specials/SpecialListfiles.php', 'SpecialListGroupRights' => 'includes/specials/SpecialListgrouprights.php', 'SpecialListUsers' => 'includes/specials/SpecialListusers.php', @@ -800,12 +877,13 @@ $wgAutoloadLocalClasses = array( 'WithoutInterwikiPage' => 'includes/specials/SpecialWithoutinterwiki.php', # includes/templates - 'UsercreateTemplate' => 'includes/templates/Userlogin.php', 'UserloginTemplate' => 'includes/templates/Userlogin.php', + 'UsercreateTemplate' => 'includes/templates/Usercreate.php', # includes/upload 'UploadBase' => 'includes/upload/UploadBase.php', 'UploadFromFile' => 'includes/upload/UploadFromFile.php', + 'UploadFromChunks' => 'includes/upload/UploadFromChunks.php', 'UploadFromStash' => 'includes/upload/UploadFromStash.php', 'UploadFromUrl' => 'includes/upload/UploadFromUrl.php', 'UploadStash' => 'includes/upload/UploadStash.php', @@ -816,6 +894,9 @@ $wgAutoloadLocalClasses = array( 'UploadStashFileNotFoundException' => 'includes/upload/UploadStash.php', 'UploadStashNotAvailableException' => 'includes/upload/UploadStash.php', 'UploadStashZeroLengthFileException' => 'includes/upload/UploadStash.php', + 'UploadStashNotLoggedInException' => 'includes/upload/UploadStash.php', + 'UploadStashWrongOwnerException' => 'includes/upload/UploadStash.php', + 'UploadStashNoSuchKeyException' => 'includes/upload/UploadStash.php', # languages 'FakeConverter' => 'languages/Language.php', @@ -832,10 +913,13 @@ $wgAutoloadLocalClasses = array( 'Maintenance' => 'maintenance/Maintenance.php', 'FixExtLinksProtocolRelative' => 'maintenance/fixExtLinksProtocolRelative.php', 'PopulateCategory' => 'maintenance/populateCategory.php', + 'PopulateImageSha1' => 'maintenance/populateImageSha1.php', 'PopulateLogSearch' => 'maintenance/populateLogSearch.php', 'PopulateLogUsertext' => 'maintenance/populateLogUsertext.php', 'PopulateParentId' => 'maintenance/populateParentId.php', 'PopulateRevisionLength' => 'maintenance/populateRevisionLength.php', + 'PopulateRevisionSha1' => 'maintenance/populateRevisionSha1.php', + 'RefreshLinks' => 'maintenance/refreshLinks.php', 'SevenZipStream' => 'maintenance/7zip.inc', 'Sqlite' => 'maintenance/sqlite.inc', 'UpdateCollation' => 'maintenance/updateCollation.php', @@ -844,15 +928,19 @@ $wgAutoloadLocalClasses = array( # maintenance/language 'csvStatsOutput' => 'maintenance/language/StatOutputs.php', + 'languages' => 'maintenance/language/languages.inc', + 'MessageWriter' => 'maintenance/language/writeMessagesArray.inc', 'statsOutput' => 'maintenance/language/StatOutputs.php', 'textStatsOutput' => 'maintenance/language/StatOutputs.php', 'wikiStatsOutput' => 'maintenance/language/StatOutputs.php', + # maintenance/term + 'AnsiTermColorer' => 'maintenance/term/MWTerm.php', + 'DummyTermColorer' => 'maintenance/term/MWTerm.php', + # tests - 'AnsiTermColorer' => 'tests/testHelpers.inc', 'DbTestPreviewer' => 'tests/testHelpers.inc', 'DbTestRecorder' => 'tests/testHelpers.inc', - 'DummyTermColorer' => 'tests/testHelpers.inc', 'TestFileIterator' => 'tests/testHelpers.inc', 'TestRecorder' => 'tests/testHelpers.inc', diff --git a/includes/Autopromote.php b/includes/Autopromote.php index 83f3c20b..a2336030 100644 --- a/includes/Autopromote.php +++ b/includes/Autopromote.php @@ -166,9 +166,9 @@ class Autopromote { $groups = array_slice( $cond, 1 ); return count( array_intersect( $groups, $user->getGroups() ) ) == count( $groups ); case APCOND_ISIP: - return $cond[1] == wfGetIP(); + return $cond[1] == $user->getRequest()->getIP(); case APCOND_IPINRANGE: - return IP::isInRange( wfGetIP(), $cond[1] ); + return IP::isInRange( $user->getRequest()->getIP(), $cond[1] ); case APCOND_BLOCKED: return $user->isBlocked(); case APCOND_ISBOT: diff --git a/includes/BacklinkCache.php b/includes/BacklinkCache.php index 8d1571ec..d17104f8 100644 --- a/includes/BacklinkCache.php +++ b/includes/BacklinkCache.php @@ -22,7 +22,7 @@ * @author Tim Starling * @copyright © 2009, Tim Starling, Domas Mituzas * @copyright © 2010, Max Sem - * @copyright © 2011, Ashar Voultoiz + * @copyright © 2011, Antoine Musso */ class BacklinkCache { @@ -75,6 +75,8 @@ class BacklinkCache { * Serialization handler, diasallows to serialize the database to prevent * failures after this class is deserialized from cache with dead DB * connection. + * + * @return array */ function __sleep() { return array( 'partitionCache', 'fullResultCache', 'title' ); @@ -190,7 +192,13 @@ class BacklinkCache { if ( isset( $prefixes[$table] ) ) { return $prefixes[$table]; } else { - throw new MWException( "Invalid table \"$table\" in " . __CLASS__ ); + $prefix = null; + wfRunHooks( 'BacklinkCacheGetPrefix', array( $table, &$prefix ) ); + if( $prefix ) { + return $prefix; + } else { + throw new MWException( "Invalid table \"$table\" in " . __CLASS__ ); + } } } @@ -237,7 +245,10 @@ class BacklinkCache { ); break; default: - throw new MWException( "Invalid table \"$table\" in " . __CLASS__ ); + $conds = null; + wfRunHooks( 'BacklinkCacheGetConditions', array( $table, $this->title, &$conds ) ); + if( !$conds ) + throw new MWException( "Invalid table \"$table\" in " . __CLASS__ ); } return $conds; diff --git a/includes/Block.php b/includes/Block.php index 27181d86..d80edb5e 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -59,7 +59,7 @@ class Block { */ function __construct( $address = '', $user = 0, $by = 0, $reason = '', $timestamp = 0, $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0, - $hideName = 0, $blockEmail = 0, $allowUsertalk = 0 ) + $hideName = 0, $blockEmail = 0, $allowUsertalk = 0, $byText = '' ) { if( $timestamp === 0 ){ $timestamp = wfTimestampNow(); @@ -71,13 +71,20 @@ class Block { } $this->setTarget( $address ); - $this->setBlocker( User::newFromID( $by ) ); + if ( $this->target instanceof User && $user ) { + $this->target->setId( $user ); // needed for foreign users + } + if ( $by ) { // local user + $this->setBlocker( User::newFromID( $by ) ); + } else { // foreign user + $this->setBlocker( $byText ); + } $this->mReason = $reason; $this->mTimestamp = wfTimestamp( TS_MW, $timestamp ); $this->mAuto = $auto; $this->isHardblock( !$anonOnly ); $this->prevents( 'createaccount', $createAccount ); - if ( $expiry == 'infinity' || $expiry == Block::infinity() ) { + if ( $expiry == 'infinity' || $expiry == wfGetDB( DB_SLAVE )->getInfinity() ) { $this->mExpiry = 'infinity'; } else { $this->mExpiry = wfTimestamp( TS_MW, $expiry ); @@ -101,6 +108,7 @@ class Block { * @deprecated since 1.18 */ public static function newFromDB( $address, $user = 0 ) { + wfDeprecated( __METHOD__, '1.18' ); return self::newFromTarget( User::whoIs( $user ), $address ); } @@ -155,6 +163,7 @@ class Block { * @deprecated since 1.18 */ public function clear() { + wfDeprecated( __METHOD__, '1.18' ); # Noop } @@ -167,7 +176,7 @@ class Block { * @deprecated since 1.18 */ public function load( $address = '', $user = 0 ) { - wfDeprecated( __METHOD__ ); + wfDeprecated( __METHOD__, '1.18' ); if( $user ){ $username = User::whoIs( $user ); $block = self::newFromTarget( $username, $address ); @@ -345,7 +354,11 @@ class Block { */ protected function initFromRow( $row ) { $this->setTarget( $row->ipb_address ); - $this->setBlocker( User::newFromId( $row->ipb_by ) ); + if ( $row->ipb_by ) { // local user + $this->setBlocker( User::newFromID( $row->ipb_by ) ); + } else { // foreign user + $this->setBlocker( $row->ipb_by_text ); + } $this->mReason = $row->ipb_reason; $this->mTimestamp = wfTimestamp( TS_MW, $row->ipb_timestamp ); @@ -418,10 +431,12 @@ class Block { # Don't collide with expired blocks Block::purgeExpired(); - $ipb_id = $dbw->nextSequenceValue( 'ipblocks_ipb_id_seq' ); + $row = $this->getDatabaseArray(); + $row['ipb_id'] = $dbw->nextSequenceValue("ipblocks_ipb_id_seq"); + $dbw->insert( 'ipblocks', - $this->getDatabaseArray(), + $row, __METHOD__, array( 'IGNORE' ) ); @@ -471,8 +486,8 @@ class Block { $a = array( 'ipb_address' => (string)$this->target, 'ipb_user' => $this->target instanceof User ? $this->target->getID() : 0, - 'ipb_by' => $this->getBlocker()->getId(), - 'ipb_by_text' => $this->getBlocker()->getName(), + 'ipb_by' => $this->getBy(), + 'ipb_by_text' => $this->getByName(), 'ipb_reason' => $this->mReason, 'ipb_timestamp' => $db->timestamp( $this->mTimestamp ), 'ipb_auto' => $this->mAuto, @@ -761,11 +776,12 @@ class Block { /** * Get the user id of the blocking sysop * - * @return Integer + * @return Integer (0 for foreign users) */ public function getBy() { - return $this->getBlocker() instanceof User - ? $this->getBlocker()->getId() + $blocker = $this->getBlocker(); + return ( $blocker instanceof User ) + ? $blocker->getId() : 0; } @@ -775,9 +791,10 @@ class Block { * @return String */ public function getByName() { - return $this->getBlocker() instanceof User - ? $this->getBlocker()->getName() - : null; + $blocker = $this->getBlocker(); + return ( $blocker instanceof User ) + ? $blocker->getName() + : (string)$blocker; // username } /** @@ -795,6 +812,7 @@ class Block { * @param $x Bool */ public function forUpdate( $x = null ) { + wfDeprecated( __METHOD__, '1.18' ); # noop } @@ -883,6 +901,7 @@ class Block { * @deprecated since 1.18; use $dbw->encodeExpiry() instead */ public static function encodeExpiry( $expiry, $db ) { + wfDeprecated( __METHOD__, '1.18' ); return $db->encodeExpiry( $expiry ); } @@ -892,9 +911,10 @@ class Block { * @param $expiry String: Database expiry format * @param $timestampType Int Requested timestamp format * @return String - * @deprecated since 1.18; use $wgLang->decodeExpiry() instead + * @deprecated since 1.18; use $wgLang->formatExpiry() instead */ public static function decodeExpiry( $expiry, $timestampType = TS_MW ) { + wfDeprecated( __METHOD__, '1.18' ); global $wgContLang; return $wgContLang->formatExpiry( $expiry, $timestampType ); } @@ -919,6 +939,7 @@ class Block { * @deprecated since 1.18, call IP::sanitizeRange() directly */ public static function normaliseRange( $range ) { + wfDeprecated( __METHOD__, '1.18' ); return IP::sanitizeRange( $range ); } @@ -927,7 +948,8 @@ class Block { */ public static function purgeExpired() { $dbw = wfGetDB( DB_MASTER ); - $dbw->delete( 'ipblocks', array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), __METHOD__ ); + $dbw->delete( 'ipblocks', + array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), __METHOD__ ); } /** @@ -937,6 +959,7 @@ class Block { * @return String */ public static function infinity() { + wfDeprecated( __METHOD__, '1.18' ); return wfGetDB( DB_SLAVE )->getInfinity(); } @@ -948,6 +971,8 @@ class Block { * @deprecated since 1.18; use $wgLang->formatExpiry() instead */ public static function formatExpiry( $encoded_expiry ) { + wfDeprecated( __METHOD__, '1.18' ); + global $wgContLang; static $msg = null; @@ -981,7 +1006,7 @@ class Block { * @deprecated since 1.18 moved to SpecialBlock::parseExpiryInput() */ public static function parseExpiryInput( $expiry ) { - wfDeprecated( __METHOD__ ); + wfDeprecated( __METHOD__, '1.18' ); return SpecialBlock::parseExpiryInput( $expiry ); } @@ -1017,7 +1042,7 @@ class Block { # passed by some callers (bug 29116) return null; - } elseif( in_array( $type, array( Block::TYPE_USER, Block::TYPE_IP, Block::TYPE_RANGE, null ) ) ) { + } elseif( in_array( $type, array( Block::TYPE_USER, Block::TYPE_IP, Block::TYPE_RANGE ) ) ) { $block = new Block(); $block->fromMaster( $fromMaster ); @@ -1027,12 +1052,9 @@ class Block { if( $block->newLoad( $vagueTarget ) ){ return $block; - } else { - return null; } - } else { - return null; } + return null; } /** @@ -1127,6 +1149,15 @@ class Block { } /** + * @since 1.19 + * + * @return Mixed|string + */ + public function getExpiry() { + return $this->mExpiry; + } + + /** * Set the target for this block, and update $this->type accordingly * @param $target Mixed */ @@ -1136,7 +1167,7 @@ class Block { /** * Get the user who implemented this block - * @return User + * @return User|string Local User object or string for a foreign user */ public function getBlocker(){ return $this->blocker; @@ -1144,9 +1175,9 @@ class Block { /** * Set the user who implemented (or will implement) this block - * @param $user User + * @param $user User|string Local User object or username string for foriegn users */ - public function setBlocker( User $user ){ + public function setBlocker( $user ){ $this->blocker = $user; } } diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php index 6a0f6132..eab7a356 100644 --- a/includes/CategoryPage.php +++ b/includes/CategoryPage.php @@ -17,6 +17,10 @@ class CategoryPage extends Article { # Subclasses can change this to override the viewer class. protected $mCategoryViewerClass = 'CategoryViewer'; + /** + * @param $title Title + * @return WikiCategoryPage + */ protected function newPage( Title $title ) { // Overload mPage with a category-specific page return new WikiCategoryPage( $title ); @@ -34,26 +38,28 @@ class CategoryPage extends Article { } function view() { - global $wgRequest, $wgUser; - - $diff = $wgRequest->getVal( 'diff' ); - $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); + $request = $this->getContext()->getRequest(); + $diff = $request->getVal( 'diff' ); + $diffOnly = $request->getBool( 'diffonly', + $this->getContext()->getUser()->getOption( 'diffonly' ) ); if ( isset( $diff ) && $diffOnly ) { - return parent::view(); + parent::view(); + return; } if ( !wfRunHooks( 'CategoryPageView', array( &$this ) ) ) { return; } - if ( NS_CATEGORY == $this->mTitle->getNamespace() ) { + $title = $this->getTitle(); + if ( NS_CATEGORY == $title->getNamespace() ) { $this->openShowCategory(); } parent::view(); - if ( NS_CATEGORY == $this->mTitle->getNamespace() ) { + if ( NS_CATEGORY == $title->getNamespace() ) { $this->closeShowCategory(); } } @@ -63,18 +69,17 @@ class CategoryPage extends Article { } function closeShowCategory() { - global $wgOut, $wgRequest; - // Use these as defaults for back compat --catrope - $oldFrom = $wgRequest->getVal( 'from' ); - $oldUntil = $wgRequest->getVal( 'until' ); + $request = $this->getContext()->getRequest(); + $oldFrom = $request->getVal( 'from' ); + $oldUntil = $request->getVal( 'until' ); + + $reqArray = $request->getValues(); - $reqArray = $wgRequest->getValues(); - $from = $until = array(); foreach ( array( 'page', 'subcat', 'file' ) as $type ) { - $from[$type] = $wgRequest->getVal( "{$type}from", $oldFrom ); - $until[$type] = $wgRequest->getVal( "{$type}until", $oldUntil ); + $from[$type] = $request->getVal( "{$type}from", $oldFrom ); + $until[$type] = $request->getVal( "{$type}until", $oldUntil ); // Do not want old-style from/until propagating in nav links. if ( !isset( $reqArray["{$type}from"] ) && isset( $reqArray["from"] ) ) { @@ -88,652 +93,7 @@ class CategoryPage extends Article { unset( $reqArray["from"] ); unset( $reqArray["to"] ); - $viewer = new $this->mCategoryViewerClass( $this->mTitle, $from, $until, $reqArray ); - $wgOut->addHTML( $viewer->getHTML() ); - } -} - -class CategoryViewer { - var $limit, $from, $until, - $articles, $articles_start_char, - $children, $children_start_char, - $showGallery, $imgsNoGalley, - $imgsNoGallery_start_char, - $imgsNoGallery; - - /** - * @var - */ - var $nextPage; - - /** - * @var Array - */ - var $flip; - - /** - * @var Title - */ - var $title; - - /** - * @var Collation - */ - var $collation; - - /** - * @var ImageGallery - */ - var $gallery; - - /** - * Category object for this page - * @var Category - */ - private $cat; - - /** - * The original query array, to be used in generating paging links. - * @var array - */ - private $query; - - function __construct( $title, $from = '', $until = '', $query = array() ) { - global $wgCategoryPagingLimit; - $this->title = $title; - $this->from = $from; - $this->until = $until; - $this->limit = $wgCategoryPagingLimit; - $this->cat = Category::newFromTitle( $title ); - $this->query = $query; - $this->collation = Collation::singleton(); - unset( $this->query['title'] ); - } - - /** - * Format the category data list. - * - * @return string HTML output - */ - public function getHTML() { - global $wgOut, $wgCategoryMagicGallery, $wgLang, $wgContLang; - wfProfileIn( __METHOD__ ); - - $this->showGallery = $wgCategoryMagicGallery && !$wgOut->mNoGallery; - - $this->clearCategoryState(); - $this->doCategoryQuery(); - $this->finaliseCategoryState(); - - $r = $this->getSubcategorySection() . - $this->getPagesSection() . - $this->getImageSection(); - - if ( $r == '' ) { - // If there is no category content to display, only - // show the top part of the navigation links. - // @todo FIXME: Cannot be completely suppressed because it - // is unknown if 'until' or 'from' makes this - // give 0 results. - $r = $r . $this->getCategoryTop(); - } else { - $r = $this->getCategoryTop() . - $r . - $this->getCategoryBottom(); - } - - // Give a proper message if category is empty - if ( $r == '' ) { - $r = wfMsgExt( 'category-empty', array( 'parse' ) ); - } - - $pageLang = $this->title->getPageLanguage(); - $langAttribs = array( 'lang' => $wgLang->getCode(), 'dir' => $wgLang->getDir() ); - # close the previous div, show the headings in user language, - # then open a new div with the page content language again - $r = Html::openElement( 'div', $langAttribs ) . $r . '</div>'; - - wfProfileOut( __METHOD__ ); - return $wgContLang->convert( $r ); - } - - function clearCategoryState() { - $this->articles = array(); - $this->articles_start_char = array(); - $this->children = array(); - $this->children_start_char = array(); - if ( $this->showGallery ) { - $this->gallery = new ImageGallery(); - $this->gallery->setHideBadImages(); - } else { - $this->imgsNoGallery = array(); - $this->imgsNoGallery_start_char = array(); - } - } - - /** - * Add a subcategory to the internal lists, using a Category object - */ - function addSubcategoryObject( Category $cat, $sortkey, $pageLength ) { - // Subcategory; strip the 'Category' namespace from the link text. - $title = $cat->getTitle(); - - $link = Linker::link( $title, htmlspecialchars( $title->getText() ) ); - if ( $title->isRedirect() ) { - // This didn't used to add redirect-in-category, but might - // as well be consistent with the rest of the sections - // on a category page. - $link = '<span class="redirect-in-category">' . $link . '</span>'; - } - $this->children[] = $link; - - $this->children_start_char[] = - $this->getSubcategorySortChar( $cat->getTitle(), $sortkey ); - } - - /** - * Add a subcategory to the internal lists, using a title object - * @deprecated since 1.17 kept for compatibility, please use addSubcategoryObject instead - */ - function addSubcategory( Title $title, $sortkey, $pageLength ) { - $this->addSubcategoryObject( Category::newFromTitle( $title ), $sortkey, $pageLength ); - } - - /** - * Get the character to be used for sorting subcategories. - * If there's a link from Category:A to Category:B, the sortkey of the resulting - * entry in the categorylinks table is Category:A, not A, which it SHOULD be. - * Workaround: If sortkey == "Category:".$title, than use $title for sorting, - * else use sortkey... - * - * @param Title $title - * @param string $sortkey The human-readable sortkey (before transforming to icu or whatever). - */ - function getSubcategorySortChar( $title, $sortkey ) { - global $wgContLang; - - if ( $title->getPrefixedText() == $sortkey ) { - $word = $title->getDBkey(); - } else { - $word = $sortkey; - } - - $firstChar = $this->collation->getFirstLetter( $word ); - - return $wgContLang->convert( $firstChar ); - } - - /** - * Add a page in the image namespace - */ - function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) { - global $wgContLang; - if ( $this->showGallery ) { - $flip = $this->flip['file']; - if ( $flip ) { - $this->gallery->insert( $title ); - } else { - $this->gallery->add( $title ); - } - } else { - $link = Linker::link( $title ); - if ( $isRedirect ) { - // This seems kind of pointless given 'mw-redirect' class, - // but keeping for back-compatibility with user css. - $link = '<span class="redirect-in-category">' . $link . '</span>'; - } - $this->imgsNoGallery[] = $link; - - $this->imgsNoGallery_start_char[] = $wgContLang->convert( - $this->collation->getFirstLetter( $sortkey ) ); - } - } - - /** - * Add a miscellaneous page - */ - function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) { - global $wgContLang; - - $link = Linker::link( $title ); - if ( $isRedirect ) { - // This seems kind of pointless given 'mw-redirect' class, - // but keeping for back-compatiability with user css. - $link = '<span class="redirect-in-category">' . $link . '</span>'; - } - $this->articles[] = $link; - - $this->articles_start_char[] = $wgContLang->convert( - $this->collation->getFirstLetter( $sortkey ) ); - } - - function finaliseCategoryState() { - if ( $this->flip['subcat'] ) { - $this->children = array_reverse( $this->children ); - $this->children_start_char = array_reverse( $this->children_start_char ); - } - if ( $this->flip['page'] ) { - $this->articles = array_reverse( $this->articles ); - $this->articles_start_char = array_reverse( $this->articles_start_char ); - } - if ( !$this->showGallery && $this->flip['file'] ) { - $this->imgsNoGallery = array_reverse( $this->imgsNoGallery ); - $this->imgsNoGallery_start_char = array_reverse( $this->imgsNoGallery_start_char ); - } - } - - function doCategoryQuery() { - $dbr = wfGetDB( DB_SLAVE, 'category' ); - - $this->nextPage = array( - 'page' => null, - 'subcat' => null, - 'file' => null, - ); - $this->flip = array( 'page' => false, 'subcat' => false, 'file' => false ); - - foreach ( array( 'page', 'subcat', 'file' ) as $type ) { - # Get the sortkeys for start/end, if applicable. Note that if - # the collation in the database differs from the one - # set in $wgCategoryCollation, pagination might go totally haywire. - $extraConds = array( 'cl_type' => $type ); - if ( $this->from[$type] !== null ) { - $extraConds[] = 'cl_sortkey >= ' - . $dbr->addQuotes( $this->collation->getSortKey( $this->from[$type] ) ); - } elseif ( $this->until[$type] !== null ) { - $extraConds[] = 'cl_sortkey < ' - . $dbr->addQuotes( $this->collation->getSortKey( $this->until[$type] ) ); - $this->flip[$type] = true; - } - - $res = $dbr->select( - array( 'page', 'categorylinks', 'category' ), - array( 'page_id', 'page_title', 'page_namespace', 'page_len', - 'page_is_redirect', 'cl_sortkey', 'cat_id', 'cat_title', - 'cat_subcats', 'cat_pages', 'cat_files', - 'cl_sortkey_prefix', 'cl_collation' ), - array_merge( array( 'cl_to' => $this->title->getDBkey() ), $extraConds ), - __METHOD__, - array( - 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ), - 'LIMIT' => $this->limit + 1, - 'ORDER BY' => $this->flip[$type] ? 'cl_sortkey DESC' : 'cl_sortkey', - ), - array( - 'categorylinks' => array( 'INNER JOIN', 'cl_from = page_id' ), - 'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY ) - ) - ); - - $count = 0; - foreach ( $res as $row ) { - $title = Title::newFromRow( $row ); - if ( $row->cl_collation === '' ) { - // Hack to make sure that while updating from 1.16 schema - // and db is inconsistent, that the sky doesn't fall. - // See r83544. Could perhaps be removed in a couple decades... - $humanSortkey = $row->cl_sortkey; - } else { - $humanSortkey = $title->getCategorySortkey( $row->cl_sortkey_prefix ); - } - - if ( ++$count > $this->limit ) { - # We've reached the one extra which shows that there - # are additional pages to be had. Stop here... - $this->nextPage[$type] = $humanSortkey; - break; - } - - if ( $title->getNamespace() == NS_CATEGORY ) { - $cat = Category::newFromRow( $row, $title ); - $this->addSubcategoryObject( $cat, $humanSortkey, $row->page_len ); - } elseif ( $title->getNamespace() == NS_FILE ) { - $this->addImage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect ); - } else { - $this->addPage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect ); - } - } - } - } - - function getCategoryTop() { - $r = $this->getCategoryBottom(); - return $r === '' - ? $r - : "<br style=\"clear:both;\"/>\n" . $r; - } - - function getSubcategorySection() { - # Don't show subcategories section if there are none. - $r = ''; - $rescnt = count( $this->children ); - $dbcnt = $this->cat->getSubcatCount(); - $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' ); - - if ( $rescnt > 0 ) { - # Showing subcategories - $r .= "<div id=\"mw-subcategories\">\n"; - $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n"; - $r .= $countmsg; - $r .= $this->getSectionPagingLinks( 'subcat' ); - $r .= $this->formatList( $this->children, $this->children_start_char ); - $r .= $this->getSectionPagingLinks( 'subcat' ); - $r .= "\n</div>"; - } - return $r; - } - - function getPagesSection() { - $ti = htmlspecialchars( $this->title->getText() ); - # Don't show articles section if there are none. - $r = ''; - - # @todo FIXME: Here and in the other two sections: we don't need to bother - # with this rigamarole if the entire category contents fit on one page - # and have already been retrieved. We can just use $rescnt in that - # case and save a query and some logic. - $dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount() - - $this->cat->getFileCount(); - $rescnt = count( $this->articles ); - $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' ); - - if ( $rescnt > 0 ) { - $r = "<div id=\"mw-pages\">\n"; - $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n"; - $r .= $countmsg; - $r .= $this->getSectionPagingLinks( 'page' ); - $r .= $this->formatList( $this->articles, $this->articles_start_char ); - $r .= $this->getSectionPagingLinks( 'page' ); - $r .= "\n</div>"; - } - return $r; - } - - function getImageSection() { - $r = ''; - $rescnt = $this->showGallery ? $this->gallery->count() : count( $this->imgsNoGallery ); - if ( $rescnt > 0 ) { - $dbcnt = $this->cat->getFileCount(); - $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' ); - - $r .= "<div id=\"mw-category-media\">\n"; - $r .= '<h2>' . wfMsg( 'category-media-header', htmlspecialchars( $this->title->getText() ) ) . "</h2>\n"; - $r .= $countmsg; - $r .= $this->getSectionPagingLinks( 'file' ); - if ( $this->showGallery ) { - $r .= $this->gallery->toHTML(); - } else { - $r .= $this->formatList( $this->imgsNoGallery, $this->imgsNoGallery_start_char ); - } - $r .= $this->getSectionPagingLinks( 'file' ); - $r .= "\n</div>"; - } - return $r; - } - - /** - * Get the paging links for a section (subcats/pages/files), to go at the top and bottom - * of the output. - * - * @param $type String: 'page', 'subcat', or 'file' - * @return String: HTML output, possibly empty if there are no other pages - */ - private function getSectionPagingLinks( $type ) { - if ( $this->until[$type] !== null ) { - return $this->pagingLinks( $this->nextPage[$type], $this->until[$type], $type ); - } elseif ( $this->nextPage[$type] !== null || $this->from[$type] !== null ) { - return $this->pagingLinks( $this->from[$type], $this->nextPage[$type], $type ); - } else { - return ''; - } - } - - function getCategoryBottom() { - return ''; - } - - /** - * Format a list of articles chunked by letter, either as a - * bullet list or a columnar format, depending on the length. - * - * @param $articles Array - * @param $articles_start_char Array - * @param $cutoff Int - * @return String - * @private - */ - function formatList( $articles, $articles_start_char, $cutoff = 6 ) { - $list = ''; - if ( count ( $articles ) > $cutoff ) { - $list = self::columnList( $articles, $articles_start_char ); - } elseif ( count( $articles ) > 0 ) { - // for short lists of articles in categories. - $list = self::shortList( $articles, $articles_start_char ); - } - - $pageLang = $this->title->getPageLanguage(); - $attribs = array( 'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(), - 'class' => 'mw-content-'.$pageLang->getDir() ); - $list = Html::rawElement( 'div', $attribs, $list ); - - return $list; - } - - /** - * Format a list of articles chunked by letter in a three-column - * list, ordered vertically. - * - * TODO: Take the headers into account when creating columns, so they're - * more visually equal. - * - * More distant TODO: Scrap this and use CSS columns, whenever IE finally - * supports those. - * - * @param $articles Array - * @param $articles_start_char Array - * @return String - * @private - */ - static function columnList( $articles, $articles_start_char ) { - $columns = array_combine( $articles, $articles_start_char ); - # Split into three columns - $columns = array_chunk( $columns, ceil( count( $columns ) / 3 ), true /* preserve keys */ ); - - $ret = '<table width="100%"><tr valign="top"><td>'; - $prevchar = null; - - foreach ( $columns as $column ) { - $colContents = array(); - - # Kind of like array_flip() here, but we keep duplicates in an - # array instead of dropping them. - foreach ( $column as $article => $char ) { - if ( !isset( $colContents[$char] ) ) { - $colContents[$char] = array(); - } - $colContents[$char][] = $article; - } - - $first = true; - foreach ( $colContents as $char => $articles ) { - $ret .= '<h3>' . htmlspecialchars( $char ); - if ( $first && $char === $prevchar ) { - # We're continuing a previous chunk at the top of a new - # column, so add " cont." after the letter. - $ret .= ' ' . wfMsgHtml( 'listingcontinuesabbrev' ); - } - $ret .= "</h3>\n"; - - $ret .= '<ul><li>'; - $ret .= implode( "</li>\n<li>", $articles ); - $ret .= '</li></ul>'; - - $first = false; - $prevchar = $char; - } - - $ret .= "</td>\n<td>"; - } - - $ret .= '</td></tr></table>'; - return $ret; - } - - /** - * Format a list of articles chunked by letter in a bullet list. - * @param $articles Array - * @param $articles_start_char Array - * @return String - * @private - */ - static function shortList( $articles, $articles_start_char ) { - $r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n"; - $r .= '<ul><li>' . $articles[0] . '</li>'; - for ( $index = 1; $index < count( $articles ); $index++ ) { - if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) { - $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>"; - } - - $r .= "<li>{$articles[$index]}</li>"; - } - $r .= '</ul>'; - return $r; - } - - /** - * Create paging links, as a helper method to getSectionPagingLinks(). - * - * @param $first String The 'until' parameter for the generated URL - * @param $last String The 'from' parameter for the genererated URL - * @param $type String A prefix for parameters, 'page' or 'subcat' or - * 'file' - * @return String HTML - */ - private function pagingLinks( $first, $last, $type = '' ) { - global $wgLang; - - $limitText = $wgLang->formatNum( $this->limit ); - - $prevLink = wfMsgExt( 'prevn', array( 'escape', 'parsemag' ), $limitText ); - - if ( $first != '' ) { - $prevQuery = $this->query; - $prevQuery["{$type}until"] = $first; - unset( $prevQuery["{$type}from"] ); - $prevLink = Linker::linkKnown( - $this->addFragmentToTitle( $this->title, $type ), - $prevLink, - array(), - $prevQuery - ); - } - - $nextLink = wfMsgExt( 'nextn', array( 'escape', 'parsemag' ), $limitText ); - - if ( $last != '' ) { - $lastQuery = $this->query; - $lastQuery["{$type}from"] = $last; - unset( $lastQuery["{$type}until"] ); - $nextLink = Linker::linkKnown( - $this->addFragmentToTitle( $this->title, $type ), - $nextLink, - array(), - $lastQuery - ); - } - - return "($prevLink) ($nextLink)"; - } - - /** - * Takes a title, and adds the fragment identifier that - * corresponds to the correct segment of the category. - * - * @param Title $title: The title (usually $this->title) - * @param String $section: Which section - */ - private function addFragmentToTitle( $title, $section ) { - switch ( $section ) { - case 'page': - $fragment = 'mw-pages'; - break; - case 'subcat': - $fragment = 'mw-subcategories'; - break; - case 'file': - $fragment = 'mw-category-media'; - break; - default: - throw new MWException( __METHOD__ . - " Invalid section $section." ); - } - - return Title::makeTitle( $title->getNamespace(), - $title->getDBkey(), $fragment ); - } - /** - * What to do if the category table conflicts with the number of results - * returned? This function says what. Each type is considered independently - * of the other types. - * - * Note for grepping: uses the messages category-article-count, - * category-article-count-limited, category-subcat-count, - * category-subcat-count-limited, category-file-count, - * category-file-count-limited. - * - * @param $rescnt Int: The number of items returned by our database query. - * @param $dbcnt Int: The number of items according to the category table. - * @param $type String: 'subcat', 'article', or 'file' - * @return String: A message giving the number of items, to output to HTML. - */ - private function getCountMessage( $rescnt, $dbcnt, $type ) { - global $wgLang; - # There are three cases: - # 1) The category table figure seems sane. It might be wrong, but - # we can't do anything about it if we don't recalculate it on ev- - # ery category view. - # 2) The category table figure isn't sane, like it's smaller than the - # number of actual results, *but* the number of results is less - # than $this->limit and there's no offset. In this case we still - # know the right figure. - # 3) We have no idea. - - # Check if there's a "from" or "until" for anything - - // This is a little ugly, but we seem to use different names - // for the paging types then for the messages. - if ( $type === 'article' ) { - $pagingType = 'page'; - } else { - $pagingType = $type; - } - - $fromOrUntil = false; - if ( $this->from[$pagingType] !== null || $this->until[$pagingType] !== null ) { - $fromOrUntil = true; - } - - if ( $dbcnt == $rescnt || ( ( $rescnt == $this->limit || $fromOrUntil ) - && $dbcnt > $rescnt ) ) { - # Case 1: seems sane. - $totalcnt = $dbcnt; - } elseif ( $rescnt < $this->limit && !$fromOrUntil ) { - # Case 2: not sane, but salvageable. Use the number of results. - # Since there are fewer than 200, we can also take this opportunity - # to refresh the incorrect category table entry -- which should be - # quick due to the small number of entries. - $totalcnt = $rescnt; - $this->cat->refreshCounts(); - } else { - # Case 3: hopeless. Don't give a total count at all. - return wfMsgExt( "category-$type-count-limited", 'parse', - $wgLang->formatNum( $rescnt ) ); - } - return wfMsgExt( - "category-$type-count", - 'parse', - $wgLang->formatNum( $rescnt ), - $wgLang->formatNum( $totalcnt ) - ); + $viewer = new $this->mCategoryViewerClass( $this->getContext()->getTitle(), $this->getContext(), $from, $until, $reqArray ); + $this->getContext()->getOutput()->addHTML( $viewer->getHTML() ); } } diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php new file mode 100644 index 00000000..e8e91423 --- /dev/null +++ b/includes/CategoryViewer.php @@ -0,0 +1,677 @@ +<?php + +if ( !defined( 'MEDIAWIKI' ) ) + die( 1 ); + +class CategoryViewer extends ContextSource { + var $limit, $from, $until, + $articles, $articles_start_char, + $children, $children_start_char, + $showGallery, $imgsNoGalley, + $imgsNoGallery_start_char, + $imgsNoGallery; + + /** + * @var Array + */ + var $nextPage; + + /** + * @var Array + */ + var $flip; + + /** + * @var Title + */ + var $title; + + /** + * @var Collation + */ + var $collation; + + /** + * @var ImageGallery + */ + var $gallery; + + /** + * Category object for this page + * @var Category + */ + private $cat; + + /** + * The original query array, to be used in generating paging links. + * @var array + */ + private $query; + + /** + * Constructor + * + * @since 1.19 $context is a second, required parameter + * @param $title Title + * @param $context IContextSource + * @param $from String + * @param $until String + * @param $query Array + */ + function __construct( $title, IContextSource $context, $from = '', $until = '', $query = array() ) { + global $wgCategoryPagingLimit; + $this->title = $title; + $this->setContext( $context ); + $this->from = $from; + $this->until = $until; + $this->limit = $wgCategoryPagingLimit; + $this->cat = Category::newFromTitle( $title ); + $this->query = $query; + $this->collation = Collation::singleton(); + unset( $this->query['title'] ); + } + + /** + * Format the category data list. + * + * @return string HTML output + */ + public function getHTML() { + global $wgCategoryMagicGallery; + wfProfileIn( __METHOD__ ); + + $this->showGallery = $wgCategoryMagicGallery && !$this->getOutput()->mNoGallery; + + $this->clearCategoryState(); + $this->doCategoryQuery(); + $this->finaliseCategoryState(); + + $r = $this->getSubcategorySection() . + $this->getPagesSection() . + $this->getImageSection(); + + if ( $r == '' ) { + // If there is no category content to display, only + // show the top part of the navigation links. + // @todo FIXME: Cannot be completely suppressed because it + // is unknown if 'until' or 'from' makes this + // give 0 results. + $r = $r . $this->getCategoryTop(); + } else { + $r = $this->getCategoryTop() . + $r . + $this->getCategoryBottom(); + } + + // Give a proper message if category is empty + if ( $r == '' ) { + $r = wfMsgExt( 'category-empty', array( 'parse' ) ); + } + + $lang = $this->getLanguage(); + $langAttribs = array( 'lang' => $lang->getCode(), 'dir' => $lang->getDir() ); + # put a div around the headings which are in the user language + $r = Html::openElement( 'div', $langAttribs ) . $r . '</div>'; + + wfProfileOut( __METHOD__ ); + return $r; + } + + function clearCategoryState() { + $this->articles = array(); + $this->articles_start_char = array(); + $this->children = array(); + $this->children_start_char = array(); + if ( $this->showGallery ) { + $this->gallery = new ImageGallery(); + $this->gallery->setHideBadImages(); + } else { + $this->imgsNoGallery = array(); + $this->imgsNoGallery_start_char = array(); + } + } + + /** + * Add a subcategory to the internal lists, using a Category object + * @param $cat Category + * @param $sortkey + * @param $pageLength + */ + function addSubcategoryObject( Category $cat, $sortkey, $pageLength ) { + // Subcategory; strip the 'Category' namespace from the link text. + $title = $cat->getTitle(); + + $link = Linker::link( $title, htmlspecialchars( $title->getText() ) ); + if ( $title->isRedirect() ) { + // This didn't used to add redirect-in-category, but might + // as well be consistent with the rest of the sections + // on a category page. + $link = '<span class="redirect-in-category">' . $link . '</span>'; + } + $this->children[] = $link; + + $this->children_start_char[] = + $this->getSubcategorySortChar( $cat->getTitle(), $sortkey ); + } + + /** + * Add a subcategory to the internal lists, using a title object + * @deprecated since 1.17 kept for compatibility, please use addSubcategoryObject instead + */ + function addSubcategory( Title $title, $sortkey, $pageLength ) { + wfDeprecated( __METHOD__, '1.17' ); + $this->addSubcategoryObject( Category::newFromTitle( $title ), $sortkey, $pageLength ); + } + + /** + * Get the character to be used for sorting subcategories. + * If there's a link from Category:A to Category:B, the sortkey of the resulting + * entry in the categorylinks table is Category:A, not A, which it SHOULD be. + * Workaround: If sortkey == "Category:".$title, than use $title for sorting, + * else use sortkey... + * + * @param Title $title + * @param string $sortkey The human-readable sortkey (before transforming to icu or whatever). + */ + function getSubcategorySortChar( $title, $sortkey ) { + global $wgContLang; + + if ( $title->getPrefixedText() == $sortkey ) { + $word = $title->getDBkey(); + } else { + $word = $sortkey; + } + + $firstChar = $this->collation->getFirstLetter( $word ); + + return $wgContLang->convert( $firstChar ); + } + + /** + * Add a page in the image namespace + * @param $title Title + * @param $sortkey + * @param $pageLength + * @param $isRedirect bool + */ + function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) { + global $wgContLang; + if ( $this->showGallery ) { + $flip = $this->flip['file']; + if ( $flip ) { + $this->gallery->insert( $title ); + } else { + $this->gallery->add( $title ); + } + } else { + $link = Linker::link( $title ); + if ( $isRedirect ) { + // This seems kind of pointless given 'mw-redirect' class, + // but keeping for back-compatibility with user css. + $link = '<span class="redirect-in-category">' . $link . '</span>'; + } + $this->imgsNoGallery[] = $link; + + $this->imgsNoGallery_start_char[] = $wgContLang->convert( + $this->collation->getFirstLetter( $sortkey ) ); + } + } + + /** + * Add a miscellaneous page + * @param $title + * @param $sortkey + * @param $pageLength + * @param $isRedirect bool + */ + function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) { + global $wgContLang; + + $link = Linker::link( $title ); + if ( $isRedirect ) { + // This seems kind of pointless given 'mw-redirect' class, + // but keeping for back-compatiability with user css. + $link = '<span class="redirect-in-category">' . $link . '</span>'; + } + $this->articles[] = $link; + + $this->articles_start_char[] = $wgContLang->convert( + $this->collation->getFirstLetter( $sortkey ) ); + } + + function finaliseCategoryState() { + if ( $this->flip['subcat'] ) { + $this->children = array_reverse( $this->children ); + $this->children_start_char = array_reverse( $this->children_start_char ); + } + if ( $this->flip['page'] ) { + $this->articles = array_reverse( $this->articles ); + $this->articles_start_char = array_reverse( $this->articles_start_char ); + } + if ( !$this->showGallery && $this->flip['file'] ) { + $this->imgsNoGallery = array_reverse( $this->imgsNoGallery ); + $this->imgsNoGallery_start_char = array_reverse( $this->imgsNoGallery_start_char ); + } + } + + function doCategoryQuery() { + $dbr = wfGetDB( DB_SLAVE, 'category' ); + + $this->nextPage = array( + 'page' => null, + 'subcat' => null, + 'file' => null, + ); + $this->flip = array( 'page' => false, 'subcat' => false, 'file' => false ); + + foreach ( array( 'page', 'subcat', 'file' ) as $type ) { + # Get the sortkeys for start/end, if applicable. Note that if + # the collation in the database differs from the one + # set in $wgCategoryCollation, pagination might go totally haywire. + $extraConds = array( 'cl_type' => $type ); + if ( $this->from[$type] !== null ) { + $extraConds[] = 'cl_sortkey >= ' + . $dbr->addQuotes( $this->collation->getSortKey( $this->from[$type] ) ); + } elseif ( $this->until[$type] !== null ) { + $extraConds[] = 'cl_sortkey < ' + . $dbr->addQuotes( $this->collation->getSortKey( $this->until[$type] ) ); + $this->flip[$type] = true; + } + + $res = $dbr->select( + array( 'page', 'categorylinks', 'category' ), + array( 'page_id', 'page_title', 'page_namespace', 'page_len', + 'page_is_redirect', 'cl_sortkey', 'cat_id', 'cat_title', + 'cat_subcats', 'cat_pages', 'cat_files', + 'cl_sortkey_prefix', 'cl_collation' ), + array_merge( array( 'cl_to' => $this->title->getDBkey() ), $extraConds ), + __METHOD__, + array( + 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ), + 'LIMIT' => $this->limit + 1, + 'ORDER BY' => $this->flip[$type] ? 'cl_sortkey DESC' : 'cl_sortkey', + ), + array( + 'categorylinks' => array( 'INNER JOIN', 'cl_from = page_id' ), + 'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY ) + ) + ); + + $count = 0; + foreach ( $res as $row ) { + $title = Title::newFromRow( $row ); + if ( $row->cl_collation === '' ) { + // Hack to make sure that while updating from 1.16 schema + // and db is inconsistent, that the sky doesn't fall. + // See r83544. Could perhaps be removed in a couple decades... + $humanSortkey = $row->cl_sortkey; + } else { + $humanSortkey = $title->getCategorySortkey( $row->cl_sortkey_prefix ); + } + + if ( ++$count > $this->limit ) { + # We've reached the one extra which shows that there + # are additional pages to be had. Stop here... + $this->nextPage[$type] = $humanSortkey; + break; + } + + if ( $title->getNamespace() == NS_CATEGORY ) { + $cat = Category::newFromRow( $row, $title ); + $this->addSubcategoryObject( $cat, $humanSortkey, $row->page_len ); + } elseif ( $title->getNamespace() == NS_FILE ) { + $this->addImage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect ); + } else { + $this->addPage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect ); + } + } + } + } + + /** + * @return string + */ + function getCategoryTop() { + $r = $this->getCategoryBottom(); + return $r === '' + ? $r + : "<br style=\"clear:both;\"/>\n" . $r; + } + + /** + * @return string + */ + function getSubcategorySection() { + # Don't show subcategories section if there are none. + $r = ''; + $rescnt = count( $this->children ); + $dbcnt = $this->cat->getSubcatCount(); + $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' ); + + if ( $rescnt > 0 ) { + # Showing subcategories + $r .= "<div id=\"mw-subcategories\">\n"; + $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n"; + $r .= $countmsg; + $r .= $this->getSectionPagingLinks( 'subcat' ); + $r .= $this->formatList( $this->children, $this->children_start_char ); + $r .= $this->getSectionPagingLinks( 'subcat' ); + $r .= "\n</div>"; + } + return $r; + } + + /** + * @return string + */ + function getPagesSection() { + $ti = htmlspecialchars( $this->title->getText() ); + # Don't show articles section if there are none. + $r = ''; + + # @todo FIXME: Here and in the other two sections: we don't need to bother + # with this rigamarole if the entire category contents fit on one page + # and have already been retrieved. We can just use $rescnt in that + # case and save a query and some logic. + $dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount() + - $this->cat->getFileCount(); + $rescnt = count( $this->articles ); + $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' ); + + if ( $rescnt > 0 ) { + $r = "<div id=\"mw-pages\">\n"; + $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n"; + $r .= $countmsg; + $r .= $this->getSectionPagingLinks( 'page' ); + $r .= $this->formatList( $this->articles, $this->articles_start_char ); + $r .= $this->getSectionPagingLinks( 'page' ); + $r .= "\n</div>"; + } + return $r; + } + + /** + * @return string + */ + function getImageSection() { + $r = ''; + $rescnt = $this->showGallery ? $this->gallery->count() : count( $this->imgsNoGallery ); + if ( $rescnt > 0 ) { + $dbcnt = $this->cat->getFileCount(); + $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' ); + + $r .= "<div id=\"mw-category-media\">\n"; + $r .= '<h2>' . wfMsg( 'category-media-header', htmlspecialchars( $this->title->getText() ) ) . "</h2>\n"; + $r .= $countmsg; + $r .= $this->getSectionPagingLinks( 'file' ); + if ( $this->showGallery ) { + $r .= $this->gallery->toHTML(); + } else { + $r .= $this->formatList( $this->imgsNoGallery, $this->imgsNoGallery_start_char ); + } + $r .= $this->getSectionPagingLinks( 'file' ); + $r .= "\n</div>"; + } + return $r; + } + + /** + * Get the paging links for a section (subcats/pages/files), to go at the top and bottom + * of the output. + * + * @param $type String: 'page', 'subcat', or 'file' + * @return String: HTML output, possibly empty if there are no other pages + */ + private function getSectionPagingLinks( $type ) { + if ( $this->until[$type] !== null ) { + return $this->pagingLinks( $this->nextPage[$type], $this->until[$type], $type ); + } elseif ( $this->nextPage[$type] !== null || $this->from[$type] !== null ) { + return $this->pagingLinks( $this->from[$type], $this->nextPage[$type], $type ); + } else { + return ''; + } + } + + /** + * @return string + */ + function getCategoryBottom() { + return ''; + } + + /** + * Format a list of articles chunked by letter, either as a + * bullet list or a columnar format, depending on the length. + * + * @param $articles Array + * @param $articles_start_char Array + * @param $cutoff Int + * @return String + * @private + */ + function formatList( $articles, $articles_start_char, $cutoff = 6 ) { + $list = ''; + if ( count ( $articles ) > $cutoff ) { + $list = self::columnList( $articles, $articles_start_char ); + } elseif ( count( $articles ) > 0 ) { + // for short lists of articles in categories. + $list = self::shortList( $articles, $articles_start_char ); + } + + $pageLang = $this->title->getPageLanguage(); + $attribs = array( 'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(), + 'class' => 'mw-content-'.$pageLang->getDir() ); + $list = Html::rawElement( 'div', $attribs, $list ); + + return $list; + } + + /** + * Format a list of articles chunked by letter in a three-column + * list, ordered vertically. + * + * TODO: Take the headers into account when creating columns, so they're + * more visually equal. + * + * More distant TODO: Scrap this and use CSS columns, whenever IE finally + * supports those. + * + * @param $articles Array + * @param $articles_start_char Array + * @return String + * @private + */ + static function columnList( $articles, $articles_start_char ) { + $columns = array_combine( $articles, $articles_start_char ); + # Split into three columns + $columns = array_chunk( $columns, ceil( count( $columns ) / 3 ), true /* preserve keys */ ); + + $ret = '<table width="100%"><tr valign="top">'; + $prevchar = null; + + foreach ( $columns as $column ) { + $ret .= '<td width="33.3%">'; + $colContents = array(); + + # Kind of like array_flip() here, but we keep duplicates in an + # array instead of dropping them. + foreach ( $column as $article => $char ) { + if ( !isset( $colContents[$char] ) ) { + $colContents[$char] = array(); + } + $colContents[$char][] = $article; + } + + $first = true; + foreach ( $colContents as $char => $articles ) { + $ret .= '<h3>' . htmlspecialchars( $char ); + if ( $first && $char === $prevchar ) { + # We're continuing a previous chunk at the top of a new + # column, so add " cont." after the letter. + $ret .= ' ' . wfMsgHtml( 'listingcontinuesabbrev' ); + } + $ret .= "</h3>\n"; + + $ret .= '<ul><li>'; + $ret .= implode( "</li>\n<li>", $articles ); + $ret .= '</li></ul>'; + + $first = false; + $prevchar = $char; + } + + $ret .= "</td>\n"; + } + + $ret .= '</tr></table>'; + return $ret; + } + + /** + * Format a list of articles chunked by letter in a bullet list. + * @param $articles Array + * @param $articles_start_char Array + * @return String + * @private + */ + static function shortList( $articles, $articles_start_char ) { + $r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n"; + $r .= '<ul><li>' . $articles[0] . '</li>'; + for ( $index = 1; $index < count( $articles ); $index++ ) { + if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) { + $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>"; + } + + $r .= "<li>{$articles[$index]}</li>"; + } + $r .= '</ul>'; + return $r; + } + + /** + * Create paging links, as a helper method to getSectionPagingLinks(). + * + * @param $first String The 'until' parameter for the generated URL + * @param $last String The 'from' parameter for the genererated URL + * @param $type String A prefix for parameters, 'page' or 'subcat' or + * 'file' + * @return String HTML + */ + private function pagingLinks( $first, $last, $type = '' ) { + $prevLink = wfMessage( 'prevn' )->numParams( $this->limit )->escaped(); + + if ( $first != '' ) { + $prevQuery = $this->query; + $prevQuery["{$type}until"] = $first; + unset( $prevQuery["{$type}from"] ); + $prevLink = Linker::linkKnown( + $this->addFragmentToTitle( $this->title, $type ), + $prevLink, + array(), + $prevQuery + ); + } + + $nextLink = wfMessage( 'nextn' )->numParams( $this->limit )->escaped(); + + if ( $last != '' ) { + $lastQuery = $this->query; + $lastQuery["{$type}from"] = $last; + unset( $lastQuery["{$type}until"] ); + $nextLink = Linker::linkKnown( + $this->addFragmentToTitle( $this->title, $type ), + $nextLink, + array(), + $lastQuery + ); + } + + return "($prevLink) ($nextLink)"; + } + + /** + * Takes a title, and adds the fragment identifier that + * corresponds to the correct segment of the category. + * + * @param Title $title: The title (usually $this->title) + * @param String $section: Which section + * @return Title + */ + private function addFragmentToTitle( $title, $section ) { + switch ( $section ) { + case 'page': + $fragment = 'mw-pages'; + break; + case 'subcat': + $fragment = 'mw-subcategories'; + break; + case 'file': + $fragment = 'mw-category-media'; + break; + default: + throw new MWException( __METHOD__ . + " Invalid section $section." ); + } + + return Title::makeTitle( $title->getNamespace(), + $title->getDBkey(), $fragment ); + } + /** + * What to do if the category table conflicts with the number of results + * returned? This function says what. Each type is considered independently + * of the other types. + * + * Note for grepping: uses the messages category-article-count, + * category-article-count-limited, category-subcat-count, + * category-subcat-count-limited, category-file-count, + * category-file-count-limited. + * + * @param $rescnt Int: The number of items returned by our database query. + * @param $dbcnt Int: The number of items according to the category table. + * @param $type String: 'subcat', 'article', or 'file' + * @return String: A message giving the number of items, to output to HTML. + */ + private function getCountMessage( $rescnt, $dbcnt, $type ) { + # There are three cases: + # 1) The category table figure seems sane. It might be wrong, but + # we can't do anything about it if we don't recalculate it on ev- + # ery category view. + # 2) The category table figure isn't sane, like it's smaller than the + # number of actual results, *but* the number of results is less + # than $this->limit and there's no offset. In this case we still + # know the right figure. + # 3) We have no idea. + + # Check if there's a "from" or "until" for anything + + // This is a little ugly, but we seem to use different names + // for the paging types then for the messages. + if ( $type === 'article' ) { + $pagingType = 'page'; + } else { + $pagingType = $type; + } + + $fromOrUntil = false; + if ( $this->from[$pagingType] !== null || $this->until[$pagingType] !== null ) { + $fromOrUntil = true; + } + + if ( $dbcnt == $rescnt || ( ( $rescnt == $this->limit || $fromOrUntil ) + && $dbcnt > $rescnt ) ) { + # Case 1: seems sane. + $totalcnt = $dbcnt; + } elseif ( $rescnt < $this->limit && !$fromOrUntil ) { + # Case 2: not sane, but salvageable. Use the number of results. + # Since there are fewer than 200, we can also take this opportunity + # to refresh the incorrect category table entry -- which should be + # quick due to the small number of entries. + $totalcnt = $rescnt; + $this->cat->refreshCounts(); + } else { + # Case 3: hopeless. Don't give a total count at all. + return wfMessage( "category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock(); + } + return wfMessage( "category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock(); + } +} diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php index 2567de0d..4a8ed709 100644 --- a/includes/Categoryfinder.php +++ b/includes/Categoryfinder.php @@ -29,6 +29,10 @@ class Categoryfinder { var $targets = array(); # Array of DBKEY category names var $name2id = array(); var $mode; # "AND" or "OR" + + /** + * @var DatabaseBase + */ var $dbr; # Read-DB slave /** diff --git a/includes/Cdb.php b/includes/Cdb.php index d7a2bca5..94aa1925 100644 --- a/includes/Cdb.php +++ b/includes/Cdb.php @@ -72,7 +72,7 @@ abstract class CdbWriter { * * @param $fileName string * - * @return bool + * @return CdbWriter_DBA|CdbWriter_PHP */ public static function open( $fileName ) { if ( CdbReader::haveExtension() ) { @@ -85,11 +85,15 @@ abstract class CdbWriter { /** * Create the object and open the file + * + * @param $fileName string */ abstract function __construct( $fileName ); /** * Set a key to a given value. The value will be converted to string. + * @param $key string + * @param $value string */ abstract public function set( $key, $value ); @@ -100,7 +104,6 @@ abstract class CdbWriter { abstract public function close(); } - /** * Reader class which uses the DBA extension */ diff --git a/includes/Cdb_PHP.php b/includes/Cdb_PHP.php index f4029ba5..53175272 100644 --- a/includes/Cdb_PHP.php +++ b/includes/Cdb_PHP.php @@ -1,6 +1,6 @@ <?php /** - * This is a port of D.J. Bernstein's CDB to PHP. It's based on the copy that + * This is a port of D.J. Bernstein's CDB to PHP. It's based on the copy that * appears in PHP 5.3. Changes are: * * Error returns replaced with exceptions * * Exception thrown if sizes or offsets are between 2GB and 4GB @@ -50,7 +50,7 @@ class CdbFunctions { /** * The CDB hash function. - * + * * @param $s * * @return @@ -62,7 +62,7 @@ class CdbFunctions { // Do a 32-bit sum // Inlined here for speed $sum = ($h & 0x3fffffff) + ($h5 & 0x3fffffff); - $h = + $h = ( ( $sum & 0x40000000 ? 1 : 0 ) + ( $h & 0x80000000 ? 2 : 0 ) @@ -82,6 +82,9 @@ class CdbFunctions { * CDB reader class */ class CdbReader_PHP extends CdbReader { + /** The filename */ + var $fileName; + /** The file handle */ var $handle; @@ -104,12 +107,16 @@ class CdbReader_PHP extends CdbReader { var $dpos; /* initialized if cdb_findnext() returns 1 */ - var $dlen; + var $dlen; + /** + * @param $fileName string + */ function __construct( $fileName ) { + $this->fileName = $fileName; $this->handle = fopen( $fileName, 'rb' ); if ( !$this->handle ) { - throw new MWException( 'Unable to open CDB file "' . $fileName . '"' ); + throw new MWException( 'Unable to open CDB file "' . $this->fileName . '".' ); } $this->findStart(); } @@ -157,7 +164,8 @@ class CdbReader_PHP extends CdbReader { protected function read( $length, $pos ) { if ( fseek( $this->handle, $pos ) == -1 ) { // This can easily happen if the internal pointers are incorrect - throw new MWException( __METHOD__.': seek failed, file may be corrupted.' ); + throw new MWException( + 'Seek failed, file "' . $this->fileName . '" may be corrupted.' ); } if ( $length == 0 ) { @@ -166,7 +174,8 @@ class CdbReader_PHP extends CdbReader { $buf = fread( $this->handle, $length ); if ( $buf === false || strlen( $buf ) !== $length ) { - throw new MWException( __METHOD__.': read from CDB file failed, file may be corrupted' ); + throw new MWException( + 'Read from CDB file failed, file "' . $this->fileName . '" may be corrupted.' ); } return $buf; } @@ -179,7 +188,8 @@ class CdbReader_PHP extends CdbReader { protected function unpack31( $s ) { $data = unpack( 'V', $s ); if ( $data[1] > 0x7fffffff ) { - throw new MWException( __METHOD__.': error in CDB file, integer too big' ); + throw new MWException( + 'Error in CDB file "' . $this->fileName . '", integer too big.' ); } return $data[1]; } @@ -257,20 +267,24 @@ class CdbWriter_PHP extends CdbWriter { var $handle, $realFileName, $tmpFileName; var $hplist; - var $numEntries, $pos; + var $numentries, $pos; + /** + * @param $fileName string + */ function __construct( $fileName ) { $this->realFileName = $fileName; $this->tmpFileName = $fileName . '.tmp.' . mt_rand( 0, 0x7fffffff ); $this->handle = fopen( $this->tmpFileName, 'wb' ); if ( !$this->handle ) { - throw new MWException( 'Unable to open CDB file for write "' . $fileName . '"' ); + $this->throwException( + 'Unable to open CDB file "' . $this->tmpFileName . '" for write.' ); } $this->hplist = array(); $this->numentries = 0; $this->pos = 2048; // leaving space for the pointer array, 256 * 8 if ( fseek( $this->handle, $this->pos ) == -1 ) { - throw new MWException( __METHOD__.': fseek failed' ); + $this->throwException( 'fseek failed in file "' . $this->tmpFileName . '".' ); } } @@ -308,7 +322,7 @@ class CdbWriter_PHP extends CdbWriter { unlink( $this->realFileName ); } if ( !rename( $this->tmpFileName, $this->realFileName ) ) { - throw new MWException( 'Unable to move the new CDB file into place.' ); + $this->throwException( 'Unable to move the new CDB file into place.' ); } unset( $this->handle ); } @@ -320,7 +334,7 @@ class CdbWriter_PHP extends CdbWriter { protected function write( $buf ) { $len = fwrite( $this->handle, $buf ); if ( $len !== strlen( $buf ) ) { - throw new MWException( 'Error writing to CDB file.' ); + $this->throwException( 'Error writing to CDB file "'.$this->tmpFileName.'".' ); } } @@ -331,7 +345,8 @@ class CdbWriter_PHP extends CdbWriter { protected function posplus( $len ) { $newpos = $this->pos + $len; if ( $newpos > 0x7fffffff ) { - throw new MWException( 'A value in the CDB file is too large' ); + $this->throwException( + 'A value in the CDB file "'.$this->tmpFileName.'" is too large.' ); } $this->pos = $newpos; } @@ -360,10 +375,10 @@ class CdbWriter_PHP extends CdbWriter { */ protected function addbegin( $keylen, $datalen ) { if ( $keylen > 0x7fffffff ) { - throw new MWException( __METHOD__.': key length too long' ); + $this->throwException( 'Key length too long in file "'.$this->tmpFileName.'".' ); } if ( $datalen > 0x7fffffff ) { - throw new MWException( __METHOD__.': data length too long' ); + $this->throwException( 'Data length too long in file "'.$this->tmpFileName.'".' ); } $buf = pack( 'VV', $keylen, $datalen ); $this->write( $buf ); @@ -391,7 +406,7 @@ class CdbWriter_PHP extends CdbWriter { } // Excessively clever and indulgent code to simultaneously fill $packedTables - // with the packed hashtables, and adjust the elements of $starts + // with the packed hashtables, and adjust the elements of $starts // to actually point to the starts instead of the ends. $packedTables = array_fill( 0, $this->numentries, false ); foreach ( $this->hplist as $item ) { @@ -416,7 +431,7 @@ class CdbWriter_PHP extends CdbWriter { // is taken. for ( $u = 0; $u < $count; ++$u ) { $hp = $packedTables[$starts[$i] + $u]; - $where = CdbFunctions::unsignedMod( + $where = CdbFunctions::unsignedMod( CdbFunctions::unsignedShiftRight( $hp['h'], 8 ), $len ); while ( $hashtable[$where]['p'] ) if ( ++$where == $len ) @@ -426,7 +441,7 @@ class CdbWriter_PHP extends CdbWriter { // Write the hashtable for ( $u = 0; $u < $len; ++$u ) { - $buf = pack( 'vvV', + $buf = pack( 'vvV', $hashtable[$u]['h'] & 0xffff, CdbFunctions::unsignedShiftRight( $hashtable[$u]['h'], 16 ), $hashtable[$u]['p'] ); @@ -438,8 +453,22 @@ class CdbWriter_PHP extends CdbWriter { // Write the pointer array at the start of the file rewind( $this->handle ); if ( ftell( $this->handle ) != 0 ) { - throw new MWException( __METHOD__.': Error rewinding to start of file' ); + $this->throwException( 'Error rewinding to start of file "'.$this->tmpFileName.'".' ); } $this->write( $final ); } + + /** + * Clean up the temp file and throw an exception + * + * @param $msg string + * @throws MWException + */ + protected function throwException( $msg ) { + if ( $this->handle ) { + fclose( $this->handle ); + unlink( $this->tmpFileName ); + } + throw new MWException( $msg ); + } } diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php index c8e522df..63d37327 100644 --- a/includes/ChangeTags.php +++ b/includes/ChangeTags.php @@ -1,6 +1,23 @@ <?php - +/** + * Functions related to change tags. + * + * @file + */ class ChangeTags { + + /** + * Creates HTML for the given tags + * + * @param $tags String: Comma-separated list of tags + * @param $page String: A label for the type of action which is being displayed, + * for example: 'history', 'contributions' or 'newpages' + * + * @return Array with two items: (html, classes) + * - html: String: HTML for displaying the tags (empty string when param $tags is empty) + * - classes: Array of strings: CSS classes used in the generated html, one class for each tag + * + */ static function formatSummaryRow( $tags, $page ) { if( !$tags ) return array( '', array() ); @@ -18,18 +35,38 @@ class ChangeTags { ); $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" ); } - $markers = '(' . implode( ', ', $displayTags ) . ')'; $markers = Xml::tags( 'span', array( 'class' => 'mw-tag-markers' ), $markers ); + return array( $markers, $classes ); } + /** + * Get a short description for a tag + * + * @param $tag String: tag + * + * @return String: Short description of the tag from "mediawiki:tag-$tag" if this message exists, + * html-escaped version of $tag otherwise + */ static function tagDescription( $tag ) { $msg = wfMessage( "tag-$tag" ); - return $msg->exists() ? $msg->parse() : htmlspecialchars( $tag ); + return $msg->exists() ? $msg->parse() : htmlspecialchars( $tag ); } - ## Basic utility method to add tags to a particular change, given its rc_id, rev_id and/or log_id. + /** + * Add tags to a change given its rc_id, rev_id and/or log_id + * + * @param $tags String|Array: Tags to add to the change + * @param $rc_id int: rc_id of the change to add the tags to + * @param $rev_id int: rev_id of the change to add the tags to + * @param $log_id int: log_id of the change to add the tags to + * @param $params String: params to put in the ct_params field of tabel 'change_tag' + * + * @return bool: false if no changes are made, otherwise true + * + * @exception MWException when $rc_id, $rev_id and $log_id are all null + */ static function addTags( $tags, $rc_id = null, $rev_id = null, $log_id = null, $params = null ) { if ( !is_array( $tags ) ) { $tags = array( $tags ); @@ -103,6 +140,16 @@ class ChangeTags { * Applies all tags-related changes to a query. * Handles selecting tags, and filtering. * Needs $tables to be set up properly, so we can figure out which join conditions to use. + * + * @param $tables String|Array: Tabel names, see DatabaseBase::select + * @param $fields String|Array: Fields used in query, see DatabaseBase::select + * @param $conds String|Array: conditions used in query, see DatabaseBase::select + * @param $join_conds Array: join conditions, see DatabaseBase::select + * @param $options Array: options, see Database::select + * @param $filter_tag String: tag to select on + * + * @exception MWException when unable to determine appropriate JOIN condition for tagging + * */ static function modifyDisplayQuery( &$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag = false ) { @@ -178,9 +225,13 @@ class ChangeTags { } /** - *Basically lists defined tags which count even if they aren't applied to anything + * Basically lists defined tags which count even if they aren't applied to anything. + * Tags on items in table 'change_tag' which are not (or no longer) in table 'valid_tag' + * are not included. + * + * Tries memcached first. * - * @return array + * @return Array of strings: tags */ static function listDefinedTags() { // Caching... diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php index c4c4a8a1..bcedf2f3 100644 --- a/includes/ChangesFeed.php +++ b/includes/ChangesFeed.php @@ -34,6 +34,11 @@ class ChangesFeed { return false; } + if( !array_key_exists( $this->format, $wgFeedClasses ) ) { + // falling back to atom + $this->format = 'atom'; + } + $feedTitle = "$wgSitename - {$title} [$wgLanguageCode]"; return new $wgFeedClasses[$this->format]( $feedTitle, htmlspecialchars( $description ), $url ); diff --git a/includes/ChangesList.php b/includes/ChangesList.php index 1858dc3a..fd97e0cb 100644 --- a/includes/ChangesList.php +++ b/includes/ChangesList.php @@ -67,6 +67,7 @@ class ChangesList extends ContextSource { * @return ChangesList|EnhancedChangesList|OldChangesList derivative */ public static function newFromUser( $unused ) { + wfDeprecated( __METHOD__, '1.18' ); return self::newFromContext( RequestContext::getMain() ); } @@ -193,10 +194,10 @@ class ChangesList extends ContextSource { $fastCharDiff[$code] = $wgMiserMode || wfMsgNoTrans( 'rc-change-size' ) === '$1'; } - $formatedSize = $wgLang->formatNum($szdiff); + $formattedSize = $wgLang->formatNum($szdiff); if ( !$fastCharDiff[$code] ) { - $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape' ), $formatedSize ); + $formattedSize = wfMsgExt( 'rc-change-size', array( 'parsemag' ), $formattedSize ); } if( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) { @@ -204,13 +205,23 @@ class ChangesList extends ContextSource { } else { $tag = 'span'; } - if( $szdiff === 0 ) { - return "<$tag class='mw-plusminus-null'>($formatedSize)</$tag>"; - } elseif( $szdiff > 0 ) { - return "<$tag class='mw-plusminus-pos'>(+$formatedSize)</$tag>"; - } else { - return "<$tag class='mw-plusminus-neg'>($formatedSize)</$tag>"; + + if ( $szdiff === 0 ) { + $formattedSizeClass = 'mw-plusminus-null'; + } + if ( $szdiff > 0 ) { + $formattedSize = '+' . $formattedSize; + $formattedSizeClass = 'mw-plusminus-pos'; } + if ( $szdiff < 0 ) { + $formattedSizeClass = 'mw-plusminus-neg'; + } + + $formattedTotalSize = wfMsgExt( 'rc-change-size-new', 'parsemag', $wgLang->formatNum( $new ) ); + + return Html::element( $tag, + array( 'dir' => 'ltr', 'class' => $formattedSizeClass, 'title' => $formattedTotalSize ), + wfMessage( 'parentheses', $formattedSize )->plain() ) . $wgLang->getDirMark(); } /** @@ -225,38 +236,9 @@ class ChangesList extends ContextSource { } } - /** - * @param $s - * @param $rc RecentChange - * @return void - */ - public function insertMove( &$s, $rc ) { - # Diff - $s .= '(' . $this->message['diff'] . ') ('; - # Hist - $s .= Linker::linkKnown( - $rc->getMovedToTitle(), - $this->message['hist'], - array(), - array( 'action' => 'history' ) - ) . ') . . '; - # "[[x]] moved to [[y]]" - $msg = ( $rc->mAttribs['rc_type'] == RC_MOVE ) ? '1movedto2' : '1movedto2_redir'; - $s .= wfMsgHtml( - $msg, - Linker::linkKnown( - $rc->getTitle(), - null, - array(), - array( 'redirect' => 'no' ) - ), - Linker::linkKnown( $rc->getMovedToTitle() ) - ); - } - public function insertDateHeader( &$s, $rc_timestamp ) { # Make date header if necessary - $date = $this->getLang()->date( $rc_timestamp, true, true ); + $date = $this->getLanguage()->date( $rc_timestamp, true, true ); if( $date != $this->lastdate ) { if( $this->lastdate != '' ) { $s .= "</ul>\n"; @@ -268,21 +250,21 @@ class ChangesList extends ContextSource { } public function insertLog( &$s, $title, $logtype ) { - $logname = LogPage::logName( $logtype ); - $s .= '(' . Linker::linkKnown( $title, htmlspecialchars( $logname ) ) . ')'; + $page = new LogPage( $logtype ); + $logname = $page->getName()->escaped(); + $s .= '(' . Linker::linkKnown( $title, $logname ) . ')'; } /** * @param $s * @param $rc RecentChange * @param $unpatrolled - * @return void */ public function insertDiffHist( &$s, &$rc, $unpatrolled ) { # Diff link if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) { $diffLink = $this->message['diff']; - } elseif( !self::userCan($rc,Revision::DELETED_TEXT) ) { + } elseif ( !self::userCan( $rc, Revision::DELETED_TEXT, $this->getUser() ) ) { $diffLink = $this->message['diff']; } else { $query = array( @@ -321,7 +303,6 @@ class ChangesList extends ContextSource { * @param $rc RecentChange * @param $unpatrolled * @param $watched - * @return void */ public function insertArticleLink( &$s, &$rc, $unpatrolled, $watched ) { # If it's a new article, there is no diff link, but if it hasn't been @@ -332,28 +313,21 @@ class ChangesList extends ContextSource { $params['rcid'] = $rc->mAttribs['rc_id']; } + $articlelink = Linker::linkKnown( + $rc->getTitle(), + null, + array(), + $params + ); if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) { - $articlelink = Linker::linkKnown( - $rc->getTitle(), - null, - array(), - $params - ); $articlelink = '<span class="history-deleted">' . $articlelink . '</span>'; - } else { - $articlelink = ' '. Linker::linkKnown( - $rc->getTitle(), - null, - array(), - $params - ); } # Bolden pages watched by this user if( $watched ) { $articlelink = "<strong class=\"mw-watched\">{$articlelink}</strong>"; } # RTL/LTR marker - $articlelink .= $this->getLang()->getDirMark(); + $articlelink .= $this->getLanguage()->getDirMark(); wfRunHooks( 'ChangesListInsertArticleLink', array(&$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched) ); @@ -364,51 +338,50 @@ class ChangesList extends ContextSource { /** * @param $s * @param $rc RecentChange - * @return void */ public function insertTimestamp( &$s, $rc ) { $s .= $this->message['semicolon-separator'] . - $this->getLang()->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; + $this->getLanguage()->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; } - /** Insert links to user page, user talk page and eventually a blocking link + /** + * Insert links to user page, user talk page and eventually a blocking link * - * @param $rc RecentChange + * @param &$s String HTML to update + * @param &$rc RecentChange */ public function insertUserRelatedLinks( &$s, &$rc ) { if( $this->isDeleted( $rc, Revision::DELETED_USER ) ) { $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>'; } else { - $s .= Linker::userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); + $s .= $this->getLanguage()->getDirMark() . Linker::userLink( $rc->mAttribs['rc_user'], + $rc->mAttribs['rc_user_text'] ); $s .= Linker::userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); } } - /** insert a formatted action + /** + * Insert a formatted action * * @param $rc RecentChange */ - public function insertAction( &$s, &$rc ) { - if( $rc->mAttribs['rc_type'] == RC_LOG ) { - if( $this->isDeleted( $rc, LogPage::DELETED_ACTION ) ) { - $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-event' ) . '</span>'; - } else { - $s .= ' '.LogPage::actionText( $rc->mAttribs['rc_log_type'], $rc->mAttribs['rc_log_action'], - $rc->getTitle(), $this->getSkin(), LogPage::extractParams( $rc->mAttribs['rc_params'] ), true, true ); - } - } + public function insertLogEntry( $rc ) { + $formatter = LogFormatter::newFromRow( $rc->mAttribs ); + $formatter->setShowUserToolLinks( true ); + $mark = $this->getLanguage()->getDirMark(); + return $formatter->getActionText() . " $mark" . $formatter->getComment(); } - /** insert a formatted comment - * + /** + * Insert a formatted comment * @param $rc RecentChange */ - public function insertComment( &$s, &$rc ) { + public function insertComment( $rc ) { if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) { if( $this->isDeleted( $rc, Revision::DELETED_COMMENT ) ) { - $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span>'; + return ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span>'; } else { - $s .= Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ); + return Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ); } } } @@ -430,7 +403,7 @@ class ChangesList extends ContextSource { if( $count > 0 ) { if( !isset( $cache[$count] ) ) { $cache[$count] = wfMsgExt( 'number_of_watching_users_RCview', - array('parsemag', 'escape' ), $this->getLang()->formatNum( $count ) ); + array('parsemag', 'escape' ), $this->getLanguage()->formatNum( $count ) ); } return $cache[$count]; } else { @@ -453,16 +426,22 @@ class ChangesList extends ContextSource { * field of this revision, if it's marked as deleted. * @param $rc RCCacheEntry * @param $field Integer + * @param $user User object to check, or null to use $wgUser * @return Boolean */ - public static function userCan( $rc, $field ) { + public static function userCan( $rc, $field, User $user = null ) { if( $rc->mAttribs['rc_type'] == RC_LOG ) { - return LogEventsList::userCanBitfield( $rc->mAttribs['rc_deleted'], $field ); + return LogEventsList::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $user ); } else { - return Revision::userCanBitfield( $rc->mAttribs['rc_deleted'], $field ); + return Revision::userCanBitfield( $rc->mAttribs['rc_deleted'], $field, $user ); } } + /** + * @param $link string + * @param $watched bool + * @return string + */ protected function maybeWatchedLink( $link, $watched = false ) { if( $watched ) { return '<strong class="mw-watched">' . $link . '</strong>'; @@ -473,7 +452,7 @@ class ChangesList extends ContextSource { /** Inserts a rollback link * - * @param $s + * @param $s string * @param $rc RecentChange */ public function insertRollback( &$s, &$rc ) { @@ -496,10 +475,9 @@ class ChangesList extends ContextSource { } /** - * @param $s + * @param $s string * @param $rc RecentChange * @param $classes - * @return */ public function insertTags( &$s, &$rc, &$classes ) { if ( empty($rc->mAttribs['ts_tags']) ) @@ -513,6 +491,18 @@ class ChangesList extends ContextSource { public function insertExtra( &$s, &$rc, &$classes ) { ## Empty, used for subclassers to add anything special. } + + protected function showAsUnpatrolled( RecentChange $rc ) { + $unpatrolled = false; + if ( !$rc->mAttribs['rc_patrolled'] ) { + if ( $this->getUser()->useRCPatrol() ) { + $unpatrolled = true; + } elseif ( $this->getUser()->useNPPatrol() && $rc->mAttribs['rc_new'] ) { + $unpatrolled = true; + } + } + return $unpatrolled; + } } @@ -528,8 +518,9 @@ class OldChangesList extends ChangesList { public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) { global $wgRCShowChangedSize; wfProfileIn( __METHOD__ ); + # Should patrol-related stuff be shown? - $unpatrolled = $this->getUser()->useRCPatrol() && !$rc->mAttribs['rc_patrolled']; + $unpatrolled = $this->showAsUnpatrolled( $rc ); $dateheader = ''; // $s now contains only <li>...</li>, for hooks' convenience. $this->insertDateHeader( $dateheader, $rc->mAttribs['rc_timestamp'] ); @@ -546,9 +537,8 @@ class OldChangesList extends ChangesList { } } - // Moved pages + // Moved pages (very very old, not supported anymore) if( $rc->mAttribs['rc_type'] == RC_MOVE || $rc->mAttribs['rc_type'] == RC_MOVE_OVER_REDIRECT ) { - $this->insertMove( $s, $rc ); // Log entries } elseif( $rc->mAttribs['rc_log_type'] ) { $logtitle = Title::newFromText( 'Log/'.$rc->mAttribs['rc_log_type'], NS_SPECIAL ); @@ -583,14 +573,17 @@ class OldChangesList extends ChangesList { $s .= "$cd . . "; } } - # User tool links - $this->insertUserRelatedLinks( $s, $rc ); - # LTR/RTL direction mark - $s .= $this->getLang()->getDirMark(); - # Log action text (if any) - $this->insertAction( $s, $rc ); - # Edit or log comment - $this->insertComment( $s, $rc ); + + if ( $rc->mAttribs['rc_type'] == RC_LOG ) { + $s .= $this->insertLogEntry( $rc ); + } else { + # User tool links + $this->insertUserRelatedLinks( $s, $rc ); + # LTR/RTL direction mark + $s .= $this->getLanguage()->getDirMark(); + $s .= $this->insertComment( $rc ); + } + # Tags $this->insertTags( $s, $rc, $classes ); # Rollback @@ -601,7 +594,7 @@ class OldChangesList extends ChangesList { # How many users watch this page if( $rc->numberofWatchingusers > 0 ) { $s .= ' ' . wfMsgExt( 'number_of_watching_users_RCview', - array( 'parsemag', 'escape' ), $this->getLang()->formatNum( $rc->numberofWatchingusers ) ); + array( 'parsemag', 'escape' ), $this->getLanguage()->formatNum( $rc->numberofWatchingusers ) ); } if( $this->watchlist ) { @@ -620,6 +613,9 @@ class OldChangesList extends ChangesList { * Generate a list of changes using an Enhanced system (uses javascript). */ class EnhancedChangesList extends ChangesList { + + protected $rc_cache; + /** * Add the JavaScript file for enhanced changeslist * @return String @@ -650,7 +646,7 @@ class EnhancedChangesList extends ChangesList { $curIdEq = array( 'curid' => $rc->mAttribs['rc_cur_id'] ); # If it's a new day, add the headline and flush the cache - $date = $this->getLang()->date( $rc->mAttribs['rc_timestamp'], true ); + $date = $this->getLanguage()->date( $rc->mAttribs['rc_timestamp'], true ); $ret = ''; if( $date != $this->lastdate ) { # Process current cache @@ -661,22 +657,14 @@ class EnhancedChangesList extends ChangesList { } # Should patrol-related stuff be shown? - if( $this->getUser()->useRCPatrol() ) { - $rc->unpatrolled = !$rc->mAttribs['rc_patrolled']; - } else { - $rc->unpatrolled = false; - } + $rc->unpatrolled = $this->showAsUnpatrolled( $rc ); $showdifflinks = true; # Make article link $type = $rc->mAttribs['rc_type']; $logType = $rc->mAttribs['rc_log_type']; - // Page moves + // Page moves, very old style, not supported anymore if( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) { - $msg = ( $type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir"; - $clink = wfMsg( $msg, Linker::linkKnown( $rc->getTitle(), null, - array(), array( 'redirect' => 'no' ) ), - Linker::linkKnown( $rc->getMovedToTitle() ) ); // New unpatrolled pages } elseif( $rc->unpatrolled && $type == RC_NEW ) { $clink = Linker::linkKnown( $rc->getTitle(), null, array(), @@ -685,34 +673,28 @@ class EnhancedChangesList extends ChangesList { } elseif( $type == RC_LOG ) { if( $logType ) { $logtitle = SpecialPage::getTitleFor( 'Log', $logType ); - $clink = '(' . Linker::linkKnown( $logtitle, - LogPage::logName( $logType ) ) . ')'; + $logpage = new LogPage( $logType ); + $logname = $logpage->getName()->escaped(); + $clink = '(' . Linker::linkKnown( $logtitle, $logname ) . ')'; } else { $clink = Linker::link( $rc->getTitle() ); } $watched = false; // Log entries (old format) and special pages } elseif( $rc->mAttribs['rc_namespace'] == NS_SPECIAL ) { - list( $specialName, $logtype ) = SpecialPageFactory::resolveAlias( $rc->mAttribs['rc_title'] ); - if ( $specialName == 'Log' ) { - # Log updates, etc - $logname = LogPage::logName( $logtype ); - $clink = '(' . Linker::linkKnown( $rc->getTitle(), $logname ) . ')'; - } else { - wfDebug( "Unexpected special page in recentchanges\n" ); - $clink = ''; - } + wfDebug( "Unexpected special page in recentchanges\n" ); + $clink = ''; // Edits } else { $clink = Linker::linkKnown( $rc->getTitle() ); } # Don't show unusable diff links - if ( !ChangesList::userCan($rc,Revision::DELETED_TEXT) ) { + if ( !ChangesList::userCan( $rc, Revision::DELETED_TEXT, $this->getUser() ) ) { $showdifflinks = false; } - $time = $this->getLang()->time( $rc->mAttribs['rc_timestamp'], true, true ); + $time = $this->getLanguage()->time( $rc->mAttribs['rc_timestamp'], true, true ); $rc->watched = $watched; $rc->link = $clink; $rc->timestamp = $time; @@ -738,13 +720,13 @@ class EnhancedChangesList extends ChangesList { if ( $type != RC_NEW ) { $curLink = $this->message['cur']; } else { - $curUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querycur ) ); + $curUrl = htmlspecialchars( $rc->getTitle()->getLinkURL( $querycur ) ); $curLink = "<a href=\"$curUrl\" tabindex=\"{$baseRC->counter}\">{$this->message['cur']}</a>"; } $diffLink = $this->message['diff']; } else { - $diffUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querydiff ) ); - $curUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querycur ) ); + $diffUrl = htmlspecialchars( $rc->getTitle()->getLinkURL( $querydiff ) ); + $curUrl = htmlspecialchars( $rc->getTitle()->getLinkURL( $querycur ) ); $diffLink = "<a href=\"$diffUrl\" tabindex=\"{$baseRC->counter}\">{$this->message['diff']}</a>"; $curLink = "<a href=\"$curUrl\" tabindex=\"{$baseRC->counter}\">{$this->message['cur']}</a>"; } @@ -806,9 +788,11 @@ class EnhancedChangesList extends ChangesList { # Add the namespace and title of the block as part of the class if ( $block[0]->mAttribs['rc_log_type'] ) { # Log entry - $classes = 'mw-collapsible mw-collapsed mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' . $block[0]->mAttribs['rc_log_type'] . '-' . $block[0]->mAttribs['rc_title'] ); + $classes = 'mw-collapsible mw-collapsed mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' + . $block[0]->mAttribs['rc_log_type'] . '-' . $block[0]->mAttribs['rc_title'] ); } else { - $classes = 'mw-collapsible mw-collapsed mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns' . $block[0]->mAttribs['rc_namespace'] . '-' . $block[0]->mAttribs['rc_title'] ); + $classes = 'mw-collapsible mw-collapsed mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns' + . $block[0]->mAttribs['rc_namespace'] . '-' . $block[0]->mAttribs['rc_title'] ); } $r = Html::openElement( 'table', array( 'class' => $classes ) ) . Html::openElement( 'tr' ); @@ -861,9 +845,9 @@ class EnhancedChangesList extends ChangesList { $users = array(); foreach( $userlinks as $userlink => $count) { $text = $userlink; - $text .= $this->getLang()->getDirMark(); + $text .= $this->getLanguage()->getDirMark(); if( $count > 1 ) { - $text .= ' (' . $this->getLang()->formatNum( $count ) . '×)'; + $text .= ' (' . $this->getLanguage()->formatNum( $count ) . '×)'; } array_push( $users, $text ); } @@ -903,20 +887,20 @@ class EnhancedChangesList extends ChangesList { $this->insertArticleLink( $r, $block[0], $block[0]->unpatrolled, $block[0]->watched ); } - $r .= $this->getLang()->getDirMark(); + $r .= $this->getLanguage()->getDirMark(); $queryParams['curid'] = $curId; # Changes message $n = count($block); static $nchanges = array(); if ( !isset( $nchanges[$n] ) ) { - $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape' ), $this->getLang()->formatNum( $n ) ); + $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape' ), $this->getLanguage()->formatNum( $n ) ); } # Total change link $r .= ' '; if( !$allLogs ) { $r .= '('; - if( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT ) ) { + if( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT, $this->getUser() ) ) { $r .= $nchanges[$n]; } elseif( $isnew ) { $r .= $nchanges[$n]; @@ -1005,7 +989,7 @@ class EnhancedChangesList extends ChangesList { if( $type == RC_LOG ) { $link = $rcObj->timestamp; # Revision link - } elseif( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) { + } elseif( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT, $this->getUser() ) ) { $link = '<span class="history-deleted">'.$rcObj->timestamp.'</span> '; } else { if ( $rcObj->unpatrolled && $type == RC_NEW) { @@ -1037,13 +1021,15 @@ class EnhancedChangesList extends ChangesList { $r .= $rcObj->getCharacterDifference() . ' . . ' ; } - # User links - $r .= $rcObj->userlink; - $r .= $rcObj->usertalklink; - // log action - $this->insertAction( $r, $rcObj ); - // log comment - $this->insertComment( $r, $rcObj ); + if ( $rcObj->mAttribs['rc_type'] == RC_LOG ) { + $r .= $this->insertLogEntry( $rcObj ); + } else { + # User links + $r .= $rcObj->userlink; + $r .= $rcObj->usertalklink; + $r .= $this->insertComment( $rcObj ); + } + # Rollback $this->insertRollback( $r, $rcObj ); # Tags @@ -1107,7 +1093,7 @@ class EnhancedChangesList extends ChangesList { * Enhanced RC ungrouped line. * * @param $rcObj RecentChange - * @return String: a HTML formated line (generated using $r) + * @return String: a HTML formatted line (generated using $r) */ protected function recentChangesBlockLine( $rcObj ) { global $wgRCShowChangedSize; @@ -1119,9 +1105,11 @@ class EnhancedChangesList extends ChangesList { $logType = $rcObj->mAttribs['rc_log_type']; if( $logType ) { # Log entry - $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' . $logType . '-' . $rcObj->mAttribs['rc_title'] ); + $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' + . $logType . '-' . $rcObj->mAttribs['rc_title'] ); } else { - $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns' . $rcObj->mAttribs['rc_namespace'] . '-' . $rcObj->mAttribs['rc_title'] ); + $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns' . + $rcObj->mAttribs['rc_namespace'] . '-' . $rcObj->mAttribs['rc_title'] ); } $r = Html::openElement( 'table', array( 'class' => $classes ) ) . Html::openElement( 'tr' ); @@ -1163,19 +1151,15 @@ class EnhancedChangesList extends ChangesList { if( $wgRCShowChangedSize && ($cd = $rcObj->getCharacterDifference()) ) { $r .= "$cd . . "; } - # User/talk - $r .= ' '.$rcObj->userlink . $rcObj->usertalklink; - # Log action (if any) - if( $logType ) { - if( $this->isDeleted($rcObj,LogPage::DELETED_ACTION) ) { - $r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>'; - } else { - $r .= ' ' . LogPage::actionText( $logType, $rcObj->mAttribs['rc_log_action'], $rcObj->getTitle(), - $this->getSkin(), LogPage::extractParams( $rcObj->mAttribs['rc_params'] ), true, true ); - } + + if ( $type == RC_LOG ) { + $r .= $this->insertLogEntry( $rcObj ); + } else { + $r .= ' '.$rcObj->userlink . $rcObj->usertalklink; + $r .= $this->insertComment( $rcObj ); + $r .= $this->insertRollback( $r, $rcObj ); } - $this->insertComment( $r, $rcObj ); - $this->insertRollback( $r, $rcObj ); + # Tags $classes = explode( ' ', $classes ); $this->insertTags( $r, $rcObj, $classes ); @@ -1219,6 +1203,7 @@ class EnhancedChangesList extends ChangesList { /** * Returns text for the end of RC * If enhanced RC is in use, returns pretty much all the text + * @return string */ public function endRecentChangesList() { return $this->recentChangesBlock() . parent::endRecentChangesList(); diff --git a/includes/Cookie.php b/includes/Cookie.php index 95a4599f..76739ccc 100644 --- a/includes/Cookie.php +++ b/includes/Cookie.php @@ -139,6 +139,10 @@ class Cookie { return $ret; } + /** + * @param $domain + * @return bool + */ protected function canServeDomain( $domain ) { if ( $domain == $this->domain || ( strlen( $domain ) > strlen( $this->domain ) @@ -151,20 +155,19 @@ class Cookie { return false; } + /** + * @param $path + * @return bool + */ protected function canServePath( $path ) { - if ( $this->path && substr_compare( $this->path, $path, 0, strlen( $this->path ) ) == 0 ) { - return true; - } - - return false; + return ( $this->path && substr_compare( $this->path, $path, 0, strlen( $this->path ) ) == 0 ); } + /** + * @return bool + */ protected function isUnExpired() { - if ( $this->isSessionKey || $this->expires > time() ) { - return true; - } - - return false; + return $this->isSessionKey || $this->expires > time(); } } diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 29d98d58..04450348 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -8,10 +8,10 @@ * To customize your installation, edit "LocalSettings.php". If you make * changes here, they will be lost on next upgrade of MediaWiki! * - * Note that since all these string interpolations are expanded - * before LocalSettings is included, if you localize something - * like $wgScriptPath, you must also localize everything that - * depends on it. + * In this file, variables whose default values depend on other + * variables are set to false. The actual default value of these variables + * will only be set in Setup.php, taking into account any custom settings + * performed in LocalSettings.php. * * Documentation is in the source and on: * http://www.mediawiki.org/wiki/Manual:Configuration_settings @@ -33,10 +33,10 @@ $wgConf = new SiteConfiguration; /** @endcond */ /** MediaWiki version number */ -$wgVersion = '1.18.3'; +$wgVersion = '1.19.0'; /** Name of the site. It must be changed in LocalSettings.php */ -$wgSitename = 'MediaWiki'; +$wgSitename = 'MediaWiki'; /** * URL of the server. @@ -49,7 +49,7 @@ $wgSitename = 'MediaWiki'; * This is usually detected correctly by MediaWiki. If MediaWiki detects the * wrong server, it will redirect incorrectly after you save a page. In that * case, set this variable to fix it. - * + * * If you want to use protocol-relative URLs on your wiki, set this to a * protocol-relative URL like '//example.com' and set $wgCanonicalServer * to a fully qualified URL. @@ -57,9 +57,9 @@ $wgSitename = 'MediaWiki'; $wgServer = WebRequest::detectServer(); /** - * Canonical URL of the server, to use in IRC feeds and notification e-mails. + * Canonical URL of the server, to use in IRC feeds and notification e-mails. * Must be fully qualified, even if $wgServer is protocol-relative. - * + * * Defaults to $wgServer, expanded to a fully qualified http:// URL if needed. */ $wgCanonicalServer = false; @@ -78,7 +78,7 @@ $wgCanonicalServer = false; * Other paths will be set to defaults based on it unless they are directly * set in LocalSettings.php */ -$wgScriptPath = '/wiki'; +$wgScriptPath = '/wiki'; /** * Whether to support URLs like index.php/Page_title These often break when PHP @@ -113,24 +113,24 @@ $wgScriptExtension = '.php'; /** * The URL path to index.php. * - * Defaults to "{$wgScriptPath}/index{$wgScriptExtension}". + * Will default to "{$wgScriptPath}/index{$wgScriptExtension}" in Setup.php */ -$wgScript = false; +$wgScript = false; /** * The URL path to redirect.php. This is a script that is used by the Nostalgia * skin. * - * Defaults to "{$wgScriptPath}/redirect{$wgScriptExtension}". + * Will default to "{$wgScriptPath}/redirect{$wgScriptExtension}" in Setup.php */ -$wgRedirectScript = false; ///< defaults to +$wgRedirectScript = false; /** * The URL path to load.php. * * Defaults to "{$wgScriptPath}/load{$wgScriptExtension}". */ -$wgLoadScript = false; +$wgLoadScript = false; /**@}*/ @@ -140,7 +140,6 @@ $wgLoadScript = false; * * These various web and file path variables are set to their defaults * in Setup.php if they are not explicitly set from LocalSettings.php. - * If you do override them, be sure to set them all! * * These will relatively rarely need to be set manually, unless you are * splitting style sheets or images outside the main document root. @@ -155,16 +154,16 @@ $wgLoadScript = false; */ /** - * The URL path of the skins directory. Defaults to "{$wgScriptPath}/skins" + * The URL path of the skins directory. Will default to "{$wgScriptPath}/skins" in Setup.php */ -$wgStylePath = false; +$wgStylePath = false; $wgStyleSheetPath = &$wgStylePath; /** * The URL path of the skins directory. Should not point to an external domain. * Defaults to "{$wgScriptPath}/skins". */ -$wgLocalStylePath = false; +$wgLocalStylePath = false; /** * The URL path of the extensions directory. @@ -174,7 +173,7 @@ $wgLocalStylePath = false; $wgExtensionAssetsPath = false; /** - * Filesystem stylesheets directory. Defaults to "{$IP}/skins" + * Filesystem stylesheets directory. Will default to "{$IP}/skins" in Setup.php */ $wgStyleDirectory = false; @@ -182,51 +181,56 @@ $wgStyleDirectory = false; * The URL path for primary article page views. This path should contain $1, * which is replaced by the article title. * - * Defaults to "{$wgScript}/$1" or "{$wgScript}?title=$1", depending on - * $wgUsePathInfo. + * Will default to "{$wgScript}/$1" or "{$wgScript}?title=$1" in Setup.php, + * depending on $wgUsePathInfo. */ -$wgArticlePath = false; +$wgArticlePath = false; /** - * The URL path for the images directory. Defaults to "{$wgScriptPath}/images" + * The URL path for the images directory. Will default to "{$wgScriptPath}/images" in Setup.php */ -$wgUploadPath = false; +$wgUploadPath = false; + +/** + * The maximum age of temporary (incomplete) uploaded files + */ +$wgUploadStashMaxAge = 6 * 3600; // 6 hours /** * The filesystem path of the images directory. Defaults to "{$IP}/images". */ -$wgUploadDirectory = false; +$wgUploadDirectory = false; /** * The URL path of the wiki logo. The logo size should be 135x135 pixels. - * Defaults to "{$wgStylePath}/common/images/wiki.png". + * Will default to "{$wgStylePath}/common/images/wiki.png" in Setup.php */ -$wgLogo = false; +$wgLogo = false; /** * The URL path of the shortcut icon. */ -$wgFavicon = '/favicon.ico'; +$wgFavicon = '/favicon.ico'; /** * The URL path of the icon for iPhone and iPod Touch web app bookmarks. * Defaults to no icon. */ -$wgAppleTouchIcon = false; +$wgAppleTouchIcon = false; /** * The local filesystem path to a temporary directory. This is not required to * be web accessible. * - * Defaults to "{$wgUploadDirectory}/tmp". + * Will default to "{$wgUploadDirectory}/tmp" in Setup.php */ -$wgTmpDirectory = false; +$wgTmpDirectory = false; /** * If set, this URL is added to the start of $wgUploadPath to form a complete * upload URL. */ -$wgUploadBaseUrl = ""; +$wgUploadBaseUrl = ''; /** * To enable remote on-demand scaling, set this to the thumbnail base URL. @@ -277,7 +281,7 @@ $wgDeletedDirectory = false; // Defaults to $wgUploadDirectory/deleted /** * Set this to true if you use img_auth and want the user to see details on why access failed. */ -$wgImgAuthDetails = false; +$wgImgAuthDetails = false; /** * If this is enabled, img_auth.php will not allow image access unless the wiki @@ -295,11 +299,19 @@ $wgImgAuthPublicTest = true; * * Properties required for all repos: * - class The class name for the repository. May come from the core or an extension. - * The core repository classes are LocalRepo, ForeignDBRepo, FSRepo. + * The core repository classes are FileRepo, LocalRepo, ForeignDBRepo. + * FSRepo is also supported for backwards compatibility. * - * - name A unique name for the repository (but $wgLocalFileRepo should be 'local'). + * - name A unique name for the repository (but $wgLocalFileRepo should be 'local'). + * The name should consist of alpha-numberic characters. + * - backend A file backend name (see $wgFileBackends). * * For most core repos: + * - zones Associative array of zone names that each map to an array with: + * container : backend container name the zone is in + * directory : root path within container for the zone + * Zones default to using <repo name>-<zone> as the + * container name and the container root as the zone directory. * - url Base public URL * - hashLevels The number of directory levels for hash-based division of files * - thumbScriptUrl The URL for thumb.php (optional, not recommended) @@ -349,6 +361,8 @@ $wgImgAuthPublicTest = true; * If you set $wgForeignFileRepos to an array of repostory structures, those will * be searched after the local file repo. * Otherwise, you will only have access to local media files. + * + * @see Setup.php for an example usage and default initialization. */ $wgLocalFileRepo = false; @@ -363,6 +377,27 @@ $wgForeignFileRepos = array(); $wgUseInstantCommons = false; /** + * File backend structure configuration. + * This is an array of file backend configuration arrays. + * Each backend configuration has the following parameters: + * 'name' : A unique name for the backend + * 'class' : The file backend class to use + * 'wikiId' : A unique string that identifies the wiki (container prefix) + * 'lockManager' : The name of a lock manager (see $wgLockManagers) + * Additional parameters are specific to the class used. + */ +$wgFileBackends = array(); + +/** + * Array of configuration arrays for each lock manager. + * Each backend configuration has the following parameters: + * 'name' : A unique name for the lock manger + * 'class' : The lock manger class to use + * Additional parameters are specific to the class used. + */ +$wgLockManagers = array(); + +/** * Show EXIF data, on by default if available. * Requires PHP's EXIF extension: http://www.php.net/manual/en/ref.exif.php * @@ -426,10 +461,10 @@ $wgAllowAsyncCopyUploads = false; * for non-specified types. * * For example: - * $wgMaxUploadSize = array( - * '*' => 250 * 1024, - * 'url' => 500 * 1024, - * ); + * $wgMaxUploadSize = array( + * '*' => 250 * 1024, + * 'url' => 500 * 1024, + * ); * Sets the maximum for all uploads to 250 kB except for upload-by-url, which * will have a maximum of 500 kB. * @@ -474,7 +509,7 @@ $wgSharedThumbnailScriptPath = false; * * Note that this variable may be ignored if $wgLocalFileRepo is set. */ -$wgHashedUploadDirectory = true; +$wgHashedUploadDirectory = true; /** * Set the following to false especially if you have a set of files that need to @@ -553,6 +588,13 @@ $wgCheckFileExtensions = true; */ $wgStrictFileExtensions = true; +/** + * Setting this to true will disable the upload system's checks for HTML/JavaScript. + * THIS IS VERY DANGEROUS on a publicly editable site, so USE wgGroupPermissions + * TO RESTRICT UPLOADING to only those that you trust + */ +$wgDisableUploadScriptChecks = false; + /** Warn if uploaded files are larger than this (in bytes), or false to disable*/ $wgUploadSizeWarning = false; @@ -586,6 +628,7 @@ $wgMediaHandlers = array( 'image/tiff' => 'TiffHandler', 'image/x-ms-bmp' => 'BmpHandler', 'image/x-bmp' => 'BmpHandler', + 'image/x-xcf' => 'XCFHandler', 'image/svg+xml' => 'SvgHandler', // official 'image/svg' => 'SvgHandler', // compat 'image/vnd.djvu' => 'DjVuHandler', // official @@ -601,9 +644,11 @@ $wgMediaHandlers = array( * * Use Image Magick instead of PHP builtin functions. */ -$wgUseImageMagick = false; +$wgUseImageMagick = false; /** The convert command shipped with ImageMagick */ -$wgImageMagickConvertCommand = '/usr/bin/convert'; +$wgImageMagickConvertCommand = '/usr/bin/convert'; +/** The identify command shipped with ImageMagick */ +$wgImageMagickIdentifyCommand = '/usr/bin/identify'; /** Sharpening parameter to ImageMagick */ $wgSharpenParameter = '0x0.4'; @@ -674,9 +719,17 @@ $wgSVGMetadataCutoff = 262144; $wgAllowTitlesInSVG = false; /** - * Don't thumbnail an image if it will use too much working memory. - * Default is 50 MB if decompressed to RGBA form, which corresponds to - * 12.5 million pixels or 3500x3500 + * The maximum number of pixels a source image can have if it is to be scaled + * down by a scaler that requires the full source image to be decompressed + * and stored in decompressed form, before the thumbnail is generated. + * + * This provides a limit on memory usage for the decompression side of the + * image scaler. The limit is used when scaling PNGs with any of the + * built-in image scalers, such as ImageMagick or GD. It is ignored for + * JPEGs with ImageMagick, and when using the VipsScaler extension. + * + * The default is 50 MB if decompressed to RGBA form, which corresponds to + * 12.5 million pixels or 3500x3500. */ $wgMaxImageArea = 1.25e7; /** @@ -751,7 +804,7 @@ $wgEnableAutoRotation = null; * $wgAntivirusSetup array. Set this to NULL to disable virus scanning. If not * null, every file uploaded will be scanned for viruses. */ -$wgAntivirus= null; +$wgAntivirus = null; /** * Configuration for different virus scanners. This an associative array of @@ -864,11 +917,11 @@ $wgTrivialMimeDetection = false; * array = ( 'rootElement' => 'associatedMimeType' ) */ $wgXMLMimeTypes = array( - 'http://www.w3.org/2000/svg:svg' => 'image/svg+xml', - 'svg' => 'image/svg+xml', - 'http://www.lysator.liu.se/~alla/dia/:diagram' => 'application/x-dia-diagram', - 'http://www.w3.org/1999/xhtml:html' => 'text/html', // application/xhtml+xml? - 'html' => 'text/html', // application/xhtml+xml? + 'http://www.w3.org/2000/svg:svg' => 'image/svg+xml', + 'svg' => 'image/svg+xml', + 'http://www.lysator.liu.se/~alla/dia/:diagram' => 'application/x-dia-diagram', + 'http://www.w3.org/1999/xhtml:html' => 'text/html', // application/xhtml+xml? + 'html' => 'text/html', // application/xhtml+xml? ); /** @@ -879,13 +932,14 @@ $wgXMLMimeTypes = array( * change it if you alter the array (see bug 8858). * This is the list of settings the user can choose from: */ -$wgImageLimits = array ( - array(320,240), - array(640,480), - array(800,600), - array(1024,768), - array(1280,1024), - array(10000,10000) ); +$wgImageLimits = array( + array( 320, 240 ), + array( 640, 480 ), + array( 800, 600 ), + array( 1024, 768 ), + array( 1280, 1024 ), + array( 10000, 10000 ) +); /** * Adjust thumbnails on image pages according to a user setting. In order to @@ -1037,7 +1091,7 @@ $wgPasswordReminderResendTime = 24; /** * The time, in seconds, when an emailed temporary password expires. */ -$wgNewPasswordExpiry = 3600 * 24 * 7; +$wgNewPasswordExpiry = 3600 * 24 * 7; /** * The time, in seconds, when an email confirmation email expires @@ -1057,7 +1111,7 @@ $wgUserEmailConfirmationTokenExpiry = 7 * 24 * 60 * 60; * "password" => password * </code> */ -$wgSMTP = false; +$wgSMTP = false; /** * Additional email parameters, will be passed as the last argument to mail() call. @@ -1069,7 +1123,7 @@ $wgAdditionalMailParams = null; * True: from page editor if s/he opted-in. False: Enotif mails appear to come * from $wgEmergencyContact */ -$wgEnotifFromEditor = false; +$wgEnotifFromEditor = false; // TODO move UPO to preferences probably ? # If set to true, users get a corresponding option in their preferences and can choose to enable or disable at their discretion @@ -1081,30 +1135,30 @@ $wgEnotifFromEditor = false; * highly recommended. It prevents MediaWiki from being used as an open spam * relay. */ -$wgEmailAuthentication = true; +$wgEmailAuthentication = true; /** * Allow users to enable email notification ("enotif") on watchlist changes. */ -$wgEnotifWatchlist = false; +$wgEnotifWatchlist = false; /** * Allow users to enable email notification ("enotif") when someone edits their * user talk page. */ -$wgEnotifUserTalk = false; +$wgEnotifUserTalk = false; /** * Set the Reply-to address in notifications to the editor's address, if user * allowed this in the preferences. */ -$wgEnotifRevealEditorAddress = false; +$wgEnotifRevealEditorAddress = false; /** * Send notification mails on minor edits to watchlist pages. This is enabled * by default. Does not affect user talk notifications. */ -$wgEnotifMinorEdits = true; +$wgEnotifMinorEdits = true; /** * Send a generic mail instead of a personalised mail for each user. This @@ -1134,7 +1188,7 @@ $wgEnotifUseRealName = false; /** * Array of usernames who will be sent a notification email for every change - * which occurs on a wiki. + * which occurs on a wiki. Users will not be notified of their own changes. */ $wgUsersNotifiedOnAllChanges = array(); @@ -1146,17 +1200,17 @@ $wgUsersNotifiedOnAllChanges = array(); * @{ */ /** Database host name or IP address */ -$wgDBserver = 'localhost'; +$wgDBserver = 'localhost'; /** Database port number (for PostgreSQL) */ -$wgDBport = 5432; +$wgDBport = 5432; /** Name of the database */ -$wgDBname = 'my_wiki'; +$wgDBname = 'my_wiki'; /** Database username */ -$wgDBuser = 'wikiuser'; +$wgDBuser = 'wikiuser'; /** Database user's password */ -$wgDBpassword = ''; +$wgDBpassword = ''; /** Database type */ -$wgDBtype = 'mysql'; +$wgDBtype = 'mysql'; /** Separate username for maintenance tasks. Leave as null to use the default. */ $wgDBadminuser = null; @@ -1169,12 +1223,12 @@ $wgDBadminpassword = null; * selected database type (eg SearchMySQL), or set to a class * name to override to a custom search engine. */ -$wgSearchType = null; +$wgSearchType = null; /** Table name prefix */ -$wgDBprefix = ''; +$wgDBprefix = ''; /** MySQL table options to use during installation or update */ -$wgDBTableOptions = 'ENGINE=InnoDB'; +$wgDBTableOptions = 'ENGINE=InnoDB'; /** * SQL Mode - default is turning off all modes, including strict, if set. @@ -1185,10 +1239,10 @@ $wgDBTableOptions = 'ENGINE=InnoDB'; $wgSQLMode = ''; /** Mediawiki schema */ -$wgDBmwschema = 'mediawiki'; +$wgDBmwschema = 'mediawiki'; /** To override default SQLite data directory ($docroot/../data) */ -$wgSQLiteDataDir = ''; +$wgSQLiteDataDir = ''; /** * Make all database connections secretly go to localhost. Fool the load balancer @@ -1215,7 +1269,7 @@ $wgAllDBsAreLocalhost = false; * $wgSharedPrefix is the table prefix for the shared database. It defaults to * $wgDBprefix. */ -$wgSharedDB = null; +$wgSharedDB = null; /** @see $wgSharedDB */ $wgSharedPrefix = false; @@ -1265,7 +1319,7 @@ $wgSharedTables = array( 'user', 'user_properties' ); * up, we at Wikimedia set read_only=1 in my.cnf on all our DB servers, even * our masters, and then set read_only=0 on masters at runtime. */ -$wgDBservers = false; +$wgDBservers = false; /** * Load balancer factory configuration @@ -1277,13 +1331,13 @@ $wgDBservers = false; * The LBFactory_Multi class is provided for this purpose, please see * includes/db/LBFactory_Multi.php for configuration information. */ -$wgLBFactoryConf = array( 'class' => 'LBFactory_Simple' ); +$wgLBFactoryConf = array( 'class' => 'LBFactory_Simple' ); /** How long to wait for a slave to catch up to the master */ $wgMasterWaitTimeout = 10; /** File to log database errors to */ -$wgDBerrorLog = false; +$wgDBerrorLog = false; /** When to give an error message */ $wgDBClusterTimeout = 10; @@ -1295,10 +1349,7 @@ $wgDBClusterTimeout = 10; $wgDBAvgStatusPoll = 2000; /** Set to true if using InnoDB tables */ -$wgDBtransactions = false; -/** Set to true for compatibility with extensions that might be checking. - * MySQL 3.23.x is no longer supported. */ -$wgDBmysql4 = true; +$wgDBtransactions = false; /** * Set to true to engage MySQL 4.1/5.0 charset-related features; @@ -1316,7 +1367,7 @@ $wgDBmysql4 = true; * characters (those not in the Basic Multilingual Plane) unless MySQL * has enhanced their Unicode support. */ -$wgDBmysql5 = false; +$wgDBmysql5 = false; /** * Other wikis on this site, can be administered from a single developer @@ -1437,7 +1488,7 @@ $wgAntiLockFlags = 0; /** * Maximum article size in kilobytes */ -$wgMaxArticleSize = 2048; +$wgMaxArticleSize = 2048; /** * The minimum amount of memory that MediaWiki "needs"; MediaWiki will try to @@ -1473,7 +1524,7 @@ $wgCacheDirectory = false; * - CACHE_NONE: Do not cache * - CACHE_DB: Store cache objects in the DB * - CACHE_MEMCACHED: MemCached, must specify servers in $wgMemCachedServers - * - CACHE_ACCEL: eAccelerator, APC, XCache or WinCache + * - CACHE_ACCEL: APC, XCache or WinCache * - CACHE_DBA: Use PHP's DBA extension to store in a DBM-style * database. This is slow, and is not recommended for * anything other than debugging. @@ -1525,7 +1576,6 @@ $wgObjectCaches = array( CACHE_ACCEL => array( 'factory' => 'ObjectCache::newAccelerator' ), CACHE_MEMCACHED => array( 'factory' => 'ObjectCache::newMemcached' ), - 'eaccelerator' => array( 'class' => 'eAccelBagOStuff' ), 'apc' => array( 'class' => 'APCBagOStuff' ), 'xcache' => array( 'class' => 'XCacheBagOStuff' ), 'wincache' => array( 'class' => 'WinCacheBagOStuff' ), @@ -1560,7 +1610,7 @@ $wgSessionsInMemcached = false; $wgSessionHandler = null; /** If enabled, will send MemCached debugging information to $wgDebugLogFile */ -$wgMemCachedDebug = false; +$wgMemCachedDebug = false; /** The list of MemCached servers and port numbers */ $wgMemCachedServers = array( '127.0.0.1:11000' ); @@ -1625,7 +1675,7 @@ $wgLocalisationCacheConf = array( ); /** Allow client-side caching of pages */ -$wgCachePages = true; +$wgCachePages = true; /** * Set this to current time to invalidate all prior cached pages. Affects both @@ -1647,12 +1697,14 @@ $wgStyleVersion = '303'; * This will cache static pages for non-logged-in users to reduce * database traffic on public sites. * Must set $wgShowIPinHeader = false + * ResourceLoader requests to default language and skins are cached + * as well as single module requests. */ $wgUseFileCache = false; /** * Directory where the cached page will be saved. - * Defaults to "$wgCacheDirectory/html". + * Will default to "{$wgUploadDirectory}/cache" in Setup.php */ $wgFileCacheDirectory = false; @@ -1861,20 +1913,24 @@ $wgExtraLanguageNames = array(); /** * List of language codes that don't correspond to an actual language. - * These codes are leftoffs from renames, or other legacy things. - * Also, qqq is a dummy "language" for documenting messages. + * These codes are mostly leftoffs from renames, or other legacy things. + * This array makes them not appear as a selectable language on the installer, + * and excludes them when running the transstat.php script. */ $wgDummyLanguageCodes = array( - 'als', - 'bat-smg', - 'be-x-old', - 'fiu-vro', - 'iu', - 'nb', - 'qqq', - 'qqx', - 'roa-rup', - 'simple', + 'als' => 'gsw', + 'bat-smg' => 'sgs', + 'be-x-old' => 'be-tarask', + 'bh' => 'bho', + 'fiu-vro' => 'vro', + 'no' => 'nb', + 'qqq' => 'qqq', # Used for message documentation. + 'qqx' => 'qqx', # Used for viewing message keys. + 'roa-rup' => 'rup', + 'simple' => 'en', + 'zh-classical' => 'lzh', + 'zh-min-nan' => 'nan', + 'zh-yue' => 'yue', ); /** @@ -1884,7 +1940,7 @@ $wgDummyLanguageCodes = array( * This historic feature is one of the first that was added by former MediaWiki * team leader Brion Vibber, and is used to support the Esperanto x-system. */ -$wgEditEncoding = ''; +$wgEditEncoding = ''; /** * Set this to true to replace Arabic presentation forms with their standard @@ -1929,7 +1985,7 @@ $wgAllUnicodeFixes = false; * user names, etc still must be converted en masse in the database before * continuing as a UTF-8 wiki. */ -$wgLegacyEncoding = false; +$wgLegacyEncoding = false; /** * Browser Blacklist for unicode non compliant browsers. Contains a list of @@ -2014,7 +2070,7 @@ $wgUseDatabaseMessages = true; /** * Expiry time for the message cache key */ -$wgMsgCacheExpiry = 86400; +$wgMsgCacheExpiry = 86400; /** * Maximum entry size in the message cache, in bytes @@ -2120,6 +2176,17 @@ $wgLocaltimezone = null; */ $wgLocalTZoffset = null; +/** + * If set to true, this will roll back a few bug fixes introduced in 1.19, + * emulating the 1.18 behaviour, to avoid introducing bug 34832. In 1.19, + * language variant conversion is disabled in interface messages. Setting this + * to true re-enables it. + * + * This variable should be removed (implicitly false) in 1.20 or earlier. + */ +$wgBug34832TransitionalRollback = true; + + /** @} */ # End of language/charset settings /*************************************************************************//** @@ -2188,6 +2255,11 @@ $wgAllowRdfaAttributes = false; $wgAllowMicrodataAttributes = false; /** + * Cleanup as much presentational html like valign -> css vertical-align as we can + */ +$wgCleanupPresentationalAttributes = true; + +/** * Should we try to make our HTML output well-formed XML? If set to false, * output will be a few bytes shorter, and the HTML will arguably be more * readable. If set to true, life will be much easier for the authors of @@ -2221,8 +2293,9 @@ $wgXhtmlNamespaces = array(); /** * Show IP address, for non-logged in users. It's necessary to switch this off * for some forms of caching. + * Will disable file cache. */ -$wgShowIPinHeader = true; +$wgShowIPinHeader = true; /** * Site notice shown at the top of each page @@ -2320,13 +2393,6 @@ $wgUseSiteJs = true; $wgUseSiteCss = true; /** - * Set to false to disable application of access keys and tooltips, - * eg to avoid keyboard conflicts with system keys or as a low-level - * optimization. - */ -$wgEnableTooltipsAndAccesskeys = true; - -/** * Break out of framesets. This can be used to prevent clickjacking attacks, * or to prevent external sites from framing your site with ads. */ @@ -2403,7 +2469,7 @@ $wgFooterIcons = array( "poweredby" => array( "mediawiki" => array( "src" => null, // Defaults to "$wgStylePath/common/images/poweredby_mediawiki_88x31.png" - "url" => "http://www.mediawiki.org/", + "url" => "//www.mediawiki.org/", "alt" => "Powered by MediaWiki", ) ), @@ -2431,11 +2497,6 @@ $wgVectorUseSimpleSearch = false; $wgVectorUseIconWatch = false; /** - * Show the name of the current variant as a label in the variants drop-down menu - */ -$wgVectorShowVariantName = false; - -/** * Display user edit counts in various prominent places. */ $wgEdititis = false; @@ -2448,6 +2509,18 @@ $wgEdititis = false; */ $wgBetterDirectionality = true; +/** + * Some web hosts attempt to rewrite all responses with a 404 (not found) + * status code, mangling or hiding MediaWiki's output. If you are using such a + * host, you should start looking for a better one. While you're doing that, + * set this to false to convert some of MediaWiki's 404 responses to 200 so + * that the generated error pages can be seen. + * + * In cases where for technical reasons it is more important for MediaWiki to + * send the correct status code than for the body to be transmitted intact, + * this configuration variable is ignored. + */ +$wgSend404Code = true; /** @} */ # End of output format settings } @@ -2471,7 +2544,20 @@ $wgBetterDirectionality = true; */ $wgResourceModules = array(); -/* +/** + * Extensions should register foreign module sources here. 'local' is a + * built-in source that is not in this array, but defined by + * ResourceLoader::__construct() so that it cannot be unset. + * + * Example: + * $wgResourceLoaderSources['foo'] = array( + * 'loadScript' => 'http://example.org/w/load.php', + * 'apiScript' => 'http://example.org/w/api.php' + * ); + */ +$wgResourceLoaderSources = array(); + +/** * Default 'remoteBasePath' value for resource loader modules. * If not set, then $wgScriptPath will be used as a fallback. */ @@ -2526,6 +2612,19 @@ $wgResourceLoaderMinifierMaxLineLength = 1000; $wgIncludeLegacyJavaScript = true; /** + * Whether to preload the mediawiki.util module as blocking module in the top queue. + * Before MediaWiki 1.19, modules used to load slower/less asynchronous which allowed + * modules to lack dependencies on 'popular' modules that were likely loaded already. + * This setting is to aid scripts during migration by providing mediawiki.util + * unconditionally (which was the most commonly missed dependency). + * It doesn't cover all missing dependencies obviously but should fix most of them. + * This should be removed at some point after site/user scripts have been fixed. + * Enable this if your wiki has a large amount of user/site scripts that are lacking + * dependencies. + */ +$wgPreloadJavaScriptMwUtil = false; + +/** * Whether or not to assing configuration variables to the global window object. * If this is set to false, old code using deprecated variables like: * " if ( window.wgRestrictionEdit ) ..." @@ -2570,6 +2669,13 @@ $wgResourceLoaderValidateJS = true; */ $wgResourceLoaderValidateStaticJS = false; +/** + * If set to true, asynchronous loading of bottom-queue scripts in the <head> + * will be enabled. This is an experimental feature that's supposed to make + * JavaScript load faster. + */ +$wgResourceLoaderExperimentalAsyncLoading = false; + /** @} */ # End of resource loader settings } @@ -2582,7 +2688,7 @@ $wgResourceLoaderValidateStaticJS = false; * Name of the project namespace. If left set to false, $wgSitename will be * used instead. */ -$wgMetaNamespace = false; +$wgMetaNamespace = false; /** * Name of the project talk namespace. @@ -2607,12 +2713,12 @@ $wgMetaNamespaceTalk = false; * Custom namespaces should start at 100 to avoid conflicting with standard * namespaces, and should always follow the even/odd main/talk pattern. */ -#$wgExtraNamespaces = -# array(100 => "Hilfe", -# 101 => "Hilfe_Diskussion", -# 102 => "Aide", -# 103 => "Discussion_Aide" -# ); +# $wgExtraNamespaces = array( +# 100 => "Hilfe", +# 101 => "Hilfe_Diskussion", +# 102 => "Aide", +# 103 => "Discussion_Aide" +# ); $wgExtraNamespaces = array(); /** @@ -2670,7 +2776,7 @@ $wgLegalTitleChars = " %!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+"; /** * The interwiki prefix of the current wiki, or false if it doesn't have one. */ -$wgLocalInterwiki = false; +$wgLocalInterwiki = false; /** * Expiry time for cache of interwiki table @@ -2922,7 +3028,7 @@ $wgTidyInternal = extension_loaded( 'tidy' ); $wgDebugTidy = false; /** Allow raw, unchecked HTML in <html>...</html> sections. - * THIS IS VERY DANGEROUS on a publically editable site, so USE wgGroupPermissions + * THIS IS VERY DANGEROUS on a publicly editable site, so USE wgGroupPermissions * TO RESTRICT EDITING to only those that you trust */ $wgRawHtml = false; @@ -2988,7 +3094,7 @@ $wgPreprocessorCacheThreshold = 1000; $wgEnableScaryTranscluding = false; /** - * Expiry time for interwiki transclusion + * (see next option $wgGlobalDatabase). */ $wgTranscludeCacheExpiry = 3600; @@ -3074,13 +3180,13 @@ $wgPasswordResetRoutes = array( /** * Maximum number of Unicode characters in signature */ -$wgMaxSigChars = 255; +$wgMaxSigChars = 255; /** * Maximum number of bytes in username. You want to run the maintenance * script ./maintenance/checkUsernames.php once you have changed this value. */ -$wgMaxNameChars = 255; +$wgMaxNameChars = 255; /** * Array of usernames which may not be registered or logged in from @@ -3091,6 +3197,7 @@ $wgReservedUsernames = array( 'Conversion script', // Used for the old Wikipedia software upgrade 'Maintenance script', // Maintenance scripts which perform editing, image import script 'Template namespace initialisation script', // Used in 1.2->1.3 upgrade + 'ScriptImporter', // Default user name used by maintenance/importSiteScripts.php 'msg:double-redirect-fixer', // Automatic double redirect fix 'msg:usermessage-editor', // Default user for leaving user messages 'msg:proxyblocker', // For Special:Blockme @@ -3260,7 +3367,7 @@ $wgAllowPrefChange = array(); * http://lists.wikimedia.org/pipermail/wikitech-l/2010-October/050065.html * @since 1.17 */ -$wgSecureLogin = false; +$wgSecureLogin = false; /** @} */ # end user accounts } @@ -3272,15 +3379,15 @@ $wgSecureLogin = false; /** * Number of seconds before autoblock entries expire. Default 86400 = 1 day. */ -$wgAutoblockExpiry = 86400; +$wgAutoblockExpiry = 86400; /** * Set this to true to allow blocked users to edit their own user talk page. */ -$wgBlockAllowsUTEdit = false; +$wgBlockAllowsUTEdit = false; /** Allow sysops to ban users from accessing Emailuser */ -$wgSysopEmailBans = true; +$wgSysopEmailBans = true; /** * Limits on the possible sizes of range blocks. @@ -3329,6 +3436,12 @@ $wgEmailConfirmToEdit = false; /** * Permission keys given to users in each group. + * This is an array where the keys are all groups and each value is an + * array of the format (right => boolean). + * + * The second format is used to support per-namespace permissions. + * Note that this feature does not fully work for all permission types. + * * All users are implicitly in the '*' group including anonymous visitors; * logged-in users are all implicitly in the 'user' group. These will be * combined with the permissions of all groups that a given user is listed @@ -3342,7 +3455,7 @@ $wgEmailConfirmToEdit = false; * Functionality to make pages inaccessible has not been extensively tested * for security. Use at your own risk! * - * This replaces wgWhitelistAccount and wgWhitelistEdit + * This replaces $wgWhitelistAccount and $wgWhitelistEdit */ $wgGroupPermissions = array(); @@ -3360,7 +3473,7 @@ $wgGroupPermissions['*']['writeapi'] = true; $wgGroupPermissions['user']['move'] = true; $wgGroupPermissions['user']['move-subpages'] = true; $wgGroupPermissions['user']['move-rootuserpages'] = true; // can move root userpages -//$wgGroupPermissions['user']['movefile'] = true; // Disabled for now due to possible bugs and security concerns +$wgGroupPermissions['user']['movefile'] = true; $wgGroupPermissions['user']['read'] = true; $wgGroupPermissions['user']['edit'] = true; $wgGroupPermissions['user']['createpage'] = true; @@ -3424,7 +3537,6 @@ $wgGroupPermissions['sysop']['movefile'] = true; $wgGroupPermissions['sysop']['unblockself'] = true; $wgGroupPermissions['sysop']['suppressredirect'] = true; #$wgGroupPermissions['sysop']['mergehistory'] = true; -#$wgGroupPermissions['sysop']['trackback'] = true; // Permission to change users' group assignments $wgGroupPermissions['bureaucrat']['userrights'] = true; @@ -3602,7 +3714,7 @@ $wgAutopromoteOnce = array( 'onView' => array() ); -/* +/** * Put user rights log entries for autopromotion in recent changes? * @since 1.18 */ @@ -3669,6 +3781,7 @@ $wgSummarySpamRegex = array(); * - false : let it through * * @deprecated since 1.17 Use hooks. See SpamBlacklist extension. + * @var $wgFilterCallback bool|string|Closure */ $wgFilterCallback = false; @@ -3685,7 +3798,20 @@ $wgEnableDnsBlacklist = false; $wgEnableSorbs = false; /** - * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true + * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true. This is an + * array of either a URL or an array with the URL and a key (should the blacklist + * require a key). For example: + * @code + * $wgDnsBlacklistUrls = array( + * // String containing URL + * 'http.dnsbl.sorbs.net', + * // Array with URL and key, for services that require a key + * array( 'dnsbl.httpbl.net', 'mykey' ), + * // Array with just the URL. While this works, it is recommended that you + * // just use a string as shown above + * array( 'opm.tornevall.org' ) + * ); + * @endcode * @since 1.16 */ $wgDnsBlacklistUrls = array( 'http.dnsbl.sorbs.net.' ); @@ -3751,6 +3877,12 @@ $wgRateLimitsExcludedIPs = array(); $wgPutIPinRC = true; /** + * Integer defining default number of entries to show on + * special pages which are query-pages such as Special:Whatlinkshere. + */ +$wgQueryPageDefaultLimit = 50; + +/** * Limit password attempts to X attempts per Y seconds per IP per account. * Requires memcached. */ @@ -3796,7 +3928,7 @@ $wgProxyKey = false; /** * Default cookie expiration time. Setting to 0 makes all cookies session-only. */ -$wgCookieExpiration = 30*86400; +$wgCookieExpiration = 180*86400; /** * Set to set an explicit domain on the login cookies eg, "justthis.domain.org" @@ -3888,26 +4020,26 @@ $wgUseTeX = false; * The debug log file should be not be publicly accessible if it is used, as it * may contain private data. */ -$wgDebugLogFile = ''; +$wgDebugLogFile = ''; /** * Prefix for debug log lines */ -$wgDebugLogPrefix = ''; +$wgDebugLogPrefix = ''; /** * If true, instead of redirecting, show a page with a link to the redirect * destination. This allows for the inspection of PHP error messages, and easy * resubmission of form data. For developer use only. */ -$wgDebugRedirects = false; +$wgDebugRedirects = false; /** * If true, log debugging data from action=raw and load.php. * This is normally false to avoid overlapping debug entries due to gen=css and * gen=js requests. */ -$wgDebugRawPage = false; +$wgDebugRawPage = false; /** * Send debug data to an HTML comment in the output. @@ -3917,12 +4049,12 @@ $wgDebugRawPage = false; * contains private data for the current user. But it's not ideal for development * use since data is lost on fatal errors and redirects. */ -$wgDebugComments = false; +$wgDebugComments = false; /** * Write SQL queries to the debug log */ -$wgDebugDumpSql = false; +$wgDebugDumpSql = false; /** * Set to an array of log group keys to filenames. @@ -3930,17 +4062,18 @@ $wgDebugDumpSql = false; * of the regular $wgDebugLogFile. Useful for enabling selective logging * in production. */ -$wgDebugLogGroups = array(); +$wgDebugLogGroups = array(); /** * Display debug data at the bottom of the main content area. * * Useful for developers and technical users trying to working on a closed wiki. */ -$wgShowDebug = false; +$wgShowDebug = false; /** * Prefix debug messages with relative timestamp. Very-poor man's profiler. + * Since 1.19 also includes memory usage. */ $wgDebugTimestamps = false; @@ -3952,14 +4085,14 @@ $wgDebugPrintHttpHeaders = true; /** * Show the contents of $wgHooks in Special:Version */ -$wgSpecialVersionShowHooks = false; +$wgSpecialVersionShowHooks = false; /** * Whether to show "we're sorry, but there has been a database error" pages. * Displaying errors aids in debugging, but may display information useful * to an attacker. */ -$wgShowSQLErrors = false; +$wgShowSQLErrors = false; /** * If set to true, uncaught exceptions will print a complete stack trace @@ -3985,6 +4118,13 @@ $wgShowHostnames = false; */ $wgDevelopmentWarnings = false; +/** + * Release limitation to wfDeprecated warnings, if set to a release number + * development warnings will not be generated for deprecations added in releases + * after the limit. + */ +$wgDeprecationReleaseLimit = '1.17'; + /** Only record profiling info for pages that took longer than this */ $wgProfileLimit = 0.0; @@ -4025,7 +4165,7 @@ $wgUDPProfilerPort = '3811'; $wgDebugProfiling = false; /** Output debug message on every wfProfileIn/wfProfileOut */ -$wgDebugFunctionEntry = 0; +$wgDebugFunctionEntry = false; /** * Destination for wfIncrStats() data... @@ -4049,14 +4189,6 @@ $wgAggregateStatsID = false; $wgDisableCounters = false; /** - * Support blog-style "trackbacks" for articles. See - * http://www.sixapart.com/pronet/docs/trackback_spec for details. - * - * If enabling this, you also need to grant the 'trackback' right to a group - */ -$wgUseTrackbacks = false; - -/** * Parser test suite files to be run by parserTests.php when no specific * filename is passed to it. * @@ -4084,6 +4216,36 @@ $wgParserTestFiles = array( * ); */ $wgParserTestRemote = false; + +/** + * Allow running of javascript test suites via [[Special:JavaScriptTest]] (such as QUnit). + */ +$wgEnableJavaScriptTest = false; + +/** + * Configuration for javascript testing. + */ +$wgJavaScriptTestConfig = array( + 'qunit' => array( + 'documentation' => '//www.mediawiki.org/wiki/Manual:JavaScript_unit_testing', + ), +); + + +/** + * Overwrite the caching key prefix with custom value. + * @since 1.19 + */ +$wgCachePrefix = false; + +/** + * Display the new debugging toolbar. This also enables profiling on database + * queries and other useful output. + * Will disable file cache. + * + * @since 1.19 + */ +$wgDebugToolbar = false; /** @} */ # end of profiling, testing and debugging } @@ -4174,7 +4336,7 @@ $wgDisableSearchUpdate = false; * </code> */ $wgNamespacesToBeSearchedDefault = array( - NS_MAIN => true, + NS_MAIN => true, ); /** @@ -4184,8 +4346,8 @@ $wgNamespacesToBeSearchedDefault = array( * Same format as $wgNamespacesToBeSearchedDefault */ $wgNamespacesToBeSearchedHelp = array( - NS_PROJECT => true, - NS_HELP => true, + NS_PROJECT => true, + NS_HELP => true, ); /** @@ -4228,6 +4390,27 @@ $wgUseTwoButtonsSearchForm = true; */ $wgSitemapNamespaces = false; +/** + * Custom namespace priorities for sitemaps. Setting this will allow you to + * set custom priorities to namsepaces when sitemaps are generated using the + * maintenance/generateSitemap.php script. + * + * This should be a map of namespace IDs to priority + * Example: + * $wgSitemapNamespacesPriorities = array( + * NS_USER => '0.9', + * NS_HELP => '0.0', + * ); + */ +$wgSitemapNamespacesPriorities = false; + +/** + * If true, searches for IP addresses will be redirected to that IP's + * contributions page. E.g. searching for "1.2.3.4" will redirect to + * [[Special:Contributions/1.2.3.4]] + */ +$wgEnableSearchContributorsByIP = true; + /** @} */ # end of search settings /************************************************************************//** @@ -4252,7 +4435,7 @@ $wgDiff = '/usr/bin/diff'; * can specify namespaces of pages they have special treatment for */ $wgPreviewOnOpenNamespaces = array( - NS_CATEGORY => true + NS_CATEGORY => true ); /** @@ -4313,16 +4496,16 @@ $wgMaintenanceScripts = array(); * still be possible. To prevent database writes completely, use the read_only * option in MySQL. */ -$wgReadOnly = null; +$wgReadOnly = null; /** * If this lock file exists (size > 0), the wiki will be forced into read-only mode. * Its contents will be shown to users as part of the read-only warning * message. * - * Defaults to "{$wgUploadDirectory}/lock_yBgMBwiR". + * Will default to "{$wgUploadDirectory}/lock_yBgMBwiR" in Setup.php */ -$wgReadOnlyFile = false; +$wgReadOnlyFile = false; /** * When you run the web-based upgrade utility, it will tell you what to set @@ -4363,7 +4546,7 @@ $wgRCFilterByAge = false; * Special:Recentchangeslinked pages. */ $wgRCLinkLimits = array( 50, 100, 250, 500 ); -$wgRCLinkDays = array( 1, 3, 7, 14, 30 ); +$wgRCLinkDays = array( 1, 3, 7, 14, 30 ); /** * Send recent changes updates via UDP. The updates will be formatted for IRC. @@ -4454,23 +4637,23 @@ $wgFeedClasses = array( $wgAdvertisedFeedTypes = array( 'atom' ); /** Show watching users in recent changes, watchlist and page history views */ -$wgRCShowWatchingUsers = false; # UPO +$wgRCShowWatchingUsers = false; # UPO /** Show watching users in Page views */ -$wgPageShowWatchingUsers = false; +$wgPageShowWatchingUsers = false; /** Show the amount of changed characters in recent changes */ -$wgRCShowChangedSize = true; +$wgRCShowChangedSize = true; /** * If the difference between the character counts of the text * before and after the edit is below that value, the value will be * highlighted on the RC page. */ -$wgRCChangedSizeThreshold = 500; +$wgRCChangedSizeThreshold = 500; /** * Show "Updated (since my last visit)" marker in RC view, watchlist and history * view for watched pages with new changes */ -$wgShowUpdatedMarker = true; +$wgShowUpdatedMarker = true; /** * Disable links to talk pages of anonymous users (IPs) in listings on special @@ -4593,7 +4776,7 @@ $wgExportMaxHistory = 0; /** * Return distinct author list (when not returning full history) */ -$wgExportAllowListContributors = false ; +$wgExportAllowListContributors = false; /** * If non-zero, Special:Export accepts a "pagelink-depth" parameter @@ -4613,6 +4796,11 @@ $wgExportMaxLinkDepth = 0; */ $wgExportFromNamespaces = false; +/** +* Whether to allow exporting the entire wiki into a single file +*/ +$wgExportAllowAll = false; + /** @} */ # end of import/export } /*************************************************************************//** @@ -4643,12 +4831,6 @@ $wgExtensionFunctions = array(); $wgExtensionMessagesFiles = array(); /** - * Aliases for special pages provided by extensions. - * @deprecated since 1.16 Use $specialPageAliases in a file referred to by $wgExtensionMessagesFiles - */ -$wgExtensionAliasesFiles = array(); - -/** * Parser output hooks. * This is an associative array where the key is an extension-defined tag * (typically the extension name), and the value is a PHP callback. @@ -4691,13 +4873,13 @@ $wgAutoloadClasses = array(); * * <code> * $wgExtensionCredits[$type][] = array( - * 'name' => 'Example extension', - * 'version' => 1.9, - * 'path' => __FILE__, - * 'author' => 'Foo Barstein', - * 'url' => 'http://wwww.example.com/Example%20Extension/', - * 'description' => 'An example extension', - * 'descriptionmsg' => 'exampleextension-desc', + * 'name' => 'Example extension', + * 'version' => 1.9, + * 'path' => __FILE__, + * 'author' => 'Foo Barstein', + * 'url' => 'http://wwww.example.com/Example%20Extension/', + * 'description' => 'An example extension', + * 'descriptionmsg' => 'exampleextension-desc', * ); * </code> * @@ -4709,7 +4891,7 @@ $wgExtensionCredits = array(); /** * Authentication plugin. - * @var AuthPlugin + * @var $wgAuth AuthPlugin */ $wgAuth = null; @@ -4740,6 +4922,7 @@ $wgJobClasses = array( ); /** + * Jobs that must be explicitly requested, i.e. aren't run by job runners unless special flags are set. * * These can be: @@ -4755,7 +4938,7 @@ $wgJobTypesExcludedFromDefaultQueue = array(); * Expensive Querypages are already updated. */ $wgSpecialPageCacheUpdates = array( - 'Statistics' => array('SiteStatsUpdate','cacheUpdate') + 'Statistics' => array( 'SiteStatsUpdate', 'cacheUpdate' ) ); /** @@ -4767,7 +4950,6 @@ $wgSpecialPageCacheUpdates = array( */ $wgExceptionHooks = array(); - /** * Page property link table invalidation lists. When a page property * changes, this may require other link tables to be updated (eg @@ -4789,7 +4971,7 @@ $wgPagePropLinkInvalidations = array( /** * Use experimental, DMOZ-like category browser */ -$wgUseCategoryBrowser = false; +$wgUseCategoryBrowser = false; /** * On category pages, show thumbnail gallery for images belonging to that @@ -4842,7 +5024,8 @@ $wgCategoryCollation = 'uppercase'; * an action, which is a specific kind of event that can exist in that * log type. */ -$wgLogTypes = array( '', +$wgLogTypes = array( + '', 'block', 'protect', 'rights', @@ -4895,6 +5078,9 @@ $wgFilterLogTypes = array( * will be listed in the user interface. * * Extensions with custom log types may add to this array. + * + * Since 1.19, if you follow the naming convention log-name-TYPE, + * where TYPE is your log type, yoy don't need to use this array. */ $wgLogNames = array( '' => 'all-logs-page', @@ -4915,6 +5101,9 @@ $wgLogNames = array( * top of each log type. * * Extensions with custom log types may add to this array. + * + * Since 1.19, if you follow the naming convention log-description-TYPE, + * where TYPE is your log type, yoy don't need to use this array. */ $wgLogHeaders = array( '' => 'alllogstext', @@ -4946,33 +5135,32 @@ $wgLogActions = array( 'protect/move_prot' => 'movedarticleprotection', 'rights/rights' => 'rightslogentry', 'rights/autopromote' => 'rightslogentry-autopromote', - 'delete/delete' => 'deletedarticle', - 'delete/restore' => 'undeletedarticle', - 'delete/revision' => 'revdelete-logentry', - 'delete/event' => 'logdelete-logentry', 'upload/upload' => 'uploadedimage', 'upload/overwrite' => 'overwroteimage', 'upload/revert' => 'uploadedimage', - 'move/move' => '1movedto2', - 'move/move_redir' => '1movedto2_redir', 'import/upload' => 'import-logentry-upload', 'import/interwiki' => 'import-logentry-interwiki', 'merge/merge' => 'pagemerge-logentry', - 'suppress/revision' => 'revdelete-logentry', - 'suppress/file' => 'revdelete-logentry', - 'suppress/event' => 'logdelete-logentry', - 'suppress/delete' => 'suppressedarticle', 'suppress/block' => 'blocklogentry', 'suppress/reblock' => 'reblock-logentry', - 'patrol/patrol' => 'patrol-log-line', ); /** * The same as above, but here values are names of functions, * not messages. * @see LogPage::actionText - */ -$wgLogActionsHandlers = array(); + * @see LogFormatter + */ +$wgLogActionsHandlers = array( + // move, move_redir + 'move/*' => 'MoveLogFormatter', + // delete, restore, revision, event + 'delete/*' => 'DeleteLogFormatter', + 'suppress/revision' => 'DeleteLogFormatter', + 'suppress/event' => 'DeleteLogFormatter', + 'suppress/delete' => 'DeleteLogFormatter', + 'patrol/patrol' => 'PatrolLogFormatter', +); /** * Maintain a log of newusers at Log/newusers? @@ -5056,6 +5244,7 @@ $wgSpecialPageGroups = array( 'Block' => 'users', 'Unblock' => 'users', 'Preferences' => 'users', + 'ChangeEmail' => 'users', 'ChangePassword' => 'users', 'DeletedContributions' => 'users', 'PasswordReset' => 'users', @@ -5100,6 +5289,7 @@ $wgSpecialPageGroups = array( 'Specialpages' => 'other', 'Blockme' => 'other', 'Booksources' => 'other', + 'JavaScriptTest' => 'other', ); /** Whether or not to sort special pages in Special:Specialpages */ @@ -5136,16 +5326,24 @@ $wgMaxRedirectLinksRetrieved = 500; * Unsetting core actions will probably cause things to complain loudly. */ $wgActions = array( - 'credits' => true, - 'deletetrackback' => true, - 'info' => true, - 'markpatrolled' => true, - 'purge' => true, - 'revert' => true, + 'credits' => true, + 'delete' => true, + 'edit' => true, + 'history' => true, + 'info' => true, + 'markpatrolled' => true, + 'protect' => true, + 'purge' => true, + 'raw' => true, + 'render' => true, + 'revert' => true, 'revisiondelete' => true, - 'rollback' => true, - 'unwatch' => true, - 'watch' => true, + 'rollback' => true, + 'submit' => true, + 'unprotect' => true, + 'unwatch' => true, + 'view' => true, + 'watch' => true, ); /** diff --git a/includes/DeferredUpdates.php b/includes/DeferredUpdates.php new file mode 100644 index 00000000..262994e3 --- /dev/null +++ b/includes/DeferredUpdates.php @@ -0,0 +1,89 @@ +<?php +/** + * Interface that deferrable updates should implement. Basically required so we + * can validate input on DeferredUpdates::addUpdate() + * + * @since 1.19 + */ +interface DeferrableUpdate { + /** + * Perform the actual work + */ + function doUpdate(); +} + +/** + * Class for mananging the deferred updates. + * + * @since 1.19 + */ +class DeferredUpdates { + /** + * Store of updates to be deferred until the end of the request. + */ + private static $updates = array(); + + /** + * Add an update to the deferred list + * @param $update DeferrableUpdate Some object that implements doUpdate() + */ + public static function addUpdate( DeferrableUpdate $update ) { + array_push( self::$updates, $update ); + } + + /** + * HTMLCacheUpdates are the most common deferred update people use. This + * is a shortcut method for that. + * @see HTMLCacheUpdate::__construct() + * @param $title + * @param $table + */ + public static function addHTMLCacheUpdate( $title, $table ) { + self::addUpdate( new HTMLCacheUpdate( $title, $table ) ); + } + + /** + * Do any deferred updates and clear the list + * + * @param $commit String: set to 'commit' to commit after every update to + * prevent lock contention + */ + public static function doUpdates( $commit = '' ) { + global $wgDeferredUpdateList; + + wfProfileIn( __METHOD__ ); + + $updates = array_merge( $wgDeferredUpdateList, self::$updates ); + + // No need to get master connections in case of empty updates array + if ( !count( $updates ) ) { + wfProfileOut( __METHOD__ ); + return; + } + + $doCommit = $commit == 'commit'; + if ( $doCommit ) { + $dbw = wfGetDB( DB_MASTER ); + } + + foreach ( $updates as $update ) { + $update->doUpdate(); + + if ( $doCommit && $dbw->trxLevel() ) { + $dbw->commit( __METHOD__ ); + } + } + + self::clearPendingUpdates(); + wfProfileOut( __METHOD__ ); + } + + /** + * Clear all pending updates without performing them. Generally, you don't + * want or need to call this. Unit tests need it though. + */ + public static function clearPendingUpdates() { + global $wgDeferredUpdateList; + $wgDeferredUpdateList = self::$updates = array(); + } +} diff --git a/includes/Defines.php b/includes/Defines.php index ff7d7980..26deb2ba 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -2,7 +2,7 @@ /** * A few constants that might be needed during LocalSettings.php. * - * Note: these constants must all be resolvable at compile time by HipHop, + * Note: these constants must all be resolvable at compile time by HipHop, * since this file will not be executed during request startup for a compiled * MediaWiki. * @@ -92,7 +92,7 @@ define( 'CACHE_ANYTHING', -1 ); // Use anything, as long as it works define( 'CACHE_NONE', 0 ); // Do not cache define( 'CACHE_DB', 1 ); // Store cache objects in the DB define( 'CACHE_MEMCACHED', 2 ); // MemCached, must specify servers in $wgMemCacheServers -define( 'CACHE_ACCEL', 3 ); // eAccelerator +define( 'CACHE_ACCEL', 3 ); // APC, XCache or WinCache define( 'CACHE_DBA', 4 ); // Use PHP's DBA extension to store in a DBM-style database /**@}*/ @@ -149,13 +149,12 @@ define( 'MW_DATE_ISO', 'ISO 8601' ); /**@{ * RecentChange type identifiers - * This may be obsolete; log items are now used for moves? */ define( 'RC_EDIT', 0); define( 'RC_NEW', 1); -define( 'RC_MOVE', 2); +define( 'RC_MOVE', 2); // obsolete define( 'RC_LOG', 3); -define( 'RC_MOVE_OVER_REDIRECT', 4); +define( 'RC_MOVE_OVER_REDIRECT', 4); // obsolete /**@}*/ /**@{ diff --git a/includes/EditPage.php b/includes/EditPage.php index e6e7111d..d00d9114 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -15,31 +15,133 @@ * redirects go to, etc. $this->mTitle (as well as $mArticle) is the * page in the database that is actually being edited. These are * usually the same, but they are now allowed to be different. + * + * Surgeon General's Warning: prolonged exposure to this class is known to cause + * headaches, which may be fatal. */ class EditPage { + + /** + * Status: Article successfully updated + */ const AS_SUCCESS_UPDATE = 200; + + /** + * Status: Article successfully created + */ const AS_SUCCESS_NEW_ARTICLE = 201; + + /** + * Status: Article update aborted by a hook function + */ const AS_HOOK_ERROR = 210; + + /** + * Status: The filter function set in $wgFilterCallback returned true (= block it) + */ const AS_FILTERING = 211; + + /** + * Status: A hook function returned an error + */ const AS_HOOK_ERROR_EXPECTED = 212; + + /** + * Status: User is blocked from editting this page + */ const AS_BLOCKED_PAGE_FOR_USER = 215; + + /** + * Status: Content too big (> $wgMaxArticleSize) + */ const AS_CONTENT_TOO_BIG = 216; + + /** + * Status: User cannot edit? (not used) + */ const AS_USER_CANNOT_EDIT = 217; + + /** + * Status: this anonymous user is not allowed to edit this page + */ const AS_READ_ONLY_PAGE_ANON = 218; + + /** + * Status: this logged in user is not allowed to edit this page + */ const AS_READ_ONLY_PAGE_LOGGED = 219; + + /** + * Status: wiki is in readonly mode (wfReadOnly() == true) + */ const AS_READ_ONLY_PAGE = 220; + + /** + * Status: rate limiter for action 'edit' was tripped + */ const AS_RATE_LIMITED = 221; + + /** + * Status: article was deleted while editting and param wpRecreate == false or form + * was not posted + */ const AS_ARTICLE_WAS_DELETED = 222; + + /** + * Status: user tried to create this page, but is not allowed to do that + * ( Title->usercan('create') == false ) + */ const AS_NO_CREATE_PERMISSION = 223; + + /** + * Status: user tried to create a blank page + */ const AS_BLANK_ARTICLE = 224; + + /** + * Status: (non-resolvable) edit conflict + */ const AS_CONFLICT_DETECTED = 225; + + /** + * Status: no edit summary given and the user has forceeditsummary set and the user is not + * editting in his own userspace or talkspace and wpIgnoreBlankSummary == false + */ const AS_SUMMARY_NEEDED = 226; + + /** + * Status: user tried to create a new section without content + */ const AS_TEXTBOX_EMPTY = 228; + + /** + * Status: article is too big (> $wgMaxArticleSize), after merging in the new section + */ const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229; + + /** + * not used + */ const AS_OK = 230; + + /** + * Status: WikiPage::doEdit() was unsuccessfull + */ const AS_END = 231; + + /** + * Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex + */ const AS_SPAM_ERROR = 232; + + /** + * Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false) + */ const AS_IMAGE_REDIRECT_ANON = 233; + + /** + * Status: logged in user is not allowed to upload (User::isAllowed('upload') == false) + */ const AS_IMAGE_REDIRECT_LOGGED = 234; /** @@ -52,7 +154,7 @@ class EditPage { */ var $mTitle; private $mContextTitle = null; - var $action; + var $action = 'submit'; var $isConflict = false; var $isCssJsSubpage = false; var $isCssSubpage = false; @@ -88,20 +190,20 @@ class EditPage { var $save = false, $preview = false, $diff = false; var $minoredit = false, $watchthis = false, $recreate = false; var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false; - var $edittime = '', $section = '', $starttime = ''; + var $edittime = '', $section = '', $sectiontitle = '', $starttime = ''; var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true; # Placeholders for text injection by hooks (must be HTML) # extensions should take care to _append_ to the present value - public $editFormPageTop; // Before even the preview - public $editFormTextTop; - public $editFormTextBeforeContent; - public $editFormTextAfterWarn; - public $editFormTextAfterTools; - public $editFormTextBottom; - public $editFormTextAfterContent; - public $previewTextAfterContent; - public $mPreloadText; + public $editFormPageTop = ''; // Before even the preview + public $editFormTextTop = ''; + public $editFormTextBeforeContent = ''; + public $editFormTextAfterWarn = ''; + public $editFormTextAfterTools = ''; + public $editFormTextBottom = ''; + public $editFormTextAfterContent = ''; + public $previewTextAfterContent = ''; + public $mPreloadText = ''; /* $didSave should be set to true whenever an article was succesfully altered. */ public $didSave = false; @@ -110,34 +212,29 @@ class EditPage { public $suppressIntro = false; /** - * @todo document * @param $article Article */ - function __construct( $article ) { - $this->mArticle =& $article; + public function __construct( Article $article ) { + $this->mArticle = $article; $this->mTitle = $article->getTitle(); - $this->action = 'submit'; - - # Placeholders for text injection by hooks (empty per default) - $this->editFormPageTop = - $this->editFormTextTop = - $this->editFormTextBeforeContent = - $this->editFormTextAfterWarn = - $this->editFormTextAfterTools = - $this->editFormTextBottom = - $this->editFormTextAfterContent = - $this->previewTextAfterContent = - $this->mPreloadText = ""; } /** * @return Article */ - function getArticle() { + public function getArticle() { return $this->mArticle; } /** + * @since 1.19 + * @return Title + */ + public function getTitle() { + return $this->mTitle; + } + + /** * Set the context Title object * * @param $title Title object or null @@ -162,193 +259,6 @@ class EditPage { } } - /** - * Fetch initial editing page content. - * - * @param $def_text string - * @returns mixed string on success, $def_text for invalid sections - * @private - */ - function getContent( $def_text = '' ) { - global $wgOut, $wgRequest, $wgParser; - - wfProfileIn( __METHOD__ ); - # Get variables from query string :P - $section = $wgRequest->getVal( 'section' ); - - $preload = $wgRequest->getVal( 'preload', - // Custom preload text for new sections - $section === 'new' ? 'MediaWiki:addsection-preload' : '' ); - $undoafter = $wgRequest->getVal( 'undoafter' ); - $undo = $wgRequest->getVal( 'undo' ); - - // For message page not locally set, use the i18n message. - // For other non-existent articles, use preload text if any. - if ( !$this->mTitle->exists() ) { - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - # If this is a system message, get the default text. - $text = $this->mTitle->getDefaultMessageText(); - if( $text === false ) { - $text = $this->getPreloadedText( $preload ); - } - } else { - # If requested, preload some text. - $text = $this->getPreloadedText( $preload ); - } - // For existing pages, get text based on "undo" or section parameters. - } else { - $text = $this->mArticle->getContent(); - if ( $undo > 0 && $undoafter > 0 && $undo < $undoafter ) { - # If they got undoafter and undo round the wrong way, switch them - list( $undo, $undoafter ) = array( $undoafter, $undo ); - } - if ( $undo > 0 && $undo > $undoafter ) { - # Undoing a specific edit overrides section editing; section-editing - # doesn't work with undoing. - if ( $undoafter ) { - $undorev = Revision::newFromId( $undo ); - $oldrev = Revision::newFromId( $undoafter ); - } else { - $undorev = Revision::newFromId( $undo ); - $oldrev = $undorev ? $undorev->getPrevious() : null; - } - - # Sanity check, make sure it's the right page, - # the revisions exist and they were not deleted. - # Otherwise, $text will be left as-is. - if ( !is_null( $undorev ) && !is_null( $oldrev ) && - $undorev->getPage() == $oldrev->getPage() && - $undorev->getPage() == $this->mArticle->getID() && - !$undorev->isDeleted( Revision::DELETED_TEXT ) && - !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) { - - $undotext = $this->mArticle->getUndoText( $undorev, $oldrev ); - if ( $undotext === false ) { - # Warn the user that something went wrong - $this->editFormPageTop .= $wgOut->parse( '<div class="error mw-undo-failure">' . wfMsgNoTrans( 'undo-failure' ) . '</div>' ); - } else { - $text = $undotext; - # Inform the user of our success and set an automatic edit summary - $this->editFormPageTop .= $wgOut->parse( '<div class="mw-undo-success">' . wfMsgNoTrans( 'undo-success' ) . '</div>' ); - $firstrev = $oldrev->getNext(); - # If we just undid one rev, use an autosummary - if ( $firstrev->mId == $undo ) { - $this->summary = wfMsgForContent( 'undo-summary', $undo, $undorev->getUserText() ); - $this->undidRev = $undo; - } - $this->formtype = 'diff'; - } - } else { - // Failed basic sanity checks. - // Older revisions may have been removed since the link - // was created, or we may simply have got bogus input. - $this->editFormPageTop .= $wgOut->parse( '<div class="error mw-undo-norev">' . wfMsgNoTrans( 'undo-norev' ) . '</div>' ); - } - } elseif ( $section != '' ) { - if ( $section == 'new' ) { - $text = $this->getPreloadedText( $preload ); - } else { - // Get section edit text (returns $def_text for invalid sections) - $text = $wgParser->getSection( $text, $section, $def_text ); - } - } - } - - wfProfileOut( __METHOD__ ); - return $text; - } - - /** - * Use this method before edit() to preload some text into the edit box - * - * @param $text string - */ - public function setPreloadedText( $text ) { - $this->mPreloadText = $text; - } - - /** - * Get the contents to be preloaded into the box, either set by - * an earlier setPreloadText() or by loading the given page. - * - * @param $preload String: representing the title to preload from. - * @return String - */ - protected function getPreloadedText( $preload ) { - global $wgUser, $wgParser; - if ( !empty( $this->mPreloadText ) ) { - return $this->mPreloadText; - } elseif ( $preload !== '' ) { - $title = Title::newFromText( $preload ); - # Check for existence to avoid getting MediaWiki:Noarticletext - if ( isset( $title ) && $title->exists() && $title->userCanRead() ) { - $article = new Article( $title ); - - if ( $article->isRedirect() ) { - $title = Title::newFromRedirectRecurse( $article->getContent() ); - # Redirects to missing titles are displayed, to hidden pages are followed - # Copying observed behaviour from ?action=view - if ( $title->exists() ) { - if ($title->userCanRead() ) { - $article = new Article( $title ); - } else { - return ""; - } - } - } - $parserOptions = ParserOptions::newFromUser( $wgUser ); - return $wgParser->getPreloadText( $article->getContent(), $title, $parserOptions ); - } - } - return ''; - } - - /** - * Check if a page was deleted while the user was editing it, before submit. - * Note that we rely on the logging table, which hasn't been always there, - * but that doesn't matter, because this only applies to brand new - * deletes. - */ - protected function wasDeletedSinceLastEdit() { - if ( $this->deletedSinceEdit !== null ) { - return $this->deletedSinceEdit; - } - - $this->deletedSinceEdit = false; - - if ( $this->mTitle->isDeletedQuick() ) { - $this->lastDelete = $this->getLastDelete(); - if ( $this->lastDelete ) { - $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp ); - if ( $deleteTime > $this->starttime ) { - $this->deletedSinceEdit = true; - } - } - } - - return $this->deletedSinceEdit; - } - - /** - * Checks whether the user entered a skin name in uppercase, - * e.g. "User:Example/Monobook.css" instead of "monobook.css" - * - * @return bool - */ - protected function isWrongCaseCssJsPage() { - if( $this->mTitle->isCssJsSubpage() ) { - $name = $this->mTitle->getSkinFromCssJsSubpage(); - $skins = array_merge( - array_keys( Skin::getSkinNames() ), - array( 'common' ) - ); - return !in_array( $name, $skins ) - && in_array( strtolower( $name ), $skins ); - } else { - return false; - } - } - function submit() { $this->edit(); } @@ -374,8 +284,12 @@ class EditPage { wfProfileIn( __METHOD__ ); wfDebug( __METHOD__.": enter\n" ); - // This is not an article - $wgOut->setArticleFlag( false ); + // If they used redlink=1 and the page exists, redirect to the main article + if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) { + $wgOut->redirect( $this->mTitle->getFullURL() ); + wfProfileOut( __METHOD__ ); + return; + } $this->importFormData( $wgRequest ); $this->firsttime = false; @@ -392,45 +306,31 @@ class EditPage { $this->preview = true; } - $wgOut->addModules( array( 'mediawiki.action.edit' ) ); - - if ( $wgUser->getOption( 'uselivepreview', false ) ) { - $wgOut->addModules( 'mediawiki.legacy.preview' ); + if ( $this->save ) { + $this->formtype = 'save'; + } elseif ( $this->preview ) { + $this->formtype = 'preview'; + } elseif ( $this->diff ) { + $this->formtype = 'diff'; + } else { # First time through + $this->firsttime = true; + if ( $this->previewOnOpen() ) { + $this->formtype = 'preview'; + } else { + $this->formtype = 'initial'; + } } - // Bug #19334: textarea jumps when editing articles in IE8 - $wgOut->addStyle( 'common/IE80Fixes.css', 'screen', 'IE 8' ); $permErrors = $this->getEditPermissionErrors(); if ( $permErrors ) { + wfDebug( __METHOD__ . ": User can't edit\n" ); // Auto-block user's IP if the account was "hard" blocked $wgUser->spreadAnyEditBlock(); - wfDebug( __METHOD__ . ": User can't edit\n" ); - $content = $this->getContent( null ); - $content = $content === '' ? null : $content; - $this->readOnlyPage( $content, true, $permErrors, 'edit' ); + $this->displayPermissionsError( $permErrors ); + wfProfileOut( __METHOD__ ); return; - } else { - if ( $this->save ) { - $this->formtype = 'save'; - } elseif ( $this->preview ) { - $this->formtype = 'preview'; - } elseif ( $this->diff ) { - $this->formtype = 'diff'; - } else { # First time through - $this->firsttime = true; - if ( $this->previewOnOpen() ) { - $this->formtype = 'preview'; - } else { - $this->formtype = 'initial'; - } - } - } - - // If they used redlink=1 and the page exists, redirect to the main article - if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) { - $wgOut->redirect( $this->mTitle->getFullURL() ); } wfProfileIn( __METHOD__."-business-end" ); @@ -444,29 +344,8 @@ class EditPage { $this->isNew = !$this->mTitle->exists() || $this->section == 'new'; # Show applicable editing introductions - if ( $this->formtype == 'initial' || $this->firsttime ) + if ( $this->formtype == 'initial' || $this->firsttime ) { $this->showIntro(); - - if ( $this->mTitle->isTalkPage() ) { - $wgOut->addWikiMsg( 'talkpagetext' ); - } - - # Optional notices on a per-namespace and per-page basis - $editnotice_ns = 'editnotice-'.$this->mTitle->getNamespace(); - $editnotice_ns_message = wfMessage( $editnotice_ns )->inContentLanguage(); - if ( $editnotice_ns_message->exists() ) { - $wgOut->addWikiText( $editnotice_ns_message->plain() ); - } - if ( MWNamespace::hasSubpages( $this->mTitle->getNamespace() ) ) { - $parts = explode( '/', $this->mTitle->getDBkey() ); - $editnotice_base = $editnotice_ns; - while ( count( $parts ) > 0 ) { - $editnotice_base .= '-'.array_shift( $parts ); - $editnotice_base_msg = wfMessage( $editnotice_base )->inContentLanguage(); - if ( $editnotice_base_msg->exists() ) { - $wgOut->addWikiText( $editnotice_base_msg->plain() ); - } - } } # Attempt submission here. This will check for edit conflicts, @@ -527,11 +406,70 @@ class EditPage { } /** + * Display a permissions error page, like OutputPage::showPermissionsErrorPage(), + * but with the following differences: + * - If redlink=1, the user will be redirected to the page + * - If there is content to display or the error occurs while either saving, + * previewing or showing the difference, it will be a + * "View source for ..." page displaying the source code after the error message. + * + * @since 1.19 + * @param $permErrors Array of permissions errors, as returned by + * Title::getUserPermissionsErrors(). + */ + protected function displayPermissionsError( array $permErrors ) { + global $wgRequest, $wgOut; + + if ( $wgRequest->getBool( 'redlink' ) ) { + // The edit page was reached via a red link. + // Redirect to the article page and let them click the edit tab if + // they really want a permission error. + $wgOut->redirect( $this->mTitle->getFullUrl() ); + return; + } + + $content = $this->getContent(); + + # Use the normal message if there's nothing to display + if ( $this->firsttime && $content === '' ) { + $action = $this->mTitle->exists() ? 'edit' : + ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' ); + throw new PermissionsError( $action, $permErrors ); + } + + $wgOut->setPageTitle( wfMessage( 'viewsource-title', $this->getContextTitle()->getPrefixedText() ) ); + $wgOut->addBacklinkSubtitle( $this->getContextTitle() ); + $wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' ) ); + $wgOut->addHTML( "<hr />\n" ); + + # If the user made changes, preserve them when showing the markup + # (This happens when a user is blocked during edit, for instance) + if ( !$this->firsttime ) { + $content = $this->textbox1; + $wgOut->addWikiMsg( 'viewyourtext' ); + } else { + $wgOut->addWikiMsg( 'viewsourcetext' ); + } + + $this->showTextbox( $content, 'wpTextbox1', array( 'readonly' ) ); + + $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ), + Linker::formatTemplates( $this->getTemplates() ) ) ); + + if ( $this->mTitle->exists() ) { + $wgOut->returnToMain( null, $this->mTitle ); + } + } + + /** * Show a read-only error * Parameters are the same as OutputPage:readOnlyPage() * Redirect to the article page if redlink=1 + * @deprecated in 1.19; use displayPermissionsError() instead */ function readOnlyPage( $source = null, $protected = false, $reasons = array(), $action = null ) { + wfDeprecated( __METHOD__, '1.19' ); + global $wgRequest, $wgOut; if ( $wgRequest->getBool( 'redlink' ) ) { // The edit page was reached via a red link. @@ -574,30 +512,37 @@ class EditPage { } /** - * Does this EditPage class support section editing? - * This is used by EditPage subclasses to indicate their ui cannot handle section edits + * Checks whether the user entered a skin name in uppercase, + * e.g. "User:Example/Monobook.css" instead of "monobook.css" * * @return bool */ - protected function isSectionEditSupported() { - return true; + protected function isWrongCaseCssJsPage() { + if( $this->mTitle->isCssJsSubpage() ) { + $name = $this->mTitle->getSkinFromCssJsSubpage(); + $skins = array_merge( + array_keys( Skin::getSkinNames() ), + array( 'common' ) + ); + return !in_array( $name, $skins ) + && in_array( strtolower( $name ), $skins ); + } else { + return false; + } } /** - * Returns the URL to use in the form's action attribute. - * This is used by EditPage subclasses when simply customizing the action - * variable in the constructor is not enough. This can be used when the - * EditPage lives inside of a Special page rather than a custom page action. + * Does this EditPage class support section editing? + * This is used by EditPage subclasses to indicate their ui cannot handle section edits * - * @param $title Title object for which is being edited (where we go to for &action= links) - * @return string + * @return bool */ - protected function getActionURL( Title $title ) { - return $title->getLocalURL( array( 'action' => $this->action ) ); + protected function isSectionEditSupported() { + return true; } /** - * @todo document + * This function collects the form data and uses it to populate various member variables. * @param $request WebRequest */ function importFormData( &$request ) { @@ -627,15 +572,25 @@ class EditPage { # Truncate for whole multibyte characters. +5 bytes for ellipsis $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 ); - # Remove extra headings from summaries and new sections. - $this->summary = preg_replace('/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary); + # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the + # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for + # section titles. + $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary ); + + # Treat sectiontitle the same way as summary. + # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is + # currently doing double duty as both edit summary and section title. Right now this + # is just to allow API edits to work around this limitation, but this should be + # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312). + $this->sectiontitle = $wgLang->truncate( $request->getText( 'wpSectionTitle' ), 250 ); + $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle ); $this->edittime = $request->getVal( 'wpEdittime' ); $this->starttime = $request->getVal( 'wpStarttime' ); $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' ); - if ($this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null) { + if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) { // wpTextbox1 field is missing, possibly due to being "too big" // according to some filter rules such as Suhosin's setting for // suhosin.request.max_value_length (d'oh) @@ -702,19 +657,24 @@ class EditPage { } else { # Not a posted form? Start with nothing. wfDebug( __METHOD__ . ": Not a posted form.\n" ); - $this->textbox1 = ''; - $this->summary = ''; - $this->edittime = ''; - $this->starttime = wfTimestampNow(); - $this->edit = false; - $this->preview = false; - $this->save = false; - $this->diff = false; - $this->minoredit = false; - $this->watchthis = $request->getBool( 'watchthis', false ); // Watch may be overriden by request parameters - $this->recreate = false; - + $this->textbox1 = ''; + $this->summary = ''; + $this->sectiontitle = ''; + $this->edittime = ''; + $this->starttime = wfTimestampNow(); + $this->edit = false; + $this->preview = false; + $this->save = false; + $this->diff = false; + $this->minoredit = false; + $this->watchthis = $request->getBool( 'watchthis', false ); // Watch may be overriden by request parameters + $this->recreate = false; + + // When creating a new section, we can preload a section title by passing it as the + // preloadtitle parameter in the URL (Bug 13100) if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) { + $this->sectiontitle = $request->getVal( 'preloadtitle' ); + // Once wpSummary isn't being use for setting section titles, we should delete this. $this->summary = $request->getVal( 'preloadtitle' ); } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) { @@ -729,7 +689,6 @@ class EditPage { $this->bot = $request->getBool( 'bot', true ); $this->nosummary = $request->getBool( 'nosummary' ); - // @todo FIXME: Unused variable? $this->oldid = $request->getInt( 'oldid' ); $this->live = $request->getCheck( 'live' ); @@ -756,101 +715,324 @@ class EditPage { } /** - * Make sure the form isn't faking a user's credentials. + * Initialise form fields in the object + * Called on the first invocation, e.g. when a user clicks an edit link + * @return bool -- if the requested section is valid + */ + function initialiseForm() { + global $wgUser; + $this->edittime = $this->mArticle->getTimestamp(); + $this->textbox1 = $this->getContent( false ); + // activate checkboxes if user wants them to be always active + # Sort out the "watch" checkbox + if ( $wgUser->getOption( 'watchdefault' ) ) { + # Watch all edits + $this->watchthis = true; + } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { + # Watch creations + $this->watchthis = true; + } elseif ( $this->mTitle->userIsWatching() ) { + # Already watched + $this->watchthis = true; + } + if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) { + $this->minoredit = true; + } + if ( $this->textbox1 === false ) { + return false; + } + wfProxyCheck(); + return true; + } + + /** + * Fetch initial editing page content. * - * @param $request WebRequest - * @return bool + * @param $def_text string + * @return mixed string on success, $def_text for invalid sections * @private */ - function tokenOk( &$request ) { - global $wgUser; - $token = $request->getVal( 'wpEditToken' ); - $this->mTokenOk = $wgUser->matchEditToken( $token ); - $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token ); - return $this->mTokenOk; + function getContent( $def_text = '' ) { + global $wgOut, $wgRequest, $wgParser; + + wfProfileIn( __METHOD__ ); + + $text = false; + + // For message page not locally set, use the i18n message. + // For other non-existent articles, use preload text if any. + if ( !$this->mTitle->exists() || $this->section == 'new' ) { + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) { + # If this is a system message, get the default text. + $text = $this->mTitle->getDefaultMessageText(); + } + if ( $text === false ) { + # If requested, preload some text. + $preload = $wgRequest->getVal( 'preload', + // Custom preload text for new sections + $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' ); + $text = $this->getPreloadedText( $preload ); + } + // For existing pages, get text based on "undo" or section parameters. + } else { + if ( $this->section != '' ) { + // Get section edit text (returns $def_text for invalid sections) + $text = $wgParser->getSection( $this->getOriginalContent(), $this->section, $def_text ); + } else { + $undoafter = $wgRequest->getInt( 'undoafter' ); + $undo = $wgRequest->getInt( 'undo' ); + + if ( $undo > 0 && $undoafter > 0 ) { + if ( $undo < $undoafter ) { + # If they got undoafter and undo round the wrong way, switch them + list( $undo, $undoafter ) = array( $undoafter, $undo ); + } + + $undorev = Revision::newFromId( $undo ); + $oldrev = Revision::newFromId( $undoafter ); + + # Sanity check, make sure it's the right page, + # the revisions exist and they were not deleted. + # Otherwise, $text will be left as-is. + if ( !is_null( $undorev ) && !is_null( $oldrev ) && + $undorev->getPage() == $oldrev->getPage() && + $undorev->getPage() == $this->mTitle->getArticleId() && + !$undorev->isDeleted( Revision::DELETED_TEXT ) && + !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) { + + $text = $this->mArticle->getUndoText( $undorev, $oldrev ); + if ( $text === false ) { + # Warn the user that something went wrong + $undoMsg = 'failure'; + } else { + # Inform the user of our success and set an automatic edit summary + $undoMsg = 'success'; + + # If we just undid one rev, use an autosummary + $firstrev = $oldrev->getNext(); + if ( $firstrev->getId() == $undo ) { + $undoSummary = wfMsgForContent( 'undo-summary', $undo, $undorev->getUserText() ); + if ( $this->summary === '' ) { + $this->summary = $undoSummary; + } else { + $this->summary = $undoSummary . wfMsgForContent( 'colon-separator' ) . $this->summary; + } + $this->undidRev = $undo; + } + $this->formtype = 'diff'; + } + } else { + // Failed basic sanity checks. + // Older revisions may have been removed since the link + // was created, or we may simply have got bogus input. + $undoMsg = 'norev'; + } + + $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}"; + $this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" . + wfMsgNoTrans( 'undo-' . $undoMsg ) . '</div>', true, /* interface */true ); + } + + if ( $text === false ) { + $text = $this->getOriginalContent(); + } + } + } + + wfProfileOut( __METHOD__ ); + return $text; } /** - * Show all applicable editing introductions + * Get the content of the wanted revision, without section extraction. + * + * The result of this function can be used to compare user's input with + * section replaced in its context (using WikiPage::replaceSection()) + * to the original text of the edit. + * + * This difers from Article::getContent() that when a missing revision is + * encountered the result will be an empty string and not the + * 'missing-article' message. + * + * @since 1.19 + * @return string */ - protected function showIntro() { - global $wgOut, $wgUser; - if ( $this->suppressIntro ) { - return; + private function getOriginalContent() { + if ( $this->section == 'new' ) { + return $this->getCurrentText(); + } + $revision = $this->mArticle->getRevisionFetched(); + if ( $revision === null ) { + return ''; } + return $this->mArticle->getContent(); + } - $namespace = $this->mTitle->getNamespace(); + /** + * Get the actual text of the page. This is basically similar to + * WikiPage::getRawText() except that when the page doesn't exist an empty + * string is returned instead of false. + * + * @since 1.19 + * @return string + */ + private function getCurrentText() { + $text = $this->mArticle->getRawText(); + if ( $text === false ) { + return ''; + } else { + return $text; + } + } - if ( $namespace == NS_MEDIAWIKI ) { - # Show a warning if editing an interface message - $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' ); + /** + * Use this method before edit() to preload some text into the edit box + * + * @param $text string + */ + public function setPreloadedText( $text ) { + $this->mPreloadText = $text; + } + + /** + * Get the contents to be preloaded into the box, either set by + * an earlier setPreloadText() or by loading the given page. + * + * @param $preload String: representing the title to preload from. + * @return String + */ + protected function getPreloadedText( $preload ) { + global $wgUser, $wgParser; + + if ( !empty( $this->mPreloadText ) ) { + return $this->mPreloadText; + } + + if ( $preload === '' ) { + return ''; } - # Show a warning message when someone creates/edits a user (talk) page but the user does not exist - # Show log extract when the user is currently blocked - if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) { - $parts = explode( '/', $this->mTitle->getText(), 2 ); - $username = $parts[0]; - $user = User::newFromName( $username, false /* allow IP users*/ ); - $ip = User::isIP( $username ); - if ( !$user->isLoggedIn() && !$ip ) { # User does not exist - $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>", - array( 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ) ); - } elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked - LogEventsList::showLogExtract( - $wgOut, - 'block', - $user->getUserPage()->getPrefixedText(), - '', - array( - 'lim' => 1, - 'showIfEmpty' => false, - 'msgKey' => array( - 'blocked-notice-logextract', - $user->getName() # Support GENDER in notice - ) - ) - ); - } + $title = Title::newFromText( $preload ); + # Check for existence to avoid getting MediaWiki:Noarticletext + if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) { + return ''; } - # Try to add a custom edit intro, or use the standard one if this is not possible. - if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) { - if ( $wgUser->isLoggedIn() ) { - $wgOut->wrapWikiMsg( "<div class=\"mw-newarticletext\">\n$1\n</div>", 'newarticletext' ); - } else { - $wgOut->wrapWikiMsg( "<div class=\"mw-newarticletextanon\">\n$1\n</div>", 'newarticletextanon' ); + + $page = WikiPage::factory( $title ); + if ( $page->isRedirect() ) { + $title = $page->getRedirectTarget(); + # Same as before + if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) { + return ''; } + $page = WikiPage::factory( $title ); } - # Give a notice if the user is editing a deleted/moved page... - if ( !$this->mTitle->exists() ) { - LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->mTitle->getPrefixedText(), - '', array( 'lim' => 10, - 'conds' => array( "log_action != 'revision'" ), - 'showIfEmpty' => false, - 'msgKey' => array( 'recreate-moveddeleted-warn') ) - ); - } + + $parserOptions = ParserOptions::newFromUser( $wgUser ); + return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions ); } /** - * Attempt to show a custom editing introduction, if supplied + * Make sure the form isn't faking a user's credentials. * + * @param $request WebRequest * @return bool + * @private */ - protected function showCustomIntro() { - if ( $this->editintro ) { - $title = Title::newFromText( $this->editintro ); - if ( $title instanceof Title && $title->exists() && $title->userCanRead() ) { - global $wgOut; - // Added using template syntax, to take <noinclude>'s into account. - $wgOut->addWikiTextTitleTidy( '{{:' . $title->getFullText() . '}}', $this->mTitle ); + function tokenOk( &$request ) { + global $wgUser; + $token = $request->getVal( 'wpEditToken' ); + $this->mTokenOk = $wgUser->matchEditToken( $token ); + $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token ); + return $this->mTokenOk; + } + + /** + * Attempt submission + * @return bool false if output is done, true if the rest of the form should be displayed + */ + function attemptSave() { + global $wgUser, $wgOut; + + $resultDetails = false; + # Allow bots to exempt some edits from bot flagging + $bot = $wgUser->isAllowed( 'bot' ) && $this->bot; + $status = $this->internalAttemptSave( $resultDetails, $bot ); + // FIXME: once the interface for internalAttemptSave() is made nicer, this should use the message in $status + + if ( $status->value == self::AS_SUCCESS_UPDATE || $status->value == self::AS_SUCCESS_NEW_ARTICLE ) { + $this->didSave = true; + } + + switch ( $status->value ) { + case self::AS_HOOK_ERROR_EXPECTED: + case self::AS_CONTENT_TOO_BIG: + case self::AS_ARTICLE_WAS_DELETED: + case self::AS_CONFLICT_DETECTED: + case self::AS_SUMMARY_NEEDED: + case self::AS_TEXTBOX_EMPTY: + case self::AS_MAX_ARTICLE_SIZE_EXCEEDED: + case self::AS_END: return true; - } else { + + case self::AS_HOOK_ERROR: + case self::AS_FILTERING: return false; - } - } else { - return false; + + case self::AS_SUCCESS_NEW_ARTICLE: + $query = $resultDetails['redirect'] ? 'redirect=no' : ''; + $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : ''; + $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor ); + return false; + + case self::AS_SUCCESS_UPDATE: + $extraQuery = ''; + $sectionanchor = $resultDetails['sectionanchor']; + + // Give extensions a chance to modify URL query on update + wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this->mArticle, &$sectionanchor, &$extraQuery ) ); + + if ( $resultDetails['redirect'] ) { + if ( $extraQuery == '' ) { + $extraQuery = 'redirect=no'; + } else { + $extraQuery = 'redirect=no&' . $extraQuery; + } + } + $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor ); + return false; + + case self::AS_BLANK_ARTICLE: + $wgOut->redirect( $this->getContextTitle()->getFullURL() ); + return false; + + case self::AS_SPAM_ERROR: + $this->spamPageWithContent( $resultDetails['spam'] ); + return false; + + case self::AS_BLOCKED_PAGE_FOR_USER: + throw new UserBlockedError( $wgUser->mBlock ); + + case self::AS_IMAGE_REDIRECT_ANON: + case self::AS_IMAGE_REDIRECT_LOGGED: + throw new PermissionsError( 'upload' ); + + case self::AS_READ_ONLY_PAGE_ANON: + case self::AS_READ_ONLY_PAGE_LOGGED: + throw new PermissionsError( 'edit' ); + + case self::AS_READ_ONLY_PAGE: + throw new ReadOnlyError; + + case self::AS_RATE_LIMITED: + throw new ThrottledError(); + + case self::AS_NO_CREATE_PERMISSION: + $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage'; + throw new PermissionsError( $permission ); + } + return false; } /** @@ -866,9 +1048,9 @@ class EditPage { * AS_CONTENT_TOO_BIG and AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some time. */ function internalAttemptSave( &$result, $bot = false ) { - global $wgFilterCallback, $wgUser, $wgParser; + global $wgFilterCallback, $wgUser, $wgRequest, $wgParser; global $wgMaxArticleSize; - + $status = Status::newGood(); wfProfileIn( __METHOD__ ); @@ -891,7 +1073,6 @@ class EditPage { $status->setResult( false, $code ); wfProfileOut( __METHOD__ . '-checks' ); - wfProfileOut( __METHOD__ ); return $status; @@ -904,7 +1085,7 @@ class EditPage { } if ( $match !== false ) { $result['spam'] = $match; - $ip = wfGetIP(); + $ip = $wgRequest->getIP(); $pdbk = $this->mTitle->getPrefixedDBkey(); $match = str_replace( "\n", '', $match ); wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" ); @@ -914,7 +1095,7 @@ class EditPage { wfProfileOut( __METHOD__ ); return $status; } - if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section, $this->hookError, $this->summary ) ) { + if ( $wgFilterCallback && is_callable( $wgFilterCallback ) && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section, $this->hookError, $this->summary ) ) { # Error messages or other handling should be performed by the filter function $status->setResult( false, self::AS_FILTERING ); wfProfileOut( __METHOD__ . '-checks' ); @@ -936,6 +1117,7 @@ class EditPage { wfProfileOut( __METHOD__ ); return $status; } + if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) { // Auto-block user's IP if the account was "hard" blocked $wgUser->spreadAnyEditBlock(); @@ -945,6 +1127,7 @@ class EditPage { wfProfileOut( __METHOD__ ); return $status; } + $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 ); if ( $this->kblength > $wgMaxArticleSize ) { // Error will be displayed by showEditForm() @@ -1044,8 +1227,33 @@ class EditPage { } $text = $this->textbox1; - if ( $this->section == 'new' && $this->summary != '' ) { - $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text; + $result['sectionanchor'] = ''; + if ( $this->section == 'new' ) { + if ( $this->sectiontitle !== '' ) { + // Insert the section title above the content. + $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->sectiontitle ) . "\n\n" . $text; + + // Jump to the new section + $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); + + // If no edit summary was specified, create one automatically from the section + // title and have it link to the new section. Otherwise, respect the summary as + // passed. + if ( $this->summary === '' ) { + $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle ); + } + } elseif ( $this->summary !== '' ) { + // Insert the section title above the content. + $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text; + + // Jump to the new section + $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); + + // Create a link to the new section from the edit summary. + $cleanSummary = $wgParser->stripSectionName( $this->summary ); + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); + } } $status->value = self::AS_SUCCESS_NEW_ARTICLE; @@ -1055,10 +1263,11 @@ class EditPage { # Article exists. Check for edit conflict. $this->mArticle->clear(); # Force reload of dates, etc. + $timestamp = $this->mArticle->getTimestamp(); - wfDebug( "timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n" ); + wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" ); - if ( $this->mArticle->getTimestamp() != $this->edittime ) { + if ( $timestamp != $this->edittime ) { $this->isConflict = true; if ( $this->section == 'new' ) { if ( $this->mArticle->getUserText() == $wgUser->getName() && @@ -1072,23 +1281,27 @@ class EditPage { $this->isConflict = false; wfDebug( __METHOD__ .": conflict suppressed; new section\n" ); } + } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) { + # Suppress edit conflict with self, except for section edits where merging is required. + wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); + $this->isConflict = false; } } - $userid = $wgUser->getId(); - - # Suppress edit conflict with self, except for section edits where merging is required. - if ( $this->isConflict && $this->section == '' && $this->userWasLastToEdit( $userid, $this->edittime ) ) { - wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); - $this->isConflict = false; + + // If sectiontitle is set, use it, otherwise use the summary as the section title (for + // backwards compatibility with old forms/bots). + if ( $this->sectiontitle !== '' ) { + $sectionTitle = $this->sectiontitle; + } else { + $sectionTitle = $this->summary; } - + if ( $this->isConflict ) { - wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '" . - $this->mArticle->getTimestamp() . "')\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime ); + wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" ); + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime ); } else { wfDebug( __METHOD__ . ": getting section '$this->section'\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary ); + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle ); } if ( is_null( $text ) ) { wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" ); @@ -1113,8 +1326,6 @@ class EditPage { return $status; } - $oldtext = $this->mArticle->getContent(); - // Run post-section-merge edit filter if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) { # Error messages etc. could be handled within the hook... @@ -1131,7 +1342,8 @@ class EditPage { } # Handle the user preference to force summaries here, but not for null edits - if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp( $oldtext, $text ) + if ( $this->section != 'new' && !$this->allowBlankSummary + && $this->getOriginalContent() != $text && !Title::newFromRedirect( $text ) ) # check if it's not a redirect { if ( md5( $this->summary ) == $this->autoSumm ) { @@ -1166,7 +1378,16 @@ class EditPage { wfProfileOut( __METHOD__ ); return $status; } - if ( $this->summary != '' ) { + if ( $this->sectiontitle !== '' ) { + $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); + // If no edit summary was specified, create one automatically from the section + // title and have it link to the new section. Otherwise, respect the summary as + // passed. + if ( $this->summary === '' ) { + $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle ); + } + } elseif ( $this->summary !== '' ) { $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); # This is a new section, so create a link to the new section # in the revision summary. @@ -1259,7 +1480,7 @@ class EditPage { $res = $dbw->select( 'revision', 'rev_user', array( - 'rev_page' => $this->mArticle->getId(), + 'rev_page' => $this->mTitle->getArticleId(), 'rev_timestamp > '.$dbw->addQuotes( $dbw->timestamp($edittime) ) ), __METHOD__, @@ -1273,6 +1494,60 @@ class EditPage { } /** + * @private + * @todo document + * + * @parma $editText string + * + * @return bool + */ + function mergeChangesInto( &$editText ){ + wfProfileIn( __METHOD__ ); + + $db = wfGetDB( DB_MASTER ); + + // This is the revision the editor started from + $baseRevision = $this->getBaseRevision(); + if ( is_null( $baseRevision ) ) { + wfProfileOut( __METHOD__ ); + return false; + } + $baseText = $baseRevision->getText(); + + // The current state, we want to merge updates into it + $currentRevision = Revision::loadFromTitle( $db, $this->mTitle ); + if ( is_null( $currentRevision ) ) { + wfProfileOut( __METHOD__ ); + return false; + } + $currentText = $currentRevision->getText(); + + $result = ''; + if ( wfMerge( $baseText, $editText, $currentText, $result ) ) { + $editText = $result; + wfProfileOut( __METHOD__ ); + return true; + } else { + wfProfileOut( __METHOD__ ); + return false; + } + } + + /** + * @return Revision + */ + function getBaseRevision() { + if ( !$this->mBaseRevision ) { + $db = wfGetDB( DB_MASTER ); + $baseRevision = Revision::loadFromTimestamp( + $db, $this->mTitle, $this->edittime ); + return $this->mBaseRevision = $baseRevision; + } else { + return $this->mBaseRevision; + } + } + + /** * Check given input text against $wgSpamRegex, and return the text of the first match. * * @param $text string @@ -1314,48 +1589,27 @@ class EditPage { return false; } - /** - * Initialise form fields in the object - * Called on the first invocation, e.g. when a user clicks an edit link - * @return bool -- if the requested section is valid - */ - function initialiseForm() { - global $wgUser; - $this->edittime = $this->mArticle->getTimestamp(); - $this->textbox1 = $this->getContent( false ); - // activate checkboxes if user wants them to be always active - # Sort out the "watch" checkbox - if ( $wgUser->getOption( 'watchdefault' ) ) { - # Watch all edits - $this->watchthis = true; - } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { - # Watch creations - $this->watchthis = true; - } elseif ( $this->mTitle->userIsWatching() ) { - # Already watched - $this->watchthis = true; - } - if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) { - $this->minoredit = true; - } - if ( $this->textbox1 === false ) { - return false; + function setHeaders() { + global $wgOut, $wgUser; + + $wgOut->addModules( 'mediawiki.action.edit' ); + + if ( $wgUser->getOption( 'uselivepreview', false ) ) { + $wgOut->addModules( 'mediawiki.legacy.preview' ); } - wfProxyCheck(); - return true; - } + // Bug #19334: textarea jumps when editing articles in IE8 + $wgOut->addStyle( 'common/IE80Fixes.css', 'screen', 'IE 8' ); - function setHeaders() { - global $wgOut; $wgOut->setRobotPolicy( 'noindex,nofollow' ); - if ( $this->formtype == 'preview' ) { - $wgOut->setPageTitleActionText( wfMsg( 'preview' ) ); - } + + # Enabled article-related sidebar, toplinks, etc. + $wgOut->setArticleRelated( true ); + if ( $this->isConflict ) { - $wgOut->setPageTitle( wfMsg( 'editconflict', $this->getContextTitle()->getPrefixedText() ) ); + $wgOut->setPageTitle( wfMessage( 'editconflict', $this->getContextTitle()->getPrefixedText() ) ); } elseif ( $this->section != '' ) { $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection'; - $wgOut->setPageTitle( wfMsg( $msg, $this->getContextTitle()->getPrefixedText() ) ); + $wgOut->setPageTitle( wfMessage( $msg, $this->getContextTitle()->getPrefixedText() ) ); } else { # Use the title defined by DISPLAYTITLE magic word when present if ( isset( $this->mParserOutput ) @@ -1364,7 +1618,90 @@ class EditPage { } else { $title = $this->getContextTitle()->getPrefixedText(); } - $wgOut->setPageTitle( wfMsg( 'editing', $title ) ); + $wgOut->setPageTitle( wfMessage( 'editing', $title ) ); + } + } + + /** + * Show all applicable editing introductions + */ + protected function showIntro() { + global $wgOut, $wgUser; + if ( $this->suppressIntro ) { + return; + } + + $namespace = $this->mTitle->getNamespace(); + + if ( $namespace == NS_MEDIAWIKI ) { + # Show a warning if editing an interface message + $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' ); + } + + # Show a warning message when someone creates/edits a user (talk) page but the user does not exist + # Show log extract when the user is currently blocked + if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) { + $parts = explode( '/', $this->mTitle->getText(), 2 ); + $username = $parts[0]; + $user = User::newFromName( $username, false /* allow IP users*/ ); + $ip = User::isIP( $username ); + if ( !($user && $user->isLoggedIn()) && !$ip ) { # User does not exist + $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>", + array( 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ) ); + } elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked + LogEventsList::showLogExtract( + $wgOut, + 'block', + $user->getUserPage(), + '', + array( + 'lim' => 1, + 'showIfEmpty' => false, + 'msgKey' => array( + 'blocked-notice-logextract', + $user->getName() # Support GENDER in notice + ) + ) + ); + } + } + # Try to add a custom edit intro, or use the standard one if this is not possible. + if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) { + if ( $wgUser->isLoggedIn() ) { + $wgOut->wrapWikiMsg( "<div class=\"mw-newarticletext\">\n$1\n</div>", 'newarticletext' ); + } else { + $wgOut->wrapWikiMsg( "<div class=\"mw-newarticletextanon\">\n$1\n</div>", 'newarticletextanon' ); + } + } + # Give a notice if the user is editing a deleted/moved page... + if ( !$this->mTitle->exists() ) { + LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->mTitle, + '', array( 'lim' => 10, + 'conds' => array( "log_action != 'revision'" ), + 'showIfEmpty' => false, + 'msgKey' => array( 'recreate-moveddeleted-warn') ) + ); + } + } + + /** + * Attempt to show a custom editing introduction, if supplied + * + * @return bool + */ + protected function showCustomIntro() { + if ( $this->editintro ) { + $title = Title::newFromText( $this->editintro ); + if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) { + global $wgOut; + // Added using template syntax, to take <noinclude>'s into account. + $wgOut->addWikiTextTitleTidy( '{{:' . $title->getFullText() . '}}', $this->mTitle ); + return true; + } else { + return false; + } + } else { + return false; } } @@ -1391,24 +1728,11 @@ class EditPage { $this->setHeaders(); - # Enabled article-related sidebar, toplinks, etc. - $wgOut->setArticleRelated( true ); - if ( $this->showHeader() === false ) { wfProfileOut( __METHOD__ ); return; } - $action = htmlspecialchars( $this->getActionURL( $this->getContextTitle() ) ); - - if ( $wgUser->getOption( 'showtoolbar' ) and !$this->isCssJsSubpage ) { - # prepare toolbar for edit buttons - $toolbar = EditPage::getEditToolbar(); - } else { - $toolbar = ''; - } - - $wgOut->addHTML( $this->editFormPageTop ); if ( $wgUser->getOption( 'previewontop' ) ) { @@ -1417,26 +1741,21 @@ class EditPage { $wgOut->addHTML( $this->editFormTextTop ); - $templates = $this->getTemplates(); - $formattedtemplates = Linker::formatTemplates( $templates, $this->preview, $this->section != ''); - - $hiddencats = $this->mArticle->getHiddenCategories(); - $formattedhiddencats = Linker::formatHiddenCategories( $hiddencats ); - - if ( $this->wasDeletedSinceLastEdit() && 'save' != $this->formtype ) { - $wgOut->wrapWikiMsg( - "<div class='error mw-deleted-while-editing'>\n$1\n</div>", - 'deletedwhileediting' ); - } elseif ( $this->wasDeletedSinceLastEdit() ) { - // Hide the toolbar and edit area, user can click preview to get it back - // Add an confirmation checkbox and explanation. - $toolbar = ''; - // @todo move this to a cleaner conditional instead of blanking a variable + $showToolbar = true; + if ( $this->wasDeletedSinceLastEdit() ) { + if ( $this->formtype == 'save' ) { + // Hide the toolbar and edit area, user can click preview to get it back + // Add an confirmation checkbox and explanation. + $showToolbar = false; + } else { + $wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>", + 'deletedwhileediting' ); + } } - $wgOut->addHTML( <<<HTML -<form id="editform" name="editform" method="post" action="$action" enctype="multipart/form-data"> -HTML -); + + $wgOut->addHTML( Html::openElement( 'form', array( 'id' => 'editform', 'name' => 'editform', + 'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ), + 'enctype' => 'multipart/form-data' ) ) ); if ( is_callable( $formCallback ) ) { call_user_func_array( $formCallback, array( &$wgOut ) ); @@ -1473,13 +1792,14 @@ HTML ##### # For a bit more sophisticated detection of blank summaries, hash the # automatic one and pass that in the hidden field wpAutoSummary. - if ( $this->missingSummary || - ( $this->section == 'new' && $this->nosummary ) ) - $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) ); + if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) { + $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) ); + } + $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) ); - $wgOut->addHTML( Html::hidden( 'oldid', $this->mArticle->getOldID() ) ); + $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) ); if ( $this->section == 'new' ) { $this->showSummaryInput( true, $this->summary ); @@ -1488,14 +1808,19 @@ HTML $wgOut->addHTML( $this->editFormTextBeforeContent ); - $wgOut->addHTML( $toolbar ); + if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) { + $wgOut->addHTML( EditPage::getEditToolbar() ); + } if ( $this->isConflict ) { // In an edit conflict bypass the overrideable content form method // and fallback to the raw wpTextbox1 since editconflicts can't be // resolved between page source edits and custom ui edits using the // custom edit ui. - $this->showTextbox1( null, $this->getContent() ); + $this->textbox2 = $this->textbox1; + $this->textbox1 = $this->getCurrentText(); + + $this->showTextbox1(); } else { $this->showContentForm(); } @@ -1503,32 +1828,31 @@ HTML $wgOut->addHTML( $this->editFormTextAfterContent ); $wgOut->addWikiText( $this->getCopywarn() ); - if ( isset($this->editFormTextAfterWarn) && $this->editFormTextAfterWarn !== '' ) - $wgOut->addHTML( $this->editFormTextAfterWarn ); + + $wgOut->addHTML( $this->editFormTextAfterWarn ); $this->showStandardInputs(); $this->showFormAfterText(); $this->showTosSummary(); + $this->showEditTools(); - $wgOut->addHTML( <<<HTML -{$this->editFormTextAfterTools} -<div class='templatesUsed'> -{$formattedtemplates} -</div> -<div class='hiddencats'> -{$formattedhiddencats} -</div> -HTML -); + $wgOut->addHTML( $this->editFormTextAfterTools . "\n" ); + + $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ), + Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) ); + + $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'hiddencats' ), + Linker::formatHiddenCategories( $this->mArticle->getHiddenCategories() ) ) ); - if ( $this->isConflict ) + if ( $this->isConflict ) { $this->showConflict(); + } + + $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" ); - $wgOut->addHTML( $this->editFormTextBottom ); - $wgOut->addHTML( "</form>\n" ); if ( !$wgUser->getOption( 'previewontop' ) ) { $this->displayPreviewArea( $previewOutput, false ); } @@ -1536,8 +1860,54 @@ HTML wfProfileOut( __METHOD__ ); } + /** + * Extract the section title from current section text, if any. + * + * @param string $text + * @return Mixed|string or false + */ + public static function extractSectionTitle( $text ) { + preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches ); + if ( !empty( $matches[2] ) ) { + global $wgParser; + return $wgParser->stripSectionName(trim($matches[2])); + } else { + return false; + } + } + protected function showHeader() { global $wgOut, $wgUser, $wgMaxArticleSize, $wgLang; + + if ( $this->mTitle->isTalkPage() ) { + $wgOut->addWikiMsg( 'talkpagetext' ); + } + + # Optional notices on a per-namespace and per-page basis + $editnotice_ns = 'editnotice-'.$this->mTitle->getNamespace(); + $editnotice_ns_message = wfMessage( $editnotice_ns )->inContentLanguage(); + if ( $editnotice_ns_message->exists() ) { + $wgOut->addWikiText( $editnotice_ns_message->plain() ); + } + if ( MWNamespace::hasSubpages( $this->mTitle->getNamespace() ) ) { + $parts = explode( '/', $this->mTitle->getDBkey() ); + $editnotice_base = $editnotice_ns; + while ( count( $parts ) > 0 ) { + $editnotice_base .= '-'.array_shift( $parts ); + $editnotice_base_msg = wfMessage( $editnotice_base )->inContentLanguage(); + if ( $editnotice_base_msg->exists() ) { + $wgOut->addWikiText( $editnotice_base_msg->plain() ); + } + } + } else { + # Even if there are no subpages in namespace, we still don't want / in MW ns. + $editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->mTitle->getDBkey() ); + $editnoticeMsg = wfMessage( $editnoticeText )->inContentLanguage(); + if ( $editnoticeMsg->exists() ) { + $wgOut->addWikiText( $editnoticeMsg->plain() ); + } + } + if ( $this->isConflict ) { $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' ); $this->edittime = $this->mArticle->getTimestamp(); @@ -1551,14 +1921,10 @@ HTML } if ( $this->section != '' && $this->section != 'new' ) { - $matches = array(); if ( !$this->summary && !$this->preview && !$this->diff ) { - preg_match( "/^(=+)(.+)\\1/mi", $this->textbox1, $matches ); - if ( !empty( $matches[2] ) ) { - global $wgParser; - $this->summary = "/* " . - $wgParser->stripSectionName(trim($matches[2])) . - " */ "; + $sectionTitle = self::extractSectionTitle( $this->textbox1 ); + if ( $sectionTitle !== false ) { + $this->summary = "/* $sectionTitle */ "; } } } @@ -1583,18 +1949,27 @@ HTML $wgOut->addWikiMsg( 'nonunicodebrowser' ); } - if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) { - // Let sysop know that this will make private content public if saved + if ( $this->section != 'new' ) { + $revision = $this->mArticle->getRevisionFetched(); + if ( $revision ) { + // Let sysop know that this will make private content public if saved - if ( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) { - $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-permission' ); - } elseif ( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { - $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-view' ); - } + if ( !$revision->userCan( Revision::DELETED_TEXT ) ) { + $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-permission' ); + } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) { + $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-view' ); + } + + if ( !$revision->isCurrent() ) { + $this->mArticle->setOldSubtitle( $revision->getId() ); + $wgOut->addWikiMsg( 'editingold' ); + } + } elseif ( $this->mTitle->exists() ) { + // Something went wrong - if ( !$this->mArticle->mRevision->isCurrent() ) { - $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() ); - $wgOut->addWikiMsg( 'editingold' ); + $wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n", + array( 'missing-article', $this->mTitle->getPrefixedText(), + wfMsgNoTrans( 'missingarticle-rev', $this->oldid ) ) ); } } } @@ -1611,7 +1986,7 @@ HTML if ( $this->isCssJsSubpage ) { # Check the skin exists if ( $this->isWrongCaseCssJsPage ) { - $wgOut->wrapWikiMsg( "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>", array( 'userinvalidcssjstitle', $this->getContextTitle()->getSkinFromCssJsSubpage() ) ); + $wgOut->wrapWikiMsg( "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>", array( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) ); } if ( $this->formtype !== 'preview' ) { if ( $this->isCssSubpage ) @@ -1630,7 +2005,7 @@ HTML # Then it must be protected based on static groups (regular) $noticeMsg = 'protectedpagewarning'; } - LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle->getPrefixedText(), '', + LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '', array( 'lim' => 1, 'msgKey' => array( $noticeMsg ) ) ); } if ( $this->mTitle->isCascadeProtected() ) { @@ -1648,7 +2023,7 @@ HTML $wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', $cascadeSourcesCount ) ); } if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) { - LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle->getPrefixedText(), '', + LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '', array( 'lim' => 1, 'showIfEmpty' => false, 'msgKey' => array( 'titleprotectedwarning' ), @@ -1788,7 +2163,7 @@ HTML * include the constant suffix to prevent editing from * broken text-mangling proxies. */ - $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->editToken() ) . "\n" ); + $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" ); } /** @@ -1811,34 +2186,40 @@ HTML * @param $customAttribs An array of html attributes to use in the textarea * @param $textoverride String: optional text to override $this->textarea1 with */ - protected function showTextbox1($customAttribs = null, $textoverride = null) { - $classes = array(); // Textarea CSS - if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) { - # Is the title semi-protected? - if ( $this->mTitle->isSemiProtected() ) { - $classes[] = 'mw-textarea-sprotected'; - } else { - # Then it must be protected based on static groups (regular) - $classes[] = 'mw-textarea-protected'; + protected function showTextbox1( $customAttribs = null, $textoverride = null ) { + if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) { + $attribs = array( 'style' => 'display:none;' ); + } else { + $classes = array(); // Textarea CSS + if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) { + # Is the title semi-protected? + if ( $this->mTitle->isSemiProtected() ) { + $classes[] = 'mw-textarea-sprotected'; + } else { + # Then it must be protected based on static groups (regular) + $classes[] = 'mw-textarea-protected'; + } + # Is the title cascade-protected? + if ( $this->mTitle->isCascadeProtected() ) { + $classes[] = 'mw-textarea-cprotected'; + } } - # Is the title cascade-protected? - if ( $this->mTitle->isCascadeProtected() ) { - $classes[] = 'mw-textarea-cprotected'; + + $attribs = array( 'tabindex' => 1 ); + + if ( is_array( $customAttribs ) ) { + $attribs += $customAttribs; } - } - $attribs = array( 'tabindex' => 1 ); - if ( is_array($customAttribs) ) - $attribs += $customAttribs; - if ( $this->wasDeletedSinceLastEdit() ) - $attribs['type'] = 'hidden'; - if ( !empty( $classes ) ) { - if ( isset($attribs['class']) ) - $classes[] = $attribs['class']; - $attribs['class'] = implode( ' ', $classes ); + if ( count( $classes ) ) { + if ( isset( $attribs['class'] ) ) { + $classes[] = $attribs['class']; + } + $attribs['class'] = implode( ' ', $classes ); + } } - $this->showTextbox( isset($textoverride) ? $textoverride : $this->textbox1, 'wpTextbox1', $attribs ); + $this->showTextbox( $textoverride !== null ? $textoverride : $this->textbox1, 'wpTextbox1', $attribs ); } protected function showTextbox2() { @@ -1849,7 +2230,7 @@ HTML global $wgOut, $wgUser; $wikitext = $this->safeUnicodeOutput( $content ); - if ( $wikitext !== '' ) { + if ( strval($wikitext) !== '' ) { // Ensure there's a newline at the end, otherwise adding lines // is awkward. // But don't add a newline if the ext is empty, or Firefox in XHTML @@ -1917,6 +2298,40 @@ HTML } /** + * Get a diff between the current contents of the edit box and the + * version of the page we're editing from. + * + * If this is a section edit, we'll replace the section as for final + * save and then make a comparison. + */ + function showDiff() { + global $wgUser, $wgContLang, $wgParser, $wgOut; + + $oldtext = $this->mArticle->getRawText(); + $newtext = $this->mArticle->replaceSection( + $this->section, $this->textbox1, $this->summary, $this->edittime ); + + wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) ); + + $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang ); + $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts ); + + if ( $oldtext !== false || $newtext != '' ) { + $oldtitle = wfMsgExt( 'currentrev', array( 'parseinline' ) ); + $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) ); + + $de = new DifferenceEngine( $this->mArticle->getContext() ); + $de->setText( $oldtext, $newtext ); + $difftext = $de->getDiff( $oldtitle, $newtitle ); + $de->showDiffStyle(); + } else { + $difftext = ''; + } + + $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' ); + } + + /** * Give a chance for site and per-namespace customizations of * terms of service summary link that might exist separately * from the copyright notice. @@ -1992,20 +2407,75 @@ HTML */ protected function showConflict() { global $wgOut; - $this->textbox2 = $this->textbox1; - $this->textbox1 = $this->getContent(); + if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) { $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" ); - $de = new DifferenceEngine( $this->mTitle ); + $de = new DifferenceEngine( $this->mArticle->getContext() ); $de->setText( $this->textbox2, $this->textbox1 ); - $de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) ); + $de->showDiff( wfMsgExt( 'yourtext', 'parseinline' ), wfMsg( 'storedversion' ) ); $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" ); $this->showTextbox2(); } } + /** + * @return string + */ + public function getCancelLink() { + $cancelParams = array(); + if ( !$this->isConflict && $this->oldid > 0 ) { + $cancelParams['oldid'] = $this->oldid; + } + + return Linker::linkKnown( + $this->getContextTitle(), + wfMsgExt( 'cancel', array( 'parseinline' ) ), + array( 'id' => 'mw-editform-cancel' ), + $cancelParams + ); + } + + /** + * Returns the URL to use in the form's action attribute. + * This is used by EditPage subclasses when simply customizing the action + * variable in the constructor is not enough. This can be used when the + * EditPage lives inside of a Special page rather than a custom page action. + * + * @param $title Title object for which is being edited (where we go to for &action= links) + * @return string + */ + protected function getActionURL( Title $title ) { + return $title->getLocalURL( array( 'action' => $this->action ) ); + } + + /** + * Check if a page was deleted while the user was editing it, before submit. + * Note that we rely on the logging table, which hasn't been always there, + * but that doesn't matter, because this only applies to brand new + * deletes. + */ + protected function wasDeletedSinceLastEdit() { + if ( $this->deletedSinceEdit !== null ) { + return $this->deletedSinceEdit; + } + + $this->deletedSinceEdit = false; + + if ( $this->mTitle->isDeletedQuick() ) { + $this->lastDelete = $this->getLastDelete(); + if ( $this->lastDelete ) { + $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp ); + if ( $deleteTime > $this->starttime ) { + $this->deletedSinceEdit = true; + } + } + } + + return $this->deletedSinceEdit; + } + protected function getLastDelete() { $dbr = wfGetDB( DB_SLAVE ); $data = $dbr->selectRow( @@ -2043,10 +2513,25 @@ HTML * @return string */ function getPreviewText() { - global $wgOut, $wgUser, $wgParser; + global $wgOut, $wgUser, $wgParser, $wgRawHtml; wfProfileIn( __METHOD__ ); + if ( $wgRawHtml && !$this->mTokenOk ) { + // Could be an offsite preview attempt. This is very unsafe if + // HTML is enabled, as it could be an attack. + $parsedNote = ''; + if ( $this->textbox1 !== '' ) { + // Do not put big scary notice, if previewing the empty + // string, which happens when you initially edit + // a category page, due to automatic preview-on-open. + $parsedNote = $wgOut->parse( "<div class='previewnote'>" . + wfMsg( 'session_fail_preview_html' ) . "</div>", true, /* interface */true ); + } + wfProfileOut( __METHOD__ ); + return $parsedNote; + } + if ( $this->mTriedSave && !$this->mTokenOk ) { if ( $this->mTokenOkExceptSuffix ) { $note = wfMsg( 'token_suffix_mismatch' ); @@ -2061,47 +2546,36 @@ HTML $parserOptions = ParserOptions::newFromUser( $wgUser ); $parserOptions->setEditSection( false ); + $parserOptions->setTidy( true ); $parserOptions->setIsPreview( true ); $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' ); - global $wgRawHtml; - if ( $wgRawHtml && !$this->mTokenOk ) { - // Could be an offsite preview attempt. This is very unsafe if - // HTML is enabled, as it could be an attack. - $parsedNote = ''; - if ( $this->textbox1 !== '' ) { - // Do not put big scary notice, if previewing the empty - // string, which happens when you initially edit - // a category page, due to automatic preview-on-open. - $parsedNote = $wgOut->parse( "<div class='previewnote'>" . - wfMsg( 'session_fail_preview_html' ) . "</div>" ); - } - wfProfileOut( __METHOD__ ); - return $parsedNote; - } - - # don't parse user css/js, show message about preview + # don't parse non-wikitext pages, show message about preview # XXX: stupid php bug won't let us use $this->getContextTitle()->isCssJsSubpage() here -- This note has been there since r3530. Sure the bug was fixed time ago? - if ( $this->isCssJsSubpage || $this->mTitle->isCssOrJsPage() ) { - $level = 'user'; - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + if ( $this->isCssJsSubpage || !$this->mTitle->isWikitextPage() ) { + if( $this->mTitle->isCssJsSubpage() ) { + $level = 'user'; + } elseif( $this->mTitle->isCssOrJsPage() ) { $level = 'site'; + } else { + $level = false; } # Used messages to make sure grep find them: # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview - if (preg_match( "/\\.css$/", $this->mTitle->getText() ) ) { - $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMsg( "{$level}csspreview" ) . "\n</div>"; - $class = "mw-code mw-css"; - } elseif (preg_match( "/\\.js$/", $this->mTitle->getText() ) ) { - $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMsg( "{$level}jspreview" ) . "\n</div>"; - $class = "mw-code mw-js"; - } else { - throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' ); + if( $level ) { + if (preg_match( "/\\.css$/", $this->mTitle->getText() ) ) { + $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMsg( "{$level}csspreview" ) . "\n</div>"; + $class = "mw-code mw-css"; + } elseif (preg_match( "/\\.js$/", $this->mTitle->getText() ) ) { + $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMsg( "{$level}jspreview" ) . "\n</div>"; + $class = "mw-code mw-js"; + } else { + throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' ); + } } - $parserOptions->setTidy( true ); $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions ); $previewHTML = $parserOutput->mText; $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n"; @@ -2115,15 +2589,15 @@ HTML # If we're adding a comment, we need to show the # summary as the headline if ( $this->section == "new" && $this->summary != "" ) { - $toparse = "== {$this->summary} ==\n\n" . $toparse; + $toparse = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $toparse; } wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) ); - $parserOptions->setTidy( true ); $parserOptions->enableLimitReport(); - $parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ), - $this->mTitle, $parserOptions ); + + $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions ); + $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions ); $previewHTML = $parserOutput->getText(); $this->mParserOutput = $parserOutput; @@ -2143,7 +2617,7 @@ HTML $previewhead = "<div class='previewnote'>\n" . '<h2 id="mw-previewheader">' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>" . - $wgOut->parse( $note ) . $conflict . "</div>\n"; + $wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n"; $pageLang = $this->mTitle->getPageLanguage(); $attribs = array( 'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(), @@ -2170,203 +2644,11 @@ HTML } return $templates; } else { - return $this->mArticle->getUsedTemplates(); + return $this->mTitle->getTemplateLinksFrom(); } } /** - * Call the stock "user is blocked" page - */ - function blockedPage() { - global $wgOut; - $wgOut->blockedPage( false ); # Standard block notice on the top, don't 'return' - - # If the user made changes, preserve them when showing the markup - # (This happens when a user is blocked during edit, for instance) - $first = $this->firsttime || ( !$this->save && $this->textbox1 == '' ); - if ( $first ) { - $source = $this->mTitle->exists() ? $this->getContent() : false; - } else { - $source = $this->textbox1; - } - - # Spit out the source or the user's modified version - if ( $source !== false ) { - $wgOut->addHTML( '<hr />' ); - $wgOut->addWikiMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() ); - $this->showTextbox1( array( 'readonly' ), $source ); - } - } - - /** - * Produce the stock "please login to edit pages" page - */ - function userNotLoggedInPage() { - global $wgOut; - - $loginTitle = SpecialPage::getTitleFor( 'Userlogin' ); - $loginLink = Linker::linkKnown( - $loginTitle, - wfMsgHtml( 'loginreqlink' ), - array(), - array( 'returnto' => $this->getContextTitle()->getPrefixedText() ) - ); - - $wgOut->setPageTitle( wfMsg( 'whitelistedittitle' ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->setArticleRelated( false ); - - $wgOut->addHTML( wfMessage( 'whitelistedittext' )->rawParams( $loginLink )->parse() ); - $wgOut->returnToMain( false, $this->getContextTitle() ); - } - - /** - * Creates a basic error page which informs the user that - * they have attempted to edit a nonexistent section. - */ - function noSuchSectionPage() { - global $wgOut; - - $wgOut->setPageTitle( wfMsg( 'nosuchsectiontitle' ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->setArticleRelated( false ); - - $res = wfMsgExt( 'nosuchsectiontext', 'parse', $this->section ); - wfRunHooks( 'EditPageNoSuchSection', array( &$this, &$res ) ); - $wgOut->addHTML( $res ); - - $wgOut->returnToMain( false, $this->mTitle ); - } - - /** - * Produce the stock "your edit contains spam" page - * - * @param $match Text which triggered one or more filters - * @deprecated since 1.17 Use method spamPageWithContent() instead - */ - static function spamPage( $match = false ) { - global $wgOut, $wgTitle; - - $wgOut->setPageTitle( wfMsg( 'spamprotectiontitle' ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->setArticleRelated( false ); - - $wgOut->addHTML( '<div id="spamprotected">' ); - $wgOut->addWikiMsg( 'spamprotectiontext' ); - if ( $match ) { - $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) ); - } - $wgOut->addHTML( '</div>' ); - - $wgOut->returnToMain( false, $wgTitle ); - } - - /** - * Show "your edit contains spam" page with your diff and text - * - * @param $match Text which triggered one or more filters - */ - public function spamPageWithContent( $match = false ) { - global $wgOut; - $this->textbox2 = $this->textbox1; - - $wgOut->setPageTitle( wfMsg( 'spamprotectiontitle' ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->setArticleRelated( false ); - - $wgOut->addHTML( '<div id="spamprotected">' ); - $wgOut->addWikiMsg( 'spamprotectiontext' ); - if ( $match ) { - $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) ); - } - $wgOut->addHTML( '</div>' ); - - $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" ); - $de = new DifferenceEngine( $this->mTitle ); - $de->setText( $this->getContent(), $this->textbox2 ); - $de->showDiff( wfMsg( "storedversion" ), wfMsg( "yourtext" ) ); - - $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" ); - $this->showTextbox2(); - - $wgOut->addReturnTo( $this->getContextTitle(), array( 'action' => 'edit' ) ); - } - - - /** - * @private - * @todo document - * - * @parma $editText string - * - * @return bool - */ - function mergeChangesInto( &$editText ){ - wfProfileIn( __METHOD__ ); - - $db = wfGetDB( DB_MASTER ); - - // This is the revision the editor started from - $baseRevision = $this->getBaseRevision(); - if ( is_null( $baseRevision ) ) { - wfProfileOut( __METHOD__ ); - return false; - } - $baseText = $baseRevision->getText(); - - // The current state, we want to merge updates into it - $currentRevision = Revision::loadFromTitle( $db, $this->mTitle ); - if ( is_null( $currentRevision ) ) { - wfProfileOut( __METHOD__ ); - return false; - } - $currentText = $currentRevision->getText(); - - $result = ''; - if ( wfMerge( $baseText, $editText, $currentText, $result ) ) { - $editText = $result; - wfProfileOut( __METHOD__ ); - return true; - } else { - wfProfileOut( __METHOD__ ); - return false; - } - } - - /** - * Check if the browser is on a blacklist of user-agents known to - * mangle UTF-8 data on form submission. Returns true if Unicode - * should make it through, false if it's known to be a problem. - * @return bool - * @private - */ - function checkUnicodeCompliantBrowser() { - global $wgBrowserBlackList; - if ( empty( $_SERVER["HTTP_USER_AGENT"] ) ) { - // No User-Agent header sent? Trust it by default... - return true; - } - $currentbrowser = $_SERVER["HTTP_USER_AGENT"]; - foreach ( $wgBrowserBlackList as $browser ) { - if ( preg_match($browser, $currentbrowser) ) { - return false; - } - } - return true; - } - - /** - * Format an anchor fragment as it would appear for a given section name - * @param $text String - * @return String - * @private - */ - function sectionAnchor( $text ) { - global $wgParser; - return $wgParser->guessSectionNameFromWikiText( $text ); - } - - /** * Shows a bulletin board style toolbar for common editing functions. * It can be disabled in the user preferences. * The necessary JavaScript code can be found in skins/common/edit.js. @@ -2384,10 +2666,8 @@ HTML * filename of the button image (without path), the opening * tag, the closing tag, optionally a sample text that is * inserted between the two when no selection is highlighted - * and an option to select which switches the automatic - * selection of inserted text (default is true, see - * mw-editbutton-image). The tip text is shown when the user - * moves the mouse over the button. + * and. The tip text is shown when the user moves the mouse + * over the button. * * Also here: accesskeys (key), which are not used yet until * someone can figure out a way to make them work in @@ -2448,7 +2728,6 @@ HTML 'sample' => wfMsg( 'image_sample' ), 'tip' => wfMsg( 'image_tip' ), 'key' => 'D', - 'select' => true ) : false, $imagesAvailable ? array( 'image' => $wgLang->getImageFile( 'button-media' ), @@ -2497,16 +2776,12 @@ HTML ) ); - $script = ''; + $script = 'mw.loader.using("mediawiki.action.edit", function() {'; foreach ( $toolarray as $tool ) { if ( !$tool ) { continue; } - if( !isset( $tool['select'] ) ) { - $tool['select'] = true; - } - $params = array( $image = $wgStylePath . '/common/images/' . $tool['image'], // Note that we use the tip both for the ALT tag and the TITLE tag of the image. @@ -2522,6 +2797,14 @@ HTML $script .= Xml::encodeJsCall( 'mw.toolbar.addButton', $params ); } + + // This used to be called on DOMReady from mediawiki.action.edit, which + // ended up causing race conditions with the setup code above. + $script .= "\n" . + "// Create button bar\n" . + "$(function() { mw.toolbar.init(); } );\n"; + + $script .= '});'; $wgOut->addScript( Html::inlineScript( ResourceLoader::makeLoaderConditionalScript( $script ) ) ); $toolbar = '<div id="toolbar"></div>'; @@ -2663,50 +2946,138 @@ HTML } /** - * @return string + * Call the stock "user is blocked" page + * + * @deprecated in 1.19; throw an exception directly instead */ - public function getCancelLink() { - $cancelParams = array(); - if ( !$this->isConflict && $this->mArticle->getOldID() > 0 ) { - $cancelParams['oldid'] = $this->mArticle->getOldID(); - } + function blockedPage() { + wfDeprecated( __METHOD__, '1.19' ); + global $wgUser; - return Linker::linkKnown( - $this->getContextTitle(), - wfMsgExt( 'cancel', array( 'parseinline' ) ), - array( 'id' => 'mw-editform-cancel' ), - $cancelParams - ); + throw new UserBlockedError( $wgUser->mBlock ); } /** - * Get a diff between the current contents of the edit box and the - * version of the page we're editing from. + * Produce the stock "please login to edit pages" page * - * If this is a section edit, we'll replace the section as for final - * save and then make a comparison. + * @deprecated in 1.19; throw an exception directly instead */ - function showDiff() { - $oldtext = $this->mArticle->fetchContent(); - $newtext = $this->mArticle->replaceSection( - $this->section, $this->textbox1, $this->summary, $this->edittime ); + function userNotLoggedInPage() { + wfDeprecated( __METHOD__, '1.19' ); + throw new PermissionsError( 'edit' ); + } - wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) ); + /** + * Show an error page saying to the user that he has insufficient permissions + * to create a new page + * + * @deprecated in 1.19; throw an exception directly instead + */ + function noCreatePermission() { + wfDeprecated( __METHOD__, '1.19' ); + $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage'; + throw new PermissionsError( $permission ); + } - $newtext = $this->mArticle->preSaveTransform( $newtext ); - $oldtitle = wfMsgExt( 'currentrev', array( 'parseinline' ) ); - $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) ); - if ( $oldtext !== false || $newtext != '' ) { - $de = new DifferenceEngine( $this->mTitle ); - $de->setText( $oldtext, $newtext ); - $difftext = $de->getDiff( $oldtitle, $newtitle ); - $de->showDiffStyle(); - } else { - $difftext = ''; + /** + * Creates a basic error page which informs the user that + * they have attempted to edit a nonexistent section. + */ + function noSuchSectionPage() { + global $wgOut; + + $wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) ); + + $res = wfMsgExt( 'nosuchsectiontext', 'parse', $this->section ); + wfRunHooks( 'EditPageNoSuchSection', array( &$this, &$res ) ); + $wgOut->addHTML( $res ); + + $wgOut->returnToMain( false, $this->mTitle ); + } + + /** + * Produce the stock "your edit contains spam" page + * + * @param $match Text which triggered one or more filters + * @deprecated since 1.17 Use method spamPageWithContent() instead + */ + static function spamPage( $match = false ) { + wfDeprecated( __METHOD__, '1.17' ); + + global $wgOut, $wgTitle; + + $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) ); + + $wgOut->addHTML( '<div id="spamprotected">' ); + $wgOut->addWikiMsg( 'spamprotectiontext' ); + if ( $match ) { + $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) ); } + $wgOut->addHTML( '</div>' ); + $wgOut->returnToMain( false, $wgTitle ); + } + + /** + * Show "your edit contains spam" page with your diff and text + * + * @param $match Text which triggered one or more filters + */ + public function spamPageWithContent( $match = false ) { global $wgOut; - $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' ); + $this->textbox2 = $this->textbox1; + + $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) ); + + $wgOut->addHTML( '<div id="spamprotected">' ); + $wgOut->addWikiMsg( 'spamprotectiontext' ); + if ( $match ) { + $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) ); + } + $wgOut->addHTML( '</div>' ); + + $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" ); + $de = new DifferenceEngine( $this->mArticle->getContext() ); + $de->setText( $this->getCurrentText(), $this->textbox2 ); + $de->showDiff( wfMsg( "storedversion" ), wfMsgExt( 'yourtext', 'parseinline' ) ); + + $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" ); + $this->showTextbox2(); + + $wgOut->addReturnTo( $this->getContextTitle(), array( 'action' => 'edit' ) ); + } + + /** + * Format an anchor fragment as it would appear for a given section name + * @param $text String + * @return String + * @private + */ + function sectionAnchor( $text ) { + global $wgParser; + return $wgParser->guessSectionNameFromWikiText( $text ); + } + + /** + * Check if the browser is on a blacklist of user-agents known to + * mangle UTF-8 data on form submission. Returns true if Unicode + * should make it through, false if it's known to be a problem. + * @return bool + * @private + */ + function checkUnicodeCompliantBrowser() { + global $wgBrowserBlackList; + if ( empty( $_SERVER["HTTP_USER_AGENT"] ) ) { + // No User-Agent header sent? Trust it by default... + return true; + } + $currentbrowser = $_SERVER["HTTP_USER_AGENT"]; + foreach ( $wgBrowserBlackList as $browser ) { + if ( preg_match($browser, $currentbrowser) ) { + return false; + } + } + return true; } /** @@ -2835,117 +3206,4 @@ HTML // reverse the transform that we made for reversability reasons. return strtr( $result, array( "�" => "&#x" ) ); } - - function noCreatePermission() { - global $wgOut; - $wgOut->setPageTitle( wfMsg( 'nocreatetitle' ) ); - $wgOut->addWikiMsg( 'nocreatetext' ); - } - - /** - * Attempt submission - * @return bool false if output is done, true if the rest of the form should be displayed - */ - function attemptSave() { - global $wgUser, $wgOut; - - $resultDetails = false; - # Allow bots to exempt some edits from bot flagging - $bot = $wgUser->isAllowed( 'bot' ) && $this->bot; - $status = $this->internalAttemptSave( $resultDetails, $bot ); - // FIXME: once the interface for internalAttemptSave() is made nicer, this should use the message in $status - - if ( $status->value == self::AS_SUCCESS_UPDATE || $status->value == self::AS_SUCCESS_NEW_ARTICLE ) { - $this->didSave = true; - } - - switch ( $status->value ) { - case self::AS_HOOK_ERROR_EXPECTED: - case self::AS_CONTENT_TOO_BIG: - case self::AS_ARTICLE_WAS_DELETED: - case self::AS_CONFLICT_DETECTED: - case self::AS_SUMMARY_NEEDED: - case self::AS_TEXTBOX_EMPTY: - case self::AS_MAX_ARTICLE_SIZE_EXCEEDED: - case self::AS_END: - return true; - - case self::AS_HOOK_ERROR: - case self::AS_FILTERING: - return false; - - case self::AS_SUCCESS_NEW_ARTICLE: - $query = $resultDetails['redirect'] ? 'redirect=no' : ''; - $wgOut->redirect( $this->mTitle->getFullURL( $query ) ); - return false; - - case self::AS_SUCCESS_UPDATE: - $extraQuery = ''; - $sectionanchor = $resultDetails['sectionanchor']; - - // Give extensions a chance to modify URL query on update - wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this->mArticle, &$sectionanchor, &$extraQuery ) ); - - if ( $resultDetails['redirect'] ) { - if ( $extraQuery == '' ) { - $extraQuery = 'redirect=no'; - } else { - $extraQuery = 'redirect=no&' . $extraQuery; - } - } - $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor ); - return false; - - case self::AS_SPAM_ERROR: - $this->spamPageWithContent( $resultDetails['spam'] ); - return false; - - case self::AS_BLOCKED_PAGE_FOR_USER: - $this->blockedPage(); - return false; - - case self::AS_IMAGE_REDIRECT_ANON: - $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); - return false; - - case self::AS_READ_ONLY_PAGE_ANON: - $this->userNotLoggedInPage(); - return false; - - case self::AS_READ_ONLY_PAGE_LOGGED: - case self::AS_READ_ONLY_PAGE: - $wgOut->readOnlyPage(); - return false; - - case self::AS_RATE_LIMITED: - $wgOut->rateLimited(); - return false; - - case self::AS_NO_CREATE_PERMISSION: - $this->noCreatePermission(); - return false; - - case self::AS_BLANK_ARTICLE: - $wgOut->redirect( $this->getContextTitle()->getFullURL() ); - return false; - - case self::AS_IMAGE_REDIRECT_LOGGED: - $wgOut->permissionRequired( 'upload' ); - return false; - } - } - - /** - * @return Revision - */ - function getBaseRevision() { - if ( !$this->mBaseRevision ) { - $db = wfGetDB( DB_MASTER ); - $baseRevision = Revision::loadFromTimestamp( - $db, $this->mTitle, $this->edittime ); - return $this->mBaseRevision = $baseRevision; - } else { - return $this->mBaseRevision; - } - } } diff --git a/includes/Exception.php b/includes/Exception.php index 1f599d66..3bd89b6e 100644 --- a/includes/Exception.php +++ b/includes/Exception.php @@ -53,7 +53,7 @@ class MWException extends Exception { global $wgExceptionHooks; if ( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) { - return; // Just silently ignore + return; // Just silently ignore } if ( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[ $name ] ) ) { @@ -70,8 +70,9 @@ class MWException extends Exception { $result = null; } - if ( is_string( $result ) ) + if ( is_string( $result ) ) { return $result; + } } } @@ -118,6 +119,7 @@ class MWException extends Exception { /** * If $wgShowExceptionDetails is true, return a text message with a * backtrace to the error. + * @return string */ function getText() { global $wgShowExceptionDetails; @@ -131,10 +133,12 @@ class MWException extends Exception { } } - /* Return titles of this error page */ + /** + * Return titles of this error page + * @return String + */ function getPageTitle() { - global $wgSitename; - return $this->msg( 'internalerror', "$wgSitename error" ); + return $this->msg( 'internalerror', "Internal error" ); } /** @@ -166,12 +170,7 @@ class MWException extends Exception { function reportHTML() { global $wgOut; if ( $this->useOutputPage() ) { - $wgOut->setPageTitle( $this->getPageTitle() ); - $wgOut->setRobotPolicy( "noindex,nofollow" ); - $wgOut->setArticleRelated( false ); - $wgOut->enableClientCache( false ); - $wgOut->redirect( '' ); - $wgOut->clearHTML(); + $wgOut->prepareErrorPage( $this->getPageTitle() ); $hookResult = $this->runHooks( get_class( $this ) ); if ( $hookResult ) { @@ -182,6 +181,7 @@ class MWException extends Exception { $wgOut->output(); } else { + header( "Content-Type: text/html; charset=utf-8" ); $hookResult = $this->runHooks( get_class( $this ) . "Raw" ); if ( $hookResult ) { die( $hookResult ); @@ -210,6 +210,10 @@ class MWException extends Exception { } } + /** + * @static + * @return bool + */ static function isCommandLine() { return !empty( $GLOBALS['wgCommandLineMode'] ); } @@ -221,10 +225,17 @@ class MWException extends Exception { * @ingroup Exception */ class FatalError extends MWException { + + /** + * @return string + */ function getHTML() { return $this->getMessage(); } + /** + * @return string + */ function getText() { return $this->getMessage(); } @@ -255,44 +266,76 @@ class ErrorPageError extends MWException { function report() { global $wgOut; + $wgOut->showErrorPage( $this->title, $this->msg, $this->params ); $wgOut->output(); } } /** + * Show an error page on a badtitle. + * Similar to ErrorPage, but emit a 400 HTTP error code to let mobile + * browser it is not really a valid content. + */ +class BadTitleError extends ErrorPageError { + + /** + * @param $msg string A message key (default: 'badtitletext') + * @param $params Array parameter to wfMsg() + */ + function __construct( $msg = 'badtitletext', $params = null ) { + parent::__construct( 'badtitle', $msg, $params ); + } + + /** + * Just like ErrorPageError::report() but additionally set + * a 400 HTTP status code (bug 33646). + */ + function report() { + global $wgOut; + + // bug 33646: a badtitle error page need to return an error code + // to let mobile browser now that it is not a normal page. + $wgOut->setStatusCode( 400 ); + parent::report(); + } + +} + +/** * Show an error when a user tries to do something they do not have the necessary * permissions for. * @ingroup Exception */ class PermissionsError extends ErrorPageError { - public $permission; + public $permission, $errors; - function __construct( $permission ) { + function __construct( $permission, $errors = array() ) { global $wgLang; $this->permission = $permission; - $groups = array_map( - array( 'User', 'makeGroupLinkWiki' ), - User::getGroupsWithPermission( $this->permission ) - ); - - if( $groups ) { - parent::__construct( - 'badaccess', - 'badaccess-groups', - array( - $wgLang->commaList( $groups ), - count( $groups ) - ) - ); - } else { - parent::__construct( - 'badaccess', - 'badaccess-group0' + if ( !count( $errors ) ) { + $groups = array_map( + array( 'User', 'makeGroupLinkWiki' ), + User::getGroupsWithPermission( $this->permission ) ); + + if ( $groups ) { + $errors[] = array( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) ); + } else { + $errors[] = array( 'badaccess-group0' ); + } } + + $this->errors = $errors; + } + + function report() { + global $wgOut; + + $wgOut->showPermissionsErrorPage( $this->errors, $this->permission ); + $wgOut->output(); } } @@ -322,6 +365,7 @@ class ThrottledError extends ErrorPageError { 'actionthrottledtext' ); } + public function report(){ global $wgOut; $wgOut->setStatusCode( 503 ); @@ -335,10 +379,15 @@ class ThrottledError extends ErrorPageError { */ class UserBlockedError extends ErrorPageError { public function __construct( Block $block ){ - global $wgLang; - - $blockerUserpage = $block->getBlocker()->getUserPage(); - $link = "[[{$blockerUserpage->getPrefixedText()}|{$blockerUserpage->getText()}]]"; + global $wgLang, $wgRequest; + + $blocker = $block->getBlocker(); + if ( $blocker instanceof User ) { // local user + $blockerUserpage = $block->getBlocker()->getUserPage(); + $link = "[[{$blockerUserpage->getPrefixedText()}|{$blockerUserpage->getText()}]]"; + } else { // foreign user + $link = $blocker; + } $reason = $block->mReason; if( $reason == '' ) { @@ -355,8 +404,8 @@ class UserBlockedError extends ErrorPageError { array( $link, $reason, - wfGetIP(), - $block->getBlocker()->getName(), + $wgRequest->getIP(), + $block->getByName(), $block->getId(), $wgLang->formatExpiry( $block->mExpiry ), $intended, @@ -367,6 +416,55 @@ class UserBlockedError extends ErrorPageError { } /** + * Show an error that looks like an HTTP server error. + * Replacement for wfHttpError(). + * + * @ingroup Exception + */ +class HttpError extends MWException { + private $httpCode, $header, $content; + + /** + * Constructor + * + * @param $httpCode Integer: HTTP status code to send to the client + * @param $content String|Message: content of the message + * @param $header String|Message: content of the header (\<title\> and \<h1\>) + */ + public function __construct( $httpCode, $content, $header = null ){ + parent::__construct( $content ); + $this->httpCode = (int)$httpCode; + $this->header = $header; + $this->content = $content; + } + + public function reportHTML() { + $httpMessage = HttpStatus::getMessage( $this->httpCode ); + + header( "Status: {$this->httpCode} {$httpMessage}" ); + header( 'Content-type: text/html; charset=utf-8' ); + + if ( $this->header === null ) { + $header = $httpMessage; + } elseif ( $this->header instanceof Message ) { + $header = $this->header->escaped(); + } else { + $header = htmlspecialchars( $this->header ); + } + + if ( $this->content instanceof Message ) { + $content = $this->content->escaped(); + } else { + $content = htmlspecialchars( $this->content ); + } + + print "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n". + "<html><head><title>$header</title></head>\n" . + "<body><h1>$header</h1><p>$content</p></body></html>\n"; + } +} + +/** * Handler class for MWExceptions * @ingroup Exception */ diff --git a/includes/Export.php b/includes/Export.php index 87c735c1..7773d03c 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -56,7 +56,7 @@ class WikiExporter { * make additional queries to pull source data while the * main query is still running. * - * @param $db Database + * @param $db DatabaseBase * @param $history Mixed: one of WikiExporter::FULL, WikiExporter::CURRENT, * WikiExporter::RANGE or WikiExporter::STABLE, * or an associative array: @@ -380,7 +380,7 @@ class XmlDumpWriter { * @return string */ function schemaVersion() { - return "0.5"; + return "0.6"; } /** @@ -477,10 +477,22 @@ class XmlDumpWriter { $out = " <page>\n"; $title = Title::makeTitle( $row->page_namespace, $row->page_title ); $out .= ' ' . Xml::elementClean( 'title', array(), self::canonicalTitle( $title ) ) . "\n"; + $out .= ' ' . Xml::element( 'ns', array(), strval( $row->page_namespace) ) . "\n"; $out .= ' ' . Xml::element( 'id', array(), strval( $row->page_id ) ) . "\n"; if ( $row->page_is_redirect ) { - $out .= ' ' . Xml::element( 'redirect', array() ) . "\n"; + $page = WikiPage::factory( $title ); + $redirect = $page->getRedirectTarget(); + if ( $redirect instanceOf Title && $redirect->isValidRedirectTarget() ) { + $out .= ' ' . Xml::element( 'redirect', array( 'title' => self::canonicalTitle( $redirect ) ) ) . "\n"; + } + } + + if ( $row->rev_sha1 ) { + $out .= " " . Xml::element('sha1', null, strval($row->rev_sha1) ) . "\n"; + } else { + $out .= " <sha1/>\n"; } + if ( $row->page_restrictions != '' ) { $out .= ' ' . Xml::element( 'restrictions', array(), strval( $row->page_restrictions ) ) . "\n"; @@ -538,12 +550,12 @@ class XmlDumpWriter { // Raw text from the database may have invalid chars $text = strval( Revision::getRevisionText( $row ) ); $out .= " " . Xml::elementClean( 'text', - array( 'xml:space' => 'preserve', 'bytes' => $row->rev_len ), + array( 'xml:space' => 'preserve', 'bytes' => intval( $row->rev_len ) ), strval( $text ) ) . "\n"; } else { // Stub output $out .= " " . Xml::element( 'text', - array( 'id' => $row->rev_text_id, 'bytes' => $row->rev_len ), + array( 'id' => $row->rev_text_id, 'bytes' => intval( $row->rev_len ) ), "" ) . "\n"; } @@ -609,7 +621,7 @@ class XmlDumpWriter { function writeContributor( $id, $text ) { $out = " <contributor>\n"; - if ( $id ) { + if ( $id || !IP::isValid( $text ) ) { $out .= " " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n"; $out .= " " . Xml::element( 'id', null, strval( $id ) ) . "\n"; } else { @@ -677,9 +689,10 @@ class XmlDumpWriter { * canonical namespace. This skips any special-casing such as gendered * user namespaces -- which while useful, are not yet listed in the * XML <siteinfo> data so are unsafe in export. - * + * * @param Title $title * @return string + * @since 1.18 */ public static function canonicalTitle( Title $title ) { if ( $title->getInterwiki() ) { @@ -689,7 +702,7 @@ class XmlDumpWriter { global $wgContLang; $prefix = str_replace( '_', ' ', $wgContLang->getNsText( $title->getNamespace() ) ); - if ($prefix !== '') { + if ( $prefix !== '' ) { $prefix .= ':'; } @@ -892,8 +905,6 @@ class DumpBZip2Output extends DumpPipeOutput { * @ingroup Dump */ class Dump7ZipOutput extends DumpPipeOutput { - protected $filename; - function __construct( $file ) { $command = $this->setup7zCommand( $file ); parent::__construct( $command ); @@ -908,10 +919,6 @@ class Dump7ZipOutput extends DumpPipeOutput { return( $command ); } - function closeRenameAndReopen( $newname ) { - $this->closeAndRename( $newname, true ); - } - function closeAndRename( $newname, $open = false ) { $newname = $this->checkRenameArgCount( $newname ); if ( $newname ) { @@ -919,7 +926,7 @@ class Dump7ZipOutput extends DumpPipeOutput { proc_close( $this->procOpenResource ); $this->renameOrException( $newname ); if ( $open ) { - $command = $this->setup7zCommand( $file ); + $command = $this->setup7zCommand( $this->filename ); $this->startCommand( $command ); } } diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php index bf97c1a5..b8704758 100644 --- a/includes/ExternalEdit.php +++ b/includes/ExternalEdit.php @@ -18,60 +18,99 @@ * and save the modified data back to the server. * */ -class ExternalEdit { +class ExternalEdit extends ContextSource { + /** - * Title to perform the edit on - * @var Title + * Array of URLs to link to + * @var Array */ - private $title; + private $urls; /** - * Mode of editing - * @var String + * Constructor + * @param $context IContextSource context to use + * @param $urls array */ - private $mode; + public function __construct( IContextSource $context, array $urls = array() ) { + $this->setContext( $context ); + $this->urls = $urls; + } /** - * Constructor - * @param $title Title object we're performing the edit on - * @param $mode String What mode we're using. Only 'file' has any effect + * Check whether external edit or diff should be used. + * + * @param $context IContextSource context to use + * @param $type String can be either 'edit' or 'diff' + * @return Bool */ - public function __construct( $title, $mode ) { - $this->title = $title; - $this->mode = $mode; + public static function useExternalEngine( IContextSource $context, $type ) { + global $wgUseExternalEditor; + + if ( !$wgUseExternalEditor ) { + return false; + } + + $pref = $type == 'diff' ? 'externaldiff' : 'externaleditor'; + $request = $context->getRequest(); + + return !$request->getVal( 'internaledit' ) && + ( $context->getUser()->getOption( $pref ) || $request->getVal( 'externaledit' ) ); } /** * Output the information for the external editor */ - public function edit() { - global $wgOut, $wgScript, $wgScriptPath, $wgCanonicalServer, $wgLang; - $wgOut->disable(); - header( 'Content-type: application/x-external-editor; charset=utf-8' ); - header( 'Cache-control: no-cache' ); + public function execute() { + global $wgContLang, $wgScript, $wgScriptPath, $wgCanonicalServer; + + $this->getOutput()->disable(); + + $response = $this->getRequest()->response(); + $response->header( 'Content-type: application/x-external-editor; charset=utf-8' ); + $response->header( 'Cache-control: no-cache' ); + + $special = $wgContLang->getNsText( NS_SPECIAL ); # $type can be "Edit text", "Edit file" or "Diff text" at the moment # See the protocol specifications at [[m:Help:External editors/Tech]] for # details. - if( $this->mode == "file" ) { + if ( count( $this->urls ) ) { + $urls = $this->urls; + $type = "Diff text"; + } elseif ( $this->getRequest()->getVal( 'mode' ) == 'file' ) { $type = "Edit file"; - $image = wfLocalFile( $this->title ); - $url = $image->getCanonicalURL(); - $extension = $image->getExtension(); + $image = wfLocalFile( $this->getTitle() ); + $urls = array( 'File' => array( + 'Extension' => $image->getExtension(), + 'URL' => $image->getCanonicalURL() + ) ); } else { $type = "Edit text"; - $url = $this->title->getCanonicalURL( - array( 'action' => 'edit', 'internaledit' => 'true' ) ); # *.wiki file extension is used by some editors for syntax # highlighting, so we follow that convention - $extension = "wiki"; + $urls = array( 'File' => array( + 'Extension' => 'wiki', + 'URL' => $this->getTitle()->getCanonicalURL( + array( 'action' => 'edit', 'internaledit' => 'true' ) ) + ) ); + } + + $files = ''; + foreach( $urls as $key => $vars ) { + $files .= "\n[$key]\n"; + foreach( $vars as $varname => $varval ) { + $files .= "$varname=$varval\n"; + } } - $special = $wgLang->getNsText( NS_SPECIAL ); + + $url = $this->getTitle()->getFullURL( + $this->getRequest()->appendQueryValue( 'internaledit', 1, true ) ); + $control = <<<CONTROL -; You're seeing this file because you're using Mediawiki's External Editor -; feature. This is probably because you selected use external editor -; in your preferences. To edit normally, either disable that preference -; or go to the URL $url . +; You're seeing this file because you're using Mediawiki's External Editor feature. +; This is probably because you selected use external editor in your preferences. +; To edit normally, either disable that preference or go to the URL: +; $url ; See http://www.mediawiki.org/wiki/Manual:External_editors for details. [Process] Type=$type @@ -80,10 +119,7 @@ Script={$wgCanonicalServer}{$wgScript} Server={$wgCanonicalServer} Path={$wgScriptPath} Special namespace=$special - -[File] -Extension=$extension -URL=$url +$files CONTROL; echo $control; } diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php index 552c3109..4920a91c 100644 --- a/includes/ExternalStoreDB.php +++ b/includes/ExternalStoreDB.php @@ -34,7 +34,7 @@ class ExternalStoreDB { $wiki = isset($this->mParams['wiki']) ? $this->mParams['wiki'] : false; $lb =& $this->getLoadBalancer( $cluster ); - if ( !in_array( "DB://" . $cluster, $wgDefaultExternalStore ) ) { + if ( !in_array( "DB://" . $cluster, (array)$wgDefaultExternalStore ) ) { wfDebug( "read only external store" ); $lb->allowLagged(true); } else { @@ -120,12 +120,12 @@ class ExternalStoreDB { wfDebug( "ExternalStoreDB::fetchBlob cache miss on $cacheID\n" ); $dbr =& $this->getSlave( $cluster ); - $ret = $dbr->selectField( $this->getTable( $dbr ), 'blob_text', array( 'blob_id' => $id ) ); + $ret = $dbr->selectField( $this->getTable( $dbr ), 'blob_text', array( 'blob_id' => $id ), __METHOD__ ); if ( $ret === false ) { wfDebugLog( 'ExternalStoreDB', "ExternalStoreDB::fetchBlob master fallback on $cacheID\n" ); // Try the master $dbw =& $this->getMaster( $cluster ); - $ret = $dbw->selectField( $this->getTable( $dbw ), 'blob_text', array( 'blob_id' => $id ) ); + $ret = $dbw->selectField( $this->getTable( $dbw ), 'blob_text', array( 'blob_id' => $id ), __METHOD__ ); if( $ret === false) { wfDebugLog( 'ExternalStoreDB', "ExternalStoreDB::fetchBlob master failed to find $cacheID\n" ); } diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php index 515ff387..8415ec08 100644 --- a/includes/FakeTitle.php +++ b/includes/FakeTitle.php @@ -31,9 +31,9 @@ class FakeTitle extends Title { function getPrefixedURL() { $this->error(); } function getFullURL( $query = '', $variant = false ) { $this->error(); } function getLocalURL( $query = '', $variant = false ) { $this->error(); } - function getLinkUrl( $query = array(), $variant = false ) { $this->error(); } - function escapeLocalURL( $query = '' ) { $this->error(); } - function escapeFullURL( $query = '' ) { $this->error(); } + function getLinkURL( $query = array(), $variant = false ) { $this->error(); } + function escapeLocalURL( $query = '', $query2 = false ) { $this->error(); } + function escapeFullURL( $query = '', $query2 = false ) { $this->error(); } function getInternalURL( $query = '', $variant = false ) { $this->error(); } function getEditURL() { $this->error(); } function getEscapedText() { $this->error(); } @@ -42,9 +42,9 @@ class FakeTitle extends Title { function isProtected( $action = '' ) { $this->error(); } function isConversionTable() { $this->error(); } function userIsWatching() { $this->error(); } - function quickUserCan( $action ) { $this->error(); } - function isNamespaceProtected() { $this->error(); } - function userCan( $action, $doExpensiveQueries = true ) { $this->error(); } + function quickUserCan( $action, $user = null ) { $this->error(); } + function isNamespaceProtected( User $user ) { $this->error(); } + function userCan( $action, $user = null, $doExpensiveQueries = true ) { $this->error(); } function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) { $this->error(); } function updateTitleProtection( $create_perm, $reason, $expiry ) { $this->error(); } function deleteTitleProtection() { $this->error(); } @@ -56,7 +56,6 @@ class FakeTitle extends Title { function getSubpages( $limit = -1 ) { $this->error(); } function isCssJsSubpage() { $this->error(); } function isCssOrJsPage() { $this->error(); } - function isValidCssJsSubpage() { $this->error(); } function getSkinFromCssJsSubpage() { $this->error(); } function isCssSubpage() { $this->error(); } function isJsSubpage() { $this->error(); } @@ -102,7 +101,7 @@ class FakeTitle extends Title { function getNextRevisionID( $revId, $flags=0 ) { $this->error(); } function getFirstRevision( $flags=0 ) { $this->error(); } function isNewPage() { $this->error(); } - function getEarliestRevTime() { $this->error(); } + function getEarliestRevTime( $flags = 0 ) { $this->error(); } function countRevisionsBetween( $old, $new ) { $this->error(); } function equals( Title $title ) { $this->error(); } function exists() { $this->error(); } @@ -112,8 +111,6 @@ class FakeTitle extends Title { function touchLinks() { $this->error(); } function getTouched( $db = null ) { $this->error(); } function getNotificationTimestamp( $user = null ) { $this->error(); } - function trackbackURL() { $this->error(); } - function trackbackRDF() { $this->error(); } function getNamespaceKey( $prepend = 'nstab-' ) { $this->error(); } function isSpecialPage() { $this->error(); } function isSpecial( $name ) { $this->error(); } diff --git a/includes/Fallback.php b/includes/Fallback.php index 2cca1e81..b517cd16 100644 --- a/includes/Fallback.php +++ b/includes/Fallback.php @@ -22,7 +22,13 @@ * Fallback functions for PHP installed without mbstring support */ class Fallback { - + + /** + * @param $from + * @param $to + * @param $string + * @return string + */ public static function iconv( $from, $to, $string ) { if ( substr( $to, -8 ) == '//IGNORE' ) { $to = substr( $to, 0, strlen( $to ) - 8 ); @@ -48,7 +54,7 @@ class Fallback { * Larger offsets are still fairly efficient for Latin text, but * can be up to 100x slower than native if the text is heavily * multibyte and we have to slog through a few hundred kb. - * + * * @param $str * @param $start * @param $count string @@ -60,22 +66,27 @@ class Fallback { $split = self::mb_substr_split_unicode( $str, intval( $start ) ); $str = substr( $str, $split ); } - + if( $count !== 'end' ) { $split = self::mb_substr_split_unicode( $str, intval( $count ) ); $str = substr( $str, 0, $split ); } - + return $str; } - + + /** + * @param $str + * @param $splitPos + * @return int + */ public static function mb_substr_split_unicode( $str, $splitPos ) { if( $splitPos == 0 ) { return 0; } - + $byteLen = strlen( $str ); - + if( $splitPos > 0 ) { if( $splitPos > 256 ) { // Optimize large string offsets by skipping ahead N bytes. @@ -90,7 +101,7 @@ class Fallback { $charPos = 0; $bytePos = 0; } - + while( $charPos++ < $splitPos ) { ++$bytePos; // Move past any tail bytes @@ -110,10 +121,10 @@ class Fallback { } } } - + return $bytePos; } - + /** * Fallback implementation of mb_strlen, hardcoded to UTF-8. * @param string $str @@ -123,20 +134,20 @@ class Fallback { public static function mb_strlen( $str, $enc = '' ) { $counts = count_chars( $str ); $total = 0; - + // Count ASCII bytes for( $i = 0; $i < 0x80; $i++ ) { $total += $counts[$i]; } - + // Count multibyte sequence heads for( $i = 0xc0; $i < 0xff; $i++ ) { $total += $counts[$i]; } return $total; } - - + + /** * Fallback implementation of mb_strpos, hardcoded to UTF-8. * @param $haystack String @@ -147,17 +158,17 @@ class Fallback { */ public static function mb_strpos( $haystack, $needle, $offset = 0, $encoding = '' ) { $needle = preg_quote( $needle, '/' ); - + $ar = array(); preg_match( '/' . $needle . '/u', $haystack, $ar, PREG_OFFSET_CAPTURE, $offset ); - + if( isset( $ar[0][1] ) ) { return $ar[0][1]; } else { return false; } - } - + } + /** * Fallback implementation of mb_strrpos, hardcoded to UTF-8. * @param $haystack String @@ -168,10 +179,10 @@ class Fallback { */ public static function mb_strrpos( $haystack, $needle, $offset = 0, $encoding = '' ) { $needle = preg_quote( $needle, '/' ); - + $ar = array(); preg_match_all( '/' . $needle . '/u', $haystack, $ar, PREG_OFFSET_CAPTURE, $offset ); - + if( isset( $ar[0] ) && count( $ar[0] ) > 0 && isset( $ar[0][count( $ar[0] ) - 1][1] ) ) { return $ar[0][count( $ar[0] ) - 1][1]; @@ -196,5 +207,5 @@ class Fallback { } return false; } - + } diff --git a/includes/Feed.php b/includes/Feed.php index ef33c78f..351f3572 100644 --- a/includes/Feed.php +++ b/includes/Feed.php @@ -39,42 +39,34 @@ class FeedItem { /** * @var Title */ - var $Title = 'Wiki'; - var $Description = ''; - var $Url = ''; - var $Date = ''; - var $Author = ''; - var $UniqueId = ''; - var $RSSIsPermalink; + var $title; - /** - * Constructor - * - * @param $Title String|Title Item's title - * @param $Description String - * @param $Url String: URL uniquely designating the item. - * @param $Date String: Item's date - * @param $Author String: Author's user name - * @param $Comments String - */ - function __construct( $Title, $Description, $Url, $Date = '', $Author = '', $Comments = '' ) { - $this->Title = $Title; - $this->Description = $Description; - $this->Url = $Url; - $this->UniqueId = $Url; - $this->RSSIsPermalink = false; - $this->Date = $Date; - $this->Author = $Author; - $this->Comments = $Comments; - } + var $description; + var $url; + var $date; + var $author; + var $uniqueId; + var $comments; + var $rssIsPermalink = false; /** - * Get the last touched timestamp + * Constructor * - * @return String last-touched timestamp - */ - public function getLastMod() { - return $this->Title->getTouched(); + * @param $title String|Title Item's title + * @param $description String + * @param $url String: URL uniquely designating the item. + * @param $date String: Item's date + * @param $author String: Author's user name + * @param $comments String + */ + function __construct( $title, $description, $url, $date = '', $author = '', $comments = '' ) { + $this->title = $title; + $this->description = $description; + $this->url = $url; + $this->uniqueId = $url; + $this->date = $date; + $this->author = $author; + $this->comments = $comments; } /** @@ -95,8 +87,8 @@ class FeedItem { * @return String */ public function getUniqueId() { - if ( $this->UniqueId ) { - return $this->xmlEncode( $this->UniqueId ); + if ( $this->uniqueId ) { + return $this->xmlEncode( $this->uniqueId ); } } @@ -104,11 +96,11 @@ class FeedItem { * set the unique id of an item * * @param $uniqueId String: unique id for the item - * @param $RSSisPermalink Boolean: set to true if the guid (unique id) is a permalink (RSS feeds only) + * @param $rssIsPermalink Boolean: set to true if the guid (unique id) is a permalink (RSS feeds only) */ - public function setUniqueId($uniqueId, $RSSisPermalink = false) { - $this->UniqueId = $uniqueId; - $this->RSSIsPermalink = $RSSisPermalink; + public function setUniqueId( $uniqueId, $rssIsPermalink = false ) { + $this->uniqueId = $uniqueId; + $this->rssIsPermalink = $rssIsPermalink; } /** @@ -117,17 +109,7 @@ class FeedItem { * @return String */ public function getTitle() { - return $this->xmlEncode( $this->Title ); - } - - /** - * Get the DB prefixed title - * - * @return String the prefixed title, with underscores and - * any interwiki and namespace prefixes - */ - public function getDBPrefixedTitle() { - return $this->Title->getPrefixedDBKey(); + return $this->xmlEncode( $this->title ); } /** @@ -136,7 +118,7 @@ class FeedItem { * @return String */ public function getUrl() { - return $this->xmlEncode( $this->Url ); + return $this->xmlEncode( $this->url ); } /** @@ -145,7 +127,7 @@ class FeedItem { * @return String */ public function getDescription() { - return $this->xmlEncode( $this->Description ); + return $this->xmlEncode( $this->description ); } /** @@ -164,7 +146,7 @@ class FeedItem { * @return String */ public function getDate() { - return $this->Date; + return $this->date; } /** @@ -173,7 +155,7 @@ class FeedItem { * @return String */ public function getAuthor() { - return $this->xmlEncode( $this->Author ); + return $this->xmlEncode( $this->author ); } /** @@ -182,7 +164,7 @@ class FeedItem { * @return String */ public function getComments() { - return $this->xmlEncode( $this->Comments ); + return $this->xmlEncode( $this->comments ); } /** @@ -325,7 +307,7 @@ class RSSFeed extends ChannelFeed { <item> <title><?php print $item->getTitle() ?></title> <link><?php print wfExpandUrl( $item->getUrl(), PROTO_CURRENT ) ?></link> - <guid<?php if( !$item->RSSIsPermalink ) print ' isPermaLink="false"' ?>><?php print $item->getUniqueId() ?></guid> + <guid<?php if( !$item->rssIsPermalink ) print ' isPermaLink="false"' ?>><?php print $item->getUniqueId() ?></guid> <description><?php print $item->getDescription() ?></description> <?php if( $item->getDate() ) { ?><pubDate><?php print $this->formatTime( $item->getDate() ) ?></pubDate><?php } ?> <?php if( $item->getAuthor() ) { ?><dc:creator><?php print $item->getAuthor() ?></dc:creator><?php }?> diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php index 4502c3a8..cf42329b 100644 --- a/includes/FeedUtils.php +++ b/includes/FeedUtils.php @@ -31,16 +31,15 @@ class FeedUtils { * @return Boolean */ public static function checkFeedOutput( $type ) { - global $wgFeed, $wgFeedClasses; + global $wgOut, $wgFeed, $wgFeedClasses; if ( !$wgFeed ) { - global $wgOut; $wgOut->addWikiMsg( 'feed-unavailable' ); return false; } if( !isset( $wgFeedClasses[$type] ) ) { - wfHttpError( 500, "Internal Server Error", "Unsupported feed type." ); + $wgOut->addWikiMsg( 'feed-invalid' ); return false; } @@ -54,24 +53,21 @@ class FeedUtils { * @return String */ public static function formatDiff( $row ) { - global $wgUser; - $titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title ); $timestamp = wfTimestamp( TS_MW, $row->rc_timestamp ); $actiontext = ''; if( $row->rc_type == RC_LOG ) { - if( $row->rc_deleted & LogPage::DELETED_ACTION ) { - $actiontext = wfMsgHtml('rev-deleted-event'); - } else { - $actiontext = LogPage::actionText( $row->rc_log_type, $row->rc_log_action, - $titleObj, $wgUser->getSkin(), LogPage::extractParams($row->rc_params,true,true) ); - } + $rcRow = (array)$row; // newFromRow() only accepts arrays for RC rows + $actiontext = LogFormatter::newFromRow( $rcRow )->getActionText(); } return self::formatDiffRow( $titleObj, $row->rc_last_oldid, $row->rc_this_oldid, $timestamp, - ($row->rc_deleted & Revision::DELETED_COMMENT) ? wfMsgHtml('rev-deleted-comment') : $row->rc_comment, - $actiontext ); + ($row->rc_deleted & Revision::DELETED_COMMENT) + ? wfMsgHtml('rev-deleted-comment') + : $row->rc_comment, + $actiontext + ); } /** @@ -86,88 +82,109 @@ class FeedUtils { * @return String */ public static function formatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext='' ) { - global $wgFeedDiffCutoff, $wgLang, $wgUser; + global $wgFeedDiffCutoff, $wgLang; wfProfileIn( __METHOD__ ); - $skin = $wgUser->getSkin(); # log enties $completeText = '<p>' . implode( ' ', array_filter( array( $actiontext, - $skin->formatComment( $comment ) ) ) ) . "</p>\n"; + Linker::formatComment( $comment ) ) ) ) . "</p>\n"; - //NOTE: Check permissions for anonymous users, not current user. - // No "privileged" version should end up in the cache. - // Most feed readers will not log in anway. + // NOTE: Check permissions for anonymous users, not current user. + // No "privileged" version should end up in the cache. + // Most feed readers will not log in anway. $anon = new User(); $accErrors = $title->getUserPermissionsErrors( 'read', $anon, true ); - if( $title->getNamespace() >= 0 && !$accErrors && $newid ) { - if( $oldid ) { - wfProfileIn( __METHOD__."-dodiff" ); - - #$diffText = $de->getDiff( wfMsg( 'revisionasof', - # $wgLang->timeanddate( $timestamp ), - # $wgLang->date( $timestamp ), - # $wgLang->time( $timestamp ) ), - # wfMsg( 'currentrev' ) ); - - // Don't bother generating the diff if we won't be able to show it - if ( $wgFeedDiffCutoff > 0 ) { - $de = new DifferenceEngine( $title, $oldid, $newid ); - $diffText = $de->getDiff( - wfMsg( 'previousrevision' ), // hack - wfMsg( 'revisionasof', - $wgLang->timeanddate( $timestamp ), - $wgLang->date( $timestamp ), - $wgLang->time( $timestamp ) ) ); - } - - if ( $wgFeedDiffCutoff <= 0 || ( strlen( $diffText ) > $wgFeedDiffCutoff ) ) { - // Omit large diffs - $diffLink = $title->escapeFullUrl( - 'diff=' . $newid . - '&oldid=' . $oldid ); - $diffText = '<a href="' . - $diffLink . - '">' . - htmlspecialchars( wfMsgForContent( 'showdiff' ) ) . - '</a>'; - } elseif ( $diffText === false ) { - // Error in diff engine, probably a missing revision - $diffText = "<p>Can't load revision $newid</p>"; - } else { - // Diff output fine, clean up any illegal UTF-8 - $diffText = UtfNormal::cleanUp( $diffText ); - $diffText = self::applyDiffStyle( $diffText ); - } - wfProfileOut( __METHOD__."-dodiff" ); + // Can't diff special pages, unreadable pages or pages with no new revision + // to compare against: just return the text. + if( $title->getNamespace() < 0 || $accErrors || !$newid ) { + wfProfileOut( __METHOD__ ); + return $completeText; + } + + if( $oldid ) { + wfProfileIn( __METHOD__."-dodiff" ); + + #$diffText = $de->getDiff( wfMsg( 'revisionasof', + # $wgLang->timeanddate( $timestamp ), + # $wgLang->date( $timestamp ), + # $wgLang->time( $timestamp ) ), + # wfMsg( 'currentrev' ) ); + + // Don't bother generating the diff if we won't be able to show it + if ( $wgFeedDiffCutoff > 0 ) { + $de = new DifferenceEngine( $title, $oldid, $newid ); + $diffText = $de->getDiff( + wfMsg( 'previousrevision' ), // hack + wfMsg( 'revisionasof', + $wgLang->timeanddate( $timestamp ), + $wgLang->date( $timestamp ), + $wgLang->time( $timestamp ) ) ); + } + + if ( $wgFeedDiffCutoff <= 0 || ( strlen( $diffText ) > $wgFeedDiffCutoff ) ) { + // Omit large diffs + $diffText = self::getDiffLink( $title, $newid, $oldid ); + } elseif ( $diffText === false ) { + // Error in diff engine, probably a missing revision + $diffText = "<p>Can't load revision $newid</p>"; + } else { + // Diff output fine, clean up any illegal UTF-8 + $diffText = UtfNormal::cleanUp( $diffText ); + $diffText = self::applyDiffStyle( $diffText ); + } + wfProfileOut( __METHOD__."-dodiff" ); + } else { + $rev = Revision::newFromId( $newid ); + if( $wgFeedDiffCutoff <= 0 || is_null( $rev ) ) { + $newtext = ''; + } else { + $newtext = $rev->getText(); + } + if ( $wgFeedDiffCutoff <= 0 || strlen( $newtext ) > $wgFeedDiffCutoff ) { + // Omit large new page diffs, bug 29110 + $diffText = self::getDiffLink( $title, $newid ); } else { - $rev = Revision::newFromId( $newid ); - if( is_null( $rev ) ) { - $newtext = ''; - } else { - $newtext = $rev->getText(); - } $diffText = '<p><b>' . wfMsg( 'newpage' ) . '</b></p>' . '<div>' . nl2br( htmlspecialchars( $newtext ) ) . '</div>'; } - $completeText .= $diffText; } + $completeText .= $diffText; wfProfileOut( __METHOD__ ); return $completeText; } /** + * Generates a diff link. Used when the full diff is not wanted for example + * when $wgFeedDiffCutoff is 0. + * + * @param $title Title object: used to generate the diff URL + * @param $newid Integer newid for this diff + * @param $oldid Integer|null oldid for the diff. Null means it is a new article + */ + protected static function getDiffLink( Title $title, $newid, $oldid = null ) { + $queryParameters = ($oldid == null) + ? "diff={$newid}" + : "diff={$newid}&oldid={$oldid}" ; + $diffUrl = $title->getFullUrl( $queryParameters ); + + $diffLink = Html::element( 'a', array( 'href' => $diffUrl ), + wfMsgForContent( 'showdiff' ) ); + + return $diffLink; + } + + /** * Hacky application of diff styles for the feeds. * Might be 'cleaner' to use DOM or XSLT or something, * but *gack* it's a pain in the ass. * * @param $text String: diff's HTML output * @return String: modified HTML - * @private */ public static function applyDiffStyle( $text ) { $styles = array( diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index 515768ff..11f9aea5 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -8,9 +8,19 @@ */ class FileDeleteForm { + /** + * @var Title + */ private $title = null; + + /** + * @var File + */ private $file = null; + /** + * @var File + */ private $oldfile = null; private $oldimage = ''; @@ -29,30 +39,31 @@ class FileDeleteForm { * pending authentication, confirmation, etc. */ public function execute() { - global $wgOut, $wgRequest, $wgUser; - $this->setHeaders(); + global $wgOut, $wgRequest, $wgUser, $wgUploadMaintenance; - if( wfReadOnly() ) { - $wgOut->readOnlyPage(); - return; + $permissionErrors = $this->title->getUserPermissionsErrors( 'delete', $wgUser ); + if ( count( $permissionErrors ) ) { + throw new PermissionsError( 'delete', $permissionErrors ); } - $permission_errors = $this->title->getUserPermissionsErrors('delete', $wgUser); - if (count($permission_errors)>0) { - $wgOut->showPermissionsErrorPage( $permission_errors ); - return; + + if ( wfReadOnly() ) { + throw new ReadOnlyError; } + if ( $wgUploadMaintenance ) { + throw new ErrorPageError( 'filedelete-maintenance-title', 'filedelete-maintenance' ); + } + + $this->setHeaders(); + $this->oldimage = $wgRequest->getText( 'oldimage', false ); $token = $wgRequest->getText( 'wpEditToken' ); # Flag to hide all contents of the archived revisions $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed('suppressrevision'); - if( $this->oldimage && !self::isValidOldSpec($this->oldimage) ) { - $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->oldimage ) ); - return; - } - if( $this->oldimage ) + if( $this->oldimage ) { $this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->oldimage ); + } if( !self::haveDeletableFile($this->file, $this->oldfile, $this->oldimage) ) { $wgOut->addHTML( $this->prepareMessage( 'filedelete-nofile' ) ); @@ -62,26 +73,38 @@ class FileDeleteForm { // Perform the deletion if appropriate if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) { - $this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList' ); - $this->DeleteReason = $wgRequest->getText( 'wpReason' ); - $reason = $this->DeleteReasonList; - if ( $reason != 'other' && $this->DeleteReason != '') { + $deleteReasonList = $wgRequest->getText( 'wpDeleteReasonList' ); + $deleteReason = $wgRequest->getText( 'wpReason' ); + + if ( $deleteReasonList == 'other' ) { + $reason = $deleteReason; + } elseif ( $deleteReason != '' ) { // Entry from drop down menu + additional comment - $reason .= wfMsgForContent( 'colon-separator' ) . $this->DeleteReason; - } elseif ( $reason == 'other' ) { - $reason = $this->DeleteReason; + $reason = $deleteReasonList . wfMsgForContent( 'colon-separator' ) . $deleteReason; + } else { + $reason = $deleteReasonList; } - $status = self::doDelete( $this->title, $this->file, $this->oldimage, $reason, $suppress ); + $status = self::doDelete( $this->title, $this->file, $this->oldimage, $reason, $suppress, $wgUser ); - if( !$status->isGood() ) + if( !$status->isGood() ) { + $wgOut->addHTML( '<h2>' . $this->prepareMessage( 'filedeleteerror-short' ) . "</h2>\n" ); + $wgOut->addHTML( '<span class="error">' ); $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) ); + $wgOut->addHTML( '</span>' ); + } if( $status->ok ) { - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); + $wgOut->setPageTitle( wfMessage( 'actioncomplete' ) ); $wgOut->addHTML( $this->prepareMessage( 'filedelete-success' ) ); // Return to the main page if we just deleted all versions of the // file, otherwise go back to the description page $wgOut->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() ); + + if ( $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn() ) { + WatchAction::doWatch( $this->title, $wgUser ); + } elseif ( $this->title->userIsWatching() ) { + WatchAction::doUnwatch( $this->title, $wgUser ); + } } return; } @@ -94,17 +117,20 @@ class FileDeleteForm { * Really delete the file * * @param $title Title object - * @param $file File object + * @param File $file: file object * @param $oldimage String: archive name * @param $reason String: reason of the deletion * @param $suppress Boolean: whether to mark all deleted versions as restricted + * @param $user User object performing the request */ - public static function doDelete( &$title, &$file, &$oldimage, $reason, $suppress ) { - global $wgUser; - $article = null; - $status = Status::newFatal( 'error' ); + public static function doDelete( &$title, &$file, &$oldimage, $reason, $suppress, User $user = null ) { + if ( $user === null ) { + global $wgUser; + $user = $wgUser; + } if( $oldimage ) { + $page = null; $status = $file->deleteOld( $oldimage, $reason, $suppress ); if( $status->ok ) { // Need to do a log item @@ -116,20 +142,17 @@ class FileDeleteForm { $log->addEntry( 'delete', $title, $logComment ); } } else { - $id = $title->getArticleID( Title::GAID_FOR_UPDATE ); - $article = new Article( $title ); + $status = Status::newFatal( 'cannotdelete', + wfEscapeWikiText( $title->getPrefixedText() ) + ); + $page = WikiPage::factory( $title ); $dbw = wfGetDB( DB_MASTER ); try { // delete the associated article first - if( $article->doDeleteArticle( $reason, $suppress, $id, false ) ) { - global $wgRequest; - if ( $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn() ) { - WatchAction::doWatch( $title, $wgUser ); - } elseif ( $title->userIsWatching() ) { - WatchAction::doUnwatch( $title, $wgUser ); - } + $error = ''; + if ( $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error, $user ) >= WikiPage::DELETE_SUCCESS ) { $status = $file->delete( $reason, $suppress ); - if( $status->ok ) { + if( $status->isOK() ) { $dbw->commit(); } else { $dbw->rollback(); @@ -141,8 +164,10 @@ class FileDeleteForm { throw $e; } } - if( $status->isGood() ) - wfRunHooks('FileDeleteComplete', array( &$file, &$oldimage, &$article, &$wgUser, &$reason)); + + if ( $status->isOK() ) { + wfRunHooks( 'FileDeleteComplete', array( &$file, &$oldimage, &$page, &$user, &$reason ) ); + } return $status; } @@ -170,7 +195,7 @@ class FileDeleteForm { 'id' => 'mw-img-deleteconfirm' ) ) . Xml::openElement( 'fieldset' ) . Xml::element( 'legend', null, wfMsg( 'filedelete-legend' ) ) . - Html::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) ) . + Html::hidden( 'wpEditToken', $wgUser->getEditToken( $this->oldimage ) ) . $this->prepareMessage( 'filedelete-intro' ) . Xml::openElement( 'table', array( 'id' => 'mw-img-deleteconfirm-table' ) ) . "<tr> @@ -193,7 +218,7 @@ class FileDeleteForm { "</td> </tr> {$suppress}"; - if( $wgUser->isLoggedIn() ) { + if( $wgUser->isLoggedIn() ) { $form .= " <tr> <td></td> @@ -216,9 +241,8 @@ class FileDeleteForm { Xml::closeElement( 'form' ); if ( $wgUser->isAllowed( 'editinterface' ) ) { - $skin = $wgUser->getSkin(); $title = Title::makeTitle( NS_MEDIAWIKI, 'Filedelete-reason-dropdown' ); - $link = $skin->link( + $link = Linker::link( $title, wfMsgHtml( 'filedelete-edit-reasonlist' ), array(), @@ -236,7 +260,7 @@ class FileDeleteForm { private function showLogEntries() { global $wgOut; $wgOut->addHTML( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" ); - LogEventsList::showLogExtract( $wgOut, 'delete', $this->title->getPrefixedText() ); + LogEventsList::showLogExtract( $wgOut, 'delete', $this->title ); } /** @@ -270,19 +294,10 @@ class FileDeleteForm { * Set headers, titles and other bits */ private function setHeaders() { - global $wgOut, $wgUser; - $wgOut->setPageTitle( wfMsg( 'filedelete', $this->title->getText() ) ); + global $wgOut; + $wgOut->setPageTitle( wfMessage( 'filedelete', $this->title->getText() ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->setSubtitle( wfMsg( - 'filedelete-backlink', - $wgUser->getSkin()->link( - $this->title, - null, - array(), - array(), - array( 'known', 'noclasses' ) - ) - ) ); + $wgOut->addBacklinkSubtitle( $this->title ); } /** @@ -301,6 +316,9 @@ class FileDeleteForm { * value was provided, does it correspond to an * existing, local, old version of this file? * + * @param $file File + * @param $oldfile File + * @param $oldimage File * @return bool */ public static function haveDeletableFile(&$file, &$oldfile, $oldimage) { diff --git a/includes/ForkController.php b/includes/ForkController.php index d87dfb1e..9cacef54 100644 --- a/includes/ForkController.php +++ b/includes/ForkController.php @@ -34,7 +34,7 @@ class ForkController { public function __construct( $numProcs, $flags = 0 ) { if ( php_sapi_name() != 'cli' ) { - throw new MWException( "MultiProcess cannot be used from the web." ); + throw new MWException( "ForkController cannot be used from the web." ); } $this->procsToStart = $numProcs; $this->flags = $flags; @@ -119,13 +119,13 @@ class ForkController { // Don't share DB or memcached connections wfGetLBFactory()->destroyInstance(); ObjectCache::clear(); - unset( $wgMemc ); + $wgMemc = null; } /** * Fork a number of worker processes. * - * return string + * @return string */ protected function forkWorkers( $numProcs ) { $this->prepareEnvironment(); diff --git a/includes/FormOptions.php b/includes/FormOptions.php index b668ff46..ccc87d8a 100644 --- a/includes/FormOptions.php +++ b/includes/FormOptions.php @@ -5,10 +5,10 @@ * * Copyright © 2008, Niklas Laxstiröm * - * Copyright © 2011, Ashar Voultoiz + * Copyright © 2011, Antoine Musso * * @author Niklas Laxström - * @author Ashar Voultoiz + * @author Antoine Musso */ class FormOptions implements ArrayAccess { diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 8ed79c40..52cd46a5 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -81,6 +81,9 @@ if ( !function_exists( 'istainted' ) ) { /** * Like array_diff( $a, $b ) except that it works with two-dimensional arrays. + * @param $a array + * @param $b array + * @return array */ function wfArrayDiff2( $a, $b ) { return array_udiff( $a, $b, 'wfArrayDiff2_cmp' ); @@ -164,7 +167,7 @@ function wfArrayMerge( $array1/* ... */ ) { * array( array( 'x' ) ), * array( array( 'x', '2' ) ), * array( array( 'x' ) ), - * array( array( 'y') ) + * array( array( 'y' ) ) * ); * returns: * array( @@ -296,9 +299,9 @@ function wfUrlencode( $s ) { static $needle; if ( is_null( $s ) ) { $needle = null; - return; + return ''; } - + if ( is_null( $needle ) ) { $needle = array( '%3B', '%40', '%24', '%21', '%2A', '%28', '%29', '%2C', '%2F' ); if ( !isset( $_SERVER['SERVER_SOFTWARE'] ) || ( strpos( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/7' ) === false ) ) { @@ -319,7 +322,7 @@ function wfUrlencode( $s ) { /** * This function takes two arrays as input, and returns a CGI-style string, e.g. * "days=7&limit=100". Options in the first array override options in the second. - * Options set to "" will not be output. + * Options set to null or false will not be output. * * @param $array1 Array ( String|Array ) * @param $array2 Array ( String|Array ) @@ -333,7 +336,7 @@ function wfArrayToCGI( $array1, $array2 = null, $prefix = '' ) { $cgi = ''; foreach ( $array1 as $key => $value ) { - if ( $value !== '' ) { + if ( !is_null($value) && $value !== false ) { if ( $cgi != '' ) { $cgi .= '&'; } @@ -366,8 +369,7 @@ function wfArrayToCGI( $array1, $array2 = null, $prefix = '' ) { * This is the logical opposite of wfArrayToCGI(): it accepts a query string as * its argument and returns the same string in array form. This allows compa- * tibility with legacy functions that accept raw query strings instead of nice - * arrays. Of course, keys and values are urldecode()d. Don't try passing in- - * valid query strings, or it will explode. + * arrays. Of course, keys and values are urldecode()d. * * @param $query String: query string * @return array Array version of input @@ -382,7 +384,13 @@ function wfCgiToArray( $query ) { if ( $bit === '' ) { continue; } - list( $key, $value ) = explode( '=', $bit ); + if ( strpos( $bit, '=' ) === false ) { + // Pieces like &qwerty become 'qwerty' => '' (at least this is what php does) + $key = $bit; + $value = ''; + } else { + list( $key, $value ) = explode( '=', $bit ); + } $key = urldecode( $key ); $value = urldecode( $value ); if ( strpos( $key, '[' ) !== false ) { @@ -444,8 +452,11 @@ function wfAppendQuery( $url, $query ) { * like "subdir/foo.html", etc. * * @param $url String: either fully-qualified or a local path + query - * @param $defaultProto Mixed: one of the PROTO_* constants. Determines the protocol to use if $url or $wgServer is protocol-relative - * @return string Fully-qualified URL + * @param $defaultProto Mixed: one of the PROTO_* constants. Determines the + * protocol to use if $url or $wgServer is + * protocol-relative + * @return string Fully-qualified URL, current-path-relative URL or false if + * no valid URL can be constructed */ function wfExpandUrl( $url, $defaultProto = PROTO_CURRENT ) { global $wgServer, $wgCanonicalServer, $wgInternalServer; @@ -477,21 +488,170 @@ function wfExpandUrl( $url, $defaultProto = PROTO_CURRENT ) { $defaultProtoWithoutSlashes = substr( $defaultProto, 0, -2 ); - if( substr( $url, 0, 2 ) == '//' ) { - return $defaultProtoWithoutSlashes . $url; - } elseif( substr( $url, 0, 1 ) == '/' ) { + if ( substr( $url, 0, 2 ) == '//' ) { + $url = $defaultProtoWithoutSlashes . $url; + } elseif ( substr( $url, 0, 1 ) == '/' ) { // If $serverUrl is protocol-relative, prepend $defaultProtoWithoutSlashes, otherwise leave it alone - return ( $serverHasProto ? '' : $defaultProtoWithoutSlashes ) . $serverUrl . $url; - } else { + $url = ( $serverHasProto ? '' : $defaultProtoWithoutSlashes ) . $serverUrl . $url; + } + + $bits = wfParseUrl( $url ); + if ( $bits && isset( $bits['path'] ) ) { + $bits['path'] = wfRemoveDotSegments( $bits['path'] ); + return wfAssembleUrl( $bits ); + } elseif ( $bits ) { + # No path to expand return $url; + } elseif ( substr( $url, 0, 1 ) != '/' ) { + # URL is a relative path + return wfRemoveDotSegments( $url ); + } + + # Expanded URL is not valid. + return false; +} + +/** + * This function will reassemble a URL parsed with wfParseURL. This is useful + * if you need to edit part of a URL and put it back together. + * + * This is the basic structure used (brackets contain keys for $urlParts): + * [scheme][delimiter][user]:[pass]@[host]:[port][path]?[query]#[fragment] + * + * @todo Need to integrate this into wfExpandUrl (bug 32168) + * + * @since 1.19 + * @param $urlParts Array URL parts, as output from wfParseUrl + * @return string URL assembled from its component parts + */ +function wfAssembleUrl( $urlParts ) { + $result = ''; + + if ( isset( $urlParts['delimiter'] ) ) { + if ( isset( $urlParts['scheme'] ) ) { + $result .= $urlParts['scheme']; + } + + $result .= $urlParts['delimiter']; + } + + if ( isset( $urlParts['host'] ) ) { + if ( isset( $urlParts['user'] ) ) { + $result .= $urlParts['user']; + if ( isset( $urlParts['pass'] ) ) { + $result .= ':' . $urlParts['pass']; + } + $result .= '@'; + } + + $result .= $urlParts['host']; + + if ( isset( $urlParts['port'] ) ) { + $result .= ':' . $urlParts['port']; + } + } + + if ( isset( $urlParts['path'] ) ) { + $result .= $urlParts['path']; + } + + if ( isset( $urlParts['query'] ) ) { + $result .= '?' . $urlParts['query']; + } + + if ( isset( $urlParts['fragment'] ) ) { + $result .= '#' . $urlParts['fragment']; + } + + return $result; +} + +/** + * Remove all dot-segments in the provided URL path. For example, + * '/a/./b/../c/' becomes '/a/c/'. For details on the algorithm, please see + * RFC3986 section 5.2.4. + * + * @todo Need to integrate this into wfExpandUrl (bug 32168) + * + * @param $urlPath String URL path, potentially containing dot-segments + * @return string URL path with all dot-segments removed + */ +function wfRemoveDotSegments( $urlPath ) { + $output = ''; + $inputOffset = 0; + $inputLength = strlen( $urlPath ); + + while ( $inputOffset < $inputLength ) { + $prefixLengthOne = substr( $urlPath, $inputOffset, 1 ); + $prefixLengthTwo = substr( $urlPath, $inputOffset, 2 ); + $prefixLengthThree = substr( $urlPath, $inputOffset, 3 ); + $prefixLengthFour = substr( $urlPath, $inputOffset, 4 ); + $trimOutput = false; + + if ( $prefixLengthTwo == './' ) { + # Step A, remove leading "./" + $inputOffset += 2; + } elseif ( $prefixLengthThree == '../' ) { + # Step A, remove leading "../" + $inputOffset += 3; + } elseif ( ( $prefixLengthTwo == '/.' ) && ( $inputOffset + 2 == $inputLength ) ) { + # Step B, replace leading "/.$" with "/" + $inputOffset += 1; + $urlPath[$inputOffset] = '/'; + } elseif ( $prefixLengthThree == '/./' ) { + # Step B, replace leading "/./" with "/" + $inputOffset += 2; + } elseif ( $prefixLengthThree == '/..' && ( $inputOffset + 3 == $inputLength ) ) { + # Step C, replace leading "/..$" with "/" and + # remove last path component in output + $inputOffset += 2; + $urlPath[$inputOffset] = '/'; + $trimOutput = true; + } elseif ( $prefixLengthFour == '/../' ) { + # Step C, replace leading "/../" with "/" and + # remove last path component in output + $inputOffset += 3; + $trimOutput = true; + } elseif ( ( $prefixLengthOne == '.' ) && ( $inputOffset + 1 == $inputLength ) ) { + # Step D, remove "^.$" + $inputOffset += 1; + } elseif ( ( $prefixLengthTwo == '..' ) && ( $inputOffset + 2 == $inputLength ) ) { + # Step D, remove "^..$" + $inputOffset += 2; + } else { + # Step E, move leading path segment to output + if ( $prefixLengthOne == '/' ) { + $slashPos = strpos( $urlPath, '/', $inputOffset + 1 ); + } else { + $slashPos = strpos( $urlPath, '/', $inputOffset ); + } + if ( $slashPos === false ) { + $output .= substr( $urlPath, $inputOffset ); + $inputOffset = $inputLength; + } else { + $output .= substr( $urlPath, $inputOffset, $slashPos - $inputOffset ); + $inputOffset += $slashPos - $inputOffset; + } + } + + if ( $trimOutput ) { + $slashPos = strrpos( $output, '/' ); + if ( $slashPos === false ) { + $output = ''; + } else { + $output = substr( $output, 0, $slashPos ); + } + } } + + return $output; } /** * Returns a regular expression of url protocols * * @param $includeProtocolRelative bool If false, remove '//' from the returned protocol list. - * DO NOT USE this directy, use wfUrlProtocolsWithoutProtRel() instead + * DO NOT USE this directly, use wfUrlProtocolsWithoutProtRel() instead * @return String */ function wfUrlProtocols( $includeProtocolRelative = true ) { @@ -537,6 +697,7 @@ function wfUrlProtocols( $includeProtocolRelative = true ) { * Like wfUrlProtocols(), but excludes '//' from the protocol list. Use this if * you need a regex that matches all URL protocols but does not match protocol- * relative URLs + * @return String */ function wfUrlProtocolsWithoutProtRel() { return wfUrlProtocols( false ); @@ -554,7 +715,7 @@ function wfUrlProtocolsWithoutProtRel() { */ function wfParseUrl( $url ) { global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php - + // Protocol-relative URLs are handled really badly by parse_url(). It's so bad that the easiest // way to handle them is to just prepend 'http:' and strip the protocol out later $wasRelative = substr( $url, 0, 2 ) == '//'; @@ -564,7 +725,9 @@ function wfParseUrl( $url ) { wfSuppressWarnings(); $bits = parse_url( $url ); wfRestoreWarnings(); - if ( !$bits ) { + // parse_url() returns an array without scheme for some invalid URLs, e.g. + // parse_url("%0Ahttp://example.com") == array( 'host' => '%0Ahttp', 'path' => 'example.com' ) + if ( !$bits || !isset( $bits['scheme'] ) ) { return false; } @@ -592,7 +755,7 @@ function wfParseUrl( $url ) { $bits['path'] = '/' . $bits['path']; } } - + // If the URL was protocol-relative, fix scheme and delimiter if ( $wasRelative ) { $bits['scheme'] = ''; @@ -717,6 +880,8 @@ function wfDebug( $text, $logonly = false ) { wfErrorLog( $text, $wgDebugLogFile ); } } + + MWDebug::debugMsg( $text ); } /** @@ -747,20 +912,15 @@ function wfIsDebugRawPage() { * @return string */ function wfDebugTimer() { - global $wgDebugTimestamps; + global $wgDebugTimestamps, $wgRequestTime; + if ( !$wgDebugTimestamps ) { return ''; } - static $start = null; - if ( $start === null ) { - $start = microtime( true ); - $prefix = "\n$start"; - } else { - $prefix = sprintf( "%6.4f", microtime( true ) - $start ); - } - - return $prefix . ' '; + $prefix = sprintf( "%6.4f", microtime( true ) - $wgRequestTime ); + $mem = sprintf( "%5.1fM", ( memory_get_usage( true ) / ( 1024 * 1024 ) ) ); + return "$prefix $mem "; } /** @@ -788,16 +948,12 @@ function wfDebugMem( $exact = false ) { * log file is specified, (default true) */ function wfDebugLog( $logGroup, $text, $public = true ) { - global $wgDebugLogGroups, $wgShowHostnames; + global $wgDebugLogGroups; $text = trim( $text ) . "\n"; if( isset( $wgDebugLogGroups[$logGroup] ) ) { $time = wfTimestamp( TS_DB ); $wiki = wfWikiID(); - if ( $wgShowHostnames ) { - $host = wfHostname(); - } else { - $host = ''; - } + $host = wfHostname(); if ( wfRunHooks( 'Debug', array( $text, $logGroup ) ) ) { wfErrorLog( "$time $host $wiki: $text", $wgDebugLogGroups[$logGroup] ); } @@ -812,15 +968,98 @@ function wfDebugLog( $logGroup, $text, $public = true ) { * @param $text String: database error message. */ function wfLogDBError( $text ) { - global $wgDBerrorLog, $wgDBname; + global $wgDBerrorLog; if ( $wgDBerrorLog ) { - $host = trim(`hostname`); - $text = date( 'D M j G:i:s T Y' ) . "\t$host\t$wgDBname\t$text"; + $host = wfHostname(); + $wiki = wfWikiID(); + $text = date( 'D M j G:i:s T Y' ) . "\t$host\t$wiki\t$text"; wfErrorLog( $text, $wgDBerrorLog ); } } /** + * Throws a warning that $function is deprecated + * + * @param $function String + * @param $version String|false: Added in 1.19. + * @param $component String|false: Added in 1.19. + * + * @return null + */ +function wfDeprecated( $function, $version = false, $component = false ) { + static $functionsWarned = array(); + + MWDebug::deprecated( $function, $version, $component ); + + if ( !isset( $functionsWarned[$function] ) ) { + $functionsWarned[$function] = true; + + if ( $version ) { + global $wgDeprecationReleaseLimit; + + if ( $wgDeprecationReleaseLimit && $component === false ) { + # Strip -* off the end of $version so that branches can use the + # format #.##-branchname to avoid issues if the branch is merged into + # a version of MediaWiki later than what it was branched from + $comparableVersion = preg_replace( '/-.*$/', '', $version ); + + # If the comparableVersion is larger than our release limit then + # skip the warning message for the deprecation + if ( version_compare( $wgDeprecationReleaseLimit, $comparableVersion, '<' ) ) { + return; + } + } + + $component = $component === false ? 'MediaWiki' : $component; + wfWarn( "Use of $function was deprecated in $component $version.", 2 ); + } else { + wfWarn( "Use of $function is deprecated.", 2 ); + } + } +} + +/** + * Send a warning either to the debug log or in a PHP error depending on + * $wgDevelopmentWarnings + * + * @param $msg String: message to send + * @param $callerOffset Integer: number of items to go back in the backtrace to + * find the correct caller (1 = function calling wfWarn, ...) + * @param $level Integer: PHP error level; only used when $wgDevelopmentWarnings + * is true + */ +function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) { + global $wgDevelopmentWarnings; + + MWDebug::warning( $msg, $callerOffset + 2 ); + + $callers = wfDebugBacktrace(); + if ( isset( $callers[$callerOffset + 1] ) ) { + $callerfunc = $callers[$callerOffset + 1]; + $callerfile = $callers[$callerOffset]; + if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) { + $file = $callerfile['file'] . ' at line ' . $callerfile['line']; + } else { + $file = '(internal function)'; + } + $func = ''; + if ( isset( $callerfunc['class'] ) ) { + $func .= $callerfunc['class'] . '::'; + } + if ( isset( $callerfunc['function'] ) ) { + $func .= $callerfunc['function']; + } + $msg .= " [Called from $func in $file]"; + } + + if ( $wgDevelopmentWarnings ) { + trigger_error( $msg, $level ); + } else { + wfDebug( "$msg\n" ); + } +} + +/** * Log to a file without getting "file size exceeded" signals. * * Can also log to TCP or UDP with the syntax udp://host:port/prefix. This will @@ -855,21 +1094,22 @@ function wfErrorLog( $text, $file ) { $text = preg_replace( '/^/m', $prefix . ' ', $text ); // Limit to 64KB - if ( strlen( $text ) > 65534 ) { - $text = substr( $text, 0, 65534 ); + if ( strlen( $text ) > 65506 ) { + $text = substr( $text, 0, 65506 ); } if ( substr( $text, -1 ) != "\n" ) { $text .= "\n"; } - } elseif ( strlen( $text ) > 65535 ) { - $text = substr( $text, 0, 65535 ); + } elseif ( strlen( $text ) > 65507 ) { + $text = substr( $text, 0, 65507 ); } $sock = socket_create( $domain, SOCK_DGRAM, SOL_UDP ); if ( !$sock ) { return; } + socket_sendto( $sock, $text, strlen( $text ), 0, $host, $port ); socket_close( $sock ); } else { @@ -899,8 +1139,7 @@ function wfLogProfilingData() { // Get total page request time and only show pages that longer than // $wgProfileLimit time (default is 0) - $now = wfTime(); - $elapsed = $now - $wgRequestTime; + $elapsed = microtime( true ) - $wgRequestTime; if ( $elapsed <= $wgProfileLimit ) { return; } @@ -962,6 +1201,9 @@ function wfReadOnly() { return (bool)$wgReadOnly; } +/** + * @return bool + */ function wfReadOnlyReason() { global $wgReadOnly; wfReadOnly(); @@ -977,9 +1219,10 @@ function wfReadOnlyReason() { * a valid code create a language for that language, if * it is a string but not a valid code then make a basic * language object - * - a boolean: if it's false then use the current users - * language (as a fallback for the old parameter - * functionality), or if it is true then use the wikis + * - a boolean: if it's false then use the global object for + * the current user's language (as a fallback for the old parameter + * functionality), or if it is true then use global object + * for the wiki's content language. * @return Language object */ function wfGetLangObj( $langcode = false ) { @@ -1023,6 +1266,7 @@ function wfGetLangObj( $langcode = false ) { * @return Language */ function wfUILang() { + wfDeprecated( __METHOD__, '1.18' ); global $wgLang; return $wgLang; } @@ -1249,7 +1493,8 @@ function wfMsgWikiHtml( $key ) { $args = func_get_args(); array_shift( $args ); return wfMsgReplaceArgs( - MessageCache::singleton()->parse( wfMsgGetKey( $key ), null, /* can't be set to false */ true )->getText(), + MessageCache::singleton()->parse( wfMsgGetKey( $key ), null, + /* can't be set to false */ true, /* interface */ true )->getText(), $args ); } @@ -1311,13 +1556,18 @@ function wfMsgExt( $key, $options ) { } $messageCache = MessageCache::singleton(); - if( in_array( 'parse', $options, true ) ) { - $string = $messageCache->parse( $string, null, true, !$forContent, $langCodeObj )->getText(); - } elseif ( in_array( 'parseinline', $options, true ) ) { - $string = $messageCache->parse( $string, null, true, !$forContent, $langCodeObj )->getText(); - $m = array(); - if( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $string, $m ) ) { - $string = $m[1]; + $parseInline = in_array( 'parseinline', $options, true ); + if( in_array( 'parse', $options, true ) || $parseInline ) { + $string = $messageCache->parse( $string, null, true, !$forContent, $langCodeObj ); + if ( $string instanceof ParserOutput ) { + $string = $string->getText(); + } + + if ( $parseInline ) { + $m = array(); + if( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $string, $m ) ) { + $string = $m[1]; + } } } elseif ( in_array( 'parsemag', $options, true ) ) { $string = $messageCache->transform( $string, @@ -1353,7 +1603,7 @@ function wfEmptyMsg( $key ) { * Throw a debugging exception. This function previously once exited the process, * but now throws an exception instead, with similar results. * - * @param $msg String: message shown when dieing. + * @param $msg String: message shown when dying. */ function wfDebugDieBacktrace( $msg = '' ) { throw new MWException( $msg ); @@ -1397,8 +1647,7 @@ function wfHostname() { function wfReportTime() { global $wgRequestTime, $wgShowHostnames; - $now = wfTime(); - $elapsed = $now - $wgRequestTime; + $elapsed = microtime( true ) - $wgRequestTime; return $wgShowHostnames ? sprintf( '<!-- Served by %s in %01.3f secs. -->', wfHostname(), $elapsed ) @@ -1575,80 +1824,40 @@ function wfShowingResults( $offset, $limit ) { * @param $query String: optional URL query parameter string * @param $atend Bool: optional param for specified if this is the last page * @return String + * @deprecated in 1.19; use Language::viewPrevNext() instead */ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { + wfDeprecated( __METHOD__, '1.19' ); + global $wgLang; - $fmtLimit = $wgLang->formatNum( $limit ); - // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip? - # Get prev/next link display text - $prev = wfMsgExt( 'prevn', array( 'parsemag', 'escape' ), $fmtLimit ); - $next = wfMsgExt( 'nextn', array( 'parsemag', 'escape' ), $fmtLimit ); - # Get prev/next link title text - $pTitle = wfMsgExt( 'prevn-title', array( 'parsemag', 'escape' ), $fmtLimit ); - $nTitle = wfMsgExt( 'nextn-title', array( 'parsemag', 'escape' ), $fmtLimit ); - # Fetch the title object + + $query = wfCgiToArray( $query ); + if( is_object( $link ) ) { - $title =& $link; + $title = $link; } else { $title = Title::newFromText( $link ); if( is_null( $title ) ) { return false; } } - # Make 'previous' link - if( 0 != $offset ) { - $po = $offset - $limit; - $po = max( $po, 0 ); - $q = "limit={$limit}&offset={$po}"; - if( $query != '' ) { - $q .= '&' . $query; - } - $plink = '<a href="' . $title->escapeLocalURL( $q ) . "\" title=\"{$pTitle}\" class=\"mw-prevlink\">{$prev}</a>"; - } else { - $plink = $prev; - } - # Make 'next' link - $no = $offset + $limit; - $q = "limit={$limit}&offset={$no}"; - if( $query != '' ) { - $q .= '&' . $query; - } - if( $atend ) { - $nlink = $next; - } else { - $nlink = '<a href="' . $title->escapeLocalURL( $q ) . "\" title=\"{$nTitle}\" class=\"mw-nextlink\">{$next}</a>"; - } - # Make links to set number of items per page - $nums = $wgLang->pipeList( array( - wfNumLink( $offset, 20, $title, $query ), - wfNumLink( $offset, 50, $title, $query ), - wfNumLink( $offset, 100, $title, $query ), - wfNumLink( $offset, 250, $title, $query ), - wfNumLink( $offset, 500, $title, $query ) - ) ); - return wfMsgHtml( 'viewprevnext', $plink, $nlink, $nums ); + + return $wgLang->viewPrevNext( $title, $offset, $limit, $query, $atend ); } /** - * Generate links for (20|50|100...) items-per-page links + * Make a list item, used by various special pages * - * @param $offset String - * @param $limit Integer - * @param $title Title - * @param $query String: optional URL query parameter string + * @param $page String Page link + * @param $details String Text between brackets + * @param $oppositedm Boolean Add the direction mark opposite to your + * language, to display text properly + * @return String + * @deprecated since 1.19; use Language::specialList() instead */ -function wfNumLink( $offset, $limit, $title, $query = '' ) { +function wfSpecialList( $page, $details, $oppositedm = true ) { global $wgLang; - if( $query == '' ) { - $q = ''; - } else { - $q = $query.'&'; - } - $q .= "limit={$limit}&offset={$offset}"; - $fmtLimit = $wgLang->formatNum( $limit ); - $lTitle = wfMsgExt( 'shown-title', array( 'parsemag', 'escape' ), $limit ); - $s = '<a href="' . $title->escapeLocalURL( $q ) . "\" title=\"{$lTitle}\" class=\"mw-numlink\">{$fmtLimit}</a>"; - return $s; + return $wgLang->specialList( $page, $details, $oppositedm ); } /** @@ -1675,7 +1884,7 @@ function wfClientAcceptsGzip( $force = false ) { $result = false; return $result; } - wfDebug( " accepts gzip\n" ); + wfDebug( "wfClientAcceptsGzip: client accepts gzip.\n" ); $result = true; } } @@ -1750,6 +1959,8 @@ function wfSetVar( &$dest, $source, $force = false ) { * @param $dest Int * @param $bit Int * @param $state Bool + * + * @return bool */ function wfSetBit( &$dest, $bit, $state = true ) { $temp = (bool)( $dest & $bit ); @@ -1764,217 +1975,6 @@ function wfSetBit( &$dest, $bit, $state = true ) { } /** - * Windows-compatible version of escapeshellarg() - * Windows doesn't recognise single-quotes in the shell, but the escapeshellarg() - * function puts single quotes in regardless of OS. - * - * Also fixes the locale problems on Linux in PHP 5.2.6+ (bug backported to - * earlier distro releases of PHP) - * - * @param varargs - * @return String - */ -function wfEscapeShellArg( ) { - wfInitShellLocale(); - - $args = func_get_args(); - $first = true; - $retVal = ''; - foreach ( $args as $arg ) { - if ( !$first ) { - $retVal .= ' '; - } else { - $first = false; - } - - if ( wfIsWindows() ) { - // Escaping for an MSVC-style command line parser and CMD.EXE - // Refs: - // * http://web.archive.org/web/20020708081031/http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html - // * http://technet.microsoft.com/en-us/library/cc723564.aspx - // * Bug #13518 - // * CR r63214 - // Double the backslashes before any double quotes. Escape the double quotes. - $tokens = preg_split( '/(\\\\*")/', $arg, -1, PREG_SPLIT_DELIM_CAPTURE ); - $arg = ''; - $iteration = 0; - foreach ( $tokens as $token ) { - if ( $iteration % 2 == 1 ) { - // Delimiter, a double quote preceded by zero or more slashes - $arg .= str_replace( '\\', '\\\\', substr( $token, 0, -1 ) ) . '\\"'; - } elseif ( $iteration % 4 == 2 ) { - // ^ in $token will be outside quotes, need to be escaped - $arg .= str_replace( '^', '^^', $token ); - } else { // $iteration % 4 == 0 - // ^ in $token will appear inside double quotes, so leave as is - $arg .= $token; - } - $iteration++; - } - // Double the backslashes before the end of the string, because - // we will soon add a quote - $m = array(); - if ( preg_match( '/^(.*?)(\\\\+)$/', $arg, $m ) ) { - $arg = $m[1] . str_replace( '\\', '\\\\', $m[2] ); - } - - // Add surrounding quotes - $retVal .= '"' . $arg . '"'; - } else { - $retVal .= escapeshellarg( $arg ); - } - } - return $retVal; -} - -/** - * wfMerge attempts to merge differences between three texts. - * Returns true for a clean merge and false for failure or a conflict. - * - * @param $old String - * @param $mine String - * @param $yours String - * @param $result String - * @return Bool - */ -function wfMerge( $old, $mine, $yours, &$result ) { - global $wgDiff3; - - # This check may also protect against code injection in - # case of broken installations. - wfSuppressWarnings(); - $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 ); - wfRestoreWarnings(); - - if( !$haveDiff3 ) { - wfDebug( "diff3 not found\n" ); - return false; - } - - # Make temporary files - $td = wfTempDir(); - $oldtextFile = fopen( $oldtextName = tempnam( $td, 'merge-old-' ), 'w' ); - $mytextFile = fopen( $mytextName = tempnam( $td, 'merge-mine-' ), 'w' ); - $yourtextFile = fopen( $yourtextName = tempnam( $td, 'merge-your-' ), 'w' ); - - fwrite( $oldtextFile, $old ); - fclose( $oldtextFile ); - fwrite( $mytextFile, $mine ); - fclose( $mytextFile ); - fwrite( $yourtextFile, $yours ); - fclose( $yourtextFile ); - - # Check for a conflict - $cmd = $wgDiff3 . ' -a --overlap-only ' . - wfEscapeShellArg( $mytextName ) . ' ' . - wfEscapeShellArg( $oldtextName ) . ' ' . - wfEscapeShellArg( $yourtextName ); - $handle = popen( $cmd, 'r' ); - - if( fgets( $handle, 1024 ) ) { - $conflict = true; - } else { - $conflict = false; - } - pclose( $handle ); - - # Merge differences - $cmd = $wgDiff3 . ' -a -e --merge ' . - wfEscapeShellArg( $mytextName, $oldtextName, $yourtextName ); - $handle = popen( $cmd, 'r' ); - $result = ''; - do { - $data = fread( $handle, 8192 ); - if ( strlen( $data ) == 0 ) { - break; - } - $result .= $data; - } while ( true ); - pclose( $handle ); - unlink( $mytextName ); - unlink( $oldtextName ); - unlink( $yourtextName ); - - if ( $result === '' && $old !== '' && !$conflict ) { - wfDebug( "Unexpected null result from diff3. Command: $cmd\n" ); - $conflict = true; - } - return !$conflict; -} - -/** - * Returns unified plain-text diff of two texts. - * Useful for machine processing of diffs. - * - * @param $before String: the text before the changes. - * @param $after String: the text after the changes. - * @param $params String: command-line options for the diff command. - * @return String: unified diff of $before and $after - */ -function wfDiff( $before, $after, $params = '-u' ) { - if ( $before == $after ) { - return ''; - } - - global $wgDiff; - wfSuppressWarnings(); - $haveDiff = $wgDiff && file_exists( $wgDiff ); - wfRestoreWarnings(); - - # This check may also protect against code injection in - # case of broken installations. - if( !$haveDiff ) { - wfDebug( "diff executable not found\n" ); - $diffs = new Diff( explode( "\n", $before ), explode( "\n", $after ) ); - $format = new UnifiedDiffFormatter(); - return $format->format( $diffs ); - } - - # Make temporary files - $td = wfTempDir(); - $oldtextFile = fopen( $oldtextName = tempnam( $td, 'merge-old-' ), 'w' ); - $newtextFile = fopen( $newtextName = tempnam( $td, 'merge-your-' ), 'w' ); - - fwrite( $oldtextFile, $before ); - fclose( $oldtextFile ); - fwrite( $newtextFile, $after ); - fclose( $newtextFile ); - - // Get the diff of the two files - $cmd = "$wgDiff " . $params . ' ' . wfEscapeShellArg( $oldtextName, $newtextName ); - - $h = popen( $cmd, 'r' ); - - $diff = ''; - - do { - $data = fread( $h, 8192 ); - if ( strlen( $data ) == 0 ) { - break; - } - $diff .= $data; - } while ( true ); - - // Clean up - pclose( $h ); - unlink( $oldtextName ); - unlink( $newtextName ); - - // Kill the --- and +++ lines. They're not useful. - $diff_lines = explode( "\n", $diff ); - if ( strpos( $diff_lines[0], '---' ) === 0 ) { - unset( $diff_lines[0] ); - } - if ( strpos( $diff_lines[1], '+++' ) === 0 ) { - unset( $diff_lines[1] ); - } - - $diff = implode( "\n", $diff_lines ); - - return $diff; -} - -/** * A wrapper around the PHP function var_export(). * Either print it or add it to the regular output ($wgOut). * @@ -2005,7 +2005,7 @@ function wfHttpError( $code, $label, $desc ) { $wgOut->sendCacheControl(); header( 'Content-type: text/html; charset=utf-8' ); - print "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">". + print "<!doctype html>" . '<html><head><title>' . htmlspecialchars( $label ) . '</title></head><body><h1>' . @@ -2161,7 +2161,7 @@ function mimeTypeMatch( $type, $avail ) { function wfNegotiateType( $cprefs, $sprefs ) { $combine = array(); - foreach( array_keys($sprefs) as $type ) { + foreach( array_keys( $sprefs ) as $type ) { $parts = explode( '/', $type ); if( $parts[1] != '*' ) { $ckey = mimeTypeMatch( $type, $cprefs ); @@ -2213,7 +2213,7 @@ function wfSuppressWarnings( $end = false ) { } else { if ( !$suppressCount ) { // E_DEPRECATED is undefined in PHP 5.2 - if( !defined( 'E_DEPRECATED' ) ){ + if( !defined( 'E_DEPRECATED' ) ) { define( 'E_DEPRECATED', 8192 ); } $originalLevel = error_reporting( E_ALL & ~( E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED ) ); @@ -2313,7 +2313,7 @@ function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) { } elseif ( preg_match( '/^-?\d{1,13}$/D', $ts ) ) { # TS_UNIX $uts = $ts; - $strtime = "@$ts"; // Undocumented? + $strtime = "@$ts"; // http://php.net/manual/en/datetime.formats.compound.php } elseif ( preg_match( '/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}.\d{6}$/', $ts ) ) { # TS_ORACLE // session altered to DD-MM-YYYY HH24:MI:SS.FF6 $strtime = preg_replace( '/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3", @@ -2326,7 +2326,7 @@ function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) { # TS_POSTGRES } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d* GMT$/', $ts, $da ) ) { # TS_POSTGRES - } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.\d\d\d$/',$ts,$da)) { + } elseif (preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.\d\d\d$/', $ts, $da ) ) { # TS_DB2 } elseif ( preg_match( '/^[ \t\r\n]*([A-Z][a-z]{2},[ \t\r\n]*)?' . # Day of week '\d\d?[ \t\r\n]*[A-Z][a-z]{2}[ \t\r\n]*\d{2}(?:\d{2})?' . # dd Mon yyyy @@ -2508,8 +2508,12 @@ function wfTempDir() { function wfMkdirParents( $dir, $mode = null, $caller = null ) { global $wgDirectoryMode; + if ( FileBackend::isStoragePath( $dir ) ) { // sanity + throw new MWException( __FUNCTION__ . " given storage path `$dir`."); + } + if ( !is_null( $caller ) ) { - wfDebug( "$caller: called wfMkdirParents($dir)" ); + wfDebug( "$caller: called wfMkdirParents($dir)\n" ); } if( strval( $dir ) === '' || file_exists( $dir ) ) { @@ -2586,6 +2590,29 @@ function wfIncrStats( $key, $count = 1 ) { } /** + * Remove a directory and all its content. + * Does not hide error. + */ +function wfRecursiveRemoveDir( $dir ) { + wfDebug( __FUNCTION__ . "( $dir )\n" ); + // taken from http://de3.php.net/manual/en/function.rmdir.php#98622 + if ( is_dir( $dir ) ) { + $objects = scandir( $dir ); + foreach ( $objects as $object ) { + if ( $object != "." && $object != ".." ) { + if ( filetype( $dir . '/' . $object ) == "dir" ) { + wfRecursiveRemoveDir( $dir . '/' . $object ); + } else { + unlink( $dir . '/' . $object ); + } + } + } + reset( $objects ); + rmdir( $dir ); + } +} + +/** * @param $nr Mixed: the number to format * @param $acc Integer: the number of digits after the decimal point, default 2 * @param $round Boolean: whether or not to round the value, default true @@ -2612,23 +2639,6 @@ function in_string( $needle, $str, $insensitive = false ) { } /** - * Make a list item, used by various special pages - * - * @param $page String Page link - * @param $details String Text between brackets - * @param $oppositedm Boolean Add the direction mark opposite to your - * language, to display text properly - * @return String - */ -function wfSpecialList( $page, $details, $oppositedm = true ) { - global $wgLang; - $dirmark = ( $oppositedm ? $wgLang->getDirMark( true ) : '' ) . - $wgLang->getDirMark(); - $details = $details ? $dirmark . " ($details)" : ''; - return $page . $details; -} - -/** * Safety wrapper around ini_get() for boolean settings. * The values returned from ini_get() are pre-normalized for settings * set via php.ini or php_flag/php_admin_flag... but *not* @@ -2696,6 +2706,70 @@ function wfDl( $extension, $fileName = null ) { } /** + * Windows-compatible version of escapeshellarg() + * Windows doesn't recognise single-quotes in the shell, but the escapeshellarg() + * function puts single quotes in regardless of OS. + * + * Also fixes the locale problems on Linux in PHP 5.2.6+ (bug backported to + * earlier distro releases of PHP) + * + * @param varargs + * @return String + */ +function wfEscapeShellArg( ) { + wfInitShellLocale(); + + $args = func_get_args(); + $first = true; + $retVal = ''; + foreach ( $args as $arg ) { + if ( !$first ) { + $retVal .= ' '; + } else { + $first = false; + } + + if ( wfIsWindows() ) { + // Escaping for an MSVC-style command line parser and CMD.EXE + // Refs: + // * http://web.archive.org/web/20020708081031/http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html + // * http://technet.microsoft.com/en-us/library/cc723564.aspx + // * Bug #13518 + // * CR r63214 + // Double the backslashes before any double quotes. Escape the double quotes. + $tokens = preg_split( '/(\\\\*")/', $arg, -1, PREG_SPLIT_DELIM_CAPTURE ); + $arg = ''; + $iteration = 0; + foreach ( $tokens as $token ) { + if ( $iteration % 2 == 1 ) { + // Delimiter, a double quote preceded by zero or more slashes + $arg .= str_replace( '\\', '\\\\', substr( $token, 0, -1 ) ) . '\\"'; + } elseif ( $iteration % 4 == 2 ) { + // ^ in $token will be outside quotes, need to be escaped + $arg .= str_replace( '^', '^^', $token ); + } else { // $iteration % 4 == 0 + // ^ in $token will appear inside double quotes, so leave as is + $arg .= $token; + } + $iteration++; + } + // Double the backslashes before the end of the string, because + // we will soon add a quote + $m = array(); + if ( preg_match( '/^(.*?)(\\\\+)$/', $arg, $m ) ) { + $arg = $m[1] . str_replace( '\\', '\\\\', $m[2] ); + } + + // Add surrounding quotes + $retVal .= '"' . $arg . '"'; + } else { + $retVal .= escapeshellarg( $arg ); + } + } + return $retVal; +} + +/** * Execute a shell command, with time and memory limits mirrored from the PHP * configuration if supported. * @param $cmd String Command line, properly escaped for shell. @@ -2805,6 +2879,178 @@ function wfInitShellLocale() { } /** + * Generate a shell-escaped command line string to run a maintenance script. + * Note that $parameters should be a flat array and an option with an argument + * should consist of two consecutive items in the array (do not use "--option value"). + * @param $script string MediaWiki maintenance script path + * @param $parameters Array Arguments and options to the script + * @param $options Array Associative array of options: + * 'php': The path to the php executable + * 'wrapper': Path to a PHP wrapper to handle the maintenance script + * @return Array + */ +function wfShellMaintenanceCmd( $script, array $parameters = array(), array $options = array() ) { + global $wgPhpCli; + // Give site config file a chance to run the script in a wrapper. + // The caller may likely want to call wfBasename() on $script. + wfRunHooks( 'wfShellMaintenanceCmd', array( &$script, &$parameters, &$options ) ); + $cmd = isset( $options['php'] ) ? array( $options['php'] ) : array( $wgPhpCli ); + if ( isset( $options['wrapper'] ) ) { + $cmd[] = $options['wrapper']; + } + $cmd[] = $script; + // Escape each parameter for shell + return implode( " ", array_map( 'wfEscapeShellArg', array_merge( $cmd, $parameters ) ) ); +} + +/** + * wfMerge attempts to merge differences between three texts. + * Returns true for a clean merge and false for failure or a conflict. + * + * @param $old String + * @param $mine String + * @param $yours String + * @param $result String + * @return Bool + */ +function wfMerge( $old, $mine, $yours, &$result ) { + global $wgDiff3; + + # This check may also protect against code injection in + # case of broken installations. + wfSuppressWarnings(); + $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 ); + wfRestoreWarnings(); + + if( !$haveDiff3 ) { + wfDebug( "diff3 not found\n" ); + return false; + } + + # Make temporary files + $td = wfTempDir(); + $oldtextFile = fopen( $oldtextName = tempnam( $td, 'merge-old-' ), 'w' ); + $mytextFile = fopen( $mytextName = tempnam( $td, 'merge-mine-' ), 'w' ); + $yourtextFile = fopen( $yourtextName = tempnam( $td, 'merge-your-' ), 'w' ); + + fwrite( $oldtextFile, $old ); + fclose( $oldtextFile ); + fwrite( $mytextFile, $mine ); + fclose( $mytextFile ); + fwrite( $yourtextFile, $yours ); + fclose( $yourtextFile ); + + # Check for a conflict + $cmd = $wgDiff3 . ' -a --overlap-only ' . + wfEscapeShellArg( $mytextName ) . ' ' . + wfEscapeShellArg( $oldtextName ) . ' ' . + wfEscapeShellArg( $yourtextName ); + $handle = popen( $cmd, 'r' ); + + if( fgets( $handle, 1024 ) ) { + $conflict = true; + } else { + $conflict = false; + } + pclose( $handle ); + + # Merge differences + $cmd = $wgDiff3 . ' -a -e --merge ' . + wfEscapeShellArg( $mytextName, $oldtextName, $yourtextName ); + $handle = popen( $cmd, 'r' ); + $result = ''; + do { + $data = fread( $handle, 8192 ); + if ( strlen( $data ) == 0 ) { + break; + } + $result .= $data; + } while ( true ); + pclose( $handle ); + unlink( $mytextName ); + unlink( $oldtextName ); + unlink( $yourtextName ); + + if ( $result === '' && $old !== '' && !$conflict ) { + wfDebug( "Unexpected null result from diff3. Command: $cmd\n" ); + $conflict = true; + } + return !$conflict; +} + +/** + * Returns unified plain-text diff of two texts. + * Useful for machine processing of diffs. + * + * @param $before String: the text before the changes. + * @param $after String: the text after the changes. + * @param $params String: command-line options for the diff command. + * @return String: unified diff of $before and $after + */ +function wfDiff( $before, $after, $params = '-u' ) { + if ( $before == $after ) { + return ''; + } + + global $wgDiff; + wfSuppressWarnings(); + $haveDiff = $wgDiff && file_exists( $wgDiff ); + wfRestoreWarnings(); + + # This check may also protect against code injection in + # case of broken installations. + if( !$haveDiff ) { + wfDebug( "diff executable not found\n" ); + $diffs = new Diff( explode( "\n", $before ), explode( "\n", $after ) ); + $format = new UnifiedDiffFormatter(); + return $format->format( $diffs ); + } + + # Make temporary files + $td = wfTempDir(); + $oldtextFile = fopen( $oldtextName = tempnam( $td, 'merge-old-' ), 'w' ); + $newtextFile = fopen( $newtextName = tempnam( $td, 'merge-your-' ), 'w' ); + + fwrite( $oldtextFile, $before ); + fclose( $oldtextFile ); + fwrite( $newtextFile, $after ); + fclose( $newtextFile ); + + // Get the diff of the two files + $cmd = "$wgDiff " . $params . ' ' . wfEscapeShellArg( $oldtextName, $newtextName ); + + $h = popen( $cmd, 'r' ); + + $diff = ''; + + do { + $data = fread( $h, 8192 ); + if ( strlen( $data ) == 0 ) { + break; + } + $diff .= $data; + } while ( true ); + + // Clean up + pclose( $h ); + unlink( $oldtextName ); + unlink( $newtextName ); + + // Kill the --- and +++ lines. They're not useful. + $diff_lines = explode( "\n", $diff ); + if ( strpos( $diff_lines[0], '---' ) === 0 ) { + unset( $diff_lines[0] ); + } + if ( strpos( $diff_lines[1], '+++' ) === 0 ) { + unset( $diff_lines[1] ); + } + + $diff = implode( "\n", $diff_lines ); + + return $diff; +} + +/** * This function works like "use VERSION" in Perl, the program will die with a * backtrace if the current version of PHP is less than the version provided * @@ -2920,35 +3166,13 @@ function wfRelativePath( $path, $from ) { /** * Do any deferred updates and clear the list * - * @param $commit String: set to 'commit' to commit after every update to - * prevent lock contention + * @deprecated since 1.19 + * @see DeferredUpdates::doUpdate() + * @param $commit string */ function wfDoUpdates( $commit = '' ) { - global $wgDeferredUpdateList; - - wfProfileIn( __METHOD__ ); - - // No need to get master connections in case of empty updates array - if ( !count( $wgDeferredUpdateList ) ) { - wfProfileOut( __METHOD__ ); - return; - } - - $doCommit = $commit == 'commit'; - if ( $doCommit ) { - $dbw = wfGetDB( DB_MASTER ); - } - - foreach ( $wgDeferredUpdateList as $update ) { - $update->doUpdate(); - - if ( $doCommit && $dbw->trxLevel() ) { - $dbw->commit(); - } - } - - $wgDeferredUpdateList = array(); - wfProfileOut( __METHOD__ ); + wfDeprecated( __METHOD__, '1.19' ); + DeferredUpdates::doUpdates( $commit ); } /** @@ -3043,13 +3267,17 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, $lowercase = t * * @param $name String * @param $p Array: parameters + * @return object * @deprecated since 1.18, warnings in 1.18, removal in 1.20 */ function wfCreateObject( $name, $p ) { - wfDeprecated( __FUNCTION__ ); + wfDeprecated( __FUNCTION__, '1.18' ); return MWFunction::newObj( $name, $p ); } +/** + * @return bool + */ function wfHttpOnlySafe() { global $wgHttpOnlyBlacklist; @@ -3164,8 +3392,10 @@ function wfGetPrecompiledData( $name ) { * @return String */ function wfMemcKey( /*... */ ) { + global $wgCachePrefix; + $prefix = $wgCachePrefix === false ? wfWikiID() : $wgCachePrefix; $args = func_get_args(); - $key = wfWikiID() . ':' . implode( ':', $args ); + $key = $prefix . ':' . implode( ':', $args ); $key = str_replace( ' ', '_', $key ); return $key; } @@ -3207,7 +3437,8 @@ function wfWikiID() { * Split a wiki ID into DB name and table prefix * * @param $wiki String - * @param $bits String + * + * @return array */ function wfSplitWikiID( $wiki ) { $bits = explode( '-', $wiki, 2 ); @@ -3290,14 +3521,23 @@ function wfFindFile( $title, $options = array() ) { * Get an object referring to a locally registered file. * Returns a valid placeholder object if the file does not exist. * - * @param $title Title or String - * @return File, or null if passed an invalid Title + * @param $title Title|String + * @return File|null A File, or null if passed an invalid Title */ function wfLocalFile( $title ) { return RepoGroup::singleton()->getLocalRepo()->newFile( $title ); } /** + * Stream a file to the browser. Back-compat alias for StreamFile::stream() + * @deprecated since 1.19 + */ +function wfStreamFile( $fname, $headers = array() ) { + wfDeprecated( __FUNCTION__, '1.19' ); + StreamFile::stream( $fname, $headers ); +} + +/** * Should low-performance queries be disabled? * * @return Boolean @@ -3364,7 +3604,7 @@ function wfBoolToStr( $value ) { * @codeCoverageIgnore */ function wfLoadExtensionMessages() { - wfDeprecated( __FUNCTION__ ); + wfDeprecated( __FUNCTION__, '1.16' ); } /** @@ -3379,59 +3619,6 @@ function wfGetNull() { } /** - * Throws a warning that $function is deprecated - * - * @param $function String - * @return null - */ -function wfDeprecated( $function ) { - static $functionsWarned = array(); - if ( !isset( $functionsWarned[$function] ) ) { - $functionsWarned[$function] = true; - wfWarn( "Use of $function is deprecated.", 2 ); - } -} - -/** - * Send a warning either to the debug log or in a PHP error depending on - * $wgDevelopmentWarnings - * - * @param $msg String: message to send - * @param $callerOffset Integer: number of items to go back in the backtrace to - * find the correct caller (1 = function calling wfWarn, ...) - * @param $level Integer: PHP error level; only used when $wgDevelopmentWarnings - * is true - */ -function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) { - global $wgDevelopmentWarnings; - - $callers = wfDebugBacktrace(); - if ( isset( $callers[$callerOffset + 1] ) ) { - $callerfunc = $callers[$callerOffset + 1]; - $callerfile = $callers[$callerOffset]; - if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) { - $file = $callerfile['file'] . ' at line ' . $callerfile['line']; - } else { - $file = '(internal function)'; - } - $func = ''; - if ( isset( $callerfunc['class'] ) ) { - $func .= $callerfunc['class'] . '::'; - } - if ( isset( $callerfunc['function'] ) ) { - $func .= $callerfunc['function']; - } - $msg .= " [Called from $func in $file]"; - } - - if ( $wgDevelopmentWarnings ) { - trigger_error( $msg, $level ); - } else { - wfDebug( "$msg\n" ); - } -} - -/** * Modern version of wfWaitForSlaves(). Instead of looking at replication lag * and waiting for it to go down, this waits for the slaves to catch up to the * master position. Use this when updating very large numbers of rows, as @@ -3440,7 +3627,6 @@ function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) { * * @param $maxLag Integer (deprecated) * @param $wiki mixed Wiki identifier accepted by wfGetLB - * @return null */ function wfWaitForSlaves( $maxLag = false, $wiki = false ) { $lb = wfGetLB( $wiki ); @@ -3458,7 +3644,7 @@ function wfWaitForSlaves( $maxLag = false, $wiki = false ) { * @deprecated since 1.18, warnings in 1.18, remove in 1.20 */ function wfOut( $s ) { - wfDeprecated( __METHOD__ ); + wfDeprecated( __FUNCTION__, '1.18' ); global $wgCommandLineMode; if ( $wgCommandLineMode ) { echo $s; @@ -3472,6 +3658,7 @@ function wfOut( $s ) { * Count down from $n to zero on the terminal, with a one-second pause * between showing each number. For use in command-line scripts. * @codeCoverageIgnore + * @param $n int */ function wfCountDown( $n ) { for ( $i = $n; $i >= 0; $i-- ) { @@ -3580,7 +3767,7 @@ function wfShorthandToInteger( $string = '' ) { * See unit test for examples. * * @param $code String: The language code. - * @return $langCode String: The language code which complying with BCP 47 standards. + * @return String: The language code which complying with BCP 47 standards. */ function wfBCP47( $code ) { $codeSegment = explode( '-', $code ); @@ -3588,7 +3775,7 @@ function wfBCP47( $code ) { foreach ( $codeSegment as $segNo => $seg ) { if ( count( $codeSegment ) > 0 ) { // when previous segment is x, it is a private segment and should be lc - if( $segNo > 0 && strtolower( $codeSegment[($segNo - 1)] ) == 'x') { + if( $segNo > 0 && strtolower( $codeSegment[( $segNo - 1 )] ) == 'x' ) { $codeBCP[$segNo] = strtolower( $seg ); // ISO 3166 country code } elseif ( ( strlen( $seg ) == 2 ) && ( $segNo > 0 ) ) { @@ -3654,7 +3841,7 @@ function wfGetParserCacheStorage() { * * @param $event String: event name * @param $args Array: parameters passed to hook functions - * @return Boolean + * @return Boolean True if no handler aborted the hook */ function wfRunHooks( $event, $args = array() ) { return Hooks::run( $event, $args ); diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php index 948de61f..7326bf5c 100644 --- a/includes/HTMLForm.php +++ b/includes/HTMLForm.php @@ -33,10 +33,10 @@ * 'help-message' -- message key for a message to use as a help text. * can be an array of msg key and then parameters to * the message. - * Overwrites 'help-messages'. - * 'help-messages' -- array of message key. As above, each item can - * be an array of msg key and then parameters. - * Overwrites 'help-message'. + * Overwrites 'help-messages'. + * 'help-messages' -- array of message key. As above, each item can + * be an array of msg key and then parameters. + * Overwrites 'help-message'. * 'required' -- passed through to the object, indicating that it * is a required field. * 'size' -- the length of text fields @@ -53,9 +53,9 @@ * * TODO: Document 'section' / 'subsection' stuff */ -class HTMLForm { +class HTMLForm extends ContextSource { - # A mapping of 'type' inputs onto standard HTMLFormField subclasses + // A mapping of 'type' inputs onto standard HTMLFormField subclasses static $typeMappings = array( 'text' => 'HTMLTextField', 'textarea' => 'HTMLTextAreaField', @@ -73,15 +73,18 @@ class HTMLForm { 'hidden' => 'HTMLHiddenField', 'edittools' => 'HTMLEditTools', - # HTMLTextField will output the correct type="" attribute automagically. - # There are about four zillion other HTML5 input types, like url, but - # we don't use those at the moment, so no point in adding all of them. + // HTMLTextField will output the correct type="" attribute automagically. + // There are about four zillion other HTML5 input types, like url, but + // we don't use those at the moment, so no point in adding all of them. 'email' => 'HTMLTextField', 'password' => 'HTMLTextField', ); protected $mMessagePrefix; + + /** @var HTMLFormField[] */ protected $mFlatFields; + protected $mFieldTree; protected $mShowReset = false; public $mFieldData; @@ -102,15 +105,30 @@ class HTMLForm { protected $mSubmitText; protected $mSubmitTooltip; - protected $mContext; // <! IContextSource protected $mTitle; protected $mMethod = 'post'; + /** + * Form action URL. false means we will use the URL to set Title + * @since 1.19 + * @var false|string + */ + protected $mAction = false; + protected $mUseMultipart = false; protected $mHiddenFields = array(); protected $mButtons = array(); protected $mWrapperLegend = false; + + /** + * If true, sections that contain both fields and subsections will + * render their subsections before their fields. + * + * Subclasses may set this to false to render subsections after fields + * instead. + */ + protected $mSubSectionBeforeFields = true; /** * Build a new HTMLForm from an array of field attributes @@ -121,7 +139,7 @@ class HTMLForm { */ public function __construct( $descriptor, /*IContextSource*/ $context = null, $messagePrefix = '' ) { if( $context instanceof IContextSource ){ - $this->mContext = $context; + $this->setContext( $context ); $this->mTitle = false; // We don't need them to set a title $this->mMessagePrefix = $messagePrefix; } else { @@ -175,11 +193,12 @@ class HTMLForm { * done already. * @deprecated since 1.18 load modules with ResourceLoader instead */ - static function addJS() { } + static function addJS() { wfDeprecated( __METHOD__, '1.18' ); } /** * Initialise a new Object for the field - * @param $descriptor input Descriptor, as described above + * @param $fieldname string + * @param $descriptor string input Descriptor, as described above * @return HTMLFormField subclass */ static function loadInputFromParameters( $fieldname, $descriptor ) { @@ -221,12 +240,27 @@ class HTMLForm { * @return Status|boolean */ function tryAuthorizedSubmit() { - $editToken = $this->getRequest()->getVal( 'wpEditToken' ); - $result = false; - if ( $this->getMethod() != 'post' || $this->getUser()->matchEditToken( $editToken ) ) { + + $submit = false; + if ( $this->getMethod() != 'post' ) { + $submit = true; // no session check needed + } elseif ( $this->getRequest()->wasPosted() ) { + $editToken = $this->getRequest()->getVal( 'wpEditToken' ); + if ( $this->getUser()->isLoggedIn() || $editToken != null ) { + // Session tokens for logged-out users have no security value. + // However, if the user gave one, check it in order to give a nice + // "session expired" error instead of "permission denied" or such. + $submit = $this->getUser()->matchEditToken( $editToken ); + } else { + $submit = true; + } + } + + if ( $submit ) { $result = $this->trySubmit(); } + return $result; } @@ -276,7 +310,7 @@ class HTMLForm { $data = $this->filterDataForSubmit( $this->mFieldData ); - $res = call_user_func( $callback, $data ); + $res = call_user_func( $callback, $data, $this ); return $res; } @@ -306,7 +340,16 @@ class HTMLForm { * Set the introductory message, overwriting any existing message. * @param $msg String complete text of message to display */ - function setIntro( $msg ) { $this->mPre = $msg; } + function setIntro( $msg ) { + $this->setPreText( $msg ); + } + + /** + * Set the introductory message, overwriting any existing message. + * @since 1.19 + * @param $msg String complete text of message to display + */ + function setPreText( $msg ) { $this->mPre = $msg; } /** * Add introductory text. @@ -317,7 +360,7 @@ class HTMLForm { /** * Add header text, inside the form. * @param $msg String complete text of message to display - * @param $section The section to add the header to + * @param $section string The section to add the header to */ function addHeaderText( $msg, $section = null ) { if ( is_null( $section ) ) { @@ -331,6 +374,20 @@ class HTMLForm { } /** + * Set header text, inside the form. + * @since 1.19 + * @param $msg String complete text of message to display + * @param $section The section to add the header to + */ + function setHeaderText( $msg, $section = null ) { + if ( is_null( $section ) ) { + $this->mHeader = $msg; + } else { + $this->mSectionHeaders[$section] = $msg; + } + } + + /** * Add footer text, inside the form. * @param $msg String complete text of message to display * @param $section string The section to add the footer text to @@ -347,12 +404,32 @@ class HTMLForm { } /** + * Set footer text, inside the form. + * @since 1.19 + * @param $msg String complete text of message to display + * @param $section string The section to add the footer text to + */ + function setFooterText( $msg, $section = null ) { + if ( is_null( $section ) ) { + $this->mFooter = $msg; + } else { + $this->mSectionFooters[$section] = $msg; + } + } + + /** * Add text to the end of the display. * @param $msg String complete text of message to display */ function addPostText( $msg ) { $this->mPost .= $msg; } /** + * Set text at the end of the display. + * @param $msg String complete text of message to display + */ + function setPostText( $msg ) { $this->mPost = $msg; } + + /** * Add a hidden field to the output * @param $name String field name. This will be used exactly as entered * @param $value String field value @@ -368,11 +445,20 @@ class HTMLForm { } /** - * Display the form (sending to wgOut), with an appropriate error + * Display the form (sending to $wgOut), with an appropriate error * message or stack of messages, and any validation errors, etc. * @param $submitResult Mixed output from HTMLForm::trySubmit() */ function displayForm( $submitResult ) { + $this->getOutput()->addHTML( $this->getHTML( $submitResult ) ); + } + + /** + * Returns the raw HTML generated by the form + * @param $submitResult Mixed output from HTMLForm::trySubmit() + * @return string + */ + function getHTML( $submitResult ) { # For good measure (it is the default) $this->getOutput()->preventClickjacking(); $this->getOutput()->addModules( 'mediawiki.htmlform' ); @@ -388,11 +474,7 @@ class HTMLForm { $html = $this->wrapForm( $html ); - $this->getOutput()->addHTML( '' - . $this->mPre - . $html - . $this->mPost - ); + return '' . $this->mPre . $html . $this->mPost; } /** @@ -412,7 +494,7 @@ class HTMLForm { : 'application/x-www-form-urlencoded'; # Attributes $attribs = array( - 'action' => $this->getTitle()->getFullURL(), + 'action' => $this->mAction === false ? $this->getTitle()->getFullURL() : $this->mAction, 'method' => $this->mMethod, 'class' => 'visualClear', 'enctype' => $encType, @@ -433,7 +515,7 @@ class HTMLForm { $html = ''; if( $this->getMethod() == 'post' ){ - $html .= Html::hidden( 'wpEditToken', $this->getUser()->editToken(), array( 'id' => 'wpEditToken' ) ) . "\n"; + $html .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken(), array( 'id' => 'wpEditToken' ) ) . "\n"; $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; } @@ -506,6 +588,7 @@ class HTMLForm { /** * Get the whole body of the form. + * @return String */ function getBody() { return $this->displaySection( $this->mFieldTree ); @@ -571,6 +654,15 @@ class HTMLForm { } /** + * Set the text for the submit button to a message + * @since 1.19 + * @param $msg String message key + */ + public function setSubmitTextMsg( $msg ) { + return $this->setSubmitText( $this->msg( $msg )->escaped() ); + } + + /** * Get the text for the submit button, either customised or a default. * @return unknown_type */ @@ -609,6 +701,16 @@ class HTMLForm { public function setWrapperLegend( $legend ) { $this->mWrapperLegend = $legend; } /** + * Prompt the whole form to be wrapped in a <fieldset>, with + * this message as its <legend> element. + * @since 1.19 + * @param $msg String message key + */ + public function setWrapperLegendMsg( $msg ) { + return $this->setWrapperLegend( $this->msg( $msg )->escaped() ); + } + + /** * Set the prefix for various default messages * TODO: currently only used for the <fieldset> legend on forms * with multiple sections; should be used elsewhre? @@ -637,36 +739,6 @@ class HTMLForm { } /** - * @return IContextSource - */ - public function getContext(){ - return $this->mContext instanceof IContextSource - ? $this->mContext - : RequestContext::getMain(); - } - - /** - * @return OutputPage - */ - public function getOutput(){ - return $this->getContext()->getOutput(); - } - - /** - * @return WebRequest - */ - public function getRequest(){ - return $this->getContext()->getRequest(); - } - - /** - * @return User - */ - public function getUser(){ - return $this->getContext()->getUser(); - } - - /** * Set the method used to submit the form * @param $method String */ @@ -680,9 +752,10 @@ class HTMLForm { /** * TODO: Document - * @param $fields array of fields (either arrays or objects) + * @param $fields array[]|HTMLFormField[] array of fields (either arrays or objects) * @param $sectionName string ID attribute of the <table> tag for this section, ignored if empty * @param $fieldsetIDPrefix string ID prefix for the <fieldset> tag of each subsection, ignored if empty + * @return String */ function displaySection( $fields, $sectionName = '', $fieldsetIDPrefix = '' ) { $tableHtml = ''; @@ -696,8 +769,9 @@ class HTMLForm { : $value->getDefault(); $tableHtml .= $value->getTableRow( $v ); - if ( $value->getLabel() != ' ' ) + if ( $value->getLabel() != ' ' ) { $hasLeftColumn = true; + } } elseif ( is_array( $value ) ) { $section = $this->displaySection( $value, $key ); $legend = $this->getLegend( $key ); @@ -732,7 +806,11 @@ class HTMLForm { $tableHtml = Html::rawElement( 'table', $attribs, Html::rawElement( 'tbody', array(), "\n$tableHtml\n" ) ) . "\n"; - return $subsectionHtml . "\n" . $tableHtml; + if ( $this->mSubSectionBeforeFields ) { + return $subsectionHtml . "\n" . $tableHtml; + } else { + return $tableHtml . "\n" . $subsectionHtml; + } } /** @@ -789,6 +867,19 @@ class HTMLForm { public function getLegend( $key ) { return wfMsg( "{$this->mMessagePrefix}-$key" ); } + + /** + * Set the value for the action attribute of the form. + * When set to false (which is the default state), the set title is used. + * + * @since 1.19 + * + * @param string|false $action + */ + public function setAction( $action ) { + $this->mAction = $action; + } + } /** @@ -830,20 +921,20 @@ abstract class HTMLFormField { * @return Mixed Bool true on success, or String error to display. */ function validate( $value, $alldata ) { - if ( isset( $this->mValidationCallback ) ) { - return call_user_func( $this->mValidationCallback, $value, $alldata ); - } - if ( isset( $this->mParams['required'] ) && $value === '' ) { return wfMsgExt( 'htmlform-required', 'parseinline' ); } + if ( isset( $this->mValidationCallback ) ) { + return call_user_func( $this->mValidationCallback, $value, $alldata, $this->mParent ); + } + return true; } function filter( $value, $alldata ) { if ( isset( $this->mFilterCallback ) ) { - $value = call_user_func( $this->mFilterCallback, $value, $alldata ); + $value = call_user_func( $this->mFilterCallback, $value, $alldata, $this->mParent ); } return $value; @@ -934,6 +1025,10 @@ abstract class HTMLFormField { if ( isset( $params['filter-callback'] ) ) { $this->mFilterCallback = $params['filter-callback']; } + + if ( isset( $params['flatlist'] ) ){ + $this->mClass .= ' mw-htmlform-flatlist'; + } } /** @@ -1055,7 +1150,7 @@ abstract class HTMLFormField { /** * flatten an array of options to a single array, for instance, * a set of <options> inside <optgroups>. - * @param $options Associative Array with values either Strings + * @param $options array Associative Array with values either Strings * or Arrays * @return Array flattened input */ @@ -1118,6 +1213,10 @@ class HTMLTextField extends HTMLFormField { 'value' => $value, ) + $this->getTooltipAndAccessKey(); + if ( $this->mClass !== '' ) { + $attribs['class'] = $this->mClass; + } + if ( isset( $this->mParams['maxlength'] ) ) { $attribs['maxlength'] = $this->mParams['maxlength']; } @@ -1188,7 +1287,10 @@ class HTMLTextAreaField extends HTMLFormField { 'rows' => $this->getRows(), ) + $this->getTooltipAndAccessKey(); - + if ( $this->mClass !== '' ) { + $attribs['class'] = $this->mClass; + } + if ( !empty( $this->mParams['disabled'] ) ) { $attribs['disabled'] = 'disabled'; } @@ -1295,6 +1397,10 @@ class HTMLCheckField extends HTMLFormField { if ( !empty( $this->mParams['disabled'] ) ) { $attr['disabled'] = 'disabled'; } + + if ( $this->mClass !== '' ) { + $attr['class'] = $this->mClass; + } return Xml::check( $this->mName, $value, $attr ) . ' ' . Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel ); @@ -1303,6 +1409,7 @@ class HTMLCheckField extends HTMLFormField { /** * For a checkbox, the label goes on the right hand side, and is * added in getInputHTML(), rather than HTMLFormField::getRow() + * @return String */ function getLabel() { return ' '; @@ -1371,6 +1478,10 @@ class HTMLSelectField extends HTMLFormField { if ( !empty( $this->mParams['disabled'] ) ) { $select->setAttribute( 'disabled', 'disabled' ); } + + if ( $this->mClass !== '' ) { + $select->setAttribute( 'class', $this->mClass ); + } $select->addOptions( $this->mParams['options'] ); @@ -1432,6 +1543,10 @@ class HTMLSelectOrOtherField extends HTMLTextField { if ( isset( $this->mParams['maxlength'] ) ) { $tbAttribs['maxlength'] = $this->mParams['maxlength']; } + + if ( $this->mClass !== '' ) { + $tbAttribs['class'] = $this->mClass; + } $textbox = Html::input( $this->mName . '-other', @@ -1467,13 +1582,6 @@ class HTMLSelectOrOtherField extends HTMLTextField { */ class HTMLMultiSelectField extends HTMLFormField { - public function __construct( $params ){ - parent::__construct( $params ); - if( isset( $params['flatlist'] ) ){ - $this->mClass .= ' mw-htmlform-multiselect-flatlist'; - } - } - function validate( $value, $alldata ) { $p = parent::validate( $value, $alldata ); @@ -1525,7 +1633,7 @@ class HTMLMultiSelectField extends HTMLFormField { $attribs + $thisAttribs ); $checkbox .= ' ' . Html::rawElement( 'label', array( 'for' => "{$this->mID}-$info" ), $label ); - $html .= ' ' . Html::rawElement( 'div', array( 'class' => 'mw-htmlform-multiselect-item' ), $checkbox ); + $html .= ' ' . Html::rawElement( 'div', array( 'class' => 'mw-htmlform-flatlist-item' ), $checkbox ); } } @@ -1655,6 +1763,10 @@ class HTMLSelectAndOtherField extends HTMLSelectField { 'id' => $this->mID . '-other', 'size' => $this->getSize(), ); + + if ( $this->mClass !== '' ) { + $textAttribs['class'] = $this->mClass; + } foreach ( array( 'required', 'autofocus', 'multiple', 'disabled' ) as $param ) { if ( isset( $this->mParams[$param] ) ) { @@ -1696,7 +1808,17 @@ class HTMLSelectAndOtherField extends HTMLSelectField { } else { $final = $this->getDefault(); - $list = $text = ''; + + $list = 'other'; + $text = $final; + foreach ( $this->mFlatOptions as $option ) { + $match = $option . wfMsgForContent( 'colon-separator' ); + if( strpos( $text, $match ) === 0 ) { + $list = $option; + $text = substr( $text, strlen( $match ) ); + break; + } + } } return array( $final, $list, $text ); } @@ -1729,6 +1851,8 @@ class HTMLSelectAndOtherField extends HTMLSelectField { * Radio checkbox fields. */ class HTMLRadioField extends HTMLFormField { + + function validate( $value, $alldata ) { $p = parent::validate( $value, $alldata ); @@ -1752,6 +1876,8 @@ class HTMLRadioField extends HTMLFormField { /** * This returns a block of all the radio options, in one cell. * @see includes/HTMLFormField#getInputHTML() + * @param $value String + * @return String */ function getInputHTML( $value ) { $html = $this->formatOptions( $this->mParams['options'], $value ); @@ -1774,16 +1900,16 @@ class HTMLRadioField extends HTMLFormField { $html .= $this->formatOptions( $info, $value ); } else { $id = Sanitizer::escapeId( $this->mID . "-$info" ); - $html .= Xml::radio( + $radio = Xml::radio( $this->mName, $info, $info == $value, $attribs + array( 'id' => $id ) ); - $html .= ' ' . + $radio .= ' ' . Html::rawElement( 'label', array( 'for' => $id ), $label ); - $html .= "<br />\n"; + $html .= ' ' . Html::rawElement( 'div', array( 'class' => 'mw-htmlform-flatlist-item' ), $radio ); } } @@ -1864,7 +1990,7 @@ class HTMLSubmitField extends HTMLFormField { return Xml::submitButton( $value, array( - 'class' => 'mw-htmlform-submit', + 'class' => 'mw-htmlform-submit ' . $this->mClass, 'name' => $this->mName, 'id' => $this->mID, ) @@ -1877,6 +2003,9 @@ class HTMLSubmitField extends HTMLFormField { /** * Button cannot be invalid + * @param $value String + * @param $alldata Array + * @return Bool */ public function validate( $value, $alldata ){ return true; diff --git a/includes/Hooks.php b/includes/Hooks.php index dd08d03b..e1c1d50b 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -86,7 +86,7 @@ class Hooks { * * @param $event String: event name * @param $args Array: parameters passed to hook functions - * @return Boolean + * @return Boolean True if no handler aborted the hook */ public static function run( $event, $args = array() ) { global $wgHooks; @@ -222,9 +222,7 @@ class Hooks { /* String return is an error; false return means stop processing. */ if ( is_string( $retval ) ) { - global $wgOut; - $wgOut->showFatalError( $retval ); - return false; + throw new FatalError( $retval ); } elseif( $retval === null ) { if ( $closure ) { $prettyFunc = "$event closure"; diff --git a/includes/Html.php b/includes/Html.php index be9a1e1b..c61a1baf 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -48,7 +48,7 @@ * @since 1.16 */ class Html { - # List of void elements from HTML5, section 9.1.2 as of 2009-08-10 + # List of void elements from HTML5, section 8.1.2 as of 2011-08-12 private static $voidElements = array( 'area', 'base', @@ -64,16 +64,19 @@ class Html { 'meta', 'param', 'source', + 'track', + 'wbr', ); # Boolean attributes, which may have the value omitted entirely. Manually - # collected from the HTML5 spec as of 2010-06-07. + # collected from the HTML5 spec as of 2011-08-12. private static $boolAttribs = array( 'async', 'autofocus', 'autoplay', 'checked', 'controls', + 'default', 'defer', 'disabled', 'formnovalidate', @@ -82,6 +85,7 @@ class Html { 'itemscope', 'loop', 'multiple', + 'muted', 'novalidate', 'open', 'pubdate', @@ -91,25 +95,40 @@ class Html { 'scoped', 'seamless', 'selected', + 'truespeed', + 'typemustmatch', + # HTML5 Microdata + 'itemscope', + ); + + private static $HTMLFiveOnlyAttribs = array( + 'autocomplete', + 'autofocus', + 'max', + 'min', + 'multiple', + 'pattern', + 'placeholder', + 'required', + 'step', + 'spellcheck', ); /** * Returns an HTML element in a string. The major advantage here over * manually typing out the HTML is that it will escape all attribute * values. If you're hardcoding all the attributes, or there are none, you - * should probably type out the string yourself. + * should probably just type out the html element yourself. * * This is quite similar to Xml::tags(), but it implements some useful * HTML-specific logic. For instance, there is no $allowShortTag * parameter: the closing tag is magically omitted if $element has an empty * content model. If $wgWellFormedXml is false, then a few bytes will be - * shaved off the HTML output as well. In the future, other HTML-specific - * features might be added, like allowing arrays for the values of - * attributes like class= and media=. + * shaved off the HTML output as well. * * @param $element string The element's name, e.g., 'a' * @param $attribs array Associative array of attributes, e.g., array( - * 'href' => 'http://www.mediawiki.org/' ). See expandAttributes() for + * 'href' => 'http://www.mediawiki.org/' ). See expandAttributes() for * further documentation. * @param $contents string The raw HTML contents of the element: *not* * escaped! @@ -352,6 +371,28 @@ class Html { * For instance, it will omit quotation marks if $wgWellFormedXml is false, * and will treat boolean attributes specially. * + * Attributes that should contain space-separated lists (such as 'class') array + * values are allowed as well, which will automagically be normalized + * and converted to a space-separated string. In addition to a numerical + * array, the attribute value may also be an associative array. See the + * example below for how that works. + * + * @par Numerical array + * @code + * Html::element( 'em', array( + * 'class' => array( 'foo', 'bar' ) + * ) ); + * // gives '<em class="foo bar"></em>' + * @endcode + * + * @par Associative array + * @code + * Html::element( 'em', array( + * 'class' => array( 'foo', 'bar', 'foo' => false, 'quux' => true ) + * ) ); + * // gives '<em class="bar quux"></em>' + * @endcode + * * @param $attribs array Associative array of attributes, e.g., array( * 'href' => 'http://www.mediawiki.org/' ). Values will be HTML-escaped. * A value of false means to omit the attribute. For boolean attributes, @@ -381,6 +422,12 @@ class Html { # and we'd like consistency and better compression anyway. $key = strtolower( $key ); + # Here we're blacklisting some HTML5-only attributes... + if ( !$wgHtml5 && in_array( $key, self::$HTMLFiveOnlyAttribs ) + ) { + continue; + } + # Bug 23769: Blacklist all form validation attributes for now. Current # (June 2010) WebKit has no UI, so the form just refuses to submit # without telling the user why, which is much worse than failing @@ -391,20 +438,53 @@ class Html { continue; } - # Here we're blacklisting some HTML5-only attributes... - if ( !$wgHtml5 && in_array( $key, array( - 'autocomplete', - 'autofocus', - 'max', - 'min', - 'multiple', - 'pattern', - 'placeholder', - 'required', - 'step', - 'spellcheck', - ) ) ) { - continue; + // http://www.w3.org/TR/html401/index/attributes.html ("space-separated") + // http://www.w3.org/TR/html5/index.html#attributes-1 ("space-separated") + $spaceSeparatedListAttributes = array( + 'class', // html4, html5 + 'accesskey', // as of html5, multiple space-separated values allowed + // html4-spec doesn't document rel= as space-separated + // but has been used like that and is now documented as such + // in the html5-spec. + 'rel', + ); + + # Specific features for attributes that allow a list of space-separated values + if ( in_array( $key, $spaceSeparatedListAttributes ) ) { + // Apply some normalization and remove duplicates + + // Convert into correct array. Array can contain space-seperated + // values. Implode/explode to get those into the main array as well. + if ( is_array( $value ) ) { + // If input wasn't an array, we can skip this step + + $newValue = array(); + foreach ( $value as $k => $v ) { + if ( is_string( $v ) ) { + // String values should be normal `array( 'foo' )` + // Just append them + if ( !isset( $value[$v] ) ) { + // As a special case don't set 'foo' if a + // separate 'foo' => true/false exists in the array + // keys should be authoritive + $newValue[] = $v; + } + } elseif ( $v ) { + // If the value is truthy but not a string this is likely + // an array( 'foo' => true ), falsy values don't add strings + $newValue[] = $k; + } + } + $value = implode( ' ', $newValue ); + } + $value = explode( ' ', $value ); + + // Normalize spacing by fixing up cases where people used + // more than 1 space and/or a trailing/leading space + $value = array_diff( $value, array( '', ' ' ) ); + + // Remove duplicates and create the string + $value = implode( ' ', array_unique( $value ) ); } # See the "Attributes" section in the HTML syntax part of HTML5, @@ -460,6 +540,7 @@ class Html { # @todo FIXME: Is this really true? $map['<'] = '<'; } + $ret .= " $key=$quote" . strtr( $value, $map ) . $quote; } } @@ -620,6 +701,77 @@ class Html { } return self::element( 'textarea', $attribs, $spacedValue ); } + /** + * Build a drop-down box for selecting a namespace + * + * @param $params array: + * - selected: [optional] Id of namespace which should be pre-selected + * - all: [optional] Value of item for "all namespaces". If null or unset, no <option> is generated to select all namespaces + * - label: text for label to add before the field + * @param $selectAttribs array HTML attributes for the generated select element. + * - id: [optional], default: 'namespace' + * - name: [optional], default: 'namespace' + * @return string HTML code to select a namespace. + */ + public static function namespaceSelector( Array $params = array(), Array $selectAttribs = array() ) { + global $wgContLang; + + // Default 'id' & 'name' <select> attributes + $selectAttribs = $selectAttribs + array( + 'id' => 'namespace', + 'name' => 'namespace', + ); + ksort( $selectAttribs ); + + // Is a namespace selected? + if ( isset( $params['selected'] ) ) { + // If string only contains digits, convert to clean int. Selected could also + // be "all" or "" etc. which needs to be left untouched. + // PHP is_numeric() has issues with large strings, PHP ctype_digit has other issues + // and returns false for already clean ints. Use regex instead.. + if ( preg_match( '/^\d+$/', $params['selected'] ) ) { + $params['selected'] = intval( $params['selected'] ); + } + // else: leaves it untouched for later processing + } else { + $params['selected'] = ''; + } + + // Array holding the <option> elements + $options = array(); + + if ( isset( $params['all'] ) ) { + // add an <option> that would let the user select all namespaces. + // Value is provided by user, the name shown is localized. + $options[$params['all']] = wfMsg( 'namespacesall' ); + } + // Add defaults <option> according to content language + $options += $wgContLang->getFormattedNamespaces(); + + // Convert $options to HTML + $optionsHtml = array(); + foreach ( $options as $nsId => $nsName ) { + if ( $nsId < NS_MAIN ) { + continue; + } + if ( $nsId === 0 ) { + $nsName = wfMsg( 'blanknamespace' ); + } + $optionsHtml[] = Xml::option( $nsName, $nsId, $nsId === $params['selected'] ); + } + + // Forge a <select> element and returns it + $ret = ''; + if ( isset( $params['label'] ) ) { + $ret .= Xml::label( $params['label'], $selectAttribs['id'] ) . ' '; + } + $ret .= Html::openElement( 'select', $selectAttribs ) + . "\n" + . implode( "\n", $optionsHtml ) + . "\n" + . Html::closeElement( 'select' ); + return $ret; + } /** * Constructs the opening html-tag with necessary doctypes depending on diff --git a/includes/HttpFunctions.old.php b/includes/HttpFunctions.old.php index ddfa608e..479b4d23 100644 --- a/includes/HttpFunctions.old.php +++ b/includes/HttpFunctions.old.php @@ -9,5 +9,4 @@ * This is for backwards compatibility. * @since 1.17 */ - class HttpRequest extends MWHttpRequest { } diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index a80fec17..147823fe 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -29,6 +29,8 @@ class Http { * - followRedirects Whether to follow redirects (defaults to false). * Note: this should only be used when the target URL is trusted, * to avoid attacks on intranet services accessible by HTTP. + * - userAgent A user agent, if you want to override the default + * MediaWiki/$wgVersion * @return Mixed: (bool)false on failure or a string on success */ public static function request( $method, $url, $options = array() ) { @@ -40,6 +42,9 @@ class Http { } $req = MWHttpRequest::factory( $url, $options ); + if( isset( $options['userAgent'] ) ) { + $req->setUserAgent( $options['userAgent'] ); + } $status = $req->execute(); if ( $status->isOK() ) { @@ -53,6 +58,9 @@ class Http { * Simple wrapper for Http::request( 'GET' ) * @see Http::request() * + * @param $url + * @param $timeout string + * @param $options array * @return string */ public static function get( $url, $timeout = 'default', $options = array() ) { @@ -64,6 +72,8 @@ class Http { * Simple wrapper for Http::request( 'POST' ) * @see Http::request() * + * @param $url + * @param $options array * @return string */ public static function post( $url, $options = array() ) { @@ -124,10 +134,12 @@ class Http { * protocols, because we only want protocols that both cURL * and php support. * + * file:// should not be allowed here for security purpose (r67684) + * * @fixme this is wildly inaccurate and fails to actually check most stuff * * @param $uri Mixed: URI to check for validity - * @returns Boolean + * @return Boolean */ public static function isValidURI( $uri ) { return preg_match( @@ -184,9 +196,9 @@ class MWHttpRequest { global $wgHTTPTimeout; $this->url = wfExpandUrl( $url, PROTO_HTTP ); - $this->parsedUrl = parse_url( $this->url ); + $this->parsedUrl = wfParseUrl( $this->url ); - if ( !Http::isValidURI( $this->url ) ) { + if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) { $this->status = Status::newFatal( 'http-invalid-url' ); } else { $this->status = Status::newGood( 100 ); // continue @@ -221,6 +233,7 @@ class MWHttpRequest { * Generate a new request object * @param $url String: url to use * @param $options Array: (optional) extra params to pass (see Http::request()) + * @return CurlHttpRequest|PhpHttpRequest * @see MWHttpRequest::__construct */ public static function factory( $url, $options = null ) { @@ -278,7 +291,7 @@ class MWHttpRequest { } if ( Http::isLocalURL( $this->url ) ) { - $this->proxy = 'http://localhost:80/'; + $this->proxy = ''; } elseif ( $wgHTTPProxy ) { $this->proxy = $wgHTTPProxy ; } elseif ( getenv( "http_proxy" ) ) { @@ -295,6 +308,7 @@ class MWHttpRequest { /** * Set the user agent + * @param $UA string */ public function setUserAgent( $UA ) { $this->setHeader( 'User-Agent', $UA ); @@ -302,6 +316,8 @@ class MWHttpRequest { /** * Set an arbitrary header + * @param $name + * @param $value */ public function setHeader( $name, $value ) { // I feel like I should normalize the case here... @@ -310,6 +326,7 @@ class MWHttpRequest { /** * Get an array of the headers + * @return array */ public function getHeaderList() { $list = array(); @@ -525,7 +542,7 @@ class MWHttpRequest { /** * Returns the cookie jar in use. * - * @returns CookieJar + * @return CookieJar */ public function getCookieJar() { if ( !$this->respHeaders ) { @@ -540,6 +557,9 @@ class MWHttpRequest { * cookies. Used internally after a request to parse the * Set-Cookie headers. * @see Cookie::set + * @param $name + * @param $value null + * @param $attr null */ public function setCookie( $name, $value = null, $attr = null ) { if ( !$this->cookieJar ) { @@ -568,13 +588,48 @@ class MWHttpRequest { /** * Returns the final URL after all redirections. * - * @return String + * Relative values of the "Location" header are incorrect as stated in RFC, however they do happen and modern browsers support them. + * This function loops backwards through all locations in order to build the proper absolute URI - Marooned at wikia-inc.com + * + * Note that the multiple Location: headers are an artifact of CURL -- they + * shouldn't actually get returned this way. Rewrite this when bug 29232 is + * taken care of (high-level redirect handling rewrite). + * + * @return string */ public function getFinalUrl() { - $location = $this->getResponseHeader( "Location" ); + $headers = $this->getResponseHeaders(); + + //return full url (fix for incorrect but handled relative location) + if ( isset( $headers[ 'location' ] ) ) { + $locations = $headers[ 'location' ]; + $domain = ''; + $foundRelativeURI = false; + $countLocations = count($locations); + + for ( $i = $countLocations - 1; $i >= 0; $i-- ) { + $url = parse_url( $locations[ $i ] ); + + if ( isset($url[ 'host' ]) ) { + $domain = $url[ 'scheme' ] . '://' . $url[ 'host' ]; + break; //found correct URI (with host) + } else { + $foundRelativeURI = true; + } + } - if ( $location ) { - return $location; + if ( $foundRelativeURI ) { + if ( $domain ) { + return $domain . $locations[ $countLocations - 1 ]; + } else { + $url = parse_url( $this->url ); + if ( isset($url[ 'host' ]) ) { + return $url[ 'scheme' ] . '://' . $url[ 'host' ] . $locations[ $countLocations - 1 ]; + } + } + } else { + return $locations[ $countLocations - 1 ]; + } } return $this->url; @@ -583,6 +638,7 @@ class MWHttpRequest { /** * Returns true if the backend can follow redirects. Overridden by the * child classes. + * @return bool */ public function canFollowRedirects() { return true; @@ -603,6 +659,11 @@ class CurlHttpRequest extends MWHttpRequest { protected $curlOptions = array(); protected $headerText = ""; + /** + * @param $fh + * @param $content + * @return int + */ protected function readHeader( $fh, $content ) { $this->headerText .= $content; return strlen( $content ); @@ -694,6 +755,9 @@ class CurlHttpRequest extends MWHttpRequest { return $this->status; } + /** + * @return bool + */ public function canFollowRedirects() { if ( strval( ini_get( 'open_basedir' ) ) !== '' || wfIniGetBool( 'safe_mode' ) ) { wfDebug( "Cannot follow redirects in safe mode\n" ); @@ -710,6 +774,11 @@ class CurlHttpRequest extends MWHttpRequest { } class PhpHttpRequest extends MWHttpRequest { + + /** + * @param $url string + * @return string + */ protected function urlToTcp( $url ) { $parsedUrl = parse_url( $url ); @@ -797,7 +866,7 @@ class PhpHttpRequest extends MWHttpRequest { # Check security of URL $url = $this->getResponseHeader( "Location" ); - if ( substr( $url, 0, 7 ) !== 'http://' ) { + if ( !Http::isValidURI( $url ) ) { wfDebug( __METHOD__ . ": insecure redirection\n" ); break; } diff --git a/includes/IP.php b/includes/IP.php index 1da7cd07..e3f61214 100644 --- a/includes/IP.php +++ b/includes/IP.php @@ -18,7 +18,7 @@ * http://www.gnu.org/copyleft/gpl.html * * @file - * @author Ashar Voultoiz <hashar at free dot fr>, Aaron Schulz + * @author Antoine Musso <hashar at free dot fr>, Aaron Schulz */ // Some regex definition to "play" with IP address and IP address blocks @@ -186,14 +186,14 @@ class IP { } /** - * Given a host/port string, like one might find in the host part of a URL - * per RFC 2732, split the hostname part and the port part and return an - * array with an element for each. If there is no port part, the array will - * have false in place of the port. If the string was invalid in some way, + * Given a host/port string, like one might find in the host part of a URL + * per RFC 2732, split the hostname part and the port part and return an + * array with an element for each. If there is no port part, the array will + * have false in place of the port. If the string was invalid in some way, * false is returned. * - * This was easy with IPv4 and was generally done in an ad-hoc way, but - * with IPv6 it's somewhat more complicated due to the need to parse the + * This was easy with IPv4 and was generally done in an ad-hoc way, but + * with IPv6 it's somewhat more complicated due to the need to parse the * square brackets and colons. * * A bare IPv6 address is accepted despite the lack of square brackets. @@ -241,8 +241,13 @@ class IP { /** * Given a host name and a port, combine them into host/port string like * you might find in a URL. If the host contains a colon, wrap it in square - * brackets like in RFC 2732. If the port matches the default port, omit + * brackets like in RFC 2732. If the port matches the default port, omit * the port specification + * + * @param $host string + * @param $port int + * @param $defaultPort bool|int + * @return string */ public static function combineHostAndPort( $host, $port, $defaultPort = false ) { if ( strpos( $host, ':' ) !== false ) { @@ -449,6 +454,10 @@ class IP { return $n; } + /** + * @param $ip + * @return String + */ private static function toUnsigned6( $ip ) { return wfBaseConvert( self::IPv6ToRawHex( $ip ), 16, 10 ); } @@ -548,6 +557,8 @@ class IP { * Convert a network specification in IPv6 CIDR notation to an * integer network and a number of bits * + * @param $range + * * @return array(string, int) */ private static function parseCIDR6( $range ) { @@ -585,6 +596,9 @@ class IP { * 2001:0db8:85a3::7344/96 CIDR * 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range * 2001:0db8:85a3::7344/96 Single IP + * + * @param $range + * * @return array(string, string) */ private static function parseRange6( $range ) { diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php index d048a9dd..4b90e24a 100644 --- a/includes/ImageFunctions.php +++ b/includes/ImageFunctions.php @@ -16,10 +16,11 @@ * * @param $name string the image name to check * @param $contextTitle Title|bool the page on which the image occurs, if known + * @param $blacklist string wikitext of a file blacklist * @return bool */ -function wfIsBadImage( $name, $contextTitle = false ) { - static $badImages = false; +function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) { + static $badImageCache = null; // based on bad_image_list msg wfProfileIn( __METHOD__ ); # Handle redirects @@ -34,11 +35,17 @@ function wfIsBadImage( $name, $contextTitle = false ) { wfProfileOut( __METHOD__ ); return $bad; } - - if( !$badImages ) { + + $cacheable = ( $blacklist === null ); + if( $cacheable && $badImageCache !== null ) { + $badImages = $badImageCache; + } else { // cache miss + if ( $blacklist === null ) { + $blacklist = wfMsgForContentNoTrans( 'bad_image_list' ); // site list + } # Build the list now $badImages = array(); - $lines = explode( "\n", wfMsgForContentNoTrans( 'bad_image_list' ) ); + $lines = explode( "\n", $blacklist ); foreach( $lines as $line ) { # List items only if ( substr( $line, 0, 1 ) !== '*' ) { @@ -68,6 +75,9 @@ function wfIsBadImage( $name, $contextTitle = false ) { $badImages[$imageDBkey] = $exceptions; } } + if ( $cacheable ) { + $badImageCache = $badImages; + } } $contextKey = $contextTitle ? $contextTitle->getPrefixedDBkey() : false; @@ -75,20 +85,3 @@ function wfIsBadImage( $name, $contextTitle = false ) { wfProfileOut( __METHOD__ ); return $bad; } - -/** - * Calculate the largest thumbnail width for a given original file size - * such that the thumbnail's height is at most $maxHeight. - * @param $boxWidth Integer Width of the thumbnail box. - * @param $boxHeight Integer Height of the thumbnail box. - * @param $maxHeight Integer Maximum height expected for the thumbnail. - * @return Integer. - */ -function wfFitBoxWidth( $boxWidth, $boxHeight, $maxHeight ) { - $idealWidth = $boxWidth * $maxHeight / $boxHeight; - $roundedUp = ceil( $idealWidth ); - if( round( $roundedUp * $boxHeight / $boxWidth ) > $maxHeight ) - return floor( $idealWidth ); - else - return $roundedUp; -} diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php index 4d5f067c..1106124a 100644 --- a/includes/ImageGallery.php +++ b/includes/ImageGallery.php @@ -28,9 +28,9 @@ class ImageGallery { * Contextual title, used when images are being screened * against the bad image list */ - private $contextTitle = false; + protected $contextTitle = false; - private $mAttribs = array(); + protected $mAttribs = array(); /** * Fixed margins @@ -131,7 +131,7 @@ class ImageGallery { * @deprecated since 1.18 Not used anymore */ function useSkin( $skin ) { - wfDeprecated( __METHOD__ ); + wfDeprecated( __METHOD__, '1.18' ); /* no op */ } @@ -249,11 +249,11 @@ class ImageGallery { # Get the file... if ( $this->mParser instanceof Parser ) { # Give extensions a chance to select the file revision for us - $time = $sha1 = false; + $options = array(); wfRunHooks( 'BeforeParserFetchFileAndTitle', - array( $this->mParser, $nt, &$time, &$sha1, &$descQuery ) ); + array( $this->mParser, $nt, &$options, &$descQuery ) ); # Fetch and register the file (file title may be different via hooks) - list( $img, $nt ) = $this->mParser->fetchFileAndTitle( $nt, $time, $sha1 ); + list( $img, $nt ) = $this->mParser->fetchFileAndTitle( $nt, $options ); } else { $img = wfFindFile( $nt ); } @@ -314,8 +314,7 @@ class ImageGallery { if( $this->mShowBytes ) { if( $img ) { - $fileSize = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'), - $wgLang->formatNum( $img->getSize() ) ); + $fileSize = htmlspecialchars( $wgLang->formatSize( $img->getSize() ) ); } else { $fileSize = wfMsgHtml( 'filemissing' ); } diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 956977e0..dcb09a41 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -18,6 +18,10 @@ class ImagePage extends Article { var $mExtraDescription = false; + /** + * @param $title Title + * @return WikiFilePage + */ protected function newPage( Title $title ) { // Overload mPage with a file-specific page return new WikiFilePage( $title ); @@ -99,13 +103,11 @@ class ImagePage extends Article { $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() ); $wgOut->addHTML( $this->viewRedirect( Title::makeTitle( NS_FILE, $this->mPage->getFile()->getName() ), /* $appendSubtitle */ true, /* $forceKnown */ true ) ); - $this->mPage->viewUpdates(); + $this->mPage->doViewUpdates( $this->getContext()->getUser() ); return; } } - $this->showRedirectedFromHeader(); - if ( $wgShowEXIF && $this->displayImg->exists() ) { // @todo FIXME: Bad interface, see note on MediaHandler::formatMetadata(). $formattedMetadata = $this->displayImg->formatMetadata(); @@ -136,7 +138,7 @@ class ImagePage extends Article { # Just need to set the right headers $wgOut->setArticleFlag( true ); $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() ); - $this->mPage->viewUpdates(); + $this->mPage->doViewUpdates( $this->getContext()->getUser() ); } # Show shared description, if needed @@ -184,6 +186,9 @@ class ImagePage extends Article { $wgOut->addModuleStyles( 'filepage' ); } + /** + * @return File + */ public function getDisplayedFile() { $this->loadFile(); return $this->displayImg; @@ -243,6 +248,7 @@ class ImagePage extends Article { * * Omit noarticletext if sharedupload; text will be fetched from the * shared upload server if possible. + * @return string */ public function getContent() { $this->loadFile(); @@ -254,7 +260,7 @@ class ImagePage extends Article { protected function openShowImage() { global $wgOut, $wgUser, $wgImageLimits, $wgRequest, - $wgLang, $wgEnableUploads; + $wgLang, $wgEnableUploads, $wgSend404Code; $this->loadFile(); @@ -322,12 +328,21 @@ class ImagePage extends Article { } $msgsmall = wfMessage( 'show-big-image-preview' )-> rawParams( $this->makeSizeLink( $params, $width, $height ) )-> - parse() . ' ' . - wfMessage( 'show-big-image-other' )-> - rawParams( $wgLang->pipeList( $otherSizes ) )->parse(); + parse(); + if ( count( $otherSizes ) && $this->displayImg->getRepo()->canTransformVia404() ) { + $msgsmall .= ' ' . + Html::rawElement( 'span', array( 'class' => 'mw-filepage-other-resolutions' ), + wfMessage( 'show-big-image-other' )->rawParams( $wgLang->pipeList( $otherSizes ) )-> + params( count( $otherSizes ) )->parse() + ); + } + } elseif ( $width == 0 && $height == 0 ){ + # Some sort of audio file that doesn't have dimensions + # Don't output a no hi res message for such a file + $msgsmall = ''; } else { # Image is small enough to show full size on image page - $msgsmall = wfMsgExt( 'file-nohires', array( 'parseinline' ) ); + $msgsmall = wfMessage( 'file-nohires' )->parse(); } $params['width'] = $width; @@ -335,7 +350,7 @@ class ImagePage extends Article { $thumbnail = $this->displayImg->transform( $params ); $showLink = true; - $anchorclose = '<br />' . $msgsmall; + $anchorclose = Html::rawElement( 'div', array( 'class' => 'mw-filepage-resolutioninfo' ), $msgsmall ); $isMulti = $this->displayImg->isMultipage() && $this->displayImg->pageCount() > 1; if ( $isMulti ) { @@ -392,7 +407,7 @@ class ImagePage extends Article { 'action' => $wgScript, 'onchange' => 'document.pageselector.submit();', ); - + $options = array(); for ( $i = 1; $i <= $count; $i++ ) { $options[] = Xml::option( $wgLang->formatNum( $i ), $i, $i == $page ); } @@ -407,7 +422,7 @@ class ImagePage extends Article { wfMsgExt( 'imgmultigoto', array( 'parseinline', 'replaceafter' ), $select ) . Xml::submitButton( wfMsg( 'imgmultigo' ) ) . Xml::closeElement( 'form' ) . - "<hr />$thumb1\n$thumb2<br clear=\"all\" /></div></td></tr></table>" + "<hr />$thumb1\n$thumb2<br style=\"clear: both\" /></div></td></tr></table>" ); } } else { @@ -467,7 +482,7 @@ EOT // by Article::View(). $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile ); - if ( !$this->getID() ) { + if ( !$this->getID() && $wgSend404Code ) { // If there is no image, no shared image, and no description page, // output a 404, to be consistent with articles. $wgRequest->response()->header( 'HTTP/1.1 404 Not Found' ); @@ -478,9 +493,10 @@ EOT /** * Creates an thumbnail of specified size and returns an HTML link to it - * @param array $params Scaler parameters - * @param int $width - * @param int $height + * @param $params array Scaler parameters + * @param $width int + * @param $height int + * @return string */ private function makeSizeLink( $params, $width, $height ) { $params['width'] = $width; @@ -609,6 +625,11 @@ EOT } } + /** + * @param $target + * @param $limit + * @return ResultWrapper + */ protected function queryImageLinks( $target, $limit ) { $dbr = wfGetDB( DB_SLAVE ); @@ -741,6 +762,9 @@ EOT ); $wgOut->addHTML( "<ul class='mw-imagepage-duplicates'>\n" ); + /** + * @var $file File + */ foreach ( $dupes as $file ) { $fromSrc = ''; if ( $file->isLocal() ) { @@ -765,29 +789,25 @@ EOT * Delete the file, or an earlier version of it */ public function delete() { - global $wgUploadMaintenance; - if ( $wgUploadMaintenance && $this->getTitle() && $this->getTitle()->getNamespace() == NS_FILE ) { - global $wgOut; - $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n", array( 'filedelete-maintenance' ) ); - return; - } - - $this->loadFile(); - if ( !$this->mPage->getFile()->exists() || !$this->mPage->getFile()->isLocal() || $this->mPage->getFile()->getRedirected() ) { + $file = $this->mPage->getFile(); + if ( !$file->exists() || !$file->isLocal() || $file->getRedirected() ) { // Standard article deletion parent::delete(); return; } - $deleter = new FileDeleteForm( $this->mPage->getFile() ); + + $deleter = new FileDeleteForm( $file ); $deleter->execute(); } /** * Display an error with a wikitext description + * + * @param $description String */ function showError( $description ) { global $wgOut; - $wgOut->setPageTitle( wfMsg( 'internalerror' ) ); + $wgOut->setPageTitle( wfMessage( 'internalerror' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); $wgOut->enableClientCache( false ); @@ -833,6 +853,11 @@ class ImageHistoryList { */ protected $imagePage; + /** + * @var File + */ + protected $current; + protected $repo, $showThumb; protected $preventClickjacking = false; @@ -848,14 +873,24 @@ class ImageHistoryList { $this->showThumb = $wgShowArchiveThumbnails && $this->img->canRender(); } + /** + * @return ImagePage + */ public function getImagePage() { return $this->imagePage; } + /** + * @return File + */ public function getFile() { return $this->img; } + /** + * @param $navLinks string + * @return string + */ public function beginImageHistoryList( $navLinks = '' ) { global $wgOut, $wgUser; return Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'filehist' ) ) . "\n" @@ -873,6 +908,10 @@ class ImageHistoryList { . "</tr>\n"; } + /** + * @param $navLinks string + * @return string + */ public function endImageHistoryList( $navLinks = '' ) { return "</table>\n$navLinks\n</div>\n"; } @@ -948,7 +987,7 @@ class ImageHistoryList { array( 'action' => 'revert', 'oldimage' => $img, - 'wpEditToken' => $wgUser->editToken( $img ) + 'wpEditToken' => $wgUser->getEditToken( $img ) ), array( 'known', 'noclasses' ) ); @@ -963,7 +1002,7 @@ class ImageHistoryList { $row .= "<td $selected style='white-space: nowrap;'>"; if ( !$file->userCan( File::DELETED_FILE ) ) { # Don't link to unviewable files - $row .= '<span class="history-deleted">' . $wgLang->timeAndDate( $timestamp, true ) . '</span>'; + $row .= '<span class="history-deleted">' . $wgLang->timeanddate( $timestamp, true ) . '</span>'; } elseif ( $file->isDeleted( File::DELETED_FILE ) ) { if ( $local ) { $this->preventClickjacking(); @@ -971,22 +1010,22 @@ class ImageHistoryList { # Make a link to review the image $url = Linker::link( $revdel, - $wgLang->timeAndDate( $timestamp, true ), + $wgLang->timeanddate( $timestamp, true ), array(), array( 'target' => $this->title->getPrefixedText(), 'file' => $img, - 'token' => $wgUser->editToken( $img ) + 'token' => $wgUser->getEditToken( $img ) ), array( 'known', 'noclasses' ) ); } else { - $url = $wgLang->timeAndDate( $timestamp, true ); + $url = $wgLang->timeanddate( $timestamp, true ); } $row .= '<span class="history-deleted">' . $url . '</span>'; } else { $url = $iscur ? $this->current->getUrl() : $this->current->getArchiveUrl( $img ); - $row .= Xml::element( 'a', array( 'href' => $url ), $wgLang->timeAndDate( $timestamp, true ) ); + $row .= Xml::element( 'a', array( 'href' => $url ), $wgLang->timeanddate( $timestamp, true ) ); } $row .= "</td>"; @@ -1020,7 +1059,7 @@ class ImageHistoryList { if ( $file->isDeleted( File::DELETED_COMMENT ) ) { $row .= '<td><span class="history-deleted">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></td>'; } else { - $row .= '<td dir="' . $wgContLang->getDir() . '">' . Linker::commentBlock( $description, $this->title ) . '</td>'; + $row .= '<td dir="' . $wgContLang->getDir() . '">' . Linker::formatComment( $description, $this->title ) . '</td>'; } $rowClass = null; @@ -1047,7 +1086,7 @@ class ImageHistoryList { $thumbnail = $file->transform( $params ); $options = array( 'alt' => wfMsg( 'filehist-thumbtext', - $wgLang->timeAndDate( $timestamp, true ), + $wgLang->timeanddate( $timestamp, true ), $wgLang->date( $timestamp, true ), $wgLang->time( $timestamp, true ) ), 'file-link' => true, @@ -1063,10 +1102,16 @@ class ImageHistoryList { } } + /** + * @param $enable bool + */ protected function preventClickjacking( $enable = true ) { $this->preventClickjacking = $enable; } + /** + * @return bool + */ public function getPreventClickjacking() { return $this->preventClickjacking; } @@ -1098,6 +1143,9 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager { $this->mRange = array( 0, 0 ); // display range } + /** + * @return Title + */ function getTitle() { return $this->mTitle; } @@ -1106,14 +1154,23 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager { return false; } + /** + * @return string + */ function getIndexField() { return ''; } + /** + * @return string + */ function formatRow( $row ) { return ''; } |