diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2008-08-15 01:29:47 +0200 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2008-08-15 01:29:47 +0200 |
commit | 370e83bb0dfd0c70de268c93bf07ad5ee0897192 (patch) | |
tree | 491674f4c242e4d6ba0d04eafa305174c35a3391 /includes | |
parent | f4debf0f12d0524d2b2427c55ea3f16b680fad97 (diff) |
Update auf 1.13.0
Diffstat (limited to 'includes')
348 files changed, 67745 insertions, 8718 deletions
diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index 7b85ed20..c489cf1c 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -1,5 +1,9 @@ <?php /** + * @defgroup Ajax Ajax + * + * @file + * @ingroup Ajax * Handle ajax requests and send them to the proper handler. */ @@ -11,7 +15,7 @@ require_once( 'AjaxFunctions.php' ); /** * Object-Oriented Ajax functions. - * @addtogroup Ajax + * @ingroup Ajax */ class AjaxDispatcher { /** The way the request was made, either a 'get' or a 'post' */ @@ -58,6 +62,7 @@ class AjaxDispatcher { break; default: + wfProfileOut( __METHOD__ ); return; # Or we could throw an exception: #throw new MWException( __METHOD__ . ' called without any data (mode empty).' ); @@ -81,9 +86,13 @@ class AjaxDispatcher { wfProfileIn( __METHOD__ ); if (! in_array( $this->func_name, $wgAjaxExportList ) ) { + wfDebug( __METHOD__ . ' Bad Request for unknown function ' . $this->func_name . "\n" ); + wfHttpError( 400, 'Bad Request', "unknown function " . (string) $this->func_name ); } else { + wfDebug( __METHOD__ . ' dispatching ' . $this->func_name . "\n" ); + if ( strpos( $this->func_name, '::' ) !== false ) { $func = explode( '::', $this->func_name, 2 ); } else { @@ -93,6 +102,10 @@ class AjaxDispatcher { $result = call_user_func_array($func, $this->args); if ( $result === false || $result === NULL ) { + wfDebug( __METHOD__ . ' ERROR while dispatching ' + . $this->func_name . "(" . var_export( $this->args, true ) . "): " + . "no data returned\n" ); + wfHttpError( 500, 'Internal Error', "{$this->func_name} returned no data" ); } @@ -103,9 +116,15 @@ class AjaxDispatcher { $result->sendHeaders(); $result->printText(); + + wfDebug( __METHOD__ . ' dispatch complete for ' . $this->func_name . "\n" ); } } catch (Exception $e) { + wfDebug( __METHOD__ . ' ERROR while dispatching ' + . $this->func_name . "(" . var_export( $this->args, true ) . "): " + . get_class($e) . ": " . $e->getMessage() . "\n" ); + if (!headers_sent()) { wfHttpError( 500, 'Internal Error', $e->getMessage() ); @@ -119,5 +138,3 @@ class AjaxDispatcher { $wgOut = null; } } - - diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php index ffd3168a..9daca9e5 100644 --- a/includes/AjaxFunctions.php +++ b/includes/AjaxFunctions.php @@ -1,8 +1,7 @@ <?php - -/** - * @package MediaWiki - * @addtogroup Ajax +/** + * @file + * @ingroup Ajax */ if( !defined( 'MEDIAWIKI' ) ) { @@ -56,7 +55,7 @@ function js_unescape($source, $iconv_to = 'UTF-8') { /** * Function coverts number of utf char into that character. - * Function taken from: http://sk2.php.net/manual/en/function.utf8-encode.php#49336 + * Function taken from: http://www.php.net/manual/en/function.utf8-encode.php#49336 * * @param $num Integer * @return utf8char @@ -76,7 +75,7 @@ function code2utf($num){ define( 'AJAX_SEARCH_VERSION', 2 ); //AJAX search cache version function wfSajaxSearch( $term ) { - global $wgContLang, $wgOut, $wgUser, $wgCapitalLinks, $wgMemc; + global $wgContLang, $wgUser, $wgCapitalLinks, $wgMemc; $limit = 16; $sk = $wgUser->getSkin(); $output = ''; @@ -84,7 +83,7 @@ function wfSajaxSearch( $term ) { $term = trim( $term ); $term = $wgContLang->checkTitleEncoding( $wgContLang->recodeInput( js_unescape( $term ) ) ); if ( $wgCapitalLinks ) - $term = $wgContLang->ucfirst( $term ); + $term = $wgContLang->ucfirst( $term ); $term_title = Title::newFromText( $term ); $memckey = $term_title ? wfMemcKey( 'ajaxsearch', md5( $term_title->getFullText() ) ) : wfMemcKey( 'ajaxsearch', md5( $term ) ); @@ -97,12 +96,12 @@ function wfSajaxSearch( $term ) { $r = $more = ''; $canSearch = true; - + $results = PrefixSearch::titleSearch( $term, $limit + 1 ); foreach( array_slice( $results, 0, $limit ) as $titleText ) { $r .= '<li>' . $sk->makeKnownLink( $titleText ) . "</li>\n"; } - + // Hack to check for specials if( $results ) { $t = Title::newFromText( $results[0] ); @@ -128,9 +127,10 @@ function wfSajaxSearch( $term ) { $valid = (bool) $term_title; $term_url = urlencode( $term ); - $term_diplay = htmlspecialchars( $valid ? $term_title->getFullText() : $term ); + $term_normalized = $valid ? $term_title->getFullText() : $term; + $term_display = htmlspecialchars( $term ); $subtitlemsg = ( $valid ? 'searchsubtitle' : 'searchsubtitleinvalid' ); - $subtitle = wfMsgWikiHtml( $subtitlemsg, $term_diplay ); + $subtitle = wfMsgExt( $subtitlemsg, array( 'parse' ), wfEscapeWikiText( $term_normalized ) ); $html = '<div id="searchTargetHide"><a onclick="Searching_Hide_Results();">' . wfMsgHtml( 'hideresults' ) . '</a></div>' . '<h1 class="firstHeading">'.wfMsgHtml('search') @@ -138,15 +138,15 @@ function wfSajaxSearch( $term ) { if( $canSearch ) { $html .= '<ul><li>' . $sk->makeKnownLink( $wgContLang->specialPage( 'Search' ), - wfMsgHtml( 'searchcontaining', $term_diplay ), + wfMsgHtml( 'searchcontaining', $term_display ), "search={$term_url}&fulltext=Search" ) . '</li><li>' . $sk->makeKnownLink( $wgContLang->specialPage( 'Search' ), - wfMsgHtml( 'searchnamed', $term_diplay ) , + wfMsgHtml( 'searchnamed', $term_display ) , "search={$term_url}&go=Go" ) . "</li></ul>"; } if( $r ) { - $html .= "<h2>" . wfMsgHtml( 'articletitles', $term_diplay ) . "</h2>" + $html .= "<h2>" . wfMsgHtml( 'articletitles', $term_display ) . "</h2>" . '<ul>' .$r .'</ul>' . $more; } @@ -161,7 +161,7 @@ function wfSajaxSearch( $term ) { * Called for AJAX watch/unwatch requests. * @param $pagename Prefixed title string for page to watch/unwatch * @param $watch String 'w' to watch, 'u' to unwatch - * @return String '<w#>' or '<u#>' on successful watch or unwatch, + * @return String '<w#>' or '<u#>' on successful watch or unwatch, * respectively, followed by an HTML message to display in the alert box; or * '<err#>' on error */ @@ -169,7 +169,7 @@ function wfAjaxWatch($pagename = "", $watch = "") { if(wfReadOnly()) { // redirect to action=(un)watch, which will display the database lock // message - return '<err#>'; + return '<err#>'; } if('w' !== $watch && 'u' !== $watch) { @@ -206,4 +206,3 @@ function wfAjaxWatch($pagename = "", $watch = "") { return '<u#>'.wfMsgExt( 'removedwatchtext', array( 'parse' ), $title->getPrefixedText() ); } } - diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index 8fa08539..c79e928b 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -1,11 +1,16 @@ <?php +/** + * @file + * @ingroup Ajax + */ + if( !defined( 'MEDIAWIKI' ) ) { die( 1 ); } /** * @todo document - * @addtogroup Ajax + * @ingroup Ajax */ class AjaxResponse { @@ -99,7 +104,7 @@ class AjaxResponse { if ( $this->mCacheDuration ) { - # If squid caches are configured, tell them to cache the response, + # If squid caches are configured, tell them to cache the response, # and tell the client to always check with the squid. Otherwise, # tell the client to use a cached copy, without a way to purge it. @@ -220,4 +225,3 @@ class AjaxResponse { return true; } } - diff --git a/includes/Article.php b/includes/Article.php index 0544db7d..4d8277bb 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -1,6 +1,7 @@ <?php /** * File for articles + * @file */ /** @@ -34,6 +35,8 @@ class Article { var $mTouched; //!< var $mUser; //!< var $mUserText; //!< + var $mRedirectTarget; //!< + var $mIsRedirect; /**@}}*/ /** @@ -57,12 +60,69 @@ class Article { } /** + * If this page is a redirect, get its target + * + * The target will be fetched from the redirect table if possible. + * If this page doesn't have an entry there, call insertRedirect() + * @return mixed Title object, or null if this page is not a redirect + */ + public function getRedirectTarget() { + if(!$this->mTitle || !$this->mTitle->isRedirect()) + return null; + if(!is_null($this->mRedirectTarget)) + return $this->mRedirectTarget; + + # Query the redirect table + $dbr = wfGetDB(DB_SLAVE); + $res = $dbr->select('redirect', + array('rd_namespace', 'rd_title'), + array('rd_from' => $this->getID()), + __METHOD__ + ); + $row = $dbr->fetchObject($res); + if($row) + return $this->mRedirectTarget = Title::makeTitle($row->rd_namespace, $row->rd_title); + + # This page doesn't have an entry in the redirect table + return $this->mRedirectTarget = $this->insertRedirect(); + } + + /** + * Insert an entry for this page into the redirect table. + * + * Don't call this function directly unless you know what you're doing. + * @return Title object + */ + public function insertRedirect() { + $retval = Title::newFromRedirect($this->getContent()); + if(!$retval) + return null; + $dbw = wfGetDB(DB_MASTER); + $dbw->replace('redirect', array('rd_from'), array( + 'rd_from' => $this->getID(), + 'rd_namespace' => $retval->getNamespace(), + 'rd_title' => $retval->getDBKey() + ), __METHOD__); + return $retval; + } + + /** + * Get the Title object this page redirects to + * * @return mixed false, Title of in-wiki target, or string with URL */ - function followRedirect() { + public function followRedirect() { $text = $this->getContent(); + return self::followRedirectText( $text ); + } + + /** + * Get the Title object this text redirects to + * + * @return mixed false, Title of in-wiki target, or string with URL + */ + public function followRedirectText( $text ) { $rt = Title::newFromRedirect( $text ); - # process if title object is valid and not special:userlogout if( $rt ) { if( $rt->getInterwiki() != '' ) { @@ -92,7 +152,6 @@ class Article { return $rt; } } - // No or invalid redirect return false; } @@ -114,6 +173,7 @@ class Article { $this->mCurID = $this->mUser = $this->mCounter = -1; # Not loaded $this->mRedirectedFrom = null; # Title object if set + $this->mRedirectTarget = null; # Title object if set $this->mUserText = $this->mTimestamp = $this->mComment = ''; $this->mGoodAdjustment = $this->mTotalAdjustment = 0; @@ -138,7 +198,7 @@ class Article { * @return Return the text of this revision */ function getContent() { - global $wgUser, $wgOut; + global $wgUser, $wgOut, $wgMessageCache; wfProfileIn( __METHOD__ ); @@ -147,12 +207,13 @@ class Article { $wgOut->setRobotpolicy( 'noindex,nofollow' ); if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + $wgMessageCache->loadAllMessages(); $ret = wfMsgWeirdKey ( $this->mTitle->getText() ) ; } else { $ret = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' ); } - return "<div class='noarticletext'>$ret</div>"; + return "<div class='noarticletext'>\n$ret\n</div>"; } else { $this->loadContent(); wfProfileOut( __METHOD__ ); @@ -298,13 +359,13 @@ class Article { */ function loadPageData( $data = 'fromdb' ) { if ( $data === 'fromdb' ) { - $dbr = $this->getDB(); + $dbr = wfGetDB( DB_MASTER ); $data = $this->pageDataFromId( $dbr, $this->getId() ); } - $lc =& LinkCache::singleton(); + $lc = LinkCache::singleton(); if ( $data ) { - $lc->addGoodLinkObj( $data->page_id, $this->mTitle ); + $lc->addGoodLinkObj( $data->page_id, $this->mTitle, $data->page_len, $data->page_is_redirect ); $this->mTitle->mArticleID = $data->page_id; @@ -336,15 +397,13 @@ class Article { return $this->mContent; } - $dbr = $this->getDB(); + $dbr = wfGetDB( DB_MASTER ); # Pre-fill content with error message so that if something # fails we'll have something telling us what we intended. $t = $this->mTitle->getPrefixedText(); - if( $oldid ) { - $t .= ',oldid='.$oldid; - } - $this->mContent = wfMsg( 'missingarticle', $t ) ; + $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : ''; + $this->mContent = wfMsg( 'missing-article', $t, $d ) ; if( $oldid ) { $revision = Revision::newFromId( $oldid ); @@ -377,15 +436,14 @@ class Article { // 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->userCan( Revision::DELETED_TEXT ) ? $revision->getRawText() : ""; - //$this->mContent = $revision->getText(); + $this->mContent = $revision->revText(); // Loads if user is allowed $this->mUser = $revision->getUser(); $this->mUserText = $revision->getUserText(); $this->mComment = $revision->getComment(); $this->mTimestamp = wfTimestamp( TS_MW, $revision->getTimestamp() ); - $this->mRevIdFetched = $revision->getID(); + $this->mRevIdFetched = $revision->getId(); $this->mContentLoaded = true; $this->mRevision =& $revision; @@ -407,8 +465,10 @@ class Article { * Get the database which should be used for reads * * @return Database + * @deprecated - just call wfGetDB( DB_MASTER ) instead */ function getDB() { + wfDeprecated( __METHOD__ ); return wfGetDB( DB_MASTER ); } @@ -490,6 +550,10 @@ class Article { */ function isRedirect( $text = false ) { if ( $text === false ) { + if ( $this->mDataLoaded ) + return $this->mIsRedirect; + + // Apparently loadPageData was never called $this->loadContent(); $titleObj = Title::newFromRedirect( $this->fetchContent() ); } else { @@ -526,14 +590,14 @@ class Article { $id = $this->getID(); if ( 0 == $id ) return; - $this->mLastRevision = Revision::loadFromPageId( $this->getDB(), $id ); + $this->mLastRevision = Revision::loadFromPageId( wfGetDB( DB_MASTER ), $id ); if( !is_null( $this->mLastRevision ) ) { $this->mUser = $this->mLastRevision->getUser(); $this->mUserText = $this->mLastRevision->getUserText(); $this->mTimestamp = $this->mLastRevision->getTimestamp(); $this->mComment = $this->mLastRevision->getComment(); $this->mMinorEdit = $this->mLastRevision->isMinor(); - $this->mRevIdFetched = $this->mLastRevision->getID(); + $this->mRevIdFetched = $this->mLastRevision->getId(); } } @@ -571,7 +635,6 @@ class Article { } /** - * @todo Document, fixme $offset never used. * @param $limit Integer: default 0. * @param $offset Integer: default 0. */ @@ -593,6 +656,8 @@ class Article { ORDER BY timestamp DESC"; if ($limit > 0) { $sql .= ' LIMIT '.$limit; } + if ($offset > 0) { $sql .= ' OFFSET '.$offset; } + $sql .= ' '. $this->getSelectOptions(); $res = $dbr->query($sql, __METHOD__); @@ -609,7 +674,7 @@ class Article { * This is the default action of the script: just view the page of * the given title. */ - function view() { + function view() { global $wgUser, $wgOut, $wgRequest, $wgContLang; global $wgEnableParserCache, $wgStylePath, $wgParser; global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies; @@ -618,7 +683,7 @@ class Article { wfProfileIn( __METHOD__ ); - $parserCache =& ParserCache::singleton(); + $parserCache = ParserCache::singleton(); $ns = $this->mTitle->getNamespace(); # shortcut # Get variables from query string @@ -689,12 +754,7 @@ class Article { } # Should the parser cache be used? - $pcache = $wgEnableParserCache - && intval( $wgUser->getOption( 'stubthreshold' ) ) == 0 - && $this->exists() - && empty( $oldid ) - && !$this->mTitle->isCssOrJsPage() - && !$this->mTitle->isCssJsSubpage(); + $pcache = $this->useParserCache( $oldid ); wfDebug( 'Article::view using parser cache: ' . ($pcache ? 'yes' : 'no' ) . "\n" ); if ( $wgUser->getOption( 'stubthreshold' ) ) { wfIncrStats( 'pcache_miss_stub' ); @@ -705,7 +765,6 @@ class Article { // This is an internally redirected page view. // We'll need a backlink to the source page for navigation. if ( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) { - $sk = $wgUser->getSkin(); $redir = $sk->makeKnownLinkObj( $this->mRedirectedFrom, '', 'redirect=no' ); $s = wfMsg( 'redirectedfrom', $redir ); $wgOut->setSubtitle( $s ); @@ -722,7 +781,6 @@ class Article { // If it was reported from a trusted site, supply a backlink. global $wgRedirectSources; if( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) { - $sk = $wgUser->getSkin(); $redir = $sk->makeExternalLink( $rdfrom, $rdfrom ); $s = wfMsg( 'redirectedfrom', $redir ); $wgOut->setSubtitle( $s ); @@ -740,16 +798,17 @@ class Article { $outputDone = true; } } + # Fetch content and check for errors if ( !$outputDone ) { $text = $this->getContent(); if ( $text === false ) { # Failed to load, replace text with error message $t = $this->mTitle->getPrefixedText(); if( $oldid ) { - $t .= ',oldid='.$oldid; - $text = wfMsg( 'missingarticle', $t ); + $d = wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ); + $text = wfMsg( 'missing-article', $t, $d ); } else { - $text = wfMsg( 'noarticletext', $t ); + $text = wfMsg( 'noarticletext' ); } } @@ -757,11 +816,11 @@ class Article { if ( !$this->mTitle->userCanRead() ) { $wgOut->loginToUse(); $wgOut->output(); + wfProfileOut( __METHOD__ ); exit; } # We're looking at an old revision - if ( !empty( $oldid ) ) { $wgOut->setRobotpolicy( 'noindex,nofollow' ); if( is_null( $this->mRevision ) ) { @@ -772,6 +831,7 @@ class Article { if( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) { $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + wfProfileOut( __METHOD__ ); return; } else { $wgOut->addWikiMsg( 'rev-deleted-text-view' ); @@ -779,16 +839,13 @@ class Article { } } } - } - } - if( !$outputDone ) { - $wgOut->setRevisionId( $this->getRevIdFetched() ); + $wgOut->setRevisionId( $this->getRevIdFetched() ); + // Pages containing custom CSS or JavaScript get special treatment if( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) { $wgOut->addHtml( wfMsgExt( 'clearyourcache', 'parse' ) ); - // Give hooks a chance to customise the output if( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->mTitle, $wgOut ) ) ) { // Wrap the whole lot in a <pre> and don't parse @@ -798,22 +855,9 @@ class Article { $wgOut->addHtml( htmlspecialchars( $this->mContent ) ); $wgOut->addHtml( "\n</pre>\n" ); } - - } - - elseif ( $rt = Title::newFromRedirect( $text ) ) { - # Display redirect - $imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr'; - $imageUrl = $wgStylePath.'/common/images/redirect' . $imageDir . '.png'; - # Don't overwrite the subtitle if this was an old revision - if( !$wasRedirected && $this->isCurrent() ) { - $wgOut->setSubtitle( wfMsgHtml( 'redirectpagesub' ) ); - } - $link = $sk->makeLinkObj( $rt, $rt->getFullText() ); - - $wgOut->addHTML( '<img src="'.$imageUrl.'" alt="#REDIRECT " />' . - '<span class="redirectText">'.$link.'</span>' ); - + } else if ( $rt = Title::newFromRedirect( $text ) ) { + # Don't append the subtitle if this was an old revision + $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ); $parseout = $wgParser->parse($text, $this->mTitle, ParserOptions::newFromUser($wgUser)); $wgOut->addParserOutputNoText( $parseout ); } else if ( $pcache ) { @@ -840,7 +884,6 @@ class Article { if( !$this->isCurrent() ) { $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); } - } } /* title may have been set from the cache */ @@ -850,8 +893,7 @@ class Article { } # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page - if( $ns == NS_USER_TALK && - User::isIP( $this->mTitle->getText() ) ) { + if( $ns == NS_USER_TALK && IP::isValid( $this->mTitle->getText() ) ) { $wgOut->addWikiMsg('anontalkpagetext'); } @@ -875,6 +917,41 @@ class Article { $this->viewUpdates(); wfProfileOut( __METHOD__ ); } + + /* + * Should the parser cache be used? + */ + protected function useParserCache( $oldid ) { + global $wgUser, $wgEnableParserCache; + + return $wgEnableParserCache + && intval( $wgUser->getOption( 'stubthreshold' ) ) == 0 + && $this->exists() + && empty( $oldid ) + && !$this->mTitle->isCssOrJsPage() + && !$this->mTitle->isCssJsSubpage(); + } + + protected function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) { + global $wgParser, $wgOut, $wgContLang, $wgStylePath, $wgUser; + + # Display redirect + $imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr'; + $imageUrl = $wgStylePath.'/common/images/redirect' . $imageDir . '.png'; + + if( $appendSubtitle ) { + $wgOut->appendSubtitle( wfMsgHtml( 'redirectpagesub' ) ); + } + $sk = $wgUser->getSkin(); + if ( $forceKnown ) + $link = $sk->makeKnownLinkObj( $target, htmlspecialchars( $target->getFullText() ) ); + else + $link = $sk->makeLinkObj( $target, htmlspecialchars( $target->getFullText() ) ); + + $wgOut->addHTML( '<img src="'.$imageUrl.'" alt="#REDIRECT " />' . + '<span class="redirectText">'.$link.'</span>' ); + + } function addTrackbacks() { global $wgOut, $wgUser; @@ -897,6 +974,7 @@ class Article { . $o->tb_id . "&token=" . urlencode( $wgUser->editToken() ) ); $rmvtxt = wfMsg( 'trackbackremove', htmlspecialchars( $delurl ) ); } + $tbtext .= "\n"; $tbtext .= wfMsg(strlen($o->tb_ex) ? 'trackbackexcerpt' : 'trackback', $o->tb_title, $o->tb_url, @@ -1067,7 +1145,6 @@ class Article { $result = $dbw->affectedRows() != 0; if ($result) { - // FIXME: Should the result from updateRedirectOn() be returned instead? $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); } @@ -1112,6 +1189,8 @@ class Article { $dbw->delete( 'redirect', $where, __METHOD__); } + if( $this->getTitle()->getNamespace() == NS_IMAGE ) + RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() ); wfProfileOut( __METHOD__ ); return ( $dbw->affectedRows() != 0 ); } @@ -1252,10 +1331,10 @@ class Article { } } - $extraq = ''; // Give extensions a chance to modify URL query on update - wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraq ) ); + $extraQuery = ''; // Give extensions a chance to modify URL query on update + wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraQuery ) ); - $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraq ); + $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraQuery ); } return $good; } @@ -1291,11 +1370,12 @@ class Article { * EDIT_NEW is specified and the article does exist, a duplicate key error will cause an exception * to be thrown from the Database. These two conditions are also possible with auto-detection due * to MediaWiki's performance-optimised locking strategy. + * @param $baseRevId, the revision ID this edit was based off, if any * * @return bool success */ - function doEdit( $text, $summary, $flags = 0 ) { - global $wgUser, $wgDBtransactions; + function doEdit( $text, $summary, $flags = 0, $baseRevId = false ) { + global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries; wfProfileIn( __METHOD__ ); $good = true; @@ -1325,9 +1405,10 @@ class Article { $oldtext = $this->getContent(); $oldsize = strlen( $oldtext ); - # Provide autosummaries if one is not provided. - if ($flags & EDIT_AUTOSUMMARY && $summary == '') + # Provide autosummaries if one is not provided and autosummaries are enabled. + if( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { $summary = $this->getAutosummary( $oldtext, $text, $flags ); + } $editInfo = $this->prepareTextForEdit( $text ); $text = $editInfo->pst; @@ -1346,7 +1427,7 @@ class Article { $lastRevision = 0; $revisionId = 0; - + $changed = ( strcmp( $text, $oldtext ) != 0 ); if ( $changed ) { @@ -1368,7 +1449,8 @@ class Article { 'page' => $this->getId(), 'comment' => $summary, 'minor_edit' => $isminor, - 'text' => $text + 'text' => $text, + 'parent_id' => $lastRevision ) ); $dbw->begin(); @@ -1382,6 +1464,8 @@ class Article { $good = false; $dbw->rollback(); } else { + wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId ) ); + # Update recentchanges if( !( $flags & EDIT_SUPPRESS_RC ) ) { $rcid = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $wgUser, $summary, @@ -1414,7 +1498,7 @@ class Article { # Invalidate cache of this article and all pages using this article # as a template. Partly deferred. Article::onArticleEdit( $this->mTitle ); - + # Update links tables, site stats, etc. $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed ); } @@ -1446,6 +1530,8 @@ class Article { # Update the page record with revision data $this->updateRevisionOn( $dbw, $revision, 0 ); + + wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false) ); if( !( $flags & EDIT_SUPPRESS_RC ) ) { $rcid = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, $bot, @@ -1495,15 +1581,16 @@ class Article { * * @param boolean $noRedir Add redirect=no * @param string $sectionAnchor section to redirect to, including "#" + * @param string $extraQuery, extra query params */ - function doRedirect( $noRedir = false, $sectionAnchor = '', $extraq = '' ) { + function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) { global $wgOut; if ( $noRedir ) { $query = 'redirect=no'; - if( $extraq ) + if( $extraQuery ) $query .= "&$query"; } else { - $query = $extraq; + $query = $extraQuery; } $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $sectionAnchor ); } @@ -1518,8 +1605,8 @@ class Article { # Check patrol config options if ( !($wgUseNPPatrol || $wgUseRCPatrol)) { - $wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); - return; + $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); + return; } # If we haven't been given an rc_id value, we can't do anything @@ -1527,18 +1614,18 @@ class Article { $rc = $rcid ? RecentChange::newFromId($rcid) : null; if ( is_null ( $rc ) ) { - $wgOut->errorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); + $wgOut->showErrorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); return; } - if ( !$wgUseRCPatrol && $rc->mAttribs['rc_type'] != RC_NEW) { + if ( !$wgUseRCPatrol && $rc->getAttribute( 'rc_type' ) != RC_NEW) { // Only new pages can be patrolled if the general patrolling is off....??? // @fixme -- is this necessary? Shouldn't we only bother controlling the // front end here? - $wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); + $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); return; } - + # Check permissions $permission_errors = $this->mTitle->getUserPermissionsErrors( 'patrol', $wgUser ); @@ -1554,7 +1641,7 @@ class Article { } #It would be nice to see where the user had actually come from, but for now just guess - $returnto = $rc->mAttribs['rc_type'] == RC_NEW ? 'Newpages' : 'Recentchanges'; + $returnto = $rc->getAttribute( 'rc_type' ) == RC_NEW ? 'Newpages' : 'Recentchanges'; $return = Title::makeTitle( NS_SPECIAL, $returnto ); # If it's left up to us, check that the user is allowed to patrol this edit @@ -1575,10 +1662,14 @@ class Article { } } - # Mark the edit as patrolled - RecentChange::markPatrolled( $rcid ); - PatrolLog::record( $rcid ); - wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) ); + # Check that the revision isn't patrolled already + # Prevents duplicate log entries + if( !$rc->getAttribute( 'rc_patrolled' ) ) { + # Mark the edit as patrolled + RecentChange::markPatrolled( $rcid ); + PatrolLog::record( $rcid ); + wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) ); + } # Inform the user $wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) ); @@ -1724,8 +1815,8 @@ class Article { $updated = Article::flattenRestrictions( $limit ); $changed = ( $current != $updated ); - $changed = $changed || ($this->mTitle->areRestrictionsCascading() != $cascade); - $changed = $changed || ($this->mTitle->mRestrictionsExpiry != $expiry); + $changed = $changed || ($updated && $this->mTitle->areRestrictionsCascading() != $cascade); + $changed = $changed || ($updated && $this->mTitle->mRestrictionsExpiry != $expiry); $protect = ( $updated != '' ); # If nothing's changed, do nothing @@ -1751,12 +1842,16 @@ class Article { } $comment = $wgContLang->ucfirst( wfMsgForContent( $comment_type, $this->mTitle->getPrefixedText() ) ); - foreach( $limit as $action => $restrictions ) { - # Check if the group level required to edit also can protect pages - # Otherwise, people who cannot normally protect can "protect" pages via transclusion - $cascade = ( $cascade && isset($wgGroupPermissions[$restrictions]['protect']) && $wgGroupPermissions[$restrictions]['protect'] ); + # Only restrictions with the 'protect' right can cascade... + # Otherwise, people who cannot normally protect can "protect" pages via transclusion + foreach( $limit as $action => $restriction ) { + # FIXME: can $restriction be an array or what? (same as fixme above) + if( $restriction != 'protect' && $restriction != 'sysop' ) { + $cascade = false; + break; + } } - + $cascade_description = ''; if ($cascade) { $cascade_description = ' ['.wfMsg('protect-summary-cascade').']'; @@ -1770,8 +1865,7 @@ class Article { $comment .= "$expiry_description"; if ( $cascade ) $comment .= "$cascade_description"; - - $rowsAffected = false; + # Update restrictions table foreach( $limit as $action => $restrictions ) { if ($restrictions != '' ) { @@ -1779,18 +1873,11 @@ class Article { array( 'pr_page' => $id, 'pr_type' => $action , 'pr_level' => $restrictions, 'pr_cascade' => $cascade ? 1 : 0 , 'pr_expiry' => $encodedExpiry ), __METHOD__ ); - if($dbw->affectedRows() != 0) - $rowsAffected = true; } else { $dbw->delete( 'page_restrictions', array( 'pr_page' => $id, 'pr_type' => $action ), __METHOD__ ); - if($dbw->affectedRows() != 0) - $rowsAffected = true; } } - if(!$rowsAffected) - // No change - return true; # Insert a null revision $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true ); @@ -1806,15 +1893,15 @@ class Article { 'page_id' => $id ), 'Article::protect' ); + + wfRunHooks( 'NewRevisionFromEditComplete', array($this, $nullRevision, false) ); wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) ); # Update the protection log $log = new LogPage( 'protect' ); - - - if( $protect ) { - $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason . " [$updated]$cascade_description$expiry_description" ) ); + $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, + trim( $reason . " [$updated]$cascade_description$expiry_description" ) ); } else { $log->addEntry( 'unprotect', $this->mTitle, $reason ); } @@ -1845,7 +1932,7 @@ class Article { } return implode( ':', $bits ); } - + /** * Auto-generates a deletion reason * @param bool &$hasHistory Whether the page has a history @@ -1907,7 +1994,7 @@ class Article { else $reason = wfMsgForContent('excontent', '$1'); } - + // Replace newlines with spaces to prevent uglyness $contents = preg_replace("/[\n\r]/", ' ', $contents); // Calculate the maximum amount of chars to get @@ -1930,18 +2017,20 @@ class Article { $confirm = $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ); - + $this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' ); $this->DeleteReason = $wgRequest->getText( 'wpReason' ); - + $reason = $this->DeleteReasonList; - + if ( $reason != 'other' && $this->DeleteReason != '') { // Entry from drop down menu + additional comment $reason .= ': ' . $this->DeleteReason; } elseif ( $reason == 'other' ) { $reason = $this->DeleteReason; } + # Flag to hide all contents of the archived revisions + $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed('suppressrevision'); # This code desperately needs to be totally rewritten @@ -1950,7 +2039,7 @@ class Article { $wgOut->readOnlyPage(); return; } - + # Check permissions $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser ); @@ -1980,7 +2069,7 @@ class Article { } if( $confirm ) { - $this->doDelete( $reason ); + $this->doDelete( $reason, $suppress ); if( $wgRequest->getCheck( 'wpWatch' ) ) { $this->doWatch(); } elseif( $this->mTitle->userIsWatching() ) { @@ -2003,10 +2092,10 @@ class Article { array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) ); } } - - return $this->confirmDelete( '', $reason ); + + return $this->confirmDelete( $reason ); } - + /** * @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions */ @@ -2018,7 +2107,7 @@ class Article { } return false; } - + /** * @return int approximate revision count */ @@ -2079,10 +2168,9 @@ class Article { /** * Output deletion confirmation dialog - * @param $par string FIXME: do we need this parameter? One Call from Article::delete with '' only. * @param $reason string Prefilled reason */ - function confirmDelete( $par, $reason ) { + function confirmDelete( $reason ) { global $wgOut, $wgUser, $wgContLang; $align = $wgContLang->isRtl() ? 'left' : 'right'; @@ -2092,9 +2180,17 @@ class Article { $wgOut->setRobotpolicy( 'noindex,nofollow' ); $wgOut->addWikiMsg( 'confirmdeletetext' ); - $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->mTitle->getLocalURL( 'action=delete' . $par ), 'id' => 'deleteconfirm' ) ) . - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', array(), wfMsg( 'delete-legend' ) ) . + if( $wgUser->isAllowed( 'suppressrevision' ) ) { + $suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\"><td></td><td>"; + $suppress .= Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '2' ) ); + $suppress .= "</td></tr>"; + } else { + $suppress = ''; + } + + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->mTitle->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) . + Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) . + Xml::tags( 'legend', null, wfMsgExt( 'delete-legend', array( 'parsemag', 'escapenoentities' ) ) ) . Xml::openElement( 'table' ) . "<tr id=\"wpDeleteReasonListRow\"> <td align='$align'>" . @@ -2102,7 +2198,7 @@ class Article { "</td> <td>" . Xml::listDropDown( 'wpDeleteReasonList', - wfMsgForContent( 'deletereason-dropdown' ), + wfMsgForContent( 'deletereason-dropdown' ), wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) . "</td> </tr> @@ -2120,6 +2216,7 @@ class Article { Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '3' ) ) . "</td> </tr> + $suppress <tr> <td></td> <td>" . @@ -2131,6 +2228,12 @@ class Article { Xml::hidden( 'wpEditToken', $wgUser->editToken() ) . Xml::closeElement( 'form' ); + if ( $wgUser->isAllowed( 'editinterface' ) ) { + $skin = $wgUser->getSkin(); + $link = $skin->makeLink ( 'MediaWiki:Deletereason-dropdown', wfMsgHtml( 'delete-edit-reasonlist' ) ); + $form .= '<p class="mw-delete-editreasons">' . $link . '</p>'; + } + $wgOut->addHTML( $form ); $this->showLogExtract( $wgOut ); } @@ -2140,25 +2243,24 @@ class Article { * Show relevant lines from the deletion log */ function showLogExtract( $out ) { - $out->addHtml( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . '</h2>' ); - $logViewer = new LogViewer( - new LogReader( - new FauxRequest( - array( 'page' => $this->mTitle->getPrefixedText(), - 'type' => 'delete' ) ) ) ); - $logViewer->showList( $out ); + $out->addHtml( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); + LogEventsList::showLogExtract( $out, 'delete', $this->mTitle->getPrefixedText() ); } /** * Perform a deletion and output success or failure messages */ - function doDelete( $reason ) { + function doDelete( $reason, $suppress = false ) { global $wgOut, $wgUser; wfDebug( __METHOD__."\n" ); + + $id = $this->getId(); + + $error = ''; - if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason))) { - if ( $this->doDeleteArticle( $reason ) ) { + if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason, &$error))) { + if ( $this->doDeleteArticle( $reason, $suppress ) ) { $deleted = $this->mTitle->getPrefixedText(); $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); @@ -2168,9 +2270,12 @@ class Article { $wgOut->addWikiMsg( 'deletedtext', $deleted, $loglink ); $wgOut->returnToMain( false ); - wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason)); + wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason, $id)); } else { - $wgOut->showFatalError( wfMsg( 'cannotdelete' ) ); + if ($error = '') + $wgOut->showFatalError( wfMsg( 'cannotdelete' ) ); + else + $wgOut->showFatalError( $error ); } } } @@ -2180,7 +2285,7 @@ class Article { * Deletes the article with database consistency, writes logs, purges caches * Returns success */ - function doDeleteArticle( $reason ) { + function doDeleteArticle( $reason, $suppress = false ) { global $wgUseSquid, $wgDeferredUpdateList; global $wgUseTrackbacks; @@ -2198,6 +2303,19 @@ class Article { $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getContent() ), -1 ); array_push( $wgDeferredUpdateList, $u ); + // Bitfields to further suppress the content + if ( $suppress ) { + $bitfield = 0; + // This should be 15... + $bitfield |= Revision::DELETED_TEXT; + $bitfield |= Revision::DELETED_COMMENT; + $bitfield |= Revision::DELETED_USER; + $bitfield |= Revision::DELETED_RESTRICTED; + } else { + $bitfield = 'rev_deleted'; + } + + $dbw->begin(); // For now, shunt the revision data into the archive table. // Text is *not* removed from the text table; bulk storage // is left intact to avoid breaking block-compression or @@ -2221,8 +2339,9 @@ class Article { 'ar_text_id' => 'rev_text_id', 'ar_text' => '\'\'', // Be explicit to appease 'ar_flags' => '\'\'', // MySQL's "strict mode"... - 'ar_len' => 'rev_len', + 'ar_len' => 'rev_len', 'ar_page_id' => 'page_id', + 'ar_deleted' => $bitfield ), array( 'page_id' => $id, 'page_id = rev_page' @@ -2232,12 +2351,25 @@ class Article { # Delete restrictions for it $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ ); + # Fix category table counts + $cats = array(); + $res = $dbw->select( 'categorylinks', 'cl_to', + array( 'cl_from' => $id ), __METHOD__ ); + foreach( $res as $row ) { + $cats []= $row->cl_to; + } + $this->updateCategoryCounts( array(), $cats ); + # Now that it's safely backed up, delete it $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__); + $ok = ( $dbw->affectedRows() > 0 ); // getArticleId() uses slave, could be laggy + if( !$ok ) { + $dbw->rollback(); + return false; + } # If using cascading deletes, we can skip some explicit deletes if ( !$dbw->cascadingDeletes() ) { - $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); if ($wgUseTrackbacks) @@ -2257,19 +2389,26 @@ class Article { if ( !$dbw->cleanupTriggers() ) { # Clean up recentchanges entries... - $dbw->delete( 'recentchanges', array( 'rc_namespace' => $ns, 'rc_title' => $t ), __METHOD__ ); + $dbw->delete( 'recentchanges', + array( 'rc_namespace' => $ns, 'rc_title' => $t, 'rc_type != '.RC_LOG ), + __METHOD__ ); } + $dbw->commit(); # Clear caches Article::onArticleDelete( $this->mTitle ); - # Log the deletion - $log = new LogPage( 'delete' ); - $log->addEntry( 'delete', $this->mTitle, $reason ); - # Clear the cached article id so the interface doesn't act like we exist $this->mTitle->resetArticleID( 0 ); $this->mTitle->mArticleID = 0; + + # Log the deletion, if the page was suppressed, log it at Oversight instead + $logtype = $suppress ? 'suppress' : 'delete'; + $log = new LogPage( $logtype ); + + # Make sure logging got through + $log->addEntry( 'delete', $this->mTitle, $reason, array() ); + return true; } @@ -2280,15 +2419,15 @@ class Article { * performs permissions checks on $wgUser, then calls commitRollback() * to do the dirty work * - * @param string $fromP - Name of the user whose edits to rollback. + * @param string $fromP - Name of the user whose edits to rollback. * @param string $summary - Custom summary. Set to default summary if empty. * @param string $token - Rollback token. * @param bool $bot - If true, mark all reverted edits as bot. - * + * * @param array $resultDetails contains result-specific array of additional values * 'alreadyrolled' : 'current' (rev) * success : 'summary' (str), 'current' (rev), 'target' (rev) - * + * * @return array of errors, each error formatted as * array(messagekey, param1, param2, ...). * On success, the array is empty. This array can also be passed to @@ -2310,10 +2449,10 @@ class Article { # If there were errors, bail out now if(!empty($errors)) return $errors; - + return $this->commitRollback($fromP, $summary, $bot, $resultDetails); } - + /** * Backend implementation of doRollback(), please refer there for parameter * and return value documentation @@ -2322,9 +2461,9 @@ class Article { * rollback to the DB Therefore, you should only call this function direct- * ly if you want to use custom permissions checks. If you don't, use * doRollback() instead. - */ + */ public function commitRollback($fromP, $summary, $bot, &$resultDetails) { - global $wgUseRCPatrol, $wgUser; + global $wgUseRCPatrol, $wgUser, $wgLang; $dbw = wfGetDB( DB_MASTER ); if( wfReadOnly() ) { @@ -2352,7 +2491,7 @@ class Article { $user = intval( $current->getUser() ); $user_text = $dbw->addQuotes( $current->getUserText() ); $s = $dbw->selectRow( 'revision', - array( 'rev_id', 'rev_timestamp' ), + array( 'rev_id', 'rev_timestamp', 'rev_deleted' ), array( 'rev_page' => $current->getPage(), "rev_user <> {$user} OR rev_user_text <> {$user_text}" ), __METHOD__, @@ -2362,8 +2501,11 @@ class Article { if( $s === false ) { # No one else ever edited this page return array(array('cantrollback')); + } else if( $s->rev_deleted & REVISION::DELETED_TEXT || $s->rev_deleted & REVISION::DELETED_USER ) { + # Only admins can see this text + return array(array('notvisiblerev')); } - + $set = array(); if ( $bot && $wgUser->isAllowed('markbotedits') ) { # Mark all reverted edits as bot @@ -2386,15 +2528,17 @@ class Article { # Generate the edit summary if necessary $target = Revision::newFromId( $s->rev_id ); - if( empty( $summary ) ) - { - global $wgLang; - $summary = wfMsgForContent( 'revertpage', - $target->getUserText(), $from, - $s->rev_id, $wgLang->timeanddate(wfTimestamp(TS_MW, $s->rev_timestamp), true), - $current->getId(), $wgLang->timeanddate($current->getTimestamp()) - ); + if( empty( $summary ) ){ + $summary = wfMsgForContent( 'revertpage' ); } + + # Allow the custom summary to use the same args as the default message + $args = array( + $target->getUserText(), $from, $s->rev_id, + $wgLang->timeanddate(wfTimestamp(TS_MW, $s->rev_timestamp), true), + $current->getId(), $wgLang->timeanddate($current->getTimestamp()) + ); + $summary = wfMsgReplaceArgs( $summary, $args ); # Save $flags = EDIT_UPDATE; @@ -2404,7 +2548,7 @@ class Article { if( $bot && ($wgUser->isAllowed('markbotedits') || $wgUser->isAllowed('bot')) ) $flags |= EDIT_FORCE_BOT; - $this->doEdit( $target->getText(), $summary, $flags ); + $this->doEdit( $target->getText(), $summary, $flags, $target->getId() ); wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target ) ); @@ -2439,6 +2583,19 @@ class Article { $wgOut->rateLimited(); return; } + if( isset( $result[0][0] ) && ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) ){ + $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); + $errArray = $result[0]; + $errMsg = array_shift( $errArray ); + $wgOut->addWikiMsgArray( $errMsg, $errArray ); + if( isset( $details['current'] ) ){ + $current = $details['current']; + if( $current->getComment() != '' ) { + $wgOut->addWikiMsgArray( 'editcomment', array( $wgUser->getSkin()->formatComment( $current->getComment() ) ), array( 'replaceafter' ) ); + } + } + return; + } # Display permissions errors before read-only message -- there's no # point in misleading the user into thinking the inability to rollback # is only temporary. @@ -2469,6 +2626,11 @@ class Article { . $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() ); $wgOut->addHtml( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) ); $wgOut->returnToMain( false, $this->mTitle ); + + if( !$wgRequest->getBool( 'hidediff', false ) ) { + $de = new DifferenceEngine( $this->mTitle, $current->getId(), 'next', false, true ); + $de->showDiff( '', '' ); + } } @@ -2477,11 +2639,12 @@ class Article { * @private */ function viewUpdates() { - global $wgDeferredUpdateList; + global $wgDeferredUpdateList, $wgUser; if ( 0 != $this->getID() ) { + # Don't update page view counters on views from bot users (bug 14044) global $wgDisableCounters; - if( !$wgDisableCounters ) { + if( !$wgDisableCounters && !$wgUser->isAllowed( 'bot' ) ) { Article::incViewCount( $this->getID() ); $u = new SiteStatsUpdate( 1, 0, 0 ); array_push( $wgDeferredUpdateList, $u ); @@ -2489,7 +2652,6 @@ class Article { } # Update newtalk / watchlist notification status - global $wgUser; $wgUser->clearNotification( $this->mTitle ); } @@ -2546,7 +2708,7 @@ class Article { # Save it to the parser cache if ( $wgEnableParserCache ) { - $parserCache =& ParserCache::singleton(); + $parserCache = ParserCache::singleton(); $parserCache->save( $editInfo->output, $this, $wgUser ); } @@ -2646,7 +2808,7 @@ class Article { $sk = $wgUser->getSkin(); $lnk = $current ? wfMsg( 'currentrevisionlink' ) - : $lnk = $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) ); + : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) ); $curdiff = $current ? wfMsg( 'diff' ) : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=cur&oldid='.$oldid ); @@ -2664,16 +2826,38 @@ class Article { ? wfMsg( 'diff' ) : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=next&oldid='.$oldid ); - $userlinks = $sk->userLink( $revision->getUser(), $revision->getUserText() ) - . $sk->userToolLinks( $revision->getUser(), $revision->getUserText() ); + $cdel=''; + if( $wgUser->isAllowed( 'deleterevision' ) ) { + $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); + if( $revision->isCurrent() ) { + // We don't handle top deleted edits too well + $cdel = wfMsgHtml('rev-delundel'); + } else if( !$revision->userCan( Revision::DELETED_RESTRICTED ) ) { + // If revision was hidden from sysops + $cdel = wfMsgHtml('rev-delundel'); + } else { + $cdel = $sk->makeKnownLinkObj( $revdel, + wfMsgHtml('rev-delundel'), + 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) . + '&oldid=' . urlencode( $oldid ) ); + // Bolden oversighted content + if( $revision->isDeleted( Revision::DELETED_RESTRICTED ) ) + $cdel = "<strong>$cdel</strong>"; + } + $cdel = "(<small>$cdel</small>) "; + } + # Show user links if allowed to see them. Normally they + # are hidden regardless, but since we can already see the text here... + $userlinks = $sk->revUserTools( $revision, false ); $m = wfMsg( 'revision-info-current' ); $infomsg = $current && !wfEmptyMsg( 'revision-info-current', $m ) && $m != '-' ? 'revision-info-current' : 'revision-info'; - + $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . wfMsg( $infomsg, $td, $userlinks ) . "</div>\n" . - "\n\t\t\t\t<div id=\"mw-revision-nav\">" . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t"; + + "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t"; $wgOut->setSubtitle( $r ); } @@ -2731,9 +2915,9 @@ class Article { $printable = $wgRequest->getVal( 'printable' ); $page = $wgRequest->getVal( 'page' ); - //check for non-standard user language; this covers uselang, + //check for non-standard user language; this covers uselang, //and extensions for auto-detecting user language. - $ulang = $wgLang->getCode(); + $ulang = $wgLang->getCode(); $clang = $wgContLang->getCode(); $cacheable = $wgUseFileCache @@ -2814,6 +2998,8 @@ class Article { $revision->insertOn( $dbw ); $this->updateRevisionOn( $dbw, $revision ); $dbw->commit(); + + wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false) ); wfProfileOut( __METHOD__ ); } @@ -2911,6 +3097,15 @@ class Article { static function onArticleDelete( $title ) { global $wgUseFileCache, $wgMessageCache; + // Update existence markers on article/talk tabs... + if( $title->isTalkPage() ) { + $other = $title->getSubjectPage(); + } else { + $other = $title->getTalkPage(); + } + $other->invalidateCache(); + $other->purgeSquid(); + $title->touchLinks(); $title->purgeSquid(); @@ -2920,13 +3115,20 @@ class Article { @unlink( $cm->fileCacheName() ); } - if( $title->getNamespace() == NS_MEDIAWIKI) { + # Messages + if( $title->getNamespace() == NS_MEDIAWIKI ) { $wgMessageCache->replace( $title->getDBkey(), false ); } + # Images if( $title->getNamespace() == NS_IMAGE ) { $update = new HTMLCacheUpdate( $title, 'imagelinks' ); $update->doUpdate(); } + # User talk pages + if( $title->getNamespace() == NS_USER_TALK ) { + $user = User::newFromName( $title->getText(), false ); + $user->setNewtalk( false ); + } } /** @@ -2940,7 +3142,7 @@ class Article { // Invalidate the caches of all pages which redirect here $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'redirect' ); - + # Purge squid for this page only $title->purgeSquid(); @@ -2954,6 +3156,15 @@ class Article { /**#@-*/ /** + * Overriden by ImagePage class, only present here to avoid a fatal error + * Called for ?action=revert + */ + public function revert(){ + global $wgOut; + $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' ); + } + + /** * Info about this page * Called for ?action=info when $wgAllowPageInfo is on. * @@ -3080,38 +3291,33 @@ class Article { } /** - * Return an auto-generated summary if the text provided is a redirect. + * Returns a list of hidden categories this page is a member of. + * Uses the page_props and categorylinks tables. * - * @param string $text The wikitext to check - * @return string '' or an appropriate summary + * @return array Array of Title objects */ - public static function getRedirectAutosummary( $text ) { - $rt = Title::newFromRedirect( $text ); - if( is_object( $rt ) ) - return wfMsgForContent( 'autoredircomment', $rt->getFullText() ); - else - return ''; - } + function getHiddenCategories() { + $result = array(); + $id = $this->mTitle->getArticleID(); + if( $id == 0 ) { + return array(); + } - /** - * Return an auto-generated summary if the new text is much shorter than - * the old text. - * - * @param string $oldtext The previous text of the page - * @param string $text The submitted text of the page - * @return string An appropriate autosummary, or an empty string. - */ - public static function getBlankingAutosummary( $oldtext, $text ) { - if ($oldtext!='' && $text=='') { - return wfMsgForContent('autosumm-blank'); - } elseif (strlen($oldtext) > 10 * strlen($text) && strlen($text) < 500) { - #Removing more than 90% of the article - global $wgContLang; - $truncatedtext = $wgContLang->truncate($text, max(0, 200 - strlen(wfMsgForContent('autosumm-replace'))), '...'); - return wfMsgForContent('autosumm-replace', $truncatedtext); - } else { - return ''; + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ), + array( 'cl_to' ), + array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat', + 'page_namespace' => NS_CATEGORY, 'page_title=cl_to'), + 'Article:getHiddenCategories' ); + if ( false !== $res ) { + if ( $dbr->numRows( $res ) ) { + while ( $row = $dbr->fetchObject( $res ) ) { + $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); + } + } } + $dbr->freeResult( $res ); + return $result; } /** @@ -3122,38 +3328,42 @@ class Article { * @return string An appropriate autosummary, or an empty string. */ public static function getAutosummary( $oldtext, $newtext, $flags ) { + # Decide what kind of autosummary is needed. - # This code is UGLY UGLY UGLY. - # Somebody PLEASE come up with a more elegant way to do it. - - #Redirect autosummaries - $summary = self::getRedirectAutosummary( $newtext ); - - if ($summary) - return $summary; - - #Blanking autosummaries - if (!($flags & EDIT_NEW)) - $summary = self::getBlankingAutosummary( $oldtext, $newtext ); - - if ($summary) - return $summary; + # Redirect autosummaries + $rt = Title::newFromRedirect( $newtext ); + if( is_object( $rt ) ) { + return wfMsgForContent( 'autoredircomment', $rt->getFullText() ); + } - #New page autosummaries - if ($flags & EDIT_NEW && strlen($newtext)) { - #If they're making a new article, give its text, truncated, in the summary. + # New page autosummaries + if( $flags & EDIT_NEW && strlen( $newtext ) ) { + # If they're making a new article, give its text, truncated, in the summary. global $wgContLang; $truncatedtext = $wgContLang->truncate( str_replace("\n", ' ', $newtext), max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new') ) ), '...' ); - $summary = wfMsgForContent( 'autosumm-new', $truncatedtext ); + return wfMsgForContent( 'autosumm-new', $truncatedtext ); } - if ($summary) - return $summary; + # Blanking autosummaries + if( $oldtext != '' && $newtext == '' ) { + return wfMsgForContent('autosumm-blank'); + } elseif( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500) { + # Removing more than 90% of the article + global $wgContLang; + $truncatedtext = $wgContLang->truncate( + $newtext, + max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ), + '...' + ); + return wfMsgForContent( 'autosumm-replace', $truncatedtext ); + } - return $summary; + # If we reach this point, there's no applicable autosummary for our case, so our + # autosummary is empty. + return ''; } /** @@ -3175,7 +3385,7 @@ class Article { $popts->setTidy(false); $popts->enableLimitReport( false ); if ( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) { - $parserCache =& ParserCache::singleton(); + $parserCache = ParserCache::singleton(); $parserCache->save( $parserOutput, $this, $wgUser ); } @@ -3235,4 +3445,60 @@ class Article { $wgOut->addParserOutput( $parserOutput ); } + /** + * Update all the appropriate counts in the category table, given that + * we've added the categories $added and deleted the categories $deleted. + * + * @param $added array The names of categories that were added + * @param $deleted array The names of categories that were deleted + * @return null + */ + public function updateCategoryCounts( $added, $deleted ) { + $ns = $this->mTitle->getNamespace(); + $dbw = wfGetDB( DB_MASTER ); + + # First make sure the rows exist. If one of the "deleted" ones didn't + # exist, we might legitimately not create it, but it's simpler to just + # create it and then give it a negative value, since the value is bogus + # anyway. + # + # Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE. + $insertCats = array_merge( $added, $deleted ); + if( !$insertCats ) { + # Okay, nothing to do + return; + } + $insertRows = array(); + foreach( $insertCats as $cat ) { + $insertRows[] = array( 'cat_title' => $cat ); + } + $dbw->insert( 'category', $insertRows, __METHOD__, 'IGNORE' ); + + $addFields = array( 'cat_pages = cat_pages + 1' ); + $removeFields = array( 'cat_pages = cat_pages - 1' ); + if( $ns == NS_CATEGORY ) { + $addFields[] = 'cat_subcats = cat_subcats + 1'; + $removeFields[] = 'cat_subcats = cat_subcats - 1'; + } elseif( $ns == NS_IMAGE ) { + $addFields[] = 'cat_files = cat_files + 1'; + $removeFields[] = 'cat_files = cat_files - 1'; + } + + if ( $added ) { + $dbw->update( + 'category', + $addFields, + array( 'cat_title' => $added ), + __METHOD__ + ); + } + if ( $deleted ) { + $dbw->update( + 'category', + $removeFields, + array( 'cat_title' => $deleted ), + __METHOD__ + ); + } + } } diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php index 2ad137e2..7717e001 100644 --- a/includes/AuthPlugin.php +++ b/includes/AuthPlugin.php @@ -230,7 +230,7 @@ class AuthPlugin { * @param $autocreate bool True if user is being autocreated on login * @public */ - function initUser( $user, $autocreate=false ) { + function initUser( &$user, $autocreate=false ) { # Override this to do something. } @@ -242,5 +242,3 @@ class AuthPlugin { return $username; } } - - diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 2e2083b2..4f36784a 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -4,352 +4,265 @@ ini_set('unserialize_callback_func', '__autoload' ); -function __autoload($className) { - global $wgAutoloadClasses; - +class AutoLoader { # Locations of core classes # Extension classes are specified with $wgAutoloadClasses static $localClasses = array( # Includes 'AjaxDispatcher' => 'includes/AjaxDispatcher.php', - 'AjaxCachePolicy' => 'includes/AjaxFunctions.php', 'AjaxResponse' => 'includes/AjaxResponse.php', 'AlphabeticPager' => 'includes/Pager.php', + 'APCBagOStuff' => 'includes/BagOStuff.php', + 'ArrayDiffFormatter' => 'includes/DifferenceEngine.php', 'Article' => 'includes/Article.php', + 'AtomFeed' => 'includes/Feed.php', 'AuthPlugin' => 'includes/AuthPlugin.php', 'Autopromote' => 'includes/Autopromote.php', 'BagOStuff' => 'includes/BagOStuff.php', - 'HashBagOStuff' => 'includes/BagOStuff.php', - 'SqlBagOStuff' => 'includes/BagOStuff.php', - 'MediaWikiBagOStuff' => 'includes/BagOStuff.php', - 'TurckBagOStuff' => 'includes/BagOStuff.php', - 'APCBagOStuff' => 'includes/BagOStuff.php', - 'eAccelBagOStuff' => 'includes/BagOStuff.php', - 'XCacheBagOStuff' => 'includes/BagOStuff.php', - 'DBABagOStuff' => 'includes/BagOStuff.php', 'Block' => 'includes/Block.php', - 'HTMLFileCache' => 'includes/HTMLFileCache.php', - 'DependencyWrapper' => 'includes/CacheDependency.php', - 'FileDependency' => 'includes/CacheDependency.php', - 'TitleDependency' => 'includes/CacheDependency.php', - 'TitleListDependency' => 'includes/CacheDependency.php', + 'CacheDependency' => 'includes/CacheDependency.php', + 'Category' => 'includes/Category.php', + 'Categoryfinder' => 'includes/Categoryfinder.php', 'CategoryPage' => 'includes/CategoryPage.php', 'CategoryViewer' => 'includes/CategoryPage.php', - 'Categoryfinder' => 'includes/Categoryfinder.php', - 'RCCacheEntry' => 'includes/ChangesList.php', 'ChangesList' => 'includes/ChangesList.php', - 'OldChangesList' => 'includes/ChangesList.php', - 'EnhancedChangesList' => 'includes/ChangesList.php', - 'CoreParserFunctions' => 'includes/CoreParserFunctions.php', - 'DBObject' => 'includes/Database.php', - 'Database' => 'includes/Database.php', - 'DatabaseMysql' => 'includes/Database.php', - 'ResultWrapper' => 'includes/Database.php', - 'DatabasePostgres' => 'includes/DatabasePostgres.php', - 'DatabaseOracle' => 'includes/DatabaseOracle.php', - 'DateFormatter' => 'includes/DateFormatter.php', + 'ChangesFeed' => 'includes/ChangesFeed.php', + 'ChannelFeed' => 'includes/Feed.php', + 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php', + 'ConstantDependency' => 'includes/CacheDependency.php', + 'DBABagOStuff' => 'includes/BagOStuff.php', + 'DependencyWrapper' => 'includes/CacheDependency.php', + '_DiffEngine' => 'includes/DifferenceEngine.php', 'DifferenceEngine' => 'includes/DifferenceEngine.php', - '_DiffOp' => 'includes/DifferenceEngine.php', - '_DiffOp_Copy' => 'includes/DifferenceEngine.php', - '_DiffOp_Delete' => 'includes/DifferenceEngine.php', + 'DiffFormatter' => 'includes/DifferenceEngine.php', + 'Diff' => 'includes/DifferenceEngine.php', '_DiffOp_Add' => 'includes/DifferenceEngine.php', '_DiffOp_Change' => 'includes/DifferenceEngine.php', - '_DiffEngine' => 'includes/DifferenceEngine.php', - 'Diff' => 'includes/DifferenceEngine.php', - 'MappedDiff' => 'includes/DifferenceEngine.php', - 'DiffFormatter' => 'includes/DifferenceEngine.php', - 'UnifiedDiffFormatter' => 'includes/DifferenceEngine.php', - 'ArrayDiffFormatter' => 'includes/DifferenceEngine.php', + '_DiffOp_Copy' => 'includes/DifferenceEngine.php', + '_DiffOp_Delete' => 'includes/DifferenceEngine.php', + '_DiffOp' => 'includes/DifferenceEngine.php', 'DjVuImage' => 'includes/DjVuImage.php', - '_HWLDF_WordAccumulator' => 'includes/DifferenceEngine.php', - 'WordLevelDiff' => 'includes/DifferenceEngine.php', - 'TableDiffFormatter' => 'includes/DifferenceEngine.php', - 'EditPage' => 'includes/EditPage.php', - 'MWException' => 'includes/Exception.php', - 'Exif' => 'includes/Exif.php', - 'FormatExif' => 'includes/Exif.php', - 'WikiExporter' => 'includes/Export.php', - 'XmlDumpWriter' => 'includes/Export.php', - 'DumpOutput' => 'includes/Export.php', - 'DumpFileOutput' => 'includes/Export.php', - 'DumpPipeOutput' => 'includes/Export.php', - 'DumpGZipOutput' => 'includes/Export.php', - 'DumpBZip2Output' => 'includes/Export.php', + 'DoubleReplacer' => 'includes/StringUtils.php', + 'DoubleRedirectJob' => 'includes/DoubleRedirectJob.php', 'Dump7ZipOutput' => 'includes/Export.php', + 'DumpBZip2Output' => 'includes/Export.php', + 'DumpFileOutput' => 'includes/Export.php', 'DumpFilter' => 'includes/Export.php', - 'DumpNotalkFilter' => 'includes/Export.php', - 'DumpNamespaceFilter' => 'includes/Export.php', + 'DumpGZipOutput' => 'includes/Export.php', 'DumpLatestFilter' => 'includes/Export.php', 'DumpMultiWriter' => 'includes/Export.php', + 'DumpNamespaceFilter' => 'includes/Export.php', + 'DumpNotalkFilter' => 'includes/Export.php', + 'DumpOutput' => 'includes/Export.php', + 'DumpPipeOutput' => 'includes/Export.php', + 'eAccelBagOStuff' => 'includes/BagOStuff.php', + 'EditPage' => 'includes/EditPage.php', + 'EmaillingJob' => 'includes/EmaillingJob.php', + 'EmailNotification' => 'includes/UserMailer.php', + 'EnhancedChangesList' => 'includes/ChangesList.php', + 'EnotifNotifyJob' => 'includes/EnotifNotifyJob.php', + 'ErrorPageError' => 'includes/Exception.php', + 'Exif' => 'includes/Exif.php', 'ExternalEdit' => 'includes/ExternalEdit.php', - 'ExternalStore' => 'includes/ExternalStore.php', 'ExternalStoreDB' => 'includes/ExternalStoreDB.php', 'ExternalStoreHttp' => 'includes/ExternalStoreHttp.php', + 'ExternalStore' => 'includes/ExternalStore.php', + 'FatalError' => 'includes/Exception.php', 'FakeTitle' => 'includes/FakeTitle.php', + 'FauxRequest' => 'includes/WebRequest.php', 'FeedItem' => 'includes/Feed.php', - 'ChannelFeed' => 'includes/Feed.php', - 'RSSFeed' => 'includes/Feed.php', - 'AtomFeed' => 'includes/Feed.php', + 'FeedUtils' => 'includes/FeedUtils.php', + 'FileDeleteForm' => 'includes/FileDeleteForm.php', + 'FileDependency' => 'includes/CacheDependency.php', + 'FileRevertForm' => 'includes/FileRevertForm.php', 'FileStore' => 'includes/FileStore.php', + 'FormatExif' => 'includes/Exif.php', + 'FormOptions' => 'includes/FormOptions.php', 'FSException' => 'includes/FileStore.php', 'FSTransaction' => 'includes/FileStore.php', + 'GlobalDependency' => 'includes/CacheDependency.php', + 'HashBagOStuff' => 'includes/BagOStuff.php', + 'HashtableReplacer' => 'includes/StringUtils.php', + 'HistoryBlobCurStub' => 'includes/HistoryBlob.php', 'HistoryBlob' => 'includes/HistoryBlob.php', - 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php', 'HistoryBlobStub' => 'includes/HistoryBlob.php', - 'HistoryBlobCurStub' => 'includes/HistoryBlob.php', 'HTMLCacheUpdate' => 'includes/HTMLCacheUpdate.php', + 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php', + 'HTMLFileCache' => 'includes/HTMLFileCache.php', 'Http' => 'includes/HttpFunctions.php', - 'IP' => 'includes/IP.php', + '_HWLDF_WordAccumulator' => 'includes/DifferenceEngine.php', 'ImageGallery' => 'includes/ImageGallery.php', - 'ImagePage' => 'includes/ImagePage.php', 'ImageHistoryList' => 'includes/ImagePage.php', - 'FileDeleteForm' => 'includes/FileDeleteForm.php', - 'FileRevertForm' => 'includes/FileRevertForm.php', + 'ImagePage' => 'includes/ImagePage.php', + 'ImageQueryPage' => 'includes/ImageQueryPage.php', + 'IncludableSpecialPage' => 'includes/SpecialPage.php', + 'IndexPager' => 'includes/Pager.php', + 'IP' => 'includes/IP.php', 'Job' => 'includes/JobQueue.php', - 'EmaillingJob' => 'includes/EmaillingJob.php', - 'EnotifNotifyJob' => 'includes/EnotifNotifyJob.php', - 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php', - 'RefreshLinksJob' => 'includes/RefreshLinksJob.php', - 'Licenses' => 'includes/Licenses.php', 'License' => 'includes/Licenses.php', + 'Licenses' => 'includes/Licenses.php', 'LinkBatch' => 'includes/LinkBatch.php', 'LinkCache' => 'includes/LinkCache.php', - 'LinkFilter' => 'includes/LinkFilter.php', 'Linker' => 'includes/Linker.php', + 'LinkFilter' => 'includes/LinkFilter.php', 'LinksUpdate' => 'includes/LinksUpdate.php', - 'LoadBalancer' => 'includes/LoadBalancer.php', 'LogPage' => 'includes/LogPage.php', + 'LogPager' => 'includes/LogEventsList.php', + 'LogEventsList' => 'includes/LogEventsList.php', + 'LogReader' => 'includes/LogEventsList.php', + 'LogViewer' => 'includes/LogEventsList.php', 'MacBinary' => 'includes/MacBinary.php', - 'MagicWord' => 'includes/MagicWord.php', 'MagicWordArray' => 'includes/MagicWord.php', + 'MagicWord' => 'includes/MagicWord.php', + 'MailAddress' => 'includes/UserMailer.php', + 'MappedDiff' => 'includes/DifferenceEngine.php', 'MathRenderer' => 'includes/Math.php', - 'MediaTransformOutput' => 'includes/MediaTransformOutput.php', - 'ThumbnailImage' => 'includes/MediaTransformOutput.php', 'MediaTransformError' => 'includes/MediaTransformOutput.php', - 'TransformParameterError' => 'includes/MediaTransformOutput.php', + 'MediaTransformOutput' => 'includes/MediaTransformOutput.php', + 'MediaWikiBagOStuff' => 'includes/BagOStuff.php', + 'MediaWiki_I18N' => 'includes/SkinTemplate.php', + 'MediaWiki' => 'includes/Wiki.php', + 'memcached' => 'includes/memcached-client.php', 'MessageCache' => 'includes/MessageCache.php', 'MimeMagic' => 'includes/MimeMagic.php', - 'Namespace' => 'includes/Namespace.php', - 'FakeMemCachedClient' => 'includes/ObjectCache.php', + 'MWException' => 'includes/Exception.php', + 'MWNamespace' => 'includes/Namespace.php', + 'MySQLSearchResultSet' => 'includes/SearchMySQL.php', + 'Namespace' => 'includes/NamespaceCompat.php', // Compat + 'OldChangesList' => 'includes/ChangesList.php', + 'OracleSearchResultSet' => 'includes/SearchOracle.php', 'OutputPage' => 'includes/OutputPage.php', 'PageHistory' => 'includes/PageHistory.php', - 'IndexPager' => 'includes/Pager.php', - 'ReverseChronologicalPager' => 'includes/Pager.php', - 'TablePager' => 'includes/Pager.php', - 'Parser' => 'includes/Parser.php', - 'Parser_OldPP' => 'includes/Parser_OldPP.php', - 'Parser_DiffTest' => 'includes/Parser_DiffTest.php', - 'ParserCache' => 'includes/ParserCache.php', - 'ParserOutput' => 'includes/ParserOutput.php', - 'ParserOptions' => 'includes/ParserOptions.php', + 'PageHistoryPager' => 'includes/PageHistory.php', + 'PageQueryPage' => 'includes/PageQueryPage.php', + 'Pager' => 'includes/Pager.php', + 'PasswordError' => 'includes/User.php', 'PatrolLog' => 'includes/PatrolLog.php', - 'Preprocessor' => 'includes/Preprocessor.php', + 'PostgresSearchResult' => 'includes/SearchPostgres.php', + 'PostgresSearchResultSet' => 'includes/SearchPostgres.php', 'PrefixSearch' => 'includes/PrefixSearch.php', - 'PPFrame' => 'includes/Preprocessor.php', - 'PPNode' => 'includes/Preprocessor.php', - 'Preprocessor_DOM' => 'includes/Preprocessor_DOM.php', - 'PPFrame_DOM' => 'includes/Preprocessor_DOM.php', - 'PPTemplateFrame_DOM' => 'includes/Preprocessor_DOM.php', - 'PPDStack' => 'includes/Preprocessor_DOM.php', - 'PPDStackElement' => 'includes/Preprocessor_DOM.php', - 'PPNode_DOM' => 'includes/Preprocessor_DOM.php', - 'Preprocessor_Hash' => 'includes/Preprocessor_Hash.php', + 'Profiler' => 'includes/Profiler.php', 'ProfilerSimple' => 'includes/ProfilerSimple.php', + 'ProfilerSimpleText' => 'includes/ProfilerSimpleText.php', 'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php', - 'Profiler' => 'includes/Profiler.php', - 'ProxyTools' => 'includes/ProxyTools.php', 'ProtectionForm' => 'includes/ProtectionForm.php', 'QueryPage' => 'includes/QueryPage.php', - 'PageQueryPage' => 'includes/PageQueryPage.php', - 'ImageQueryPage' => 'includes/ImageQueryPage.php', + 'QuickTemplate' => 'includes/SkinTemplate.php', 'RawPage' => 'includes/RawPage.php', + 'RCCacheEntry' => 'includes/ChangesList.php', 'RecentChange' => 'includes/RecentChange.php', + 'RefreshLinksJob' => 'includes/RefreshLinksJob.php', + 'RegexlikeReplacer' => 'includes/StringUtils.php', + 'ReplacementArray' => 'includes/StringUtils.php', + 'Replacer' => 'includes/StringUtils.php', + 'ReverseChronologicalPager' => 'includes/Pager.php', 'Revision' => 'includes/Revision.php', + 'RSSFeed' => 'includes/Feed.php', 'Sanitizer' => 'includes/Sanitizer.php', - 'SearchEngine' => 'includes/SearchEngine.php', - 'SearchResultSet' => 'includes/SearchEngine.php', - 'SearchResult' => 'includes/SearchEngine.php', 'SearchEngineDummy' => 'includes/SearchEngine.php', - 'SearchMySQL' => 'includes/SearchMySQL.php', - 'MySQLSearchResultSet' => 'includes/SearchMySQL.php', + 'SearchEngine' => 'includes/SearchEngine.php', + 'SearchHighlighter' => 'includes/SearchEngine.php', 'SearchMySQL4' => 'includes/SearchMySQL4.php', + 'SearchMySQL' => 'includes/SearchMySQL.php', + 'SearchOracle' => 'includes/SearchOracle.php', 'SearchPostgres' => 'includes/SearchPostgres.php', + 'SearchResult' => 'includes/SearchEngine.php', + 'SearchResultSet' => 'includes/SearchEngine.php', + 'SearchResultTooMany' => 'includes/SearchEngine.php', 'SearchUpdate' => 'includes/SearchUpdate.php', 'SearchUpdateMyISAM' => 'includes/SearchUpdate.php', - 'SearchOracle' => 'includes/SearchOracle.php', 'SiteConfiguration' => 'includes/SiteConfiguration.php', 'SiteStats' => 'includes/SiteStats.php', 'SiteStatsUpdate' => 'includes/SiteStats.php', 'Skin' => 'includes/Skin.php', - 'MediaWiki_I18N' => 'includes/SkinTemplate.php', 'SkinTemplate' => 'includes/SkinTemplate.php', - 'QuickTemplate' => 'includes/SkinTemplate.php', - 'SpecialAllpages' => 'includes/SpecialAllpages.php', - 'AncientPagesPage' => 'includes/SpecialAncientpages.php', - 'IPBlockForm' => 'includes/SpecialBlockip.php', - 'SpecialBookSources' => 'includes/SpecialBooksources.php', - 'BrokenRedirectsPage' => 'includes/SpecialBrokenRedirects.php', - 'EmailConfirmation' => 'includes/SpecialConfirmemail.php', - 'ContributionsPage' => 'includes/SpecialContributions.php', - 'DeadendPagesPage' => 'includes/SpecialDeadendpages.php', - 'DisambiguationsPage' => 'includes/SpecialDisambiguations.php', - 'DoubleRedirectsPage' => 'includes/SpecialDoubleRedirects.php', - 'EmailUserForm' => 'includes/SpecialEmailuser.php', - 'WikiRevision' => 'includes/SpecialImport.php', - 'WikiImporter' => 'includes/SpecialImport.php', - 'ImportStringSource' => 'includes/SpecialImport.php', - 'ImportStreamSource' => 'includes/SpecialImport.php', - 'IPUnblockForm' => 'includes/SpecialIpblocklist.php', - 'ListredirectsPage' => 'includes/SpecialListredirects.php', - 'DBLockForm' => 'includes/SpecialLockdb.php', - 'LogReader' => 'includes/SpecialLog.php', - 'LogViewer' => 'includes/SpecialLog.php', - 'LonelyPagesPage' => 'includes/SpecialLonelypages.php', - 'LongPagesPage' => 'includes/SpecialLongpages.php', - 'MIMEsearchPage' => 'includes/SpecialMIMEsearch.php', - 'MostcategoriesPage' => 'includes/SpecialMostcategories.php', - 'MostimagesPage' => 'includes/SpecialMostimages.php', - 'MostlinkedPage' => 'includes/SpecialMostlinked.php', - 'MostlinkedCategoriesPage' => 'includes/SpecialMostlinkedcategories.php', - 'SpecialMostlinkedtemplates' => 'includes/SpecialMostlinkedtemplates.php', - 'MostrevisionsPage' => 'includes/SpecialMostrevisions.php', - 'FewestrevisionsPage' => 'includes/SpecialFewestrevisions.php', - 'MovePageForm' => 'includes/SpecialMovepage.php', - 'NewbieContributionsPage' => 'includes/SpecialNewbieContributions.php', - 'NewPagesPage' => 'includes/SpecialNewpages.php', + 'SpecialMycontributions' => 'includes/SpecialPage.php', + 'SpecialMypage' => 'includes/SpecialPage.php', + 'SpecialMytalk' => 'includes/SpecialPage.php', 'SpecialPage' => 'includes/SpecialPage.php', - 'UnlistedSpecialPage' => 'includes/SpecialPage.php', - 'IncludableSpecialPage' => 'includes/SpecialPage.php', - 'PopularPagesPage' => 'includes/SpecialPopularpages.php', - 'PreferencesForm' => 'includes/SpecialPreferences.php', - 'SpecialPrefixindex' => 'includes/SpecialPrefixindex.php', - 'RandomPage' => 'includes/SpecialRandompage.php', - 'SpecialRandomredirect' => 'includes/SpecialRandomredirect.php', - 'PasswordResetForm' => 'includes/SpecialResetpass.php', - 'RevisionDeleteForm' => 'includes/SpecialRevisiondelete.php', - 'RevisionDeleter' => 'includes/SpecialRevisiondelete.php', - 'SpecialSearch' => 'includes/SpecialSearch.php', - 'ShortPagesPage' => 'includes/SpecialShortpages.php', - 'UncategorizedCategoriesPage' => 'includes/SpecialUncategorizedcategories.php', - 'UncategorizedPagesPage' => 'includes/SpecialUncategorizedpages.php', - 'UncategorizedTemplatesPage' => 'includes/SpecialUncategorizedtemplates.php', - 'PageArchive' => 'includes/SpecialUndelete.php', - 'UndeleteForm' => 'includes/SpecialUndelete.php', - 'DBUnlockForm' => 'includes/SpecialUnlockdb.php', - 'UnusedCategoriesPage' => 'includes/SpecialUnusedcategories.php', - 'UnusedimagesPage' => 'includes/SpecialUnusedimages.php', - 'UnusedtemplatesPage' => 'includes/SpecialUnusedtemplates.php', - 'UnwatchedpagesPage' => 'includes/SpecialUnwatchedpages.php', - 'UploadForm' => 'includes/SpecialUpload.php', - 'UploadFormMogile' => 'includes/SpecialUploadMogile.php', - 'LoginForm' => 'includes/SpecialUserlogin.php', - 'UserrightsPage' => 'includes/SpecialUserrights.php', - 'SpecialVersion' => 'includes/SpecialVersion.php', - 'WantedCategoriesPage' => 'includes/SpecialWantedcategories.php', - 'WantedPagesPage' => 'includes/SpecialWantedpages.php', - 'WhatLinksHerePage' => 'includes/SpecialWhatlinkshere.php', - 'WithoutInterwikiPage' => 'includes/SpecialWithoutinterwiki.php', + 'SpecialRedirectToSpecial' => 'includes/SpecialPage.php', + 'SqlBagOStuff' => 'includes/BagOStuff.php', 'SquidUpdate' => 'includes/SquidUpdate.php', - 'ReplacementArray' => 'includes/StringUtils.php', - 'Replacer' => 'includes/StringUtils.php', - 'RegexlikeReplacer' => 'includes/StringUtils.php', - 'DoubleReplacer' => 'includes/StringUtils.php', - 'HashtableReplacer' => 'includes/StringUtils.php', + 'Status' => 'includes/Status.php', 'StringUtils' => 'includes/StringUtils.php', + 'TableDiffFormatter' => 'includes/DifferenceEngine.php', + 'TablePager' => 'includes/Pager.php', + 'ThumbnailImage' => 'includes/MediaTransformOutput.php', + 'TitleDependency' => 'includes/CacheDependency.php', 'Title' => 'includes/Title.php', + 'TitleListDependency' => 'includes/CacheDependency.php', + 'TransformParameterError' => 'includes/MediaTransformOutput.php', + 'TurckBagOStuff' => 'includes/BagOStuff.php', + 'UnifiedDiffFormatter' => 'includes/DifferenceEngine.php', + 'UnlistedSpecialPage' => 'includes/SpecialPage.php', 'User' => 'includes/User.php', - 'UserRightsProxy' => 'includes/UserRightsProxy.php', - 'MailAddress' => 'includes/UserMailer.php', - 'EmailNotification' => 'includes/UserMailer.php', + 'UserArray' => 'includes/UserArray.php', + 'UserArrayFromResult' => 'includes/UserArray.php', 'UserMailer' => 'includes/UserMailer.php', + 'UserRightsProxy' => 'includes/UserRightsProxy.php', 'WatchedItem' => 'includes/WatchedItem.php', + 'WatchlistEditor' => 'includes/WatchlistEditor.php', 'WebRequest' => 'includes/WebRequest.php', 'WebResponse' => 'includes/WebResponse.php', - 'FauxRequest' => 'includes/WebRequest.php', - 'MediaWiki' => 'includes/Wiki.php', 'WikiError' => 'includes/WikiError.php', 'WikiErrorMsg' => 'includes/WikiError.php', + 'WikiExporter' => 'includes/Export.php', 'WikiXmlError' => 'includes/WikiError.php', + 'WordLevelDiff' => 'includes/DifferenceEngine.php', + 'XCacheBagOStuff' => 'includes/BagOStuff.php', + 'XmlDumpWriter' => 'includes/Export.php', 'Xml' => 'includes/Xml.php', + 'XmlSelect' => 'includes/Xml.php', 'XmlTypeCheck' => 'includes/XmlTypeCheck.php', 'ZhClient' => 'includes/ZhClient.php', - 'memcached' => 'includes/memcached-client.php', - 'EmaillingJob' => 'includes/JobQueue.php', - 'WatchlistEditor' => 'includes/WatchlistEditor.php', - # filerepo - 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php', - 'File' => 'includes/filerepo/File.php', - 'FileRepo' => 'includes/filerepo/FileRepo.php', - 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php', - 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php', - 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php', - 'FSRepo' => 'includes/filerepo/FSRepo.php', - 'Image' => 'includes/filerepo/LocalFile.php', - 'LocalFile' => 'includes/filerepo/LocalFile.php', - 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php', - 'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php', - 'LocalRepo' => 'includes/filerepo/LocalRepo.php', - 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php', - 'RepoGroup' => 'includes/filerepo/RepoGroup.php', - 'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php', - - # Media - 'BitmapHandler' => 'includes/media/Bitmap.php', - 'BmpHandler' => 'includes/media/BMP.php', - 'DjVuHandler' => 'includes/media/DjVu.php', - 'MediaHandler' => 'includes/media/Generic.php', - 'ImageHandler' => 'includes/media/Generic.php', - 'SvgHandler' => 'includes/media/SVG.php', - - # Normal - 'UtfNormal' => 'includes/normal/UtfNormal.php', - - # Templates - 'UsercreateTemplate' => 'includes/templates/Userlogin.php', - 'UserloginTemplate' => 'includes/templates/Userlogin.php', - - # Languages - 'Language' => 'languages/Language.php', - - # API + # includes/api 'ApiBase' => 'includes/api/ApiBase.php', + 'ApiBlock' => 'includes/api/ApiBlock.php', + 'ApiDelete' => 'includes/api/ApiDelete.php', + 'ApiEditPage' => 'includes/api/ApiEditPage.php', + 'ApiEmailUser' => 'includes/api/ApiEmailUser.php', 'ApiExpandTemplates' => 'includes/api/ApiExpandTemplates.php', - 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php', 'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php', 'ApiFormatBase' => 'includes/api/ApiFormatBase.php', - 'Services_JSON' => 'includes/api/ApiFormatJson_json.php', + 'ApiFormatDbg' => 'includes/api/ApiFormatDbg.php', + 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php', 'ApiFormatJson' => 'includes/api/ApiFormatJson.php', 'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php', + 'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php', 'ApiFormatWddx' => 'includes/api/ApiFormatWddx.php', 'ApiFormatXml' => 'includes/api/ApiFormatXml.php', - 'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php', - 'ApiFormatDbg' => 'includes/api/ApiFormatDbg.php', - 'Spyc' => 'includes/api/ApiFormatYaml_spyc.php', 'ApiFormatYaml' => 'includes/api/ApiFormatYaml.php', 'ApiHelp' => 'includes/api/ApiHelp.php', 'ApiLogin' => 'includes/api/ApiLogin.php', 'ApiLogout' => 'includes/api/ApiLogout.php', 'ApiMain' => 'includes/api/ApiMain.php', + 'ApiMove' => 'includes/api/ApiMove.php', 'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php', 'ApiPageSet' => 'includes/api/ApiPageSet.php', 'ApiParamInfo' => 'includes/api/ApiParamInfo.php', 'ApiParse' => 'includes/api/ApiParse.php', + 'ApiProtect' => 'includes/api/ApiProtect.php', 'ApiQuery' => 'includes/api/ApiQuery.php', - 'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php', - 'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php', 'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php', + 'ApiQueryAllimages' => 'includes/api/ApiQueryAllimages.php', + 'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php', 'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php', - 'ApiQueryBase' => 'includes/api/ApiQueryBase.php', - 'ApiQueryGeneratorBase' => 'includes/api/ApiQueryBase.php', + 'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php', + 'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php', 'ApiQueryBacklinks' => 'includes/api/ApiQueryBacklinks.php', + 'ApiQueryBase' => 'includes/api/ApiQueryBase.php', + 'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php', 'ApiQueryCategories' => 'includes/api/ApiQueryCategories.php', + 'ApiQueryCategoryInfo' => 'includes/api/ApiQueryCategoryInfo.php', 'ApiQueryCategoryMembers' => 'includes/api/ApiQueryCategoryMembers.php', 'ApiQueryContributions' => 'includes/api/ApiQueryUserContributions.php', - 'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php', + 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php', 'ApiQueryExtLinksUsage' => 'includes/api/ApiQueryExtLinksUsage.php', - 'ApiQueryImages' => 'includes/api/ApiQueryImages.php', + 'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php', + 'ApiQueryGeneratorBase' => 'includes/api/ApiQueryBase.php', 'ApiQueryImageInfo' => 'includes/api/ApiQueryImageInfo.php', + 'ApiQueryImages' => 'includes/api/ApiQueryImages.php', 'ApiQueryInfo' => 'includes/api/ApiQueryInfo.php', 'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php', 'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php', @@ -358,72 +271,264 @@ function __autoload($className) { 'ApiQueryRecentChanges'=> 'includes/api/ApiQueryRecentChanges.php', 'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php', 'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php', - 'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php', 'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php', - 'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php', 'ApiQueryUserInfo' => 'includes/api/ApiQueryUserInfo.php', + 'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php', 'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php', 'ApiResult' => 'includes/api/ApiResult.php', - - # apiedit branch - 'ApiBlock' => 'includes/api/ApiBlock.php', - #'ApiChangeRights' => 'includes/api/ApiChangeRights.php', - # Disabled for now - 'ApiDelete' => 'includes/api/ApiDelete.php', - 'ApiMove' => 'includes/api/ApiMove.php', - 'ApiProtect' => 'includes/api/ApiProtect.php', - 'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php', - 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php', 'ApiRollback' => 'includes/api/ApiRollback.php', 'ApiUnblock' => 'includes/api/ApiUnblock.php', - 'ApiUndelete' => 'includes/api/ApiUndelete.php' + 'ApiUndelete' => 'includes/api/ApiUndelete.php', + 'Services_JSON' => 'includes/api/ApiFormatJson_json.php', + 'Services_JSON_Error' => 'includes/api/ApiFormatJson_json.php', + 'Spyc' => 'includes/api/ApiFormatYaml_spyc.php', + 'UsageException' => 'includes/api/ApiMain.php', + 'YAMLNode' => 'includes/api/ApiFormatYaml_spyc.php', + + # includes/db + 'Blob' => 'includes/db/Database.php', + 'ChronologyProtector' => 'includes/db/LBFactory.php', + 'Database' => 'includes/db/Database.php', + 'DatabaseMssql' => 'includes/db/DatabaseMssql.php', + 'DatabaseMysql' => 'includes/db/Database.php', + 'DatabaseOracle' => 'includes/db/DatabaseOracle.php', + 'DatabasePostgres' => 'includes/db/DatabasePostgres.php', + 'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php', + 'DBConnectionError' => 'includes/db/Database.php', + 'DBError' => 'includes/db/Database.php', + 'DBObject' => 'includes/db/Database.php', + 'DBQueryError' => 'includes/db/Database.php', + 'DBUnexpectedError' => 'includes/db/Database.php', + 'LBFactory' => 'includes/db/LBFactory.php', + 'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php', + 'LBFactory_Simple' => 'includes/db/LBFactory.php', + 'LoadBalancer' => 'includes/db/LoadBalancer.php', + 'LoadMonitor' => 'includes/db/LoadMonitor.php', + 'LoadMonitor_MySQL' => 'includes/db/LoadMonitor.php', + 'MSSQLField' => 'includes/db/DatabaseMssql.php', + 'MySQLField' => 'includes/db/Database.php', + 'MySQLMasterPos' => 'includes/db/Database.php', + 'ORABlob' => 'includes/db/DatabaseOracle.php', + 'ORAResult' => 'includes/db/DatabaseOracle.php', + 'PostgresField' => 'includes/db/DatabasePostgres.php', + 'ResultWrapper' => 'includes/db/Database.php', + 'SQLiteField' => 'includes/db/DatabaseSqlite.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', + 'Image' => 'includes/filerepo/Image.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', + 'RepoGroup' => 'includes/filerepo/RepoGroup.php', + 'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php', + + # includes/media + 'BitmapHandler' => 'includes/media/Bitmap.php', + 'BmpHandler' => 'includes/media/BMP.php', + 'DjVuHandler' => 'includes/media/DjVu.php', + 'ImageHandler' => 'includes/media/Generic.php', + 'MediaHandler' => 'includes/media/Generic.php', + 'SvgHandler' => 'includes/media/SVG.php', + + # includes/normal + 'UtfNormal' => 'includes/normal/UtfNormal.php', + + # includes/parser + 'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php', + 'DateFormatter' => 'includes/parser/DateFormatter.php', + 'OnlyIncludeReplacer' => 'includes/parser/Parser.php', + 'PPDAccum_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPDPart' => 'includes/parser/Preprocessor_DOM.php', + 'PPDPart_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPDStack' => 'includes/parser/Preprocessor_DOM.php', + 'PPDStackElement' => 'includes/parser/Preprocessor_DOM.php', + 'PPDStackElement_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPDStack_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPFrame' => 'includes/parser/Preprocessor.php', + 'PPFrame_DOM' => 'includes/parser/Preprocessor_DOM.php', + 'PPFrame_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPNode' => 'includes/parser/Preprocessor.php', + 'PPNode_DOM' => 'includes/parser/Preprocessor_DOM.php', + 'PPNode_Hash_Array' => 'includes/parser/Preprocessor_Hash.php', + 'PPNode_Hash_Attr' => 'includes/parser/Preprocessor_Hash.php', + 'PPNode_Hash_Text' => 'includes/parser/Preprocessor_Hash.php', + 'PPNode_Hash_Tree' => 'includes/parser/Preprocessor_Hash.php', + 'PPTemplateFrame_DOM' => 'includes/parser/Preprocessor_DOM.php', + 'PPTemplateFrame_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'Parser' => 'includes/parser/Parser.php', + 'ParserCache' => 'includes/parser/ParserCache.php', + 'ParserOptions' => 'includes/parser/ParserOptions.php', + 'ParserOutput' => 'includes/parser/ParserOutput.php', + 'Parser_DiffTest' => 'includes/parser/Parser_DiffTest.php', + 'Parser_OldPP' => 'includes/parser/Parser_OldPP.php', + 'Preprocessor' => 'includes/parser/Preprocessor.php', + 'Preprocessor_DOM' => 'includes/parser/Preprocessor_DOM.php', + 'Preprocessor_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'StripState' => 'includes/parser/Parser.php', + + # includes/specials + 'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php', + 'BrokenRedirectsPage' => 'includes/specials/SpecialBrokenRedirects.php', + 'ContribsPager' => 'includes/specials/SpecialContributions.php', + 'DBLockForm' => 'includes/specials/SpecialLockdb.php', + 'DBUnlockForm' => 'includes/specials/SpecialUnlockdb.php', + 'DeadendPagesPage' => 'includes/specials/SpecialDeadendpages.php', + 'DisambiguationsPage' => 'includes/specials/SpecialDisambiguations.php', + 'DoubleRedirectsPage' => 'includes/specials/SpecialDoubleRedirects.php', + 'EmailConfirmation' => 'includes/specials/SpecialConfirmemail.php', + 'EmailInvalidation' => 'includes/specials/SpecialConfirmemail.php', + 'EmailUserForm' => 'includes/specials/SpecialEmailuser.php', + 'FewestrevisionsPage' => 'includes/specials/SpecialFewestrevisions.php', + 'FileDuplicateSearchPage' => 'includes/specials/SpecialFileDuplicateSearch.php', + 'IPBlockForm' => 'includes/specials/SpecialBlockip.php', + 'IPBlocklistPager' => 'includes/specials/SpecialIpblocklist.php', + 'IPUnblockForm' => 'includes/specials/SpecialIpblocklist.php', + 'ImportReporter' => 'includes/specials/SpecialImport.php', + 'ImportStreamSource' => 'includes/specials/SpecialImport.php', + 'ImportStringSource' => 'includes/specials/SpecialImport.php', + 'ListredirectsPage' => 'includes/specials/SpecialListredirects.php', + 'LoginForm' => 'includes/specials/SpecialUserlogin.php', + 'LonelyPagesPage' => 'includes/specials/SpecialLonelypages.php', + 'LongPagesPage' => 'includes/specials/SpecialLongpages.php', + 'MIMEsearchPage' => 'includes/specials/SpecialMIMEsearch.php', + 'MostcategoriesPage' => 'includes/specials/SpecialMostcategories.php', + 'MostimagesPage' => 'includes/specials/SpecialMostimages.php', + 'MostlinkedCategoriesPage' => 'includes/specials/SpecialMostlinkedcategories.php', + 'MostlinkedPage' => 'includes/specials/SpecialMostlinked.php', + 'MostrevisionsPage' => 'includes/specials/SpecialMostrevisions.php', + 'MovePageForm' => 'includes/specials/SpecialMovepage.php', + 'SpecialNewpages' => 'includes/specials/SpecialNewpages.php', + 'NewPagesPager' => 'includes/specials/SpecialNewpages.php', + 'PageArchive' => 'includes/specials/SpecialUndelete.php', + 'PasswordResetForm' => 'includes/specials/SpecialResetpass.php', + 'PopularPagesPage' => 'includes/specials/SpecialPopularpages.php', + 'PreferencesForm' => 'includes/specials/SpecialPreferences.php', + 'RandomPage' => 'includes/specials/SpecialRandompage.php', + 'RevisionDeleteForm' => 'includes/specials/SpecialRevisiondelete.php', + 'RevisionDeleter' => 'includes/specials/SpecialRevisiondelete.php', + 'ShortPagesPage' => 'includes/specials/SpecialShortpages.php', + 'SpecialAllpages' => 'includes/specials/SpecialAllpages.php', + 'SpecialBookSources' => 'includes/specials/SpecialBooksources.php', + 'SpecialListGroupRights' => 'includes/specials/SpecialListgrouprights.php', + 'SpecialMostlinkedtemplates' => 'includes/specials/SpecialMostlinkedtemplates.php', + 'SpecialPrefixindex' => 'includes/specials/SpecialPrefixindex.php', + 'SpecialRandomredirect' => 'includes/specials/SpecialRandomredirect.php', + 'SpecialRecentchanges' => 'includes/specials/SpecialRecentchanges.php', + 'SpecialRecentchangeslinked' => 'includes/specials/SpecialRecentchangeslinked.php', + 'SpecialSearch' => 'includes/specials/SpecialSearch.php', + 'SpecialVersion' => 'includes/specials/SpecialVersion.php', + 'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php', + 'UncategorizedPagesPage' => 'includes/specials/SpecialUncategorizedpages.php', + 'UncategorizedTemplatesPage' => 'includes/specials/SpecialUncategorizedtemplates.php', + 'UndeleteForm' => 'includes/specials/SpecialUndelete.php', + 'UnusedCategoriesPage' => 'includes/specials/SpecialUnusedcategories.php', + 'UnusedimagesPage' => 'includes/specials/SpecialUnusedimages.php', + 'UnusedtemplatesPage' => 'includes/specials/SpecialUnusedtemplates.php', + 'UnwatchedpagesPage' => 'includes/specials/SpecialUnwatchedpages.php', + 'UploadForm' => 'includes/specials/SpecialUpload.php', + 'UploadFormMogile' => 'includes/specials/SpecialUploadMogile.php', + 'UserrightsPage' => 'includes/specials/SpecialUserrights.php', + 'UsersPager' => 'includes/specials/SpecialListusers.php', + 'WantedCategoriesPage' => 'includes/specials/SpecialWantedcategories.php', + 'WantedPagesPage' => 'includes/specials/SpecialWantedpages.php', + 'WhatLinksHerePage' => 'includes/specials/SpecialWhatlinkshere.php', + 'WikiImporter' => 'includes/specials/SpecialImport.php', + 'WikiRevision' => 'includes/specials/SpecialImport.php', + 'WithoutInterwikiPage' => 'includes/specials/SpecialWithoutinterwiki.php', + + # includes/templates + 'UsercreateTemplate' => 'includes/templates/Userlogin.php', + 'UserloginTemplate' => 'includes/templates/Userlogin.php', + + # languages + 'Language' => 'languages/Language.php', + 'FakeConverter' => 'languages/Language.php', + + # maintenance/language + 'statsOutput' => 'maintenance/language/StatOutputs.php', + 'wikiStatsOutput' => 'maintenance/language/StatOutputs.php', + 'metawikiStatsOutput' => 'maintenance/language/StatOutputs.php', + 'textStatsOutput' => 'maintenance/language/StatOutputs.php', + 'csvStatsOutput' => 'maintenance/language/StatOutputs.php', + ); - - wfProfileIn( __METHOD__ ); - if ( isset( $localClasses[$className] ) ) { - $filename = $localClasses[$className]; - } elseif ( isset( $wgAutoloadClasses[$className] ) ) { - $filename = $wgAutoloadClasses[$className]; - } else { - # Try a different capitalisation - # The case can sometimes be wrong when unserializing PHP 4 objects - $filename = false; - $lowerClass = strtolower( $className ); - foreach ( $localClasses as $class2 => $file2 ) { - if ( strtolower( $class2 ) == $lowerClass ) { - $filename = $file2; + + /** + * autoload - take a class name and attempt to load it + * + * @param string $className Name of class we're looking for. + * @return bool Returning false is important on failure as + * it allows Zend to try and look in other registered autoloaders + * as well. + */ + static function autoload( $className ) { + global $wgAutoloadClasses; + + wfProfileIn( __METHOD__ ); + if ( isset( self::$localClasses[$className] ) ) { + $filename = self::$localClasses[$className]; + } elseif ( isset( $wgAutoloadClasses[$className] ) ) { + $filename = $wgAutoloadClasses[$className]; + } else { + # Try a different capitalisation + # The case can sometimes be wrong when unserializing PHP 4 objects + $filename = false; + $lowerClass = strtolower( $className ); + foreach ( self::$localClasses as $class2 => $file2 ) { + if ( strtolower( $class2 ) == $lowerClass ) { + $filename = $file2; + } + } + if ( !$filename ) { + # Give up + wfProfileOut( __METHOD__ ); + return false; } } - if ( !$filename ) { - # Give up - wfProfileOut( __METHOD__ ); - return; + + # Make an absolute path, this improves performance by avoiding some stat calls + if ( substr( $filename, 0, 1 ) != '/' && substr( $filename, 1, 1 ) != ':' ) { + global $IP; + $filename = "$IP/$filename"; } + require( $filename ); + wfProfileOut( __METHOD__ ); + return true; } - # Make an absolute path, this improves performance by avoiding some stat calls - if ( substr( $filename, 0, 1 ) != '/' && substr( $filename, 1, 1 ) != ':' ) { - global $IP; - $filename = "$IP/$filename"; + static function loadAllExtensions() { + global $wgAutoloadClasses; + + foreach( $wgAutoloadClasses as $class => $file ) { + if( !( class_exists( $class ) || interface_exists( $class ) ) ) { + require( $file ); + } + } } - require( $filename ); - wfProfileOut( __METHOD__ ); } function wfLoadAllExtensions() { - global $wgAutoloadClasses; + AutoLoader::loadAllExtensions(); +} - # It is crucial that SpecialPage.php is included before any special page - # extensions are loaded. Otherwise the parent class will not be available - # when APC loads the early-bound extension class. Normally this is - # guaranteed by entering special pages via SpecialPage members such as - # executePath(), but here we have to take a more explicit measure. - - require_once( dirname(__FILE__) . '/SpecialPage.php' ); - - foreach( $wgAutoloadClasses as $class => $file ) { - if( !( class_exists( $class ) || interface_exists( $class ) ) ) { - require( $file ); - } +if ( function_exists( 'spl_autoload_register' ) ) { + spl_autoload_register( array( 'AutoLoader', 'autoload' ) ); +} else { + function __autoload( $class ) { + AutoLoader::autoload( $class ); } } + diff --git a/includes/Autopromote.php b/includes/Autopromote.php index b5097423..68fe6636 100644 --- a/includes/Autopromote.php +++ b/includes/Autopromote.php @@ -8,7 +8,7 @@ class Autopromote { /** * Get the groups for the given user based on $wgAutopromote. * - * @param User $user The user to get the groups for + * @param $user The user to get the groups for * @return array Array of groups to promote to. */ public static function getAutopromoteGroups( User $user ) { @@ -18,6 +18,9 @@ class Autopromote { if( self::recCheckCondition( $cond, $user ) ) $promote[] = $group; } + + wfRunHooks( 'GetAutoPromoteGroups', array($user, &$promote) ); + return $promote; } @@ -33,12 +36,12 @@ class Autopromote { * This function evaluates the former type recursively, and passes off to * self::checkCondition for evaluation of the latter type. * - * @param mixed $cond A condition, possibly containing other conditions - * @param User $user The user to check the conditions against + * @param $cond Mixed: a condition, possibly containing other conditions + * @param $user The user to check the conditions against * @return bool Whether the condition is true */ private static function recCheckCondition( $cond, User $user ) { - $validOps = array( '&', '|', '^' ); + $validOps = array( '&', '|', '^', '!' ); if( is_array( $cond ) && count( $cond ) >= 2 && in_array( $cond[0], $validOps ) ) { # Recursive condition if( $cond[0] == '&' ) { @@ -47,7 +50,7 @@ class Autopromote { return false; return true; } elseif( $cond[0] == '|' ) { - foreach( array_slice( $cond, 1 ) as $subcond ) + foreach( array_slice( $cond, 1 ) as $subcond ) if( self::recCheckCondition( $subcond, $user ) ) return true; return false; @@ -60,6 +63,11 @@ class Autopromote { $res = ($res xor self::recCheckCondition( $subcond, $user )); } return $res; + } elseif ( $cond[0] = '!' ) { + foreach( array_slice( $cond, 1 ) as $subcond ) + if( self::recCheckCondition( $subcond, $user ) ) + return false; + return true; } } # If we got here, the array presumably does not contain other condi- @@ -75,8 +83,8 @@ class Autopromote { * APCOND_AGE. Other types will throw an exception if no extension evalu- * ates them. * - * @param array $cond A condition, which must not contain other conditions - * @param User $user The user to check the condition against + * @param $cond Array: A condition, which must not contain other conditions + * @param $user The user to check the condition against * @return bool Whether the condition is true for the user */ private static function checkCondition( $cond, User $user ) { @@ -87,7 +95,7 @@ class Autopromote { if( User::isValidEmailAddr( $user->getEmail() ) ) { global $wgEmailAuthentication; if( $wgEmailAuthentication ) { - return $user->getEmailAuthenticationTimestamp() ? true : false; + return (bool)$user->getEmailAuthenticationTimestamp(); } else { return true; } diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php index 226abb35..b4fefc97 100644 --- a/includes/BagOStuff.php +++ b/includes/BagOStuff.php @@ -17,22 +17,25 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html + /** + * @defgroup Cache Cache * + * @file + * @ingroup Cache */ /** - * Simple generic object store - * * interface is intended to be more or less compatible with * the PHP memcached client. * * backends for local hash array and SQL table included: * <code> * $bag = new HashBagOStuff(); - * $bag = new MysqlBagOStuff($tablename); # connect to db first + * $bag = new MediaWikiBagOStuff($tablename); # connect to db first * </code> * + * @ingroup Cache */ class BagOStuff { var $debugmode; @@ -167,14 +170,12 @@ class BagOStuff { /** * Functional versions! - * @todo document + * This is a test of the interface, mainly. It stores things in an associative + * array, which is not going to persist between program runs. + * + * @ingroup Cache */ class HashBagOStuff extends BagOStuff { - /* - This is a test of the interface, mainly. It stores - things in an associative array, which is not going to - persist between program runs. - */ var $bag; function __construct() { @@ -213,24 +214,20 @@ class HashBagOStuff extends BagOStuff { } } -/* -CREATE TABLE objectcache ( - keyname char(255) binary not null default '', - value mediumblob, - exptime datetime, - unique key (keyname), - key (exptime) -); -*/ - /** - * @todo document - * @abstract + * Generic class to store objects in a database + * + * @ingroup Cache */ abstract class SqlBagOStuff extends BagOStuff { var $table; var $lastexpireall = 0; + /** + * Constructor + * + * @param $tablename String: name of the table to use + */ function __construct($tablename = 'objectcache') { $this->table = $tablename; } @@ -247,8 +244,8 @@ abstract class SqlBagOStuff extends BagOStuff { } if($row=$this->_fetchobject($res)) { $this->_debug("get: retrieved data; exp time is " . $row->exptime); - if ( $row->exptime != $this->_maxdatetime() && - wfTimestamp( TS_UNIX, $row->exptime ) < time() ) + if ( $row->exptime != $this->_maxdatetime() && + wfTimestamp( TS_UNIX, $row->exptime ) < time() ) { $this->_debug("get: key has expired, deleting"); $this->delete($key); @@ -262,7 +259,7 @@ abstract class SqlBagOStuff extends BagOStuff { } function set($key,$value,$exptime=0) { - if ( wfReadOnly() ) { + if ( $this->_readonly() ) { return false; } $exptime = intval($exptime); @@ -284,7 +281,7 @@ abstract class SqlBagOStuff extends BagOStuff { } function delete($key,$time=0) { - if ( wfReadOnly() ) { + if ( $this->_readonly() ) { return false; } $this->_query( @@ -340,6 +337,8 @@ abstract class SqlBagOStuff extends BagOStuff { abstract function _doinsert($table, $vals); abstract function _doquery($sql); + abstract function _readonly(); + function _freeresult($result) { /* stub */ return false; @@ -367,7 +366,7 @@ abstract class SqlBagOStuff extends BagOStuff { function expireall() { /* Remove any items that have expired */ - if ( wfReadOnly() ) { + if ( $this->_readonly() ) { return false; } $now = $this->_fromunixtime( time() ); @@ -376,7 +375,7 @@ abstract class SqlBagOStuff extends BagOStuff { function deleteall() { /* Clear *all* items from cache table */ - if ( wfReadOnly() ) { + if ( $this->_readonly() ) { return false; } $this->_query( "DELETE FROM $0" ); @@ -387,7 +386,7 @@ abstract class SqlBagOStuff extends BagOStuff { * On typical message and page data, this can provide a 3X decrease * in storage requirements. * - * @param mixed $data + * @param $data mixed * @return string */ function _serialize( &$data ) { @@ -401,7 +400,7 @@ abstract class SqlBagOStuff extends BagOStuff { /** * Unserialize and, if necessary, decompress an object. - * @param string $serial + * @param $serial string * @return mixed */ function _unserialize( $serial ) { @@ -417,31 +416,33 @@ abstract class SqlBagOStuff extends BagOStuff { } /** - * @todo document + * Stores objects in the main database of the wiki + * + * @ingroup Cache */ class MediaWikiBagOStuff extends SqlBagOStuff { var $tableInitialised = false; + function _getDB(){ + static $db; + if( !isset( $db ) ) + $db = wfGetDB( DB_MASTER ); + return $db; + } function _doquery($sql) { - $dbw = wfGetDB( DB_MASTER ); - return $dbw->query($sql, 'MediaWikiBagOStuff::_doquery'); + return $this->_getDB()->query( $sql, __METHOD__ ); } function _doinsert($t, $v) { - $dbw = wfGetDB( DB_MASTER ); - return $dbw->insert($t, $v, 'MediaWikiBagOStuff::_doinsert', - array( 'IGNORE' ) ); + return $this->_getDB()->insert($t, $v, __METHOD__, array( 'IGNORE' ) ); } function _fetchobject($result) { - $dbw = wfGetDB( DB_MASTER ); - return $dbw->fetchObject($result); + return $this->_getDB()->fetchObject($result); } function _freeresult($result) { - $dbw = wfGetDB( DB_MASTER ); - return $dbw->freeResult($result); + return $this->_getDB()->freeResult($result); } function _dberror($result) { - $dbw = wfGetDB( DB_MASTER ); - return $dbw->lastError(); + return $this->_getDB()->lastError(); } function _maxdatetime() { if ( time() > 0x7fffffff ) { @@ -451,24 +452,23 @@ class MediaWikiBagOStuff extends SqlBagOStuff { } } function _fromunixtime($ts) { - $dbw = wfGetDB(DB_MASTER); - return $dbw->timestamp($ts); + return $this->_getDB()->timestamp($ts); + } + function _readonly(){ + return wfReadOnly(); } function _strencode($s) { - $dbw = wfGetDB( DB_MASTER ); - return $dbw->strencode($s); + return $this->_getDB()->strencode($s); } function _blobencode($s) { - $dbw = wfGetDB( DB_MASTER ); - return $dbw->encodeBlob($s); + return $this->_getDB()->encodeBlob($s); } function _blobdecode($s) { - $dbw = wfGetDB( DB_MASTER ); - return $dbw->decodeBlob($s); + return $this->_getDB()->decodeBlob($s); } function getTableName() { if ( !$this->tableInitialised ) { - $dbw = wfGetDB( DB_MASTER ); + $dbw = $this->_getDB(); /* This is actually a hack, we should be able to use Language classes here... or not */ if (!$dbw) @@ -493,6 +493,7 @@ class MediaWikiBagOStuff extends SqlBagOStuff { * that Turck's serializer is faster, so a possible future extension would be * to use it for arrays but not for objects. * + * @ingroup Cache */ class TurckBagOStuff extends BagOStuff { function get($key) { @@ -527,6 +528,7 @@ class TurckBagOStuff extends BagOStuff { /** * This is a wrapper for APC's shared memory functions * + * @ingroup Cache */ class APCBagOStuff extends BagOStuff { function get($key) { @@ -536,12 +538,12 @@ class APCBagOStuff extends BagOStuff { } return $val; } - + function set($key, $value, $exptime=0) { apc_store($key, serialize($value), $exptime); return true; } - + function delete($key, $time=0) { apc_delete($key); return true; @@ -555,6 +557,7 @@ class APCBagOStuff extends BagOStuff { * This is basically identical to the Turck MMCache version, * mostly because eAccelerator is based on Turck MMCache. * + * @ingroup Cache */ class eAccelBagOStuff extends BagOStuff { function get($key) { @@ -589,13 +592,15 @@ class eAccelBagOStuff extends BagOStuff { /** * Wrapper for XCache object caching functions; identical interface * to the APC wrapper + * + * @ingroup Cache */ class XCacheBagOStuff extends BagOStuff { /** * Get a value from the XCache object cache * - * @param string $key Cache key + * @param $key String: cache key * @return mixed */ public function get( $key ) { @@ -604,40 +609,41 @@ class XCacheBagOStuff extends BagOStuff { $val = unserialize( $val ); return $val; } - + /** * Store a value in the XCache object cache * - * @param string $key Cache key - * @param mixed $value Object to store - * @param int $expire Expiration time + * @param $key String: cache key + * @param $value Mixed: object to store + * @param $expire Int: expiration time * @return bool */ public function set( $key, $value, $expire = 0 ) { xcache_set( $key, serialize( $value ), $expire ); return true; } - + /** * Remove a value from the XCache object cache * - * @param string $key Cache key - * @param int $time Not used in this implementation + * @param $key String: cache key + * @param $time Int: not used in this implementation * @return bool */ public function delete( $key, $time = 0 ) { xcache_unset( $key ); return true; } - + } /** * @todo document + * @ingroup Cache */ class DBABagOStuff extends BagOStuff { var $mHandler, $mFile, $mReader, $mWriter, $mDisabled; - + function __construct( $handler = 'db3', $dir = false ) { if ( $dir === false ) { global $wgTmpDirectory; @@ -645,6 +651,7 @@ class DBABagOStuff extends BagOStuff { } $this->mFile = "$dir/mw-cache-" . wfWikiID(); $this->mFile .= '.db'; + wfDebug( __CLASS__.": using cache file {$this->mFile}\n" ); $this->mHandler = $handler; } @@ -664,7 +671,7 @@ class DBABagOStuff extends BagOStuff { if ( !is_string( $blob ) ) { return array( null, 0 ); } else { - return array( + return array( unserialize( substr( $blob, 11 ) ), intval( substr( $blob, 0, 10 ) ) ); @@ -779,5 +786,3 @@ class DBABagOStuff extends BagOStuff { return $result; } } - - diff --git a/includes/Block.php b/includes/Block.php index 3688d7cf..b208fa8a 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -1,5 +1,6 @@ <?php /** + * @file * Blocks and bans object */ @@ -15,16 +16,16 @@ class Block { /* public*/ var $mAddress, $mUser, $mBy, $mReason, $mTimestamp, $mAuto, $mId, $mExpiry, - $mRangeStart, $mRangeEnd, $mAnonOnly, $mEnableAutoblock, $mHideName, - $mBlockEmail; - /* private */ var $mNetworkBits, $mIntegerAddr, $mForUpdate, $mFromMaster, $mByName; - + $mRangeStart, $mRangeEnd, $mAnonOnly, $mEnableAutoblock, $mHideName, + $mBlockEmail, $mByName, $mAngryAutoblock; + /* private */ var $mNetworkBits, $mIntegerAddr, $mForUpdate, $mFromMaster; + const EB_KEEP_EXPIRED = 1; const EB_FOR_UPDATE = 2; const EB_RANGE_ONLY = 4; function __construct( $address = '', $user = 0, $by = 0, $reason = '', - $timestamp = '' , $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0, + $timestamp = '' , $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0, $hideName = 0, $blockEmail = 0 ) { $this->mId = 0; @@ -45,6 +46,7 @@ class Block $this->mForUpdate = false; $this->mFromMaster = false; $this->mByName = false; + $this->mAngryAutoblock = false; $this->initialiseRange(); } @@ -59,10 +61,10 @@ class Block } } - static function newFromID( $id ) + static function newFromID( $id ) { $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->resultObject( $dbr->select( 'ipblocks', '*', + $res = $dbr->resultObject( $dbr->select( 'ipblocks', '*', array( 'ipb_id' => $id ), __METHOD__ ) ); $block = new Block; if ( $block->loadFromResult( $res ) ) { @@ -75,8 +77,8 @@ class Block function clear() { $this->mAddress = $this->mReason = $this->mTimestamp = ''; - $this->mId = $this->mAnonOnly = $this->mCreateAccount = - $this->mEnableAutoblock = $this->mAuto = $this->mUser = + $this->mId = $this->mAnonOnly = $this->mCreateAccount = + $this->mEnableAutoblock = $this->mAuto = $this->mUser = $this->mBy = $this->mHideName = $this->mBlockEmail = 0; $this->mByName = false; } @@ -124,7 +126,7 @@ class Block # Try user block if ( $user ) { - $res = $db->resultObject( $db->select( 'ipblocks', '*', array( 'ipb_user' => $user ), + $res = $db->resultObject( $db->select( 'ipblocks', '*', array( 'ipb_user' => $user ), __METHOD__, $options ) ); if ( $this->loadFromResult( $res, $killExpired ) ) { return true; @@ -170,7 +172,7 @@ class Block return true; } } - + # Give up $this->clear(); return false; @@ -179,7 +181,7 @@ class Block /** * Fill in member variables from a result wrapper */ - function loadFromResult( ResultWrapper $res, $killExpired = true ) + function loadFromResult( ResultWrapper $res, $killExpired = true ) { $ret = false; if ( 0 != $res->numRows() ) { @@ -234,7 +236,7 @@ class Block "ipb_range_start <= '$iaddr'", "ipb_range_end >= '$iaddr'" ); - + if ( $user ) { $conds['ipb_anon_only'] = 0; } @@ -270,7 +272,7 @@ class Block if ( isset( $row->user_name ) ) { $this->mByName = $row->user_name; } else { - $this->mByName = false; + $this->mByName = $row->ipb_by_text; } $this->mRangeStart = $row->ipb_range_start; $this->mRangeEnd = $row->ipb_range_end; @@ -358,7 +360,7 @@ class Block /** * Insert a block into the block table. - *@return Whether or not the insertion was successful. + * @return Whether or not the insertion was successful. */ function insert() { @@ -376,6 +378,15 @@ class Block $this->mBlockEmail = 0; //Same goes for email... } + if( !$this->mByName ) { + if( $this->mBy ) { + $this->mByName = User::whoIs( $this->mBy ); + } else { + global $wgUser; + $this->mByName = $wgUser->getName(); + } + } + # Don't collide with expired blocks Block::purgeExpired(); @@ -386,6 +397,7 @@ class Block 'ipb_address' => $this->mAddress, 'ipb_user' => $this->mUser, 'ipb_by' => $this->mBy, + 'ipb_by_text' => $this->mByName, 'ipb_reason' => $this->mReason, 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), 'ipb_auto' => $this->mAuto, @@ -400,7 +412,6 @@ class Block ), 'Block::insert', array( 'IGNORE' ) ); $affected = $dbw->affectedRows(); - $dbw->commit(); if ($affected) $this->doRetroactiveAutoblock(); @@ -420,17 +431,30 @@ class Block if ($this->mEnableAutoblock && $this->mUser) { wfDebug("Doing retroactive autoblocks for " . $this->mAddress . "\n"); + + $options = array( 'ORDER BY' => 'rc_timestamp DESC' ); + $conds = array( 'rc_user_text' => $this->mAddress ); + + if ($this->mAngryAutoblock) { + // Block any IP used in the last 7 days. Up to five IPs. + $conds[] = 'rc_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( time() - (7*86400) ) ); + $options['LIMIT'] = 5; + } else { + // Just the last IP used. + $options['LIMIT'] = 1; + } - $row = $dbr->selectRow( 'recentchanges', array( 'rc_ip' ), array( 'rc_user_text' => $this->mAddress ), - __METHOD__ , array( 'ORDER BY' => 'rc_timestamp DESC' ) ); + $res = $dbr->select( 'recentchanges', array( 'rc_ip' ), $conds, + __METHOD__ , $options); - if ( !$row || !$row->rc_ip ) { + if ( !$dbr->numRows( $res ) ) { #No results, don't autoblock anything wfDebug("No IP found to retroactively autoblock\n"); } else { - #Limit is 1, so no loop needed. - $retroblockip = $row->rc_ip; - return $this->doAutoblock( $retroblockip, true ); + while ( $row = $dbr->fetchObject( $res ) ) { + if ( $row->rc_ip ) + $this->doAutoblock( $row->rc_ip ); + } } } } @@ -476,6 +500,12 @@ class Block wfDebug( " No match\n" ); } } + + ## Allow hooks to cancel the autoblock. + if (!wfRunHooks( 'AbortAutoblock', array( $autoblockip, &$this ) )) { + wfDebug( "Autoblock aborted by hook." ); + return false; + } # It's okay to autoblock. Go ahead and create/insert the block. @@ -502,6 +532,7 @@ class Block $ipblock->mAddress = $autoblockip; $ipblock->mUser = 0; $ipblock->mBy = $this->mBy; + $ipblock->mByName = $this->mByName; $ipblock->mReason = wfMsgForContent( 'autoblocker', $this->mAddress, $this->mReason ); $ipblock->mTimestamp = wfTimestampNow(); $ipblock->mAuto = 1; @@ -592,9 +623,6 @@ class Block */ function getByName() { - if ( $this->mByName === false ) { - $this->mByName = User::whoIs( $this->mBy ); - } return $this->mByName; } @@ -613,7 +641,7 @@ class Block return $this->mAddress; } } - + /** * Encode expiry for DB */ @@ -625,7 +653,7 @@ class Block } } - /** + /** * Decode expiry which has come from the DB */ static function decodeExpiry( $expiry, $timestampType = TS_MW ) { @@ -635,14 +663,14 @@ class Block return wfTimestamp( $timestampType, $expiry ); } } - + static function getAutoblockExpiry( $timestamp ) { global $wgAutoblockExpiry; return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry ); } - - /** + + /** * Gets rid of uneeded numbers in quad-dotted/octet IP strings * For example, 127.111.113.151/24 -> 127.111.113.0/24 */ @@ -675,7 +703,7 @@ class Block return $range; } - /** + /** * Purge expired blocks from the ipblocks table */ static function purgeExpired() { @@ -684,8 +712,8 @@ class Block } static function infinity() { - # This is a special keyword for timestamps in PostgreSQL, and - # works with CHAR(14) as well because "i" sorts after all numbers. + # This is a special keyword for timestamps in PostgreSQL, and + # works with CHAR(14) as well because "i" sorts after all numbers. return 'infinity'; /* @@ -697,6 +725,48 @@ class Block return $infinity; */ } + + /** + * Convert a DB-encoded expiry into a real string that humans can read. + */ + static function formatExpiry( $encoded_expiry ) { + + static $msg = null; + + if( is_null( $msg ) ) { + $msg = array(); + $keys = array( 'infiniteblock', 'expiringblock' ); + foreach( $keys as $key ) { + $msg[$key] = wfMsgHtml( $key ); + } + } + + $expiry = Block::decodeExpiry( $encoded_expiry ); + if ($expiry == 'infinity') { + $expirystr = $msg['infiniteblock']; + } else { + global $wgLang; + $expiretimestr = $wgLang->timeanddate( $expiry, true ); + $expirystr = wfMsgReplaceArgs( $msg['expiringblock'], array($expiretimestr) ); + } + + return $expirystr; + } + + /** + * Convert a typed-in expiry time into something we can put into the database. + */ + static function parseExpiryInput( $expiry_input ) { + if ( $expiry_input == 'infinite' || $expiry_input == 'indefinite' ) { + $expiry = 'infinity'; + } else { + $expiry = strtotime( $expiry_input ); + if ($expiry < 0 || $expiry === false) { + return false; + } + } + + return $expiry; + } } - diff --git a/includes/CacheDependency.php b/includes/CacheDependency.php index 1d48c383..b050c46d 100644 --- a/includes/CacheDependency.php +++ b/includes/CacheDependency.php @@ -1,10 +1,10 @@ <?php /** - * This class stores an arbitrary value along with its dependencies. + * This class stores an arbitrary value along with its dependencies. * Users should typically only use DependencyWrapper::getFromCache(), rather * than instantiating one of these objects directly. - * @addtogroup Cache + * @ingroup Cache */ class DependencyWrapper { var $value; @@ -24,7 +24,7 @@ class DependencyWrapper { $this->deps = $deps; } - /** + /** * Returns true if any of the dependencies have expired */ function isExpired() { @@ -62,24 +62,24 @@ class DependencyWrapper { } /** - * Attempt to get a value from the cache. If the value is expired or missing, + * Attempt to get a value from the cache. If the value is expired or missing, * it will be generated with the callback function (if present), and the newly - * calculated value will be stored to the cache in a wrapper. + * calculated value will be stored to the cache in a wrapper. * * @param object $cache A cache object such as $wgMemc * @param string $key The cache key * @param integer $expiry The expiry timestamp or interval in seconds * @param mixed $callback The callback for generating the value, or false * @param array $callbackParams The function parameters for the callback - * @param array $deps The dependencies to store on a cache miss. Note: these + * @param array $deps The dependencies to store on a cache miss. Note: these * are not the dependencies used on a cache hit! Cache hits use the stored * dependency array. * * @return mixed The value, or null if it was not present in the cache and no * callback was defined. */ - static function getValueFromCache( $cache, $key, $expiry = 0, $callback = false, - $callbackParams = array(), $deps = array() ) + static function getValueFromCache( $cache, $key, $expiry = 0, $callback = false, + $callbackParams = array(), $deps = array() ) { $obj = $cache->get( $key ); if ( is_object( $obj ) && $obj instanceof DependencyWrapper && !$obj->isExpired() ) { @@ -97,7 +97,7 @@ class DependencyWrapper { } /** - * @addtogroup Cache + * @ingroup Cache */ abstract class CacheDependency { /** @@ -112,7 +112,7 @@ abstract class CacheDependency { } /** - * @addtogroup Cache + * @ingroup Cache */ class FileDependency extends CacheDependency { var $filename, $timestamp; @@ -122,11 +122,11 @@ class FileDependency extends CacheDependency { * * @param string $filename The name of the file, preferably fully qualified * @param mixed $timestamp The unix last modified timestamp, or false if the - * file does not exist. If omitted, the timestamp will be loaded from + * file does not exist. If omitted, the timestamp will be loaded from * the file. * - * A dependency on a nonexistent file will be triggered when the file is - * created. A dependency on an existing file will be triggered when the + * A dependency on a nonexistent file will be triggered when the file is + * created. A dependency on an existing file will be triggered when the * file is changed. */ function __construct( $filename, $timestamp = null ) { @@ -171,7 +171,7 @@ class FileDependency extends CacheDependency { } /** - * @addtogroup Cache + * @ingroup Cache */ class TitleDependency extends CacheDependency { var $titleObj; @@ -191,7 +191,7 @@ class TitleDependency extends CacheDependency { function loadDependencyValues() { $this->touched = $this->getTitle()->getTouched(); } - + /** * Get rid of bulky Title object for sleep */ @@ -202,7 +202,7 @@ class TitleDependency extends CacheDependency { function getTitle() { if ( !isset( $this->titleObj ) ) { $this->titleObj = Title::makeTitle( $this->ns, $this->dbk ); - } + } return $this->titleObj; } @@ -230,12 +230,12 @@ class TitleDependency extends CacheDependency { } /** - * @addtogroup Cache + * @ingroup Cache */ class TitleListDependency extends CacheDependency { var $linkBatch; var $timestamps; - + /** * Construct a dependency on a list of titles */ @@ -259,7 +259,7 @@ class TitleListDependency extends CacheDependency { if ( count( $timestamps ) ) { $dbr = wfGetDB( DB_SLAVE ); $where = $this->getLinkBatch()->constructSet( 'page', $dbr ); - $res = $dbr->select( 'page', + $res = $dbr->select( 'page', array( 'page_namespace', 'page_title', 'page_touched' ), $where, __METHOD__ ); while ( $row = $dbr->fetchObject( $res ) ) { @@ -313,11 +313,11 @@ class TitleListDependency extends CacheDependency { } /** - * @addtogroup Cache + * @ingroup Cache */ class GlobalDependency extends CacheDependency { var $name, $value; - + function __construct( $name ) { $this->name = $name; $this->value = $GLOBALS[$name]; @@ -329,7 +329,7 @@ class GlobalDependency extends CacheDependency { } /** - * @addtogroup Cache + * @ingroup Cache */ class ConstantDependency extends CacheDependency { var $name, $value; @@ -343,5 +343,3 @@ class ConstantDependency extends CacheDependency { return constant( $this->name ) != $this->value; } } - - diff --git a/includes/Category.php b/includes/Category.php new file mode 100644 index 00000000..acafc47a --- /dev/null +++ b/includes/Category.php @@ -0,0 +1,261 @@ +<?php +/** + * Category objects are immutable, strictly speaking. If you call methods that change the database, like to refresh link counts, the objects will be appropriately reinitialized. Member variables are lazy-initialized. + * + * TODO: Move some stuff from CategoryPage.php to here, and use that. + * + * @author Simetrical + */ + +class Category { + /** Name of the category, normalized to DB-key form */ + private $mName = null; + private $mID = null; + /** Category page title */ + private $mTitle = null; + /** Counts of membership (cat_pages, cat_subcats, cat_files) */ + private $mPages = null, $mSubcats = null, $mFiles = null; + + private function __construct() {} + + /** + * Set up all member variables using a database query. + * @return bool True on success, false on failure. + */ + protected function initialize() { + if ( $this->mName === null && $this->mTitle ) + $this->mName = $title->getDBKey(); + + if( $this->mName === null && $this->mID === null ) { + throw new MWException( __METHOD__.' has both names and IDs null' ); + } elseif( $this->mID === null ) { + $where = array( 'cat_title' => $this->mName ); + } elseif( $this->mName === null ) { + $where = array( 'cat_id' => $this->mID ); + } else { + # Already initialized + return true; + } + $dbr = wfGetDB( DB_SLAVE ); + $row = $dbr->selectRow( + 'category', + array( 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', + 'cat_files' ), + $where, + __METHOD__ + ); + if( !$row ) { + # Okay, there were no contents. Nothing to initialize. + if ( $this->mTitle ) { + # If there is a title object but no record in the category table, treat this as an empty category + $this->mID = false; + $this->mName = $this->mTitle->getDBKey(); + $this->mPages = 0; + $this->mSubcats = 0; + $this->mFiles = 0; + + return true; + } else { + return false; # Fail + } + } + $this->mID = $row->cat_id; + $this->mName = $row->cat_title; + $this->mPages = $row->cat_pages; + $this->mSubcats = $row->cat_subcats; + $this->mFiles = $row->cat_files; + + # (bug 13683) If the count is negative, then 1) it's obviously wrong + # and should not be kept, and 2) we *probably* don't have to scan many + # rows to obtain the correct figure, so let's risk a one-time recount. + if( $this->mPages < 0 || $this->mSubcats < 0 || + $this->mFiles < 0 ) { + $this->refreshCounts(); + } + + return true; + } + + /** + * Factory function. + * + * @param array $name A category name (no "Category:" prefix). It need + * not be normalized, with spaces replaced by underscores. + * @return mixed Category, or false on a totally invalid name + */ + public static function newFromName( $name ) { + $cat = new self(); + $title = Title::makeTitleSafe( NS_CATEGORY, $name ); + if( !is_object( $title ) ) { + return false; + } + + $cat->mTitle = $title; + $cat->mName = $title->getDBKey(); + + return $cat; + } + + /** + * Factory function. + * + * @param array $title Title for the category page + * @return mixed Category, or false on a totally invalid name + */ + public static function newFromTitle( $title ) { + $cat = new self(); + + $cat->mTitle = $title; + $cat->mName = $title->getDBKey(); + + return $cat; + } + + /** + * Factory function. + * + * @param array $id A category id + * @return Category + */ + public static function newFromID( $id ) { + $cat = new self(); + $cat->mID = intval( $id ); + return $cat; + } + + /** + * Factory function, for constructing a Category object from a result set + * + * @param $row result set row, must contain the cat_xxx fields. If the fields are null, + * the resulting Category object will represent an empty category if a title object + * was given. If the fields are null and no title was given, this method fails and returns false. + * @param $title optional title object for the category represented by the given row. + * May be provided if it is already known, to avoid having to re-create a title object later. + * @return Category + */ + public static function newFromRow( $row, $title = null ) { + $cat = new self(); + $cat->mTitle = $title; + + + # NOTE: the row often results from a LEFT JOIN on categorylinks. This may result in + # all the cat_xxx fields being null, if the category page exists, but nothing + # was ever added to the category. This case should be treated linke an empty + # category, if possible. + + if ( $row->cat_title === null ) { + if ( $title === null ) { + # the name is probably somewhere in the row, for example as page_title, + # but we can't know that here... + return false; + } else { + $cat->mName = $title->getDBKey(); # if we have a title object, fetch the category name from there + } + + $cat->mID = false; + $cat->mSubcats = 0; + $cat->mPages = 0; + $cat->mFiles = 0; + } else { + $cat->mName = $row->cat_title; + $cat->mID = $row->cat_id; + $cat->mSubcats = $row->cat_subcats; + $cat->mPages = $row->cat_pages; + $cat->mFiles = $row->cat_files; + } + + return $cat; + } + + /** @return mixed DB key name, or false on failure */ + public function getName() { return $this->getX( 'mName' ); } + /** @return mixed Category ID, or false on failure */ + public function getID() { return $this->getX( 'mID' ); } + /** @return mixed Total number of member pages, or false on failure */ + public function getPageCount() { return $this->getX( 'mPages' ); } + /** @return mixed Number of subcategories, or false on failure */ + public function getSubcatCount() { return $this->getX( 'mSubcats' ); } + /** @return mixed Number of member files, or false on failure */ + public function getFileCount() { return $this->getX( 'mFiles' ); } + + /** + * @return mixed The Title for this category, or false on failure. + */ + public function getTitle() { + if( $this->mTitle ) return $this->mTitle; + + if( !$this->initialize() ) { + return false; + } + + $this->mTitle = Title::makeTitleSafe( NS_CATEGORY, $this->mName ); + return $this->mTitle; + } + + /** Generic accessor */ + private function getX( $key ) { + if( !$this->initialize() ) { + return false; + } + return $this->{$key}; + } + + /** + * Refresh the counts for this category. + * + * @return bool True on success, false on failure + */ + public function refreshCounts() { + if( wfReadOnly() ) { + return false; + } + $dbw = wfGetDB( DB_MASTER ); + $dbw->begin(); + # Note, we must use names for this, since categorylinks does. + if( $this->mName === null ) { + if( !$this->initialize() ) { + return false; + } + } else { + # Let's be sure that the row exists in the table. We don't need to + # do this if we got the row from the table in initialization! + $dbw->insert( + 'category', + array( 'cat_title' => $this->mName ), + __METHOD__, + 'IGNORE' + ); + } + + $cond1 = $dbw->conditional( 'page_namespace='.NS_CATEGORY, 1, 'NULL' ); + $cond2 = $dbw->conditional( 'page_namespace='.NS_IMAGE, 1, 'NULL' ); + $result = $dbw->selectRow( + array( 'categorylinks', 'page' ), + array( 'COUNT(*) AS pages', + "COUNT($cond1) AS subcats", + "COUNT($cond2) AS files" + ), + array( 'cl_to' => $this->mName, 'page_id = cl_from' ), + __METHOD__, + 'LOCK IN SHARE MODE' + ); + $ret = $dbw->update( + 'category', + array( + 'cat_pages' => $result->pages, + 'cat_subcats' => $result->subcats, + 'cat_files' => $result->files + ), + array( 'cat_title' => $this->mName ), + __METHOD__ + ); + $dbw->commit(); + + # Now we should update our local counts. + $this->mPages = $result->pages; + $this->mSubcats = $result->subcats; + $this->mFiles = $result->files; + + return $ret; + } +} diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php index 6fbcd3c1..92e4e279 100644 --- a/includes/CategoryPage.php +++ b/includes/CategoryPage.php @@ -66,10 +66,12 @@ class CategoryPage extends Article { class CategoryViewer { var $title, $limit, $from, $until, - $articles, $articles_start_char, + $articles, $articles_start_char, $children, $children_start_char, $showGallery, $gallery, $skin; + /** Category object for this page */ + private $cat; function __construct( $title, $from = '', $until = '' ) { global $wgCategoryPagingLimit; @@ -77,8 +79,9 @@ class CategoryViewer { $this->from = $from; $this->until = $until; $this->limit = $wgCategoryPagingLimit; + $this->cat = Category::newFromName( $title->getDBKey() ); } - + /** * Format the category data list. * @@ -132,12 +135,21 @@ class CategoryViewer { } /** - * Add a subcategory to the internal lists + * Add a subcategory to the internal lists, using a Category object + */ + function addSubcategoryObject( $cat, $sortkey, $pageLength ) { + $title = $cat->getTitle(); + $this->addSubcategory( $title, $sortkey, $pageLength ); + } + + /** + * Add a subcategory to the internal lists, using a title object + * @deprectated kept for compatibility, please use addSubcategoryObject instead */ function addSubcategory( $title, $sortkey, $pageLength ) { global $wgContLang; // Subcategory; strip the 'Category' namespace from the link text. - $this->children[] = $this->getSkin()->makeKnownLinkObj( + $this->children[] = $this->getSkin()->makeKnownLinkObj( $title, $wgContLang->convertHtml( $title->getText() ) ); $this->children_start_char[] = $this->getSubcategorySortChar( $title, $sortkey ); @@ -152,13 +164,13 @@ class CategoryViewer { */ function getSubcategorySortChar( $title, $sortkey ) { global $wgContLang; - + if( $title->getPrefixedText() == $sortkey ) { $firstChar = $wgContLang->firstChar( $title->getDBkey() ); } else { $firstChar = $wgContLang->firstChar( $sortkey ); } - + return $wgContLang->convert( $firstChar ); } @@ -167,11 +179,10 @@ class CategoryViewer { */ function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) { if ( $this->showGallery ) { - $image = new Image( $title ); if( $this->flip ) { - $this->gallery->insert( $image ); + $this->gallery->insert( $title ); } else { - $this->gallery->add( $image ); + $this->gallery->add( $title ); } } else { $this->addPage( $title, $sortkey, $pageLength, $isRedirect ); @@ -211,17 +222,17 @@ class CategoryViewer { $this->flip = false; } $res = $dbr->select( - array( 'page', 'categorylinks' ), - array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey' ), + array( 'page', 'categorylinks', 'category' ), + array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey', + 'cat_id', 'cat_title', 'cat_subcats', 'cat_pages', 'cat_files' ), array( $pageCondition, - 'cl_from = page_id', - 'cl_to' => $this->title->getDBkey()), - #'page_is_redirect' => 0), - #+ $pageCondition, + 'cl_to' => $this->title->getDBkey() ), __METHOD__, array( 'ORDER BY' => $this->flip ? 'cl_sortkey DESC' : 'cl_sortkey', - 'USE INDEX' => 'cl_sortkey', - 'LIMIT' => $this->limit + 1 ) ); + 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ), + 'LIMIT' => $this->limit + 1 ), + array( 'categorylinks' => array( 'INNER JOIN', 'cl_from = page_id' ), + 'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY ) ) ); $count = 0; $this->nextPage = null; @@ -236,7 +247,8 @@ class CategoryViewer { $title = Title::makeTitle( $x->page_namespace, $x->page_title ); if( $title->getNamespace() == NS_CATEGORY ) { - $this->addSubcategory( $title, $x->cl_sortkey, $x->page_len ); + $cat = Category::newFromRow( $x, $title ); + $this->addSubcategoryObject( $cat, $x->cl_sortkey, $x->page_len ); } elseif( $this->showGallery && $title->getNamespace() == NS_IMAGE ) { $this->addImage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect ); } else { @@ -261,12 +273,14 @@ class CategoryViewer { function getSubcategorySection() { # Don't show subcategories section if there are none. $r = ''; - $c = count( $this->children ); - if( $c > 0 ) { + $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 .= wfMsgExt( 'subcategorycount', array( 'parse' ), $c ); + $r .= $countmsg; $r .= $this->formatList( $this->children, $this->children_start_char ); $r .= "\n</div>"; } @@ -277,11 +291,20 @@ class CategoryViewer { $ti = htmlspecialchars( $this->title->getText() ); # Don't show articles section if there are none. $r = ''; - $c = count( $this->articles ); - if( $c > 0 ) { + + # 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 .= wfMsgExt( 'categoryarticlecount', array( 'parse' ), $c ); + $r .= $countmsg; $r .= $this->formatList( $this->articles, $this->articles_start_char ); $r .= "\n</div>"; } @@ -290,10 +313,13 @@ class CategoryViewer { function getImageSection() { if( $this->showGallery && ! $this->gallery->isEmpty() ) { + $dbcnt = $this->cat->getFileCount(); + $rescnt = $this->gallery->count(); + $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' ); + return "<div id=\"mw-category-media\">\n" . '<h2>' . wfMsg( 'category-media-header', htmlspecialchars($this->title->getText()) ) . "</h2>\n" . - wfMsgExt( 'category-media-count', array( 'parse' ), $this->gallery->count() ) . - $this->gallery->toHTML() . "\n</div>"; + $countmsg . $this->gallery->toHTML() . "\n</div>"; } else { return ''; } @@ -440,7 +466,48 @@ class CategoryViewer { return "($prevLink) ($nextLink)"; } -} - - + /** + * What to do if the category table conflicts with the number of results + * returned? This function says what. It works the same whether the + * things being counted are articles, subcategories, or files. + * + * 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 int $rescnt The number of items returned by our database query. + * @param int $dbcnt The number of items according to the category table. + * @param string $type '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. + $totalrescnt = count( $this->articles ) + count( $this->children ) + + ($this->showGallery ? $this->gallery->count() : 0); + if($dbcnt == $rescnt || (($totalrescnt == $this->limit || $this->from + || $this->until) && $dbcnt > $rescnt)){ + # Case 1: seems sane. + $totalcnt = $dbcnt; + } elseif($totalrescnt < $this->limit && !$this->from && !$this->until){ + # Case 2: not sane, but salvageable. + $totalcnt = $rescnt; + } 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 ) ); + } +} diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php index b94dcf5e..d28f2eeb 100644 --- a/includes/Categoryfinder.php +++ b/includes/Categoryfinder.php @@ -188,5 +188,3 @@ class Categoryfinder { } } # END OF CLASS "Categoryfinder" - - diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php new file mode 100644 index 00000000..9bee1790 --- /dev/null +++ b/includes/ChangesFeed.php @@ -0,0 +1,129 @@ +<?php + +class ChangesFeed { + + public $format, $type, $titleMsg, $descMsg; + + public function __construct( $format, $type ) { + $this->format = $format; + $this->type = $type; + } + + public function getFeedObject( $title, $description ) { + global $wgSitename, $wgContLanguageCode, $wgFeedClasses, $wgTitle; + $feedTitle = "$wgSitename - {$title} [$wgContLanguageCode]"; + + return new $wgFeedClasses[$this->format]( + $feedTitle, htmlspecialchars( $description ), $wgTitle->getFullUrl() ); + } + + public function execute( $feed, $rows, $limit = 0 , $hideminor = false, $lastmod = false ) { + global $messageMemc, $wgFeedCacheTimeout; + global $wgFeedClasses, $wgTitle, $wgSitename, $wgContLanguageCode; + + if ( !FeedUtils::checkFeedOutput( $this->format ) ) { + return; + } + + $timekey = wfMemcKey( $this->type, $this->format, 'timestamp' ); + $key = wfMemcKey( $this->type, $this->format, 'limit', $limit, 'minor', $hideminor ); + + FeedUtils::checkPurge($timekey, $key); + + /* + * Bumping around loading up diffs can be pretty slow, so where + * possible we want to cache the feed output so the next visitor + * gets it quick too. + */ + $cachedFeed = $this->loadFromCache( $lastmod, $timekey, $key ); + if( is_string( $cachedFeed ) ) { + wfDebug( "RC: Outputting cached feed\n" ); + $feed->httpHeaders(); + echo $cachedFeed; + } else { + wfDebug( "RC: rendering new feed and caching it\n" ); + ob_start(); + self::generateFeed( $rows, $feed ); + $cachedFeed = ob_get_contents(); + ob_end_flush(); + $this->saveToCache( $cachedFeed, $timekey, $key ); + } + return true; + } + + public function saveToCache( $feed, $timekey, $key ) { + global $messageMemc; + $expire = 3600 * 24; # One day + $messageMemc->set( $key, $feed ); + $messageMemc->set( $timekey, wfTimestamp( TS_MW ), $expire ); + } + + public function loadFromCache( $lastmod, $timekey, $key ) { + global $wgFeedCacheTimeout, $messageMemc; + $feedLastmod = $messageMemc->get( $timekey ); + + if( ( $wgFeedCacheTimeout > 0 ) && $feedLastmod ) { + /* + * If the cached feed was rendered very recently, we may + * go ahead and use it even if there have been edits made + * since it was rendered. This keeps a swarm of requests + * from being too bad on a super-frequently edited wiki. + */ + + $feedAge = time() - wfTimestamp( TS_UNIX, $feedLastmod ); + $feedLastmodUnix = wfTimestamp( TS_UNIX, $feedLastmod ); + $lastmodUnix = wfTimestamp( TS_UNIX, $lastmod ); + + if( $feedAge < $wgFeedCacheTimeout || $feedLastmodUnix > $lastmodUnix) { + wfDebug( "RC: loading feed from cache ($key; $feedLastmod; $lastmod)...\n" ); + return $messageMemc->get( $key ); + } else { + wfDebug( "RC: cached feed timestamp check failed ($feedLastmod; $lastmod)\n" ); + } + } + return false; + } + + /** + * @todo document + * @param $rows Database resource with recentchanges rows + * @param $feed Feed object + */ + public static function generateFeed( $rows, &$feed ) { + wfProfileIn( __METHOD__ ); + + $feed->outHeader(); + + # Merge adjacent edits by one user + $sorted = array(); + $n = 0; + foreach( $rows as $obj ) { + if( $n > 0 && + $obj->rc_namespace >= 0 && + $obj->rc_cur_id == $sorted[$n-1]->rc_cur_id && + $obj->rc_user_text == $sorted[$n-1]->rc_user_text ) { + $sorted[$n-1]->rc_last_oldid = $obj->rc_last_oldid; + } else { + $sorted[$n] = $obj; + $n++; + } + } + + foreach( $sorted as $obj ) { + $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title ); + $talkpage = $title->getTalkPage(); + $item = new FeedItem( + $title->getPrefixedText(), + FeedUtils::formatDiff( $obj ), + $title->getFullURL( 'diff=' . $obj->rc_this_oldid . '&oldid=prev' ), + $obj->rc_timestamp, + ($obj->rc_deleted & Revision::DELETED_USER) ? wfMsgHtml('rev-deleted-user') : $obj->rc_user_text, + $talkpage->getFullURL() + ); + $feed->outItem( $item ); + } + $feed->outFooter(); + wfProfileOut( __METHOD__ ); + } + +}
\ No newline at end of file diff --git a/includes/ChangesList.php b/includes/ChangesList.php index 507e88fa..436f006e 100644 --- a/includes/ChangesList.php +++ b/includes/ChangesList.php @@ -27,7 +27,10 @@ class ChangesList { # Called by history lists and recent changes # - /** @todo document */ + /** + * Changeslist contructor + * @param Skin $skin + */ function __construct( &$skin ) { $this->skin =& $skin; $this->preCacheMessages(); @@ -54,12 +57,12 @@ class ChangesList { * As we use the same small set of messages in various methods and that * they are called often, we call them once and save them in $this->message */ - function preCacheMessages() { + private function preCacheMessages() { // Precache various messages if( !isset( $this->message ) ) { foreach( explode(' ', 'cur diff hist minoreditletter newpageletter last '. 'blocklink history boteditletter semicolon-separator' ) as $msg ) { - $this->message[$msg] = wfMsgExt( $msg, array( 'escape') ); + $this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) ); } } } @@ -67,8 +70,14 @@ class ChangesList { /** * Returns the appropriate flags for new page, minor change and patrolling + * @param bool $new + * @param bool $minor + * @param bool $patrolled + * @param string $nothing, string to use for empty space + * @param bool $bot + * @return string */ - function recentChangesFlags( $new, $minor, $patrolled, $nothing = ' ', $bot = false ) { + protected function recentChangesFlags( $new, $minor, $patrolled, $nothing = ' ', $bot = false ) { $f = $new ? '<span class="newpage">' . $this->message['newpageletter'] . '</span>' : $nothing; $f .= $minor ? '<span class="minor">' . $this->message['minoreditletter'] . '</span>' @@ -80,8 +89,9 @@ class ChangesList { /** * Returns text for the start of the tabular part of RC + * @return string */ - function beginRecentChangesList() { + public function beginRecentChangesList() { $this->rc_cache = array(); $this->rcMoveIndex = 0; $this->rcCacheIndex = 0; @@ -92,8 +102,9 @@ class ChangesList { /** * Returns text for the end of RC + * @return string */ - function endRecentChangesList() { + public function endRecentChangesList() { if( $this->rclistOpen ) { return "</ul>\n"; } else { @@ -101,8 +112,7 @@ class ChangesList { } } - - function insertMove( &$s, $rc ) { + protected function insertMove( &$s, $rc ) { # Diff $s .= '(' . $this->message['diff'] . ') ('; # Hist @@ -115,7 +125,7 @@ class ChangesList { $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) ); } - function insertDateHeader(&$s, $rc_timestamp) { + protected function insertDateHeader(&$s, $rc_timestamp) { global $wgLang; # Make date header if necessary @@ -131,15 +141,16 @@ class ChangesList { } } - function insertLog(&$s, $title, $logtype) { + protected function insertLog(&$s, $title, $logtype) { $logname = LogPage::logName( $logtype ); $s .= '(' . $this->skin->makeKnownLinkObj($title, $logname ) . ')'; } - - function insertDiffHist(&$s, &$rc, $unpatrolled) { + protected function insertDiffHist(&$s, &$rc, $unpatrolled) { # Diff link - if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) { + if( !$this->userCan($rc,Revision::DELETED_TEXT) ) { + $diffLink = $this->message['diff']; + } else if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) { $diffLink = $this->message['diff']; } else { $rcidparam = $unpatrolled @@ -163,14 +174,19 @@ class ChangesList { $s .= ') . . '; } - function insertArticleLink(&$s, &$rc, $unpatrolled, $watched) { + protected function insertArticleLink(&$s, &$rc, $unpatrolled, $watched) { # Article link # If it's a new article, there is no diff link, but if it hasn't been # patrolled yet, we need to give users a way to do so $params = ( $unpatrolled && $rc->mAttribs['rc_type'] == RC_NEW ) ? 'rcid='.$rc->mAttribs['rc_id'] : ''; - $articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params ); + if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) { + $articlelink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params ); + $articlelink = '<span class="history-deleted">'.$articlelink.'</span>'; + } else { + $articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params ); + } if( $watched ) $articlelink = "<strong class=\"mw-watched\">{$articlelink}</strong>"; global $wgContLang; @@ -178,27 +194,50 @@ class ChangesList { wfRunHooks('ChangesListInsertArticleLink', array(&$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched)); - + $s .= ' '.$articlelink; } - function insertTimestamp(&$s, $rc) { + protected function insertTimestamp(&$s, $rc) { global $wgLang; # Timestamp $s .= $this->message['semicolon-separator'] . ' ' . $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; } /** Insert links to user page, user talk page and eventually a blocking link */ - function insertUserRelatedLinks(&$s, &$rc) { - $s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); - $s .= $this->skin->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); + protected function insertUserRelatedLinks(&$s, &$rc) { + if ( $this->isDeleted($rc,Revision::DELETED_USER) ) { + $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-user') . '</span>'; + } else { + $s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); + $s .= $this->skin->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); + } + } + + /** insert a formatted action */ + protected function insertAction(&$s, &$rc) { + # Add action + if( $rc->mAttribs['rc_type'] == RC_LOG ) { + // log action + 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->skin, LogPage::extractParams($rc->mAttribs['rc_params']), true, true ); + } + } } /** insert a formatted comment */ - function insertComment(&$s, &$rc) { + protected function insertComment(&$s, &$rc) { # Add comment if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) { - $s .= $this->skin->commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ); + // log comment + if ( $this->isDeleted($rc,Revision::DELETED_COMMENT) ) { + $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>'; + } else { + $s .= $this->skin->commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ); + } } } @@ -206,15 +245,15 @@ class ChangesList { * Check whether to enable recent changes patrol features * @return bool */ - function usePatrol() { - global $wgUseRCPatrol, $wgUser; - return( $wgUseRCPatrol && ($wgUser->isAllowed('patrol') || $wgUser->isAllowed('patrolmarks')) ); + public static function usePatrol() { + global $wgUser; + return $wgUser->useRCPatrol(); } /** * Returns the string which indicates the number of watching users */ - function numberofWatchingusers( $count ) { + protected function numberofWatchingusers( $count ) { global $wgLang; static $cache = array(); if ( $count > 0 ) { @@ -227,6 +266,36 @@ class ChangesList { return ''; } } + + /** + * Determine if said field of a revision is hidden + * @param RCCacheEntry $rc + * @param int $field one of DELETED_* bitfield constants + * @return bool + */ + public static function isDeleted( $rc, $field ) { + return ($rc->mAttribs['rc_deleted'] & $field) == $field; + } + + /** + * Determine if the current user is allowed to view a particular + * field of this revision, if it's marked as deleted. + * @param RCCacheEntry $rc + * @param int $field + * @return bool + */ + public static function userCan( $rc, $field ) { + if( ( $rc->mAttribs['rc_deleted'] & $field ) == $field ) { + global $wgUser; + $permission = ( $rc->mAttribs['rc_deleted'] & Revision::DELETED_RESTRICTED ) == Revision::DELETED_RESTRICTED + ? 'suppressrevision' + : 'deleterevision'; + wfDebug( "Checking for $permission due to $field match on $rc->mAttribs['rc_deleted']\n" ); + return $wgUser->isAllowed( $permission ); + } else { + return true; + } + } } @@ -237,8 +306,8 @@ class OldChangesList extends ChangesList { /** * Format a line using the old system (aka without any javascript). */ - function recentChangesLine( &$rc, $watched = false ) { - global $wgContLang, $wgRCShowChangedSize; + public function recentChangesLine( &$rc, $watched = false ) { + global $wgContLang, $wgRCShowChangedSize, $wgUser; $fname = 'ChangesList::recentChangesLineOld'; wfProfileIn( $fname ); @@ -248,31 +317,35 @@ class OldChangesList extends ChangesList { extract( $rc->mAttribs ); # Should patrol-related stuff be shown? - $unpatrolled = $this->usePatrol() && $rc_patrolled == 0; + $unpatrolled = $wgUser->useRCPatrol() && $rc_patrolled == 0; $this->insertDateHeader($s,$rc_timestamp); $s .= '<li>'; - // moved pages + // Moved pages if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { $this->insertMove( $s, $rc ); - // log entries - } elseif ( $rc_namespace == NS_SPECIAL ) { + // Log entries + } elseif( $rc_log_type ) { + $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL ); + $this->insertLog( $s, $logtitle, $rc_log_type ); + // Log entries (old format) or log targets, and special pages + } elseif( $rc_namespace == NS_SPECIAL ) { list( $specialName, $specialSubpage ) = SpecialPage::resolveAliasWithSubpage( $rc_title ); if ( $specialName == 'Log' ) { $this->insertLog( $s, $rc->getTitle(), $specialSubpage ); } else { wfDebug( "Unexpected special page in recentchanges\n" ); } - // all other stuff + // Regular entries } else { wfProfileIn($fname.'-page'); $this->insertDiffHist($s, $rc, $unpatrolled); # M, N, b and ! (minor, new, bot and unpatrolled) - $s .= ' ' . $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $unpatrolled, '', $rc_bot ); + $s .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $unpatrolled, '', $rc_bot ); $this->insertArticleLink($s, $rc, $unpatrolled, $watched); wfProfileOut($fname.'-page'); @@ -285,11 +358,19 @@ class OldChangesList extends ChangesList { if( $wgRCShowChangedSize ) { $s .= ( $rc->getCharacterDifference() == '' ? '' : $rc->getCharacterDifference() . ' . . ' ); } - + # User tool links $this->insertUserRelatedLinks($s,$rc); + # Log action text (if any) + $this->insertAction($s, $rc); + # Edit or log comment $this->insertComment($s, $rc); - $s .= rtrim(' ' . $this->numberofWatchingusers($rc->numberofWatchingusers)); + # Mark revision as deleted if so + if ( !$rc_log_type && $this->isDeleted($rc,Revision::DELETED_TEXT) ) + $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>'; + if($rc->numberofWatchingusers > 0) { + $s .= ' ' . wfMsg('number_of_watching_users_RCview', $wgContLang->formatNum($rc->numberofWatchingusers)); + } $s .= "</li>\n"; @@ -308,8 +389,8 @@ class EnhancedChangesList extends ChangesList { /** * Format a line for enhanced recentchange (aka with javascript and block of lines). */ - function recentChangesLine( &$baseRC, $watched = false ) { - global $wgLang, $wgContLang; + public function recentChangesLine( &$baseRC, $watched = false ) { + global $wgLang, $wgContLang, $wgUser; # Create a specialised object $rc = RCCacheEntry::newFromParent( $baseRC ); @@ -331,17 +412,20 @@ class EnhancedChangesList extends ChangesList { } # Should patrol-related stuff be shown? - if( $this->usePatrol() ) { + if( $wgUser->useRCPatrol() ) { $rc->unpatrolled = !$rc_patrolled; } else { $rc->unpatrolled = false; } + $showdifflinks = true; # Make article link + // Page moves if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { $msg = ( $rc_type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir"; $clink = wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ), $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) ); + // Log entries (old format) and special pages } elseif( $rc_namespace == NS_SPECIAL ) { list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $rc_title ); if ( $specialName == 'Log' ) { @@ -352,13 +436,28 @@ class EnhancedChangesList extends ChangesList { wfDebug( "Unexpected special page in recentchanges\n" ); $clink = ''; } - } elseif( $rc->unpatrolled && $rc_type == RC_NEW ) { - # Unpatrolled new page, give rc_id in query + // New unpatrolled pages + } else if( $rc->unpatrolled && $rc_type == RC_NEW ) { $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', "rcid={$rc_id}" ); + // Log entries + } else if( $rc_type == RC_LOG ) { + if( $rc_log_type ) { + $logtitle = SpecialPage::getTitleFor( 'Log', $rc_log_type ); + $clink = '(' . $this->skin->makeKnownLinkObj( $logtitle, LogPage::logName($rc_log_type) ) . ')'; + } else { + $clink = $this->skin->makeLinkObj( $rc->getTitle(), '' ); + } + $watched = false; + // Edits } else { $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '' ); } + # Don't show unusable diff links + if ( !ChangesList::userCan($rc,Revision::DELETED_TEXT) ) { + $showdifflinks = false; + } + $time = $wgContLang->time( $rc_timestamp, true, true ); $rc->watched = $watched; $rc->link = $clink; @@ -375,7 +474,12 @@ class EnhancedChangesList extends ChangesList { $querydiff = $curIdEq."&diff=$rc_this_oldid&oldid=$rc_last_oldid$rcIdQuery"; $aprops = ' tabindex="'.$baseRC->counter.'"'; $curLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['cur'], $querycur, '' ,'', $aprops ); - if( $rc_type == RC_NEW || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + + # Make "diff" an "cur" links + if( !$showdifflinks ) { + $curLink = $this->message['cur']; + $diffLink = $this->message['diff']; + } else if( $rc_type == RC_NEW || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { if( $rc_type != RC_NEW ) { $curLink = $this->message['cur']; } @@ -385,21 +489,27 @@ class EnhancedChangesList extends ChangesList { } # Make "last" link - if( $rc_last_oldid == 0 || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + if( !$showdifflinks ) { + $lastLink = $this->message['last']; + } else if( $rc_last_oldid == 0 || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { $lastLink = $this->message['last']; } else { $lastLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['last'], - $curIdEq.'&diff='.$rc_this_oldid.'&oldid='.$rc_last_oldid . $rcIdQuery ); + $curIdEq.'&diff='.$rc_this_oldid.'&oldid='.$rc_last_oldid . $rcIdQuery ); } - $rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text ); + # Make user links + if( $this->isDeleted($rc,Revision::DELETED_USER) ) { + $rc->userlink = ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-user') . '</span>'; + } else { + $rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text ); + $rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text ); + } $rc->lastlink = $lastLink; $rc->curlink = $curLink; $rc->difflink = $diffLink; - $rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text ); - # Put accumulated information into the cache, for later display # Page moves go on their own line $title = $rc->getTitle(); @@ -408,7 +518,11 @@ class EnhancedChangesList extends ChangesList { # Use an @ character to prevent collision with page names $this->rc_cache['@@' . ($this->rcMoveIndex++)] = array($rc); } else { - if( !isset ( $this->rc_cache[$secureName] ) ) { + # Logs are grouped by type + if( $rc_type == RC_LOG ){ + $secureName = SpecialPage::getTitleFor( 'Log', $rc_log_type )->getPrefixedDBkey(); + } + if( !isset( $this->rc_cache[$secureName] ) ) { $this->rc_cache[$secureName] = array(); } array_push( $this->rc_cache[$secureName], $rc ); @@ -419,19 +533,29 @@ class EnhancedChangesList extends ChangesList { /** * Enhanced RC group */ - function recentChangesBlockGroup( $block ) { + protected function recentChangesBlockGroup( $block ) { global $wgLang, $wgContLang, $wgRCShowChangedSize; - $r = ''; + $r = '<table cellpadding="0" cellspacing="0" border="0" style="background: none"><tr>'; # Collate list of users - $isnew = false; - $unpatrolled = false; $userlinks = array(); + # Other properties + $unpatrolled = false; + $isnew = false; + $curId = $currentRevision = 0; + # Some catalyst variables... + $namehidden = true; + $alllogs = true; foreach( $block as $rcObj ) { $oldid = $rcObj->mAttribs['rc_last_oldid']; if( $rcObj->mAttribs['rc_new'] ) { $isnew = true; } + // If all log actions to this page were hidden, then don't + // give the name of the affected page for this block! + if( !$this->isDeleted( $rcObj, LogPage::DELETED_ACTION ) ) { + $namehidden = false; + } $u = $rcObj->userlink; if( !isset( $userlinks[$u] ) ) { $userlinks[$u] = 0; @@ -439,6 +563,18 @@ class EnhancedChangesList extends ChangesList { if( $rcObj->unpatrolled ) { $unpatrolled = true; } + if( $rcObj->mAttribs['rc_type'] != RC_LOG ) { + $alllogs = false; + } + # Get the latest entry with a page_id and oldid + # since logs may not have these. + if( !$curId && $rcObj->mAttribs['rc_cur_id'] ) { + $curId = $rcObj->mAttribs['rc_cur_id']; + } + if( !$currentRevision && $rcObj->mAttribs['rc_this_oldid'] ) { + $currentRevision = $rcObj->mAttribs['rc_this_oldid']; + } + $bot = $rcObj->mAttribs['rc_bot']; $userlinks[$u]++; } @@ -465,111 +601,148 @@ class EnhancedChangesList extends ChangesList { $toggleLink = "javascript:toggleVisibility('$rci','$rcm','$rcl')"; $tl = '<span id="'.$rcm.'"><a href="'.$toggleLink.'">' . $this->sideArrow() . '</a></span>'; $tl .= '<span id="'.$rcl.'" style="display:none"><a href="'.$toggleLink.'">' . $this->downArrow() . '</a></span>'; - $r .= $tl; + $r .= '<td valign="top" style="white-space: nowrap"><tt>'.$tl.' '; # Main line - $r .= '<tt>'; $r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, ' ', $bot ); # Timestamp - $r .= ' '.$block[0]->timestamp.' </tt>'; + $r .= ' '.$block[0]->timestamp.' </tt></td><td>'; # Article link - $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched ); - $r .= $wgContLang->getDirMark(); - - $curIdEq = 'curid=' . $block[0]->mAttribs['rc_cur_id']; - $currentRevision = $block[0]->mAttribs['rc_this_oldid']; - if( $block[0]->mAttribs['rc_type'] != RC_LOG ) { - # Changes - - $n = count($block); - static $nchanges = array(); - if ( !isset( $nchanges[$n] ) ) { - $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape'), - $wgLang->formatNum( $n ) ); - } + if( $namehidden ) { + $r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>'; + } else { + $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched ); + } - $r .= ' ('; + $r .= $wgContLang->getDirMark(); - if( $isnew ) { + $curIdEq = 'curid=' . $curId; + # Changes message + $n = count($block); + static $nchanges = array(); + if ( !isset( $nchanges[$n] ) ) { + $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape' ), $wgLang->formatNum( $n ) ); + } + # Total change link + $r .= ' '; + if( !$alllogs ) { + $r .= '('; + if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) { + $r .= $nchanges[$n]; + } else if( $isnew ) { $r .= $nchanges[$n]; } else { $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(), $nchanges[$n], $curIdEq."&diff=$currentRevision&oldid=$oldid" ); } - $r .= ') . . '; + } - if( $wgRCShowChangedSize ) { - # Character difference - $chardiff = $rcObj->getCharacterDifference( $block[ count( $block ) - 1 ]->mAttribs['rc_old_len'], - $block[0]->mAttribs['rc_new_len'] ); - if( $chardiff == '' ) { - $r .= ' ('; - } else { - $r .= ' ' . $chardiff. ' . . '; - } - } - - # History + # Character difference (does not apply if only log items) + if( $wgRCShowChangedSize && !$alllogs ) { + $last = 0; + $first = count($block) - 1; + # Some events (like logs) have an "empty" size, so we need to skip those... + while( $last < $first && $block[$last]->mAttribs['rc_new_len'] === NULL ) { + $last++; + } + while( $first > $last && $block[$first]->mAttribs['rc_old_len'] === NULL ) { + $first--; + } + # Get net change + $chardiff = $rcObj->getCharacterDifference( $block[$first]->mAttribs['rc_old_len'], + $block[$last]->mAttribs['rc_new_len'] ); + + if( $chardiff == '' ) { + $r .= ' '; + } else { + $r .= ' ' . $chardiff. ' . . '; + } + } + + # History + if( $alllogs ) { + // don't show history link for logs + } else if( $namehidden || !$block[0]->getTitle()->exists() ) { + $r .= '(' . $this->message['history'] . ')'; + } else { $r .= '(' . $this->skin->makeKnownLinkObj( $block[0]->getTitle(), - $this->message['history'], $curIdEq.'&action=history' ); - $r .= ')'; + $this->message['history'], $curIdEq.'&action=history' ) . ')'; } $r .= $users; - $r .= $this->numberofWatchingusers($block[0]->numberofWatchingusers); - $r .= "<br />\n"; + + $r .= "</td></tr></table>\n"; # Sub-entries - $r .= '<div id="'.$rci.'" style="display:none">'; + $r .= '<div id="'.$rci.'" style="display:none;"><table cellpadding="0" cellspacing="0" border="0" style="background: none">'; foreach( $block as $rcObj ) { # Get rc_xxxx variables // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( $rcObj->mAttribs ); - $r .= $this->spacerArrow(); - $r .= '<tt> '; + #$r .= '<tr><td valign="top">'.$this->spacerArrow(); + $r .= '<tr><td valign="top">'; + $r .= '<tt>'.$this->spacerIndent() . $this->spacerIndent(); $r .= $this->recentChangesFlags( $rc_new, $rc_minor, $rcObj->unpatrolled, ' ', $rc_bot ); - $r .= ' </tt>'; + $r .= ' </tt></td><td valign="top">'; $o = ''; if( $rc_this_oldid != 0 ) { $o = 'oldid='.$rc_this_oldid; } + # Log timestamp if( $rc_type == RC_LOG ) { - $link = $rcObj->timestamp; + $link = '<tt>'.$rcObj->timestamp.'</tt> '; + # Revision link + } else if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) { + $link = '<span class="history-deleted"><tt>'.$rcObj->timestamp.'</tt></span> '; } else { - $link = $this->skin->makeKnownLinkObj( $rcObj->getTitle(), $rcObj->timestamp, $curIdEq.'&'.$o ); + $rcIdEq = ($rcObj->unpatrolled && $rc_type == RC_NEW) ? '&rcid='.$rcObj->mAttribs['rc_id'] : ''; + + $link = '<tt>'.$this->skin->makeKnownLinkObj( $rcObj->getTitle(), $rcObj->timestamp, $curIdEq.'&'.$o.$rcIdEq ).'</tt>'; + if( $this->isDeleted($rcObj,Revision::DELETED_TEXT) ) + $link = '<span class="history-deleted">'.$link.'</span> '; } - $link = '<tt>'.$link.'</tt>'; - $r .= $link; - $r .= ' ('; - $r .= $rcObj->curlink; - $r .= $this->message['semicolon-separator'] . ' '; - $r .= $rcObj->lastlink; - $r .= ') . . '; + + if ( !$rc_type == RC_LOG || $rc_type == RC_NEW ) { + $r .= ' ('; + $r .= $rcObj->curlink; + $r .= $this->message['semicolon-separator'] . ' '; + $r .= $rcObj->lastlink; + $r .= ')'; + } + $r .= ' . . '; # Character diff if( $wgRCShowChangedSize ) { $r .= ( $rcObj->getCharacterDifference() == '' ? '' : $rcObj->getCharacterDifference() . ' . . ' ) ; } - + # User links $r .= $rcObj->userlink; $r .= $rcObj->usertalklink; - $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() ); - $r .= "<br />\n"; + // log action + parent::insertAction( $r, $rcObj ); + // log comment + parent::insertComment( $r, $rcObj ); + # Mark revision as deleted + if( !$rc_log_type && $this->isDeleted($rcObj,Revision::DELETED_TEXT) ) { + $r .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>'; + } + + $r .= "</td></tr>\n"; } - $r .= "</div>\n"; + $r .= "</table></div>\n"; $this->rcCacheIndex++; return $r; } - function maybeWatchedLink( $link, $watched=false ) { + protected function maybeWatchedLink( $link, $watched=false ) { if( $watched ) { // FIXME: css style might be more appropriate return '<strong class="mw-watched">' . $link . '</strong>'; @@ -583,9 +756,8 @@ class EnhancedChangesList extends ChangesList { * @param string $dir one of '', 'd', 'l', 'r' * @param string $alt text * @return string HTML <img> tag - * @access private */ - function arrow( $dir, $alt='' ) { + protected function arrow( $dir, $alt='' ) { global $wgStylePath; $encUrl = htmlspecialchars( $wgStylePath . '/common/images/Arr_' . $dir . '.png' ); $encAlt = htmlspecialchars( $alt ); @@ -596,9 +768,8 @@ class EnhancedChangesList extends ChangesList { * Generate HTML for a right- or left-facing arrow, * depending on language direction. * @return string HTML <img> tag - * @access private */ - function sideArrow() { + protected function sideArrow() { global $wgContLang; $dir = $wgContLang->isRTL() ? 'l' : 'r'; return $this->arrow( $dir, '+' ); @@ -608,26 +779,32 @@ class EnhancedChangesList extends ChangesList { * Generate HTML for a down-facing arrow * depending on language direction. * @return string HTML <img> tag - * @access private */ - function downArrow() { + protected function downArrow() { return $this->arrow( 'd', '-' ); } /** * Generate HTML for a spacer image * @return string HTML <img> tag - * @access private */ - function spacerArrow() { + protected function spacerArrow() { return $this->arrow( '', ' ' ); } /** + * Add a set of spaces + * @return string HTML <td> tag + */ + protected function spacerIndent() { + return ' '; + } + + /** * Enhanced RC ungrouped line. * @return string a HTML formated line (generated using $r) */ - function recentChangesBlockLine( $rcObj ) { + protected function recentChangesBlockLine( $rcObj ) { global $wgContLang, $wgRCShowChangedSize; # Get rc_xxxx variables @@ -635,29 +812,35 @@ class EnhancedChangesList extends ChangesList { extract( $rcObj->mAttribs ); $curIdEq = 'curid='.$rc_cur_id; - $r = ''; + $r = '<table cellspacing="0" cellpadding="0" border="0" style="background: none"><tr>'; - # Spacer image - $r .= $this->spacerArrow(); + $r .= '<td valign="top" style="white-space: nowrap"><tt>' . $this->spacerArrow() . ' '; # Flag and Timestamp - $r .= '<tt>'; - if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { - $r .= ' '; + $r .= ' '; // 4 flags -> 4 spaces } else { $r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, ' ', $rc_bot ); } - $r .= ' '.$rcObj->timestamp.' </tt>'; - - # Article link - $r .= $this->maybeWatchedLink( $rcObj->link, $rcObj->watched ); - - # Diff - $r .= ' ('. $rcObj->difflink . $this->message['semicolon-separator'] . ' '; + $r .= ' '.$rcObj->timestamp.' </tt></td><td>'; + + # Article or log link + if( $rc_log_type ) { + $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL ); + $logname = LogPage::logName( $rc_log_type ); + $r .= '(' . $this->skin->makeKnownLinkObj($logtitle, $logname ) . ')'; + } else if( !$this->userCan($rcObj,Revision::DELETED_TEXT) ) { + $r .= '<span class="history-deleted">' . $rcObj->link . '</span>'; + } else { + $r .= $this->maybeWatchedLink( $rcObj->link, $rcObj->watched ); + } - # Hist - $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ) . ') . . '; + # Diff and hist links + if ( $rc_type != RC_LOG ) { + $r .= ' ('. $rcObj->difflink . $this->message['semicolon-separator'] . ' '; + $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ) . ')'; + } + $r .= ' . . '; # Character diff if( $wgRCShowChangedSize ) { @@ -665,16 +848,32 @@ class EnhancedChangesList extends ChangesList { } # User/talk - $r .= $rcObj->userlink . $rcObj->usertalklink; + $r .= ' '.$rcObj->userlink . $rcObj->usertalklink; - # Comment + # Log action (if any) + if( $rc_log_type ) { + if( $this->isDeleted($rcObj,LogPage::DELETED_ACTION) ) { + $r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>'; + } else { + $r .= ' ' . LogPage::actionText( $rc_log_type, $rc_log_action, $rcObj->getTitle(), + $this->skin, LogPage::extractParams($rc_params), true, true ); + } + } + + # Edit or log comment if( $rc_type != RC_MOVE && $rc_type != RC_MOVE_OVER_REDIRECT ) { - $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() ); + // log comment + if ( $this->isDeleted($rcObj,LogPage::DELETED_COMMENT) ) { + $r .= ' <span class="history-deleted">' . wfMsg('rev-deleted-comment') . '</span>'; + } else { + $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() ); + } } + # Show how many people are watching this if enabled $r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers); - $r .= "<br />\n"; + $r .= "</td></tr></table>\n"; return $r; } @@ -682,7 +881,7 @@ class EnhancedChangesList extends ChangesList { * If enhanced RC is in use, this function takes the previously cached * RC lines, arranges them, and outputs the HTML */ - function recentChangesBlock() { + protected function recentChangesBlock() { if( count ( $this->rc_cache ) == 0 ) { return ''; } @@ -702,7 +901,7 @@ class EnhancedChangesList extends ChangesList { * Returns text for the end of RC * If enhanced RC is in use, returns pretty much all the text */ - function endRecentChangesList() { + public function endRecentChangesList() { return $this->recentChangesBlock() . parent::endRecentChangesList(); } diff --git a/includes/Credits.php b/includes/Credits.php index 580a8d92..6326e3a2 100644 --- a/includes/Credits.php +++ b/includes/Credits.php @@ -29,7 +29,7 @@ function showCreditsPage($article) { $fname = 'showCreditsPage'; wfProfileIn( $fname ); - + $wgOut->setPageTitle( $article->mTitle->getPrefixedText() ); $wgOut->setSubtitle( wfMsg( 'creditspage' ) ); $wgOut->setArticleFlag( false ); @@ -184,5 +184,3 @@ function creditOthersLink($article) { $skin = $wgUser->getSkin(); return $skin->makeKnownLink($article->mTitle->getPrefixedText(), wfMsg('others'), 'action=credits'); } - - diff --git a/includes/DatabaseFunctions.php b/includes/DatabaseFunctions.php index a4a0444f..ad6e7f6c 100644 --- a/includes/DatabaseFunctions.php +++ b/includes/DatabaseFunctions.php @@ -2,7 +2,8 @@ /** * Legacy database functions, for compatibility with pre-1.3 code * NOTE: this file is no longer loaded by default. - * + * @file + * @ingroup Database */ /** @@ -399,4 +400,3 @@ function wfUseIndexClause( $index, $dbi = DB_SLAVE ) { return false; } } - diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 376e55b1..d6db7030 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -31,20 +31,25 @@ require_once( "$IP/includes/SiteConfiguration.php" ); $wgConf = new SiteConfiguration; /** MediaWiki version number */ -$wgVersion = '1.12.0'; +$wgVersion = '1.13.0'; /** Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; -/** - * Name of the project namespace. If left set to false, $wgSitename will be +/** + * Name of the project namespace. If left set to false, $wgSitename will be * used instead. */ $wgMetaNamespace = false; /** - * Name of the project talk namespace. If left set to false, a name derived - * from the name of the project namespace will be used. + * Name of the project talk namespace. + * + * Normally you can ignore this and it will be something like + * $wgMetaNamespace . "_talk". In some languages, you may want to set this + * manually for grammatical reasons. It is currently only respected by those + * languages where it might be relevant and where no automatic grammar converter + * exists. */ $wgMetaNamespaceTalk = false; @@ -90,25 +95,20 @@ if( isset( $_SERVER['SERVER_PORT'] ) $wgScriptPath = '/wiki'; /** - * Whether to support URLs like index.php/Page_title - * These often break when PHP is set up in CGI mode. - * PATH_INFO *may* be correct if cgi.fix_pathinfo is - * set, but then again it may not; lighttpd converts - * incoming path data to lowercase on systems with - * case-insensitive filesystems, and there have been - * reports of problems on Apache as well. + * Whether to support URLs like index.php/Page_title These often break when PHP + * is set up in CGI mode. PATH_INFO *may* be correct if cgi.fix_pathinfo is set, + * but then again it may not; lighttpd converts incoming path data to lowercase + * on systems with case-insensitive filesystems, and there have been reports of + * problems on Apache as well. * * To be safe we'll continue to keep it off by default. * - * Override this to false if $_SERVER['PATH_INFO'] - * contains unexpectedly incorrect garbage, or to - * true if it is really correct. - * - * The default $wgArticlePath will be set based on - * this value at runtime, but if you have customized - * it, having this incorrectly set to true can - * cause redirect loops when "pretty URLs" are used. + * Override this to false if $_SERVER['PATH_INFO'] contains unexpectedly + * incorrect garbage, or to true if it is really correct. * + * The default $wgArticlePath will be set based on this value at runtime, but if + * you have customized it, having this incorrectly set to true can cause + * redirect loops when "pretty URLs" are used. */ $wgUsePathInfo = ( strpos( php_sapi_name(), 'cgi' ) === false ) && @@ -116,53 +116,56 @@ $wgUsePathInfo = ( strpos( php_sapi_name(), 'isapi' ) === false ); -/**#@+ +/**@{ * Script users will request to get articles - * ATTN: Old installations used wiki.phtml and redirect.phtml - - * make sure that LocalSettings.php is correctly set! + * ATTN: Old installations used wiki.phtml and redirect.phtml - make sure that + * LocalSettings.php is correctly set! * - * Will be set based on $wgScriptPath in Setup.php if not overridden - * in LocalSettings.php. Generally you should not need to change this - * unless you don't like seeing "index.php". + * Will be set based on $wgScriptPath in Setup.php if not overridden in + * LocalSettings.php. Generally you should not need to change this unless you + * don't like seeing "index.php". */ -$wgScriptExtension = '.php'; /// extension to append to script names by default -$wgScript = false; /// defaults to "{$wgScriptPath}/index{$wgScriptExtension}" -$wgRedirectScript = false; /// defaults to "{$wgScriptPath}/redirect{$wgScriptExtension}" -/**#@-*/ +$wgScriptExtension = '.php'; ///< extension to append to script names by default +$wgScript = false; ///< defaults to "{$wgScriptPath}/index{$wgScriptExtension}" +$wgRedirectScript = false; ///< defaults to "{$wgScriptPath}/redirect{$wgScriptExtension}" +/**@}*/ -/**#@+ +/**@{ * 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. - * - * @global string */ /** * style path as seen by users */ -$wgStylePath = false; /// defaults to "{$wgScriptPath}/skins" +$wgStylePath = false; ///< defaults to "{$wgScriptPath}/skins" /** * filesystem stylesheets directory */ -$wgStyleDirectory = false; /// defaults to "{$IP}/skins" +$wgStyleDirectory = false; ///< defaults to "{$IP}/skins" $wgStyleSheetPath = &$wgStylePath; -$wgArticlePath = false; /// default to "{$wgScript}/$1" or "{$wgScript}?title=$1", depending on $wgUsePathInfo +$wgArticlePath = false; ///< default to "{$wgScript}/$1" or "{$wgScript}?title=$1", depending on $wgUsePathInfo $wgVariantArticlePath = false; -$wgUploadPath = false; /// defaults to "{$wgScriptPath}/images" -$wgUploadDirectory = false; /// defaults to "{$IP}/images" +$wgUploadPath = false; ///< defaults to "{$wgScriptPath}/images" +$wgUploadDirectory = false; ///< defaults to "{$IP}/images" $wgHashedUploadDirectory = true; -$wgLogo = false; /// defaults to "{$wgStylePath}/common/images/wiki.png" +$wgLogo = false; ///< defaults to "{$wgStylePath}/common/images/wiki.png" $wgFavicon = '/favicon.ico'; -$wgAppleTouchIcon = false; /// This one'll actually default to off. For iPhone and iPod Touch web app bookmarks -$wgMathPath = false; /// defaults to "{$wgUploadPath}/math" -$wgMathDirectory = false; /// defaults to "{$wgUploadDirectory}/math" -$wgTmpDirectory = false; /// defaults to "{$wgUploadDirectory}/tmp" +$wgAppleTouchIcon = false; ///< This one'll actually default to off. For iPhone and iPod Touch web app bookmarks +$wgMathPath = false; ///< defaults to "{$wgUploadPath}/math" +$wgMathDirectory = false; ///< defaults to "{$wgUploadDirectory}/math" +$wgTmpDirectory = false; ///< defaults to "{$wgUploadDirectory}/tmp" $wgUploadBaseUrl = ""; -/**#@-*/ +/**@}*/ + +/** + * Default value for chmoding of new directories. + */ +$wgDirectoryMode = 0777; /** * New file storage paths; currently used only for deleted files. @@ -172,46 +175,46 @@ $wgUploadBaseUrl = ""; * */ $wgFileStore = array(); -$wgFileStore['deleted']['directory'] = false;// Defaults to $wgUploadDirectory/deleted -$wgFileStore['deleted']['url'] = null; // Private -$wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split +$wgFileStore['deleted']['directory'] = false;///< Defaults to $wgUploadDirectory/deleted +$wgFileStore['deleted']['url'] = null; ///< Private +$wgFileStore['deleted']['hash'] = 3; ///< 3-level subdirectory split -/**#@+ +/**@{ * File repository structures * * $wgLocalFileRepo is a single repository structure, and $wgForeignFileRepo is - * a an array of such structures. Each repository structure is an associative - * array of properties configuring the repository. + * a an array of such structures. Each repository structure is an associative + * array of properties configuring the repository. * * Properties required for all repos: - * class The class name for the repository. May come from the core or an extension. + * class The class name for the repository. May come from the core or an extension. * The core repository classes are LocalRepo, ForeignDBRepo, FSRepo. * * name A unique name for the repository. - * + * * For all core repos: * 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) - * transformVia404 Whether to skip media file transformation on parse and rely on a 404 + * transformVia404 Whether to skip media file transformation on parse and rely on a 404 * handler instead. - * initialCapital Equivalent to $wgCapitalLinks, determines whether filenames implicitly + * initialCapital Equivalent to $wgCapitalLinks, determines whether filenames implicitly * start with a capital letter. The current implementation may give incorrect - * description page links when the local $wgCapitalLinks and initialCapital + * description page links when the local $wgCapitalLinks and initialCapital * are mismatched. * pathDisclosureProtection - * May be 'paranoid' to remove all parameters from error messages, 'none' to - * leave the paths in unchanged, or 'simple' to replace paths with + * May be 'paranoid' to remove all parameters from error messages, 'none' to + * leave the paths in unchanged, or 'simple' to replace paths with * placeholders. Default for LocalRepo is 'simple'. * * These settings describe a foreign MediaWiki installation. They are optional, and will be ignored * for local repositories: * descBaseUrl URL of image description pages, e.g. http://en.wikipedia.org/wiki/Image: - * scriptDirUrl URL of the MediaWiki installation, equivalent to $wgScriptPath, e.g. + * scriptDirUrl URL of the MediaWiki installation, equivalent to $wgScriptPath, e.g. * http://en.wikipedia.org/w * * articleUrl Equivalent to $wgArticlePath, e.g. http://en.wikipedia.org/wiki/$1 - * fetchDescription Fetch the text of the remote file description page. Equivalent to + * fetchDescription Fetch the text of the remote file description page. Equivalent to * $wgFetchCommonsDescriptions. * * ForeignDBRepo: @@ -220,12 +223,12 @@ $wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split * tablePrefix Table prefix, the foreign wiki's $wgDBprefix * hasSharedCache True if the wiki's shared cache is accessible via the local $wgMemc * - * The default is to initialise these arrays from the MW<1.11 backwards compatible settings: + * The default is to initialise these arrays from the MW<1.11 backwards compatible settings: * $wgUploadPath, $wgThumbnailScriptPath, $wgSharedUploadDirectory, etc. */ $wgLocalFileRepo = false; $wgForeignFileRepos = array(); -/**#@-*/ +/**@}*/ /** * Allowed title characters -- regex character class @@ -274,7 +277,6 @@ $wgUrlProtocols = array( /** internal name of virus scanner. This servers as a key to the $wgAntivirusSetup array. * Set this to NULL to disable virus scanning. If not null, every file uploaded will be scanned for viruses. - * @global string $wgAntivirus */ $wgAntivirus= NULL; @@ -301,8 +303,6 @@ $wgAntivirus= NULL; * "messagepattern" is a perl regular expression to extract the meaningful part of the scanners * output. The relevant part should be matched as group one (\1). * If not defined or the pattern does not match, the full message is shown to the user. - * - * @global array $wgAntivirusSetup */ $wgAntivirusSetup = array( @@ -336,51 +336,54 @@ $wgAntivirusSetup = array( ); -/** Determines if a failed virus scan (AV_SCAN_FAILED) will cause the file to be rejected. - * @global boolean $wgAntivirusRequired -*/ +/** Determines if a failed virus scan (AV_SCAN_FAILED) will cause the file to be rejected. */ $wgAntivirusRequired= true; -/** Determines if the mime type of uploaded files should be checked - * @global boolean $wgVerifyMimeType -*/ +/** Determines if the mime type of uploaded files should be checked */ $wgVerifyMimeType= true; -/** Sets the mime type definition file to use by MimeMagic.php. -* @global string $wgMimeTypeFile -*/ +/** Sets the mime type definition file to use by MimeMagic.php. */ $wgMimeTypeFile= "includes/mime.types"; #$wgMimeTypeFile= "/etc/mime.types"; #$wgMimeTypeFile= NULL; #use built-in defaults only. -/** Sets the mime type info file to use by MimeMagic.php. -* @global string $wgMimeInfoFile -*/ +/** Sets the mime type info file to use by MimeMagic.php. */ $wgMimeInfoFile= "includes/mime.info"; #$wgMimeInfoFile= NULL; #use built-in defaults only. /** Switch for loading the FileInfo extension by PECL at runtime. - * This should be used only if fileinfo is installed as a shared object + * This should be used only if fileinfo is installed as a shared object * or a dynamic libary - * @global string $wgLoadFileinfoExtension -*/ + */ $wgLoadFileinfoExtension= false; /** Sets an external mime detector program. The command must print only * the mime type to standard output. * The name of the file to process will be appended to the command given here. * If not set or NULL, mime_content_type will be used if available. -*/ + */ $wgMimeDetectorCommand= NULL; # use internal mime_content_type function, available since php 4.3.0 #$wgMimeDetectorCommand= "file -bi"; #use external mime detector (Linux) /** Switch for trivial mime detection. Used by thumb.php to disable all fance * things, because only a few types of images are needed and file extensions * can be trusted. -*/ + */ $wgTrivialMimeDetection= false; /** + * Additional XML types we can allow via mime-detection. + * 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? +); + +/** * To set 'pretty' URL paths for actions other than * plain page views, add to this array. For instance: * 'edit' => "$wgScriptPath/edit/$1" @@ -430,7 +433,7 @@ $wgMaxUploadSize = 1024*1024*100; # 100MB * Useful if you want to use a shared repository by default * without disabling local uploads (use $wgEnableUploads = false for that) * e.g. $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload'; -*/ + */ $wgUploadNavigationUrl = false; /** @@ -461,12 +464,6 @@ $wgHashedSharedUploadDirectory = true; */ $wgRepositoryBaseUrl = "http://commons.wikimedia.org/wiki/Image:"; -/** - * Experimental feature still under debugging. - */ -$wgFileRedirects = false; - - # # Email settings # @@ -474,7 +471,6 @@ $wgFileRedirects = false; /** * Site admin email address * Default to wikiadmin@SERVER_NAME - * @global string $wgEmergencyContact */ $wgEmergencyContact = 'wikiadmin@' . $wgServerName; @@ -482,7 +478,6 @@ $wgEmergencyContact = 'wikiadmin@' . $wgServerName; * Password reminder email address * The address we should use as sender when a user is requesting his password * Default to apache@SERVER_NAME - * @global string $wgPasswordSender */ $wgPasswordSender = 'MediaWiki Mail <apache@' . $wgServerName . '>'; @@ -498,14 +493,12 @@ $wgNoReplyAddress = 'reply@not.possible'; * Set to true to enable the e-mail basic features: * Password reminders, etc. If sending e-mail on your * server doesn't work, you might want to disable this. - * @global bool $wgEnableEmail */ $wgEnableEmail = true; /** * Set to true to enable user-to-user e-mail. * This can potentially be abused, as it's hard to track. - * @global bool $wgEnableUserEmail */ $wgEnableUserEmail = true; @@ -537,13 +530,11 @@ $wgPasswordReminderResendTime = 24; * "username" => user, * "password" => password * </code> - * - * @global mixed $wgSMTP */ $wgSMTP = false; -/**#@+ +/**@{ * Database settings */ /** database host name or ip address */ @@ -556,21 +547,39 @@ $wgDBname = 'wikidb'; $wgDBconnection = ''; /** Database username */ $wgDBuser = 'wikiuser'; -/** Database type - */ -$wgDBtype = "mysql"; +/** Database user's password */ +$wgDBpassword = ''; +/** Database type */ +$wgDBtype = 'mysql'; + /** Search type * Leave as null to select the default search engine for the - * selected database type (eg SearchMySQL4), or set to a class + * selected database type (eg SearchMySQL), or set to a class * name to override to a custom search engine. */ $wgSearchType = null; + /** Table name prefix */ $wgDBprefix = ''; /** MySQL table options to use during installation or update */ -$wgDBTableOptions = 'TYPE=InnoDB'; +$wgDBTableOptions = 'ENGINE=InnoDB'; + +/** Mediawiki schema */ +$wgDBmwschema = 'mediawiki'; +/** Tsearch2 schema */ +$wgDBts2schema = 'public'; + +/** To override default SQLite data directory ($docroot/../data) */ +$wgSQLiteDataDir = ''; + +/** + * Make all database connections secretly go to localhost. Fool the load balancer + * thinking there is an arbitrarily large cluster of servers to connect to. + * Useful for debugging. + */ +$wgAllDBsAreLocalhost = false; -/**#@-*/ +/**@}*/ /** Live high performance sites should disable this - some checks acquire giant mysql locks */ @@ -578,55 +587,76 @@ $wgCheckDBSchema = true; /** - * Shared database for multiple wikis. Presently used for storing a user table + * Shared database for multiple wikis. Commonly used for storing a user table * for single sign-on. The server for this database must be the same as for the * main database. + * For backwards compatibility the shared prefix is set to the same as the local + * prefix, and the user table is listed in the default list of shared tables. + * + * $wgSharedTables may be customized with a list of tables to share in the shared + * datbase. However it is advised to limit what tables you do share as many of + * MediaWiki's tables may have side effects if you try to share them. * EXPERIMENTAL */ -$wgSharedDB = null; - -# Database load balancer -# This is a two-dimensional array, an array of server info structures -# Fields are: -# host: Host name -# dbname: Default database name -# user: DB user -# password: DB password -# type: "mysql" or "postgres" -# load: ratio of DB_SLAVE load, must be >=0, the sum of all loads must be >0 -# groupLoads: array of load ratios, the key is the query group name. A query may belong -# to several groups, the most specific group defined here is used. -# -# flags: bit field -# DBO_DEFAULT -- turns on DBO_TRX only if !$wgCommandLineMode (recommended) -# DBO_DEBUG -- equivalent of $wgDebugDumpSql -# DBO_TRX -- wrap entire request in a transaction -# DBO_IGNORE -- ignore errors (not useful in LocalSettings.php) -# DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php) -# -# max lag: (optional) Maximum replication lag before a slave will taken out of rotation -# max threads: (optional) Maximum number of running threads -# -# These and any other user-defined properties will be assigned to the mLBInfo member -# variable of the Database object. -# -# Leave at false to use the single-server variables above. If you set this -# variable, the single-server variables will generally be ignored (except -# perhaps in some command-line scripts). -# -# The first server listed in this array (with key 0) will be the master. The -# rest of the servers will be slaves. To prevent writes to your slaves due to -# accidental misconfiguration or MediaWiki bugs, set read_only=1 on all your -# slaves in my.cnf. You can set read_only mode at runtime using: -# -# SET @@read_only=1; -# -# Since the effect of writing to a slave is so damaging and difficult to clean -# 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. -# +$wgSharedDB = null; +$wgSharedPrefix = false; # Defaults to $wgDBprefix +$wgSharedTables = array( 'user' ); + +/** + * Database load balancer + * This is a two-dimensional array, an array of server info structures + * Fields are: + * host: Host name + * dbname: Default database name + * user: DB user + * password: DB password + * type: "mysql" or "postgres" + * load: ratio of DB_SLAVE load, must be >=0, the sum of all loads must be >0 + * groupLoads: array of load ratios, the key is the query group name. A query may belong + * to several groups, the most specific group defined here is used. + * + * flags: bit field + * DBO_DEFAULT -- turns on DBO_TRX only if !$wgCommandLineMode (recommended) + * DBO_DEBUG -- equivalent of $wgDebugDumpSql + * DBO_TRX -- wrap entire request in a transaction + * DBO_IGNORE -- ignore errors (not useful in LocalSettings.php) + * DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php) + * + * max lag: (optional) Maximum replication lag before a slave will taken out of rotation + * max threads: (optional) Maximum number of running threads + * + * These and any other user-defined properties will be assigned to the mLBInfo member + * variable of the Database object. + * + * Leave at false to use the single-server variables above. If you set this + * variable, the single-server variables will generally be ignored (except + * perhaps in some command-line scripts). + * + * The first server listed in this array (with key 0) will be the master. The + * rest of the servers will be slaves. To prevent writes to your slaves due to + * accidental misconfiguration or MediaWiki bugs, set read_only=1 on all your + * slaves in my.cnf. You can set read_only mode at runtime using: + * + * SET @@read_only=1; + * + * Since the effect of writing to a slave is so damaging and difficult to clean + * 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; +/** + * Load balancer factory configuration + * To set up a multi-master wiki farm, set the class here to something that + * can return a LoadBalancer with an appropriate master on a call to getMainLB(). + * The class identified here is responsible for reading $wgDBservers, + * $wgDBserver, etc., so overriding it may cause those globals to be ignored. + * + * The LBFactory_Multi class is provided for this purpose, please see + * includes/db/LBFactory_Multi.php for configuration information. + */ +$wgLBFactoryConf = array( 'class' => 'LBFactory_Simple' ); + /** How long to wait for a slave to catch up to the master */ $wgMasterWaitTimeout = 10; @@ -681,40 +711,28 @@ $wgDBmysql5 = false; */ $wgLocalDatabases = array(); -/** - * For multi-wiki clusters with multiple master servers; if an alternate - * is listed for the requested database, a connection to it will be opened - * instead of to the current wiki's regular master server when cross-wiki - * data operations are done from here. - * - * Requires that the other server be accessible by network, with the same - * username/password as the primary. - * - * eg $wgAlternateMaster['enwiki'] = 'ariel'; - */ -$wgAlternateMaster = array(); - -/** +/** @{ * Object cache settings * See Defines.php for types */ $wgMainCacheType = CACHE_NONE; $wgMessageCacheType = CACHE_ANYTHING; $wgParserCacheType = CACHE_ANYTHING; +/**@}*/ $wgParserCacheExpireTime = 86400; $wgSessionsInMemcached = false; -$wgLinkCacheMemcached = false; # Not fully tested -/** +/**@{ * Memcached-specific settings * See docs/memcached.txt */ $wgUseMemCached = false; -$wgMemCachedDebug = false; # Will be set to false in Setup.php, if the server isn't working +$wgMemCachedDebug = false; ///< Will be set to false in Setup.php, if the server isn't working $wgMemCachedServers = array( '127.0.0.1:11000' ); $wgMemCachedPersistent = false; +/**@}*/ /** * Directory for local copy of message cache, for use in addition to memcached @@ -759,14 +777,16 @@ $wgInputEncoding = 'UTF-8'; $wgOutputEncoding = 'UTF-8'; $wgEditEncoding = ''; -# Set this to eg 'ISO-8859-1' to perform character set -# conversion when loading old revisions not marked with -# "utf-8" flag. Use this when converting wiki to UTF-8 -# without the burdensome mass conversion of old text data. -# -# NOTE! This DOES NOT touch any fields other than old_text. -# Titles, comments, user names, etc still must be converted -# en masse in the database before continuing as a UTF-8 wiki. +/** + * Set this to eg 'ISO-8859-1' to perform character set + * conversion when loading old revisions not marked with + * "utf-8" flag. Use this when converting wiki to UTF-8 + * without the burdensome mass conversion of old text data. + * + * NOTE! This DOES NOT touch any fields other than old_text. + * Titles, comments, user names, etc still must be converted + * en masse in the database before continuing as a UTF-8 wiki. + */ $wgLegacyEncoding = false; /** @@ -792,12 +812,14 @@ $wgDocType = '-//W3C//DTD XHTML 1.0 Transitional//EN'; $wgDTD = 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'; $wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml'; -# Permit other namespaces in addition to the w3.org default. -# Use the prefix for the key and the namespace for the value. For -# example: -# $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg'; -# Normally we wouldn't have to define this in the root <html> -# element, but IE needs it there in some circumstances. +/** + * Permit other namespaces in addition to the w3.org default. + * Use the prefix for the key and the namespace for the value. For + * example: + * $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg'; + * Normally we wouldn't have to define this in the root <html> + * element, but IE needs it there in some circumstances. + */ $wgXhtmlNamespaces = array(); /** Enable to allow rewriting dates in page text. @@ -836,10 +858,10 @@ $wgMaxMsgCacheEntrySize = 10000; */ $wgCheckSerialized = true; -# Whether to enable language variant conversion. +/** Whether to enable language variant conversion. */ $wgDisableLangConversion = false; -# Default variant code, if false, the default will be the language code +/** Default variant code, if false, the default will be the language code */ $wgDefaultLanguageVariant = false; /** @@ -849,21 +871,15 @@ $wgDefaultLanguageVariant = false; */ $wgLoginLanguageSelector = false; -# Whether to use zhdaemon to perform Chinese text processing -# zhdaemon is under developement, so normally you don't want to -# use it unless for testing +/** + * Whether to use zhdaemon to perform Chinese text processing + * zhdaemon is under developement, so normally you don't want to + * use it unless for testing + */ $wgUseZhdaemon = false; $wgZhdaemonHost="localhost"; $wgZhdaemonPort=2004; -/** Normally you can ignore this and it will be something - like $wgMetaNamespace . "_talk". In some languages, you - may want to set this manually for grammatical reasons. - It is currently only respected by those languages - where it might be relevant and where no automatic - grammar converter exists. -*/ -$wgMetaNamespaceTalk = false; # Miscellaneous configuration settings # @@ -886,7 +902,7 @@ $wgInterwikiExpiry = 10800; # Expiry time for cache of interwiki table 2 - wiki and global levels 3 - site levels $wgInterwikiFallbackSite - if unable to resolve from cache -*/ + */ $wgInterwikiCache = false; $wgInterwikiScopes = 3; $wgInterwikiFallbackSite = 'wiki'; @@ -907,19 +923,22 @@ $wgRedirectSources = false; $wgShowIPinHeader = true; # For non-logged in users -$wgMaxNameChars = 255; # Maximum number of bytes in username $wgMaxSigChars = 255; # Maximum number of Unicode characters in signature $wgMaxArticleSize = 2048; # Maximum article size in kilobytes +# Maximum number of bytes in username. You want to run the maintenance +# script ./maintenancecheckUsernames.php once you have changed this value +$wgMaxNameChars = 255; $wgMaxPPNodeCount = 1000000; # A complexity limit on template expansion /** * Maximum recursion depth for templates within templates. - * The current parser adds two levels to the PHP call stack for each template, + * The current parser adds two levels to the PHP call stack for each template, * and xdebug limits the call stack to 100 by default. So this should hopefully * stop the parser before it hits the xdebug limit. */ $wgMaxTemplateDepth = 40; +$wgMaxPPExpandDepth = 40; $wgExtraSubtitle = ''; $wgSiteSupportPage = ''; # A page where you users can receive donations @@ -929,16 +948,13 @@ $wgSiteSupportPage = ''; # A page where you users can receive donations * Its contents will be shown to users as part of the read-only warning * message. */ -$wgReadOnlyFile = false; /// defaults to "{$wgUploadDirectory}/lock_yBgMBwiR"; +$wgReadOnlyFile = false; ///< defaults to "{$wgUploadDirectory}/lock_yBgMBwiR"; /** * The debug log file should be not be publicly accessible if it is used, as it * may contain private data. */ $wgDebugLogFile = ''; -/**#@+ - * @global bool - */ $wgDebugRedirects = false; $wgDebugRawPage = false; # Avoid overlapping debug entries by leaving out CSS @@ -960,6 +976,11 @@ $wgDebugDumpSql = false; $wgDebugLogGroups = array(); /** + * Show the contents of $wgHooks in Special:Version + */ +$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. @@ -1026,18 +1047,16 @@ $wgSidebarCacheExpiry = 86400; * * Retroactively changing this variable will not affect * the existing count (cf. maintenance/recount.sql). -*/ + */ $wgUseCommaCount = false; -/**#@-*/ - /** * wgHitcounterUpdateFreq sets how often page counters should be updated, higher * values are easier on the database. A value of 1 causes the counters to be * updated on every hit, any higher value n cause them to update *on average* * every n hits. Should be set to either 1 or something largish, eg 1000, for * maximum efficiency. -*/ + */ $wgHitcounterUpdateFreq = 1; # Basic user rights and block settings @@ -1048,7 +1067,8 @@ $wgBlockAllowsUTEdit = false; # Blocks allow users to edit their own user tal $wgSysopEmailBans = true; # Allow sysops to ban users from accessing Emailuser # Pages anonymous user may see as an array, e.g.: -# array ( "Main Page", "Special:Userlogin", "Wikipedia:Help"); +# array ( "Main Page", "Wikipedia:Help"); +# Special:Userlogin and Special:Resetpass are always whitelisted. # NOTE: This will only work if $wgGroupPermissions['*']['read'] # is false -- see below. Otherwise, ALL pages are accessible, # regardless of this setting. @@ -1057,7 +1077,7 @@ $wgSysopEmailBans = true; # Allow sysops to ban users from accessing Email # directory name unguessable, or use .htaccess to protect it. $wgWhitelistRead = false; -/** +/** * Should editors be required to have a validated e-mail * address before being allowed to edit? */ @@ -1083,79 +1103,88 @@ $wgEmailConfirmToEdit=false; $wgGroupPermissions = array(); // Implicit group for all visitors -$wgGroupPermissions['*' ]['createaccount'] = true; -$wgGroupPermissions['*' ]['read'] = true; -$wgGroupPermissions['*' ]['edit'] = true; -$wgGroupPermissions['*' ]['createpage'] = true; -$wgGroupPermissions['*' ]['createtalk'] = true; +$wgGroupPermissions['*' ]['createaccount'] = true; +$wgGroupPermissions['*' ]['read'] = true; +$wgGroupPermissions['*' ]['edit'] = true; +$wgGroupPermissions['*' ]['createpage'] = true; +$wgGroupPermissions['*' ]['createtalk'] = true; +$wgGroupPermissions['*' ]['writeapi'] = true; // Implicit group for all logged-in accounts -$wgGroupPermissions['user' ]['move'] = true; -$wgGroupPermissions['user' ]['read'] = true; -$wgGroupPermissions['user' ]['edit'] = true; -$wgGroupPermissions['user' ]['createpage'] = true; -$wgGroupPermissions['user' ]['createtalk'] = true; -$wgGroupPermissions['user' ]['upload'] = true; -$wgGroupPermissions['user' ]['reupload'] = true; -$wgGroupPermissions['user' ]['reupload-shared'] = true; -$wgGroupPermissions['user' ]['minoredit'] = true; -$wgGroupPermissions['user' ]['purge'] = true; // can use ?action=purge without clicking "ok" +$wgGroupPermissions['user' ]['move'] = true; +$wgGroupPermissions['user' ]['move-subpages'] = true; +$wgGroupPermissions['user' ]['read'] = true; +$wgGroupPermissions['user' ]['edit'] = true; +$wgGroupPermissions['user' ]['createpage'] = true; +$wgGroupPermissions['user' ]['createtalk'] = true; +$wgGroupPermissions['user' ]['writeapi'] = true; +$wgGroupPermissions['user' ]['upload'] = true; +$wgGroupPermissions['user' ]['reupload'] = true; +$wgGroupPermissions['user' ]['reupload-shared'] = true; +$wgGroupPermissions['user' ]['minoredit'] = true; +$wgGroupPermissions['user' ]['purge'] = true; // can use ?action=purge without clicking "ok" // Implicit group for accounts that pass $wgAutoConfirmAge $wgGroupPermissions['autoconfirmed']['autoconfirmed'] = true; -// Implicit group for accounts with confirmed email addresses -// This has little use when email address confirmation is off -$wgGroupPermissions['emailconfirmed']['emailconfirmed'] = true; - // Users with bot privilege can have their edits hidden // from various log pages by default -$wgGroupPermissions['bot' ]['bot'] = true; -$wgGroupPermissions['bot' ]['autoconfirmed'] = true; -$wgGroupPermissions['bot' ]['nominornewtalk'] = true; -$wgGroupPermissions['bot' ]['autopatrol'] = true; +$wgGroupPermissions['bot' ]['bot'] = true; +$wgGroupPermissions['bot' ]['autoconfirmed'] = true; +$wgGroupPermissions['bot' ]['nominornewtalk'] = true; +$wgGroupPermissions['bot' ]['autopatrol'] = true; $wgGroupPermissions['bot' ]['suppressredirect'] = true; -$wgGroupPermissions['bot' ]['apihighlimits'] = true; +$wgGroupPermissions['bot' ]['apihighlimits'] = true; +$wgGroupPermissions['bot' ]['writeapi'] = true; +#$wgGroupPermissions['bot' ]['editprotected'] = true; // can edit all protected pages without cascade protection enabled // Most extra permission abilities go to this group -$wgGroupPermissions['sysop']['block'] = true; -$wgGroupPermissions['sysop']['createaccount'] = true; -$wgGroupPermissions['sysop']['delete'] = true; -$wgGroupPermissions['sysop']['bigdelete'] = true; // can be separately configured for pages with > $wgDeleteRevisionsLimit revs -$wgGroupPermissions['sysop']['deletedhistory'] = true; // can view deleted history entries, but not see or restore the text -$wgGroupPermissions['sysop']['undelete'] = true; -$wgGroupPermissions['sysop']['editinterface'] = true; -$wgGroupPermissions['sysop']['editusercssjs'] = true; -$wgGroupPermissions['sysop']['import'] = true; -$wgGroupPermissions['sysop']['importupload'] = true; -$wgGroupPermissions['sysop']['move'] = true; -$wgGroupPermissions['sysop']['patrol'] = true; -$wgGroupPermissions['sysop']['autopatrol'] = true; -$wgGroupPermissions['sysop']['protect'] = true; -$wgGroupPermissions['sysop']['proxyunbannable'] = true; -$wgGroupPermissions['sysop']['rollback'] = true; -$wgGroupPermissions['sysop']['trackback'] = true; -$wgGroupPermissions['sysop']['upload'] = true; -$wgGroupPermissions['sysop']['reupload'] = true; -$wgGroupPermissions['sysop']['reupload-shared'] = true; -$wgGroupPermissions['sysop']['unwatchedpages'] = true; -$wgGroupPermissions['sysop']['autoconfirmed'] = true; -$wgGroupPermissions['sysop']['upload_by_url'] = true; -$wgGroupPermissions['sysop']['ipblock-exempt'] = true; -$wgGroupPermissions['sysop']['blockemail'] = true; -$wgGroupPermissions['sysop']['markbotedits'] = true; +$wgGroupPermissions['sysop']['block'] = true; +$wgGroupPermissions['sysop']['createaccount'] = true; +$wgGroupPermissions['sysop']['delete'] = true; +$wgGroupPermissions['sysop']['bigdelete'] = true; // can be separately configured for pages with > $wgDeleteRevisionsLimit revs +$wgGroupPermissions['sysop']['deletedhistory'] = true; // can view deleted history entries, but not see or restore the text +$wgGroupPermissions['sysop']['undelete'] = true; +$wgGroupPermissions['sysop']['editinterface'] = true; +$wgGroupPermissions['sysop']['editusercssjs'] = true; +$wgGroupPermissions['sysop']['import'] = true; +$wgGroupPermissions['sysop']['importupload'] = true; +$wgGroupPermissions['sysop']['move'] = true; +$wgGroupPermissions['sysop']['move-subpages'] = true; +$wgGroupPermissions['sysop']['patrol'] = true; +$wgGroupPermissions['sysop']['autopatrol'] = true; +$wgGroupPermissions['sysop']['protect'] = true; +$wgGroupPermissions['sysop']['proxyunbannable'] = true; +$wgGroupPermissions['sysop']['rollback'] = true; +$wgGroupPermissions['sysop']['trackback'] = true; +$wgGroupPermissions['sysop']['upload'] = true; +$wgGroupPermissions['sysop']['reupload'] = true; +$wgGroupPermissions['sysop']['reupload-shared'] = true; +$wgGroupPermissions['sysop']['unwatchedpages'] = true; +$wgGroupPermissions['sysop']['autoconfirmed'] = true; +$wgGroupPermissions['sysop']['upload_by_url'] = true; +$wgGroupPermissions['sysop']['ipblock-exempt'] = true; +$wgGroupPermissions['sysop']['blockemail'] = true; +$wgGroupPermissions['sysop']['markbotedits'] = true; $wgGroupPermissions['sysop']['suppressredirect'] = true; -$wgGroupPermissions['sysop']['apihighlimits'] = true; -#$wgGroupPermissions['sysop']['mergehistory'] = true; +$wgGroupPermissions['sysop']['apihighlimits'] = true; +$wgGroupPermissions['sysop']['browsearchive'] = true; +$wgGroupPermissions['sysop']['noratelimit'] = true; +#$wgGroupPermissions['sysop']['mergehistory'] = true; // Permission to change users' group assignments -$wgGroupPermissions['bureaucrat']['userrights'] = true; +$wgGroupPermissions['bureaucrat']['userrights'] = true; +$wgGroupPermissions['bureaucrat']['noratelimit'] = true; // Permission to change users' groups assignments across wikis #$wgGroupPermissions['bureaucrat']['userrights-interwiki'] = true; -// Experimental permissions, not ready for production use -//$wgGroupPermissions['sysop']['deleterevision'] = true; -//$wgGroupPermissions['bureaucrat']['hiderevision'] = true; +#$wgGroupPermissions['sysop']['deleterevision'] = true; +// To hide usernames from users and Sysops +#$wgGroupPermissions['suppress']['hideuser'] = true; +// To hide revisions/log items from users and Sysops +#$wgGroupPermissions['suppress']['suppressrevision'] = true; +// For private suppression log access +#$wgGroupPermissions['suppress']['suppressionlog'] = true; /** * The developer group is deprecated, but can be activated if need be @@ -1169,7 +1198,7 @@ $wgGroupPermissions['bureaucrat']['userrights'] = true; /** * Implicit groups, aren't shown on Special:Listusers or somewhere else */ -$wgImplicitGroups = array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ); +$wgImplicitGroups = array( '*', 'user', 'autoconfirmed' ); /** * These are the groups that users are allowed to add to or remove from @@ -1206,11 +1235,11 @@ $wgNamespaceProtection = array(); $wgNamespaceProtection[ NS_MEDIAWIKI ] = array( 'editinterface' ); /** -* Pages in namespaces in this array can not be used as templates. -* Elements must be numeric namespace ids. -* Among other things, this may be useful to enforce read-restrictions -* which may otherwise be bypassed by using the template machanism. -*/ + * Pages in namespaces in this array can not be used as templates. + * Elements must be numeric namespace ids. + * Among other things, this may be useful to enforce read-restrictions + * which may otherwise be bypassed by using the template machanism. + */ $wgNonincludableNamespaces = array(); /** @@ -1252,7 +1281,6 @@ $wgAutopromote = array( array( APCOND_EDITCOUNT, &$wgAutoConfirmCount ), array( APCOND_AGE, &$wgAutoConfirmAge ), ), - 'emailconfirmed' => APCOND_EMAILCONFIRMED, ); /** @@ -1260,22 +1288,37 @@ $wgAutopromote = array( * groups at Special:Userrights. Example configuration: * * // Bureaucrat can add any group - * $wgAddGroups['bureaucrat'] = true; + * $wgAddGroups['bureaucrat'] = true; * // Bureaucrats can only remove bots and sysops - * $wgRemoveGroups['bureaucrat'] = array( 'bot', 'sysop' ); + * $wgRemoveGroups['bureaucrat'] = array( 'bot', 'sysop' ); * // Sysops can make bots - * $wgAddGroups['sysop'] = array( 'bot' ); + * $wgAddGroups['sysop'] = array( 'bot' ); * // Sysops can disable other sysops in an emergency, and disable bots - * $wgRemoveGroups['sysop'] = array( 'sysop', 'bot' ); + * $wgRemoveGroups['sysop'] = array( 'sysop', 'bot' ); */ $wgAddGroups = $wgRemoveGroups = array(); + +/** + * A list of available rights, in addition to the ones defined by the core. + * For extensions only. + */ +$wgAvailableRights = array(); + /** * Optional to restrict deletion of pages with higher revision counts * to users with the 'bigdelete' permission. (Default given to sysops.) */ $wgDeleteRevisionsLimit = 0; +/** + * Used to figure out if a user is "active" or not. User::isActiveEditor() + * sees if a user has made at least $wgActiveUserEditCount number of edits + * within the last $wgActiveUserDays days. + */ +$wgActiveUserEditCount = 30; +$wgActiveUserDays = 30; + # Proxy scanner settings # @@ -1325,7 +1368,7 @@ $wgCacheEpoch = '20030516000000'; * to ensure that client-side caches don't keep obsolete copies of global * styles. */ -$wgStyleVersion = '116'; +$wgStyleVersion = '164'; # Server-side caching: @@ -1338,7 +1381,7 @@ $wgStyleVersion = '116'; $wgUseFileCache = false; /** Directory where the cached page will be saved */ -$wgFileCacheDirectory = false; /// defaults to "{$wgUploadDirectory}/cache"; +$wgFileCacheDirectory = false; ///< defaults to "{$wgUploadDirectory}/cache"; /** * When using the file cache, we can store the cached HTML gzipped to save disk @@ -1382,17 +1425,17 @@ $wgEnotifMinorEdits = true; # UPO; false: "minor edits" on pages do not trigger $wgEnotifImpersonal = false; -# Maximum number of users to mail at once when using impersonal mail. Should +# Maximum number of users to mail at once when using impersonal mail. Should # match the limit on your mail server. $wgEnotifMaxRecips = 500; # Send mails via the job queue. $wgEnotifUseJobQ = false; -/** +/** * Array of usernames who will be sent a notification email for every change which occurs on a wiki */ -$wgUsersNotifedOnAllChanges = array(); +$wgUsersNotifiedOnAllChanges = array(); /** Show watching users in recent changes, watchlist and page history views */ $wgRCShowWatchingUsers = false; # UPO @@ -1403,7 +1446,7 @@ $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 + * before and after the edit is below that value, the value will be * highlighted on the RC page. */ $wgRCChangedSizeThreshold = -500; @@ -1484,6 +1527,35 @@ $wgCookiePath = '/'; $wgCookieSecure = ($wgProto == 'https'); $wgDisableCookieCheck = false; +/** + * Set $wgCookiePrefix to use a custom one. Setting to false sets the default of + * using the database name. + */ +$wgCookiePrefix = false; + +/** + * Set authentication cookies to HttpOnly to prevent access by JavaScript, + * in browsers that support this feature. This can mitigates some classes of + * XSS attack. + * + * Only supported on PHP 5.2 or higher. + */ +$wgCookieHttpOnly = version_compare("5.2", PHP_VERSION, "<"); + +/** + * If the requesting browser matches a regex in this blacklist, we won't + * send it cookies with HttpOnly mode, even if $wgCookieHttpOnly is on. + */ +$wgHttpOnlyBlacklist = array( + // Internet Explorer for Mac; sometimes the cookies work, sometimes + // they don't. It's difficult to predict, as combinations of path + // and expiration options affect its parsing. + '/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/', +); + +/** A list of cookies that vary the cache (for use by extensions) */ +$wgCacheVaryCookies = array(); + /** Override to customise the session name */ $wgSessionName = false; @@ -1499,6 +1571,9 @@ $wgAllowExternalImages = false; */ $wgAllowExternalImagesFrom = ''; +/** Allows to move images and other media files. Experemintal, not sure if it always works */ +$wgAllowImageMoving = false; + /** Disable database-intensive features */ $wgMiserMode = false; /** Disable all query pages if miser mode is on, not just some */ @@ -1520,6 +1595,7 @@ $wgJobClasses = array( 'html_cache_update' => 'HTMLCacheUpdateJob', // backwards-compatible 'sendMail' => 'EmaillingJob', 'enotifNotify' => 'EnotifNotifyJob', + 'fixDoubleRedirect' => 'DoubleRedirectJob', ); /** @@ -1577,6 +1653,46 @@ $wgDisableCounters = false; $wgDisableTextSearch = false; $wgDisableSearchContext = false; + + +/** + * Set to true to have nicer highligted text in search results, + * by default off due to execution overhead + */ +$wgAdvancedSearchHighlighting = false; + +/** + * Regexp to match word boundaries, defaults for non-CJK languages + * should be empty for CJK since the words are not separate + */ +$wgSearchHighlightBoundaries = version_compare("5.1", PHP_VERSION, "<")? '[\p{Z}\p{P}\p{C}]' + : '[ ,.;:!?~!@#$%\^&*\(\)+=\-\\|\[\]"\'<>\n\r\/{}]'; // PHP 5.0 workaround + +/** + * Template for OpenSearch suggestions, defaults to API action=opensearch + * + * Sites with heavy load would tipically have these point to a custom + * PHP wrapper to avoid firing up mediawiki for every keystroke + * + * Placeholders: {searchTerms} + * + */ +$wgOpenSearchTemplate = false; + +/** + * Enable suggestions while typing in search boxes + * (results are passed around in OpenSearch format) + */ +$wgEnableMWSuggest = false; + +/** + * Template for internal MediaWiki suggestion engine, defaults to API action=opensearch + * + * Placeholders: {searchTerms}, {namespaces}, {dbname} + * + */ +$wgMWSuggestTemplate = false; + /** * If you've disabled search semi-permanently, this also disables updates to the * table. If you ever re-enable, be sure to rebuild the search table. @@ -1632,6 +1748,11 @@ $wgAntiLockFlags = 0; $wgDiff3 = '/usr/bin/diff3'; /** + * Path to the GNU diff utility. + */ +$wgDiff = '/usr/bin/diff'; + +/** * We can also compress text stored in the 'text' table. If this is set on, new * revisions will be compressed on page save if zlib support is available. Any * compressed revisions will be decompressed on load regardless of this setting @@ -1679,8 +1800,8 @@ $wgCheckFileExtensions = true; */ $wgStrictFileExtensions = true; -/** Warn if uploaded files are larger than this (in bytes)*/ -$wgUploadSizeWarning = 150 * 1024; +/** Warn if uploaded files are larger than this (in bytes), or false to disable*/ +$wgUploadSizeWarning = false; /** For compatibility with old installations set to false */ $wgPasswordSalt = true; @@ -1717,7 +1838,7 @@ $wgSiteNotice = ''; # Images settings # -/** +/** * Plugins for media file type handling. * Each entry in the array maps a MIME type to a class name */ @@ -1726,6 +1847,7 @@ $wgMediaHandlers = array( 'image/png' => 'BitmapHandler', 'image/gif' => 'BitmapHandler', 'image/x-ms-bmp' => 'BmpHandler', + 'image/x-bmp' => 'BmpHandler', 'image/svg+xml' => 'SvgHandler', // official 'image/svg' => 'SvgHandler', // compat 'image/vnd.djvu' => 'DjVuHandler', // official @@ -1774,13 +1896,14 @@ $wgSVGConverters = array( 'inkscape' => '$path/inkscape -z -w $width -f $input -e $output', 'batik' => 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input', 'rsvg' => '$path/rsvg -w$width -h$height $input $output', + 'imgserv' => '$path/imgserv-wrapper -i svg -o png -w$width $input $output', ); /** Pick one of the above */ $wgSVGConverter = 'ImageMagick'; /** If not in the executable PATH, specify */ $wgSVGConverterPath = ''; /** Don't scale a SVG larger than this */ -$wgSVGMaxSize = 1024; +$wgSVGMaxSize = 2048; /** * 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 @@ -1809,11 +1932,11 @@ $wgThumbnailEpoch = '20030516000000'; $wgIgnoreImageErrors = false; /** - * Allow thumbnail rendering on page view. If this is false, a valid - * thumbnail URL is still output, but no file will be created at - * the target location. This may save some time if you have a - * thumb.php or 404 handler set up which is faster than the regular - * webserver(s). + * Allow thumbnail rendering on page view. If this is false, a valid + * thumbnail URL is still output, but no file will be created at + * the target location. This may save some time if you have a + * thumb.php or 404 handler set up which is faster than the regular + * webserver(s). */ $wgGenerateThumbnailOnParse = true; @@ -1843,11 +1966,29 @@ $wgPutIPinRC = true; */ $wgRCMaxAge = 7 * 24 * 3600; +/** + * Filter $wgRCLinkDays by $wgRCMaxAge to avoid showing links for numbers higher than what will be stored. + * Note that this is disabled by default because we sometimes do have RC data which is beyond the limit + * for some reason, and some users may use the high numbers to display that data which is still there. + */ +$wgRCFilterByAge = false; + +/** + * List of Days and Limits options to list in the Special:Recentchanges and Special:Recentchangeslinked pages. + */ +$wgRCLinkLimits = array( 50, 100, 250, 500 ); +$wgRCLinkDays = array( 1, 3, 7, 14, 30 ); # Send RC updates via UDP $wgRC2UDPAddress = false; $wgRC2UDPPort = false; $wgRC2UDPPrefix = ''; +$wgRC2UDPOmitBots = false; + +# Enable user search in Special:Newpages +# This is really a temporary hack around an index install bug on some Wikipedias. +# Kill it once fixed. +$wgEnableNewpagesUserFilter = true; # # Copyright and credits settings @@ -1987,7 +2128,7 @@ $wgTidyInternal = extension_loaded( 'tidy' ); $wgDebugTidy = false; /** - * Validate the overall output using tidy and refuse + * Validate the overall output using tidy and refuse * to display the page if it's not valid. */ $wgValidateAllHtml = false; @@ -2002,49 +2143,65 @@ $wgDefaultSkin = 'monobook'; * $wgDefaultUserOptions ['editsection'] = 0; * */ -$wgDefaultUserOptions = array( - 'quickbar' => 1, - 'underline' => 2, - 'cols' => 80, - 'rows' => 25, - 'searchlimit' => 20, - 'contextlines' => 5, - 'contextchars' => 50, - 'skin' => false, - 'math' => 1, - 'rcdays' => 7, - 'rclimit' => 50, - 'wllimit' => 250, - 'highlightbroken' => 1, - 'stubthreshold' => 0, - 'previewontop' => 1, - 'editsection' => 1, - 'editsectiononrightclick'=> 0, - 'showtoc' => 1, - 'showtoolbar' => 1, - 'date' => 'default', - 'imagesize' => 2, - 'thumbsize' => 2, - 'rememberpassword' => 0, - 'enotifwatchlistpages' => 0, - 'enotifusertalkpages' => 1, - 'enotifminoredits' => 0, - 'enotifrevealaddr' => 0, - 'shownumberswatching' => 1, - 'fancysig' => 0, - 'externaleditor' => 0, - 'externaldiff' => 0, - 'showjumplinks' => 1, - 'numberheadings' => 0, - 'uselivepreview' => 0, - 'watchlistdays' => 3.0, +$wgDefaultUserOptions = array( + 'quickbar' => 1, + 'underline' => 2, + 'cols' => 80, + 'rows' => 25, + 'searchlimit' => 20, + 'contextlines' => 5, + 'contextchars' => 50, + 'disablesuggest' => 0, + 'ajaxsearch' => 0, + 'skin' => false, + 'math' => 1, + 'usenewrc' => 0, + 'rcdays' => 7, + 'rclimit' => 50, + 'wllimit' => 250, + 'hideminor' => 0, + 'highlightbroken' => 1, + 'stubthreshold' => 0, + 'previewontop' => 1, + 'previewonfirst' => 0, + 'editsection' => 1, + 'editsectiononrightclick' => 0, + 'editondblclick' => 0, + 'editwidth' => 0, + 'showtoc' => 1, + 'showtoolbar' => 1, + 'minordefault' => 0, + 'date' => 'default', + 'imagesize' => 2, + 'thumbsize' => 2, + 'rememberpassword' => 0, + 'enotifwatchlistpages' => 0, + 'enotifusertalkpages' => 1, + 'enotifminoredits' => 0, + 'enotifrevealaddr' => 0, + 'shownumberswatching' => 1, + 'fancysig' => 0, + 'externaleditor' => 0, + 'externaldiff' => 0, + 'showjumplinks' => 1, + 'numberheadings' => 0, + 'uselivepreview' => 0, + 'watchlistdays' => 3.0, + 'extendwatchlist' => 0, + 'watchlisthideminor' => 0, + 'watchlisthidebots' => 0, + 'watchlisthideown' => 0, + 'watchcreations' => 0, + 'watchdefault' => 0, + 'watchmoves' => 0, + 'watchdeletion' => 0, ); /** Whether or not to allow and use real name fields. Defaults to true. */ $wgAllowRealName = true; /***************************************************************************** - * Extensions + * Extensions */ /** @@ -2053,7 +2210,7 @@ $wgAllowRealName = true; $wgExtensionFunctions = array(); /** - * Extension functions for initialisation of skins. This is called somewhat earlier + * Extension functions for initialisation of skins. This is called somewhat earlier * than $wgExtensionFunctions. */ $wgSkinExtensionFunctions = array(); @@ -2064,20 +2221,32 @@ $wgSkinExtensionFunctions = array(); * The file must create a variable called $messages. * When the messages are needed, the extension should call wfLoadExtensionMessages(). * - * Example: + * Example: * $wgExtensionMessagesFiles['ConfirmEdit'] = dirname(__FILE__).'/ConfirmEdit.i18n.php'; * */ $wgExtensionMessagesFiles = array(); /** + * Aliases for special pages provided by extensions. + * Associative array mapping special page to array of aliases. First alternative + * for each special page will be used as the normalised name for it. English + * aliases will be added to the end of the list so that they always work. The + * file must define a variable $aliases. + * + * Example: + * $wgExtensionAliasesFiles['Translate'] = dirname(__FILE__).'/Translate.alias.php'; + */ +$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. * These will be called as an OutputPageParserOutput hook, if the relevant * tag has been registered with the parser output object. * - * Registration is done with $pout->addOutputHook( $tag, $data ). + * Registration is done with $pout->addOutputHook( $tag, $data ). * * The callback has the form: * function outputHook( $outputPage, $parserOutput, $data ) { ... } @@ -2087,7 +2256,7 @@ $wgParserOutputHooks = array(); /** * List of valid skin names. * The key should be the name in all lower case, the value should be a display name. - * The default skins will be added later, by Skin::getSkinNames(). Use + * The default skins will be added later, by Skin::getSkinNames(). Use * Skin::getSkinNames() as an accessor if you wish to have access to the full list. */ $wgValidSkinNames = array(); @@ -2096,7 +2265,7 @@ $wgValidSkinNames = array(); * Special page list. * See the top of SpecialPage.php for documentation. */ -$wgSpecialPages = array(); +$wgSpecialPages = array(); /** * Array mapping class names to filenames, for autoloading. @@ -2111,7 +2280,8 @@ $wgAutoloadClasses = array(); * <code> * $wgExtensionCredits[$type][] = array( * 'name' => 'Example extension', - * 'version' => 1.9, + * 'version' => 1.9, + * 'svn-revision' => '$LastChangedRevision: 39340 $', * 'author' => 'Foo Barstein', * 'url' => 'http://wwww.example.com/Example%20Extension/', * 'description' => 'An example extension', @@ -2161,9 +2331,12 @@ $wgExternalDiffEngine = false; /** Use RC Patrolling to check for vandalism */ $wgUseRCPatrol = true; -/** Use new page patrolling to check new pages on special:Newpages */ +/** Use new page patrolling to check new pages on Special:Newpages */ $wgUseNPPatrol = true; +/** Provide syndication feeds (RSS, Atom) for, e.g., Recentchanges, Newpages */ +$wgFeed = true; + /** Set maximum number of results to return in syndication feeds (RSS, Atom) for * eg Recentchanges, Newpages. */ $wgFeedLimit = 50; @@ -2203,14 +2376,14 @@ $wgExtraNamespaces = NULL; /** * Namespace aliases - * These are alternate names for the primary localised namespace names, which - * are defined by $wgExtraNamespaces and the language file. If a page is - * requested with such a prefix, the request will be redirected to the primary - * name. + * These are alternate names for the primary localised namespace names, which + * are defined by $wgExtraNamespaces and the language file. If a page is + * requested with such a prefix, the request will be redirected to the primary + * name. * * Set this to a map from namespace names to IDs. * Example: - * $wgNamespaceAliases = array( + * $wgNamespaceAliases = array( * 'Wikipedian' => NS_USER, * 'Help' => 100, * ); @@ -2274,16 +2447,16 @@ $wgBrowserBlackList = array( * Netscape 2-4 detection * The minor version may contain strings such as "Gold" or "SGoldC-SGI" * Lots of non-netscape user agents have "compatible", so it's useful to check for that - * with a negative assertion. The [UIN] identifier specifies the level of security - * in a Netscape/Mozilla browser, checking for it rules out a number of fakers. + * with a negative assertion. The [UIN] identifier specifies the level of security + * in a Netscape/Mozilla browser, checking for it rules out a number of fakers. * The language string is unreliable, it is missing on NS4 Mac. - * + * * Reference: http://www.psychedelix.com/agents/index.shtml */ '/^Mozilla\/2\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/', '/^Mozilla\/3\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/', '/^Mozilla\/4\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/', - + /** * MSIE on Mac OS 9 is teh sux0r, converts þ to <thorn>, ð to <eth>, Þ to <THORN> and Ð to <ETH> * @@ -2297,7 +2470,7 @@ $wgBrowserBlackList = array( * @link http://en.wikipedia.org/wiki/Template%3AOS9 */ '/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/', - + /** * Google wireless transcoder, seems to eat a lot of chars alive * http://it.wikipedia.org/w/index.php?title=Luciano_Ligabue&diff=prev&oldid=8857361 @@ -2391,6 +2564,17 @@ $wgLogTypes = array( '', 'import', 'patrol', 'merge', + 'suppress', +); + +/** + * This restricts log access to those who have a certain right + * Users without this will not see it in the option menu and can not view it + * Restricted logs are not added to recent changes + * Logs should remain non-transcludable + */ +$wgLogRestrictions = array( + 'suppress' => 'suppressionlog' ); /** @@ -2410,6 +2594,7 @@ $wgLogNames = array( 'import' => 'importlogpage', 'patrol' => 'patrol-log-page', 'merge' => 'mergelog', + 'suppress' => 'suppressionlog', ); /** @@ -2429,6 +2614,7 @@ $wgLogHeaders = array( 'import' => 'importlogpagetext', 'patrol' => 'patrol-log-header', 'merge' => 'mergelogpagetext', + 'suppress' => 'suppressionlogtext', ); /** @@ -2447,6 +2633,7 @@ $wgLogActions = array( 'delete/delete' => 'deletedarticle', 'delete/restore' => 'undeletedarticle', 'delete/revision' => 'revdelete-logentry', + 'delete/event' => 'logdelete-logentry', 'upload/upload' => 'uploadedimage', 'upload/overwrite' => 'overwroteimage', 'upload/revert' => 'uploadedimage', @@ -2455,6 +2642,113 @@ $wgLogActions = array( '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', +); + +/** + * The same as above, but here values are names of functions, + * not messages + */ +$wgLogActionsHandlers = array(); + +/** + * List of special pages, followed by what subtitle they should go under + * at Special:SpecialPages + */ +$wgSpecialPageGroups = array( + 'DoubleRedirects' => 'maintenance', + 'BrokenRedirects' => 'maintenance', + 'Lonelypages' => 'maintenance', + 'Uncategorizedpages' => 'maintenance', + 'Uncategorizedcategories' => 'maintenance', + 'Uncategorizedimages' => 'maintenance', + 'Uncategorizedtemplates' => 'maintenance', + 'Unusedcategories' => 'maintenance', + 'Unusedimages' => 'maintenance', + 'Protectedpages' => 'maintenance', + 'Protectedtitles' => 'maintenance', + 'Unusedtemplates' => 'maintenance', + 'Withoutinterwiki' => 'maintenance', + 'Longpages' => 'maintenance', + 'Shortpages' => 'maintenance', + 'Ancientpages' => 'maintenance', + 'Deadendpages' => 'maintenance', + 'Wantedpages' => 'maintenance', + 'Wantedcategories' => 'maintenance', + 'Unwatchedpages' => 'maintenance', + 'Fewestrevisions' => 'maintenance', + + 'Userlogin' => 'login', + 'Userlogout' => 'login', + 'CreateAccount' => 'login', + + 'Recentchanges' => 'changes', + 'Recentchangeslinked' => 'changes', + 'Watchlist' => 'changes', + 'Newimages' => 'changes', + 'Newpages' => 'changes', + 'Log' => 'changes', + + 'Upload' => 'media', + 'Imagelist' => 'media', + 'MIMEsearch' => 'media', + 'FileDuplicateSearch' => 'media', + 'Filepath' => 'media', + + 'Listusers' => 'users', + 'Listgrouprights' => 'users', + 'Ipblocklist' => 'users', + 'Contributions' => 'users', + 'Emailuser' => 'users', + 'Listadmins' => 'users', + 'Listbots' => 'users', + 'Userrights' => 'users', + 'Blockip' => 'users', + 'Preferences' => 'users', + 'Resetpass' => 'users', + + 'Mostlinked' => 'highuse', + 'Mostlinkedcategories' => 'highuse', + 'Mostlinkedtemplates' => 'highuse', + 'Mostcategories' => 'highuse', + 'Mostimages' => 'highuse', + 'Mostrevisions' => 'highuse', + + 'Allpages' => 'pages', + 'Prefixindex' => 'pages', + 'Listredirects' => 'pages', + 'Categories' => 'pages', + 'Disambiguations' => 'pages', + + 'Randompage' => 'redirects', + 'Randomredirect' => 'redirects', + 'Mypage' => 'redirects', + 'Mytalk' => 'redirects', + 'Mycontributions' => 'redirects', + 'Search' => 'redirects', + + 'Movepage' => 'pagetools', + 'MergeHistory' => 'pagetools', + 'Revisiondelete' => 'pagetools', + 'Undelete' => 'pagetools', + 'Export' => 'pagetools', + 'Import' => 'pagetools', + 'Whatlinkshere' => 'pagetools', + + 'Statistics' => 'wiki', + 'Version' => 'wiki', + 'Lockdb' => 'wiki', + 'Unlockdb' => 'wiki', + 'Allmessages' => 'wiki', + 'Popularpages' => 'wiki', + + 'Specialpages' => 'other', + 'Blockme' => 'other', + 'Booksources' => 'other', ); /** @@ -2517,7 +2811,7 @@ $wgNamespaceRobotPolicies = array(); /** * Robot policies per article. * These override the per-namespace robot policies. - * Must be in the form of an array where the key part is a properly + * Must be in the form of an array where the key part is a properly * canonicalised text form title and the value is a robot policy. * Example: * $wgArticleRobotPolicies = array( 'Main Page' => 'noindex' ); @@ -2611,8 +2905,14 @@ $wgRateLimitLog = null; /** * Array of groups which should never trigger the rate limiter + * + * @deprecated as of 1.13.0, the preferred method is using + * $wgGroupPermissions[]['noratelimit']. However, this will still + * work if desired. + * + * $wgRateLimitsExcludedGroups = array( 'sysop', 'bureaucrat' ); */ -$wgRateLimitsExcludedGroups = array( 'sysop', 'bureaucrat' ); +$wgRateLimitsExcludedGroups = array(); /** * On Special:Unusedimages, consider images "used", if they are put @@ -2634,6 +2934,7 @@ $wgExternalStores = false; /** * An array of external mysql servers, e.g. * $wgExternalServers = array( 'cluster1' => array( 'srv28', 'srv29', 'srv30' ) ); + * Used by LBFactory_Simple, may be ignored if $wgLBFactoryConf is set to another class. */ $wgExternalServers = array(); @@ -2641,7 +2942,7 @@ $wgExternalServers = array(); * The place to put new revisions, false to put them in the local text table. * Part of a URL, e.g. DB://cluster1 * - * Can be an array instead of a single string, to enable data distribution. Keys + * Can be an array instead of a single string, to enable data distribution. Keys * must be consecutive integers, starting at zero. Example: * * $wgDefaultExternalStore = array( 'DB://cluster1', 'DB://cluster2' ); @@ -2658,15 +2959,15 @@ $wgDefaultExternalStore = false; $wgRevisionCacheExpiry = 0; /** -* list of trusted media-types and mime types. -* Use the MEDIATYPE_xxx constants to represent media types. -* This list is used by Image::isSafeFile -* -* Types not listed here will have a warning about unsafe content -* displayed on the images description page. It would also be possible -* to use this for further restrictions, like disabling direct -* [[media:...]] links for non-trusted formats. -*/ + * list of trusted media-types and mime types. + * Use the MEDIATYPE_xxx constants to represent media types. + * This list is used by Image::isSafeFile + * + * Types not listed here will have a warning about unsafe content + * displayed on the images description page. It would also be possible + * to use this for further restrictions, like disabling direct + * [[media:...]] links for non-trusted formats. + */ $wgTrustedMediaFormats= array( MEDIATYPE_BITMAP, //all bitmap formats MEDIATYPE_AUDIO, //all audio formats @@ -2735,14 +3036,14 @@ $wgUpdateRowsPerQuery = 10; $wgUseAjax = true; /** - * Enable auto suggestion for the search bar + * Enable auto suggestion for the search bar * Requires $wgUseAjax to be true too. * Causes wfSajaxSearch to be added to $wgAjaxExportList */ $wgAjaxSearch = false; /** - * List of Ajax-callable functions. + * List of Ajax-callable functions. * Extensions acting as Ajax callbacks must register here */ $wgAjaxExportList = array( ); @@ -2778,6 +3079,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 + 'msg:double-redirect-fixer', // Automatic double redirect fix ); /** @@ -2798,7 +3100,7 @@ $wgAllowTitlesInSVG = false; $wgContentNamespaces = array( NS_MAIN ); /** - * Maximum amount of virtual memory available to shell processes under linux, in KB. + * Maximum amount of virtual memory available to shell processes under linux, in KB. */ $wgMaxShellMemory = 102400; @@ -2825,7 +3127,7 @@ $wgDjvuRenderer = null; /** * Path of the djvutoxml executable - * This works like djvudump except much, much slower as of version 3.5. + * This works like djvudump except much, much slower as of version 3.5. * * For now I recommend you use djvudump instead. The djvuxml output is * probably more stable, so we'll switch back to it as soon as they fix @@ -2868,6 +3170,15 @@ $wgEnableWriteAPI = false; * Extension modules may override the core modules. */ $wgAPIModules = array(); +$wgAPIMetaModules = array(); +$wgAPIPropModules = array(); +$wgAPIListModules = array(); + +/** + * Maximum amount of rows to scan in a DB query in the API + * The default value is generally fine + */ +$wgAPIMaxDBRows = 5000; /** * Parser test suite files to be run by parserTests.php when no specific @@ -2884,28 +3195,23 @@ $wgParserTestFiles = array( /** * Break out of framesets. This can be used to prevent external sites from - * framing your site with ads. + * framing your site with ads. */ $wgBreakFrames = false; /** - * Set this to an array of special page names to prevent + * Set this to an array of special page names to prevent * maintenance/updateSpecialPages.php from updating those pages. */ $wgDisableQueryPageUpdate = false; /** - * Set this to false to disable cascading protection - */ -$wgEnableCascadingProtection = true; - -/** * Disable output compression (enabled by default if zlib is available) */ $wgDisableOutputCompression = false; /** - * If lag is higher than $wgSlaveLagWarning, show a warning in some special + * If lag is higher than $wgSlaveLagWarning, show a warning in some special * pages (like watchlist). If the lag is higher than $wgSlaveLagCritical, * show a more obvious warning. */ @@ -2915,25 +3221,76 @@ $wgSlaveLagCritical = 30; /** * Parser configuration. Associative array with the following members: * - * class The class name - * - * The entire associative array will be passed through to the constructor as - * the first parameter. Note that only Setup.php can use this variable -- - * the configuration will change at runtime via $wgParser member functions, so - * the contents of this variable will be out-of-date. The variable can only be - * changed during LocalSettings.php, in particular, it can't be changed during - * an extension setup function. + * class The class name + * + * preprocessorClass The preprocessor class. Two classes are currently available: + * Preprocessor_Hash, which uses plain PHP arrays for tempoarary + * storage, and Preprocessor_DOM, which uses the DOM module for + * temporary storage. Preprocessor_DOM generally uses less memory; + * the speed of the two is roughly the same. + * + * If this parameter is not given, it uses Preprocessor_DOM if the + * DOM module is available, otherwise it uses Preprocessor_Hash. + * + * Has no effect on Parser_OldPP. + * + * The entire associative array will be passed through to the constructor as + * the first parameter. Note that only Setup.php can use this variable -- + * the configuration will change at runtime via $wgParser member functions, so + * the contents of this variable will be out-of-date. The variable can only be + * changed during LocalSettings.php, in particular, it can't be changed during + * an extension setup function. */ -$wgParserConf = array( +$wgParserConf = array( 'class' => 'Parser', + #'preprocessorClass' => 'Preprocessor_Hash', ); /** - * Hooks that are used for outputting exceptions - * Format is: - * $wgExceptionHooks[] = $funcname + * Hooks that are used for outputting exceptions. Format is: + * $wgExceptionHooks[] = $funcname * or: - * $wgExceptionHooks[] = array( $class, $funcname ) + * $wgExceptionHooks[] = array( $class, $funcname ) * Hooks should return strings or false */ $wgExceptionHooks = array(); + +/** + * Page property link table invalidation lists. Should only be set by exten- + * sions. + */ +$wgPagePropLinkInvalidations = array( + 'hiddencat' => 'categorylinks', +); + +/** + * Maximum number of links to a redirect page listed on + * Special:Whatlinkshere/RedirectDestination + */ +$wgMaxRedirectLinksRetrieved = 500; + +/** + * Maximum number of calls per parse to expensive parser functions such as + * PAGESINCATEGORY. + */ +$wgExpensiveParserFunctionLimit = 100; + +/** + * Maximum number of pages to move at once when moving subpages with a page. + */ +$wgMaximumMovedPages = 100; + +/** + * Array of namespaces to generate a sitemap for when the + * maintenance/generateSitemap.php script is run, or false if one is to be ge- + * nerated for all namespaces. + */ +$wgSitemapNamespaces = false; + + +/** + * If user doesn't specify any edit summary when making a an edit, MediaWiki + * will try to automatically create one. This feature can be disabled by set- + * ting this variable false. + */ +$wgUseAutomaticEditSummaries = true; diff --git a/includes/Defines.php b/includes/Defines.php index 2d6aee5f..98cee57d 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -1,6 +1,7 @@ <?php /** * A few constants that might be needed during LocalSettings.php + * @file */ /** @@ -84,30 +85,6 @@ define( 'MW_MATH_MODERN', 4 ); define( 'MW_MATH_MATHML', 5 ); /**#@-*/ -/** - * User rights list - * @deprecated - */ -$wgAvailableRights = array( - 'block', - 'bot', - 'createaccount', - 'delete', - 'edit', - 'editinterface', - 'import', - 'importupload', - 'move', - 'patrol', - 'protect', - 'read', - 'rollback', - 'siteadmin', - 'unwatchedpages', - 'upload', - 'userrights', -); - /**#@+ * Cache type */ @@ -188,15 +165,15 @@ define( 'RC_MOVE_OVER_REDIRECT', 4); */ define( 'EDIT_NEW', 1 ); define( 'EDIT_UPDATE', 2 ); -define( 'EDIT_MINOR', 4 ); +define( 'EDIT_MINOR', 4 ); define( 'EDIT_SUPPRESS_RC', 8 ); define( 'EDIT_FORCE_BOT', 16 ); define( 'EDIT_DEFER_UPDATES', 32 ); define( 'EDIT_AUTOSUMMARY', 64 ); /**#@-*/ -/** - * Flags for Database::makeList() +/** + * Flags for Database::makeList() * These are also available as Database class constants */ define( 'LIST_COMMA', 0 ); @@ -208,57 +185,7 @@ define( 'LIST_OR', 4); /** * Unicode and normalisation related */ -define( 'UNICODE_HANGUL_FIRST', 0xac00 ); -define( 'UNICODE_HANGUL_LAST', 0xd7a3 ); - -define( 'UNICODE_HANGUL_LBASE', 0x1100 ); -define( 'UNICODE_HANGUL_VBASE', 0x1161 ); -define( 'UNICODE_HANGUL_TBASE', 0x11a7 ); - -define( 'UNICODE_HANGUL_LCOUNT', 19 ); -define( 'UNICODE_HANGUL_VCOUNT', 21 ); -define( 'UNICODE_HANGUL_TCOUNT', 28 ); -define( 'UNICODE_HANGUL_NCOUNT', UNICODE_HANGUL_VCOUNT * UNICODE_HANGUL_TCOUNT ); - -define( 'UNICODE_HANGUL_LEND', UNICODE_HANGUL_LBASE + UNICODE_HANGUL_LCOUNT - 1 ); -define( 'UNICODE_HANGUL_VEND', UNICODE_HANGUL_VBASE + UNICODE_HANGUL_VCOUNT - 1 ); -define( 'UNICODE_HANGUL_TEND', UNICODE_HANGUL_TBASE + UNICODE_HANGUL_TCOUNT - 1 ); - -define( 'UNICODE_SURROGATE_FIRST', 0xd800 ); -define( 'UNICODE_SURROGATE_LAST', 0xdfff ); -define( 'UNICODE_MAX', 0x10ffff ); -define( 'UNICODE_REPLACEMENT', 0xfffd ); - - -define( 'UTF8_HANGUL_FIRST', "\xea\xb0\x80" /*codepointToUtf8( UNICODE_HANGUL_FIRST )*/ ); -define( 'UTF8_HANGUL_LAST', "\xed\x9e\xa3" /*codepointToUtf8( UNICODE_HANGUL_LAST )*/ ); - -define( 'UTF8_HANGUL_LBASE', "\xe1\x84\x80" /*codepointToUtf8( UNICODE_HANGUL_LBASE )*/ ); -define( 'UTF8_HANGUL_VBASE', "\xe1\x85\xa1" /*codepointToUtf8( UNICODE_HANGUL_VBASE )*/ ); -define( 'UTF8_HANGUL_TBASE', "\xe1\x86\xa7" /*codepointToUtf8( UNICODE_HANGUL_TBASE )*/ ); - -define( 'UTF8_HANGUL_LEND', "\xe1\x84\x92" /*codepointToUtf8( UNICODE_HANGUL_LEND )*/ ); -define( 'UTF8_HANGUL_VEND', "\xe1\x85\xb5" /*codepointToUtf8( UNICODE_HANGUL_VEND )*/ ); -define( 'UTF8_HANGUL_TEND', "\xe1\x87\x82" /*codepointToUtf8( UNICODE_HANGUL_TEND )*/ ); - -define( 'UTF8_SURROGATE_FIRST', "\xed\xa0\x80" /*codepointToUtf8( UNICODE_SURROGATE_FIRST )*/ ); -define( 'UTF8_SURROGATE_LAST', "\xed\xbf\xbf" /*codepointToUtf8( UNICODE_SURROGATE_LAST )*/ ); -define( 'UTF8_MAX', "\xf4\x8f\xbf\xbf" /*codepointToUtf8( UNICODE_MAX )*/ ); -define( 'UTF8_REPLACEMENT', "\xef\xbf\xbd" /*codepointToUtf8( UNICODE_REPLACEMENT )*/ ); -#define( 'UTF8_REPLACEMENT', '!' ); - -define( 'UTF8_OVERLONG_A', "\xc1\xbf" ); -define( 'UTF8_OVERLONG_B', "\xe0\x9f\xbf" ); -define( 'UTF8_OVERLONG_C', "\xf0\x8f\xbf\xbf" ); - -# These two ranges are illegal -define( 'UTF8_FDD0', "\xef\xb7\x90" /*codepointToUtf8( 0xfdd0 )*/ ); -define( 'UTF8_FDEF', "\xef\xb7\xaf" /*codepointToUtf8( 0xfdef )*/ ); -define( 'UTF8_FFFE', "\xef\xbf\xbe" /*codepointToUtf8( 0xfffe )*/ ); -define( 'UTF8_FFFF', "\xef\xbf\xbf" /*codepointToUtf8( 0xffff )*/ ); - -define( 'UTF8_HEAD', false ); -define( 'UTF8_TAIL', true ); +require_once dirname(__FILE__).'/normal/UtfNormalDefines.php'; # Hook support constants define( 'MW_SUPPORTS_EDITFILTERMERGED', 1 ); diff --git a/includes/DifferenceEngine.php b/includes/DifferenceEngine.php index 9aa17bbb..0b4028cb 100644 --- a/includes/DifferenceEngine.php +++ b/includes/DifferenceEngine.php @@ -1,8 +1,6 @@ <?php /** - * See diff.doc - * @todo indicate where diff.doc can be found. - * @addtogroup DifferenceEngine + * @defgroup DifferenceEngine DifferenceEngine */ /** @@ -15,8 +13,7 @@ define( 'MW_DIFF_VERSION', '1.11a' ); /** * @todo document - * @public - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ class DifferenceEngine { /**#@+ @@ -40,7 +37,7 @@ class DifferenceEngine { * @param $rcid Integer: ??? FIXME (default 0) * @param $refreshCache boolean If set, refreshes the diff cache */ - function DifferenceEngine( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false ) { + function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false ) { $this->mTitle = $titleObj; wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n"); @@ -72,10 +69,13 @@ class DifferenceEngine { $this->mRefreshCache = $refreshCache; } + function getTitle() { + return $this->mTitle; + } + function showDiffPage( $diffOnly = false ) { global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol; - $fname = 'DifferenceEngine::showDiffPage'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); # If external diffs are enabled both globally and for the user, # we'll use the application/x-external-editor interface to call @@ -108,13 +108,14 @@ CONTROL; $wgOut->setArticleFlag( false ); if ( ! $this->loadRevisionData() ) { - $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, {$this->mNewid})"; + $t = $this->mTitle->getPrefixedText(); + $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid ); $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) ); - $wgOut->addWikiMsg( 'missingarticle', "<nowiki>$t</nowiki>" ); - wfProfileOut( $fname ); + $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d ); + wfProfileOut( __METHOD__ ); return; } - + wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) ); if ( $this->mNewRev->isCurrent() ) { @@ -127,7 +128,7 @@ CONTROL; if ( $this->mOldid === false ) { $this->showFirstRevision(); $this->renderNewRevision(); // should we respect $diffOnly here or not? - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return; } @@ -146,18 +147,20 @@ CONTROL; if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) { $wgOut->loginToUse(); $wgOut->output(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); exit; } $sk = $wgUser->getSkin(); - if ( $this->mNewRev->isCurrent() && $wgUser->isAllowed('rollback') ) { + // Check if page is editable + $editable = $this->mNewRev->getTitle()->userCan( 'edit' ); + if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) { $rollback = ' ' . $sk->generateRollback( $this->mNewRev ); } else { $rollback = ''; } - + // Prepare a change patrol link, if applicable if( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) { // If we've been given an explicit change identifier, use it; saves time @@ -186,11 +189,11 @@ CONTROL; } // Build the link if( $rcid ) { - $patrol = ' [' . $sk->makeKnownLinkObj( + $patrol = ' <span class="patrollink">[' . $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$rcid}" - ) . ']'; + ) . ']</span>'; } else { $patrol = ''; } @@ -211,21 +214,19 @@ CONTROL; $newminor = ''; if ($this->mOldRev->mMinorEdit == 1) { - $oldminor = wfElement( 'span', array( 'class' => 'minor' ), - wfMsg( 'minoreditletter') ) . ' '; + $oldminor = Xml::span( wfMsg( 'minoreditletter'), 'minor' ) . ' '; } if ($this->mNewRev->mMinorEdit == 1) { - $newminor = wfElement( 'span', array( 'class' => 'minor' ), - wfMsg( 'minoreditletter') ) . ' '; + $newminor = Xml::span( wfMsg( 'minoreditletter'), 'minor' ) . ' '; } - + $rdel = ''; $ldel = ''; if( $wgUser->isAllowed( 'deleterevision' ) ) { $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) { // If revision was hidden from sysops - $ldel = wfMsgHtml('rev-delundel'); + $ldel = wfMsgHtml('rev-delundel'); } else { $ldel = $sk->makeKnownLinkObj( $revdel, wfMsgHtml('rev-delundel'), @@ -239,10 +240,10 @@ CONTROL; // We don't currently handle well changing the top revision's settings if( $this->mNewRev->isCurrent() ) { // If revision was hidden from sysops - $rdel = wfMsgHtml('rev-delundel'); + $rdel = wfMsgHtml('rev-delundel'); } else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) { // If revision was hidden from sysops - $rdel = wfMsgHtml('rev-delundel'); + $rdel = wfMsgHtml('rev-delundel'); } else { $rdel = $sk->makeKnownLinkObj( $revdel, wfMsgHtml('rev-delundel'), @@ -269,7 +270,7 @@ CONTROL; if ( !$diffOnly ) $this->renderNewRevision(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } /** @@ -277,8 +278,7 @@ CONTROL; */ function renderNewRevision() { global $wgOut; - $fname = 'DifferenceEngine::renderNewRevision'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" ); #add deleted rev tag if needed @@ -316,7 +316,7 @@ CONTROL; $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } /** @@ -325,18 +325,16 @@ CONTROL; */ function showFirstRevision() { global $wgOut, $wgUser; - - $fname = 'DifferenceEngine::showFirstRevision'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); # Get article text from the DB # if ( ! $this->loadNewText() ) { - $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " . - "{$this->mNewid})"; + $t = $this->mTitle->getPrefixedText(); + $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid ); $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) ); - $wgOut->addWikiMsg( 'missingarticle', "<nowiki>$t</nowiki>" ); - wfProfileOut( $fname ); + $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d ); + wfProfileOut( __METHOD__ ); return; } if ( $this->mNewRev->isCurrent() ) { @@ -348,7 +346,7 @@ CONTROL; if ( !( $this->mTitle->userCanRead() ) ) { $wgOut->loginToUse(); $wgOut->output(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); exit; } @@ -367,7 +365,7 @@ CONTROL; $wgOut->setSubtitle( wfMsg( 'difference' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } /** @@ -378,7 +376,7 @@ CONTROL; global $wgOut; $diff = $this->getDiff( $otitle, $ntitle ); if ( $diff === false ) { - $wgOut->addWikiMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" ); + $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' ); return false; } else { $this->showDiffStyle(); @@ -386,14 +384,14 @@ CONTROL; return true; } } - + /** * Add style sheets and supporting JS for diff display. */ function showDiffStyle() { global $wgStylePath, $wgStyleVersion, $wgOut; $wgOut->addStyle( 'common/diff.css' ); - + // JS is needed to detect old versions of Mozilla to work around an annoyance bug. $wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" ); } @@ -422,9 +420,13 @@ CONTROL; */ function getDiffBody() { global $wgMemc; - $fname = 'DifferenceEngine::getDiffBody'; - wfProfileIn( $fname ); - + wfProfileIn( __METHOD__ ); + // Check if the diff should be hidden from this user + if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) { + return ''; + } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { + return ''; + } // Cacheable? $key = false; if ( $this->mOldid && $this->mNewid ) { @@ -436,7 +438,7 @@ CONTROL; wfIncrStats( 'diff_cache_hit' ); $difftext = $this->localiseLineNumbers( $difftext ); $difftext .= "\n<!-- diff cache key $key -->\n"; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $difftext; } } // don't try to load but save the result @@ -444,23 +446,14 @@ CONTROL; // Loadtext is permission safe, this just clears out the diff if ( !$this->loadText() ) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return false; - } else if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) { - return ''; - } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { - return ''; } $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); - + // Save to cache for 7 days - // Only do this for public revs, otherwise an admin can view the diff and a non-admin can nab it! - if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) { - wfIncrStats( 'diff_uncacheable' ); - } else if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { - wfIncrStats( 'diff_uncacheable' ); - } else if ( $key !== false && $difftext !== false ) { + if ( $key !== false && $difftext !== false ) { wfIncrStats( 'diff_cache_miss' ); $wgMemc->set( $key, $difftext, 7*86400 ); } else { @@ -470,7 +463,7 @@ CONTROL; if ( $difftext !== false ) { $difftext = $this->localiseLineNumbers( $difftext ); } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $difftext; } @@ -480,11 +473,10 @@ CONTROL; */ function generateDiffBody( $otext, $ntext ) { global $wgExternalDiffEngine, $wgContLang; - $fname = 'DifferenceEngine::generateDiffBody'; $otext = str_replace( "\r\n", "\n", $otext ); $ntext = str_replace( "\r\n", "\n", $ntext ); - + if ( $wgExternalDiffEngine == 'wikidiff' ) { # For historical reasons, external diff engine expects # input text to be HTML-escaped already @@ -493,20 +485,22 @@ CONTROL; if( !function_exists( 'wikidiff_do_diff' ) ) { dl('php_wikidiff.so'); } - return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ); + return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) . + $this->debug( 'wikidiff1' ); } - + if ( $wgExternalDiffEngine == 'wikidiff2' ) { # Better external diff engine, the 2 may some day be dropped # This one does the escaping and segmenting itself if ( !function_exists( 'wikidiff2_do_diff' ) ) { - wfProfileIn( "$fname-dl" ); + wfProfileIn( __METHOD__ . "-dl" ); @dl('php_wikidiff2.so'); - wfProfileOut( "$fname-dl" ); + wfProfileOut( __METHOD__ . "-dl" ); } if ( function_exists( 'wikidiff2_do_diff' ) ) { wfProfileIn( 'wikidiff2_do_diff' ); $text = wikidiff2_do_diff( $otext, $ntext, 2 ); + $text .= $this->debug( 'wikidiff2' ); wfProfileOut( 'wikidiff2_do_diff' ); return $text; } @@ -519,12 +513,12 @@ CONTROL; $tempFile1 = fopen( $tempName1, "w" ); if ( !$tempFile1 ) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return false; } $tempFile2 = fopen( $tempName2, "w" ); if ( !$tempFile2 ) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return false; } fwrite( $tempFile1, $otext ); @@ -532,22 +526,42 @@ CONTROL; fclose( $tempFile1 ); fclose( $tempFile2 ); $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 ); - wfProfileIn( "$fname-shellexec" ); + wfProfileIn( __METHOD__ . "-shellexec" ); $difftext = wfShellExec( $cmd ); - wfProfileOut( "$fname-shellexec" ); + $difftext .= $this->debug( "external $wgExternalDiffEngine" ); + wfProfileOut( __METHOD__ . "-shellexec" ); unlink( $tempName1 ); unlink( $tempName2 ); return $difftext; } - + # Native PHP diff $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); $diffs = new Diff( $ota, $nta ); $formatter = new TableDiffFormatter(); - return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ); + return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) . + $this->debug(); + } + + /** + * Generate a debug comment indicating diff generating time, + * server node, and generator backend. + */ + protected function debug( $generator="internal" ) { + global $wgShowHostnames, $wgNodeName; + $data = array( $generator ); + if( $wgShowHostnames ) { + $data[] = $wgNodeName; + } + $data[] = wfTimestamp( TS_DB ); + return "<!-- diff generator: " . + implode( " ", + array_map( + "htmlspecialchars", + $data ) ) . + " -->\n"; } - /** * Replace line numbers with the text in the user's language @@ -562,19 +576,19 @@ CONTROL; return wfMsgExt( 'lineno', array('parseinline'), $wgLang->formatNum( $matches[1] ) ); } - + /** * If there are revisions between the ones being compared, return a note saying so. */ function getMultiNotice() { if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) ) return ''; - + if( !$this->mOldPage->equals( $this->mNewPage ) ) { // Comparing two different pages? Count would be meaningless. return ''; } - + $oldid = $this->mOldRev->getId(); $newid = $this->mNewRev->getId(); if ( $oldid > $newid ) { @@ -601,7 +615,7 @@ CONTROL; <col class='diff-content' /> <col class='diff-marker' /> <col class='diff-content' /> - <tr> + <tr valign='top'> <td colspan='2' class='diff-otitle'>{$otitle}</td> <td colspan='2' class='diff-ntitle'>{$ntitle}</td> </tr> @@ -647,28 +661,31 @@ CONTROL; : Revision::newFromTitle( $this->mTitle ); if( !$this->mNewRev instanceof Revision ) return false; - + // Update the new revision ID in case it was 0 (makes life easier doing UI stuff) $this->mNewid = $this->mNewRev->getId(); - + + // Check if page is editable + $editable = $this->mNewRev->getTitle()->userCan( 'edit' ); + // Set assorted variables $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true ); $this->mNewPage = $this->mNewRev->getTitle(); if( $this->mNewRev->isCurrent() ) { - $newLink = $this->mNewPage->escapeLocalUrl(); + $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid ); $this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) ); $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' ); - $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)" - . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)"; + $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)"; } else { $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid ); $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid ); $this->mPagetitle = wfMsgHTML( 'revisionasof', $timestamp ); - $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>" - . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>"; + $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)"; } if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>"; @@ -703,18 +720,19 @@ CONTROL; $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid ); $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid ); $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) ); - + $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>" - . " (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)"; // Add an "undo" link $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid); - if ( $this->mNewRev->userCan(Revision::DELETED_TEXT) ) + if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)"; - - if ( !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) { - $this->mOldtitle = "<span class='history-deleted'>{$this->mOldPagetitle}</span>"; - } else if ( $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) { - $this->mOldtitle = '<span class="history-deleted">'.$this->mOldtitle.'</span>'; + } + + if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) { + $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>'; + } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) { + $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>'; } } @@ -780,7 +798,7 @@ define('USE_ASSERTS', function_exists('assert')); /** * @todo document * @private - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ class _DiffOp { var $type; @@ -803,7 +821,7 @@ class _DiffOp { /** * @todo document * @private - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ class _DiffOp_Copy extends _DiffOp { var $type = 'copy'; @@ -823,7 +841,7 @@ class _DiffOp_Copy extends _DiffOp { /** * @todo document * @private - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ class _DiffOp_Delete extends _DiffOp { var $type = 'delete'; @@ -841,7 +859,7 @@ class _DiffOp_Delete extends _DiffOp { /** * @todo document * @private - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ class _DiffOp_Add extends _DiffOp { var $type = 'add'; @@ -859,7 +877,7 @@ class _DiffOp_Add extends _DiffOp { /** * @todo document * @private - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ class _DiffOp_Change extends _DiffOp { var $type = 'change'; @@ -896,15 +914,13 @@ class _DiffOp_Change extends _DiffOp { * * @author Geoffrey T. Dairiki, Tim Starling * @private - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ -class _DiffEngine -{ +class _DiffEngine { const MAX_XREF_LENGTH = 10000; function diff ($from_lines, $to_lines) { - $fname = '_DiffEngine::diff'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $n_from = sizeof($from_lines); $n_to = sizeof($to_lines); @@ -991,7 +1007,7 @@ class _DiffEngine elseif ($add) $edits[] = new _DiffOp_Add($add); } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $edits; } @@ -1024,8 +1040,7 @@ class _DiffEngine * of the portions it is going to specify. */ function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) { - $fname = '_DiffEngine::_diag'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $flip = false; if ($xlim - $xoff > $ylim - $yoff) { @@ -1051,7 +1066,7 @@ class _DiffEngine $numer = $xlim - $xoff + $nchunks - 1; $x = $xoff; for ($chunk = 0; $chunk < $nchunks; $chunk++) { - wfProfileIn( "$fname-chunk" ); + wfProfileIn( __METHOD__ . "-chunk" ); if ($chunk > 0) for ($i = 0; $i <= $this->lcs; $i++) $ymids[$i][$chunk-1] = $this->seq[$i]; @@ -1085,7 +1100,7 @@ class _DiffEngine } } } - wfProfileOut( "$fname-chunk" ); + wfProfileOut( __METHOD__ . "-chunk" ); } $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff); @@ -1097,19 +1112,18 @@ class _DiffEngine } $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return array($this->lcs, $seps); } function _lcs_pos ($ypos) { - $fname = '_DiffEngine::_lcs_pos'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $end = $this->lcs; if ($end == 0 || $ypos > $this->seq[$end]) { $this->seq[++$this->lcs] = $ypos; $this->in_seq[$ypos] = 1; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $this->lcs; } @@ -1127,7 +1141,7 @@ class _DiffEngine $this->in_seq[$this->seq[$end]] = false; $this->seq[$end] = $ypos; $this->in_seq[$ypos] = 1; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $end; } @@ -1143,8 +1157,7 @@ class _DiffEngine * All line numbers are origin-0 and discarded lines are not counted. */ function _compareseq ($xoff, $xlim, $yoff, $ylim) { - $fname = '_DiffEngine::_compareseq'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); // Slide down the bottom initial diagonal. while ($xoff < $xlim && $yoff < $ylim @@ -1187,7 +1200,7 @@ class _DiffEngine $pt1 = $pt2; } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } /* Adjust inserts/deletes of identical lines to join changes @@ -1203,8 +1216,7 @@ class _DiffEngine * This is extracted verbatim from analyze.c (GNU diffutils-2.7). */ function _shift_boundaries ($lines, &$changed, $other_changed) { - $fname = '_DiffEngine::_shift_boundaries'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $i = 0; $j = 0; @@ -1309,7 +1321,7 @@ class _DiffEngine USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]'); } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } } @@ -1317,7 +1329,7 @@ class _DiffEngine * Class representing a 'diff' between two sequences of strings. * @todo document * @private - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ class Diff { @@ -1427,8 +1439,7 @@ class Diff * This is here only for debugging purposes. */ function _check ($from_lines, $to_lines) { - $fname = 'Diff::_check'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); if (serialize($from_lines) != serialize($this->orig())) trigger_error("Reconstructed original doesn't match", E_USER_ERROR); if (serialize($to_lines) != serialize($this->closing())) @@ -1450,14 +1461,14 @@ class Diff $lcs = $this->lcs(); trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } } /** * @todo document, bad name. * @private - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ class MappedDiff extends Diff { @@ -1485,9 +1496,8 @@ class MappedDiff extends Diff * have the same number of elements as $to_lines. */ function MappedDiff($from_lines, $to_lines, - $mapped_from_lines, $mapped_to_lines) { - $fname = 'MappedDiff::MappedDiff'; - wfProfileIn( $fname ); + $mapped_from_lines, $mapped_to_lines) { + wfProfileIn( __METHOD__ ); assert(sizeof($from_lines) == sizeof($mapped_from_lines)); assert(sizeof($to_lines) == sizeof($mapped_to_lines)); @@ -1508,7 +1518,7 @@ class MappedDiff extends Diff $yi += sizeof($closing); } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } } @@ -1520,10 +1530,9 @@ class MappedDiff extends Diff * to obtain fancier outputs. * @todo document * @private - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ -class DiffFormatter -{ +class DiffFormatter { /** * Number of leading context "lines" to preserve. * @@ -1547,8 +1556,7 @@ class DiffFormatter * @return string The formatted output. */ function format($diff) { - $fname = 'DiffFormatter::format'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $xi = $yi = 1; $block = false; @@ -1602,13 +1610,12 @@ class DiffFormatter $block); $end = $this->_end_diff(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $end; } function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) { - $fname = 'DiffFormatter::_block'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen)); foreach ($edits as $edit) { if ($edit->type == 'copy') @@ -1623,7 +1630,7 @@ class DiffFormatter trigger_error('Unknown edit type', E_USER_ERROR); } $this->_end_block(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } function _start_diff() { @@ -1677,14 +1684,13 @@ class DiffFormatter /** * A formatter that outputs unified diffs - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ -class UnifiedDiffFormatter extends DiffFormatter -{ +class UnifiedDiffFormatter extends DiffFormatter { var $leading_context_lines = 2; var $trailing_context_lines = 2; - + function _added($lines) { $this->_lines($lines, '+'); } @@ -1702,21 +1708,17 @@ class UnifiedDiffFormatter extends DiffFormatter /** * A pseudo-formatter that just passes along the Diff::$edits array - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ -class ArrayDiffFormatter extends DiffFormatter -{ - function format($diff) - { +class ArrayDiffFormatter extends DiffFormatter { + function format($diff) { $oldline = 1; $newline = 1; $retval = array(); foreach($diff->edits as $edit) - switch($edit->type) - { + switch($edit->type) { case 'add': - foreach($edit->closing as $l) - { + foreach($edit->closing as $l) { $retval[] = array( 'action' => 'add', 'new'=> $l, @@ -1725,8 +1727,7 @@ class ArrayDiffFormatter extends DiffFormatter } break; case 'delete': - foreach($edit->orig as $l) - { + foreach($edit->orig as $l) { $retval[] = array( 'action' => 'delete', 'old' => $l, @@ -1735,8 +1736,7 @@ class ArrayDiffFormatter extends DiffFormatter } break; case 'change': - foreach($edit->orig as $i => $l) - { + foreach($edit->orig as $i => $l) { $retval[] = array( 'action' => 'change', 'old' => $l, @@ -1751,7 +1751,7 @@ class ArrayDiffFormatter extends DiffFormatter $newline += count($edit->orig); } return $retval; - } + } } /** @@ -1759,12 +1759,12 @@ class ArrayDiffFormatter extends DiffFormatter * */ -define('NBSP', ' '); // iso-8859-x non-breaking space. +define('NBSP', ' '); // iso-8859-x non-breaking space. /** * @todo document * @private - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ class _HWLDF_WordAccumulator { function _HWLDF_WordAccumulator () { @@ -1777,10 +1777,10 @@ class _HWLDF_WordAccumulator { function _flushGroup ($new_tag) { if ($this->_group !== '') { if ($this->_tag == 'ins') - $this->_line .= '<ins class="diffchange">' . + $this->_line .= '<ins class="diffchange diffchange-inline">' . htmlspecialchars ( $this->_group ) . '</ins>'; elseif ($this->_tag == 'del') - $this->_line .= '<del class="diffchange">' . + $this->_line .= '<del class="diffchange diffchange-inline">' . htmlspecialchars ( $this->_group ) . '</del>'; else $this->_line .= htmlspecialchars ( $this->_group ); @@ -1825,27 +1825,24 @@ class _HWLDF_WordAccumulator { /** * @todo document * @private - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ -class WordLevelDiff extends MappedDiff -{ +class WordLevelDiff extends MappedDiff { const MAX_LINE_LENGTH = 10000; function WordLevelDiff ($orig_lines, $closing_lines) { - $fname = 'WordLevelDiff::WordLevelDiff'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); list ($orig_words, $orig_stripped) = $this->_split($orig_lines); list ($closing_words, $closing_stripped) = $this->_split($closing_lines); $this->MappedDiff($orig_words, $closing_words, - $orig_stripped, $closing_stripped); - wfProfileOut( $fname ); + $orig_stripped, $closing_stripped); + wfProfileOut( __METHOD__ ); } function _split($lines) { - $fname = 'WordLevelDiff::_split'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $words = array(); $stripped = array(); @@ -1872,13 +1869,12 @@ class WordLevelDiff extends MappedDiff } } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return array($words, $stripped); } function orig () { - $fname = 'WordLevelDiff::orig'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $orig = new _HWLDF_WordAccumulator; foreach ($this->edits as $edit) { @@ -1888,13 +1884,12 @@ class WordLevelDiff extends MappedDiff $orig->addWords($edit->orig, 'del'); } $lines = $orig->getLines(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $lines; } function closing () { - $fname = 'WordLevelDiff::closing'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $closing = new _HWLDF_WordAccumulator; foreach ($this->edits as $edit) { @@ -1904,24 +1899,30 @@ class WordLevelDiff extends MappedDiff $closing->addWords($edit->closing, 'ins'); } $lines = $closing->getLines(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $lines; } } /** - * Wikipedia Table style diff formatter. + * Wikipedia Table style diff formatter. * @todo document * @private - * @addtogroup DifferenceEngine + * @ingroup DifferenceEngine */ -class TableDiffFormatter extends DiffFormatter -{ +class TableDiffFormatter extends DiffFormatter { function TableDiffFormatter() { $this->leading_context_lines = 2; $this->trailing_context_lines = 2; } + public static function escapeWhiteSpace( $msg ) { + $msg = preg_replace( '/^ /m', ' ', $msg ); + $msg = preg_replace( '/ $/m', ' ', $msg ); + $msg = preg_replace( '/ /', ' ', $msg ); + return $msg; + } + function _block_header( $xbeg, $xlen, $ybeg, $ylen ) { $r = '<tr><td colspan="2" class="diff-lineno"><!--LINE '.$xbeg."--></td>\n" . '<td colspan="2" class="diff-lineno"><!--LINE '.$ybeg."--></td></tr>\n"; @@ -1952,11 +1953,11 @@ class TableDiffFormatter extends DiffFormatter function contextLine( $line ) { return $this->wrapLine( ' ', 'diff-context', $line ); } - + private function wrapLine( $marker, $class, $line ) { if( $line !== '' ) { // The <div> wrapper is needed for 'overflow: auto' style to scroll properly - $line = "<div>$line</div>"; + $line = Xml::tags( 'div', null, $this->escapeWhiteSpace( $line ) ); } return "<td class='diff-marker'>$marker</td><td class='$class'>$line</td>"; } @@ -1990,8 +1991,7 @@ class TableDiffFormatter extends DiffFormatter } function _changed( $orig, $closing ) { - $fname = 'TableDiffFormatter::_changed'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $diff = new WordLevelDiff( $orig, $closing ); $del = $diff->orig(); @@ -2009,9 +2009,6 @@ class TableDiffFormatter extends DiffFormatter echo '<tr>' . $this->emptyLine() . $this->addedLine( $line ) . "</tr>\n"; } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } } - - - diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php index b48aaffd..8e7caf63 100644 --- a/includes/DjVuImage.php +++ b/includes/DjVuImage.php @@ -29,13 +29,13 @@ * File format docs are available in source package for DjVuLibre: * http://djvulibre.djvuzone.org/ * - * @addtogroup Media + * @ingroup Media */ class DjVuImage { function __construct( $filename ) { $this->mFilename = $filename; } - + /** * Check if the given file is indeed a valid DjVu image file * @return bool @@ -44,27 +44,27 @@ class DjVuImage { $info = $this->getInfo(); return $info !== false; } - - + + /** * Return data in the style of getimagesize() * @return array or false on failure */ public function getImageSize() { $data = $this->getInfo(); - + if( $data !== false ) { $width = $data['width']; $height = $data['height']; - + return array( $width, $height, 'DjVu', "width=\"$width\" height=\"$height\"" ); } return false; } - + // --------- - + /** * For debugging; dump the IFF chunk structure */ @@ -77,7 +77,7 @@ class DjVuImage { $this->dumpForm( $file, $chunkLength, 1 ); fclose( $file ); } - + private function dumpForm( $file, $length, $indent ) { $start = ftell( $file ); $secondary = fread( $file, 4 ); @@ -90,7 +90,7 @@ class DjVuImage { // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4chunk/NchunkLength', $chunkHeader ) ); echo str_repeat( ' ', $indent * 4 ) . "$chunk $chunkLength\n"; - + if( $chunk == 'FORM' ) { $this->dumpForm( $file, $chunkLength, $indent + 1 ); } else { @@ -102,7 +102,7 @@ class DjVuImage { } } } - + function getInfo() { wfSuppressWarnings(); $file = fopen( $this->mFilename, 'rb' ); @@ -111,16 +111,16 @@ class DjVuImage { wfDebug( __METHOD__ . ": missing or failed file read\n" ); return false; } - + $header = fread( $file, 16 ); $info = false; - + if( strlen( $header ) < 16 ) { wfDebug( __METHOD__ . ": too short file header\n" ); } else { // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4magic/a4form/NformLength/a4subtype', $header ) ); - + if( $magic != 'AT&T' ) { wfDebug( __METHOD__ . ": not a DjVu file\n" ); } elseif( $subtype == 'DJVU' ) { @@ -136,7 +136,7 @@ class DjVuImage { fclose( $file ); return $info; } - + private function readChunk( $file ) { $header = fread( $file, 8 ); if( strlen( $header ) < 8 ) { @@ -147,16 +147,16 @@ class DjVuImage { return array( $chunk, $length ); } } - + private function skipChunk( $file, $chunkLength ) { fseek( $file, $chunkLength, SEEK_CUR ); - + if( $chunkLength & 0x01 == 1 && !feof( $file ) ) { // padding byte fseek( $file, 1, SEEK_CUR ); } } - + private function getMultiPageInfo( $file, $formLength ) { // For now, we'll just look for the first page in the file // and report its information, hoping others are the same size. @@ -166,7 +166,7 @@ class DjVuImage { if( !$chunk ) { break; } - + if( $chunk == 'FORM' ) { $subtype = fread( $file, 4 ); if( $subtype == 'DJVU' ) { @@ -179,18 +179,18 @@ class DjVuImage { $this->skipChunk( $file, $length ); } } while( $length != 0 && !feof( $file ) && ftell( $file ) - $start < $formLength ); - + wfDebug( __METHOD__ . ": multi-page DJVU file contained no pages\n" ); return false; } - + private function getPageInfo( $file, $formLength ) { list( $chunk, $length ) = $this->readChunk( $file ); if( $chunk != 'INFO' ) { wfDebug( __METHOD__ . ": expected INFO chunk, got '$chunk'\n" ); return false; } - + if( $length < 9 ) { wfDebug( __METHOD__ . ": INFO should be 9 or 10 bytes, found $length\n" ); return false; @@ -200,7 +200,7 @@ class DjVuImage { wfDebug( __METHOD__ . ": INFO chunk cut off\n" ); return false; } - + // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'nwidth/' . @@ -210,7 +210,7 @@ class DjVuImage { 'vresolution/' . 'Cgamma', $data ) ); # Newer files have rotation info in byte 10, but we don't use it yet. - + return array( 'width' => $width, 'height' => $height, @@ -320,14 +320,14 @@ EOT; } if ( preg_match( '/^ *INFO *\[\d*\] *DjVu *(\d+)x(\d+), *\w*, *(\d+) *dpi, *gamma=([0-9.-]+)/', $line, $m ) ) { - $xml .= Xml::tags( 'OBJECT', + $xml .= Xml::tags( 'OBJECT', array( #'data' => '', #'type' => 'image/x.djvu', 'height' => $m[2], 'width' => $m[1], #'usemap' => '', - ), + ), "\n" . Xml::element( 'PARAM', array( 'name' => 'DPI', 'value' => $m[3] ) ) . "\n" . Xml::element( 'PARAM', array( 'name' => 'GAMMA', 'value' => $m[4] ) ) . "\n" @@ -340,6 +340,3 @@ EOT; return false; } } - - -?> diff --git a/includes/DoubleRedirectJob.php b/includes/DoubleRedirectJob.php new file mode 100644 index 00000000..889beecf --- /dev/null +++ b/includes/DoubleRedirectJob.php @@ -0,0 +1,166 @@ +<?php + +class DoubleRedirectJob extends Job { + var $reason, $redirTitle, $destTitleText; + static $user; + + /** + * Insert jobs into the job queue to fix redirects to the given title + * @param string $type The reason for the fix, see message double-redirect-fixed-<reason> + * @param Title $redirTitle The title which has changed, redirects pointing to this title are fixed + */ + public static function fixRedirects( $reason, $redirTitle, $destTitle = false ) { + # Need to use the master to get the redirect table updated in the same transaction + $dbw = wfGetDB( DB_MASTER ); + $res = $dbw->select( + array( 'redirect', 'page' ), + array( 'page_namespace', 'page_title' ), + array( + 'page_id = rd_from', + 'rd_namespace' => $redirTitle->getNamespace(), + 'rd_title' => $redirTitle->getDBkey() + ), __METHOD__ ); + if ( !$res->numRows() ) { + return; + } + $jobs = array(); + foreach ( $res as $row ) { + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + if ( !$title ) { + continue; + } + + $jobs[] = new self( $title, array( + 'reason' => $reason, + 'redirTitle' => $redirTitle->getPrefixedDBkey() ) ); + # Avoid excessive memory usage + if ( count( $jobs ) > 10000 ) { + Job::batchInsert( $jobs ); + $jobs = array(); + } + } + Job::batchInsert( $jobs ); + } + function __construct( $title, $params = false, $id = 0 ) { + parent::__construct( 'fixDoubleRedirect', $title, $params, $id ); + $this->reason = $params['reason']; + $this->redirTitle = Title::newFromText( $params['redirTitle'] ); + $this->destTitleText = !empty( $params['destTitle'] ) ? $params['destTitle'] : ''; + } + + function run() { + if ( !$this->redirTitle ) { + $this->setLastError( 'Invalid title' ); + return false; + } + + $targetRev = Revision::newFromTitle( $this->title ); + if ( !$targetRev ) { + wfDebug( __METHOD__.": target redirect already deleted, ignoring\n" ); + return true; + } + $text = $targetRev->getText(); + $currentDest = Title::newFromRedirect( $text ); + if ( !$currentDest || !$currentDest->equals( $this->redirTitle ) ) { + wfDebug( __METHOD__.": Redirect has changed since the job was queued\n" ); + return true; + } + + # Check for a suppression tag (used e.g. in periodically archived discussions) + $mw = MagicWord::get( 'staticredirect' ); + if ( $mw->match( $text ) ) { + wfDebug( __METHOD__.": skipping: suppressed with __STATICREDIRECT__\n" ); + return true; + } + + # Find the current final destination + $newTitle = self::getFinalDestination( $this->redirTitle ); + if ( !$newTitle ) { + wfDebug( __METHOD__.": skipping: single redirect, circular redirect or invalid redirect destination\n" ); + return true; + } + if ( $newTitle->equals( $this->redirTitle ) ) { + # The redirect is already right, no need to change it + # This can happen if the page was moved back (say after vandalism) + wfDebug( __METHOD__.": skipping, already good\n" ); + } + + # Preserve fragment (bug 14904) + $newTitle = Title::makeTitle( $newTitle->getNamespace(), $newTitle->getDBkey(), + $currentDest->getFragment() ); + + # Fix the text + # Remember that redirect pages can have categories, templates, etc., + # so the regex has to be fairly general + $newText = preg_replace( '/ \[ \[ [^\]]* \] \] /x', + '[[' . $newTitle->getFullText() . ']]', + $text, 1 ); + + if ( $newText === $text ) { + $this->setLastError( 'Text unchanged???' ); + return false; + } + + # Save it + global $wgUser; + $oldUser = $wgUser; + $wgUser = $this->getUser(); + $article = new Article( $this->title ); + $reason = wfMsgForContent( 'double-redirect-fixed-' . $this->reason, + $this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText() ); + $article->doEdit( $newText, $reason, EDIT_UPDATE | EDIT_SUPPRESS_RC ); + $wgUser = $oldUser; + + return true; + } + + /** + * Get the final destination of a redirect + * Returns false if the specified title is not a redirect, or if it is a circular redirect + */ + public static function getFinalDestination( $title ) { + $dbw = wfGetDB( DB_MASTER ); + + $seenTitles = array(); # Circular redirect check + $dest = false; + + while ( true ) { + $titleText = $title->getPrefixedDBkey(); + if ( isset( $seenTitles[$titleText] ) ) { + wfDebug( __METHOD__, "Circular redirect detected, aborting\n" ); + return false; + } + $seenTitles[$titleText] = true; + + $row = $dbw->selectRow( + array( 'redirect', 'page' ), + array( 'rd_namespace', 'rd_title' ), + array( + 'rd_from=page_id', + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDBkey() + ), __METHOD__ ); + if ( !$row ) { + # No redirect from here, chain terminates + break; + } else { + $dest = $title = Title::makeTitle( $row->rd_namespace, $row->rd_title ); + } + } + return $dest; + } + + /** + * Get a user object for doing edits, from a request-lifetime cache + */ + function getUser() { + if ( !self::$user ) { + self::$user = User::newFromName( wfMsgForContent( 'double-redirect-fixer' ), false ); + if ( !self::$user->isLoggedIn() ) { + self::$user->addToDatabase(); + } + } + return self::$user; + } +} + diff --git a/includes/EditPage.php b/includes/EditPage.php index 8c3a37d4..a34964bc 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -1,6 +1,7 @@ <?php /** * Contains the EditPage class + * @file */ /** @@ -61,6 +62,7 @@ class EditPage { var $autoSumm = ''; var $hookError = ''; var $mPreviewTemplates; + var $mBaseRevision = false; # Form values var $save = false, $preview = false, $diff = false; @@ -77,10 +79,10 @@ class EditPage { public $editFormTextAfterWarn; public $editFormTextAfterTools; public $editFormTextBottom; - + /* $didSave should be set to true whenever an article was succesfully altered. */ public $didSave = false; - + public $suppressIntro = false; /** @@ -99,13 +101,13 @@ class EditPage { $this->editFormTextAfterTools = $this->editFormTextBottom = ""; } - + /** * Fetch initial editing page content. * @private */ function getContent( $def_text = '' ) { - global $wgOut, $wgRequest, $wgParser; + global $wgOut, $wgRequest, $wgParser, $wgMessageCache; # Get variables from query string :P $section = $wgRequest->getVal( 'section' ); @@ -118,7 +120,8 @@ class EditPage { $text = ''; if( !$this->mTitle->exists() ) { if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - # If this is a system message, get the default text. + $wgMessageCache->loadAllMessages(); + # If this is a system message, get the default text. $text = wfMsgWeirdKey ( $this->mTitle->getText() ) ; } else { # If requested, preload some text. @@ -152,39 +155,43 @@ class EditPage { $oldrev = $undorev ? $undorev->getPrevious() : null; } - #Sanity check, make sure it's the right page. + # 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() ) { + 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 ) ) { $undorev_text = $undorev->getText(); $oldrev_text = $oldrev->getText(); $currev_text = $text; - #No use doing a merge if it's just a straight revert. if ( $currev_text != $undorev_text ) { - $result = wfMerge($undorev_text, $oldrev_text, $currev_text, $text); + $result = wfMerge( $undorev_text, $oldrev_text, $currev_text, $text ); } else { + # No use doing a merge if it's just a straight revert. $text = $oldrev_text; $result = true; } + if( $result ) { + # Inform the user of our success and set an automatic edit summary + $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-success' ) ); + $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->formtype = 'diff'; + } else { + # Warn the user that something went wrong + $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-failure' ) ); + } } else { // Failed basic sanity checks. // Older revisions may have been removed since the link // was created, or we may simply have got bogus input. - $result = false; - } - - if( $result ) { - # Inform the user of our success and set an automatic edit summary - $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-success' ) ); - $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->formtype = 'diff'; - } else { - # Warn the user that something went wrong - $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-failure' ) ); + $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-norev' ) ); } } else if( $section != '' ) { if( $section == 'new' ) { @@ -205,7 +212,7 @@ class EditPage { * @param $preload String: the title of the page. * @return string The contents of the page. */ - private function getPreloadedText($preload) { + protected function getPreloadedText($preload) { if ( $preload === '' ) return ''; else { @@ -319,6 +326,25 @@ class EditPage { $this->mMetaData = $s ; } + protected function wasDeletedSinceLastEdit() { + /* 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. + */ + if ( $this->deletedSinceEdit ) + return true; + if ( $this->mTitle->isDeleted() ) { + $this->lastDelete = $this->getLastDelete(); + if ( !is_null($this->lastDelete) ) { + $deletetime = $this->lastDelete->log_timestamp; + if ( ($deletetime - $this->starttime) > 0 ) { + $this->deletedSinceEdit = true; + } + } + } + return $this->deletedSinceEdit; + } + function submit() { $this->edit(); } @@ -355,29 +381,25 @@ class EditPage { return; } + $wgOut->addScriptFile( 'edit.js' ); + if( wfReadOnly() ) { - $wgOut->readOnlyPage( $this->getContent() ); + $this->readOnlyPage( $this->getContent() ); wfProfileOut( __METHOD__ ); return; } $permErrors = $this->mTitle->getUserPermissionsErrors('edit', $wgUser); + if( !$this->mTitle->exists() ) { - # We can't use array_diff here, because that considers ANY TWO - # ARRAYS TO BE EQUAL. Thanks, PHP. - $createErrors = $this->mTitle->getUserPermissionsErrors('create', $wgUser); - foreach( $createErrors as $error ) { - # in_array() actually *does* work as expected. - if( !in_array( $error, $permErrors ) ) { - $permErrors[] = $error; - } - } + $permErrors = array_merge( $permErrors, + wfArrayDiff2( $this->mTitle->getUserPermissionsErrors('create', $wgUser), $permErrors ) ); } # Ignore some permissions errors. $remove = array(); foreach( $permErrors as $error ) { - if ($this->preview || $this->diff && + if ( ( $this->preview || $this->diff ) && ($error[0] == 'blockedtext' || $error[0] == 'autoblockedtext')) { // Don't worry about blocks when previewing/diffing @@ -393,11 +415,11 @@ class EditPage { } } } - $permErrors = array_diff( $permErrors, $remove ); - - if ( !empty($permErrors) ) { + $permErrors = wfArrayDiff2( $permErrors, $remove ); + + if ( $permErrors ) { wfDebug( __METHOD__.": User can't edit\n" ); - $wgOut->readOnlyPage( $this->getContent(), true, $permErrors ); + $this->readOnlyPage( $this->getContent(), true, $permErrors, 'edit' ); wfProfileOut( __METHOD__ ); return; } else { @@ -425,31 +447,10 @@ class EditPage { $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage(); $this->isValidCssJsSubpage = $this->mTitle->isValidCssJsSubpage(); - /* Notice that we can't use isDeleted, because it returns true if article is ever deleted - * no matter it's current state - */ - $this->deletedSinceEdit = false; - if ( $this->edittime != '' ) { - /* Note that we rely on logging table, which hasn't been always there, - * but that doesn't matter, because this only applies to brand new - * deletes. This is done on every preview and save request. Move it further down - * to only perform it on saves - */ - if ( $this->mTitle->isDeleted() ) { - $this->lastDelete = $this->getLastDelete(); - if ( !is_null($this->lastDelete) ) { - $deletetime = $this->lastDelete->log_timestamp; - if ( ($deletetime - $this->starttime) > 0 ) { - $this->deletedSinceEdit = true; - } - } - } - } - # Show applicable editing introductions if( $this->formtype == 'initial' || $this->firsttime ) $this->showIntro(); - + if( $this->mTitle->isTalkPage() ) { $wgOut->addWikiMsg( 'talkpagetext' ); } @@ -476,7 +477,7 @@ class EditPage { wfProfileOut( __METHOD__ ); return; } - if( !$this->mTitle->getArticleId() ) + if( !$this->mTitle->getArticleId() ) wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) ); } @@ -486,11 +487,28 @@ class EditPage { } /** + * Show a read-only error + * Parameters are the same as OutputPage:readOnlyPage() + * Redirect to the article page if redlink=1 + */ + function readOnlyPage( $source = null, $protected = false, $reasons = array(), $action = null ) { + 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() ); + } else { + $wgOut->readOnlyPage( $source, $protected, $reasons, $action ); + } + } + + /** * Should we show a preview when the edit form is first shown? * * @return bool */ - private function previewOnOpen() { + protected function previewOnOpen() { global $wgRequest, $wgUser; if( $wgRequest->getVal( 'preview' ) == 'yes' ) { // Explicit override from request @@ -521,15 +539,21 @@ class EditPage { $fname = 'EditPage::importFormData'; wfProfileIn( $fname ); + # Section edit can come from either the form or a link + $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) ); + if( $request->wasPosted() ) { # These fields need to be checked for encoding. # Also remove trailing whitespace, but don't remove _initial_ # whitespace from the text boxes. This may be significant formatting. $this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' ); $this->textbox2 = $this->safeUnicodeInput( $request, 'wpTextbox2' ); - $this->mMetaData = rtrim( $request->getText( 'metadata' ) ); + $this->mMetaData = rtrim( $request->getText( 'metadata' ) ); # Truncate for whole multibyte characters. +5 bytes for ellipsis - $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 ); + $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); $this->edittime = $request->getVal( 'wpEdittime' ); $this->starttime = $request->getVal( 'wpStarttime' ); @@ -566,7 +590,7 @@ class EditPage { $this->preview = true; } } - $this->save = ! ( $this->preview OR $this->diff ); + $this->save = !$this->preview && !$this->diff; if( !preg_match( '/^\d{14}$/', $this->edittime )) { $this->edittime = null; } @@ -587,7 +611,7 @@ class EditPage { $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ); } - $this->autoSumm = $request->getText( 'wpAutoSummary' ); + $this->autoSumm = $request->getText( 'wpAutoSummary' ); } else { # Not a posted form? Start with nothing. wfDebug( "$fname: Not a posted form.\n" ); @@ -604,13 +628,14 @@ class EditPage { $this->minoredit = false; $this->watchthis = false; $this->recreate = false; + + if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) { + $this->summary = $request->getVal( 'preloadtitle' ); + } } $this->oldid = $request->getInt( 'oldid' ); - # Section edit can come from either the form or a link - $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) ); - $this->live = $request->getCheck( 'live' ); $this->editintro = $request->getText( 'editintro' ); @@ -635,7 +660,7 @@ class EditPage { /** * Show all applicable editing introductions */ - private function showIntro() { + protected function showIntro() { global $wgOut, $wgUser; if( $this->suppressIntro ) return; @@ -648,7 +673,7 @@ class EditPage { $ip = User::isIP( $username ); if ( $id == 0 && !$ip ) { - $wgOut->wrapWikiMsg( '<div class="mw-userpage-userdoesnotexist error">$1</div>', + $wgOut->wrapWikiMsg( '<div class="mw-userpage-userdoesnotexist error">$1</div>', array( 'userpage-userdoesnotexist', $username ) ); } } @@ -668,13 +693,13 @@ class EditPage { * * @return bool */ - private function showCustomIntro() { + protected function showCustomIntro() { if( $this->editintro ) { $title = Title::newFromText( $this->editintro ); if( $title instanceof Title && $title->exists() && $title->userCanRead() ) { global $wgOut; $revision = Revision::newFromTitle( $title ); - $wgOut->addSecondaryWikiText( $revision->getText() ); + $wgOut->addWikiTextTitleTidy( $revision->getText(), $this->mTitle ); return true; } else { return false; @@ -721,17 +746,21 @@ class EditPage { $matches = array(); if ( $wgSpamRegex && preg_match( $wgSpamRegex, $this->textbox1, $matches ) ) { $result['spam'] = $matches[0]; + $ip = wfGetIP(); + $pdbk = $this->mTitle->getPrefixedDBkey(); + $match = str_replace( "\n", '', $matches[0] ); + wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" ); wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); return self::AS_SPAM_ERROR; } - if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section ) ) { + if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section, $this->hookError, $this->summary ) ) { # Error messages or other handling should be performed by the filter function wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); return self::AS_FILTERING; } - if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError ) ) ) { + if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ) ) ) { # Error messages etc. could be handled within the hook... wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); @@ -783,7 +812,7 @@ class EditPage { # If the article has been deleted while editing, don't save it without # confirmation - if ( $this->deletedSinceEdit && !$this->recreate ) { + if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) { wfProfileOut( "$fname-checks" ); wfProfileOut( $fname ); return self::AS_ARTICLE_WAS_DELETED; @@ -809,14 +838,14 @@ class EditPage { } // Run post-section-merge edit filter - if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError ) ) ) { + if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) { # Error messages etc. could be handled within the hook... wfProfileOut( $fname ); return self::AS_HOOK_ERROR; } $isComment = ( $this->section == 'new' ); - + $this->mArticle->insertNewArticle( $this->textbox1, $this->summary, $this->minoredit, $this->watchthis, false, $isComment, $bot); @@ -847,7 +876,7 @@ class EditPage { } } } - $userid = $wgUser->getID(); + $userid = $wgUser->getId(); if ( $this->isConflict) { wfDebug( "EditPage::editForm conflict! getting section '$this->section' for time '$this->edittime' (article time '" . @@ -892,15 +921,18 @@ class EditPage { $oldtext = $this->mArticle->getContent(); // Run post-section-merge edit filter - if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError ) ) ) { + if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) { # Error messages etc. could be handled within the hook... wfProfileOut( $fname ); return self::AS_HOOK_ERROR; } # Handle the user preference to force summaries here, but not for null edits - if( $this->section != 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary') - && 0 != strcmp($oldtext, $text) && !Article::getRedirectAutosummary( $text )) { + if( $this->section != 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary') && + 0 != strcmp($oldtext, $text) && + !is_object( Title::newFromRedirect( $text ) ) # check if it's not a redirect + ) { + if( md5( $this->summary ) == $this->autoSumm ) { $this->missingSummary = true; wfProfileOut( $fname ); @@ -908,7 +940,7 @@ class EditPage { } } - #And a similar thing for new sections + # And a similar thing for new sections if( $this->section == 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary' ) ) { if (trim($this->summary) == '') { $this->missingSummary = true; @@ -978,7 +1010,6 @@ class EditPage { */ function initialiseForm() { $this->edittime = $this->mArticle->getTimestamp(); - $this->summary = ''; $this->textbox1 = $this->getContent(false); if ($this->textbox1 === false) return false; @@ -997,6 +1028,13 @@ class EditPage { function showEditForm( $formCallback=null ) { global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize, $wgTitle; + # If $wgTitle is null, that means we're in API mode. + # Some hook probably called this function without checking + # for is_null($wgTitle) first. Bail out right here so we don't + # do lots of work just to discard it right after. + if(is_null($wgTitle)) + return; + $fname = 'EditPage::showEditForm'; wfProfileIn( $fname ); @@ -1034,8 +1072,8 @@ class EditPage { $matches ); if( !empty( $matches[2] ) ) { global $wgParser; - $this->summary = "/* " . - $wgParser->stripSectionName(trim($matches[2])) . + $this->summary = "/* " . + $wgParser->stripSectionName(trim($matches[2])) . " */ "; } } @@ -1066,13 +1104,13 @@ class EditPage { } if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) { // Let sysop know that this will make private content public if saved - + if( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) { $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); } else if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { $wgOut->addWikiMsg( 'rev-deleted-text-view' ); } - + if( !$this->mArticle->mRevision->isCurrent() ) { $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() ); $wgOut->addWikiMsg( 'editingold' ); @@ -1162,17 +1200,17 @@ class EditPage { global $wgRightsText; if ( $wgRightsText ) { - $copywarnMsg = array( 'copyrightwarning', + $copywarnMsg = array( 'copyrightwarning', '[[' . wfMsgForContent( 'copyrightpage' ) . ']]', $wgRightsText ); } else { - $copywarnMsg = array( 'copyrightwarning2', + $copywarnMsg = array( 'copyrightwarning2', '[[' . wfMsgForContent( 'copyrightpage' ) . ']]' ); } if( $wgUser->getOption('showtoolbar') and !$this->isCssJsSubpage ) { # prepare toolbar for edit buttons - $toolbar = $this->getEditToolbar(); + $toolbar = EditPage::getEditToolbar(); } else { $toolbar = ''; } @@ -1215,14 +1253,28 @@ class EditPage { # if this is a comment, show a subject line at the top, which is also the edit summary. # Otherwise, show a summary field at the bottom $summarytext = htmlspecialchars( $wgContLang->recodeForEdit( $this->summary ) ); # FIXME + + # If a blank edit summary was previously provided, and the appropriate + # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the + # user being bounced back more than once in the event that a summary + # is not required. + ##### + # For a bit more sophisticated detection of blank summaries, hash the + # automatic one and pass that in the hidden field wpAutoSummary. + $summaryhiddens = ''; + if( $this->missingSummary ) $summaryhiddens .= Xml::hidden( 'wpIgnoreBlankSummary', true ); + $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); + $summaryhiddens .= Xml::hidden( 'wpAutoSummary', $autosumm ); if( $this->section == 'new' ) { - $commentsubject="<span id='wpSummaryLabel'><label for='wpSummary'>{$subject}:</label></span>\n<div class='editOptions'>\n<input tabindex='1' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' /><br />"; - $editsummary = ''; - $subjectpreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">".wfMsg('subject-preview').':'.$sk->commentBlock( $this->summary, $this->mTitle )."</div>\n" : ''; + $commentsubject="<span id='wpSummaryLabel'><label for='wpSummary'>{$subject}:</label></span>\n<input tabindex='1' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' />{$summaryhiddens}<br />"; + $editsummary = "<div class='editOptions'>\n"; + global $wgParser; + $formattedSummary = wfMsgForContent( 'newsectionsummary', $wgParser->stripSectionName( $this->summary ) ); + $subjectpreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">".wfMsg('subject-preview').':'.$sk->commentBlock( $formattedSummary, $this->mTitle, true )."</div>\n" : ''; $summarypreview = ''; } else { $commentsubject = ''; - $editsummary="<span id='wpSummaryLabel'><label for='wpSummary'>{$summary}:</label></span>\n<div class='editOptions'>\n<input tabindex='2' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' /><br />"; + $editsummary="<div class='editOptions'>\n<span id='wpSummaryLabel'><label for='wpSummary'>{$summary}:</label></span>\n<input tabindex='2' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' />{$summaryhiddens}<br />"; $summarypreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">".wfMsg('summary-preview').':'.$sk->commentBlock( $this->summary, $this->mTitle )."</div>\n" : ''; $subjectpreview = ''; } @@ -1234,6 +1286,9 @@ class EditPage { $templates = ($this->preview || $this->section != '') ? $this->mPreviewTemplates : $this->mArticle->getUsedTemplates(); $formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != ''); + $hiddencats = $this->mArticle->getHiddenCategories(); + $formattedhiddencats = $sk->formatHiddenCategories( $hiddencats ); + global $wgUseMetadataEdit ; if ( $wgUseMetadataEdit ) { $metadata = $this->mMetaData ; @@ -1245,7 +1300,7 @@ class EditPage { $hidden = ''; $recreate = ''; - if ($this->deletedSinceEdit) { + if ($this->wasDeletedSinceLastEdit()) { if ( 'save' != $this->formtype ) { $wgOut->addWikiMsg('deletedwhileediting'); } else { @@ -1292,18 +1347,24 @@ END <input type='hidden' value=\"{$this->edittime}\" name=\"wpEdittime\" />\n <input type='hidden' value=\"{$this->scrolltop}\" name=\"wpScrolltop\" id=\"wpScrolltop\" />\n" ); + $encodedtext = htmlspecialchars( $this->safeUnicodeOutput( $this->textbox1 ) ); + if( $encodedtext !== '' ) { + // 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 + // mode will show an extra newline. A bit annoying. + $encodedtext .= "\n"; + } + $wgOut->addHTML( <<<END $recreate {$commentsubject} {$subjectpreview} {$this->editFormTextBeforeContent} <textarea tabindex='1' accesskey="," name="wpTextbox1" id="wpTextbox1" rows='{$rows}' -cols='{$cols}'{$ew} $hidden> +cols='{$cols}'{$ew} $hidden>{$encodedtext}</textarea> END -. htmlspecialchars( $this->safeUnicodeOutput( $this->textbox1 ) ) . -" -</textarea> - " ); +); $wgOut->wrapWikiMsg( "<div id=\"editpage-copywarn\">\n$1\n</div>", $copywarnMsg ); $wgOut->addHTML( $this->editFormTextAfterWarn ); @@ -1322,18 +1383,6 @@ END </div><!-- editButtons --> </div><!-- editOptions -->"); - $wgOut->addHtml( '<div class="mw-editTools">' ); - $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) ); - $wgOut->addHtml( '</div>' ); - - $wgOut->addHTML( $this->editFormTextAfterTools ); - - $wgOut->addHTML( " -<div class='templatesUsed'> -{$formattedtemplates} -</div> -" ); - /** * To make it harder for someone to slip a user a page * which submits an edit form to the wiki without their @@ -1349,21 +1398,22 @@ END $token = htmlspecialchars( $wgUser->editToken() ); $wgOut->addHTML( "\n<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" ); + $wgOut->addHtml( '<div class="mw-editTools">' ); + $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) ); + $wgOut->addHtml( '</div>' ); - # If a blank edit summary was previously provided, and the appropriate - # user preference is active, pass a hidden tag here. This will stop the - # user being bounced back more than once in the event that a summary - # is not required. - if( $this->missingSummary ) { - $wgOut->addHTML( "<input type=\"hidden\" name=\"wpIgnoreBlankSummary\" value=\"1\" />\n" ); - } + $wgOut->addHTML( $this->editFormTextAfterTools ); - # For a bit more sophisticated detection of blank summaries, hash the - # automatic one and pass that in a hidden field. - $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); - $wgOut->addHtml( wfHidden( 'wpAutoSummary', $autosumm ) ); + $wgOut->addHTML( " +<div class='templatesUsed'> +{$formattedtemplates} +</div> +<div class='hiddencats'> +{$formattedhiddencats} +</div> +"); - if ( $this->isConflict ) { + if ( $this->isConflict && wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) { $wgOut->wrapWikiMsg( '==$1==', "yourdiff" ); $de = new DifferenceEngine( $this->mTitle ); @@ -1399,7 +1449,7 @@ END * * @param string $text The HTML to be output for the preview. */ - private function showPreview( $text ) { + protected function showPreview( $text ) { global $wgOut; $wgOut->addHTML( '<div id="wikiPreview">' ); @@ -1425,10 +1475,8 @@ END * of the preview button */ function doLivePreviewScript() { - global $wgStylePath, $wgJsMimeType, $wgStyleVersion, $wgOut, $wgTitle; - $wgOut->addHTML( '<script type="'.$wgJsMimeType.'" src="' . - htmlspecialchars( "$wgStylePath/common/preview.js?$wgStyleVersion" ) . - '"></script>' . "\n" ); + global $wgOut, $wgTitle; + $wgOut->addScriptFile( 'preview.js' ); $liveAction = $wgTitle->getLocalUrl( 'action=submit&wpPreview=true&live=true' ); return "return !lpDoPreview(" . "editform.wpTextbox1.value," . @@ -1471,7 +1519,7 @@ END * @todo document */ function getPreviewText() { - global $wgOut, $wgUser, $wgTitle, $wgParser; + global $wgOut, $wgUser, $wgTitle, $wgParser, $wgLang, $wgContLang; $fname = 'EditPage::getPreviewText'; wfProfileIn( $fname ); @@ -1519,21 +1567,40 @@ END $toparse="== {$this->summary} ==\n\n".$toparse; } - if ( $this->mMetaData != "" ) $toparse .= "\n" . $this->mMetaData ; + if ( $this->mMetaData != "" ) $toparse .= "\n" . $this->mMetaData; + + // Parse mediawiki messages with correct target language + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + $pos = strrpos( $this->mTitle->getText(), '/' ); + if ( $pos !== false ) { + $code = substr( $this->mTitle->getText(), $pos+1 ); + switch ($code) { + case $wgLang->getCode(): + $obj = $wgLang; break; + case $wgContLang->getCode(): + $obj = $wgContLang; break; + default: + $obj = Language::factory( $code ); + } + $parserOptions->setTargetLanguage( $obj ); + } + } + + $parserOptions->setTidy(true); $parserOptions->enableLimitReport(); - $parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ) ."\n\n", + $parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ), $this->mTitle, $parserOptions ); $previewHTML = $parserOutput->getText(); $wgOut->addParserOutputNoText( $parserOutput ); - + # ParserOutput might have altered the page title, so reset it # Also, use the title defined by DISPLAYTITLE magic word when present if( ( $dt = $parserOutput->getDisplayTitle() ) !== false ) { $wgOut->setPageTitle( wfMsg( 'editing', $dt ) ); } else { - $wgOut->setPageTitle( wfMsg( 'editing', $wgTitle->getPrefixedText() ) ); + $wgOut->setPageTitle( wfMsg( 'editing', $wgTitle->getPrefixedText() ) ); } foreach ( $parserOutput->getTemplates() as $ns => $template) @@ -1551,8 +1618,15 @@ END $previewhead.='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n"; } + if( $wgUser->getOption( 'previewontop' ) ) { + // Spacer for the edit toolbar + $previewfoot = '<p><br /></p>'; + } else { + $previewfoot = ''; + } + wfProfileOut( $fname ); - return $previewhead . $previewHTML; + return $previewhead . $previewHTML . $previewfoot; } /** @@ -1578,7 +1652,9 @@ END $attribs = array( 'id' => 'wpTextbox1', 'name' => 'wpTextbox1', 'cols' => $cols, 'rows' => $rows, 'readonly' => 'readonly' ); $wgOut->addHtml( '<hr />' ); $wgOut->addWikiMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() ); - $wgOut->addHtml( wfOpenElement( 'textarea', $attribs ) . htmlspecialchars( $source ) . wfCloseElement( 'textarea' ) ); + # Why we don't use Xml::element here? + # Is it because if $source is '', it returns <textarea />? + $wgOut->addHtml( Xml::openElement( 'textarea', $attribs ) . htmlspecialchars( $source ) . Xml::closeElement( 'textarea' ) ); } } @@ -1630,7 +1706,7 @@ END $wgOut->addHtml( '<div id="spamprotected">' ); $wgOut->addWikiMsg( 'spamprotectiontext' ); if ( $match ) - $wgOut->addWikiMsg( 'spamprotectionmatch',wfEscapeWikiText( $match ) ); + $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) ); $wgOut->addHtml( '</div>' ); $wgOut->returnToMain( false, $wgTitle ); @@ -1647,8 +1723,7 @@ END $db = wfGetDB( DB_MASTER ); // This is the revision the editor started from - $baseRevision = Revision::loadFromTimestamp( - $db, $this->mTitle, $this->edittime ); + $baseRevision = $this->getBaseRevision(); if( is_null( $baseRevision ) ) { wfProfileOut( $fname ); return false; @@ -1719,10 +1794,12 @@ END /** * 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 style/wikibits.js. + * The necessary JavaScript code can be found in skins/common/edit.js. + * + * @return string */ - function getEditToolbar() { - global $wgStylePath, $wgContLang, $wgJsMimeType; + static function getEditToolbar() { + global $wgStylePath, $wgContLang, $wgLang, $wgJsMimeType; /** * toolarray an array of arrays which each include the filename of @@ -1736,93 +1813,104 @@ END * sure these keys are not defined on the edit page. */ $toolarray = array( - array( 'image' => 'button_bold.png', - 'id' => 'mw-editbutton-bold', - 'open' => '\'\'\'', - 'close' => '\'\'\'', - 'sample'=> wfMsg('bold_sample'), - 'tip' => wfMsg('bold_tip'), - 'key' => 'B' + array( + 'image' => $wgLang->getImageFile('button-bold'), + 'id' => 'mw-editbutton-bold', + 'open' => '\'\'\'', + 'close' => '\'\'\'', + 'sample' => wfMsg('bold_sample'), + 'tip' => wfMsg('bold_tip'), + 'key' => 'B' ), - array( 'image' => 'button_italic.png', - 'id' => 'mw-editbutton-italic', - 'open' => '\'\'', - 'close' => '\'\'', - 'sample'=> wfMsg('italic_sample'), - 'tip' => wfMsg('italic_tip'), - 'key' => 'I' + array( + 'image' => $wgLang->getImageFile('button-italic'), + 'id' => 'mw-editbutton-italic', + 'open' => '\'\'', + 'close' => '\'\'', + 'sample' => wfMsg('italic_sample'), + 'tip' => wfMsg('italic_tip'), + 'key' => 'I' ), - array( 'image' => 'button_link.png', - 'id' => 'mw-editbutton-link', - 'open' => '[[', - 'close' => ']]', - 'sample'=> wfMsg('link_sample'), - 'tip' => wfMsg('link_tip'), - 'key' => 'L' + array( + 'image' => $wgLang->getImageFile('button-link'), + 'id' => 'mw-editbutton-link', + 'open' => '[[', + 'close' => ']]', + 'sample' => wfMsg('link_sample'), + 'tip' => wfMsg('link_tip'), + 'key' => 'L' ), - array( 'image' => 'button_extlink.png', - 'id' => 'mw-editbutton-extlink', - 'open' => '[', - 'close' => ']', - 'sample'=> wfMsg('extlink_sample'), - 'tip' => wfMsg('extlink_tip'), - 'key' => 'X' + array( + 'image' => $wgLang->getImageFile('button-extlink'), + 'id' => 'mw-editbutton-extlink', + 'open' => '[', + 'close' => ']', + 'sample' => wfMsg('extlink_sample'), + 'tip' => wfMsg('extlink_tip'), + 'key' => 'X' ), - array( 'image' => 'button_headline.png', - 'id' => 'mw-editbutton-headline', - 'open' => "\n== ", - 'close' => " ==\n", - 'sample'=> wfMsg('headline_sample'), - 'tip' => wfMsg('headline_tip'), - 'key' => 'H' + array( + 'image' => $wgLang->getImageFile('button-headline'), + 'id' => 'mw-editbutton-headline', + 'open' => "\n== ", + 'close' => " ==\n", + 'sample' => wfMsg('headline_sample'), + 'tip' => wfMsg('headline_tip'), + 'key' => 'H' ), - array( 'image' => 'button_image.png', - 'id' => 'mw-editbutton-image', - 'open' => '[['.$wgContLang->getNsText(NS_IMAGE).":", - 'close' => ']]', - 'sample'=> wfMsg('image_sample'), - 'tip' => wfMsg('image_tip'), - 'key' => 'D' + array( + 'image' => $wgLang->getImageFile('button-image'), + 'id' => 'mw-editbutton-image', + 'open' => '[['.$wgContLang->getNsText(NS_IMAGE).':', + 'close' => ']]', + 'sample' => wfMsg('image_sample'), + 'tip' => wfMsg('image_tip'), + 'key' => 'D' ), - array( 'image' => 'button_media.png', - 'id' => 'mw-editbutton-media', - 'open' => '[['.$wgContLang->getNsText(NS_MEDIA).':', - 'close' => ']]', - 'sample'=> wfMsg('media_sample'), - 'tip' => wfMsg('media_tip'), - 'key' => 'M' + array( + 'image' => $wgLang->getImageFile('button-media'), + 'id' => 'mw-editbutton-media', + 'open' => '[['.$wgContLang->getNsText(NS_MEDIA).':', + 'close' => ']]', + 'sample' => wfMsg('media_sample'), + 'tip' => wfMsg('media_tip'), + 'key' => 'M' ), - array( 'image' => 'button_math.png', - 'id' => 'mw-editbutton-math', - 'open' => "<math>", - 'close' => "</math>", - 'sample'=> wfMsg('math_sample'), - 'tip' => wfMsg('math_tip'), - 'key' => 'C' + array( + 'image' => $wgLang->getImageFile('button-math'), + 'id' => 'mw-editbutton-math', + 'open' => "<math>", + 'close' => "</math>", + 'sample' => wfMsg('math_sample'), + 'tip' => wfMsg('math_tip'), + 'key' => 'C' ), - array( 'image' => 'button_nowiki.png', - 'id' => 'mw-editbutton-nowiki', - 'open' => "<nowiki>", - 'close' => "</nowiki>", - 'sample'=> wfMsg('nowiki_sample'), - 'tip' => wfMsg('nowiki_tip'), - 'key' => 'N' + array( + 'image' => $wgLang->getImageFile('button-nowiki'), + 'id' => 'mw-editbutton-nowiki', + 'open' => "<nowiki>", + 'close' => "</nowiki>", + 'sample' => wfMsg('nowiki_sample'), + 'tip' => wfMsg('nowiki_tip'), + 'key' => 'N' ), - array( 'image' => 'button_sig.png', - 'id' => 'mw-editbutton-signature', - 'open' => '--~~~~', - 'close' => '', - 'sample'=> '', - 'tip' => wfMsg('sig_tip'), - 'key' => 'Y' + array( + 'image' => $wgLang->getImageFile('button-sig'), + 'id' => 'mw-editbutton-signature', + 'open' => '--~~~~', + 'close' => '', + 'sample' => '', + 'tip' => wfMsg('sig_tip'), + 'key' => 'Y' ), - array( 'image' => 'button_hr.png', - 'id' => 'mw-editbutton-hr', - 'open' => "\n----\n", - 'close' => '', - 'sample'=> '', - 'tip' => wfMsg('hr_tip'), - 'key' => 'R' + array( + 'image' => $wgLang->getImageFile('button-hr'), + 'id' => 'mw-editbutton-hr', + 'open' => "\n----\n", + 'close' => '', + 'sample' => '', + 'tip' => wfMsg('hr_tip'), + 'key' => 'R' ) ); $toolbar = "<div id='toolbar'>\n"; @@ -1841,7 +1929,7 @@ END $sample = $tool['sample'], $cssId = $tool['id'], ); - + $paramList = implode( ',', array_map( array( 'Xml', 'encodeJsVar' ), $params ) ); $toolbar.="addButton($paramList);\n"; @@ -1878,7 +1966,7 @@ END ); $checkboxes['minor'] = Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) . - " <label for='wpMinoredit'".$skin->tooltipAndAccesskey('minoredit').">{$minorLabel}</label>"; + " <label for='wpMinoredit'".$skin->tooltip('minoredit', 'withaccess').">{$minorLabel}</label>"; } $watchLabel = wfMsgExt('watchthis', array('parseinline')); @@ -1891,7 +1979,7 @@ END ); $checkboxes['watch'] = Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) . - " <label for='wpWatchthis'".$skin->tooltipAndAccesskey('watch').">{$watchLabel}</label>"; + " <label for='wpWatchthis'".$skin->tooltip('watch', 'withaccess').">{$watchLabel}</label>"; } return $checkboxes; } @@ -1918,7 +2006,7 @@ END 'accesskey' => wfMsg('accesskey-save'), 'title' => wfMsg( 'tooltip-save' ).' ['.wfMsg( 'accesskey-save' ).']', ); - $buttons['save'] = wfElement('input', $temp, ''); + $buttons['save'] = Xml::element('input', $temp, ''); ++$tabindex; // use the same for preview and live preview if ( $wgLivePreview && $wgUser->getOption( 'uselivepreview' ) ) { @@ -1932,7 +2020,7 @@ END 'title' => wfMsg( 'tooltip-preview' ).' ['.wfMsg( 'accesskey-preview' ).']', 'style' => 'display: none;', ); - $buttons['preview'] = wfElement('input', $temp, ''); + $buttons['preview'] = Xml::element('input', $temp, ''); $temp = array( 'id' => 'wpLivePreview', @@ -1944,7 +2032,7 @@ END 'title' => '', 'onclick' => $this->doLivePreviewScript(), ); - $buttons['live'] = wfElement('input', $temp, ''); + $buttons['live'] = Xml::element('input', $temp, ''); } else { $temp = array( 'id' => 'wpPreview', @@ -1955,7 +2043,7 @@ END 'accesskey' => wfMsg('accesskey-preview'), 'title' => wfMsg( 'tooltip-preview' ).' ['.wfMsg( 'accesskey-preview' ).']', ); - $buttons['preview'] = wfElement('input', $temp, ''); + $buttons['preview'] = Xml::element('input', $temp, ''); $buttons['live'] = ''; } @@ -1968,8 +2056,8 @@ END 'accesskey' => wfMsg('accesskey-diff'), 'title' => wfMsg( 'tooltip-diff' ).' ['.wfMsg( 'accesskey-diff' ).']', ); - $buttons['diff'] = wfElement('input', $temp, ''); - + $buttons['diff'] = Xml::element('input', $temp, ''); + wfRunHooks( 'EditPageBeforeEditButtons', array( &$this, &$buttons ) ); return $buttons; } @@ -2152,28 +2240,25 @@ END $wgOut->setPageTitle( wfMsg( 'nocreatetitle' ) ); $wgOut->addWikiMsg( 'nocreatetext' ); } - + /** * If there are rows in the deletion log for this page, show them, * along with a nice little note for the user * * @param OutputPage $out */ - private function showDeletionLog( $out ) { - $title = $this->mTitle; - $reader = new LogReader( - new FauxRequest( - array( - 'page' => $title->getPrefixedText(), - 'type' => 'delete', - ) - ) - ); - if( $reader->hasRows() ) { + protected function showDeletionLog( $out ) { + global $wgUser; + $loglist = new LogEventsList( $wgUser->getSkin(), $out ); + $pager = new LogPager( $loglist, 'delete', false, $this->mTitle->getPrefixedText() ); + if( $pager->getNumRows() > 0 ) { $out->addHtml( '<div id="mw-recreate-deleted-warn">' ); $out->addWikiMsg( 'recreate-deleted-warn' ); - $viewer = new LogViewer( $reader ); - $viewer->showList( $out ); + $out->addHTML( + $loglist->beginLogEventsList() . + $pager->getBody() . + $loglist->endLogEventsList() + ); $out->addHtml( '</div>' ); } } @@ -2187,7 +2272,7 @@ END $resultDetails = false; $value = $this->internalAttemptSave( $resultDetails, $wgUser->isAllowed('bot') && $wgRequest->getBool('bot', true) ); - + if( $value == self::AS_SUCCESS_UPDATE || $value == self::AS_SUCCESS_NEW_ARTICLE ) { $this->didSave = true; } @@ -2237,7 +2322,7 @@ END case self::AS_NO_CREATE_PERMISSION; $this->noCreatePermission(); return; - + case self::AS_BLANK_ARTICLE: $wgOut->redirect( $wgTitle->getFullURL() ); return false; @@ -2247,4 +2332,15 @@ END return false; } } + + function getBaseRevision() { + if ($this->mBaseRevision == false) { + $db = wfGetDB( DB_MASTER ); + $baseRevision = Revision::loadFromTimestamp( + $db, $this->mTitle, $this->edittime ); + return $this->mBaseRevision = $baseRevision; + } else { + return $this->mBaseRevision; + } + } } diff --git a/includes/EmaillingJob.php b/includes/EmaillingJob.php index 73d71c56..380c8982 100644 --- a/includes/EmaillingJob.php +++ b/includes/EmaillingJob.php @@ -3,6 +3,8 @@ /** * Old job used for sending single notification emails; * kept for backwards-compatibility + * + * @ingroup JobQueue */ class EmaillingJob extends Job { @@ -20,6 +22,5 @@ class EmaillingJob extends Job { ); return true; } - -} +} diff --git a/includes/EnotifNotifyJob.php b/includes/EnotifNotifyJob.php index 70d1de69..31fcb0d5 100644 --- a/includes/EnotifNotifyJob.php +++ b/includes/EnotifNotifyJob.php @@ -2,6 +2,8 @@ /** * Job for email notification mails + * + * @ingroup JobQueue */ class EnotifNotifyJob extends Job { @@ -11,16 +13,22 @@ class EnotifNotifyJob extends Job { function run() { $enotif = new EmailNotification(); + // Get the user from ID (rename safe). Anons are 0, so defer to name. + if( isset($this->params['editorID']) && $this->params['editorID'] ) { + $editor = User::newFromId( $this->params['editorID'] ); + // B/C, only the name might be given. + } else { + $editor = User::newFromName( $this->params['editor'], false ); + } $enotif->actuallyNotifyOnPageChange( - User::newFromName( $this->params['editor'], false ), - $this->title, - $this->params['timestamp'], - $this->params['summary'], - $this->params['minorEdit'], - $this->params['oldid'] + $editor, + $this->title, + $this->params['timestamp'], + $this->params['summary'], + $this->params['minorEdit'], + $this->params['oldid'] ); return true; } - -} +} diff --git a/includes/Exception.php b/includes/Exception.php index 2fd54352..74820204 100644 --- a/includes/Exception.php +++ b/includes/Exception.php @@ -1,24 +1,43 @@ <?php +/** + * @defgroup Exception Exception + */ /** * MediaWiki exception - * @addtogroup Exception + * @ingroup Exception */ -class MWException extends Exception -{ +class MWException extends Exception { + + /** + * Should the exception use $wgOut to output the error ? + * @return bool + */ function useOutputPage() { - return !empty( $GLOBALS['wgFullyInitialised'] ) && - !empty( $GLOBALS['wgArticle'] ) && !empty( $GLOBALS['wgTitle'] ); + return !empty( $GLOBALS['wgFullyInitialised'] ) && + ( !empty( $GLOBALS['wgArticle'] ) || ( !empty( $GLOBALS['wgOut'] ) && !$GLOBALS['wgOut']->isArticle() ) ) && + !empty( $GLOBALS['wgTitle'] ); } + /** + * Can the extension use wfMsg() to get i18n messages ? + * @return bool + */ function useMessageCache() { global $wgLang; return is_object( $wgLang ); } + /** + * Run hook to allow extensions to modify the text of the exception + * + * @param String $name class name of the exception + * @param Array $args arguments to pass to the callback functions + * @return mixed string to output or null if any hook has been called + */ function runHooks( $name, $args = array() ) { global $wgExceptionHooks; - if( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) + if( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) return; // Just silently ignore if( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[ $name ] ) ) return; @@ -36,7 +55,15 @@ class MWException extends Exception } } - /** Get a message from i18n */ + /** + * Get a message from i18n + * + * @param String $key message name + * @param String $fallback default message if the message cache can't be + * called by the exception + * The function also has other parameters that are arguments for the message + * @return String message with arguments replaced + */ function msg( $key, $fallback /*[, params...] */ ) { $args = array_slice( func_get_args(), 2 ); if ( $this->useMessageCache() ) { @@ -46,11 +73,17 @@ class MWException extends Exception } } - /* If wgShowExceptionDetails, return a HTML message with a backtrace to the error. */ + /** + * If $wgShowExceptionDetails is true, return a HTML message with a + * backtrace to the error, otherwise show a message to ask to set it to true + * to show that information. + * + * @return String html to output + */ function getHTML() { global $wgShowExceptionDetails; if( $wgShowExceptionDetails ) { - return '<p>' . htmlspecialchars( $this->getMessage() ) . + return '<p>' . htmlspecialchars( $this->getMessage() ) . '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . "</p>\n"; } else { @@ -60,15 +93,18 @@ class MWException extends Exception } } - /* If wgShowExceptionDetails, return a text message with a backtrace to the error */ + /** + * If $wgShowExceptionDetails is true, return a text message with a + * backtrace to the error. + */ function getText() { global $wgShowExceptionDetails; if( $wgShowExceptionDetails ) { return $this->getMessage() . "\nBacktrace:\n" . $this->getTraceAsString() . "\n"; } else { - return "<p>Set <tt>\$wgShowExceptionDetails = true;</tt> " . - "in LocalSettings.php to show detailed debugging information.</p>"; + return "Set \$wgShowExceptionDetails = true; " . + "in LocalSettings.php to show detailed debugging information.\n"; } } @@ -82,8 +118,11 @@ class MWException extends Exception } } - /** Return the requested URL and point to file and line number from which the + /** + * Return the requested URL and point to file and line number from which the * exception occured + * + * @return string */ function getLogMessage() { global $wgRequest; @@ -119,22 +158,27 @@ class MWException extends Exception } } - /* Output a report about the exception and takes care of formatting. + /** + * Output a report about the exception and takes care of formatting. * It will be either HTML or plain text based on $wgCommandLineMode. */ function report() { global $wgCommandLineMode; + $log = $this->getLogMessage(); + if ( $log ) { + wfDebugLog( 'exception', $log ); + } if ( $wgCommandLineMode ) { fwrite( STDERR, $this->getText() ); } else { - $log = $this->getLogMessage(); - if ( $log ) { - wfDebugLog( 'exception', $log ); - } $this->reportHTML(); } } + /** + * Send headers and output the beginning of the html page if not using + * $wgOut to output the exception. + */ function htmlHeader() { global $wgLogo, $wgSitename, $wgOutputEncoding; @@ -155,6 +199,9 @@ class MWException extends Exception "; } + /** + * print the end of the html page if not using $wgOut. + */ function htmlFooter() { echo "</body></html>"; } @@ -163,7 +210,7 @@ class MWException extends Exception /** * Exception class which takes an HTML error message, and does not * produce a backtrace. Replacement for OutputPage::fatalError(). - * @addtogroup Exception + * @ingroup Exception */ class FatalError extends MWException { function getHTML() { @@ -176,11 +223,11 @@ class FatalError extends MWException { } /** - * @addtogroup Exception + * @ingroup Exception */ class ErrorPageError extends MWException { public $title, $msg; - + /** * Note: these arguments are keys into wfMsg(), not text! */ @@ -256,5 +303,3 @@ function wfExceptionHandler( $e ) { // Exit value should be nonzero for the benefit of shell jobs exit( 1 ); } - - diff --git a/includes/Exif.php b/includes/Exif.php index d98a8e0d..bd93eb76 100644 --- a/includes/Exif.php +++ b/includes/Exif.php @@ -1,7 +1,6 @@ <?php /** - * @addtogroup Media - * + * @ingroup Media * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com> * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License @@ -26,7 +25,7 @@ /** * @todo document (e.g. one-sentence class-overview description) - * @addtogroup Media + * @ingroup Media */ class Exif { //@{ @@ -431,7 +430,7 @@ class Exif { if ( is_array( $in ) ) { return false; } - + if ( preg_match( "/[^\x0a\x20-\x7e]/", $in ) ) { $this->debug( $in, __FUNCTION__, 'found a character not in our whitelist' ); return false; @@ -557,8 +556,8 @@ class Exif { * * @private * - * @param $in Mixed: - * @param $fname String: + * @param $in Mixed: + * @param $fname String: * @param $action Mixed: , default NULL. */ function debug( $in, $fname, $action = NULL ) { @@ -604,7 +603,7 @@ class Exif { /** * @todo document (e.g. one-sentence class-overview description) - * @addtogroup Media + * @ingroup Media */ class FormatExif { /** @@ -1130,5 +1129,3 @@ define( 'MW_EXIF_RATIONAL', Exif::RATIONAL ); define( 'MW_EXIF_UNDEFINED', Exif::UNDEFINED ); define( 'MW_EXIF_SLONG', Exif::SLONG ); define( 'MW_EXIF_SRATIONAL', Exif::SRATIONAL ); - - diff --git a/includes/Export.php b/includes/Export.php index 69d88fc6..7d0a824e 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -17,15 +17,19 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html +/** + * @defgroup Dump Dump + */ /** - * - * @addtogroup SpecialPage + * @ingroup SpecialPage Dump */ class WikiExporter { var $list_authors = false ; # Return distinct author list (when not returning full history) var $author_list = "" ; + var $dumpUploads = false; + const FULL = 0; const CURRENT = 1; @@ -42,13 +46,13 @@ class WikiExporter { * make additional queries to pull source data while the * main query is still running. * - * @param Database $db - * @param mixed $history one of WikiExporter::FULL or WikiExporter::CURRENT, or an - * associative array: - * offset: non-inclusive offset at which to start the query - * limit: maximum number of rows to return - * dir: "asc" or "desc" timestamp order - * @param int $buffer one of WikiExporter::BUFFER or WikiExporter::STREAM + * @param $db Database + * @param $history Mixed: one of WikiExporter::FULL or WikiExporter::CURRENT, + * or an associative array: + * offset: non-inclusive offset at which to start the query + * limit: maximum number of rows to return + * dir: "asc" or "desc" timestamp order + * @param $buffer Int: one of WikiExporter::BUFFER or WikiExporter::STREAM */ function __construct( &$db, $history = WikiExporter::CURRENT, $buffer = WikiExporter::BUFFER, $text = WikiExporter::TEXT ) { @@ -65,7 +69,7 @@ class WikiExporter { * various row objects and XML output for filtering. Filters * can be chained or used as callbacks. * - * @param mixed $callback + * @param $sink mixed */ function setOutputSink( &$sink ) { $this->sink =& $sink; @@ -93,8 +97,8 @@ class WikiExporter { /** * Dumps a series of page and revision records for those pages * in the database falling within the page_id range given. - * @param int $start Inclusive lower limit (this id is included) - * @param int $end Exclusive upper limit (this id is not included) + * @param $start Int: inclusive lower limit (this id is included) + * @param $end Int: Exclusive upper limit (this id is not included) * If 0, no upper limit. */ function pagesByRange( $start, $end ) { @@ -106,7 +110,7 @@ class WikiExporter { } /** - * @param Title $title + * @param $title Title */ function pageByTitle( $title ) { return $this->dumpFrom( @@ -141,18 +145,18 @@ class WikiExporter { $this->author_list = "<contributors>"; //rev_deleted $nothidden = '(rev_deleted & '.Revision::DELETED_USER.') = 0'; - + $sql = "SELECT DISTINCT rev_user_text,rev_user FROM {$page},{$revision} WHERE page_id=rev_page AND $nothidden AND " . $cond ; $result = $this->db->query( $sql, $fname ); $resultset = $this->db->resultObject( $result ); while( $row = $resultset->fetchObject() ) { - $this->author_list .= "<contributor>" . - "<username>" . - htmlentities( $row->rev_user_text ) . - "</username>" . - "<id>" . + $this->author_list .= "<contributor>" . + "<username>" . + htmlentities( $row->rev_user_text ) . + "</username>" . + "<id>" . $row->rev_user . - "</id>" . + "</id>" . "</contributor>"; } wfProfileOut( $fname ); @@ -253,7 +257,7 @@ class WikiExporter { * separate database connection not managed by LoadBalancer; some * blob storage types will make queries to pull source data. * - * @param ResultWrapper $resultset + * @param $resultset ResultWrapper * @access private */ function outputStream( $resultset ) { @@ -263,7 +267,11 @@ class WikiExporter { $last->page_namespace != $row->page_namespace || $last->page_title != $row->page_title ) { if( isset( $last ) ) { - $output = $this->writer->closePage(); + $output = ''; + if( $this->dumpUploads ) { + $output .= $this->writer->writeUploads( $last ); + } + $output .= $this->writer->closePage(); $this->sink->writeClosePage( $output ); } $output = $this->writer->openPage( $row ); @@ -274,7 +282,12 @@ class WikiExporter { $this->sink->writeRevision( $row, $output ); } if( isset( $last ) ) { - $output = $this->author_list . $this->writer->closePage(); + $output = ''; + if( $this->dumpUploads ) { + $output .= $this->writer->writeUploads( $last ); + } + $output .= $this->author_list; + $output .= $this->writer->closePage(); $this->sink->writeClosePage( $output ); } $resultset->free(); @@ -282,7 +295,7 @@ class WikiExporter { } /** - * @addtogroup Dump + * @ingroup Dump */ class XmlDumpWriter { @@ -375,7 +388,7 @@ class XmlDumpWriter { * Opens a <page> section on the output stream, with data * from the given database row. * - * @param object $row + * @param $row object * @return string * @access private */ @@ -404,7 +417,7 @@ class XmlDumpWriter { * Dumps a <revision> section on the output stream, with * data filled in from the given database row. * - * @param object $row + * @param $row object * @return string * @access private */ @@ -415,20 +428,12 @@ class XmlDumpWriter { $out = " <revision>\n"; $out .= " " . wfElement( 'id', null, strval( $row->rev_id ) ) . "\n"; - $ts = wfTimestamp( TS_ISO_8601, $row->rev_timestamp ); - $out .= " " . wfElement( 'timestamp', null, $ts ) . "\n"; + $out .= $this->writeTimestamp( $row->rev_timestamp ); if( $row->rev_deleted & Revision::DELETED_USER ) { $out .= " " . wfElement( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n"; } else { - $out .= " <contributor>\n"; - if( $row->rev_user ) { - $out .= " " . wfElementClean( 'username', null, strval( $row->rev_user_text ) ) . "\n"; - $out .= " " . wfElement( 'id', null, strval( $row->rev_user ) ) . "\n"; - } else { - $out .= " " . wfElementClean( 'ip', null, strval( $row->rev_user_text ) ) . "\n"; - } - $out .= " </contributor>\n"; + $out .= $this->writeContributor( $row->rev_user, $row->rev_user_text ); } if( $row->rev_minor_edit ) { @@ -461,12 +466,58 @@ class XmlDumpWriter { return $out; } + function writeTimestamp( $timestamp ) { + $ts = wfTimestamp( TS_ISO_8601, $timestamp ); + return " " . wfElement( 'timestamp', null, $ts ) . "\n"; + } + + function writeContributor( $id, $text ) { + $out = " <contributor>\n"; + if( $id ) { + $out .= " " . wfElementClean( 'username', null, strval( $text ) ) . "\n"; + $out .= " " . wfElement( 'id', null, strval( $id ) ) . "\n"; + } else { + $out .= " " . wfElementClean( 'ip', null, strval( $text ) ) . "\n"; + } + $out .= " </contributor>\n"; + return $out; + } + + /** + * Warning! This data is potentially inconsistent. :( + */ + function writeUploads( $row ) { + if( $row->page_namespace == NS_IMAGE ) { + $img = wfFindFile( $row->page_title ); + if( $img ) { + $out = ''; + foreach( array_reverse( $img->getHistory() ) as $ver ) { + $out .= $this->writeUpload( $ver ); + } + $out .= $this->writeUpload( $img ); + return $out; + } + } + return ''; + } + + function writeUpload( $file ) { + return " <upload>\n" . + $this->writeTimestamp( $file->getTimestamp() ) . + $this->writeContributor( $file->getUser( 'id' ), $file->getUser( 'text' ) ) . + " " . wfElementClean( 'comment', null, $file->getDescription() ) . "\n" . + " " . wfElement( 'filename', null, $file->getName() ) . "\n" . + " " . wfElement( 'src', null, $file->getFullUrl() ) . "\n" . + " " . wfElement( 'size', null, $file->getSize() ) . "\n" . + " </upload>\n"; + } + } /** * Base class for output stream; prints to stdout or buffer or whereever. - * @addtogroup Dump + * @ingroup Dump */ class DumpOutput { function writeOpenStream( $string ) { @@ -500,7 +551,7 @@ class DumpOutput { /** * Stream outputter to send data to a file. - * @addtogroup Dump + * @ingroup Dump */ class DumpFileOutput extends DumpOutput { var $handle; @@ -518,7 +569,7 @@ class DumpFileOutput extends DumpOutput { * Stream outputter to send data to a file via some filter program. * Even if compression is available in a library, using a separate * program can allow us to make use of a multi-processor system. - * @addtogroup Dump + * @ingroup Dump */ class DumpPipeOutput extends DumpFileOutput { function DumpPipeOutput( $command, $file = null ) { @@ -531,7 +582,7 @@ class DumpPipeOutput extends DumpFileOutput { /** * Sends dump output via the gzip compressor. - * @addtogroup Dump + * @ingroup Dump */ class DumpGZipOutput extends DumpPipeOutput { function DumpGZipOutput( $file ) { @@ -541,7 +592,7 @@ class DumpGZipOutput extends DumpPipeOutput { /** * Sends dump output via the bgzip2 compressor. - * @addtogroup Dump + * @ingroup Dump */ class DumpBZip2Output extends DumpPipeOutput { function DumpBZip2Output( $file ) { @@ -551,7 +602,7 @@ class DumpBZip2Output extends DumpPipeOutput { /** * Sends dump output via the p7zip compressor. - * @addtogroup Dump + * @ingroup Dump */ class Dump7ZipOutput extends DumpPipeOutput { function Dump7ZipOutput( $file ) { @@ -569,7 +620,7 @@ class Dump7ZipOutput extends DumpPipeOutput { * Dump output filter class. * This just does output filtering and streaming; XML formatting is done * higher up, so be careful in what you do. - * @addtogroup Dump + * @ingroup Dump */ class DumpFilter { function DumpFilter( &$sink ) { @@ -615,17 +666,17 @@ class DumpFilter { /** * Simple dump output filter to exclude all talk pages. - * @addtogroup Dump + * @ingroup Dump */ class DumpNotalkFilter extends DumpFilter { function pass( $page ) { - return !Namespace::isTalk( $page->page_namespace ); + return !MWNamespace::isTalk( $page->page_namespace ); } } /** * Dump output filter to include or exclude pages in a given set of namespaces. - * @addtogroup Dump + * @ingroup Dump */ class DumpNamespaceFilter extends DumpFilter { var $invert = false; @@ -680,7 +731,7 @@ class DumpNamespaceFilter extends DumpFilter { /** * Dump output filter to include only the last revision in each page sequence. - * @addtogroup Dump + * @ingroup Dump */ class DumpLatestFilter extends DumpFilter { var $page, $pageString, $rev, $revString; @@ -712,7 +763,7 @@ class DumpLatestFilter extends DumpFilter { /** * Base class for output stream; prints to stdout or buffer or whereever. - * @addtogroup Dump + * @ingroup Dump */ class DumpMultiWriter { function DumpMultiWriter( $sinks ) { @@ -766,5 +817,3 @@ function xmlsafe( $string ) { wfProfileOut( $fname ); return $string; } - - diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php index f5ce5b9d..1c58f442 100644 --- a/includes/ExternalEdit.php +++ b/includes/ExternalEdit.php @@ -6,8 +6,6 @@ */ /** - * - * * Support for external editors to modify both text and files * in external applications. It works as follows: MediaWiki * sends a meta-file with the MIME type 'application/x-external-editor' @@ -68,4 +66,3 @@ CONTROL; echo $control; } } - diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php index 79937b85..e2b78566 100644 --- a/includes/ExternalStore.php +++ b/includes/ExternalStore.php @@ -1,49 +1,54 @@ <?php /** + * @defgroup ExternalStorage ExternalStorage + */ + +/** * Constructor class for data kept in external repositories * * External repositories might be populated by maintenance/async * scripts, thus partial moving of data may be possible, as well * as possibility to have any storage format (i.e. for archives) + * + * @ingroup ExternalStorage */ - class ExternalStore { /* Fetch data from given URL */ static function fetchFromURL($url) { global $wgExternalStores; - if (!$wgExternalStores) + if( !$wgExternalStores ) return false; - @list($proto,$path)=explode('://',$url,2); + @list( $proto, $path ) = explode( '://', $url, 2 ); /* Bad URL */ - if ($path=="") + if( $path == '' ) return false; - $store =& ExternalStore::getStoreObject( $proto ); + $store = self::getStoreObject( $proto ); if ( $store === false ) return false; - return $store->fetchFromURL($url); + return $store->fetchFromURL( $url ); } /** * Get an external store object of the given type */ - static function &getStoreObject( $proto ) { + static function getStoreObject( $proto ) { global $wgExternalStores; - if (!$wgExternalStores) + if( !$wgExternalStores ) return false; /* Protocol not enabled */ - if (!in_array( $proto, $wgExternalStores )) + if( !in_array( $proto, $wgExternalStores ) ) return false; - $class='ExternalStore'.ucfirst($proto); + $class = 'ExternalStore' . ucfirst( $proto ); /* Any custom modules should be added to $wgAutoLoadClasses for on-demand loading */ - if (!class_exists($class)) { + if( !class_exists( $class ) ){ return false; } - $store=new $class(); - return $store; + + return new $class(); } /** @@ -54,7 +59,7 @@ class ExternalStore { */ static function insert( $url, $data ) { list( $proto, $params ) = explode( '://', $url, 2 ); - $store =& ExternalStore::getStoreObject( $proto ); + $store = self::getStoreObject( $proto ); if ( $store === false ) { return false; } else { @@ -62,4 +67,3 @@ class ExternalStore { } } } - diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php index f9046f74..549412d1 100644 --- a/includes/ExternalStoreDB.php +++ b/includes/ExternalStoreDB.php @@ -1,12 +1,4 @@ <?php -/** - * - * - * DB accessable external objects - * - */ - - /** * External database storage will use one (or more) separate connection pools @@ -28,16 +20,15 @@ $wgExternalLoadBalancers = array(); global $wgExternalBlobCache; $wgExternalBlobCache = array(); +/** + * DB accessable external objects + * @ingroup ExternalStorage + */ class ExternalStoreDB { /** @todo Document.*/ function &getLoadBalancer( $cluster ) { - global $wgExternalServers, $wgExternalLoadBalancers; - if ( !array_key_exists( $cluster, $wgExternalLoadBalancers ) ) { - $wgExternalLoadBalancers[$cluster] = LoadBalancer::newFromParams( $wgExternalServers[$cluster] ); - } - $wgExternalLoadBalancers[$cluster]->allowLagged(true); - return $wgExternalLoadBalancers[$cluster]; + return wfGetLBFactory()->getExternalLB( $cluster ); } /** @todo Document.*/ @@ -144,4 +135,3 @@ class ExternalStoreDB { return "DB://$cluster/$id"; } } - diff --git a/includes/ExternalStoreHttp.php b/includes/ExternalStoreHttp.php index ef907df5..6eb33b39 100644 --- a/includes/ExternalStoreHttp.php +++ b/includes/ExternalStoreHttp.php @@ -1,11 +1,9 @@ <?php /** - * - * * Example class for HTTP accessable external objects * + * @ingroup ExternalStorage */ - class ExternalStoreHttp { /* Fetch data from given URL */ function fetchFromURL($url) { @@ -19,4 +17,3 @@ class ExternalStoreHttp { * whatever, for initial ext storage */ } - diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php index b63ae505..4c2eddc8 100644 --- a/includes/FakeTitle.php +++ b/includes/FakeTitle.php @@ -83,5 +83,3 @@ class FakeTitle { function trackbackURL() { $this->error(); } function trackbackRDF() { $this->error(); } } - - diff --git a/includes/Feed.php b/includes/Feed.php index 309b29bd..512057d9 100644 --- a/includes/Feed.php +++ b/includes/Feed.php @@ -300,5 +300,3 @@ class AtomFeed extends ChannelFeed { </feed><?php } } - -?> diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php new file mode 100644 index 00000000..aa784c02 --- /dev/null +++ b/includes/FeedUtils.php @@ -0,0 +1,152 @@ +<?php + +// TODO: document +class FeedUtils { + + public static function checkPurge( $timekey, $key ) { + global $wgRequest, $wgUser, $messageMemc; + $purge = $wgRequest->getVal( 'action' ) === 'purge'; + if ( $purge && $wgUser->isAllowed('purge') ) { + $messageMemc->delete( $timekey ); + $messageMemc->delete( $key ); + } + } + + public static function checkFeedOutput( $type ) { + global $wgFeed, $wgOut, $wgFeedClasses; + + if ( !$wgFeed ) { + global $wgOut; + $wgOut->addWikiMsg( 'feed-unavailable' ); + return false; + } + + if( !isset( $wgFeedClasses[$type] ) ) { + wfHttpError( 500, "Internal Server Error", "Unsupported feed type." ); + return false; + } + + return true; + } + + /** + * Format a diff for the newsfeed + */ + 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) ); + } + } + 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 ); + } + + public static function formatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext='' ) { + global $wgFeedDiffCutoff, $wgContLang, $wgUser; + wfProfileIn( __FUNCTION__ ); + + $skin = $wgUser->getSkin(); + # log enties + $completeText = '<p>' . implode( ' ', + array_filter( + array( + $actiontext, + $skin->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. + $anon = new User(); + $accErrors = $title->getUserPermissionsErrors( 'read', $anon, true ); + + if( $title->getNamespace() >= 0 && !$accErrors ) { + if( $oldid ) { + wfProfileIn( __FUNCTION__."-dodiff" ); + + $de = new DifferenceEngine( $title, $oldid, $newid ); + #$diffText = $de->getDiff( wfMsg( 'revisionasof', + # $wgContLang->timeanddate( $timestamp ) ), + # wfMsg( 'currentrev' ) ); + $diffText = $de->getDiff( + wfMsg( 'previousrevision' ), // hack + wfMsg( 'revisionasof', + $wgContLang->timeanddate( $timestamp ) ) ); + + + if ( 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( __FUNCTION__."-dodiff" ); + } 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; + } + + wfProfileOut( __FUNCTION__ ); + return $completeText; + } + + /** + * 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: + * @return string + * @private + */ + public static function applyDiffStyle( $text ) { + $styles = array( + 'diff' => 'background-color: white; color:black;', + 'diff-otitle' => 'background-color: white; color:black;', + 'diff-ntitle' => 'background-color: white; color:black;', + 'diff-addedline' => 'background: #cfc; color:black; font-size: smaller;', + 'diff-deletedline' => 'background: #ffa; color:black; font-size: smaller;', + 'diff-context' => 'background: #eee; color:black; font-size: smaller;', + 'diffchange' => 'color: red; font-weight: bold; text-decoration: none;', + ); + + foreach( $styles as $class => $style ) { + $text = preg_replace( "/(<[^>]+)class=(['\"])$class\\2([^>]*>)/", + "\\1style=\"$style\"\\3", $text ); + } + + return $text; + } + +}
\ No newline at end of file diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index 71e2c1ae..bc80c2b2 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -3,7 +3,7 @@ /** * File deletion user interface * - * @addtogroup Media + * @ingroup Media * @author Rob Church <robchur@gmail.com> */ class FileDeleteForm { @@ -13,7 +13,7 @@ class FileDeleteForm { private $oldfile = null; private $oldimage = ''; - + /** * Constructor * @@ -23,7 +23,7 @@ class FileDeleteForm { $this->title = $file->getTitle(); $this->file = $file; } - + /** * Fulfil the request; shows the form or deletes the file, * pending authentication, confirmation, etc. @@ -35,32 +35,31 @@ class FileDeleteForm { if( wfReadOnly() ) { $wgOut->readOnlyPage(); return; - } elseif( !$wgUser->isLoggedIn() ) { - $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); - return; - } elseif( !$wgUser->isAllowed( 'delete' ) ) { - $wgOut->permissionRequired( 'delete' ); - return; - } elseif( $wgUser->isBlocked() ) { - $wgOut->blockedPage(); + } + $permission_errors = $this->title->getUserPermissionsErrors('delete', $wgUser); + if (count($permission_errors)>0) { + $wgOut->showPermissionsErrorPage( $permission_errors ); return; } - + $this->oldimage = $wgRequest->getText( 'oldimage', false ); $token = $wgRequest->getText( 'wpEditToken' ); - if( $this->oldimage && !$this->isValidOldSpec() ) { + # 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 ) $this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->oldimage ); - - if( !$this->haveDeletableFile() ) { + + if( !self::haveDeletableFile($this->file, $this->oldfile, $this->oldimage) ) { $wgOut->addHtml( $this->prepareMessage( 'filedelete-nofile' ) ); $wgOut->addReturnTo( $this->title ); return; } - + // Perform the deletion if appropriate if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) { $this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList' ); @@ -72,24 +71,9 @@ class FileDeleteForm { } elseif ( $reason == 'other' ) { $reason = $this->DeleteReason; } - if( $this->oldimage ) { - $status = $this->file->deleteOld( $this->oldimage, $reason ); - if( $status->ok ) { - // Need to do a log item - $log = new LogPage( 'delete' ); - $logComment = wfMsgForContent( 'deletedrevision', $this->oldimage ); - if( trim( $reason ) != '' ) - $logComment .= ": {$reason}"; - $log->addEntry( 'delete', $this->title, $logComment ); - } - } else { - $status = $this->file->delete( $reason ); - if( $status->ok ) { - // Need to delete the associated article - $article = new Article( $this->title ); - $article->doDeleteArticle( $reason ); - } - } + + $status = self::doDelete( $this->title, $this->file, $this->oldimage, $reason, $suppress ); + if( !$status->isGood() ) $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) ); if( $status->ok ) { @@ -101,11 +85,40 @@ class FileDeleteForm { } return; } - + $this->showForm(); $this->showLogEntries(); } + public static function doDelete( &$title, &$file, &$oldimage, $reason, $suppress ) { + $article = null; + if( $oldimage ) { + $status = $file->deleteOld( $oldimage, $reason, $suppress ); + if( $status->ok ) { + // Need to do a log item + $log = new LogPage( 'delete' ); + $logComment = wfMsgForContent( 'deletedrevision', $oldimage ); + if( trim( $reason ) != '' ) + $logComment .= ": {$reason}"; + $log->addEntry( 'delete', $title, $logComment ); + } + } else { + $status = $file->delete( $reason, $suppress ); + if( $status->ok ) { + // Need to delete the associated article + $article = new Article( $title ); + if( wfRunHooks('ArticleDelete', array(&$article, &$wgUser, &$reason)) ) { + if( $article->doDeleteArticle( $reason, $suppress ) ) + wfRunHooks('ArticleDeleteComplete', array(&$article, &$wgUser, $reason)); + } + } + } + if( $status->isGood() ) wfRunHooks('FileDeleteComplete', array( + &$file, &$oldimage, &$article, &$wgUser, &$reason)); + + return $status; + } + /** * Show the confirmation form */ @@ -113,6 +126,14 @@ class FileDeleteForm { global $wgOut, $wgUser, $wgRequest, $wgContLang; $align = $wgContLang->isRtl() ? 'left' : 'right'; + if( $wgUser->isAllowed( 'suppressrevision' ) ) { + $suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\"><td></td><td>"; + $suppress .= Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '2' ) ); + $suppress .= "</td></tr>"; + } else { + $suppress = ''; + } + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) ) . Xml::openElement( 'fieldset' ) . Xml::element( 'legend', null, wfMsg( 'filedelete-legend' ) ) . @@ -125,7 +146,7 @@ class FileDeleteForm { "</td> <td>" . Xml::listDropDown( 'wpDeleteReasonList', - wfMsgForContent( 'filedelete-reason-dropdown' ), + wfMsgForContent( 'filedelete-reason-dropdown' ), wfMsgForContent( 'filedelete-reason-otherlist' ), '', 'wpReasonDropDown', 1 ) . "</td> </tr> @@ -137,6 +158,7 @@ class FileDeleteForm { Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ), array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) . "</td> </tr> + {$suppress} <tr> <td></td> <td>" . @@ -147,6 +169,12 @@ class FileDeleteForm { Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ); + if ( $wgUser->isAllowed( 'editinterface' ) ) { + $skin = $wgUser->getSkin(); + $link = $skin->makeLink ( 'MediaWiki:Filedelete-reason-dropdown', wfMsgHtml( 'filedelete-edit-reasonlist' ) ); + $form .= '<p class="mw-filedelete-editreasons">' . $link . '</p>'; + } + $wgOut->addHtml( $form ); } @@ -156,19 +184,9 @@ class FileDeleteForm { private function showLogEntries() { global $wgOut; $wgOut->addHtml( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" ); - $reader = new LogViewer( - new LogReader( - new FauxRequest( - array( - 'type' => 'delete', - 'page' => $this->title->getPrefixedText(), - ) - ) - ) - ); - $reader->showList( $wgOut ); + LogEventsList::showLogExtract( $wgOut, 'delete', $this->title->getPrefixedText() ); } - + /** * Prepare a message referring to the file being deleted, * showing an appropriate message depending upon whether @@ -196,7 +214,7 @@ class FileDeleteForm { ); } } - + /** * Set headers, titles and other bits */ @@ -206,18 +224,18 @@ class FileDeleteForm { $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setSubtitle( wfMsg( 'filedelete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->title ) ) ); } - + /** * Is the provided `oldimage` value valid? * * @return bool */ - private function isValidOldSpec() { - return strlen( $this->oldimage ) >= 16 - && strpos( $this->oldimage, '/' ) === false - && strpos( $this->oldimage, '\\' ) === false; + public static function isValidOldSpec($oldimage) { + return strlen( $oldimage ) >= 16 + && strpos( $oldimage, '/' ) === false + && strpos( $oldimage, '\\' ) === false; } - + /** * Could we delete the file specified? If an `oldimage` * value was provided, does it correspond to an @@ -225,12 +243,12 @@ class FileDeleteForm { * * @return bool */ - private function haveDeletableFile() { - return $this->oldimage - ? $this->oldfile && $this->oldfile->exists() && $this->oldfile->isLocal() - : $this->file && $this->file->exists() && $this->file->isLocal(); + public static function haveDeletableFile(&$file, &$oldfile, $oldimage) { + return $oldimage + ? $oldfile && $oldfile->exists() && $oldfile->isLocal() + : $file && $file->exists() && $file->isLocal(); } - + /** * Prepare the form action * @@ -243,7 +261,7 @@ class FileDeleteForm { $q[] = 'oldimage=' . urlencode( $this->oldimage ); return $this->title->getLocalUrl( implode( '&', $q ) ); } - + /** * Extract the timestamp of the old version * @@ -252,5 +270,5 @@ class FileDeleteForm { private function getTimestamp() { return $this->oldfile->getTimestamp(); } - + } diff --git a/includes/FileRevertForm.php b/includes/FileRevertForm.php index f335d024..385d83bc 100644 --- a/includes/FileRevertForm.php +++ b/includes/FileRevertForm.php @@ -3,16 +3,17 @@ /** * File reversion user interface * - * @addtogroup Media + * @ingroup Media * @author Rob Church <robchur@gmail.com> */ class FileRevertForm { - private $title = null; - private $file = null; - private $oldimage = ''; - private $timestamp = false; - + protected $title = null; + protected $file = null; + protected $archiveName = ''; + protected $timestamp = false; + protected $oldFile; + /** * Constructor * @@ -22,7 +23,7 @@ class FileRevertForm { $this->title = $file->getTitle(); $this->file = $file; } - + /** * Fulfil the request; shows the form or reverts the file, * pending authentication, confirmation, etc. @@ -37,7 +38,7 @@ class FileRevertForm { } elseif( !$wgUser->isLoggedIn() ) { $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); return; - } elseif( !$this->title->userCan( 'edit' ) ) { + } elseif( !$this->title->userCan( 'edit' ) || !$this->title->userCan( 'upload' ) ) { // The standard read-only thing doesn't make a whole lot of sense // here; surely it should show the image or something? -- RC $article = new Article( $this->title ); @@ -47,23 +48,23 @@ class FileRevertForm { $wgOut->blockedPage(); return; } - - $this->oldimage = $wgRequest->getText( 'oldimage' ); + + $this->archiveName = $wgRequest->getText( 'oldimage' ); $token = $wgRequest->getText( 'wpEditToken' ); if( !$this->isValidOldSpec() ) { - $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->oldimage ) ); + $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->archiveName ) ); return; } - + if( !$this->haveOldVersion() ) { $wgOut->addHtml( wfMsgExt( 'filerevert-badversion', 'parse' ) ); $wgOut->returnToMain( false, $this->title ); return; } - + // Perform the reversion if appropriate - if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) { - $source = $this->file->getArchiveVirtualUrl( $this->oldimage ); + if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->archiveName ) ) { + $source = $this->file->getArchiveVirtualUrl( $this->archiveName ); $comment = $wgRequest->getText( 'wpComment' ); // TODO: Preserve file properties from database instead of reloading from file $status = $this->file->upload( $source, $comment, $comment ); @@ -71,96 +72,100 @@ class FileRevertForm { $wgOut->addHtml( wfMsgExt( 'filerevert-success', 'parse', $this->title->getText(), $wgLang->date( $this->getTimestamp(), true ), $wgLang->time( $this->getTimestamp(), true ), - wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ) ) ) ); + wfExpandUrl( $this->file->getArchiveUrl( $this->archiveName ) ) ) ); $wgOut->returnToMain( false, $this->title ); } else { $wgOut->addWikiText( $status->getWikiText() ); } return; } - + // Show the form - $this->showForm(); + $this->showForm(); } - + /** * Show the confirmation form */ - private function showForm() { + protected function showForm() { global $wgOut, $wgUser, $wgRequest, $wgLang, $wgContLang; $timestamp = $this->getTimestamp(); $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) ); - $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) ); + $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->archiveName ) ); $form .= '<fieldset><legend>' . wfMsgHtml( 'filerevert-legend' ) . '</legend>'; $form .= wfMsgExt( 'filerevert-intro', 'parse', $this->title->getText(), $wgLang->date( $timestamp, true ), $wgLang->time( $timestamp, true ), - wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ) ) ); + wfExpandUrl( $this->file->getArchiveUrl( $this->archiveName ) ) ); $form .= '<p>' . Xml::inputLabel( wfMsg( 'filerevert-comment' ), 'wpComment', 'wpComment', 60, wfMsgForContent( 'filerevert-defaultcomment', $wgContLang->date( $timestamp, false, false ), $wgContLang->time( $timestamp, false, false ) ) ) . '</p>'; $form .= '<p>' . Xml::submitButton( wfMsg( 'filerevert-submit' ) ) . '</p>'; $form .= '</fieldset>'; $form .= '</form>'; - + $wgOut->addHtml( $form ); } - + /** * Set headers, titles and other bits */ - private function setHeaders() { + protected function setHeaders() { global $wgOut, $wgUser; $wgOut->setPageTitle( wfMsg( 'filerevert', $this->title->getText() ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setSubtitle( wfMsg( 'filerevert-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->title ) ) ); } - + /** * Is the provided `oldimage` value valid? * * @return bool */ - private function isValidOldSpec() { - return strlen( $this->oldimage ) >= 16 - && strpos( $this->oldimage, '/' ) === false - && strpos( $this->oldimage, '\\' ) === false; + protected function isValidOldSpec() { + return strlen( $this->archiveName ) >= 16 + && strpos( $this->archiveName, '/' ) === false + && strpos( $this->archiveName, '\\' ) === false; } - + /** * Does the provided `oldimage` value correspond * to an existing, local, old version of this file? * * @return bool */ - private function haveOldVersion() { - $file = wfFindFile( $this->title, $this->oldimage ); - return $file && $file->exists() && $file->isLocal(); + protected function haveOldVersion() { + return $this->getOldFile()->exists(); } - + /** * Prepare the form action * * @return string */ - private function getAction() { + protected function getAction() { $q = array(); $q[] = 'action=revert'; - $q[] = 'oldimage=' . urlencode( $this->oldimage ); + $q[] = 'oldimage=' . urlencode( $this->archiveName ); return $this->title->getLocalUrl( implode( '&', $q ) ); } - + /** * Extract the timestamp of the old version * * @return string */ - private function getTimestamp() { + protected function getTimestamp() { if( $this->timestamp === false ) { - $file = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->oldimage ); - $this->timestamp = $file->getTimestamp(); + $this->timestamp = $this->getOldFile()->getTimestamp(); } return $this->timestamp; } - -}
\ No newline at end of file + + protected function getOldFile() { + if ( !isset( $this->oldFile ) ) { + $this->oldFile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->archiveName ); + } + return $this->oldFile; + } +} diff --git a/includes/FileStore.php b/includes/FileStore.php index a547e7e4..c01350c0 100644 --- a/includes/FileStore.php +++ b/includes/FileStore.php @@ -5,13 +5,13 @@ */ class FileStore { const DELETE_ORIGINAL = 1; - + /** * Fetch the FileStore object for a given storage group */ static function get( $group ) { global $wgFileStore; - + if( isset( $wgFileStore[$group] ) ) { $info = $wgFileStore[$group]; return new FileStore( $group, @@ -22,14 +22,14 @@ class FileStore { return null; } } - + private function __construct( $group, $directory, $path, $hash ) { $this->mGroup = $group; $this->mDirectory = $directory; $this->mPath = $path; $this->mHashLevel = $hash; } - + /** * Acquire a lock; use when performing write operations on a store. * This is attached to your master database connection, so if you @@ -47,7 +47,7 @@ class FileStore { $result = $dbw->query( "SELECT GET_LOCK($lockname, 5) AS lockstatus", __METHOD__ ); $row = $dbw->fetchObject( $result ); $dbw->freeResult( $result ); - + if( $row->lockstatus == 1 ) { return true; } else { @@ -55,7 +55,7 @@ class FileStore { return false; } } - + /** * Release the global file store lock. */ @@ -69,11 +69,11 @@ class FileStore { $dbw->fetchObject( $result ); $dbw->freeResult( $result ); } - + private static function lockName() { return 'MediaWiki.' . wfWikiID() . '.FileStore'; } - + /** * Copy a file into the file store from elsewhere in the filesystem. * Should be protected by FileStore::lock() to avoid race conditions. @@ -89,7 +89,7 @@ class FileStore { $destPath = $this->filePath( $key ); return $this->copyFile( $sourcePath, $destPath, $flags ); } - + /** * Copy a file from the file store to elsewhere in the filesystem. * Should be protected by FileStore::lock() to avoid race conditions. @@ -105,19 +105,19 @@ class FileStore { $sourcePath = $this->filePath( $key ); return $this->copyFile( $sourcePath, $destPath, $flags ); } - + private function copyFile( $sourcePath, $destPath, $flags=0 ) { if( !file_exists( $sourcePath ) ) { // Abort! Abort! throw new FSException( "missing source file '$sourcePath'" ); } - + $transaction = new FSTransaction(); - + if( $flags & self::DELETE_ORIGINAL ) { $transaction->addCommit( FSTransaction::DELETE_FILE, $sourcePath ); } - + if( file_exists( $destPath ) ) { // An identical file is already present; no need to copy. } else { @@ -125,17 +125,17 @@ class FileStore { wfSuppressWarnings(); $ok = mkdir( dirname( $destPath ), 0777, true ); wfRestoreWarnings(); - + if( !$ok ) { throw new FSException( "failed to create directory for '$destPath'" ); } } - + wfSuppressWarnings(); $ok = copy( $sourcePath, $destPath ); wfRestoreWarnings(); - + if( $ok ) { wfDebug( __METHOD__." copied '$sourcePath' to '$destPath'\n" ); $transaction->addRollback( FSTransaction::DELETE_FILE, $destPath ); @@ -144,10 +144,10 @@ class FileStore { __METHOD__." failed to copy '$sourcePath' to '$destPath'" ); } } - + return $transaction; } - + /** * Delete a file from the file store. * Caller's responsibility to make sure it's not being used by another row. @@ -167,7 +167,7 @@ class FileStore { return FileStore::deleteFile( $destPath ); } } - + /** * Delete a non-managed file on a transactional basis. * @@ -189,7 +189,7 @@ class FileStore { throw new FSException( "cannot delete missing file '$path'" ); } } - + /** * Stream a contained file directly to HTTP output. * Will throw a 404 if file is missing; 400 if invalid key. @@ -201,12 +201,12 @@ class FileStore { wfHttpError( 400, "Bad request", "Invalid or badly-formed filename." ); return false; } - + if( file_exists( $path ) ) { // Set the filename for more convenient save behavior from browsers // FIXME: Is this safe? header( 'Content-Disposition: inline; filename="' . $key . '"' ); - + require_once 'StreamFile.php'; wfStreamFile( $path ); } else { @@ -214,7 +214,7 @@ class FileStore { "The requested resource does not exist." ); } } - + /** * Confirm that the given file key is valid. * Note that a valid key may refer to a file that does not exist. @@ -229,8 +229,8 @@ class FileStore { static function validKey( $key ) { return preg_match( '/^[0-9a-z]{31,32}(\.[0-9a-z]{1,31})?$/', $key ); } - - + + /** * Calculate file storage key from a file on disk. * You must pass an extension to it, as some files may be calculated @@ -248,14 +248,14 @@ class FileStore { wfDebug( __METHOD__.": couldn't hash file '$path'\n" ); return false; } - + $base36 = wfBaseConvert( $hash, 16, 36, 31 ); if( $extension == '' ) { $key = $base36; } else { $key = $base36 . '.' . $extension; } - + // Sanity check if( self::validKey( $key ) ) { return $key; @@ -264,7 +264,7 @@ class FileStore { return false; } } - + /** * Return filesystem path to the given file. * Note that the file may or may not exist. @@ -278,7 +278,7 @@ class FileStore { return false; } } - + /** * Return URL path to the given file, if the store is public. * @return string or false if not public @@ -290,7 +290,7 @@ class FileStore { return false; } } - + private function hashPath( $key, $separator ) { $parts = array(); for( $i = 0; $i < $this->mHashLevel; $i++ ) { @@ -310,7 +310,7 @@ class FileStore { */ class FSTransaction { const DELETE_FILE = 1; - + /** * Combine more items into a fancier transaction */ @@ -320,7 +320,7 @@ class FSTransaction { $this->mOnRollback = array_merge( $this->mOnRollback, $transaction->mOnRollback ); } - + /** * Perform final actions for success. * @return true if actions applied ok, false if errors @@ -328,7 +328,7 @@ class FSTransaction { function commit() { return $this->apply( $this->mOnCommit ); } - + /** * Perform final actions for failure. * @return true if actions applied ok, false if errors @@ -336,22 +336,22 @@ class FSTransaction { function rollback() { return $this->apply( $this->mOnRollback ); } - + // --- Private and friend functions below... - + function __construct() { $this->mOnCommit = array(); $this->mOnRollback = array(); } - + function addCommit( $action, $path ) { $this->mOnCommit[] = array( $action, $path ); } - + function addRollback( $action, $path ) { $this->mOnRollback[] = array( $action, $path ); } - + private function apply( $actions ) { $result = true; foreach( $actions as $item ) { @@ -372,8 +372,6 @@ class FSTransaction { } /** - * @addtogroup Exception + * @ingroup Exception */ class FSException extends MWException { } - - diff --git a/includes/FormOptions.php b/includes/FormOptions.php new file mode 100644 index 00000000..5888a0c4 --- /dev/null +++ b/includes/FormOptions.php @@ -0,0 +1,202 @@ +<?php +/** + * Helper class to keep track of options when mixing links and form elements. + * + * @author Niklas Laxström + * @copyright Copyright © 2008, Niklas Laxström + */ + +class FormOptions implements ArrayAccess { + const AUTO = -1; //! Automatically detects simple data types + const STRING = 0; + const INT = 1; + const BOOL = 2; + const INTNULL = 3; //! Useful for namespace selector + + protected $options = array(); + + # Setting up + + public function add( $name, $default, $type = self::AUTO ) { + $option = array(); + $option['default'] = $default; + $option['value'] = null; + $option['consumed'] = false; + + if ( $type !== self::AUTO ) { + $option['type'] = $type; + } else { + $option['type'] = self::guessType( $default ); + } + + $this->options[$name] = $option; + } + + public function delete( $name ) { + $this->validateName( $name, true ); + unset($this->options[$name]); + } + + public static function guessType( $data ) { + if ( is_bool($data) ) { + return self::BOOL; + } elseif( is_int($data) ) { + return self::INT; + } elseif( is_string($data) ) { + return self::STRING; + } else { + throw new MWException( 'Unsupported datatype' ); + } + } + + # Handling values + + public function validateName( $name, $strict = false ) { + if ( !isset($this->options[$name]) ) { + if ( $strict ) { + throw new MWException( "Invalid option $name" ); + } else { + return false; + } + } + return true; + } + + public function setValue( $name, $value, $force = false ) { + $this->validateName( $name, true ); + if ( !$force && $value === $this->options[$name]['default'] ) { + // null default values as unchanged + $this->options[$name]['value'] = null; + } else { + $this->options[$name]['value'] = $value; + } + } + + public function getValue( $name ) { + $this->validateName( $name, true ); + return $this->getValueReal( $this->options[$name] ); + } + + protected function getValueReal( $option ) { + if ( $option['value'] !== null ) { + return $option['value']; + } else { + return $option['default']; + } + } + + public function reset( $name ) { + $this->validateName( $name, true ); + $this->options[$name]['value'] = null; + } + + public function consumeValue( $name ) { + $this->validateName( $name, true ); + $this->options[$name]['consumed'] = true; + return $this->getValueReal( $this->options[$name] ); + } + + public function consumeValues( /*Array*/ $names ) { + $out = array(); + foreach ( $names as $name ) { + $this->validateName( $name, true ); + $this->options[$name]['consumed'] = true; + $out[] = $this->getValueReal( $this->options[$name] ); + } + return $out; + } + + # Validating values + + public function validateIntBounds( $name, $min, $max ) { + $this->validateName( $name, true ); + + if ( $this->options[$name]['type'] !== self::INT ) + throw new MWException( "Option $name is not of type int" ); + + $value = $this->getValueReal( $this->options[$name] ); + $value = max( $min, min( $max, $value ) ); + + $this->setValue( $name, $value ); + } + + # Getting the data out for use + + public function getUnconsumedValues( $all = false ) { + $values = array(); + foreach ( $this->options as $name => $data ) { + if ( !$data['consumed'] ) { + if ( $all || $data['value'] !== null ) { + $values[$name] = $this->getValueReal( $data ); + } + } + } + return $values; + } + + public function getChangedValues() { + $values = array(); + foreach ( $this->options as $name => $data ) { + if ( $data['value'] !== null ) { + $values[$name] = $data['value']; + } + } + return $values; + } + + public function getAllValues() { + $values = array(); + foreach ( $this->options as $name => $data ) { + $values[$name] = $this->getValueReal( $data ); + } + return $values; + } + + # Reading values + + public function fetchValuesFromRequest( WebRequest $r, $values = false ) { + if ( !$values ) { + $values = array_keys($this->options); + } + + foreach ( $values as $name ) { + $default = $this->options[$name]['default']; + $type = $this->options[$name]['type']; + + switch( $type ) { + case self::BOOL: + $value = $r->getBool( $name, $default ); break; + case self::INT: + $value = $r->getInt( $name, $default ); break; + case self::STRING: + $value = $r->getText( $name, $default ); break; + case self::INTNULL: + $value = $r->getIntOrNull( $name ); break; + default: + throw new MWException( 'Unsupported datatype' ); + } + + if ( $value !== $default && $value !== null ) { + $this->options[$name]['value'] = $value; + } + } + } + + /* ArrayAccess methods */ + public function offsetExists( $name ) { + return isset($this->options[$name]); + } + + public function offsetGet( $name ) { + return $this->getValue( $name ); + } + + public function offsetSet( $name, $value ) { + return $this->setValue( $name, $value ); + } + + public function offsetUnset( $name ) { + return $this->delete( $name ); + } + +} diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 2b9543b4..f5a2660c 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -87,6 +87,29 @@ if ( !function_exists( 'array_diff_key' ) ) { } } +/** + * Like array_diff( $a, $b ) except that it works with two-dimensional arrays. + */ +function wfArrayDiff2( $a, $b ) { + return array_udiff( $a, $b, 'wfArrayDiff2_cmp' ); +} +function wfArrayDiff2_cmp( $a, $b ) { + if ( !is_array( $a ) ) { + return strcmp( $a, $b ); + } elseif ( count( $a ) !== count( $b ) ) { + return count( $a ) < count( $b ) ? -1 : 1; + } else { + reset( $a ); + reset( $b ); + while( ( list( $keyA, $valueA ) = each( $a ) ) && ( list( $keyB, $valueB ) = each( $b ) ) ) { + $cmp = strcmp( $valueA, $valueB ); + if ( $cmp !== 0 ) { + return $cmp; + } + } + return 0; + } +} /** * Wrapper for clone(), for compatibility with PHP4-friendly extensions. @@ -153,12 +176,16 @@ function wfDebug( $text, $logonly = false ) { global $wgOut, $wgDebugLogFile, $wgDebugComments, $wgProfileOnly, $wgDebugRawPage; static $recursion = 0; + static $cache = array(); // Cache of unoutputted messages + # Check for raw action using $_GET not $wgRequest, since the latter might not be initialised yet if ( isset( $_GET['action'] ) && $_GET['action'] == 'raw' && !$wgDebugRawPage ) { return; } if ( $wgDebugComments && !$logonly ) { + $cache[] = $text; + if ( !isset( $wgOut ) ) { return; } @@ -170,7 +197,10 @@ function wfDebug( $text, $logonly = false ) { $wgOut->_unstub(); $recursion--; } - $wgOut->debug( $text ); + + // add the message and possible cached ones to the output + array_map( array( $wgOut, 'debug' ), $cache ); + $cache = array(); } if ( '' != $wgDebugLogFile && !$wgProfileOnly ) { # Strip unprintables; they can switch terminal modes when binary data @@ -232,29 +262,30 @@ function wfErrorLog( $text, $file ) { */ function wfLogProfilingData() { global $wgRequestTime, $wgDebugLogFile, $wgDebugRawPage, $wgRequest; - global $wgProfiling, $wgUser; - if ( $wgProfiling ) { - $now = wfTime(); - $elapsed = $now - $wgRequestTime; - $prof = wfGetProfilingOutput( $wgRequestTime, $elapsed ); - $forward = ''; - if( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) - $forward = ' forwarded for ' . $_SERVER['HTTP_X_FORWARDED_FOR']; - if( !empty( $_SERVER['HTTP_CLIENT_IP'] ) ) - $forward .= ' client IP ' . $_SERVER['HTTP_CLIENT_IP']; - if( !empty( $_SERVER['HTTP_FROM'] ) ) - $forward .= ' from ' . $_SERVER['HTTP_FROM']; - if( $forward ) - $forward = "\t(proxied via {$_SERVER['REMOTE_ADDR']}{$forward})"; - // Don't unstub $wgUser at this late stage just for statistics purposes - if( StubObject::isRealObject($wgUser) && $wgUser->isAnon() ) - $forward .= ' anon'; - $log = sprintf( "%s\t%04.3f\t%s\n", - gmdate( 'YmdHis' ), $elapsed, - urldecode( $wgRequest->getRequestURL() . $forward ) ); - if ( '' != $wgDebugLogFile && ( $wgRequest->getVal('action') != 'raw' || $wgDebugRawPage ) ) { - wfErrorLog( $log . $prof, $wgDebugLogFile ); - } + global $wgProfiler, $wgUser; + if ( !isset( $wgProfiler ) ) + return; + + $now = wfTime(); + $elapsed = $now - $wgRequestTime; + $prof = wfGetProfilingOutput( $wgRequestTime, $elapsed ); + $forward = ''; + if( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) + $forward = ' forwarded for ' . $_SERVER['HTTP_X_FORWARDED_FOR']; + if( !empty( $_SERVER['HTTP_CLIENT_IP'] ) ) + $forward .= ' client IP ' . $_SERVER['HTTP_CLIENT_IP']; + if( !empty( $_SERVER['HTTP_FROM'] ) ) + $forward .= ' from ' . $_SERVER['HTTP_FROM']; + if( $forward ) + $forward = "\t(proxied via {$_SERVER['REMOTE_ADDR']}{$forward})"; + // Don't unstub $wgUser at this late stage just for statistics purposes + if( StubObject::isRealObject($wgUser) && $wgUser->isAnon() ) + $forward .= ' anon'; + $log = sprintf( "%s\t%04.3f\t%s\n", + gmdate( 'YmdHis' ), $elapsed, + urldecode( $wgRequest->getRequestURL() . $forward ) ); + if ( '' != $wgDebugLogFile && ( $wgRequest->getVal('action') != 'raw' || $wgDebugRawPage ) ) { + wfErrorLog( $log . $prof, $wgDebugLogFile ); } } @@ -282,6 +313,11 @@ function wfReadOnly() { return (bool)$wgReadOnly; } +function wfReadOnlyReason() { + global $wgReadOnly; + wfReadOnly(); + return $wgReadOnly; +} /** * Get a message from anywhere, for the current user language. @@ -291,9 +327,9 @@ function wfReadOnly() { * * @param $key String: lookup key for the message, usually * defined in languages/Language.php - * - * This function also takes extra optional parameters (not - * shown in the function definition), which can by used to + * + * This function also takes extra optional parameters (not + * shown in the function definition), which can by used to * insert variable text into the predefined message. */ function wfMsg( $key ) { @@ -415,24 +451,38 @@ function wfMsgWeirdKey ( $key ) { * Fetch a message string value, but don't replace any keys yet. * @param string $key * @param bool $useDB - * @param bool $forContent + * @param string $langcode Code of the language to get the message for, or + * behaves as a content language switch if it is a + * boolean. * @return string * @private */ -function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) { +function wfMsgGetKey( $key, $useDB, $langCode = false, $transform = true ) { global $wgParser, $wgContLang, $wgMessageCache, $wgLang; + wfRunHooks('NormalizeMessageKey', array(&$key, &$useDB, &$langCode, &$transform)); + # If $wgMessageCache isn't initialised yet, try to return something sensible. if( is_object( $wgMessageCache ) ) { - $message = $wgMessageCache->get( $key, $useDB, $forContent ); + $message = $wgMessageCache->get( $key, $useDB, $langCode ); if ( $transform ) { $message = $wgMessageCache->transform( $message ); } } else { - if( $forContent ) { + if( $langCode === true ) { $lang = &$wgContLang; - } else { + } elseif( $langCode === false ) { $lang = &$wgLang; + } else { + $validCodes = array_keys( Language::getLanguageNames() ); + if( in_array( $langCode, $validCodes ) ) { + # $langcode corresponds to a valid language. + $lang = Language::factory( $langCode ); + } else { + # $langcode is a string, but not a valid language code; use content language. + $lang =& $wgContLang; + wfDebug( 'Invalid language code passed to wfMsgGetKey, falling back to content language.' ); + } } # MessageCache::get() does this already, Language::getMessage() doesn't @@ -523,6 +573,8 @@ function wfMsgWikiHtml( $key ) { * <i>replaceafter</i>: parameters are substituted after parsing or escaping * <i>parsemag</i>: transform the message using magic phrases * <i>content</i>: fetch message for content language instead of interface + * <i>language</i>: language code to fetch message for (overriden by <i>content</i>), its behaviour + * with parser, parseinline and parsemag is undefined. * Behavior for conflicting options (e.g., parse+parseinline) is undefined. */ function wfMsgExt( $key, $options ) { @@ -536,12 +588,23 @@ function wfMsgExt( $key, $options ) { $options = array($options); } - $forContent = false; if( in_array('content', $options) ) { $forContent = true; + $langCode = true; + } elseif( array_key_exists('language', $options) ) { + $forContent = false; + $langCode = $options['language']; + $validCodes = array_keys( Language::getLanguageNames() ); + if( !in_array($options['language'], $validCodes) ) { + # Fallback to en, instead of whatever interface language we might have + $langCode = 'en'; + } + } else { + $forContent = false; + $langCode = false; } - $string = wfMsgGetKey( $key, /*DB*/true, $forContent, /*Transform*/false ); + $string = wfMsgGetKey( $key, /*DB*/true, $langCode, /*Transform*/false ); if( !in_array('replaceafter', $options) ) { $string = wfMsgReplaceArgs( $string, $args ); @@ -585,7 +648,6 @@ function wfMsgExt( $key, $options ) { * @deprecated Please return control to the caller or throw an exception */ function wfAbruptExit( $error = false ){ - global $wgLoadBalancer; static $called = false; if ( $called ){ exit( -1 ); @@ -606,7 +668,7 @@ function wfAbruptExit( $error = false ){ wfLogProfilingData(); if ( !$error ) { - $wgLoadBalancer->closeAll(); + wfGetLB()->closeAll(); } exit( -1 ); } @@ -629,7 +691,7 @@ function wfDie( $msg='' ) { } /** - * Throw a debugging exception. This function previously once exited the process, + * Throw a debugging exception. This function previously once exited the process, * but now throws an exception instead, with similar results. * * @param string $msg Message shown when dieing. @@ -848,7 +910,7 @@ function wfClientAcceptsGzip() { * @param $deflimit Default limit if none supplied * @param $optionname Name of a user preference to check against * @return array - * + * */ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) { global $wgRequest; @@ -947,7 +1009,20 @@ function wfArrayToCGI( $array1, $array2 = NULL ) if ( '' != $cgi ) { $cgi .= '&'; } - $cgi .= urlencode( $key ) . '=' . urlencode( $value ); + if(is_array($value)) + { + $firstTime = true; + foreach($value as $v) + { + $cgi .= ($firstTime ? '' : '&') . + urlencode( $key . '[]' ) . '=' . + urlencode( $v ); + $firstTime = false; + } + } + else + $cgi .= urlencode( $key ) . '=' . + urlencode( $value ); } } return $cgi; @@ -1104,6 +1179,68 @@ function wfMerge( $old, $mine, $yours, &$result ){ } /** + * 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' ) { + global $wgDiff; + + # This check may also protect against code injection in + # case of broken installations. + if( !file_exists( $wgDiff ) ){ + 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; +} + +/** * @todo document */ function wfVarDump( $var ) { @@ -1208,7 +1345,7 @@ function wfClearOutputBuffers() { function wfAcceptToPrefs( $accept, $def = '*/*' ) { # No arg means accept anything (per HTTP spec) if( !$accept ) { - return array( $def => 1 ); + return array( $def => 1.0 ); } $prefs = array(); @@ -1217,12 +1354,12 @@ function wfAcceptToPrefs( $accept, $def = '*/*' ) { foreach( $parts as $part ) { # FIXME: doesn't deal with params like 'text/html; level=1' - @list( $value, $qpart ) = explode( ';', $part ); + @list( $value, $qpart ) = explode( ';', trim( $part ) ); $match = array(); if( !isset( $qpart ) ) { - $prefs[$value] = 1; + $prefs[$value] = 1.0; } elseif( preg_match( '/q\s*=\s*(\d*\.\d+)/', $qpart, $match ) ) { - $prefs[$value] = $match[1]; + $prefs[$value] = floatval($match[1]); } } @@ -1415,41 +1552,35 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) { $uts=time(); } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D',$ts,$da)) { # TS_DB - $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], - (int)$da[2],(int)$da[3],(int)$da[1]); } elseif (preg_match('/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D',$ts,$da)) { # TS_EXIF - $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], - (int)$da[2],(int)$da[3],(int)$da[1]); } elseif (preg_match('/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D',$ts,$da)) { # TS_MW - $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], - (int)$da[2],(int)$da[3],(int)$da[1]); - } elseif (preg_match('/^(\d{1,13})$/D',$ts,$da)) { + } elseif (preg_match('/^\d{1,13}$/D',$ts)) { # TS_UNIX $uts = $ts; - } elseif (preg_match('/^(\d{1,2})-(...)-(\d\d(\d\d)?) (\d\d)\.(\d\d)\.(\d\d)/', $ts, $da)) { + } elseif (preg_match('/^\d{1,2}-...-\d\d(?:\d\d)? \d\d\.\d\d\.\d\d/', $ts)) { # TS_ORACLE $uts = strtotime(preg_replace('/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3", str_replace("+00:00", "UTC", $ts))); } elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/', $ts, $da)) { # TS_ISO_8601 - $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], - (int)$da[2],(int)$da[3],(int)$da[1]); } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)[\+\- ](\d\d)$/',$ts,$da)) { # TS_POSTGRES - $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], - (int)$da[2],(int)$da[3],(int)$da[1]); } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/',$ts,$da)) { # TS_POSTGRES - $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], - (int)$da[2],(int)$da[3],(int)$da[1]); } else { # Bogus value; fall back to the epoch... wfDebug("wfTimestamp() fed bogus time value: $outputtype; $ts\n"); $uts = 0; } + if (count( $da ) ) { + // Warning! gmmktime() acts oddly if the month or day is set to 0 + // We may want to handle that explicitly at some point + $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], + (int)$da[2],(int)$da[3],(int)$da[1]); + } switch($outputtype) { case TS_UNIX: @@ -1515,9 +1646,9 @@ function wfGetCachedNotice( $name ) { global $wgOut, $parserMemc; $fname = 'wfGetCachedNotice'; wfProfileIn( $fname ); - + $needParse = false; - + if( $name === 'default' ) { // special case global $wgSiteNotice; @@ -1533,7 +1664,7 @@ function wfGetCachedNotice( $name ) { return( false ); } } - + $cachedNotice = $parserMemc->get( wfMemcKey( $name ) ); if( is_array( $cachedNotice ) ) { if( md5( $notice ) == $cachedNotice['hash'] ) { @@ -1544,7 +1675,7 @@ function wfGetCachedNotice( $name ) { } else { $needParse = true; } - + if( $needParse ) { if( is_object( $wgOut ) ) { $parsed = $wgOut->parse( $notice ); @@ -1555,21 +1686,21 @@ function wfGetCachedNotice( $name ) { $notice = ''; } } - + wfProfileOut( $fname ); return $notice; } function wfGetNamespaceNotice() { global $wgTitle; - + # Paranoia if ( !isset( $wgTitle ) || !is_object( $wgTitle ) ) return ""; $fname = 'wfGetNamespaceNotice'; wfProfileIn( $fname ); - + $key = "namespacenotice-" . $wgTitle->getNsText(); $namespaceNotice = wfGetCachedNotice( $key ); if ( $namespaceNotice && substr ( $namespaceNotice , 0 ,7 ) != "<p><" ) { @@ -1586,8 +1717,8 @@ function wfGetSiteNotice() { global $wgUser, $wgSiteNotice; $fname = 'wfGetSiteNotice'; wfProfileIn( $fname ); - $siteNotice = ''; - + $siteNotice = ''; + if( wfRunHooks( 'SiteNoticeBefore', array( &$siteNotice ) ) ) { if( is_object( $wgUser ) && $wgUser->isLoggedIn() ) { $siteNotice = wfGetCachedNotice( 'sitenotice' ); @@ -1609,7 +1740,7 @@ function wfGetSiteNotice() { return $siteNotice; } -/** +/** * BC wrapper for MimeMagic::singleton() * @deprecated */ @@ -1640,13 +1771,70 @@ function wfTempDir() { /** * Make directory, and make all parent directories if they don't exist + * + * @param string $fullDir Full path to directory to create + * @param int $mode Chmod value to use, default is $wgDirectoryMode + * @return bool */ -function wfMkdirParents( $fullDir, $mode = 0777 ) { +function wfMkdirParents( $fullDir, $mode = null ) { + global $wgDirectoryMode; if( strval( $fullDir ) === '' ) return true; if( file_exists( $fullDir ) ) return true; - return mkdir( str_replace( '/', DIRECTORY_SEPARATOR, $fullDir ), $mode, true ); + // If not defined or isn't an int, set to default + if ( is_null( $mode ) ) { + $mode = $wgDirectoryMode; + } + + + # Go back through the paths to find the first directory that exists + $currentDir = $fullDir; + $createList = array(); + while ( strval( $currentDir ) !== '' && !file_exists( $currentDir ) ) { + # Strip trailing slashes + $currentDir = rtrim( $currentDir, '/\\' ); + + # Add to create list + $createList[] = $currentDir; + + # Find next delimiter searching from the end + $p = max( strrpos( $currentDir, '/' ), strrpos( $currentDir, '\\' ) ); + if ( $p === false ) { + $currentDir = false; + } else { + $currentDir = substr( $currentDir, 0, $p ); + } + } + + if ( count( $createList ) == 0 ) { + # Directory specified already exists + return true; + } elseif ( $currentDir === false ) { + # Went all the way back to root and it apparently doesn't exist + wfDebugLog( 'mkdir', "Root doesn't exist?\n" ); + return false; + } + # Now go forward creating directories + $createList = array_reverse( $createList ); + + # Is the parent directory writable? + if ( $currentDir === '' ) { + $currentDir = '/'; + } + if ( !is_writable( $currentDir ) ) { + wfDebugLog( 'mkdir', "Not writable: $currentDir\n" ); + return false; + } + + foreach ( $createList as $dir ) { + # use chmod to override the umask, as suggested by the PHP manual + if ( !mkdir( $dir, $mode ) || !chmod( $dir, $mode ) ) { + wfDebugLog( 'mkdir', "Unable to create directory $dir\n" ); + return false; + } + } + return true; } /** @@ -1654,7 +1842,7 @@ function wfMkdirParents( $fullDir, $mode = 0777 ) { */ function wfIncrStats( $key ) { global $wgStatsMethod; - + if( $wgStatsMethod == 'udp' ) { global $wgUDPProfilerHost, $wgUDPProfilerPort, $wgDBname; static $socket; @@ -1693,15 +1881,12 @@ function wfPercent( $nr, $acc = 2, $round = true ) { * @param string $userid ID of the user * @param string $password Password of the user * @return string Hashed password + * @deprecated Use User::crypt() or User::oldCrypt() instead */ function wfEncryptPassword( $userid, $password ) { - global $wgPasswordSalt; - $p = md5( $password); - - if($wgPasswordSalt) - return md5( "{$userid}-{$p}" ); - else - return $p; + wfDeprecated(__FUNCTION__); + # Just wrap around User::oldCrypt() + return User::oldCrypt($password, $userid); } /** @@ -1809,7 +1994,7 @@ function wfIniGetBool( $setting ) { */ function wfShellExec( $cmd, &$retval=null ) { global $IP, $wgMaxShellMemory, $wgMaxShellFileSize; - + if( wfIniGetBool( 'safe_mode' ) ) { wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" ); $retval = 1; @@ -1833,14 +2018,14 @@ function wfShellExec( $cmd, &$retval=null ) { $cmd = '"' . $cmd . '"'; } wfDebug( "wfShellExec: $cmd\n" ); - + $retval = 1; // error by default? ob_start(); passthru( $cmd, $retval ); $output = ob_get_contents(); ob_end_clean(); return $output; - + } /** @@ -1899,7 +2084,7 @@ function wfRegexReplacement( $string ) { * * PHP's basename() only considers '\' a pathchar on Windows and Netware. * We'll consider it so always, as we don't want \s in our Unix paths either. - * + * * @param string $path * @param string $suffix to remove if present * @return string @@ -1929,14 +2114,14 @@ function wfRelativePath( $path, $from ) { // Normalize mixed input on Windows... $path = str_replace( '/', DIRECTORY_SEPARATOR, $path ); $from = str_replace( '/', DIRECTORY_SEPARATOR, $from ); - + // Trim trailing slashes -- fix for drive root $path = rtrim( $path, DIRECTORY_SEPARATOR ); $from = rtrim( $from, DIRECTORY_SEPARATOR ); - + $pieces = explode( DIRECTORY_SEPARATOR, dirname( $path ) ); $against = explode( DIRECTORY_SEPARATOR, $from ); - + if( $pieces[0] !== $against[0] ) { // Non-matching Windows drive letters? // Return a full path. @@ -2002,7 +2187,7 @@ function wfMakeUrlIndex( $url ) { $delimiter = ':'; // parse_url detects for news: and mailto: the host part of an url as path // We have to correct this wrong detection - if ( isset ( $bits['path'] ) ) { + if ( isset ( $bits['path'] ) ) { $bits['host'] = $bits['path']; $bits['path'] = ''; } @@ -2099,7 +2284,7 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1, $lowercase=true $digitChars = ( $lowercase ) ? '0123456789abcdefghijklmnopqrstuvwxyz' : '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; $inDigits = array(); $outChars = ''; - + // Decode and validate input string $input = strtolower( $input ); for( $i = 0; $i < strlen( $input ); $i++ ) { @@ -2109,18 +2294,18 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1, $lowercase=true } $inDigits[] = $n; } - + // Iterate over the input, modulo-ing out an output digit // at a time until input is gone. while( count( $inDigits ) ) { $work = 0; $workDigits = array(); - + // Long division... foreach( $inDigits as $digit ) { $work *= $sourceBase; $work += $digit; - + if( $work < $destBase ) { // Gonna need to pull another digit. if( count( $workDigits ) ) { @@ -2132,26 +2317,26 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1, $lowercase=true } else { // Finally! Actual division! $workDigits[] = intval( $work / $destBase ); - + // Isn't it annoying that most programming languages // don't have a single divide-and-remainder operator, // even though the CPU implements it that way? $work = $work % $destBase; } } - + // All that division leaves us with a remainder, // which is conveniently our next output digit. $outChars .= $digitChars[$work]; - + // And we continue! $inDigits = $workDigits; } - + while( strlen( $outChars ) < $pad ) { $outChars .= '0'; } - + return strrev( $outChars ); } @@ -2183,20 +2368,44 @@ function wfCreateObject( $name, $p ){ } /** - * Aliases for modularized functions + * Alias for modularized function + * @deprecated Use Http::get() instead */ -function wfGetHTTP( $url, $timeout = 'default' ) { - return Http::get( $url, $timeout ); +function wfGetHTTP( $url, $timeout = 'default' ) { + wfDeprecated(__FUNCTION__); + return Http::get( $url, $timeout ); } -function wfIsLocalURL( $url ) { - return Http::isLocalURL( $url ); + +/** + * Alias for modularized function + * @deprecated Use Http::isLocalURL() instead + */ +function wfIsLocalURL( $url ) { + wfDeprecated(__FUNCTION__); + return Http::isLocalURL( $url ); +} + +function wfHttpOnlySafe() { + global $wgHttpOnlyBlacklist; + if( !version_compare("5.2", PHP_VERSION, "<") ) + return false; + + if( isset( $_SERVER['HTTP_USER_AGENT'] ) ) { + foreach( $wgHttpOnlyBlacklist as $regex ) { + if( preg_match( $regex, $_SERVER['HTTP_USER_AGENT'] ) ) { + return false; + } + } + } + + return true; } /** * Initialise php session */ function wfSetupSession() { - global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain, $wgCookieSecure; + global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly; if( $wgSessionsInMemcached ) { require_once( 'MemcachedSessions.php' ); } elseif( 'files' != ini_get( 'session.save_handler' ) ) { @@ -2204,9 +2413,25 @@ function wfSetupSession() { # application, it will end up failing. Try to recover. ini_set ( 'session.save_handler', 'files' ); } - session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure); + $httpOnlySafe = wfHttpOnlySafe(); + wfDebugLog( 'cookie', + 'session_set_cookie_params: "' . implode( '", "', + array( + 0, + $wgCookiePath, + $wgCookieDomain, + $wgCookieSecure, + $httpOnlySafe && $wgCookieHttpOnly ) ) . '"' ); + if( $httpOnlySafe && $wgCookieHttpOnly ) { + session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly ); + } else { + // PHP 5.1 throws warnings if you pass the HttpOnly parameter for 5.2. + session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure ); + } session_cache_limiter( 'private, must-revalidate' ); - @session_start(); + wfSuppressWarnings(); + session_start(); + wfRestoreWarnings(); } /** @@ -2253,13 +2478,8 @@ function wfFormatStackFrame($frame) { * Get a cache key */ function wfMemcKey( /*... */ ) { - global $wgDBprefix, $wgDBname; $args = func_get_args(); - if ( $wgDBprefix ) { - $key = "$wgDBname-$wgDBprefix:" . implode( ':', $args ); - } else { - $key = $wgDBname . ':' . implode( ':', $args ); - } + $key = wfWikiID() . ':' . implode( ':', $args ); return $key; } @@ -2280,42 +2500,80 @@ function wfForeignMemcKey( $db, $prefix /*, ... */ ) { * Get an ASCII string identifying this wiki * This is used as a prefix in memcached keys */ -function wfWikiID() { - global $wgDBprefix, $wgDBname; - if ( $wgDBprefix ) { - return "$wgDBname-$wgDBprefix"; +function wfWikiID( $db = null ) { + if( $db instanceof Database ) { + return $db->getWikiID(); } else { - return $wgDBname; + global $wgDBprefix, $wgDBname; + if ( $wgDBprefix ) { + return "$wgDBname-$wgDBprefix"; + } else { + return $wgDBname; + } } } +/** + * Split a wiki ID into DB name and table prefix + */ +function wfSplitWikiID( $wiki ) { + $bits = explode( '-', $wiki, 2 ); + if ( count( $bits ) < 2 ) { + $bits[] = ''; + } + return $bits; +} + /* - * Get a Database object - * @param integer $db Index of the connection to get. May be DB_MASTER for the - * master (for write queries), DB_SLAVE for potentially lagged + * Get a Database object. + * @param integer $db Index of the connection to get. May be DB_MASTER for the + * master (for write queries), DB_SLAVE for potentially lagged * read queries, or an integer >= 0 for a particular server. * - * @param mixed $groups Query groups. An array of group names that this query - * belongs to. May contain a single string if the query is only + * @param mixed $groups Query groups. An array of group names that this query + * belongs to. May contain a single string if the query is only * in one group. + * + * @param string $wiki The wiki ID, or false for the current wiki + * + * Note: multiple calls to wfGetDB(DB_SLAVE) during the course of one request + * will always return the same object, unless the underlying connection or load + * balancer is manually destroyed. */ -function &wfGetDB( $db = DB_LAST, $groups = array() ) { - global $wgLoadBalancer; - $ret = $wgLoadBalancer->getConnection( $db, true, $groups ); - return $ret; +function &wfGetDB( $db = DB_LAST, $groups = array(), $wiki = false ) { + return wfGetLB( $wiki )->getConnection( $db, $groups, $wiki ); } /** - * Find a file. + * Get a load balancer object. + * + * @param array $groups List of query groups + * @param string $wiki Wiki ID, or false for the current wiki + * @return LoadBalancer + */ +function wfGetLB( $wiki = false ) { + return wfGetLBFactory()->getMainLB( $wiki ); +} + +/** + * Get the load balancer factory object + */ +function &wfGetLBFactory() { + return LBFactory::singleton(); +} + +/** + * Find a file. * Shortcut for RepoGroup::singleton()->findFile() * @param mixed $title Title object or string. May be interwiki. - * @param mixed $time Requested time for an archived image, or false for the - * current version. An image object will be returned which - * existed at the specified time. + * @param mixed $time Requested time for an archived image, or false for the + * current version. An image object will be returned which + * was created at the specified time. + * @param mixed $flags FileRepo::FIND_ flags * @return File, or false if the file does not exist */ -function wfFindFile( $title, $time = false ) { - return RepoGroup::singleton()->findFile( $title, $time ); +function wfFindFile( $title, $time = false, $flags = 0 ) { + return RepoGroup::singleton()->findFile( $title, $time, $flags ); } /** @@ -2364,13 +2622,39 @@ function wfBoolToStr( $value ) { /** * Load an extension messages file + * + * @param string $extensionName Name of extension to load messages from\for. + * @param string $langcode Language to load messages for, or false for default + * behvaiour (en, content language and user language). */ -function wfLoadExtensionMessages( $extensionName ) { - global $wgExtensionMessagesFiles, $wgMessageCache; - if ( !empty( $wgExtensionMessagesFiles[$extensionName] ) ) { - $wgMessageCache->loadMessagesFile( $wgExtensionMessagesFiles[$extensionName] ); - // Prevent double-loading - $wgExtensionMessagesFiles[$extensionName] = false; +function wfLoadExtensionMessages( $extensionName, $langcode = false ) { + global $wgExtensionMessagesFiles, $wgMessageCache, $wgLang, $wgContLang; + + #For recording whether extension message files have been loaded in a given language. + static $loaded = array(); + + if( !array_key_exists( $extensionName, $loaded ) ) { + $loaded[$extensionName] = array(); + } + + if ( !isset($wgExtensionMessagesFiles[$extensionName]) ) { + throw new MWException( "Messages file for extensions $extensionName is not defined" ); + } + + if( !$langcode && !array_key_exists( '*', $loaded[$extensionName] ) ) { + # Just do en, content language and user language. + $wgMessageCache->loadMessagesFile( $wgExtensionMessagesFiles[$extensionName], false ); + # Mark that they have been loaded. + $loaded[$extensionName]['en'] = true; + $loaded[$extensionName][$wgLang->getCode()] = true; + $loaded[$extensionName][$wgContLang->getCode()] = true; + # Mark that this part has been done to avoid weird if statements. + $loaded[$extensionName]['*'] = true; + } elseif( is_string( $langcode ) && !array_key_exists( $langcode, $loaded[$extensionName] ) ) { + # Load messages for specified language. + $wgMessageCache->loadMessagesFile( $wgExtensionMessagesFiles[$extensionName], $langcode ); + # Mark that they have been loaded. + $loaded[$extensionName][$langcode] = true; } } @@ -2388,7 +2672,7 @@ function wfGetNull() { /** * Displays a maxlag error - * + * * @param string $host Server that lags the most * @param int $lag Maxlag (actual) * @param int $maxLag Maxlag (requested) @@ -2405,3 +2689,81 @@ function wfMaxlagError( $host, $lag, $maxLag ) { echo "Waiting for a database server: $lag seconds lagged\n"; } } + +/** + * Throws an E_USER_NOTICE saying that $function is deprecated + * @param string $function + * @return null + */ +function wfDeprecated( $function ) { + global $wgDebugLogFile; + if ( !$wgDebugLogFile ) { + return; + } + $callers = wfDebugBacktrace(); + if( isset( $callers[2] ) ){ + $callerfunc = $callers[2]; + $callerfile = $callers[1]; + 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'] . '::'; + $func .= @$callerfunc['function']; + $msg = "Use of $function is deprecated. Called from $func in $file"; + } else { + $msg = "Use of $function is deprecated."; + } + wfDebug( "$msg\n" ); +} + +/** + * Sleep until the worst slave's replication lag is less than or equal to + * $maxLag, in seconds. Use this when updating very large numbers of rows, as + * in maintenance scripts, to avoid causing too much lag. Of course, this is + * a no-op if there are no slaves. + * + * Every time the function has to wait for a slave, it will print a message to + * that effect (and then sleep for a little while), so it's probably not best + * to use this outside maintenance scripts in its present form. + * + * @param int $maxLag + * @return null + */ +function wfWaitForSlaves( $maxLag ) { + if( $maxLag ) { + $lb = wfGetLB(); + list( $host, $lag ) = $lb->getMaxLag(); + while( $lag > $maxLag ) { + $name = @gethostbyaddr( $host ); + if( $name !== false ) { + $host = $name; + } + print "Waiting for $host (lagged $lag seconds)...\n"; + sleep($maxLag); + list( $host, $lag ) = $lb->getMaxLag(); + } + } +} + +/** Generate a random 32-character hexadecimal token. + * @param mixed $salt Some sort of salt, if necessary, to add to random characters before hashing. + */ +function wfGenerateToken( $salt = '' ) { + $salt = serialize($salt); + + return md5( mt_rand( 0, 0x7fffffff ) . $salt ); +} + +/** + * Replace all invalid characters with - + * @param mixed $title Filename to process + */ +function wfStripIllegalFilenameChars( $name ) { + $name = wfBaseName( $name ); + $name = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $name ); + return $name; +} diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php index 050005dd..1f250214 100644 --- a/includes/HTMLCacheUpdate.php +++ b/includes/HTMLCacheUpdate.php @@ -5,27 +5,28 @@ * Small numbers of links will be done immediately, large numbers are pushed onto * the job queue. * - * This class is designed to work efficiently with small numbers of links, and + * This class is designed to work efficiently with small numbers of links, and * to work reasonably well with up to ~10^5 links. Above ~10^6 links, the memory * and time requirements of loading all backlinked IDs in doUpdate() might become * prohibitive. The requirements measured at Wikimedia are approximately: - * + * * memory: 48 bytes per row * time: 16us per row for the query plus processing * * The reason this query is done is to support partitioning of the job - * by backlinked ID. The memory issue could be allieviated by doing this query in + * by backlinked ID. The memory issue could be allieviated by doing this query in * batches, but of course LIMIT with an offset is inefficient on the DB side. * - * The class is nevertheless a vast improvement on the previous method of using + * The class is nevertheless a vast improvement on the previous method of using * Image::getLinksTo() and Title::touchArray(), which uses about 2KB of memory per * link. + * + * @ingroup Cache */ class HTMLCacheUpdate { public $mTitle, $mTable, $mPrefix; public $mRowsPerJob, $mRowsPerQuery; - public $mResult; function __construct( $titleTo, $table ) { global $wgUpdateRowsPerJob, $wgUpdateRowsPerQuery; @@ -41,7 +42,7 @@ class HTMLCacheUpdate $cond = $this->getToCondition(); $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( $this->mTable, $this->getFromField(), $cond, __METHOD__ ); - $this->mResult = $res; + if ( $dbr->numRows( $res ) != 0 ) { if ( $dbr->numRows( $res ) > $this->mRowsPerJob ) { $this->insertJobs( $res ); @@ -67,7 +68,7 @@ class HTMLCacheUpdate break; } } - + $params = array( 'table' => $this->mTable, 'start' => $start, @@ -88,7 +89,7 @@ class HTMLCacheUpdate 'categorylinks' => 'cl', 'templatelinks' => 'tl', 'redirect' => 'rd', - + # Not needed # 'externallinks' => 'el', # 'langlinks' => 'll' @@ -102,7 +103,7 @@ class HTMLCacheUpdate } return $this->mPrefix; } - + function getFromField() { return $this->getPrefix() . '_from'; } @@ -113,7 +114,7 @@ class HTMLCacheUpdate case 'pagelinks': case 'templatelinks': case 'redirect': - return array( + return array( "{$prefix}_namespace" => $this->mTitle->getNamespace(), "{$prefix}_title" => $this->mTitle->getDBkey() ); @@ -138,7 +139,7 @@ class HTMLCacheUpdate $dbw = wfGetDB( DB_MASTER ); $timestamp = $dbw->timestamp(); $done = false; - + while ( !$done ) { # Get all IDs in this query into an array $ids = array(); @@ -155,10 +156,10 @@ class HTMLCacheUpdate if ( !count( $ids ) ) { break; } - + # Update page_touched - $dbw->update( 'page', - array( 'page_touched' => $timestamp ), + $dbw->update( 'page', + array( 'page_touched' => $timestamp ), array( 'page_id IN (' . $dbw->makeList( $ids ) . ')' ), __METHOD__ ); @@ -185,6 +186,7 @@ class HTMLCacheUpdate /** * @todo document (e.g. one-sentence top-level class description). + * @ingroup JobQueue */ class HTMLCacheUpdateJob extends Job { var $table, $start, $end; @@ -216,9 +218,8 @@ class HTMLCacheUpdateJob extends Job { $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( $this->table, $fromField, $conds, __METHOD__ ); - $update->invalidateIDs( new ResultWrapper( $dbr, $res ) ); + $update->invalidateIDs( $res ); return true; } } - diff --git a/includes/HTMLFileCache.php b/includes/HTMLFileCache.php index a7466814..ba2196eb 100644 --- a/includes/HTMLFileCache.php +++ b/includes/HTMLFileCache.php @@ -1,7 +1,8 @@ <?php /** * Contain the HTMLFileCache class - * @addtogroup Cache + * @file + * @ingroup Cache */ /** @@ -10,11 +11,13 @@ * emergency abort/fallback to cache. * * Global options that affect this module: - * $wgCachePages - * $wgCacheEpoch - * $wgUseFileCache - * $wgFileCacheDirectory - * $wgUseGzip + * - $wgCachePages + * - $wgCacheEpoch + * - $wgUseFileCache + * - $wgFileCacheDirectory + * - $wgUseGzip + * + * @ingroup Cache */ class HTMLFileCache { var $mTitle, $mFileCache; @@ -153,5 +156,3 @@ class HTMLFileCache { } } - - diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php index 984ee2d4..3772926d 100644 --- a/includes/HistoryBlob.php +++ b/includes/HistoryBlob.php @@ -1,11 +1,8 @@ <?php -/** - * - */ /** * Pure virtual parent - * @todo document (needs a one-sentence top-level class description, that answers the question: "what is a HistoryBlob?") + * @todo document (needs a one-sentence top-level class description, that answers the question: "what is a HistoryBlob?") */ interface HistoryBlob { @@ -308,6 +305,3 @@ class HistoryBlobCurStub { return $row->cur_text; } } - - - diff --git a/includes/Hooks.php b/includes/Hooks.php index 20103db4..046a149d 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -19,6 +19,7 @@ * * @author Evan Prodromou <evan@wikitravel.org> * @see hooks.txt + * @file */ @@ -27,7 +28,7 @@ * careful about its contents. So, there's a lot more error-checking * in here than would normally be necessary. */ -function wfRunHooks($event, $args = null) { +function wfRunHooks($event, $args = array()) { global $wgHooks; @@ -108,6 +109,9 @@ function wfRunHooks($event, $args = null) { $callback = $func; } + // Run autoloader (workaround for call_user_func_array bug) + is_callable( $callback ); + /* Call the hook. */ wfProfileIn( $func ); $retval = call_user_func_array( $callback, $hook_args ); @@ -140,4 +144,3 @@ function wfRunHooks($event, $args = null) { return true; } - diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index 6ea3abd0..555a79b7 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -24,7 +24,7 @@ class Http { # Use curl if available if ( function_exists( 'curl_init' ) ) { $c = curl_init( $url ); - if ( wfIsLocalURL( $url ) ) { + if ( self::isLocalURL( $url ) ) { curl_setopt( $c, CURLOPT_PROXY, 'localhost:80' ); } else if ($wgHTTPProxy) { curl_setopt($c, CURLOPT_PROXY, $wgHTTPProxy); @@ -118,4 +118,3 @@ class Http { return false; } } - diff --git a/includes/IP.php b/includes/IP.php index db712c3b..e76f66c1 100644 --- a/includes/IP.php +++ b/includes/IP.php @@ -48,7 +48,7 @@ class IP { // IPv6 IPs with two "::" strings are ambiguous and thus invalid return preg_match( '/^' . IP_ADDRESS_STRING . '$/', $ip) && ( substr_count($ip, '::') < 2 ); } - + public static function isIPv6( $ip ) { if ( !$ip ) return false; if( is_array( $ip ) ) { @@ -57,18 +57,18 @@ class IP { // IPv6 IPs with two "::" strings are ambiguous and thus invalid return preg_match( '/^' . RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)$/', $ip) && ( substr_count($ip, '::') < 2); } - + public static function isIPv4( $ip ) { if ( !$ip ) return false; return preg_match( '/^' . RE_IP_ADD . '(\/' . RE_IP_PREFIX . '|)$/', $ip); } - + /** * Given an IP address in dotted-quad notation, returns an IPv6 octet. * See http://www.answers.com/topic/ipv4-compatible-address * IPs with the first 92 bits as zeros are reserved from IPv6 * @param $ip quad-dotted IP address. - * @return string + * @return string */ public static function IPv4toIPv6( $ip ) { if ( !$ip ) return null; @@ -106,13 +106,13 @@ class IP { $r_ip = wfBaseConvert( $r_ip, 16, 10 ); return $r_ip; } - + /** * Given an IPv6 address in octet notation, returns the expanded octet. * IPv4 IPs will be trimmed, thats it... * @param $ip octet ipv6 IP address. - * @return string - */ + * @return string + */ public static function sanitizeIP( $ip ) { $ip = trim( $ip ); if ( $ip === '' ) return null; @@ -132,11 +132,11 @@ class IP { $ip = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip ); return $ip; } - + /** * Given an unsigned integer, returns an IPv6 address in octet notation * @param $ip integer IP address. - * @return string + * @return string */ public static function toOctet( $ip_int ) { // Convert to padded uppercase hex @@ -181,9 +181,9 @@ class IP { } return array( $network, $bits ); } - + /** - * Given a string range in a number of formats, return the start and end of + * Given a string range in a number of formats, return the start and end of * the range in hexadecimal. For IPv6. * * Formats are: @@ -233,7 +233,7 @@ class IP { return array( $start, $end ); } } - + /** * Validate an IP address. * @return boolean True if it is valid. @@ -310,7 +310,7 @@ class IP { * Return a zero-padded hexadecimal representation of an IP address. * * Hexadecimal addresses are used because they can easily be extended to - * IPv6 support. To separate the ranges, the return value from this + * IPv6 support. To separate the ranges, the return value from this * function for an IPv6 address will be prefixed with "v6-", a non- * hexadecimal string which sorts after the IPv4 addresses. * @@ -396,14 +396,14 @@ class IP { } /** - * Given a string range in a number of formats, return the start and end of + * Given a string range in a number of formats, return the start and end of * the range in hexadecimal. * * Formats are: * 1.2.3.4/24 CIDR * 1.2.3.4 - 1.2.3.5 Explicit range * 1.2.3.4 Single IP - * + * * 2001:0db8:85a3::7344/96 CIDR * 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range * 2001:0db8:85a3::7344 Single IP @@ -439,7 +439,7 @@ class IP { } if ( $start === false || $end === false ) { return array( false, false ); - } else { + } else { return array( $start, $end ); } } @@ -492,5 +492,3 @@ class IP { return null; // give up } } - - diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php index 3e87c994..af05c1c9 100644 --- a/includes/ImageFunctions.php +++ b/includes/ImageFunctions.php @@ -53,10 +53,10 @@ function wfGetSVGsize( $filename ) { return false; } $tag = $matches[1]; - if( preg_match( '/\bwidth\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) { + if( preg_match( '/(?:^|\s)width\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) { $width = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) ); } - if( preg_match( '/\bheight\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) { + if( preg_match( '/(?:^|\s)height\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) { $height = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) ); } @@ -73,8 +73,8 @@ function wfGetSVGsize( $filename ) { * * Any subsequent links on the same line are considered to be exceptions, * i.e. articles where the image may occur inline. * - * @param string $name the image name to check - * @param Title $contextTitle The page on which the image occurs, if known + * @param $name string the image name to check + * @param $contextTitle Title: the page on which the image occurs, if known * @return bool */ function wfIsBadImage( $name, $contextTitle = false ) { @@ -122,7 +122,7 @@ function wfIsBadImage( $name, $contextTitle = false ) { } } } - + $contextKey = $contextTitle ? $contextTitle->getPrefixedDBkey() : false; $bad = isset( $badImages[$name] ) && !isset( $badImages[$name][$contextKey] ); wfProfileOut( __METHOD__ ); @@ -145,6 +145,3 @@ function wfFitBoxWidth( $boxWidth, $boxHeight, $maxHeight ) { else return $roundedUp; } - - - diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php index 46ecd169..492a3e06 100644 --- a/includes/ImageGallery.php +++ b/includes/ImageGallery.php @@ -3,14 +3,11 @@ if ( ! defined( 'MEDIAWIKI' ) ) die( 1 ); /** - */ - -/** * Image gallery * * Add images to the gallery using add(), then render that list to HTML using toHTML(). * - * @addtogroup Media + * @ingroup Media */ class ImageGallery { @@ -37,7 +34,7 @@ class ImageGallery private $mPerRow = 4; // How many images wide should the gallery be? private $mWidths = 120, $mHeights = 120; // How wide/tall each thumbnail should be - + private $mAttribs = array(); /** @@ -196,11 +193,11 @@ class ImageGallery function setShowFilename( $f ) { $this->mShowFilename = ( $f == true); } - + /** * Set arbitrary attributes to go on the HTML gallery output element. * Should be suitable for a <table> element. - * + * * Note -- if taking from user input, you should probably run through * Sanitizer::validateAttributes() first. * @@ -240,10 +237,10 @@ class ImageGallery foreach ( $this->mImages as $pair ) { $nt = $pair[0]; $text = $pair[1]; - + # Give extensions a chance to select the file revision for us - $time = false; - wfRunHooks( 'BeforeGalleryFindFile', array( &$this, &$nt, &$time ) ); + $time = $descQuery = false; + wfRunHooks( 'BeforeGalleryFindFile', array( &$this, &$nt, &$time, &$descQuery ) ); $img = wfFindFile( $nt, $time ); @@ -261,14 +258,14 @@ class ImageGallery . htmlspecialchars( $img->getLastError() ) . '</div>'; } else { $vpad = floor( ( 1.25*$this->mHeights - $thumb->height ) /2 ) - 2; - + $thumbhtml = "\n\t\t\t". '<div class="thumb" style="padding: ' . $vpad . 'px 0; width: ' .($this->mWidths+30).'px;">' # Auto-margin centering for block-level elements. Needed now that we have video # handlers since they may emit block-level elements as opposed to simple <img> tags. # ref http://css-discuss.incutio.com/?page=CenteringBlockElement . '<div style="margin-left: auto; margin-right: auto; width: ' .$this->mWidths.'px;">' - . $thumb->toHtml( array( 'desc-link' => true ) ) . '</div></div>'; + . $thumb->toHtml( array( 'desc-link' => true, 'desc-query' => $descQuery ) ) . '</div></div>'; // Call parser transform hook if ( $this->mParser && $img->getHandler() ) { @@ -277,7 +274,7 @@ class ImageGallery } //TODO - //$ul = $sk->makeLink( $wgContLang->getNsText( Namespace::getUser() ) . ":{$ut}", $ut ); + //$ul = $sk->makeLink( $wgContLang->getNsText( MWNamespace::getUser() ) . ":{$ut}", $ut ); if( $this->mShowBytes ) { if( $img ) { @@ -328,7 +325,7 @@ class ImageGallery public function count() { return count( $this->mImages ); } - + /** * Set the contextual title * @@ -337,7 +334,7 @@ class ImageGallery public function setContextTitle( $title ) { $this->contextTitle = $title; } - + /** * Get the contextual title, if applicable * @@ -350,5 +347,3 @@ class ImageGallery } } //class - - diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 573bc4d7..30fcf13e 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -1,34 +1,46 @@ <?php -/** - */ -/** - * - */ if( !defined( 'MEDIAWIKI' ) ) die( 1 ); /** * Special handling for image description pages * - * @addtogroup Media + * @ingroup Media */ class ImagePage extends Article { - /* private */ var $img; // Image object this page is shown for + /* private */ var $img; // Image object + /* private */ var $displayImg; /* private */ var $repo; + /* private */ var $fileLoaded; var $mExtraDescription = false; + var $dupes; - function __construct( $title, $time = false ) { + function __construct( $title ) { parent::__construct( $title ); - $this->img = wfFindFile( $this->mTitle, $time ); + $this->dupes = null; + $this->repo = null; + } + + protected function loadFile() { + if ( $this->fileLoaded ) { + return true; + } + $this->fileLoaded = true; + + $this->displayImg = $this->img = false; + wfRunHooks( 'ImagePageFindFile', array( $this, &$this->img, &$this->displayImg ) ); if ( !$this->img ) { - $this->img = wfLocalFile( $this->mTitle ); - $this->current = $this->img; - } else { - $this->current = $time ? wfLocalFile( $this->mTitle ) : $this->img; + $this->img = wfFindFile( $this->mTitle ); + if ( !$this->img ) { + $this->img = wfLocalFile( $this->mTitle ); + } + } + if ( !$this->displayImg ) { + $this->displayImg = $this->img; } - $this->repo = $this->img->repo; + $this->repo = $this->img->getRepo(); } /** @@ -38,11 +50,28 @@ class ImagePage extends Article { function render() { global $wgOut; $wgOut->setArticleBodyOnly( true ); - $wgOut->addSecondaryWikitext( $this->getContent() ); + parent::view(); } function view() { global $wgOut, $wgShowEXIF, $wgRequest, $wgUser; + $this->loadFile(); + + if ( $this->mTitle->getNamespace() == NS_IMAGE && $this->img->getRedirected() ) { + if ( $this->mTitle->getDBkey() == $this->img->getName() ) { + // mTitle is the same as the redirect target so ask Article + // to perform the redirect for us. + return Article::view(); + } else { + // mTitle is not the same as the redirect target so it is + // probably the redirect page itself. Fake the redirect symbol + $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + $this->viewRedirect( Title::makeTitle( NS_IMAGE, $this->img->getName() ), + /* $appendSubtitle */ true, /* $forceKnown */ true ); + $this->viewUpdates(); + return; + } + } $diff = $wgRequest->getVal( 'diff' ); $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); @@ -50,16 +79,16 @@ class ImagePage extends Article { if ( $this->mTitle->getNamespace() != NS_IMAGE || ( isset( $diff ) && $diffOnly ) ) return Article::view(); - if ($wgShowEXIF && $this->img->exists()) { - // FIXME: bad interface, see note on MediaHandler::formatMetadata(). - $formattedMetadata = $this->img->formatMetadata(); + if ( $wgShowEXIF && $this->displayImg->exists() ) { + // FIXME: bad interface, see note on MediaHandler::formatMetadata(). + $formattedMetadata = $this->displayImg->formatMetadata(); $showmeta = $formattedMetadata !== false; } else { $showmeta = false; } - if ($this->img->exists()) - $wgOut->addHTML($this->showTOC($showmeta)); + if ( $this->displayImg->exists() ) + $wgOut->addHTML( $this->showTOC($showmeta) ); $this->openShowImage(); @@ -81,10 +110,23 @@ class ImagePage extends Article { $wgOut->addWikiText( $fol ); } $wgOut->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . '</div>' ); + } else { + $this->checkSharedConflict(); } $this->closeShowImage(); $this->imageHistory(); + // TODO: Cleanup the following + + $wgOut->addHTML( Xml::element( 'h2', + array( 'id' => 'filelinks' ), + wfMsg( 'imagelinks' ) ) . "\n" ); + $this->imageDupes(); + // TODO: We may want to find local images redirecting to a foreign + // file: "The following local files redirect to this file" + if ( $this->img->isLocal() ) { + $this->imageRedirects(); + } $this->imageLinks(); if ( $showmeta ) { @@ -93,11 +135,83 @@ class ImagePage extends Article { $collapse = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-collapse' ) ) ); $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ). "\n" ); $wgOut->addWikiText( $this->makeMetadataTable( $formattedMetadata ) ); + $wgOut->addScriptFile( 'metadata.js' ); $wgOut->addHTML( - "<script type=\"text/javascript\" src=\"$wgStylePath/common/metadata.js?$wgStyleVersion\"></script>\n" . "<script type=\"text/javascript\">attachMetadataToggle('mw_metadata', '$expand', '$collapse');</script>\n" ); } } + + public function getRedirectTarget() { + $this->loadFile(); + if ( $this->img->isLocal() ) { + return parent::getRedirectTarget(); + } + // Foreign image page + $from = $this->img->getRedirected(); + $to = $this->img->getName(); + if ( $from == $to ) { + return null; + } + return $this->mRedirectTarget = Title::makeTitle( NS_IMAGE, $to ); + } + public function followRedirect() { + $this->loadFile(); + if ( $this->img->isLocal() ) { + return parent::followRedirect(); + } + $from = $this->img->getRedirected(); + $to = $this->img->getName(); + if ( $from == $to ) { + return false; + } + return Title::makeTitle( NS_IMAGE, $to ); + } + public function isRedirect( $text = false ) { + $this->loadFile(); + if ( $this->img->isLocal() ) + return parent::isRedirect( $text ); + + return (bool)$this->img->getRedirected(); + } + + public function isLocal() { + $this->loadFile(); + return $this->img->isLocal(); + } + + public function getFile() { + $this->loadFile(); + return $this->img; + } + + public function getDisplayedFile() { + $this->loadFile(); + return $this->displayImg; + } + + public function getDuplicates() { + $this->loadFile(); + if ( !is_null($this->dupes) ) { + return $this->dupes; + } + if ( !( $hash = $this->img->getSha1() ) ) { + return $this->dupes = array(); + } + $dupes = RepoGroup::singleton()->findBySha1( $hash ); + // Remove duplicates with self and non matching file sizes + $self = $this->img->getRepoName().':'.$this->img->getName(); + $size = $this->img->getSize(); + foreach ( $dupes as $index => $file ) { + $key = $file->getRepoName().':'.$file->getName(); + if ( $key == $self ) + unset( $dupes[$index] ); + if ( $file->getSize() != $size ) + unset( $dupes[$index] ); + } + return $this->dupes = $dupes; + + } + /** * Create the TOC @@ -121,7 +235,7 @@ class ImagePage extends Article { /** * Make a table with metadata to be shown in the output page. * - * FIXME: bad interface, see note on MediaHandler::formatMetadata(). + * FIXME: bad interface, see note on MediaHandler::formatMetadata(). * * @access private * @@ -148,11 +262,12 @@ class ImagePage extends Article { /** * Overloading Article's getContent method. - * + * * Omit noarticletext if sharedupload; text will be fetched from the * shared upload server if possible. */ function getContent() { + $this->loadFile(); if( $this->img && !$this->img->isLocal() && 0 == $this->getID() ) { return ''; } @@ -162,7 +277,9 @@ class ImagePage extends Article { function openShowImage() { global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang, $wgContLang; - $full_url = $this->img->getURL(); + $this->loadFile(); + + $full_url = $this->displayImg->getURL(); $linkAttribs = false; $sizeSel = intval( $wgUser->getOption( 'imagesize') ); if( !isset( $wgImageLimits[$sizeSel] ) ) { @@ -181,7 +298,7 @@ class ImagePage extends Article { $sk = $wgUser->getSkin(); $dirmark = $wgContLang->getDirMark(); - if ( $this->img->exists() ) { + if ( $this->displayImg->exists() ) { # image $page = $wgRequest->getIntOrNull( 'page' ); if ( is_null( $page ) ) { @@ -190,22 +307,22 @@ class ImagePage extends Article { } else { $params = array( 'page' => $page ); } - $width_orig = $this->img->getWidth(); + $width_orig = $this->displayImg->getWidth(); $width = $width_orig; - $height_orig = $this->img->getHeight(); + $height_orig = $this->displayImg->getHeight(); $height = $height_orig; - $mime = $this->img->getMimeType(); + $mime = $this->displayImg->getMimeType(); $showLink = false; $linkAttribs = array( 'href' => $full_url ); - $longDesc = $this->img->getLongDesc(); + $longDesc = $this->displayImg->getLongDesc(); wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this , &$wgOut ) ) ; - if ( $this->img->allowInlineDisplay() ) { + if ( $this->displayImg->allowInlineDisplay() ) { # image # "Download high res version" link below the image - #$msgsize = wfMsgHtml('file-info-size', $width_orig, $height_orig, $sk->formatSize( $this->img->getSize() ), $mime ); + #$msgsize = wfMsgHtml('file-info-size', $width_orig, $height_orig, $sk->formatSize( $this->displayImg->getSize() ), $mime ); # We'll show a thumbnail of this image if ( $width > $maxWidth || $height > $maxHeight ) { # Calculate the thumbnail size. @@ -226,43 +343,43 @@ class ImagePage extends Article { array( 'parseinline' ), $wgLang->formatNum( $width ), $wgLang->formatNum( $height ) ); } else { # Image is small enough to show full size on image page - $msgbig = htmlspecialchars( $this->img->getName() ); + $msgbig = htmlspecialchars( $this->displayImg->getName() ); $msgsmall = wfMsgExt( 'file-nohires', array( 'parseinline' ) ); } $params['width'] = $width; - $thumbnail = $this->img->transform( $params ); + $thumbnail = $this->displayImg->transform( $params ); $anchorclose = "<br />"; - if( $this->img->mustRender() ) { + if( $this->displayImg->mustRender() ) { $showLink = true; } else { - $anchorclose .= + $anchorclose .= $msgsmall . '<br />' . Xml::tags( 'a', $linkAttribs, $msgbig ) . "$dirmark " . $longDesc; } - if ( $this->img->isMultipage() ) { + if ( $this->displayImg->isMultipage() ) { $wgOut->addHTML( '<table class="multipageimage"><tr><td>' ); } if ( $thumbnail ) { - $options = array( - 'alt' => $this->img->getTitle()->getPrefixedText(), + $options = array( + 'alt' => $this->displayImg->getTitle()->getPrefixedText(), 'file-link' => true, ); - $wgOut->addHTML( '<div class="fullImageLink" id="file">' . + $wgOut->addHTML( '<div class="fullImageLink" id="file">' . $thumbnail->toHtml( $options ) . $anchorclose . '</div>' ); } - if ( $this->img->isMultipage() ) { - $count = $this->img->pageCount(); + if ( $this->displayImg->isMultipage() ) { + $count = $this->displayImg->pageCount(); if ( $page > 1 ) { $label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false ); $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page-1) ); - $thumb1 = $sk->makeThumbLinkObj( $this->mTitle, $this->img, $link, $label, 'none', + $thumb1 = $sk->makeThumbLinkObj( $this->mTitle, $this->displayImg, $link, $label, 'none', array( 'page' => $page - 1 ) ); } else { $thumb1 = ''; @@ -271,34 +388,42 @@ class ImagePage extends Article { if ( $page < $count ) { $label = wfMsg( 'imgmultipagenext' ); $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page+1) ); - $thumb2 = $sk->makeThumbLinkObj( $this->mTitle, $this->img, $link, $label, 'none', + $thumb2 = $sk->makeThumbLinkObj( $this->mTitle, $this->displayImg, $link, $label, 'none', array( 'page' => $page + 1 ) ); } else { $thumb2 = ''; } global $wgScript; - $select = '<form name="pageselector" action="' . - htmlspecialchars( $wgScript ) . - '" method="get" onchange="document.pageselector.submit();">' . - Xml::hidden( 'title', $this->getTitle()->getPrefixedDbKey() ); - $select .= $wgOut->parse( wfMsg( 'imgmultigotopre' ), false ) . - ' <select id="pageselector" name="page">'; + + $formParams = array( + 'name' => 'pageselector', + 'action' => $wgScript, + 'onchange' => 'document.pageselector.submit();', + ); + + $option = array(); for ( $i=1; $i <= $count; $i++ ) { - $select .= Xml::option( $wgLang->formatNum( $i ), $i, - $i == $page ); + $options[] = Xml::option( $wgLang->formatNum($i), $i, $i == $page ); } - $select .= '</select>' . $wgOut->parse( wfMsg( 'imgmultigotopost' ), false ) . - '<input type="submit" value="' . - htmlspecialchars( wfMsg( 'imgmultigo' ) ) . '"></form>'; - - $wgOut->addHTML( '</td><td><div class="multipageimagenavbox">' . - "$select<hr />$thumb1\n$thumb2<br clear=\"all\" /></div></td></tr></table>" ); + $select = Xml::tags( 'select', + array( 'id' => 'pageselector', 'name' => 'page' ), + implode( "\n", $options ) ); + + $wgOut->addHTML( + '</td><td><div class="multipageimagenavbox">' . + Xml::openElement( 'form', $formParams ) . + Xml::hidden( 'title', $this->getTitle()->getPrefixedDbKey() ) . + wfMsgExt( 'imgmultigoto', array( 'parseinline', 'replaceafter' ), $select ) . + Xml::submitButton( wfMsg( 'imgmultigo' ) ) . + Xml::closeElement( 'form' ) . + "<hr />$thumb1\n$thumb2<br clear=\"all\" /></div></td></tr></table>" + ); } } else { #if direct link is allowed but it's not a renderable image, show an icon. - if ($this->img->isSafeFile()) { - $icon= $this->img->iconThumb(); + if ( $this->displayImg->isSafeFile() ) { + $icon= $this->displayImg->iconThumb(); $wgOut->addHTML( '<div class="fullImageLink" id="file">' . $icon->toHtml( array( 'desc-link' => true ) ) . @@ -310,9 +435,9 @@ class ImagePage extends Article { if ($showLink) { - $filename = wfEscapeWikiText( $this->img->getName() ); + $filename = wfEscapeWikiText( $this->displayImg->getName() ); - if (!$this->img->isSafeFile()) { + if ( !$this->displayImg->isSafeFile() ) { $warning = wfMsgNoTrans( 'mediawarning' ); $wgOut->addWikiText( <<<EOT <div class="fullMedia"> @@ -333,7 +458,7 @@ EOT } } - if(!$this->img->isLocal()) { + if( !$this->displayImg->isLocal() ) { $this->printSharedImageText(); } } else { @@ -341,31 +466,90 @@ EOT $title = SpecialPage::getTitleFor( 'Upload' ); $link = $sk->makeKnownLinkObj($title, wfMsgHtml('noimage-linktext'), - 'wpDestFile=' . urlencode( $this->img->getName() ) ); + 'wpDestFile=' . urlencode( $this->displayImg->getName() ) ); $wgOut->addHTML( wfMsgWikiHtml( 'noimage', $link ) ); } } + /** + * Show a notice that the file is from a shared repository + */ function printSharedImageText() { global $wgOut, $wgUser; + $this->loadFile(); + $descUrl = $this->img->getDescriptionUrl(); $descText = $this->img->getDescriptionText(); - $s = "<div class='sharedUploadNotice'>" . wfMsgWikiHtml("sharedupload"); - if ( $descUrl && !$descText) { + $s = "<div class='sharedUploadNotice'>" . wfMsgWikiHtml( 'sharedupload' ); + if ( $descUrl ) { $sk = $wgUser->getSkin(); - $link = $sk->makeExternalLink( $descUrl, wfMsg('shareduploadwiki-linktext') ); - $s .= " " . wfMsgWikiHtml('shareduploadwiki', $link); + $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadwiki-linktext' ) ); + $msg = ( $descText ) ? 'shareduploadwiki-desc' : 'shareduploadwiki'; + $msg = wfMsgExt( $msg, array( 'parseinline', 'replaceafter' ), $link ); + if ( $msg != '-' ) { + # Show message only if not voided by local sysops + $s .= $msg; + } } $s .= "</div>"; - $wgOut->addHTML($s); + $wgOut->addHTML( $s ); if ( $descText ) { $this->mExtraDescription = $descText; } } + /* + * Check for files with the same name on the foreign repos. + */ + function checkSharedConflict() { + global $wgOut, $wgUser; + + $repoGroup = RepoGroup::singleton(); + if( !$repoGroup->hasForeignRepos() ) { + return; + } + + $this->loadFile(); + if( !$this->img->isLocal() ) { + return; + } + + $this->dupFile = null; + $repoGroup->forEachForeignRepo( array( $this, 'checkSharedConflictCallback' ) ); + + if( !$this->dupFile ) + return; + $dupfile = $this->dupFile; + $same = ( + ($this->img->getSha1() == $dupfile->getSha1()) && + ($this->img->getSize() == $dupfile->getSize()) + ); + + $sk = $wgUser->getSkin(); + $descUrl = $dupfile->getDescriptionUrl(); + if( $same ) { + $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadduplicate-linktext' ) ); + $wgOut->addHTML( '<div id="shared-image-dup">' . wfMsgWikiHtml( 'shareduploadduplicate', $link ) . '</div>' ); + } else { + $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadconflict-linktext' ) ); + $wgOut->addHTML( '<div id="shared-image-conflict">' . wfMsgWikiHtml( 'shareduploadconflict', $link ) . '</div>' ); + } + } + + function checkSharedConflictCallback( $repo ) { + $this->loadFile(); + $dupfile = $repo->newFile( $this->img->getTitle() ); + if( $dupfile && $dupfile->exists() ) { + $this->dupFile = $dupfile; + return $dupfile->exists(); + } + return false; + } + function getUploadUrl() { + $this->loadFile(); $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); return $uploadTitle->getFullUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) ); } @@ -377,23 +561,28 @@ EOT function uploadLinksBox() { global $wgUser, $wgOut; + $this->loadFile(); if( !$this->img->isLocal() ) return; $sk = $wgUser->getSkin(); - + $wgOut->addHtml( '<br /><ul>' ); - + # "Upload a new version of this file" link if( UploadForm::userCanReUpload($wgUser,$this->img->name) ) { $ulink = $sk->makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) ); $wgOut->addHtml( "<li><div class='plainlinks'>{$ulink}</div></li>" ); } - + + # Link to Special:FileDuplicateSearch + $dupeLink = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'FileDuplicateSearch', $this->mTitle->getDBkey() ), wfMsgHtml( 'imagepage-searchdupe' ) ); + $wgOut->addHtml( "<li>{$dupeLink}</li>" ); + # External editing link $elink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'edit-externally' ), 'action=edit&externaledit=true&mode=file' ); $wgOut->addHtml( '<li>' . $elink . '<div>' . wfMsgWikiHtml( 'edit-externally-help' ) . '</div></li>' ); - + $wgOut->addHtml( '</ul>' ); } @@ -409,29 +598,20 @@ EOT */ function imageHistory() { - global $wgUser, $wgOut, $wgUseExternalEditor; - - $sk = $wgUser->getSkin(); + global $wgOut, $wgUseExternalEditor; + $this->loadFile(); if ( $this->img->exists() ) { - $list = new ImageHistoryList( $sk, $this->current ); - $file = $this->current; + $list = new ImageHistoryList( $this ); + $file = $this->img; $dims = $file->getDimensionsString(); - $s = $list->beginImageHistoryList() . - $list->imageHistoryLine( true, wfTimestamp(TS_MW, $file->getTimestamp()), - $this->mTitle->getDBkey(), $file->getUser('id'), - $file->getUser('text'), $file->getSize(), $file->getDescription(), - $dims - ); - + $s = $list->beginImageHistoryList(); + $s .= $list->imageHistoryLine( true, $file ); + // old image versions $hist = $this->img->getHistory(); foreach( $hist as $file ) { $dims = $file->getDimensionsString(); - $s .= $list->imageHistoryLine( false, wfTimestamp(TS_MW, $file->getTimestamp()), - $file->getArchiveName(), $file->getUser('id'), - $file->getUser('text'), $file->getSize(), $file->getDescription(), - $dims - ); + $s .= $list->imageHistoryLine( false, $file ); } $s .= $list->endImageHistoryList(); } else { $s=''; } @@ -451,37 +631,97 @@ EOT { global $wgUser, $wgOut; - $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'filelinks' ), wfMsg( 'imagelinks' ) ) . "\n" ); + $limit = 100; $dbr = wfGetDB( DB_SLAVE ); - $page = $dbr->tableName( 'page' ); - $imagelinks = $dbr->tableName( 'imagelinks' ); - $sql = "SELECT page_namespace,page_title FROM $imagelinks,$page WHERE il_to=" . - $dbr->addQuotes( $this->mTitle->getDBkey() ) . " AND il_from=page_id"; - $sql = $dbr->limitResult($sql, 500, 0); - $res = $dbr->query( $sql, "ImagePage::imageLinks" ); - - if ( 0 == $dbr->numRows( $res ) ) { - $wgOut->addHtml( '<p>' . wfMsg( "nolinkstoimage" ) . "</p>\n" ); + $res = $dbr->select( + array( 'imagelinks', 'page' ), + array( 'page_namespace', 'page_title' ), + array( 'il_to' => $this->mTitle->getDBkey(), 'il_from = page_id' ), + __METHOD__, + array( 'LIMIT' => $limit + 1) + ); + $count = $dbr->numRows( $res ); + if ( $count == 0 ) { + $wgOut->addHTML( "<div id='mw-imagepage-nolinkstoimage'>\n" ); + $wgOut->addWikiMsg( 'nolinkstoimage' ); + $wgOut->addHTML( "</div>\n" ); return; } - $wgOut->addHTML( '<p>' . wfMsg( 'linkstoimage' ) . "</p>\n<ul>" ); + $wgOut->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" ); + $wgOut->addWikiMsg( 'linkstoimage', $count ); + $wgOut->addHTML( "<ul class='mw-imagepage-linktoimage'>\n" ); + + $sk = $wgUser->getSkin(); + $count = 0; + while ( $s = $res->fetchObject() ) { + $count++; + if ( $count <= $limit ) { + // We have not yet reached the extra one that tells us there is more to fetch + $name = Title::makeTitle( $s->page_namespace, $s->page_title ); + $link = $sk->makeKnownLinkObj( $name, "" ); + $wgOut->addHTML( "<li>{$link}</li>\n" ); + } + } + $wgOut->addHTML( "</ul></div>\n" ); + $res->free(); + + // Add a links to [[Special:Whatlinkshere]] + if ( $count > $limit ) + $wgOut->addWikiMsg( 'morelinkstoimage', $this->mTitle->getPrefixedDBkey() ); + } + + function imageRedirects() + { + global $wgUser, $wgOut; + + $redirects = $this->getTitle()->getRedirectsHere( NS_IMAGE ); + if ( count( $redirects ) == 0 ) return; + + $wgOut->addHTML( "<div id='mw-imagepage-section-redirectstofile'>\n" ); + $wgOut->addWikiMsg( 'redirectstofile', count( $redirects ) ); + $wgOut->addHTML( "<ul class='mw-imagepage-redirectstofile'>\n" ); + + $sk = $wgUser->getSkin(); + foreach ( $redirects as $title ) { + $link = $sk->makeKnownLinkObj( $title, "", "redirect=no" ); + $wgOut->addHTML( "<li>{$link}</li>\n" ); + } + $wgOut->addHTML( "</ul></div>\n" ); + + } + + function imageDupes() { + global $wgOut, $wgUser; + + $this->loadFile(); + + $dupes = $this->getDuplicates(); + if ( count( $dupes ) == 0 ) return; + + $wgOut->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" ); + $wgOut->addWikiMsg( 'duplicatesoffile', count( $dupes ) ); + $wgOut->addHTML( "<ul class='mw-imagepage-duplicates'>\n" ); $sk = $wgUser->getSkin(); - while ( $s = $dbr->fetchObject( $res ) ) { - $name = Title::MakeTitle( $s->page_namespace, $s->page_title ); - $link = $sk->makeKnownLinkObj( $name, "" ); + foreach ( $dupes as $file ) { + if ( $file->isLocal() ) + $link = $sk->makeKnownLinkObj( $file->getTitle(), "" ); + else + $link = $sk->makeExternalLink( $file->getDescriptionUrl(), + $file->getTitle()->getPrefixedText() ); $wgOut->addHTML( "<li>{$link}</li>\n" ); } - $wgOut->addHTML( "</ul>\n" ); + $wgOut->addHTML( "</ul></div>\n" ); } /** * Delete the file, or an earlier version of it */ public function delete() { - if( !$this->img->exists() || !$this->img->isLocal() ) { + $this->loadFile(); + if( !$this->img->exists() || !$this->img->isLocal() || $this->img->getRedirected() ) { // Standard article deletion Article::delete(); return; @@ -494,14 +734,16 @@ EOT * Revert the file to an earlier version */ public function revert() { + $this->loadFile(); $reverter = new FileRevertForm( $this->img ); $reverter->execute(); } - + /** * Override handling of action=purge */ function doPurge() { + $this->loadFile(); if( $this->img->exists() ) { wfDebug( "ImagePage::doPurge purging " . $this->img->getName() . "\n" ); $update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' ); @@ -531,16 +773,31 @@ EOT /** * Builds the image revision log shown on image pages * - * @addtogroup Media + * @ingroup Media */ class ImageHistoryList { - protected $img, $skin, $title, $repo; + protected $imagePage, $img, $skin, $title, $repo; + + public function __construct( $imagePage ) { + global $wgUser; + $this->skin = $wgUser->getSkin(); + $this->current = $imagePage->getFile(); + $this->img = $imagePage->getDisplayedFile(); + $this->title = $imagePage->getTitle(); + $this->imagePage = $imagePage; + } + + function getImagePage() { + return $this->imagePage; + } - public function __construct( $skin, $img ) { - $this->skin = $skin; - $this->img = $img; - $this->title = $img->getTitle(); + function getSkin() { + return $this->skin; + } + + function getFile() { + return $this->img; } public function beginImageHistoryList() { @@ -549,12 +806,11 @@ class ImageHistoryList { . $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) ) . Xml::openElement( 'table', array( 'class' => 'filehistory' ) ) . "\n" . '<tr><td></td>' - . ( $this->img->isLocal() && $wgUser->isAllowed( 'delete' ) ? '<td></td>' : '' ) + . ( $this->current->isLocal() && ($wgUser->isAllowed('delete') || $wgUser->isAllowed('deleterevision') ) ? '<td></td>' : '' ) . '<th>' . wfMsgHtml( 'filehist-datetime' ) . '</th>' - . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>' . '<th>' . wfMsgHtml( 'filehist-dimensions' ) . '</th>' - . '<th class="mw-imagepage-filesize">' . wfMsgHtml( 'filehist-filesize' ) . '</th>' - . '<th>' . wfMsgHtml( 'filehist-comment' ) . '</th>' + . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>' + . '<th>' . wfMsgHtml( 'filehist-comment' ) . '</th>' . "</tr>\n"; } @@ -562,36 +818,57 @@ class ImageHistoryList { return "</table>\n"; } - /** - * Create one row of file history - * - * @param bool $iscur is this the current file version? - * @param string $timestamp timestamp of file version - * @param string $img filename - * @param int $user ID of uploading user - * @param string $usertext username of uploading user - * @param int $size size of file version - * @param string $description description of file version - * @param string $dims dimensions of file version - * @return string a HTML formatted table row - */ - public function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $dims ) { - global $wgUser, $wgLang, $wgContLang; - $local = $this->img->isLocal(); - $row = ''; + public function imageHistoryLine( $iscur, $file ) { + global $wgUser, $wgLang, $wgContLang, $wgTitle; + + $timestamp = wfTimestamp(TS_MW, $file->getTimestamp()); + $img = $iscur ? $file->getName() : $file->getArchiveName(); + $user = $file->getUser('id'); + $usertext = $file->getUser('text'); + $size = $file->getSize(); + $description = $file->getDescription(); + $dims = $file->getDimensionsString(); + $sha1 = $file->getSha1(); + + $local = $this->current->isLocal(); + $row = $css = $selected = ''; // Deletion link - if( $local && $wgUser->isAllowed( 'delete' ) ) { + if( $local && ($wgUser->isAllowed('delete') || $wgUser->isAllowed('deleterevision') ) ) { $row .= '<td>'; - $q = array(); - $q[] = 'action=delete'; - if( !$iscur ) - $q[] = 'oldimage=' . urlencode( $img ); - $row .= $this->skin->makeKnownLinkObj( - $this->title, - wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ), - implode( '&', $q ) - ); + # Link to remove from history + if( $wgUser->isAllowed( 'delete' ) ) { + $q = array(); + $q[] = 'action=delete'; + if( !$iscur ) + $q[] = 'oldimage=' . urlencode( $img ); + $row .= $this->skin->makeKnownLinkObj( + $this->title, + wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ), + implode( '&', $q ) + ); + } + # Link to hide content + if( $wgUser->isAllowed( 'deleterevision' ) ) { + if( $wgUser->isAllowed('delete') ) { + $row .= '<br/>'; + } + $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); + // If file is top revision or locked from this user, don't link + if( $iscur || !$file->userCan(File::DELETED_RESTRICTED) ) { + $del = wfMsgHtml( 'rev-delundel' ); + } else { + // If the file was hidden, link to sha-1 + list($ts,$name) = explode('!',$img,2); + $del = $this->skin->makeKnownLinkObj( $revdel, wfMsg( 'rev-delundel' ), + 'target=' . urlencode( $wgTitle->getPrefixedText() ) . + '&oldimage=' . urlencode( $ts ) ); + // Bolden oversighted content + if( $file->isDeleted(File::DELETED_RESTRICTED) ) + $del = "<strong>$del</strong>"; + } + $row .= "<tt style='white-space: nowrap;'><small>$del</small></tt>"; + } $row .= '</td>'; } @@ -600,47 +877,73 @@ class ImageHistoryList { if( $iscur ) { $row .= wfMsgHtml( 'filehist-current' ); } elseif( $local && $wgUser->isLoggedIn() && $this->title->userCan( 'edit' ) ) { - $q = array(); - $q[] = 'action=revert'; - $q[] = 'oldimage=' . urlencode( $img ); - $q[] = 'wpEditToken=' . urlencode( $wgUser->editToken( $img ) ); - $row .= $this->skin->makeKnownLinkObj( - $this->title, - wfMsgHtml( 'filehist-revert' ), - implode( '&', $q ) - ); + if( $file->isDeleted(File::DELETED_FILE) ) { + $row .= wfMsgHtml('filehist-revert'); + } else { + $q = array(); + $q[] = 'action=revert'; + $q[] = 'oldimage=' . urlencode( $img ); + $q[] = 'wpEditToken=' . urlencode( $wgUser->editToken( $img ) ); + $row .= $this->skin->makeKnownLinkObj( $this->title, + wfMsgHtml( 'filehist-revert' ), + implode( '&', $q ) ); + } } $row .= '</td>'; // Date/time and image link - $row .= '<td>'; - $url = $iscur ? $this->img->getUrl() : $this->img->getArchiveUrl( $img ); - $row .= Xml::element( - 'a', - array( 'href' => $url ), - $wgLang->timeAndDate( $timestamp, true ) - ); - $row .= '</td>'; + if( $file->getTimestamp() === $this->img->getTimestamp() ) { + $selected = "class='filehistory-selected'"; + } + $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>'; + } else if( $file->isDeleted(File::DELETED_FILE) ) { + $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); + # Make a link to review the image + $url = $this->skin->makeKnownLinkObj( $revdel, $wgLang->timeAndDate( $timestamp, true ), + "target=".$wgTitle->getPrefixedText()."&file=$sha1.".$this->current->getExtension() ); + $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 .= "</td><td>"; + + // Image dimensions + $row .= htmlspecialchars( $dims ); + + // File size + $row .= " <span style='white-space: nowrap;'>(" . $this->skin->formatSize( $size ) . ')</span>'; // Uploading user - $row .= '<td>'; + $row .= '</td><td>'; if( $local ) { - $row .= $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext ); + // Hide deleted usernames + if( $file->isDeleted(File::DELETED_USER) ) { + $row .= '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>'; + } else { + $row .= $this->skin->userLink( $user, $usertext ) . " <span style='white-space: nowrap;'>" . + $this->skin->userToolLinks( $user, $usertext ) . "</span>"; + } } else { $row .= htmlspecialchars( $usertext ); } - $row .= '</td>'; + $row .= '</td><td>'; - // Image dimensions - $row .= '<td>' . htmlspecialchars( $dims ) . '</td>'; - - // File size - $row .= '<td class="mw-imagepage-filesize">' . $this->skin->formatSize( $size ) . '</td>'; + // Don't show deleted descriptions + if ( $file->isDeleted(File::DELETED_COMMENT) ) { + $row .= '<span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>'; + } else { + $row .= $this->skin->commentBlock( $description, $this->title ); + } + $row .= '</td>'; - // Comment - $row .= '<td>' . $this->skin->formatComment( $description, $this->title ) . '</td>'; + wfRunHooks( 'ImagePageFileHistoryLine', array( $this, $file, &$row, &$rowClass ) ); + $classAttr = $rowClass ? " class='$rowClass'" : ""; - return "<tr>{$row}</tr>\n"; + return "<tr{$classAttr}>{$row}</tr>\n"; } - } diff --git a/includes/ImageQueryPage.php b/includes/ImageQueryPage.php index 8948ddc6..da9b6fd6 100644 --- a/includes/ImageQueryPage.php +++ b/includes/ImageQueryPage.php @@ -4,8 +4,7 @@ * Variant of QueryPage which uses a gallery to output results, thus * suited for reports generating images * - * @package MediaWiki - * @addtogroup SpecialPage + * @ingroup SpecialPage * @author Rob Church <robchur@gmail.com> */ class ImageQueryPage extends QueryPage { @@ -64,5 +63,3 @@ class ImageQueryPage extends QueryPage { } } - - diff --git a/includes/JobQueue.php b/includes/JobQueue.php index 5cec3106..8bfd1b3e 100644 --- a/includes/JobQueue.php +++ b/includes/JobQueue.php @@ -1,4 +1,7 @@ <?php +/** + * @defgroup JobQueue JobQueue + */ if ( !defined( 'MEDIAWIKI' ) ) { die( "This file is part of MediaWiki, it is not a valid entry point\n" ); @@ -6,6 +9,8 @@ if ( !defined( 'MEDIAWIKI' ) ) { /** * Class to both describe a background job and handle jobs. + * + * @ingroup JobQueue */ abstract class Job { var $command, @@ -37,8 +42,8 @@ abstract class Job { */ /** - * Pop a job of a certain type. This tries less hard than pop() to - * actually find a job; it may be adversely affected by concurrent job + * Pop a job of a certain type. This tries less hard than pop() to + * actually find a job; it may be adversely affected by concurrent job * runners. */ static function pop_type($type) { @@ -78,7 +83,7 @@ abstract class Job { /** * Pop a job off the front of the queue - * @static + * * @param $offset Number of jobs to skip * @return Job or false if there's no jobs */ @@ -87,11 +92,11 @@ abstract class Job { $dbr = wfGetDB( DB_SLAVE ); - /* Get a job from the slave, start with an offset, + /* Get a job from the slave, start with an offset, scan full set afterwards, avoid hitting purged rows - NB: If random fetch previously was used, offset - will always be ahead of few entries + NB: If random fetch previously was used, offset + will always be ahead of few entries */ $row = $dbr->selectRow( 'job', '*', "job_id >= ${offset}", __METHOD__, @@ -158,7 +163,10 @@ abstract class Job { $job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), $row->job_id ); // Remove any duplicates it may have later in the queue + // Deadlock prone section + $dbw->begin(); $dbw->delete( 'job', $job->insertFields(), __METHOD__ ); + $dbw->commit(); wfProfileOut( __METHOD__ ); return $job; @@ -167,10 +175,10 @@ abstract class Job { /** * Create the appropriate object to handle a specific job * - * @param string $command Job command - * @param Title $title Associated title - * @param array $params Job parameters - * @param int $id Job identifier + * @param $command String: Job command + * @param $title Title: Associated title + * @param $params Array: Job parameters + * @param $id Int: Job identifier * @return Job */ static function factory( $command, $title, $params = false, $id = 0 ) { @@ -181,7 +189,7 @@ abstract class Job { } throw new MWException( "Invalid job command `{$command}`" ); } - + static function makeBlob( $params ) { if ( $params !== false ) { return serialize( $params ); @@ -208,12 +216,23 @@ abstract class Job { * @param $jobs array of Job objects */ static function batchInsert( $jobs ) { - if( count( $jobs ) ) { - $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); - foreach( $jobs as $job ) { - $rows[] = $job->insertFields(); + if( !count( $jobs ) ) { + return; + } + $dbw = wfGetDB( DB_MASTER ); + $rows = array(); + foreach( $jobs as $job ) { + $rows[] = $job->insertFields(); + if ( count( $rows ) >= 50 ) { + # Do a small transaction to avoid slave lag + $dbw->begin(); + $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' ); + $dbw->commit(); + $rows = array(); } + } + if ( $rows ) { + $dbw->begin(); $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' ); $dbw->commit(); } @@ -283,9 +302,11 @@ abstract class Job { } } + protected function setLastError( $error ) { + $this->error = $error; + } + function getLastError() { return $this->error; } } - - diff --git a/includes/Licenses.php b/includes/Licenses.php index 6a034468..e76ac23c 100644 --- a/includes/Licenses.php +++ b/includes/Licenses.php @@ -1,8 +1,8 @@ <?php /** * A License class for use on Special:Upload - * - * @addtogroup SpecialPage + * + * @ingroup SpecialPage * * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com> * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason @@ -172,4 +172,3 @@ class License { $this->text = strrev( $text ); } } - diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php index db1114c9..bdc4b43a 100644 --- a/includes/LinkBatch.php +++ b/includes/LinkBatch.php @@ -4,7 +4,7 @@ * Class representing a list of titles * The execute() method checks them all for existence and adds them to a LinkCache object * - * @addtogroup Cache + * @ingroup Cache */ class LinkBatch { /** @@ -18,7 +18,7 @@ class LinkBatch { } } - function addObj( $title ) { + public function addObj( $title ) { if ( is_object( $title ) ) { $this->add( $title->getNamespace(), $title->getDBkey() ); } else { @@ -26,7 +26,7 @@ class LinkBatch { } } - function add( $ns, $dbkey ) { + public function add( $ns, $dbkey ) { if ( $ns < 0 ) { return; } @@ -41,21 +41,21 @@ class LinkBatch { * Set the link list to a given 2-d array * First key is the namespace, second is the DB key, value arbitrary */ - function setArray( $array ) { + public function setArray( $array ) { $this->data = $array; } /** * Returns true if no pages have been added, false otherwise. */ - function isEmpty() { + public function isEmpty() { return ($this->getSize() == 0); } /** * Returns the size of the batch. */ - function getSize() { + public function getSize() { return count( $this->data ); } @@ -63,8 +63,8 @@ class LinkBatch { * Do the query and add the results to the LinkCache object * Return an array mapping PDBK to ID */ - function execute() { - $linkCache =& LinkCache::singleton(); + public function execute() { + $linkCache = LinkCache::singleton(); return $this->executeInto( $linkCache ); } @@ -72,13 +72,22 @@ class LinkBatch { * Do the query and add the results to a given LinkCache object * Return an array mapping PDBK to ID */ - function executeInto( &$cache ) { - $fname = 'LinkBatch::executeInto'; - wfProfileIn( $fname ); - // Do query + protected function executeInto( &$cache ) { + wfProfileIn( __METHOD__ ); $res = $this->doQuery(); + $ids = $this->addResultToCache( $cache, $res ); + wfProfileOut( __METHOD__ ); + return $ids; + } + + /** + * Add a ResultWrapper containing IDs and titles to a LinkCache object. + * As normal, titles will go into the static Title cache field. + * This function *also* stores extra fields of the title used for link + * parsing to avoid extra DB queries. + */ + public function addResultToCache( $cache, $res ) { if ( !$res ) { - wfProfileOut( $fname ); return array(); } @@ -88,11 +97,10 @@ class LinkBatch { $remaining = $this->data; while ( $row = $res->fetchObject() ) { $title = Title::makeTitle( $row->page_namespace, $row->page_title ); - $cache->addGoodLinkObj( $row->page_id, $title ); + $cache->addGoodLinkObj( $row->page_id, $title, $row->page_len, $row->page_is_redirect ); $ids[$title->getPrefixedDBkey()] = $row->page_id; unset( $remaining[$row->page_namespace][$row->page_title] ); } - $res->free(); // The remaining links in $data are bad links, register them as such foreach ( $remaining as $ns => $dbkeys ) { @@ -102,20 +110,17 @@ class LinkBatch { $ids[$title->getPrefixedDBkey()] = 0; } } - wfProfileOut( $fname ); return $ids; } /** * Perform the existence test query, return a ResultWrapper with page_id fields */ - function doQuery() { - $fname = 'LinkBatch::doQuery'; - + public function doQuery() { if ( $this->isEmpty() ) { return false; } - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); // Construct query // This is very similar to Parser::replaceLinkHolders @@ -123,26 +128,25 @@ class LinkBatch { $page = $dbr->tableName( 'page' ); $set = $this->constructSet( 'page', $dbr ); if ( $set === false ) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return false; } - $sql = "SELECT page_id, page_namespace, page_title FROM $page WHERE $set"; + $sql = "SELECT page_id, page_namespace, page_title, page_len, page_is_redirect FROM $page WHERE $set"; // Do query - $res = new ResultWrapper( $dbr, $dbr->query( $sql, $fname ) ); - wfProfileOut( $fname ); + $res = new ResultWrapper( $dbr, $dbr->query( $sql, __METHOD__ ) ); + wfProfileOut( __METHOD__ ); return $res; } /** * Construct a WHERE clause which will match all the given titles. - * Give the appropriate table's field name prefix ('page', 'pl', etc). * - * @param $prefix String: ?? + * @param string $prefix the appropriate table's field name prefix ('page', 'pl', etc) * @return string * @public */ - function constructSet( $prefix, &$db ) { + public function constructSet( $prefix, &$db ) { $first = true; $firstTitle = true; $sql = ''; @@ -156,7 +160,7 @@ class LinkBatch { } else { $sql .= ' OR '; } - + if (count($dbkeys)==1) { // avoid multiple-reference syntax if simple equality can be used $singleKey = array_keys($dbkeys); $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title=". @@ -164,7 +168,7 @@ class LinkBatch { ")"; } else { $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title IN ("; - + $firstTitle = true; foreach( $dbkeys as $dbkey => $unused ) { if ( $firstTitle ) { @@ -185,5 +189,3 @@ class LinkBatch { } } } - - diff --git a/includes/LinkCache.php b/includes/LinkCache.php index 7c49d88e..79727615 100644 --- a/includes/LinkCache.php +++ b/includes/LinkCache.php @@ -1,13 +1,13 @@ <?php /** * Cache for article titles (prefixed DB keys) and ids linked from one source - * - * @addtogroup Cache + * + * @ingroup Cache */ class LinkCache { // Increment $mClassVer whenever old serialized versions of this class // becomes incompatible with the new version. - /* private */ var $mClassVer = 3; + /* private */ var $mClassVer = 4; /* private */ var $mPageLinks; /* private */ var $mGoodLinks, $mBadLinks; @@ -28,21 +28,18 @@ class LinkCache { $this->mForUpdate = false; $this->mPageLinks = array(); $this->mGoodLinks = array(); + $this->mGoodLinkFields = array(); $this->mBadLinks = array(); } - /* private */ function getKey( $title ) { - return wfMemcKey( 'lc', 'title', $title ); - } - /** * General accessor to get/set whether SELECT FOR UPDATE should be used */ - function forUpdate( $update = NULL ) { + public function forUpdate( $update = NULL ) { return wfSetVar( $this->mForUpdate, $update ); } - function getGoodLinkID( $title ) { + public function getGoodLinkID( $title ) { if ( array_key_exists( $title, $this->mGoodLinks ) ) { return $this->mGoodLinks[$title]; } else { @@ -50,17 +47,41 @@ class LinkCache { } } - function isBadLink( $title ) { + /** + * Get a field of a title object from cache. + * If this link is not good, it will return NULL. + * @param Title $title + * @param string $field ('length','redirect') + * @return mixed + */ + public function getGoodLinkFieldObj( $title, $field ) { + $dbkey = $title->getPrefixedDbKey(); + if ( array_key_exists( $dbkey, $this->mGoodLinkFields ) ) { + return $this->mGoodLinkFields[$dbkey][$field]; + } else { + return NULL; + } + } + + public function isBadLink( $title ) { return array_key_exists( $title, $this->mBadLinks ); } - function addGoodLinkObj( $id, $title ) { + /** + * Add a link for the title to the link cache + * @param int $id + * @param Title $title + * @param int $len + * @param int $redir + */ + public function addGoodLinkObj( $id, $title, $len = -1, $redir = NULL ) { $dbkey = $title->getPrefixedDbKey(); $this->mGoodLinks[$dbkey] = $id; + $this->mGoodLinkFields[$dbkey] = array( 'length' => $len, 'redirect' => $redir ); $this->mPageLinks[$dbkey] = $title; } - function addBadLinkObj( $title ) { + public function addBadLinkObj( $title ) { $dbkey = $title->getPrefixedDbKey(); if ( ! $this->isBadLink( $dbkey ) ) { $this->mBadLinks[$dbkey] = 1; @@ -68,30 +89,28 @@ class LinkCache { } } - function clearBadLink( $title ) { + public function clearBadLink( $title ) { unset( $this->mBadLinks[$title] ); - $this->clearLink( $title ); } - function clearLink( $title ) { - global $wgMemc, $wgLinkCacheMemcached; - if( $wgLinkCacheMemcached ) - $wgMemc->delete( $this->getKey( $title ) ); - } + /* obsolete, for old $wgLinkCacheMemcached stuff */ + public function clearLink( $title ) {} - function getPageLinks() { return $this->mPageLinks; } - function getGoodLinks() { return $this->mGoodLinks; } - function getBadLinks() { return array_keys( $this->mBadLinks ); } + public function getPageLinks() { return $this->mPageLinks; } + public function getGoodLinks() { return $this->mGoodLinks; } + public function getBadLinks() { return array_keys( $this->mBadLinks ); } /** * Add a title to the link cache, return the page_id or zero if non-existent * @param $title String: title to add + * @param $len int, page size + * @param $redir bool, is redirect? * @return integer */ - function addLink( $title ) { + public function addLink( $title, $len = -1, $redir = NULL ) { $nt = Title::newFromDBkey( $title ); if( $nt ) { - return $this->addLinkObj( $nt ); + return $this->addLinkObj( $nt, $len, $redir ); } else { return 0; } @@ -100,18 +119,20 @@ class LinkCache { /** * Add a title to the link cache, return the page_id or zero if non-existent * @param $nt Title to add. + * @param $len int, page size + * @param $redir bool, is redirect? * @return integer */ - function addLinkObj( &$nt ) { - global $wgMemc, $wgLinkCacheMemcached, $wgAntiLockFlags; + public function addLinkObj( &$nt, $len = -1, $redirect = NULL ) { + global $wgAntiLockFlags, $wgProfiler; + $title = $nt->getPrefixedDBkey(); if ( $this->isBadLink( $title ) ) { return 0; } $id = $this->getGoodLinkID( $title ); if ( 0 != $id ) { return $id; } $fname = 'LinkCache::addLinkObj'; - global $wgProfiling, $wgProfiler; - if ( $wgProfiling && isset( $wgProfiler ) ) { + if ( isset( $wgProfiler ) ) { $fname .= ' (' . $wgProfiler->getCurrentSection() . ')'; } @@ -125,36 +146,32 @@ class LinkCache { return 0; } - $id = NULL; - if( $wgLinkCacheMemcached ) - $id = $wgMemc->get( $key = $this->getKey( $title ) ); - if( ! is_integer( $id ) ) { - if ( $this->mForUpdate ) { - $db = wfGetDB( DB_MASTER ); - if ( !( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) ) { - $options = array( 'FOR UPDATE' ); - } else { - $options = array(); - } + # Some fields heavily used for linking... + if ( $this->mForUpdate ) { + $db = wfGetDB( DB_MASTER ); + if ( !( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) ) { + $options = array( 'FOR UPDATE' ); } else { - $db = wfGetDB( DB_SLAVE ); $options = array(); } - - $id = $db->selectField( 'page', 'page_id', - array( 'page_namespace' => $ns, 'page_title' => $t ), - $fname, $options ); - if ( !$id ) { - $id = 0; - } - if( $wgLinkCacheMemcached ) - $wgMemc->add( $key, $id, 3600*24 ); + } else { + $db = wfGetDB( DB_SLAVE ); + $options = array(); } + $s = $db->selectRow( 'page', + array( 'page_id', 'page_len', 'page_is_redirect' ), + array( 'page_namespace' => $ns, 'page_title' => $t ), + $fname, $options ); + # Set fields... + $id = $s ? $s->page_id : 0; + $len = $s ? $s->page_len : -1; + $redirect = $s ? $s->page_is_redirect : 0; + if( 0 == $id ) { $this->addBadLinkObj( $nt ); } else { - $this->addGoodLinkObj( $id, $nt ); + $this->addGoodLinkObj( $id, $nt, $len, $redirect ); } wfProfileOut( $fname ); return $id; @@ -163,10 +180,10 @@ class LinkCache { /** * Clears cache */ - function clear() { + public function clear() { $this->mPageLinks = array(); $this->mGoodLinks = array(); + $this->mGoodLinkFields = array(); $this->mBadLinks = array(); } } - diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php index ced76d75..dc4c1256 100644 --- a/includes/LinkFilter.php +++ b/includes/LinkFilter.php @@ -106,4 +106,3 @@ class LinkFilter { return $like; } } - diff --git a/includes/Linker.php b/includes/Linker.php index 4b092cf9..32c506a4 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -1,15 +1,12 @@ <?php /** - * Split off some of the internal bits from Skin.php. - * These functions are used for primarily page content: - * links, embedded images, table of contents. Links are - * also used in the skin. - * For the moment, Skin is a descendent class of Linker. - * In the future, it should probably be further split - * so that ever other bit of the wiki doesn't have to - * go loading up Skin to get at it. + * Split off some of the internal bits from Skin.php. These functions are used + * for primarily page content: links, embedded images, table of contents. Links + * are also used in the skin. For the moment, Skin is a descendent class of + * Linker. In the future, it should probably be further split so that every + * other bit of the wiki doesn't have to go loading up Skin to get at it. * - * @addtogroup Skins + * @ingroup Skins */ class Linker { @@ -23,72 +20,110 @@ class Linker { /** * @deprecated */ - function postParseLinkColour( $s = NULL ) { - return NULL; + function postParseLinkColour( $s = null ) { + return null; } - /** @todo document */ - function getExternalLinkAttributes( $link, $text, $class='' ) { - $link = htmlspecialchars( $link ); - - $r = ($class != '') ? " class=\"$class\"" : " class=\"external\""; - - $r .= " title=\"{$link}\""; - return $r; + /** + * Get the appropriate HTML attributes to add to the "a" element of an ex- + * ternal link, as created by [wikisyntax]. + * + * @param string $title The (unescaped) title text for the link + * @param string $unused Unused + * @param string $class The contents of the class attribute; if an empty + * string is passed, which is the default value, defaults to 'external'. + */ + function getExternalLinkAttributes( $title, $unused = null, $class='' ) { + return $this->getLinkAttributesInternal( $title, $class, 'external' ); } - function getInterwikiLinkAttributes( $link, $text, $class='' ) { + /** + * Get the appropriate HTML attributes to add to the "a" element of an in- + * terwiki link. + * + * @param string $title The title text for the link, URL-encoded (???) but + * not HTML-escaped + * @param string $unused Unused + * @param string $class The contents of the class attribute; if an empty + * string is passed, which is the default value, defaults to 'external'. + */ + function getInterwikiLinkAttributes( $title, $unused = null, $class='' ) { global $wgContLang; - $link = urldecode( $link ); - $link = $wgContLang->checkTitleEncoding( $link ); - $link = preg_replace( '/[\\x00-\\x1f]/', ' ', $link ); - $link = htmlspecialchars( $link ); + # FIXME: We have a whole bunch of handling here that doesn't happen in + # getExternalLinkAttributes, why? + $title = urldecode( $title ); + $title = $wgContLang->checkTitleEncoding( $title ); + $title = preg_replace( '/[\\x00-\\x1f]/', ' ', $title ); - $r = ($class != '') ? " class=\"$class\"" : " class=\"external\""; + return $this->getLinkAttributesInternal( $title, $class, 'external' ); + } - $r .= " title=\"{$link}\""; - return $r; + /** + * Get the appropriate HTML attributes to add to the "a" element of an in- + * ternal link. + * + * @param string $title The title text for the link, URL-encoded (???) but + * not HTML-escaped + * @param string $unused Unused + * @param string $class The contents of the class attribute, default none + */ + function getInternalLinkAttributes( $title, $unused = null, $class='' ) { + $title = urldecode( $title ); + $title = str_replace( '_', ' ', $title ); + return $this->getLinkAttributesInternal( $title, $class ); } - /** @todo document */ - function getInternalLinkAttributes( $link, $text, $class='' ) { - $link = urldecode( $link ); - $link = str_replace( '_', ' ', $link ); - $link = htmlspecialchars( $link ); - $r = ($class != '') ? ' class="' . htmlspecialchars( $class ) . '"' : ''; - $r .= " title=\"{$link}\""; - return $r; + /** + * Get the appropriate HTML attributes to add to the "a" element of an in- + * ternal link, given the Title object for the page we want to link to. + * + * @param Title $nt The Title object + * @param string $unused Unused + * @param string $class The contents of the class attribute, default none + * @param mixed $title Optional (unescaped) string to use in the title + * attribute; if false, default to the name of the page we're linking to + */ + function getInternalLinkAttributesObj( $nt, $unused = null, $class = '', $title = false ) { + if( $title === false ) { + $title = $nt->getPrefixedText(); + } + return $this->getLinkAttributesInternal( $title, $class ); } /** - * @param $nt Title object. - * @param $text String: FIXME - * @param $class String: CSS class of the link, default ''. + * Common code for getLinkAttributesX functions */ - function getInternalLinkAttributesObj( &$nt, $text, $class='' ) { - $r = ($class != '') ? ' class="' . htmlspecialchars( $class ) . '"' : ''; - $r .= ' title="' . $nt->getEscapedText() . '"'; + private function getLinkAttributesInternal( $title, $class, $classDefault = false ) { + $title = htmlspecialchars( $title ); + if( $class === '' and $classDefault !== false ) { + # FIXME: Parameter defaults the hard way! We should just have + # $class = 'external' or whatever as the default in the externally- + # exposed functions, not $class = ''. + $class = $classDefault; + } + $class = htmlspecialchars( $class ); + $r = ''; + if( $class !== '' ) { + $r .= " class=\"$class\""; + } + $r .= " title=\"$title\""; return $r; } /** * Return the CSS colour of a known link * - * @param mixed $s + * @param Title $t * @param integer $threshold user defined threshold * @return string CSS class */ - function getLinkColour( $s, $threshold ) { - if( $s === false ) { - return ''; - } - + function getLinkColour( $t, $threshold ) { $colour = ''; - if ( !empty( $s->page_is_redirect ) ) { + if ( $t->isRedirect() ) { # Page is a redirect $colour = 'mw-redirect'; - } elseif ( $threshold > 0 && $s->page_len < $threshold && Namespace::isContent( $s->page_namespace ) ) { + } elseif ( $threshold > 0 && $t->getLength() < $threshold && MWNamespace::isContent( $t->getNamespace() ) ) { # Page is a stub $colour = 'stub'; } @@ -123,7 +158,7 @@ class Linker { /** * This function is a shortcut to makeKnownLinkObj(Title::newFromText($title),...). Do not call * it if you already have a title object handy. See makeKnownLinkObj for further documentation. - * + * * @param $title String: the text of the title * @param $text String: link text * @param $query String: optional query part @@ -144,7 +179,7 @@ class Linker { /** * This function is a shortcut to makeBrokenLinkObj(Title::newFromText($title),...). Do not call * it if you already have a title object handy. See makeBrokenLinkObj for further documentation. - * + * * @param string $title The text of the title * @param string $text Link text * @param string $query Optional query part @@ -164,10 +199,10 @@ class Linker { /** * @deprecated use makeColouredLinkObj - * + * * This function is a shortcut to makeStubLinkObj(Title::newFromText($title),...). Do not call * it if you already have a title object handy. See makeStubLinkObj for further documentation. - * + * * @param $title String: the text of the title * @param $text String: link text * @param $query String: optional query part @@ -189,7 +224,7 @@ class Linker { * Make a link for a title which may or may not be in the database. If you need to * call this lots of times, pre-fill the link cache with a LinkBatch, otherwise each * call to this will result in a DB query. - * + * * @param $nt Title: the title object to make the link from, e.g. from * Title::newFromText. * @param $text String: link text @@ -228,7 +263,7 @@ class Linker { wfProfileOut( __METHOD__ ); return $t; } elseif ( $nt->isAlwaysKnown() ) { - # Image links, special page links and self-links with fragements are always known. + # Image links, special page links and self-links with fragments are always known. $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix ); } else { wfProfileIn( __METHOD__.'-immediate' ); @@ -252,15 +287,8 @@ class Linker { } else { $colour = ''; if ( $nt->isContentPage() ) { - # FIXME: This is stupid, we should combine this query with - # the Title::getArticleID() query above. $threshold = $wgUser->getOption('stubthreshold'); - $dbr = wfGetDB( DB_SLAVE ); - $s = $dbr->selectRow( - array( 'page' ), - array( 'page_len', 'page_is_redirect', 'page_namespace' ), - array( 'page_id' => $aid ), __METHOD__ ) ; - $colour = $this->getLinkColour( $s, $threshold ); + $colour = $this->getLinkColour( $nt, $threshold ); } $retVal = $this->makeColouredLinkObj( $nt, $colour, $text, $query, $trail, $prefix ); } @@ -284,15 +312,17 @@ class Linker { * @param $style String: style to apply - if empty, use getInternalLinkAttributesObj instead * @return the a-element */ - function makeKnownLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) { + function makeKnownLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) { wfProfileIn( __METHOD__ ); - if ( !$nt instanceof Title ) { + if ( !$title instanceof Title ) { # Fail gracefully wfProfileOut( __METHOD__ ); return "<!-- ERROR -->{$prefix}{$text}{$trail}"; } + $nt = $this->normaliseSpecialPage( $title ); + $u = $nt->escapeLocalURL( $query ); if ( $nt->getFragment() != '' ) { if( $nt->getPrefixedDbkey() == '' ) { @@ -320,7 +350,7 @@ class Linker { /** * Make a red link to the edit page of a given title. - * + * * @param $nt Title object of the target page * @param $text String: Link text * @param $query String: Optional query part @@ -328,30 +358,35 @@ class Linker { * be included in the link text. Other characters will be appended after * the end of the link. */ - function makeBrokenLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + function makeBrokenLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) { wfProfileIn( __METHOD__ ); - if ( !$nt instanceof Title ) { + if ( !$title instanceof Title ) { # Fail gracefully wfProfileOut( __METHOD__ ); return "<!-- ERROR -->{$prefix}{$text}{$trail}"; } + $nt = $this->normaliseSpecialPage( $title ); + if( $nt->getNamespace() == NS_SPECIAL ) { $q = $query; } else if ( '' == $query ) { - $q = 'action=edit'; + $q = 'action=edit&redlink=1'; } else { - $q = 'action=edit&'.$query; + $q = 'action=edit&redlink=1&'.$query; } $u = $nt->escapeLocalURL( $q ); + $titleText = $nt->getPrefixedText(); if ( '' == $text ) { - $text = htmlspecialchars( $nt->getPrefixedText() ); + $text = htmlspecialchars( $titleText ); } - $style = $this->getInternalLinkAttributesObj( $nt, $text, 'new' ); - + $titleAttr = wfMsg( 'red-link-title', $titleText ); + $style = $this->getInternalLinkAttributesObj( $nt, $text, 'new', $titleAttr ); list( $inside, $trail ) = Linker::splitTrail( $trail ); + + wfRunHooks( 'BrokenLink', array( &$this, $nt, $query, &$u, &$style, &$prefix, &$text, &$inside, &$trail ) ); $s = "<a href=\"{$u}\"{$style}>{$prefix}{$text}{$inside}</a>{$trail}"; wfProfileOut( __METHOD__ ); @@ -360,9 +395,9 @@ class Linker { /** * @deprecated use makeColouredLinkObj - * + * * Make a brown link to a short article. - * + * * @param $nt Title object of the target page * @param $text String: link text * @param $query String: optional query part @@ -376,7 +411,7 @@ class Linker { /** * Make a coloured link. - * + * * @param $nt Title object of the target page * @param $colour Integer: colour of the link * @param $text String: link text @@ -412,7 +447,7 @@ class Linker { return $this->makeColouredLinkObj( $nt, $colour, $text, $query, $trail, $prefix ); } - /** + /** * Make appropriate markup for a link to the current article. This is currently rendered * as the bold link text. The calling sequence is the same as the other make*LinkObj functions, * despite $query not being used. @@ -425,6 +460,16 @@ class Linker { return "<strong class=\"selflink\">{$prefix}{$text}{$inside}</strong>{$trail}"; } + function normaliseSpecialPage( Title $title ) { + if ( $title->getNamespace() == NS_SPECIAL ) { + list( $name, $subpage ) = SpecialPage::resolveAliasWithSubpage( $title->getDBkey() ); + if ( !$name ) return $title; + return SpecialPage::getTitleFor( $name, $subpage ); + } else { + return $title; + } + } + /** @todo document */ function fnamePart( $url ) { $basename = strrchr( $url, '/' ); @@ -433,7 +478,7 @@ class Linker { } else { $basename = substr( $basename, 1 ); } - return htmlspecialchars( $basename ); + return $basename; } /** Obsolete alias */ @@ -446,11 +491,19 @@ class Linker { if ( '' == $alt ) { $alt = $this->fnamePart( $url ); } - $s = '<img src="'.$url.'" alt="'.$alt.'" />'; - return $s; + $img = ''; + $success = wfRunHooks('LinkerMakeExternalImage', array( &$url, &$alt, &$img ) ); + if(!$success) { + wfDebug("Hook LinkerMakeExternalImage changed the output of external image with url {$url} and alt text {$alt} to {$img}", true); + return $img; + } + return Xml::element( 'img', + array( + 'src' => $url, + 'alt' => $alt ) ); } - /** + /** * Creates the HTML source for images * @deprecated use makeImageLink2 * @@ -490,12 +543,14 @@ class Linker { } /** - * Make an image link + * Given parameters derived from [[Image:Foo|options...]], generate the + * HTML that that syntax inserts in the page. + * * @param Title $title Title object * @param File $file File object, or false if it doesn't exist * * @param array $frameParams Associative array of parameters external to the media handler. - * Boolean parameters are indicated by presence or absence, the value is arbitrary and + * Boolean parameters are indicated by presence or absence, the value is arbitrary and * will often be false. * thumbnail If present, downscale and frame * manualthumb Image name to use as a thumbnail, instead of automatic scaling @@ -505,16 +560,24 @@ class Linker { * upright_factor Fudge factor for "upright" tweak (default 0.75) * border If present, show a border around the image * align Horizontal alignment (left, right, center, none) - * valign Vertical alignment (baseline, sub, super, top, text-top, middle, + * valign Vertical alignment (baseline, sub, super, top, text-top, middle, * bottom, text-bottom) * alt Alternate text for image (i.e. alt attribute). Plain text. * caption HTML for image caption. * - * @param array $handlerParams Associative array of media handler parameters, to be passed - * to transform(). Typical keys are "width" and "page". + * @param array $handlerParams Associative array of media handler parameters, to be passed + * to transform(). Typical keys are "width" and "page". * @param string $time, timestamp of the file, set as false for current + * @param string $query, query params for desc url + * @return string HTML for an image, with links, wrappers, etc. */ - function makeImageLink2( Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false ) { + function makeImageLink2( Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false, $query = "" ) { + $res = null; + if( !wfRunHooks( 'ImageBeforeProduceHTML', array( &$this, &$title, + &$file, &$frameParams, &$handlerParams, &$time, &$res ) ) ) { + return $res; + } + global $wgContLang, $wgUser, $wgThumbLimits, $wgThumbUpright; if ( $file && !$file->allowInlineDisplay() ) { wfDebug( __METHOD__.': '.$title->getPrefixedDBkey()." does not allow inline display\n" ); @@ -554,8 +617,8 @@ class Linker { } // Use width which is smaller: real image width or user preference width // For caching health: If width scaled down due to upright parameter, round to full __0 pixel to avoid the creation of a lot of odd thumbs - $prefWidth = isset( $fp['upright'] ) ? - round( $wgThumbLimits[$wopt] * $fp['upright'], -1 ) : + $prefWidth = isset( $fp['upright'] ) ? + round( $wgThumbLimits[$wopt] * $fp['upright'], -1 ) : $wgThumbLimits[$wopt]; if ( $hp['width'] <= 0 || $prefWidth < $hp['width'] ) { $hp['width'] = $prefWidth; @@ -575,7 +638,7 @@ class Linker { if ( $fp['align'] == '' ) { $fp['align'] = $wgContLang->isRTL() ? 'left' : 'right'; } - return $prefix.$this->makeThumbLink2( $title, $file, $fp, $hp, $time ).$postfix; + return $prefix.$this->makeThumbLink2( $title, $file, $fp, $hp, $time, $query ).$postfix; } if ( $file && isset( $fp['frameless'] ) ) { @@ -599,6 +662,7 @@ class Linker { } else { $s = $thumb->toHtml( array( 'desc-link' => true, + 'desc-query' => $query, 'alt' => $fp['alt'], 'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false , 'img-class' => isset( $fp['border'] ) ? 'thumbborder' : false ) ); @@ -611,11 +675,11 @@ class Linker { /** * Make HTML for a thumbnail including image, border and caption - * @param Title $title + * @param Title $title * @param File $file File object or false if it doesn't exist */ function makeThumbLinkObj( Title $title, $file, $label = '', $alt, $align = 'right', $params = array(), $framed=false , $manualthumb = "" ) { - $frameParams = array( + $frameParams = array( 'alt' => $alt, 'caption' => $label, 'align' => $align @@ -625,7 +689,7 @@ class Linker { return $this->makeThumbLink2( $title, $file, $frameParams, $params ); } - function makeThumbLink2( Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false ) { + function makeThumbLink2( Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false, $query = "" ) { global $wgStylePath, $wgContLang; $exists = $file && $file->exists(); @@ -639,7 +703,7 @@ class Linker { if ( !isset( $fp['caption'] ) ) $fp['caption'] = ''; if ( empty( $hp['width'] ) ) { - // Reduce width for upright images when parameter 'upright' is used + // Reduce width for upright images when parameter 'upright' is used $hp['width'] = isset( $fp['upright'] ) ? 130 : 180; } $thumb = false; @@ -678,7 +742,9 @@ class Linker { } } - $query = $page ? 'page=' . urlencode( $page ) : ''; + if( $page ) { + $query = $query ? '&page=' . urlencode( $page ) : 'page=' . urlencode( $page ); + } $url = $title->getLocalURL( $query ); $more = htmlspecialchars( wfMsg( 'thumbnail-more' ) ); @@ -694,7 +760,8 @@ class Linker { $s .= $thumb->toHtml( array( 'alt' => $fp['alt'], 'img-class' => 'thumbimage', - 'desc-link' => true ) ); + 'desc-link' => true, + 'desc-query' => $query ) ); if ( isset( $fp['framed'] ) ) { $zoomicon=""; } else { @@ -729,9 +796,9 @@ class Linker { if( $text == '' ) $text = htmlspecialchars( $title->getPrefixedText() ); $redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title ); - if( $redir ) { + if( $redir ) { return $this->makeKnownLinkObj( $title, $text, $query, $trail, $prefix ); - } + } $q = 'wpDestFile=' . $title->getPartialUrl(); if( $query != '' ) $q .= '&' . $query; @@ -750,27 +817,28 @@ class Linker { } /** @deprecated use Linker::makeMediaLinkObj() */ - function makeMediaLink( $name, $unused = '', $text = '' ) { + function makeMediaLink( $name, $unused = '', $text = '', $time = false ) { $nt = Title::makeTitleSafe( NS_IMAGE, $name ); - return $this->makeMediaLinkObj( $nt, $text ); + return $this->makeMediaLinkObj( $nt, $text, $time ); } /** * Create a direct link to a given uploaded file. * * @param $title Title object. - * @param $text String: pre-sanitized HTML + * @param $text String: pre-sanitized HTML + * @param $time string: time image was created * @return string HTML * * @public * @todo Handle invalid or missing images better. */ - function makeMediaLinkObj( $title, $text = '' ) { + function makeMediaLinkObj( $title, $text = '', $time = false ) { if( is_null( $title ) ) { ### HOTFIX. Instead of breaking, return empty string. return $text; } else { - $img = wfFindFile( $title ); + $img = wfFindFile( $title, $time ); if( $img ) { $url = $img->getURL(); $class = 'internal'; @@ -809,6 +877,12 @@ class Linker { if( $escape ) { $text = htmlspecialchars( $text ); } + $link = ''; + $success = wfRunHooks('LinkerMakeExternalLink', array( &$url, &$text, &$link ) ); + if(!$success) { + wfDebug("Hook LinkerMakeExternalLink changed the output |