diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2010-07-28 11:52:48 +0200 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2010-07-28 11:52:48 +0200 |
commit | 222b01f5169f1c7e69762e0e8904c24f78f71882 (patch) | |
tree | 8e932e12546bb991357ec48eb1638d1770be7a35 /includes | |
parent | 00ab76a6b686e98a914afc1975812d2b1aaa7016 (diff) |
update to MediaWiki 1.16.0
Diffstat (limited to 'includes')
357 files changed, 47259 insertions, 28880 deletions
diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index c489cf1c..5bd7cfa4 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -7,7 +7,7 @@ * Handle ajax requests and send them to the proper handler. */ -if( !(defined( 'MEDIAWIKI' ) && $wgUseAjax ) ) { +if ( !( defined( 'MEDIAWIKI' ) && $wgUseAjax ) ) { die( 1 ); } @@ -33,11 +33,11 @@ class AjaxDispatcher { $this->mode = ""; - if (! empty($_GET["rs"])) { + if ( ! empty( $_GET["rs"] ) ) { $this->mode = "get"; } - if (!empty($_POST["rs"])) { + if ( !empty( $_POST["rs"] ) ) { $this->mode = "post"; } @@ -45,7 +45,7 @@ class AjaxDispatcher { case 'get': $this->func_name = isset( $_GET["rs"] ) ? $_GET["rs"] : ''; - if (! empty($_GET["rsargs"])) { + if ( ! empty( $_GET["rsargs"] ) ) { $this->args = $_GET["rsargs"]; } else { $this->args = array(); @@ -54,7 +54,7 @@ class AjaxDispatcher { case 'post': $this->func_name = isset( $_POST["rs"] ) ? $_POST["rs"] : ''; - if (! empty($_POST["rsargs"])) { + if ( ! empty( $_POST["rsargs"] ) ) { $this->args = $_POST["rsargs"]; } else { $this->args = array(); @@ -65,7 +65,7 @@ class AjaxDispatcher { wfProfileOut( __METHOD__ ); return; # Or we could throw an exception: - #throw new MWException( __METHOD__ . ' called without any data (mode empty).' ); + # throw new MWException( __METHOD__ . ' called without any data (mode empty).' ); } @@ -83,9 +83,10 @@ class AjaxDispatcher { if ( empty( $this->mode ) ) { return; } + wfProfileIn( __METHOD__ ); - if (! in_array( $this->func_name, $wgAjaxExportList ) ) { + if ( ! in_array( $this->func_name, $wgAjaxExportList ) ) { wfDebug( __METHOD__ . ' Bad Request for unknown function ' . $this->func_name . "\n" ); wfHttpError( 400, 'Bad Request', @@ -99,11 +100,11 @@ class AjaxDispatcher { $func = $this->func_name; } try { - $result = call_user_func_array($func, $this->args); + $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 ) . "): " + 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', @@ -111,7 +112,7 @@ class AjaxDispatcher { } else { if ( is_string( $result ) ) { - $result= new AjaxResponse( $result ); + $result = new AjaxResponse( $result ); } $result->sendHeaders(); @@ -120,12 +121,12 @@ class AjaxDispatcher { 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" ); + } 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()) { + if ( !headers_sent() ) { wfHttpError( 500, 'Internal Error', $e->getMessage() ); } else { diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php index 1a9adbca..e3180e0a 100644 --- a/includes/AjaxFunctions.php +++ b/includes/AjaxFunctions.php @@ -4,7 +4,7 @@ * @ingroup Ajax */ -if( !defined( 'MEDIAWIKI' ) ) { +if ( !defined( 'MEDIAWIKI' ) ) { die( 1 ); } @@ -14,31 +14,31 @@ if( !defined( 'MEDIAWIKI' ) ) { * Modified function from http://pure-essence.net/stuff/code/utf8RawUrlDecode.phps * * @param $source String escaped with Javascript's escape() function - * @param $iconv_to String destination character set will be used as second parameter + * @param $iconv_to String destination character set will be used as second parameter * in the iconv function. Default is UTF-8. * @return string */ -function js_unescape($source, $iconv_to = 'UTF-8') { +function js_unescape( $source, $iconv_to = 'UTF-8' ) { $decodedStr = ''; $pos = 0; - $len = strlen ($source); + $len = strlen ( $source ); - while ($pos < $len) { - $charAt = substr ($source, $pos, 1); - if ($charAt == '%') { + while ( $pos < $len ) { + $charAt = substr ( $source, $pos, 1 ); + if ( $charAt == '%' ) { $pos++; - $charAt = substr ($source, $pos, 1); - if ($charAt == 'u') { + $charAt = substr ( $source, $pos, 1 ); + if ( $charAt == 'u' ) { // we got a unicode character $pos++; - $unicodeHexVal = substr ($source, $pos, 4); - $unicode = hexdec ($unicodeHexVal); - $decodedStr .= code2utf($unicode); + $unicodeHexVal = substr ( $source, $pos, 4 ); + $unicode = hexdec ( $unicodeHexVal ); + $decodedStr .= code2utf( $unicode ); $pos += 4; } else { // we have an escaped ascii character - $hexVal = substr ($source, $pos, 2); - $decodedStr .= chr (hexdec ($hexVal)); + $hexVal = substr ( $source, $pos, 2 ); + $decodedStr .= chr ( hexdec ( $hexVal ) ); $pos += 2; } } else { @@ -47,8 +47,8 @@ function js_unescape($source, $iconv_to = 'UTF-8') { } } - if ($iconv_to != "UTF-8") { - $decodedStr = iconv("UTF-8", $iconv_to, $decodedStr); + if ( $iconv_to != "UTF-8" ) { + $decodedStr = iconv( "UTF-8", $iconv_to, $decodedStr ); } return $decodedStr; @@ -61,16 +61,16 @@ function js_unescape($source, $iconv_to = 'UTF-8') { * @param $num Integer * @return utf8char */ -function code2utf($num){ - if ( $num<128 ) - return chr($num); - if ( $num<2048 ) - return chr(($num>>6)+192).chr(($num&63)+128); - if ( $num<65536 ) - return chr(($num>>12)+224).chr((($num>>6)&63)+128).chr(($num&63)+128); - if ( $num<2097152 ) - return chr(($num>>18)+240).chr((($num>>12)&63)+128).chr((($num>>6)&63)+128) .chr(($num&63)+128); - return ''; +function code2utf( $num ) { + if ( $num < 128 ) + return chr( $num ); + if ( $num < 2048 ) + return chr( ( $num >> 6 ) + 192 ) . chr( ( $num&63 ) + 128 ); + if ( $num < 65536 ) + return chr( ( $num >> 12 ) + 224 ) . chr( ( ( $num >> 6 )&63 ) + 128 ) . chr( ( $num&63 ) + 128 ); + if ( $num < 2097152 ) + return chr( ( $num >> 18 ) + 240 ) . chr( ( ( $num >> 12 )&63 ) + 128 ) . chr( ( ( $num >> 6 )&63 ) + 128 ) . chr( ( $num&63 ) + 128 ); + return ''; } /** @@ -81,49 +81,49 @@ function code2utf($num){ * respectively, followed by an HTML message to display in the alert box; or * '<err#>' on error */ -function wfAjaxWatch($pagename = "", $watch = "") { - if(wfReadOnly()) { +function wfAjaxWatch( $pagename = "", $watch = "" ) { + if ( wfReadOnly() ) { // redirect to action=(un)watch, which will display the database lock // message return '<err#>'; } - if('w' !== $watch && 'u' !== $watch) { + if ( 'w' !== $watch && 'u' !== $watch ) { return '<err#>'; } $watch = 'w' === $watch; - $title = Title::newFromDBkey($pagename); - if(!$title) { + $title = Title::newFromDBkey( $pagename ); + if ( !$title ) { // Invalid title return '<err#>'; } - $article = new Article($title); + $article = new Article( $title ); $watching = $title->userIsWatching(); - if($watch) { - if(!$watching) { - $dbw = wfGetDB(DB_MASTER); + if ( $watch ) { + if ( !$watching ) { + $dbw = wfGetDB( DB_MASTER ); $dbw->begin(); $ok = $article->doWatch(); $dbw->commit(); } } else { - if($watching) { - $dbw = wfGetDB(DB_MASTER); + if ( $watching ) { + $dbw = wfGetDB( DB_MASTER ); $dbw->begin(); $ok = $article->doUnwatch(); $dbw->commit(); } } // Something stopped the change - if( isset($ok) && !$ok ) { + if ( isset( $ok ) && !$ok ) { return '<err#>'; } - if( $watch ) { - return '<w#>'.wfMsgExt( 'addedwatchtext', array( 'parse' ), $title->getPrefixedText() ); + if ( $watch ) { + return '<w#>' . wfMsgExt( 'addedwatchtext', array( 'parse' ), $title->getPrefixedText() ); } else { - return '<u#>'.wfMsgExt( 'removedwatchtext', array( 'parse' ), $title->getPrefixedText() ); + return '<u#>' . wfMsgExt( 'removedwatchtext', array( 'parse' ), $title->getPrefixedText() ); } } @@ -133,12 +133,12 @@ function wfAjaxWatch($pagename = "", $watch = "") { */ function wfAjaxGetThumbnailUrl( $file, $width, $height ) { $file = wfFindFile( $file ); - + if ( !$file || !$file->exists() ) return null; - + $url = $file->getThumbnail( $width, $height )->url; - + return $url; } @@ -148,11 +148,11 @@ function wfAjaxGetThumbnailUrl( $file, $width, $height ) { */ function wfAjaxGetFileUrl( $file ) { $file = wfFindFile( $file ); - + if ( !$file || !$file->exists() ) return null; - + $url = $file->getUrl(); - + return $url; -}
\ No newline at end of file +} diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index 26b6f443..f7495666 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -4,14 +4,14 @@ * @ingroup Ajax */ -if( !defined( 'MEDIAWIKI' ) ) { +if ( !defined( 'MEDIAWIKI' ) ) { die( 1 ); } /** * Handle responses for Ajax requests (send headers, print * content, that sort of thing) - * + * * @ingroup Ajax */ class AjaxResponse { @@ -37,15 +37,15 @@ class AjaxResponse { /** Content of our HTTP response */ private $mText; - function __construct( $text = NULL ) { - $this->mCacheDuration = NULL; - $this->mVary = NULL; + function __construct( $text = null ) { + $this->mCacheDuration = null; + $this->mVary = null; $this->mDisabled = false; $this->mText = ''; $this->mResponseCode = '200 OK'; $this->mLastModified = false; - $this->mContentType= 'application/x-wiki'; + $this->mContentType = 'application/x-wiki'; if ( $text ) { $this->addText( $text ); @@ -95,13 +95,13 @@ class AjaxResponse { header( "Status: " . $this->mResponseCode, true, (int)$n ); } - header ("Content-Type: " . $this->mContentType ); + header ( "Content-Type: " . $this->mContentType ); if ( $this->mLastModified ) { - header ("Last-Modified: " . $this->mLastModified ); + header ( "Last-Modified: " . $this->mLastModified ); } else { - header ("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); + header ( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" ); } if ( $this->mCacheDuration ) { @@ -110,31 +110,31 @@ class AjaxResponse { # 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. - if( $wgUseSquid ) { + if ( $wgUseSquid ) { # Expect explicite purge of the proxy cache, but require end user agents # to revalidate against the proxy on each visit. # Surrogate-Control controls our Squid, Cache-Control downstream caches if ( $wgUseESI ) { - header( 'Surrogate-Control: max-age='.$this->mCacheDuration.', content="ESI/1.0"'); + header( 'Surrogate-Control: max-age=' . $this->mCacheDuration . ', content="ESI/1.0"' ); header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); } else { - header( 'Cache-Control: s-maxage='.$this->mCacheDuration.', must-revalidate, max-age=0' ); + header( 'Cache-Control: s-maxage=' . $this->mCacheDuration . ', must-revalidate, max-age=0' ); } } else { # Let the client do the caching. Cache is not purged. - header ("Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->mCacheDuration ) . " GMT"); - header ("Cache-Control: s-max-age={$this->mCacheDuration},public,max-age={$this->mCacheDuration}"); + header ( "Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->mCacheDuration ) . " GMT" ); + header ( "Cache-Control: s-max-age={$this->mCacheDuration},public,max-age={$this->mCacheDuration}" ); } } else { # always expired, always modified - header ("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Date in the past - header ("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 - header ("Pragma: no-cache"); // HTTP/1.0 + header ( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); // Date in the past + header ( "Cache-Control: no-cache, must-revalidate" ); // HTTP/1.1 + header ( "Pragma: no-cache" ); // HTTP/1.0 } if ( $this->mVary ) { @@ -156,11 +156,11 @@ class AjaxResponse { wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP\n" ); return; } - if( !$wgCachePages ) { + if ( !$wgCachePages ) { wfDebug( "$fname: CACHE DISABLED\n", false ); return; } - if( $wgUser->getOption( 'nocache' ) ) { + if ( $wgUser->getOption( 'nocache' ) ) { wfDebug( "$fname: USER DISABLED CACHE\n", false ); return; } @@ -168,7 +168,7 @@ class AjaxResponse { $timestamp = wfTimestamp( TS_MW, $timestamp ); $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->mTouched, $wgCacheEpoch ) ); - if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { + if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { # IE sends sizes after the date like this: # Wed, 20 Aug 2003 06:51:19 GMT; length=5202 # this breaks strtotime(). @@ -177,8 +177,8 @@ class AjaxResponse { $ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 ); wfDebug( "$fname: -- client send If-Modified-Since: " . $modsince . "\n", false ); wfDebug( "$fname: -- we might send Last-Modified : $lastmod\n", false ); - if( ($ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) { - ini_set('zlib.output_compression', 0); + if ( ( $ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) { + ini_set( 'zlib.output_compression', 0 ); $this->setResponseCode( "304 Not Modified" ); $this->disable(); $this->mLastModified = $lastmod; diff --git a/includes/Article.php b/includes/Article.php index ef219ea3..d3863c77 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -16,30 +16,32 @@ class Article { /**@{{ * @private */ - var $mComment = ''; //!< - var $mContent; //!< - var $mContentLoaded = false; //!< - var $mCounter = -1; //!< Not loaded - var $mCurID = -1; //!< Not loaded - var $mDataLoaded = false; //!< - var $mForUpdate = false; //!< - var $mGoodAdjustment = 0; //!< - var $mIsRedirect = false; //!< - var $mLatest = false; //!< - var $mMinorEdit; //!< - var $mOldId; //!< - var $mPreparedEdit = false; //!< Title object if set - var $mRedirectedFrom = null; //!< Title object if set - var $mRedirectTarget = null; //!< Title object if set - var $mRedirectUrl = false; //!< - var $mRevIdFetched = 0; //!< - var $mRevision; //!< - var $mTimestamp = ''; //!< - var $mTitle; //!< - var $mTotalAdjustment = 0; //!< - var $mTouched = '19700101000000'; //!< - var $mUser = -1; //!< Not loaded - var $mUserText = ''; //!< + var $mComment = ''; // !< + var $mContent; // !< + var $mContentLoaded = false; // !< + var $mCounter = -1; // !< Not loaded + var $mCurID = -1; // !< Not loaded + var $mDataLoaded = false; // !< + var $mForUpdate = false; // !< + var $mGoodAdjustment = 0; // !< + var $mIsRedirect = false; // !< + var $mLatest = false; // !< + var $mMinorEdit; // !< + var $mOldId; // !< + var $mPreparedEdit = false; // !< Title object if set + var $mRedirectedFrom = null; // !< Title object if set + var $mRedirectTarget = null; // !< Title object if set + var $mRedirectUrl = false; // !< + var $mRevIdFetched = 0; // !< + var $mRevision; // !< + var $mTimestamp = ''; // !< + var $mTitle; // !< + var $mTotalAdjustment = 0; // !< + var $mTouched = '19700101000000'; // !< + var $mUser = -1; // !< Not loaded + var $mUserText = ''; // !< + var $mParserOptions; // !< + var $mParserOutput; // !< /**@}}*/ /** @@ -58,7 +60,9 @@ class Article { */ public static function newFromID( $id ) { $t = Title::newFromID( $id ); - return $t == null ? null : new Article( $t ); + # FIXME: doesn't inherit right + return $t == null ? null : new self( $t ); + # return $t == null ? null : new static( $t ); // PHP 5.3 } /** @@ -78,19 +82,19 @@ class Article { * @return mixed Title object, or null if this page is not a redirect */ public function getRedirectTarget() { - if( !$this->mTitle || !$this->mTitle->isRedirect() ) + if ( !$this->mTitle || !$this->mTitle->isRedirect() ) return null; - if( !is_null($this->mRedirectTarget) ) + if ( !is_null( $this->mRedirectTarget ) ) return $this->mRedirectTarget; # Query the redirect table $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'redirect', - array('rd_namespace', 'rd_title'), - array('rd_from' => $this->getID() ), + array( 'rd_namespace', 'rd_title' ), + array( 'rd_from' => $this->getID() ), __METHOD__ ); - if( $row ) { - return $this->mRedirectTarget = Title::makeTitle($row->rd_namespace, $row->rd_title); + 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(); @@ -104,15 +108,15 @@ class Article { */ public function insertRedirect() { $retval = Title::newFromRedirect( $this->getContent() ); - if( !$retval ) { + if ( !$retval ) { return null; } $dbw = wfGetDB( DB_MASTER ); - $dbw->replace( 'redirect', array('rd_from'), + $dbw->replace( 'redirect', array( 'rd_from' ), array( 'rd_from' => $this->getID(), 'rd_namespace' => $retval->getNamespace(), - 'rd_title' => $retval->getDBKey() + 'rd_title' => $retval->getDBkey() ), __METHOD__ ); @@ -137,9 +141,9 @@ class Article { public function followRedirectText( $text ) { $rt = Title::newFromRedirectRecurse( $text ); // recurse through to only get the final target # process if title object is valid and not special:userlogout - if( $rt ) { - if( $rt->getInterwiki() != '' ) { - if( $rt->isLocal() ) { + if ( $rt ) { + if ( $rt->getInterwiki() != '' ) { + if ( $rt->isLocal() ) { // Offsite wikis need an HTTP redirect. // // This can be hard to reverse and may produce loops, @@ -148,13 +152,13 @@ class Article { return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) ); } } else { - if( $rt->getNamespace() == NS_SPECIAL ) { + if ( $rt->getNamespace() == NS_SPECIAL ) { // Gotta handle redirects to special pages differently: // Fill the HTTP response "Location" header and ignore // the rest of the page we're on. // // This can be hard to reverse, so they may be disabled. - if( $rt->isSpecial( 'Userlogout' ) ) { + if ( $rt->isSpecial( 'Userlogout' ) ) { // rolleyes } else { return $rt->getFullURL(); @@ -203,19 +207,19 @@ class Article { * the shortcut in Article::followContent() * * @return Return the text of this revision - */ + */ public function getContent() { global $wgUser, $wgContLang, $wgOut, $wgMessageCache; wfProfileIn( __METHOD__ ); - if( $this->getID() === 0 ) { + if ( $this->getID() === 0 ) { # If this is a MediaWiki:x message, then load the messages # and return the message value for x. - if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { # If this is a system message, get the default text. list( $message, $lang ) = $wgMessageCache->figureMessage( $wgContLang->lcfirst( $this->mTitle->getText() ) ); $wgMessageCache->loadAllMessages( $lang ); $text = wfMsgGetKey( $message, false, $lang, false ); - if( wfEmptyMsg( $message, $text ) ) + if ( wfEmptyMsg( $message, $text ) ) $text = ''; } else { $text = wfMsgExt( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' ); @@ -228,15 +232,15 @@ class Article { return $this->mContent; } } - + /** * Get the text of the current revision. No side-effects... * * @return Return the text of the current revision - */ + */ public function getRawText() { // Check process cache for current revision - if( $this->mContentLoaded && $this->mOldId == 0 ) { + if ( $this->mContentLoaded && $this->mOldId == 0 ) { return $this->mContent; } $rev = Revision::newFromTitle( $this->mTitle ); @@ -260,12 +264,12 @@ class Article { global $wgParser; return $wgParser->getSection( $text, $section ); } - + /** * Get the text that needs to be saved in order to undo all revisions * between $undo and $undoafter. Revisions must belong to the same page, * must exist and must not be deleted - * @param $undo Revision + * @param $undo Revision * @param $undoafter Revision Must be an earlier revision than $undo * @return mixed string on success, false on failure */ @@ -288,7 +292,7 @@ class Article { * current revision */ public function getOldID() { - if( is_null( $this->mOldId ) ) { + if ( is_null( $this->mOldId ) ) { $this->mOldId = $this->getOldIDFromRequest(); } return $this->mOldId; @@ -303,23 +307,23 @@ class Article { global $wgRequest; $this->mRedirectUrl = false; $oldid = $wgRequest->getVal( 'oldid' ); - if( isset( $oldid ) ) { + if ( isset( $oldid ) ) { $oldid = intval( $oldid ); - if( $wgRequest->getVal( 'direction' ) == 'next' ) { + if ( $wgRequest->getVal( 'direction' ) == 'next' ) { $nextid = $this->mTitle->getNextRevisionID( $oldid ); - if( $nextid ) { + if ( $nextid ) { $oldid = $nextid; } else { $this->mRedirectUrl = $this->mTitle->getFullURL( 'redirect=no' ); } - } elseif( $wgRequest->getVal( 'direction' ) == 'prev' ) { + } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) { $previd = $this->mTitle->getPreviousRevisionID( $oldid ); - if( $previd ) { + if ( $previd ) { $oldid = $previd; } } } - if( !$oldid ) { + if ( !$oldid ) { $oldid = 0; } return $oldid; @@ -329,7 +333,7 @@ class Article { * Load the revision (including text) into this object */ function loadContent() { - if( $this->mContentLoaded ) return; + if ( $this->mContentLoaded ) return; wfProfileIn( __METHOD__ ); # Query variables :P $oldid = $this->getOldID(); @@ -396,26 +400,26 @@ class Article { * @param $data Database row object or "fromdb" */ public function loadPageData( $data = 'fromdb' ) { - if( $data === 'fromdb' ) { + if ( $data === 'fromdb' ) { $dbr = wfGetDB( DB_MASTER ); $data = $this->pageDataFromId( $dbr, $this->getId() ); } $lc = LinkCache::singleton(); - if( $data ) { + if ( $data ) { $lc->addGoodLinkObj( $data->page_id, $this->mTitle, $data->page_len, $data->page_is_redirect ); - $this->mTitle->mArticleID = $data->page_id; + $this->mTitle->mArticleID = intval( $data->page_id ); # Old-fashioned restrictions $this->mTitle->loadRestrictions( $data->page_restrictions ); - $this->mCounter = $data->page_counter; + $this->mCounter = intval( $data->page_counter ); $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); - $this->mIsRedirect = $data->page_is_redirect; - $this->mLatest = $data->page_latest; + $this->mIsRedirect = intval( $data->page_is_redirect ); + $this->mLatest = intval( $data->page_latest ); } else { - if( is_object( $this->mTitle ) ) { + if ( is_object( $this->mTitle ) ) { $lc->addBadLinkObj( $this->mTitle ); } $this->mTitle->mArticleID = 0; @@ -431,7 +435,7 @@ class Article { * @return string */ function fetchContent( $oldid = 0 ) { - if( $this->mContentLoaded ) { + if ( $this->mContentLoaded ) { return $this->mContent; } @@ -441,33 +445,33 @@ class Article { # fails we'll have something telling us what we intended. $t = $this->mTitle->getPrefixedText(); $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : ''; - $this->mContent = wfMsg( 'missing-article', $t, $d ) ; + $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ; - if( $oldid ) { + if ( $oldid ) { $revision = Revision::newFromId( $oldid ); - if( is_null( $revision ) ) { - wfDebug( __METHOD__." failed to retrieve specified revision, id $oldid\n" ); + if ( is_null( $revision ) ) { + wfDebug( __METHOD__ . " failed to retrieve specified revision, id $oldid\n" ); return false; } $data = $this->pageDataFromId( $dbr, $revision->getPage() ); - if( !$data ) { - wfDebug( __METHOD__." failed to get page data linked to revision id $oldid\n" ); + if ( !$data ) { + wfDebug( __METHOD__ . " failed to get page data linked to revision id $oldid\n" ); return false; } $this->mTitle = Title::makeTitle( $data->page_namespace, $data->page_title ); $this->loadPageData( $data ); } else { - if( !$this->mDataLoaded ) { + if ( !$this->mDataLoaded ) { $data = $this->pageDataFromTitle( $dbr, $this->mTitle ); - if( !$data ) { - wfDebug( __METHOD__." failed to find page data for title " . $this->mTitle->getPrefixedText() . "\n" ); + if ( !$data ) { + wfDebug( __METHOD__ . " failed to find page data for title " . $this->mTitle->getPrefixedText() . "\n" ); return false; } $this->loadPageData( $data ); } $revision = Revision::newFromId( $this->mLatest ); - if( is_null( $revision ) ) { - wfDebug( __METHOD__." failed to retrieve current page, rev_id {$this->mLatest}\n" ); + if ( is_null( $revision ) ) { + wfDebug( __METHOD__ . " failed to retrieve current page, rev_id {$this->mLatest}\n" ); return false; } } @@ -495,7 +499,7 @@ class Article { * * @param $x Mixed: FIXME */ - public function forUpdate( $x = NULL ) { + public function forUpdate( $x = null ) { return wfSetVar( $this->mForUpdate, $x ); } @@ -518,8 +522,8 @@ class Article { * @return Array: options */ protected function getSelectOptions( $options = '' ) { - if( $this->mForUpdate ) { - if( is_array( $options ) ) { + if ( $this->mForUpdate ) { + if ( is_array( $options ) ) { $options[] = 'FOR UPDATE'; } else { $options = 'FOR UPDATE'; @@ -532,7 +536,7 @@ class Article { * @return int Page ID */ public function getID() { - if( $this->mTitle ) { + if ( $this->mTitle ) { return $this->mTitle->getArticleID(); } else { return 0; @@ -545,7 +549,7 @@ class Article { public function exists() { return $this->getId() > 0; } - + /** * Check if this page is something we're going to be showing * some sort of sensible content for. If we return false, page @@ -562,16 +566,16 @@ class Article { * @return int The view count for the page */ public function getCount() { - if( -1 == $this->mCounter ) { + if ( -1 == $this->mCounter ) { $id = $this->getID(); - if( $id == 0 ) { + if ( $id == 0 ) { $this->mCounter = 0; } else { $dbr = wfGetDB( DB_SLAVE ); - $this->mCounter = $dbr->selectField( 'page', - 'page_counter', - array( 'page_id' => $id ), - __METHOD__, + $this->mCounter = $dbr->selectField( 'page', + 'page_counter', + array( 'page_id' => $id ), + __METHOD__, $this->getSelectOptions() ); } @@ -590,7 +594,7 @@ class Article { global $wgUseCommaCount; $token = $wgUseCommaCount ? ',' : '[['; - return $this->mTitle->isContentPage() && !$this->isRedirect($text) && in_string($token,$text); + return $this->mTitle->isContentPage() && !$this->isRedirect( $text ) && in_string( $token, $text ); } /** @@ -600,8 +604,8 @@ class Article { * @return bool */ public function isRedirect( $text = false ) { - if( $text === false ) { - if( $this->mDataLoaded ) { + if ( $text === false ) { + if ( $this->mDataLoaded ) { return $this->mIsRedirect; } // Apparently loadPageData was never called @@ -610,7 +614,7 @@ class Article { } else { $titleObj = Title::newFromRedirect( $text ); } - return $titleObj !== NULL; + return $titleObj !== null; } /** @@ -620,10 +624,10 @@ class Article { */ public function isCurrent() { # If no oldid, this is the current version. - if( $this->getOldID() == 0 ) { + if ( $this->getOldID() == 0 ) { return true; } - return $this->exists() && isset($this->mRevision) && $this->mRevision->isCurrent(); + return $this->exists() && isset( $this->mRevision ) && $this->mRevision->isCurrent(); } /** @@ -631,15 +635,15 @@ class Article { * This isn't necessary for all uses, so it's only done if needed. */ protected function loadLastEdit() { - if( -1 != $this->mUser ) + if ( -1 != $this->mUser ) return; # New or non-existent articles have no user information $id = $this->getID(); - if( 0 == $id ) return; + if ( 0 == $id ) return; $this->mLastRevision = Revision::loadFromPageId( wfGetDB( DB_MASTER ), $id ); - if( !is_null( $this->mLastRevision ) ) { + if ( !is_null( $this->mLastRevision ) ) { $this->mUser = $this->mLastRevision->getUser(); $this->mUserText = $this->mLastRevision->getUserText(); $this->mTimestamp = $this->mLastRevision->getTimestamp(); @@ -651,10 +655,10 @@ class Article { public function getTimestamp() { // Check if the field has been filled by ParserCache::get() - if( !$this->mTimestamp ) { + if ( !$this->mTimestamp ) { $this->loadLastEdit(); } - return wfTimestamp(TS_MW, $this->mTimestamp); + return wfTimestamp( TS_MW, $this->mTimestamp ); } public function getUser() { @@ -687,69 +691,77 @@ class Article { * @param $limit Integer: default 0. * @param $offset Integer: default 0. */ - public function getContributors($limit = 0, $offset = 0) { + public function getContributors( $limit = 0, $offset = 0 ) { # XXX: this is expensive; cache this info somewhere. - $contribs = array(); $dbr = wfGetDB( DB_SLAVE ); $revTable = $dbr->tableName( 'revision' ); $userTable = $dbr->tableName( 'user' ); - $user = $this->getUser(); + $pageId = $this->getId(); - $hideBit = Revision::DELETED_USER; // username hidden? + $user = $this->getUser(); + if ( $user ) { + $excludeCond = "AND rev_user != $user"; + } else { + $userText = $dbr->addQuotes( $this->getUserText() ); + $excludeCond = "AND rev_user_text != $userText"; + } - $sql = "SELECT {$userTable}.*, MAX(rev_timestamp) as timestamp + $deletedBit = $dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER ); // username hidden? + + $sql = "SELECT {$userTable}.*, rev_user_text as user_name, MAX(rev_timestamp) as timestamp FROM $revTable LEFT JOIN $userTable ON rev_user = user_id WHERE rev_page = $pageId - AND rev_user != $user - AND rev_deleted & $hideBit = 0 - GROUP BY rev_user, rev_user_text, user_real_name + $excludeCond + AND $deletedBit = 0 + GROUP BY rev_user, rev_user_text ORDER BY timestamp DESC"; - if($limit > 0) { $sql .= ' LIMIT '.$limit; } - if($offset > 0) { $sql .= ' OFFSET '.$offset; } - - $sql .= ' '. $this->getSelectOptions(); + if ( $limit > 0 ) + $sql = $dbr->limitResult( $sql, $limit, $offset ); - $res = $dbr->query($sql, __METHOD__ ); + $sql .= ' ' . $this->getSelectOptions(); + $res = $dbr->query( $sql, __METHOD__ ); return new UserArrayFromResult( $res ); } /** - * This is the default action of the script: just view the page of - * the given title. - */ + * This is the default action of the index.php entry point: just view the + * page of the given title. + */ public function view() { global $wgUser, $wgOut, $wgRequest, $wgContLang; global $wgEnableParserCache, $wgStylePath, $wgParser; - global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies; - global $wgDefaultRobotPolicy; + global $wgUseTrackbacks, $wgUseFileCache; - # Let the parser know if this is the printable version - if( $wgOut->isPrintable() ) { - $wgOut->parserOptions()->setIsPrintable( true ); - } - wfProfileIn( __METHOD__ ); # Get variables from query string $oldid = $this->getOldID(); + $parserCache = ParserCache::singleton(); + + $parserOptions = clone $this->getParserOptions(); + # Render printable version, use printable version cache + if ( $wgOut->isPrintable() ) { + $parserOptions->setIsPrintable( true ); + } # Try client and file cache - if( $oldid === 0 && $this->checkTouched() ) { + if ( $oldid === 0 && $this->checkTouched() ) { global $wgUseETag; - if( $wgUseETag ) { - $parserCache = ParserCache::singleton(); - $wgOut->setETag( $parserCache->getETag($this, $wgOut->parserOptions()) ); + if ( $wgUseETag ) { + $wgOut->setETag( $parserCache->getETag( $this, $parserOptions ) ); } # Is is client cached? - if( $wgOut->checkLastModified( $this->getTouched() ) ) { + if ( $wgOut->checkLastModified( $this->getTouched() ) ) { + wfDebug( __METHOD__ . ": done 304\n" ); wfProfileOut( __METHOD__ ); return; # Try file cache - } else if( $this->tryFileCache() ) { + } else if ( $wgUseFileCache && $this->tryFileCache() ) { + wfDebug( __METHOD__ . ": done file cache\n" ); # tell wgOut that output is taken care of $wgOut->disable(); $this->viewUpdates(); @@ -758,91 +770,355 @@ class Article { } } - $ns = $this->mTitle->getNamespace(); # shortcut $sk = $wgUser->getSkin(); # getOldID may want us to redirect somewhere else - if( $this->mRedirectUrl ) { + if ( $this->mRedirectUrl ) { $wgOut->redirect( $this->mRedirectUrl ); + wfDebug( __METHOD__ . ": redirecting due to oldid\n" ); wfProfileOut( __METHOD__ ); return; } + $wgOut->setArticleFlag( true ); + # Set page title (may be overridden by DISPLAYTITLE) + $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + + # If we got diff in the query, we want to see a diff page instead of the article. + if ( !is_null( $wgRequest->getVal( 'diff' ) ) ) { + wfDebug( __METHOD__ . ": showing diff page\n" ); + $this->showDiffPage(); + wfProfileOut( __METHOD__ ); + return; + } + + # Should the parser cache be used? + $useParserCache = $this->useParserCache( $oldid ); + wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); + if ( $wgUser->getOption( 'stubthreshold' ) ) { + wfIncrStats( 'pcache_miss_stub' ); + } + + $wasRedirected = $this->showRedirectedFromHeader(); + $this->showNamespaceHeader(); + + # Iterate through the possible ways of constructing the output text. + # Keep going until $outputDone is set, or we run out of things to do. + $pass = 0; + $outputDone = false; + $this->mParserOutput = false; + while ( !$outputDone && ++$pass ) { + switch( $pass ) { + case 1: + wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$useParserCache ) ); + break; + + case 2: + # Try the parser cache + if ( $useParserCache ) { + $this->mParserOutput = $parserCache->get( $this, $parserOptions ); + if ( $this->mParserOutput !== false ) { + wfDebug( __METHOD__ . ": showing parser cache contents\n" ); + $wgOut->addParserOutput( $this->mParserOutput ); + # Ensure that UI elements requiring revision ID have + # the correct version information. + $wgOut->setRevisionId( $this->mLatest ); + $outputDone = true; + } + } + break; + + case 3: + $text = $this->getContent(); + if ( $text === false || $this->getID() == 0 ) { + wfDebug( __METHOD__ . ": showing missing article\n" ); + $this->showMissingArticle(); + wfProfileOut( __METHOD__ ); + return; + } + + # Another whitelist check in case oldid is altering the title + if ( !$this->mTitle->userCanRead() ) { + wfDebug( __METHOD__ . ": denied on secondary read check\n" ); + $wgOut->loginToUse(); + $wgOut->output(); + $wgOut->disable(); + wfProfileOut( __METHOD__ ); + return; + } + + # Are we looking at an old revision + if ( $oldid && !is_null( $this->mRevision ) ) { + $this->setOldSubtitle( $oldid ); + if ( !$this->showDeletedRevisionHeader() ) { + wfDebug( __METHOD__ . ": cannot view deleted revision\n" ); + wfProfileOut( __METHOD__ ); + return; + } + # If this "old" version is the current, then try the parser cache... + if ( $oldid === $this->getLatest() && $this->useParserCache( false ) ) { + $this->mParserOutput = $parserCache->get( $this, $parserOptions ); + if ( $this->mParserOutput ) { + wfDebug( __METHOD__ . ": showing parser cache for current rev permalink\n" ); + $wgOut->addParserOutput( $this->mParserOutput ); + $wgOut->setRevisionId( $this->mLatest ); + $this->showViewFooter(); + $this->viewUpdates(); + wfProfileOut( __METHOD__ ); + return; + } + } + } + + # Ensure that UI elements requiring revision ID have + # the correct version information. + $wgOut->setRevisionId( $this->getRevIdFetched() ); + + # Pages containing custom CSS or JavaScript get special treatment + if ( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) { + wfDebug( __METHOD__ . ": showing CSS/JS source\n" ); + $this->showCssOrJsPage(); + $outputDone = true; + } else if ( $rt = Title::newFromRedirectArray( $text ) ) { + wfDebug( __METHOD__ . ": showing redirect=no page\n" ); + # Viewing a redirect page (e.g. with parameter redirect=no) + # Don't append the subtitle if this was an old revision + $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) ); + # Parse just to get categories, displaytitle, etc. + $this->mParserOutput = $wgParser->parse( $text, $this->mTitle, $parserOptions ); + $wgOut->addParserOutputNoText( $this->mParserOutput ); + $outputDone = true; + } + break; + + case 4: + # Run the parse, protected by a pool counter + wfDebug( __METHOD__ . ": doing uncached parse\n" ); + $key = $parserCache->getKey( $this, $parserOptions ); + $poolCounter = PoolCounter::factory( 'Article::view', $key ); + $dirtyCallback = $useParserCache ? array( $this, 'tryDirtyCache' ) : false; + $status = $poolCounter->executeProtected( array( $this, 'doViewParse' ), $dirtyCallback ); + + if ( !$status->isOK() ) { + # Connection or timeout error + $this->showPoolError( $status ); + wfProfileOut( __METHOD__ ); + return; + } else { + $outputDone = true; + } + break; + + # Should be unreachable, but just in case... + default: + break 2; + } + } + + # Adjust the title if it was set by displaytitle, -{T|}- or language conversion + if ( $this->mParserOutput ) { + $titleText = $this->mParserOutput->getTitleText(); + if ( strval( $titleText ) !== '' ) { + $wgOut->setPageTitle( $titleText ); + } + } + + # For the main page, overwrite the <title> element with the con- + # tents of 'pagetitle-view-mainpage' instead of the default (if + # that's not empty). + if ( $this->mTitle->equals( Title::newMainPage() ) + && ( $m = wfMsgForContent( 'pagetitle-view-mainpage' ) ) !== '' ) + { + $wgOut->setHTMLTitle( $m ); + } + + # Now that we've filled $this->mParserOutput, we know whether + # there are any __NOINDEX__ tags on the page + $policy = $this->getRobotPolicy( 'view' ); + $wgOut->setIndexPolicy( $policy['index'] ); + $wgOut->setFollowPolicy( $policy['follow'] ); + + $this->showViewFooter(); + $this->viewUpdates(); + wfProfileOut( __METHOD__ ); + } + + /** + * Show a diff page according to current request variables. For use within + * Article::view() only, other callers should use the DifferenceEngine class. + */ + public function showDiffPage() { + global $wgOut, $wgRequest, $wgUser; + $diff = $wgRequest->getVal( 'diff' ); $rcid = $wgRequest->getVal( 'rcid' ); - $rdfrom = $wgRequest->getVal( 'rdfrom' ); $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); $purge = $wgRequest->getVal( 'action' ) == 'purge'; - $return404 = false; + $unhide = $wgRequest->getInt( 'unhide' ) == 1; + $oldid = $this->getOldID(); - $wgOut->setArticleFlag( true ); + $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $unhide ); + // DifferenceEngine directly fetched the revision: + $this->mRevIdFetched = $de->mNewid; + $de->showDiffPage( $diffOnly ); - # Discourage indexing of printable versions, but encourage following - if( $wgOut->isPrintable() ) { - $policy = 'noindex,follow'; - } elseif( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) { - $policy = $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()]; - } elseif( isset( $wgNamespaceRobotPolicies[$ns] ) ) { - # Honour customised robot policies for this namespace - $policy = $wgNamespaceRobotPolicies[$ns]; - } else { - $policy = $wgDefaultRobotPolicy; + // Needed to get the page's current revision + $this->loadPageData(); + if ( $diff == 0 || $diff == $this->mLatest ) { + # Run view updates for current revision only + $this->viewUpdates(); } - $wgOut->setRobotPolicy( $policy ); + } - # Allow admins to see deleted content if explicitly requested - $delId = $diff ? $diff : $oldid; - $unhide = $wgRequest->getInt('unhide') == 1 - && $wgUser->matchEditToken( $wgRequest->getVal('token'), $delId ); - # If we got diff and oldid in the query, we want to see a - # diff page instead of the article. - if( !is_null( $diff ) ) { - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + /** + * Show a page view for a page formatted as CSS or JavaScript. To be called by + * Article::view() only. + * + * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these + * page views. + */ + public function showCssOrJsPage() { + global $wgOut; + $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 + $m = array(); + preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m ); + $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" ); + $wgOut->addHTML( htmlspecialchars( $this->mContent ) ); + $wgOut->addHTML( "\n</pre>\n" ); + } + } - $htmldiff = $wgRequest->getVal( 'htmldiff' , false); - $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $htmldiff, $unhide ); - // DifferenceEngine directly fetched the revision: - $this->mRevIdFetched = $de->mNewid; - $de->showDiffPage( $diffOnly ); + /** + * Get the robot policy to be used for the current action=view request. + * @return String the policy that should be set + * @deprecated use getRobotPolicy() instead, which returns an associative + * array + */ + public function getRobotPolicyForView() { + wfDeprecated( __FUNC__ ); + $policy = $this->getRobotPolicy( 'view' ); + return $policy['index'] . ',' . $policy['follow']; + } - // Needed to get the page's current revision - $this->loadPageData(); - if( $diff == 0 || $diff == $this->mLatest ) { - # Run view updates for current revision only - $this->viewUpdates(); - } - wfProfileOut( __METHOD__ ); - return; - } - - if( $ns == NS_USER || $ns == NS_USER_TALK ) { - # User/User_talk subpages are not modified. (bug 11443) - if( !$this->mTitle->isSubpage() ) { + /** + * Get the robot policy to be used for the current view + * @param $action String the action= GET parameter + * @return Array the policy that should be set + * TODO: actions other than 'view' + */ + public function getRobotPolicy( $action ) { + + global $wgOut, $wgArticleRobotPolicies, $wgNamespaceRobotPolicies; + global $wgDefaultRobotPolicy, $wgRequest; + + $ns = $this->mTitle->getNamespace(); + if ( $ns == NS_USER || $ns == NS_USER_TALK ) { + # Don't index user and user talk pages for blocked users (bug 11443) + if ( !$this->mTitle->isSubpage() ) { $block = new Block(); - if( $block->load( $this->mTitle->getBaseText() ) ) { - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + if ( $block->load( $this->mTitle->getText() ) ) { + return array( 'index' => 'noindex', + 'follow' => 'nofollow' ); } } } - # Should the parser cache be used? - $pcache = $this->useParserCache( $oldid ); - wfDebug( 'Article::view using parser cache: ' . ($pcache ? 'yes' : 'no' ) . "\n" ); - if( $wgUser->getOption( 'stubthreshold' ) ) { - wfIncrStats( 'pcache_miss_stub' ); + if ( $this->getID() === 0 || $this->getOldID() ) { + # Non-articles (special pages etc), and old revisions + return array( 'index' => 'noindex', + 'follow' => 'nofollow' ); + } elseif ( $wgOut->isPrintable() ) { + # Discourage indexing of printable versions, but encourage following + return array( 'index' => 'noindex', + 'follow' => 'follow' ); + } elseif ( $wgRequest->getInt( 'curid' ) ) { + # For ?curid=x urls, disallow indexing + return array( 'index' => 'noindex', + 'follow' => 'follow' ); } - $wasRedirected = false; - if( isset( $this->mRedirectedFrom ) ) { + # Otherwise, construct the policy based on the various config variables. + $policy = self::formatRobotPolicy( $wgDefaultRobotPolicy ); + + if ( isset( $wgNamespaceRobotPolicies[$ns] ) ) { + # Honour customised robot policies for this namespace + $policy = array_merge( $policy, + self::formatRobotPolicy( $wgNamespaceRobotPolicies[$ns] ) ); + } + if ( $this->mTitle->canUseNoindex() && is_object( $this->mParserOutput ) && $this->mParserOutput->getIndexPolicy() ) { + # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates + # a final sanity check that we have really got the parser output. + $policy = array_merge( $policy, + array( 'index' => $this->mParserOutput->getIndexPolicy() ) ); + } + + if ( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) { + # (bug 14900) site config can override user-defined __INDEX__ or __NOINDEX__ + $policy = array_merge( $policy, + self::formatRobotPolicy( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ); + } + + return $policy; + + } + + /** + * Converts a String robot policy into an associative array, to allow + * merging of several policies using array_merge(). + * @param $policy Mixed, returns empty array on null/false/'', transparent + * to already-converted arrays, converts String. + * @return associative Array: 'index' => <indexpolicy>, 'follow' => <followpolicy> + */ + public static function formatRobotPolicy( $policy ) { + if ( is_array( $policy ) ) { + return $policy; + } elseif ( !$policy ) { + return array(); + } + + $policy = explode( ',', $policy ); + $policy = array_map( 'trim', $policy ); + + $arr = array(); + foreach ( $policy as $var ) { + if ( in_array( $var, array( 'index', 'noindex' ) ) ) { + $arr['index'] = $var; + } elseif ( in_array( $var, array( 'follow', 'nofollow' ) ) ) { + $arr['follow'] = $var; + } + } + return $arr; + } + + /** + * If this request is a redirect view, send "redirected from" subtitle to + * $wgOut. Returns true if the header was needed, false if this is not a + * redirect view. Handles both local and remote redirects. + */ + public function showRedirectedFromHeader() { + global $wgOut, $wgUser, $wgRequest, $wgRedirectSources; + + $rdfrom = $wgRequest->getVal( 'rdfrom' ); + $sk = $wgUser->getSkin(); + if ( isset( $this->mRedirectedFrom ) ) { // This is an internally redirected page view. // We'll need a backlink to the source page for navigation. - if( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) { - $redir = $sk->makeKnownLinkObj( $this->mRedirectedFrom, '', 'redirect=no' ); + if ( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) { + $redir = $sk->link( + $this->mRedirectedFrom, + null, + array(), + array( 'redirect' => 'no' ), + array( 'known', 'noclasses' ) + ); $s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir ); $wgOut->setSubtitle( $s ); // Set the fragment if one was specified in the redirect - if( strval( $this->mTitle->getFragment() ) != '' ) { + if ( strval( $this->mTitle->getFragment() ) != '' ) { $fragment = Xml::escapeJsString( $this->mTitle->getFragmentForURL() ); $wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" ); } @@ -851,225 +1127,198 @@ class Article { $wgOut->addLink( array( 'rel' => 'canonical', 'href' => $this->mTitle->getLocalURL() ) ); - $wasRedirected = true; + return true; } - } elseif( !empty( $rdfrom ) ) { + } elseif ( $rdfrom ) { // This is an externally redirected view, from some other wiki. // If it was reported from a trusted site, supply a backlink. - global $wgRedirectSources; - if( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) { + if ( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) { $redir = $sk->makeExternalLink( $rdfrom, $rdfrom ); $s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir ); $wgOut->setSubtitle( $s ); - $wasRedirected = true; - } - } - - $outputDone = false; - wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$pcache ) ); - if( $pcache && $wgOut->tryParserCache( $this ) ) { - // Ensure that UI elements requiring revision ID have - // the correct version information. - $wgOut->setRevisionId( $this->mLatest ); - $outputDone = true; - } - # Fetch content and check for errors - if( !$outputDone ) { - # If the article does not exist and was deleted, show the log - if( $this->getID() == 0 ) { - $this->showDeletionLog(); - } - $text = $this->getContent(); - // For now, check also for ID until getContent actually returns - // false for pages that do not exists - if( $text === false || $this->getID() === 0 ) { - # Failed to load, replace text with error message - $t = $this->mTitle->getPrefixedText(); - if( $oldid ) { - $d = wfMsgExt( 'missingarticle-rev', 'escape', $oldid ); - $text = wfMsgExt( 'missing-article', 'parsemag', $t, $d ); - // Always use page content for pages in the MediaWiki namespace - // since it contains the default message - } elseif ( $this->mTitle->getNamespace() != NS_MEDIAWIKI ) { - $text = wfMsgExt( 'noarticletext', 'parsemag' ); - } - } - - # Non-existent pages - if( $this->getID() === 0 ) { - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $text = "<div class='noarticletext'>\n$text\n</div>"; - if( !$this->hasViewableContent() ) { - // If there's no backing content, send a 404 Not Found - // for better machine handling of broken links. - $return404 = true; - } - } - - if( $return404 ) { - $wgRequest->response()->header( "HTTP/1.x 404 Not Found" ); - } - - # Another whitelist check in case oldid is altering the title - if( !$this->mTitle->userCanRead() ) { - $wgOut->loginToUse(); - $wgOut->output(); - $wgOut->disable(); - wfProfileOut( __METHOD__ ); - return; - } - - # For ?curid=x urls, disallow indexing - if( $wgRequest->getInt('curid') ) - $wgOut->setRobotPolicy( 'noindex,follow' ); - - # We're looking at an old revision - if( !empty( $oldid ) ) { - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - if( is_null( $this->mRevision ) ) { - // FIXME: This would be a nice place to load the 'no such page' text. - } else { - $this->setOldSubtitle( isset($this->mOldId) ? $this->mOldId : $oldid ); - # Allow admins to see deleted content if explicitly requested - if( $this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { - if( !$unhide || !$this->mRevision->userCan(Revision::DELETED_TEXT) ) { - $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' ); - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - wfProfileOut( __METHOD__ ); - return; - } else { - $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' ); - // and we are allowed to see... - } - } - // Is this the current revision and otherwise cacheable? Try the parser cache... - if( $oldid === $this->getLatest() && $this->useParserCache( false ) - && $wgOut->tryParserCache( $this ) ) - { - $outputDone = true; - } - } - } - - // Ensure that UI elements requiring revision ID have - // the correct version information. - $wgOut->setRevisionId( $this->getRevIdFetched() ); - - if( $outputDone ) { - // do nothing... - // Pages containing custom CSS or JavaScript get special treatment - } else 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 - $m = array(); - preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m ); - $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" ); - $wgOut->addHTML( htmlspecialchars( $this->mContent ) ); - $wgOut->addHTML( "\n</pre>\n" ); - } - } else if( $rt = Title::newFromRedirectArray( $text ) ) { # get an array of redirect targets - # Don't append the subtitle if this was an old revision - $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) ); - $parseout = $wgParser->parse($text, $this->mTitle, ParserOptions::newFromUser($wgUser)); - $wgOut->addParserOutputNoText( $parseout ); - } else if( $pcache ) { - # Display content and save to parser cache - $this->outputWikiText( $text ); - } else { - # Display content, don't attempt to save to parser cache - # Don't show section-edit links on old revisions... this way lies madness. - if( !$this->isCurrent() ) { - $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false ); - } - # Display content and don't save to parser cache - # With timing hack -- TS 2006-07-26 - $time = -wfTime(); - $this->outputWikiText( $text, false ); - $time += wfTime(); - - # Timing hack - if( $time > 3 ) { - wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, - $this->mTitle->getPrefixedDBkey())); - } - - if( !$this->isCurrent() ) { - $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); - } + return true; } } - /* title may have been set from the cache */ - $t = $wgOut->getPageTitle(); - if( empty( $t ) ) { - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + return false; + } - # For the main page, overwrite the <title> element with the con- - # tents of 'pagetitle-view-mainpage' instead of the default (if - # that's not empty). - if( $this->mTitle->equals( Title::newMainPage() ) && - wfMsgForContent( 'pagetitle-view-mainpage' ) !== '' ) { - $wgOut->setHTMLTitle( wfMsgForContent( 'pagetitle-view-mainpage' ) ); + /** + * Show a header specific to the namespace currently being viewed, like + * [[MediaWiki:Talkpagetext]]. For Article::view(). + */ + public function showNamespaceHeader() { + global $wgOut; + if ( $this->mTitle->isTalkPage() ) { + $msg = wfMsgNoTrans( 'talkpageheader' ); + if ( $msg !== '-' && !wfEmptyMsg( 'talkpageheader', $msg ) ) { + $wgOut->wrapWikiMsg( "<div class=\"mw-talkpageheader\">\n$1</div>", array( 'talkpageheader' ) ); } } + } + /** + * Show the footer section of an ordinary page view + */ + public function showViewFooter() { + global $wgOut, $wgUseTrackbacks, $wgRequest; # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page - if( $ns == NS_USER_TALK && IP::isValid( $this->mTitle->getText() ) ) { - $wgOut->addWikiMsg('anontalkpagetext'); + if ( $this->mTitle->getNamespace() == NS_USER_TALK && IP::isValid( $this->mTitle->getText() ) ) { + $wgOut->addWikiMsg( 'anontalkpagetext' ); } # If we have been passed an &rcid= parameter, we want to give the user a # chance to mark this new article as patrolled. - if( !empty($rcid) && $this->mTitle->exists() && $this->mTitle->quickUserCan('patrol') ) { - $wgOut->addHTML( - "<div class='patrollink'>" . - wfMsgHtml( 'markaspatrolledlink', - $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml('markaspatrolledtext'), - "action=markpatrolled&rcid=$rcid" ) - ) . - '</div>' - ); - } + $this->showPatrolFooter(); # Trackbacks - if( $wgUseTrackbacks ) { + if ( $wgUseTrackbacks ) { $this->addTrackbacks(); } + } - $this->viewUpdates(); - wfProfileOut( __METHOD__ ); + /** + * If patrol is possible, output a patrol UI box. This is called from the + * footer section of ordinary page views. If patrol is not possible or not + * desired, does nothing. + */ + public function showPatrolFooter() { + global $wgOut, $wgRequest, $wgUser; + $rcid = $wgRequest->getVal( 'rcid' ); + + if ( !$rcid || !$this->mTitle->exists() || !$this->mTitle->quickUserCan( 'patrol' ) ) { + return; + } + + $sk = $wgUser->getSkin(); + + $wgOut->addHTML( + "<div class='patrollink'>" . + wfMsgHtml( + 'markaspatrolledlink', + $sk->link( + $this->mTitle, + wfMsgHtml( 'markaspatrolledtext' ), + array(), + array( + 'action' => 'markpatrolled', + 'rcid' => $rcid + ), + array( 'known', 'noclasses' ) + ) + ) . + '</div>' + ); } - - protected function showDeletionLog() { - global $wgUser, $wgOut; - $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut ); - $pager = new LogPager( $loglist, 'delete', false, $this->mTitle->getPrefixedText() ); - if( $pager->getNumRows() > 0 ) { - $pager->mLimit = 10; - $wgOut->addHTML( '<div class="mw-warning-with-logexcerpt">' ); - $wgOut->addWikiMsg( 'deleted-notice' ); - $wgOut->addHTML( - $loglist->beginLogEventsList() . - $pager->getBody() . - $loglist->endLogEventsList() - ); - if( $pager->getNumRows() > 10 ) { - $wgOut->addHTML( $wgUser->getSkin()->link( - SpecialPage::getTitleFor( 'Log' ), - wfMsgHtml( 'deletelog-fulllog' ), - array(), - array( 'type' => 'delete', 'page' => $this->mTitle->getPrefixedText() ) - ) ); + + /** + * Show the error text for a missing article. For articles in the MediaWiki + * namespace, show the default message text. To be called from Article::view(). + */ + public function showMissingArticle() { + global $wgOut, $wgRequest, $wgUser; + + # Show info in user (talk) namespace. Does the user exist? Is he blocked? + if ( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) { + $parts = explode( '/', $this->mTitle->getText() ); + $rootPart = $parts[0]; + $user = User::newFromName( $rootPart, false /* allow IP users*/ ); + $ip = User::isIP( $rootPart ); + if ( !$user->isLoggedIn() && !$ip ) { # User does not exist + $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1</div>", + array( 'userpage-userdoesnotexist-view', $rootPart ) ); + } else if ( $user->isBlocked() ) { # Show log extract if the user is currently blocked + LogEventsList::showLogExtract( + $wgOut, + 'block', + $user->getUserPage()->getPrefixedText(), + '', + array( + 'lim' => 1, + 'showIfEmpty' => false, + 'msgKey' => array( + 'blocked-notice-logextract', + $user->getName() # Support GENDER in notice + ) + ) + ); } - $wgOut->addHTML( '</div>' ); + } + wfRunHooks( 'ShowMissingArticle', array( $this ) ); + # Show delete and move logs + LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->mTitle->getPrefixedText(), '', + array( 'lim' => 10, + 'conds' => array( "log_action != 'revision'" ), + 'showIfEmpty' => false, + 'msgKey' => array( 'moveddeleted-notice' ) ) + ); + + # Show error message + $oldid = $this->getOldID(); + if ( $oldid ) { + $text = wfMsgNoTrans( 'missing-article', + $this->mTitle->getPrefixedText(), + wfMsgNoTrans( 'missingarticle-rev', $oldid ) ); + } elseif ( $this->mTitle->getNamespace() === NS_MEDIAWIKI ) { + // Use the default message text + $text = $this->getContent(); + } else { + $createErrors = $this->mTitle->getUserPermissionsErrors( 'create', $wgUser ); + $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ); + $errors = array_merge( $createErrors, $editErrors ); + + if ( !count( $errors ) ) + $text = wfMsgNoTrans( 'noarticletext' ); + else + $text = wfMsgNoTrans( 'noarticletext-nopermission' ); + } + $text = "<div class='noarticletext'>\n$text\n</div>"; + if ( !$this->hasViewableContent() ) { + // If there's no backing content, send a 404 Not Found + // for better machine handling of broken links. + $wgRequest->response()->header( "HTTP/1.x 404 Not Found" ); + } + $wgOut->addWikiText( $text ); + } + + /** + * If the revision requested for view is deleted, check permissions. + * Send either an error message or a warning header to $wgOut. + * Returns true if the view is allowed, false if not. + */ + public function showDeletedRevisionHeader() { + global $wgOut, $wgRequest; + if ( !$this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { + // Not deleted + return true; + } + // If the user is not allowed to see it... + if ( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) { + $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", + 'rev-deleted-text-permission' ); + return false; + // If the user needs to confirm that they want to see it... + } else if ( $wgRequest->getInt( 'unhide' ) != 1 ) { + # Give explanation and add a link to view the revision... + $oldid = intval( $this->getOldID() ); + $link = $this->mTitle->getFullUrl( "oldid={$oldid}&unhide=1" ); + $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ? + 'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide'; + $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", + array( $msg, $link ) ); + return false; + // We are allowed to see... + } else { + $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ? + 'rev-suppressed-text-view' : 'rev-deleted-text-view'; + $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", $msg ); + return true; } } /* * Should the parser cache be used? */ - protected function useParserCache( $oldid ) { + public function useParserCache( $oldid ) { global $wgUser, $wgEnableParserCache; return $wgEnableParserCache @@ -1081,45 +1330,116 @@ class Article { } /** + * Execute the uncached parse for action=view + */ + public function doViewParse() { + global $wgOut; + $oldid = $this->getOldID(); + $useParserCache = $this->useParserCache( $oldid ); + $parserOptions = clone $this->getParserOptions(); + # Render printable version, use printable version cache + $parserOptions->setIsPrintable( $wgOut->isPrintable() ); + # Don't show section-edit links on old revisions... this way lies madness. + $parserOptions->setEditSection( $this->isCurrent() ); + $useParserCache = $this->useParserCache( $oldid ); + $this->outputWikiText( $this->getContent(), $useParserCache, $parserOptions ); + } + + /** + * Try to fetch an expired entry from the parser cache. If it is present, + * output it and return true. If it is not present, output nothing and + * return false. This is used as a callback function for + * PoolCounter::executeProtected(). + */ + public function tryDirtyCache() { + global $wgOut; + $parserCache = ParserCache::singleton(); + $options = $this->getParserOptions(); + $options->setIsPrintable( $wgOut->isPrintable() ); + $output = $parserCache->getDirty( $this, $options ); + if ( $output ) { + wfDebug( __METHOD__ . ": sending dirty output\n" ); + wfDebugLog( 'dirty', "dirty output " . $parserCache->getKey( $this, $options ) . "\n" ); + $wgOut->setSquidMaxage( 0 ); + $this->mParserOutput = $output; + $wgOut->addParserOutput( $output ); + $wgOut->addHTML( "<!-- parser cache is expired, sending anyway due to pool overload-->\n" ); + return true; + } else { + wfDebugLog( 'dirty', "dirty missing\n" ); + wfDebug( __METHOD__ . ": no dirty cache\n" ); + return false; + } + } + + /** + * Show an error page for an error from the pool counter. + * @param $status Status + */ + public function showPoolError( $status ) { + global $wgOut; + $wgOut->clearHTML(); // for release() errors + $wgOut->enableClientCache( false ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->addWikiText( + '<div class="errorbox">' . + $status->getWikiText( false, 'view-pool-error' ) . + '</div>' + ); + } + + /** * View redirect * @param $target Title object or Array of destination(s) to redirect * @param $appendSubtitle Boolean [optional] * @param $forceKnown Boolean: should the image be shown as a bluelink regardless of existence? */ public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) { - global $wgParser, $wgOut, $wgContLang, $wgStylePath, $wgUser; + global $wgOut, $wgContLang, $wgStylePath, $wgUser; # Display redirect - if( !is_array( $target ) ) { + if ( !is_array( $target ) ) { $target = array( $target ); } - $imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr'; + $imageDir = $wgContLang->getDir(); $imageUrl = $wgStylePath . '/common/images/redirect' . $imageDir . '.png'; $imageUrl2 = $wgStylePath . '/common/images/nextredirect' . $imageDir . '.png'; $alt2 = $wgContLang->isRTL() ? '←' : '→'; // should -> and <- be used instead of entities? - - if( $appendSubtitle ) { + + if ( $appendSubtitle ) { $wgOut->appendSubtitle( wfMsgHtml( 'redirectpagesub' ) ); } $sk = $wgUser->getSkin(); // the loop prepends the arrow image before the link, so the first case needs to be outside $title = array_shift( $target ); - if( $forceKnown ) { - $link = $sk->makeKnownLinkObj( $title, htmlspecialchars( $title->getFullText() ) ); + if ( $forceKnown ) { + $link = $sk->link( + $title, + htmlspecialchars( $title->getFullText() ), + array(), + array(), + array( 'known', 'noclasses' ) + ); } else { - $link = $sk->makeLinkObj( $title, htmlspecialchars( $title->getFullText() ) ); + $link = $sk->link( $title, htmlspecialchars( $title->getFullText() ) ); } // automatically append redirect=no to each link, since most of them are redirect pages themselves - foreach( $target as $rt ) { - if( $forceKnown ) { - $link .= '<img src="'.$imageUrl2.'" alt="'.$alt2.' " />' - . $sk->makeKnownLinkObj( $rt, htmlspecialchars( $rt->getFullText() ) ); + foreach ( $target as $rt ) { + if ( $forceKnown ) { + $link .= '<img src="' . $imageUrl2 . '" alt="' . $alt2 . ' " />' + . $sk->link( + $rt, + htmlspecialchars( $rt->getFullText() ), + array(), + array(), + array( 'known', 'noclasses' ) + ); } else { - $link .= '<img src="'.$imageUrl2.'" alt="'.$alt2.' " />' - . $sk->makeLinkObj( $rt, htmlspecialchars( $rt->getFullText() ) ); + $link .= '<img src="' . $imageUrl2 . '" alt="' . $alt2 . ' " />' + . $sk->link( $rt, htmlspecialchars( $rt->getFullText() ) ); } } - return '<img src="'.$imageUrl.'" alt="#REDIRECT " />' . - '<span class="redirectText">'.$link.'</span>'; + return '<img src="' . $imageUrl . '" alt="#REDIRECT " />' . + '<span class="redirectText">' . $link . '</span>'; } @@ -1127,46 +1447,46 @@ class Article { global $wgOut, $wgUser; $dbr = wfGetDB( DB_SLAVE ); $tbs = $dbr->select( 'trackbacks', - array('tb_id', 'tb_title', 'tb_url', 'tb_ex', 'tb_name'), - array('tb_page' => $this->getID() ) + array( 'tb_id', 'tb_title', 'tb_url', 'tb_ex', 'tb_name' ), + array( 'tb_page' => $this->getID() ) ); - if( !$dbr->numRows($tbs) ) return; + if ( !$dbr->numRows( $tbs ) ) return; $tbtext = ""; - while( $o = $dbr->fetchObject($tbs) ) { + while ( $o = $dbr->fetchObject( $tbs ) ) { $rmvtxt = ""; - if( $wgUser->isAllowed( 'trackback' ) ) { - $delurl = $this->mTitle->getFullURL("action=deletetrackback&tbid=" . + if ( $wgUser->isAllowed( 'trackback' ) ) { + $delurl = $this->mTitle->getFullURL( "action=deletetrackback&tbid=" . $o->tb_id . "&token=" . urlencode( $wgUser->editToken() ) ); $rmvtxt = wfMsg( 'trackbackremove', htmlspecialchars( $delurl ) ); } $tbtext .= "\n"; - $tbtext .= wfMsg(strlen($o->tb_ex) ? 'trackbackexcerpt' : 'trackback', + $tbtext .= wfMsg( strlen( $o->tb_ex ) ? 'trackbackexcerpt' : 'trackback', $o->tb_title, $o->tb_url, $o->tb_ex, $o->tb_name, - $rmvtxt); + $rmvtxt ); } $wgOut->wrapWikiMsg( "<div id='mw_trackbacks'>$1</div>\n", array( 'trackbackbox', $tbtext ) ); $this->mTitle->invalidateCache(); } public function deletetrackback() { - global $wgUser, $wgRequest, $wgOut, $wgTitle; - if( !$wgUser->matchEditToken($wgRequest->getVal('token')) ) { + global $wgUser, $wgRequest, $wgOut; + if ( !$wgUser->matchEditToken( $wgRequest->getVal( 'token' ) ) ) { $wgOut->addWikiMsg( 'sessionfailure' ); return; } $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser ); - if( count($permission_errors) ) { + if ( count( $permission_errors ) ) { $wgOut->showPermissionsErrorPage( $permission_errors ); return; } $db = wfGetDB( DB_MASTER ); - $db->delete( 'trackbacks', array('tb_id' => $wgRequest->getInt('tbid')) ); + $db->delete( 'trackbacks', array( 'tb_id' => $wgRequest->getInt( 'tbid' ) ) ); $wgOut->addWikiMsg( 'trackbackdeleteok' ); $this->mTitle->invalidateCache(); @@ -1174,7 +1494,7 @@ class Article { public function render() { global $wgOut; - $wgOut->setArticleBodyOnly(true); + $wgOut->setArticleBodyOnly( true ); $this->view(); } @@ -1183,19 +1503,19 @@ class Article { */ public function purge() { global $wgUser, $wgRequest, $wgOut; - if( $wgUser->isAllowed( 'purge' ) || $wgRequest->wasPosted() ) { - if( wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { + if ( $wgUser->isAllowed( 'purge' ) || $wgRequest->wasPosted() ) { + if ( wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { $this->doPurge(); $this->view(); } } else { $action = htmlspecialchars( $wgRequest->getRequestURL() ); - $button = wfMsgExt( 'confirm_purge_button', array('escapenoentities') ); + $button = wfMsgExt( 'confirm_purge_button', array( 'escapenoentities' ) ); $form = "<form method=\"post\" action=\"$action\">\n" . "<input type=\"submit\" name=\"submit\" value=\"$button\" />\n" . "</form>\n"; - $top = wfMsgExt( 'confirm-purge-top', array('parse') ); - $bottom = wfMsgExt( 'confirm-purge-bottom', array('parse') ); + $top = wfMsgExt( 'confirm-purge-top', array( 'parse' ) ); + $bottom = wfMsgExt( 'confirm-purge-bottom', array( 'parse' ) ); $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addHTML( $top . $form . $bottom ); @@ -1210,18 +1530,18 @@ class Article { // Invalidate the cache $this->mTitle->invalidateCache(); - if( $wgUseSquid ) { + if ( $wgUseSquid ) { // Commit the transaction before the purge is sent $dbw = wfGetDB( DB_MASTER ); - $dbw->immediateCommit(); + $dbw->commit(); // Send purge $update = SquidUpdate::newSimplePurge( $this->mTitle ); $update->doUpdate(); } - if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { global $wgMessageCache; - if( $this->getID() == 0 ) { + if ( $this->getID() == 0 ) { $text = false; } else { $text = $this->getRawText(); @@ -1260,7 +1580,7 @@ class Article { ), __METHOD__, 'IGNORE' ); $affected = $dbw->affectedRows(); - if( $affected ) { + if ( $affected ) { $newid = $dbw->insertId(); $this->mTitle->resetArticleId( $newid ); } @@ -1279,7 +1599,7 @@ class Article { * Giving 0 indicates the new page flag should be set * on. * @param $lastRevIsRedirect Boolean: if given, will optimize adding and - * removing rows in redirect table. + * removing rows in redirect table. * @return bool true on success, false on failure * @private */ @@ -1290,7 +1610,7 @@ class Article { $rt = Title::newFromRedirect( $text ); $conditions = array( 'page_id' => $this->getId() ); - if( !is_null( $lastRevision ) ) { + if ( !is_null( $lastRevision ) ) { # An extra check against threads stepping on each other $conditions['page_latest'] = $lastRevision; } @@ -1299,15 +1619,15 @@ class Article { array( /* SET */ 'page_latest' => $revision->getId(), 'page_touched' => $dbw->timestamp(), - 'page_is_new' => ($lastRevision === 0) ? 1 : 0, - 'page_is_redirect' => $rt !== NULL ? 1 : 0, + 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, + 'page_is_redirect' => $rt !== null ? 1 : 0, 'page_len' => strlen( $text ), ), $conditions, __METHOD__ ); $result = $dbw->affectedRows() != 0; - if( $result ) { + if ( $result ) { $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); } @@ -1320,9 +1640,9 @@ class Article { * * @param $dbw Database * @param $redirectTitle a title object pointing to the redirect target, - * or NULL if this is not a redirect + * or NULL if this is not a redirect * @param $lastRevIsRedirect If given, will optimize adding and - * removing rows in redirect table. + * removing rows in redirect table. * @return bool true on success, false on failure * @private */ @@ -1330,10 +1650,10 @@ class Article { // Always update redirects (target link might have changed) // Update/Insert if we don't know if the last revision was a redirect or not // Delete if changing from redirect to non-redirect - $isRedirect = !is_null($redirectTitle); - if($isRedirect || is_null($lastRevIsRedirect) || $lastRevIsRedirect !== $isRedirect) { + $isRedirect = !is_null( $redirectTitle ); + if ( $isRedirect || is_null( $lastRevIsRedirect ) || $lastRevIsRedirect !== $isRedirect ) { wfProfileIn( __METHOD__ ); - if( $isRedirect ) { + if ( $isRedirect ) { // This title is a redirect, Add/Update row in the redirect table $set = array( /* SET */ 'rd_namespace' => $redirectTitle->getNamespace(), @@ -1344,9 +1664,9 @@ class Article { } else { // This is not a redirect, remove row from redirect table $where = array( 'rd_from' => $this->getId() ); - $dbw->delete( 'redirect', $where, __METHOD__); + $dbw->delete( 'redirect', $where, __METHOD__ ); } - if( $this->getTitle()->getNamespace() == NS_FILE ) { + if ( $this->getTitle()->getNamespace() == NS_FILE ) { RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() ); } wfProfileOut( __METHOD__ ); @@ -1371,8 +1691,8 @@ class Article { 'page_id' => $this->getId(), 'page_latest=rev_id' ), __METHOD__ ); - if( $row ) { - if( wfTimestamp(TS_MW, $row->rev_timestamp) >= $revision->getTimestamp() ) { + if ( $row ) { + if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) { wfProfileOut( __METHOD__ ); return false; } @@ -1392,27 +1712,27 @@ class Article { * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...) * @return string Complete article text, or null if error */ - public function replaceSection( $section, $text, $summary = '', $edittime = NULL ) { + public function replaceSection( $section, $text, $summary = '', $edittime = null ) { wfProfileIn( __METHOD__ ); - if( strval( $section ) == '' ) { + if ( strval( $section ) == '' ) { // Whole-page edit; let the whole text through } else { - if( is_null($edittime) ) { + if ( is_null( $edittime ) ) { $rev = Revision::newFromTitle( $this->mTitle ); } else { $dbw = wfGetDB( DB_MASTER ); $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); } - if( !$rev ) { + if ( !$rev ) { wfDebug( "Article::replaceSection asked for bogus section (page: " . $this->getId() . "; section: $section; edittime: $edittime)\n" ); return null; } $oldtext = $rev->getText(); - if( $section == 'new' ) { + if ( $section == 'new' ) { # Inserting a new section - $subject = $summary ? wfMsgForContent('newsectionheaderdefaultlevel',$summary) . "\n\n" : ''; + $subject = $summary ? wfMsgForContent( 'newsectionheaderdefaultlevel', $summary ) . "\n\n" : ''; $text = strlen( trim( $oldtext ) ) > 0 ? "{$oldtext}\n\n{$subject}{$text}" : "{$subject}{$text}"; @@ -1427,31 +1747,31 @@ class Article { } /** - * @deprecated use Article::doEdit() + * This function is not deprecated until somebody fixes the core not to use + * it. Nevertheless, use Article::doEdit() instead. */ - function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false, $bot=false ) { - wfDeprecated( __METHOD__ ); + function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC = false, $comment = false, $bot = false ) { $flags = EDIT_NEW | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | ( $isminor ? EDIT_MINOR : 0 ) | ( $suppressRC ? EDIT_SUPPRESS_RC : 0 ) | ( $bot ? EDIT_FORCE_BOT : 0 ); # If this is a comment, add the summary as headline - if( $comment && $summary != "" ) { - $text = wfMsgForContent('newsectionheaderdefaultlevel',$summary) . "\n\n".$text; + if ( $comment && $summary != "" ) { + $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $summary ) . "\n\n" . $text; } $this->doEdit( $text, $summary, $flags ); $dbw = wfGetDB( DB_MASTER ); - if($watchthis) { - if(!$this->mTitle->userIsWatching()) { + if ( $watchthis ) { + if ( !$this->mTitle->userIsWatching() ) { $dbw->begin(); $this->doWatch(); $dbw->commit(); } } else { - if( $this->mTitle->userIsWatching() ) { + if ( $this->mTitle->userIsWatching() ) { $dbw->begin(); $this->doUnwatch(); $dbw->commit(); @@ -1464,25 +1784,24 @@ class Article { * @deprecated use Article::doEdit() */ function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' ) { - wfDeprecated( __METHOD__ ); $flags = EDIT_UPDATE | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | ( $minor ? EDIT_MINOR : 0 ) | ( $forceBot ? EDIT_FORCE_BOT : 0 ); $status = $this->doEdit( $text, $summary, $flags ); - if( !$status->isOK() ) { + if ( !$status->isOK() ) { return false; } $dbw = wfGetDB( DB_MASTER ); - if( $watchthis ) { - if(!$this->mTitle->userIsWatching()) { + if ( $watchthis ) { + if ( !$this->mTitle->userIsWatching() ) { $dbw->begin(); $this->doWatch(); $dbw->commit(); } } else { - if( $this->mTitle->userIsWatching() ) { + if ( $this->mTitle->userIsWatching() ) { $dbw->begin(); $this->doUnwatch(); $dbw->commit(); @@ -1523,9 +1842,9 @@ class Article { * Fill in blank summaries with generated text where possible * * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected. - * If EDIT_UPDATE is specified and the article doesn't exist, the function will an - * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an - * edit-already-exists error will be returned. These two conditions are also possible with + * If EDIT_UPDATE is specified and the article doesn't exist, the function will an + * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an + * edit-already-exists error will be returned. 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 @@ -1550,47 +1869,47 @@ class Article { global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries; # Low-level sanity check - if( $this->mTitle->getText() == '' ) { + if ( $this->mTitle->getText() == '' ) { throw new MWException( 'Something is trying to edit an article with an empty title' ); } wfProfileIn( __METHOD__ ); - $user = is_null($user) ? $wgUser : $user; + $user = is_null( $user ) ? $wgUser : $user; $status = Status::newGood( array() ); # Load $this->mTitle->getArticleID() and $this->mLatest if it's not already - $this->loadPageData(); + $this->loadPageData(); - if( !($flags & EDIT_NEW) && !($flags & EDIT_UPDATE) ) { + if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) { $aid = $this->mTitle->getArticleID(); - if( $aid ) { + if ( $aid ) { $flags |= EDIT_UPDATE; } else { $flags |= EDIT_NEW; } } - if( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary, + if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary, $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) ) { wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" ); wfProfileOut( __METHOD__ ); - if( $status->isOK() ) { - $status->fatal( 'edit-hook-aborted'); + if ( $status->isOK() ) { + $status->fatal( 'edit-hook-aborted' ); } return $status; } # Silently ignore EDIT_MINOR if not allowed - $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed('minoredit'); + $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ); $bot = $flags & EDIT_FORCE_BOT; $oldtext = $this->getRawText(); // current revision $oldsize = strlen( $oldtext ); # Provide autosummaries if one is not provided and autosummaries are enabled. - if( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { + if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { $summary = $this->getAutosummary( $oldtext, $text, $flags ); } @@ -1600,12 +1919,13 @@ class Article { $dbw = wfGetDB( DB_MASTER ); $now = wfTimestampNow(); + $this->mTimestamp = $now; - if( $flags & EDIT_UPDATE ) { + if ( $flags & EDIT_UPDATE ) { # Update article, but only if changed. $status->value['new'] = false; # Make sure the revision is either completely inserted or not inserted at all - if( !$wgDBtransactions ) { + if ( !$wgDBtransactions ) { $userAbort = ignore_user_abort( true ); } @@ -1613,14 +1933,14 @@ class Article { $changed = ( strcmp( $text, $oldtext ) != 0 ); - if( $changed ) { + if ( $changed ) { $this->mGoodAdjustment = (int)$this->isCountable( $text ) - (int)$this->isCountable( $oldtext ); $this->mTotalAdjustment = 0; - if( !$this->mLatest ) { + if ( !$this->mLatest ) { # Article gone missing - wfDebug( __METHOD__.": EDIT_UPDATE specified but article doesn't exist\n" ); + wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" ); $status->fatal( 'edit-gone-missing' ); wfProfileOut( __METHOD__ ); return $status; @@ -1641,36 +1961,36 @@ class Article { # Update page # - # Note that we use $this->mLatest instead of fetching a value from the master DB - # during the course of this function. This makes sure that EditPage can detect - # edit conflicts reliably, either by $ok here, or by $article->getTimestamp() + # Note that we use $this->mLatest instead of fetching a value from the master DB + # during the course of this function. This makes sure that EditPage can detect + # edit conflicts reliably, either by $ok here, or by $article->getTimestamp() # before this function is called. A previous function used a separate query, this # creates a window where concurrent edits can cause an ignored edit conflict. $ok = $this->updateRevisionOn( $dbw, $revision, $this->mLatest ); - if( !$ok ) { + if ( !$ok ) { /* Belated edit conflict! Run away!! */ $status->fatal( 'edit-conflict' ); # Delete the invalid revision if the DB is not transactional - if( !$wgDBtransactions ) { + if ( !$wgDBtransactions ) { $dbw->delete( 'revision', array( 'rev_id' => $revisionId ), __METHOD__ ); } $revisionId = 0; $dbw->rollback(); } else { global $wgUseRCPatrol; - wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, $baseRevId, $user) ); + wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); # Update recentchanges - if( !( $flags & EDIT_SUPPRESS_RC ) ) { + if ( !( $flags & EDIT_SUPPRESS_RC ) ) { # Mark as patrolled if the user can do so - $patrolled = $wgUseRCPatrol && $this->mTitle->userCan('autopatrol'); + $patrolled = $wgUseRCPatrol && $this->mTitle->userCan( 'autopatrol' ); # Add RC row to the DB $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, $this->mLatest, $this->getTimestamp(), $bot, '', $oldsize, $newsize, $revisionId, $patrolled ); # Log auto-patrolled edits - if( $patrolled ) { + if ( $patrolled ) { PatrolLog::record( $rc, true ); } } @@ -1687,11 +2007,11 @@ class Article { $this->mTitle->invalidateCache(); } - if( !$wgDBtransactions ) { + if ( !$wgDBtransactions ) { ignore_user_abort( $userAbort ); } // Now that ignore_user_abort is restored, we can respond to fatal errors - if( !$status->isOK() ) { + if ( !$status->isOK() ) { wfProfileOut( __METHOD__ ); return $status; } @@ -1717,7 +2037,7 @@ class Article { # This will return false if the article already exists $newid = $this->insertOn( $dbw ); - if( $newid === false ) { + if ( $newid === false ) { $dbw->rollback(); $status->fatal( 'edit-already-exists' ); wfProfileOut( __METHOD__ ); @@ -1740,17 +2060,17 @@ class Article { # Update the page record with revision data $this->updateRevisionOn( $dbw, $revision, 0 ); - wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false, $user) ); + wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); # Update recentchanges - if( !( $flags & EDIT_SUPPRESS_RC ) ) { + if ( !( $flags & EDIT_SUPPRESS_RC ) ) { global $wgUseRCPatrol, $wgUseNPPatrol; # Mark as patrolled if the user can do so - $patrolled = ($wgUseRCPatrol || $wgUseNPPatrol) && $this->mTitle->userCan('autopatrol'); + $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && $this->mTitle->userCan( 'autopatrol' ); # Add RC row to the DB $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, - '', strlen($text), $revisionId, $patrolled ); + '', strlen( $text ), $revisionId, $patrolled ); # Log auto-patrolled edits - if( $patrolled ) { + if ( $patrolled ) { PatrolLog::record( $rc, true ); } } @@ -1768,7 +2088,7 @@ class Article { } # Do updates right now unless deferral was requested - if( !( $flags & EDIT_DEFER_UPDATES ) ) { + if ( !( $flags & EDIT_DEFER_UPDATES ) ) { wfDoUpdates(); } @@ -1800,9 +2120,9 @@ class Article { */ public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) { global $wgOut; - if( $noRedir ) { + if ( $noRedir ) { $query = 'redirect=no'; - if( $extraQuery ) + if ( $extraQuery ) $query .= "&$query"; } else { $query = $extraQuery; @@ -1818,63 +2138,62 @@ class Article { $wgOut->setRobotPolicy( 'noindex,nofollow' ); # If we haven't been given an rc_id value, we can't do anything - $rcid = (int) $wgRequest->getVal('rcid'); - $rc = RecentChange::newFromId($rcid); - if( is_null($rc) ) { + $rcid = (int) $wgRequest->getVal( 'rcid' ); + $rc = RecentChange::newFromId( $rcid ); + if ( is_null( $rc ) ) { $wgOut->showErrorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); return; } - #It would be nice to see where the user had actually come from, but for now just guess + # It would be nice to see where the user had actually come from, but for now just guess $returnto = $rc->getAttribute( 'rc_type' ) == RC_NEW ? 'Newpages' : 'Recentchanges'; $return = SpecialPage::getTitleFor( $returnto ); $dbw = wfGetDB( DB_MASTER ); $errors = $rc->doMarkPatrolled(); - if( in_array(array('rcpatroldisabled'), $errors) ) { + if ( in_array( array( 'rcpatroldisabled' ), $errors ) ) { $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); return; } - - if( in_array(array('hookaborted'), $errors) ) { + + if ( in_array( array( 'hookaborted' ), $errors ) ) { // The hook itself has handled any output return; } - - if( in_array(array('markedaspatrollederror-noautopatrol'), $errors) ) { + + if ( in_array( array( 'markedaspatrollederror-noautopatrol' ), $errors ) ) { $wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) ); $wgOut->addWikiMsg( 'markedaspatrollederror-noautopatrol' ); $wgOut->returnToMain( false, $return ); return; } - if( !empty($errors) ) { + if ( !empty( $errors ) ) { $wgOut->showPermissionsErrorPage( $errors ); return; } # Inform the user $wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) ); - $wgOut->addWikiMsg( 'markedaspatrolledtext' ); + $wgOut->addWikiMsg( 'markedaspatrolledtext', $rc->getTitle()->getPrefixedText() ); $wgOut->returnToMain( false, $return ); } /** * User-interface handler for the "watch" action */ - public function watch() { global $wgUser, $wgOut; - if( $wgUser->isAnon() ) { + if ( $wgUser->isAnon() ) { $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' ); return; } - if( wfReadOnly() ) { + if ( wfReadOnly() ) { $wgOut->readOnlyPage(); return; } - if( $this->doWatch() ) { + if ( $this->doWatch() ) { $wgOut->setPagetitle( wfMsg( 'addedwatch' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addWikiMsg( 'addedwatchtext', $this->mTitle->getPrefixedText() ); @@ -1888,12 +2207,12 @@ class Article { */ public function doWatch() { global $wgUser; - if( $wgUser->isAnon() ) { + if ( $wgUser->isAnon() ) { return false; } - if( wfRunHooks('WatchArticle', array(&$wgUser, &$this)) ) { + if ( wfRunHooks( 'WatchArticle', array( &$wgUser, &$this ) ) ) { $wgUser->addWatch( $this->mTitle ); - return wfRunHooks('WatchArticleComplete', array(&$wgUser, &$this)); + return wfRunHooks( 'WatchArticleComplete', array( &$wgUser, &$this ) ); } return false; } @@ -1903,15 +2222,15 @@ class Article { */ public function unwatch() { global $wgUser, $wgOut; - if( $wgUser->isAnon() ) { + if ( $wgUser->isAnon() ) { $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' ); return; } - if( wfReadOnly() ) { + if ( wfReadOnly() ) { $wgOut->readOnlyPage(); return; } - if( $this->doUnwatch() ) { + if ( $this->doUnwatch() ) { $wgOut->setPagetitle( wfMsg( 'removedwatch' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addWikiMsg( 'removedwatchtext', $this->mTitle->getPrefixedText() ); @@ -1925,12 +2244,12 @@ class Article { */ public function doUnwatch() { global $wgUser; - if( $wgUser->isAnon() ) { + if ( $wgUser->isAnon() ) { return false; } - if( wfRunHooks('UnwatchArticle', array(&$wgUser, &$this)) ) { + if ( wfRunHooks( 'UnwatchArticle', array( &$wgUser, &$this ) ) ) { $wgUser->removeWatch( $this->mTitle ); - return wfRunHooks('UnwatchArticleComplete', array(&$wgUser, &$this)); + return wfRunHooks( 'UnwatchArticleComplete', array( &$wgUser, &$this ) ); } return false; } @@ -1960,25 +2279,27 @@ class Article { * @return bool true on success */ public function updateRestrictions( $limit = array(), $reason = '', &$cascade = 0, $expiry = array() ) { - global $wgUser, $wgRestrictionTypes, $wgContLang; + global $wgUser, $wgContLang; + + $restrictionTypes = $this->mTitle->getRestrictionTypes(); $id = $this->mTitle->getArticleID(); if ( $id <= 0 ) { wfDebug( "updateRestrictions failed: $id <= 0\n" ); return false; } - + if ( wfReadOnly() ) { wfDebug( "updateRestrictions failed: read-only\n" ); return false; } - + if ( !$this->mTitle->userCan( 'protect' ) ) { wfDebug( "updateRestrictions failed: insufficient permissions\n" ); return false; } - if( !$cascade ) { + if ( !$cascade ) { $cascade = false; } @@ -1990,17 +2311,17 @@ class Article { $current = array(); $updated = Article::flattenRestrictions( $limit ); $changed = false; - foreach( $wgRestrictionTypes as $action ) { - if( isset( $expiry[$action] ) ) { + foreach ( $restrictionTypes as $action ) { + if ( isset( $expiry[$action] ) ) { # Get current restrictions on $action $aLimits = $this->mTitle->getRestrictions( $action ); $current[$action] = implode( '', $aLimits ); # Are any actual restrictions being dealt with here? - $aRChanged = count($aLimits) || !empty($limit[$action]); + $aRChanged = count( $aLimits ) || !empty( $limit[$action] ); # If something changed, we need to log it. Checking $aRChanged # assures that "unprotecting" a page that is not protected does # not log just because the expiry was "changed". - if( $aRChanged && $this->mTitle->mRestrictionsExpiry[$action] != $expiry[$action] ) { + if ( $aRChanged && $this->mTitle->mRestrictionsExpiry[$action] != $expiry[$action] ) { $changed = true; } } @@ -2008,19 +2329,19 @@ class Article { $current = Article::flattenRestrictions( $current ); - $changed = ($changed || $current != $updated ); - $changed = $changed || ($updated && $this->mTitle->areRestrictionsCascading() != $cascade); + $changed = ( $changed || $current != $updated ); + $changed = $changed || ( $updated && $this->mTitle->areRestrictionsCascading() != $cascade ); $protect = ( $updated != '' ); # If nothing's changed, do nothing - if( $changed ) { - if( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) { + if ( $changed ) { + if ( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) { $dbw = wfGetDB( DB_MASTER ); - + # Prepare a null revision to be added to the history $modified = $current != '' && $protect; - if( $protect ) { + if ( $protect ) { $comment_type = $modified ? 'modifiedarticleprotection' : 'protectedarticle'; } else { $comment_type = 'unprotectedarticle'; @@ -2031,51 +2352,51 @@ class Article { # Otherwise, people who cannot normally protect can "protect" pages via transclusion $editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' ); # The schema allows multiple restrictions - if(!in_array('protect', $editrestriction) && !in_array('sysop', $editrestriction)) + if ( !in_array( 'protect', $editrestriction ) && !in_array( 'sysop', $editrestriction ) ) $cascade = false; - $cascade_description = ''; - if( $cascade ) { - $cascade_description = ' ['.wfMsgForContent('protect-summary-cascade').']'; + $cascade_description = ''; + if ( $cascade ) { + $cascade_description = ' [' . wfMsgForContent( 'protect-summary-cascade' ) . ']'; } - if( $reason ) + if ( $reason ) $comment .= ": $reason"; $editComment = $comment; $encodedExpiry = array(); $protect_description = ''; - foreach( $limit as $action => $restrictions ) { - if ( !isset($expiry[$action]) ) + foreach ( $limit as $action => $restrictions ) { + if ( !isset( $expiry[$action] ) ) $expiry[$action] = 'infinite'; - - $encodedExpiry[$action] = Block::encodeExpiry($expiry[$action], $dbw ); - if( $restrictions != '' ) { + + $encodedExpiry[$action] = Block::encodeExpiry( $expiry[$action], $dbw ); + if ( $restrictions != '' ) { $protect_description .= "[$action=$restrictions] ("; - if( $encodedExpiry[$action] != 'infinity' ) { - $protect_description .= wfMsgForContent( 'protect-expiring', + if ( $encodedExpiry[$action] != 'infinity' ) { + $protect_description .= wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry[$action], false, false ) , $wgContLang->date( $expiry[$action], false, false ) , - $wgContLang->time( $expiry[$action], false, false ) ); + $wgContLang->time( $expiry[$action], false, false ) ); } else { $protect_description .= wfMsgForContent( 'protect-expiry-indefinite' ); } $protect_description .= ') '; } } - $protect_description = trim($protect_description); - - if( $protect_description && $protect ) + $protect_description = trim( $protect_description ); + + if ( $protect_description && $protect ) $editComment .= " ($protect_description)"; - if( $cascade ) + if ( $cascade ) $editComment .= "$cascade_description"; # Update restrictions table - foreach( $limit as $action => $restrictions ) { - if($restrictions != '' ) { - $dbw->replace( 'page_restrictions', array(array('pr_page', 'pr_type')), - array( 'pr_page' => $id, - 'pr_type' => $action, - 'pr_level' => $restrictions, - 'pr_cascade' => ($cascade && $action == 'edit') ? 1 : 0, + foreach ( $limit as $action => $restrictions ) { + if ( $restrictions != '' ) { + $dbw->replace( 'page_restrictions', array( array( 'pr_page', 'pr_type' ) ), + array( 'pr_page' => $id, + 'pr_type' => $action, + 'pr_level' => $restrictions, + 'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0, 'pr_expiry' => $encodedExpiry[$action] ), __METHOD__ ); } else { $dbw->delete( 'page_restrictions', array( 'pr_page' => $id, @@ -2099,14 +2420,14 @@ class Article { ), 'Article::protect' ); - wfRunHooks( 'NewRevisionFromEditComplete', array($this, $nullRevision, $latest, $wgUser) ); + wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $wgUser ) ); wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) ); # Update the protection log $log = new LogPage( 'protect' ); - if( $protect ) { - $params = array($protect_description,$cascade ? 'cascade' : ''); - $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason), $params ); + if ( $protect ) { + $params = array( $protect_description, $cascade ? 'cascade' : '' ); + $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason ), $params ); } else { $log->addEntry( 'unprotect', $this->mTitle, $reason ); } @@ -2124,13 +2445,13 @@ class Article { * @return String */ protected static function flattenRestrictions( $limit ) { - if( !is_array( $limit ) ) { + if ( !is_array( $limit ) ) { throw new MWException( 'Article::flattenRestrictions given non-array restriction set' ); } $bits = array(); ksort( $limit ); - foreach( $limit as $action => $restrictions ) { - if( $restrictions != '' ) { + foreach ( $limit as $action => $restrictions ) { + if ( $restrictions != '' ) { $bits[] = "$action=$restrictions"; } } @@ -2146,7 +2467,7 @@ class Article { $dbw = wfGetDB( DB_MASTER ); // Get the last revision $rev = Revision::newFromTitle( $this->mTitle ); - if( is_null( $rev ) ) + if ( is_null( $rev ) ) return false; // Get the article's contents @@ -2154,9 +2475,9 @@ class Article { $blank = false; // If the page is blank, use the text from the previous revision, // which can only be blank if there's a move/import/protect dummy revision involved - if( $contents == '' ) { + if ( $contents == '' ) { $prev = $rev->getPrevious(); - if( $prev ) { + if ( $prev ) { $contents = $prev->getText(); $blank = true; } @@ -2164,23 +2485,21 @@ class Article { // Find out if there was only one contributor // Only scan the last 20 revisions - $limit = 20; $res = $dbw->select( 'revision', 'rev_user_text', - array( 'rev_page' => $this->getID() ), __METHOD__, - array( 'LIMIT' => $limit ) + array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ), + __METHOD__, + array( 'LIMIT' => 20 ) ); - if( $res === false ) + if ( $res === false ) // This page has no revisions, which is very weird return false; - if( $res->numRows() > 1 ) - $hasHistory = true; - else - $hasHistory = false; + + $hasHistory = ( $res->numRows() > 1 ); $row = $dbw->fetchObject( $res ); $onlyAuthor = $row->rev_user_text; // Try to find a second contributor - foreach( $res as $row ) { - if( $row->rev_user_text != $onlyAuthor ) { + foreach ( $res as $row ) { + if ( $row->rev_user_text != $onlyAuthor ) { $onlyAuthor = false; break; } @@ -2188,18 +2507,18 @@ class Article { $dbw->freeResult( $res ); // Generate the summary with a '$1' placeholder - if( $blank ) { + if ( $blank ) { // The current revision is blank and the one before is also // blank. It's just not our lucky day $reason = wfMsgForContent( 'exbeforeblank', '$1' ); } else { - if( $onlyAuthor ) + if ( $onlyAuthor ) $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor ); else $reason = wfMsgForContent( 'excontent', '$1' ); } - - if( $reason == '-' ) { + + if ( $reason == '-' ) { // Allow these UI messages to be blanked out cleanly return ''; } @@ -2208,7 +2527,7 @@ class Article { $contents = preg_replace( "/[\n\r]/", ' ', $contents ); // Calculate the maximum amount of chars to get // Max content length = max comment length - length of the comment (excl. $1) - '...' - $maxLength = 255 - (strlen( $reason ) - 2) - 3; + $maxLength = 255 - ( strlen( $reason ) - 2 ) - 3; $contents = $wgContLang->truncate( $contents, $maxLength ); // Remove possible unfinished links $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents ); @@ -2232,10 +2551,10 @@ class Article { $reason = $this->DeleteReasonList; - if( $reason != 'other' && $this->DeleteReason != '' ) { + if ( $reason != 'other' && $this->DeleteReason != '' ) { // Entry from drop down menu + additional comment $reason .= wfMsgForContent( 'colon-separator' ) . $this->DeleteReason; - } elseif( $reason == 'other' ) { + } elseif ( $reason == 'other' ) { $reason = $this->DeleteReason; } # Flag to hide all contents of the archived revisions @@ -2244,7 +2563,7 @@ class Article { # This code desperately needs to be totally rewritten # Read-only check... - if( wfReadOnly() ) { + if ( wfReadOnly() ) { $wgOut->readOnlyPage(); return; } @@ -2252,7 +2571,7 @@ class Article { # Check permissions $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser ); - if( count( $permission_errors ) > 0 ) { + if ( count( $permission_errors ) > 0 ) { $wgOut->showPermissionsErrorPage( $permission_errors ); return; } @@ -2263,27 +2582,37 @@ class Article { $dbw = wfGetDB( DB_MASTER ); $conds = $this->mTitle->pageCond(); $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); - if( $latest === false ) { - $wgOut->showFatalError( wfMsgExt( 'cannotdelete', array( 'parse' ) ) ); + if ( $latest === false ) { + $wgOut->showFatalError( + Html::rawElement( + 'div', + array( 'class' => 'error mw-error-cannotdelete' ), + wfMsgExt( 'cannotdelete', array( 'parse' ), $this->mTitle->getPrefixedText() ) + ) + ); $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); - LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTitle->getPrefixedText() ); + LogEventsList::showLogExtract( + $wgOut, + 'delete', + $this->mTitle->getPrefixedText() + ); return; } # Hack for big sites $bigHistory = $this->isBigDeletion(); - if( $bigHistory && !$this->mTitle->userCan( 'bigdelete' ) ) { + if ( $bigHistory && !$this->mTitle->userCan( 'bigdelete' ) ) { global $wgLang, $wgDeleteRevisionsLimit; $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n", array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) ); return; } - if( $confirm ) { + if ( $confirm ) { $this->doDelete( $reason, $suppress ); - if( $wgRequest->getCheck( 'wpWatch' ) ) { + if ( $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn() ) { $this->doWatch(); - } elseif( $this->mTitle->userIsWatching() ) { + } elseif ( $this->mTitle->userIsWatching() ) { $this->doUnwatch(); } return; @@ -2291,14 +2620,20 @@ class Article { // Generate deletion reason $hasHistory = false; - if( !$reason ) $reason = $this->generateReason($hasHistory); + if ( !$reason ) $reason = $this->generateReason( $hasHistory ); // If the page has a history, insert a warning - if( $hasHistory && !$confirm ) { + if ( $hasHistory && !$confirm ) { + global $wgLang; $skin = $wgUser->getSkin(); - $wgOut->addHTML( '<strong>' . wfMsgExt( 'historywarning', array( 'parseinline' ) ) . ' ' . $skin->historyLink() . '</strong>' ); - if( $bigHistory ) { - global $wgLang, $wgDeleteRevisionsLimit; + $revisions = $this->estimateRevisionCount(); + $wgOut->addHTML( '<strong class="mw-delete-warning-revisions">' . + wfMsgExt( 'historywarning', array( 'parseinline' ), $wgLang->formatNum( $revisions ) ) . + wfMsgHtml( 'word-separator' ) . $skin->historyLink() . + '</strong>' + ); + if ( $bigHistory ) { + global $wgDeleteRevisionsLimit; $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n", array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) ); } @@ -2312,7 +2647,7 @@ class Article { */ public function isBigDeletion() { global $wgDeleteRevisionsLimit; - if( $wgDeleteRevisionsLimit ) { + if ( $wgDeleteRevisionsLimit ) { $revCount = $this->estimateRevisionCount(); return $revCount > $wgDeleteRevisionsLimit; } @@ -2325,10 +2660,10 @@ class Article { public function estimateRevisionCount() { $dbr = wfGetDB( DB_SLAVE ); // For an exact count... - //return $dbr->selectField( 'revision', 'COUNT(*)', + // return $dbr->selectField( 'revision', 'COUNT(*)', // array( 'rev_page' => $this->getId() ), __METHOD__ ); return $dbr->estimateRowCount( 'revision', '*', - array( 'rev_page' => $this->getId() ), __METHOD__ ); + array( 'rev_page' => $this->getId() ), __METHOD__ ); } /** @@ -2355,12 +2690,12 @@ class Article { 'LIMIT' => $num ) ) ); - if( !$res ) { + if ( !$res ) { wfProfileOut( __METHOD__ ); return array(); } $row = $db->fetchObject( $res ); - if( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { + if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { $db = wfGetDB( DB_MASTER ); $continue--; } else { @@ -2385,24 +2720,33 @@ class Article { wfDebug( "Article::confirmDelete\n" ); - $wgOut->setSubtitle( wfMsgHtml( 'delete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->mTitle ) ) ); + $deleteBackLink = $wgUser->getSkin()->link( + $this->mTitle, + null, + array(), + array(), + array( 'known', 'noclasses' ) + ); + $wgOut->setSubtitle( wfMsgHtml( 'delete-backlink', $deleteBackLink ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addWikiMsg( 'confirmdeletetext' ); - if( $wgUser->isAllowed( 'suppressrevision' ) ) { + wfRunHooks( 'ArticleConfirmDelete', array( $this, $wgOut, &$reason ) ); + + if ( $wgUser->isAllowed( 'suppressrevision' ) ) { $suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\"> <td></td> - <td class='mw-input'>" . + <td class='mw-input'><strong>" . Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) . - "</td> + "</strong></td> </tr>"; } else { $suppress = ''; } $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(); - $form = Xml::openElement( 'form', array( 'method' => 'post', + $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' ) ) ) . @@ -2422,17 +2766,27 @@ class Article { Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) . "</td> <td class='mw-input'>" . - Xml::input( 'wpReason', 60, $reason, array( 'type' => 'text', 'maxlength' => '255', - 'tabindex' => '2', 'id' => 'wpReason' ) ) . + Html::input( 'wpReason', $reason, 'text', array( + 'size' => '60', + 'maxlength' => '255', + 'tabindex' => '2', + 'id' => 'wpReason', + 'autofocus' + ) ) . "</td> - </tr> + </tr>"; + # Dissalow watching is user is not logged in + if ( $wgUser->isLoggedIn() ) { + $form .= " <tr> <td></td> <td class='mw-input'>" . Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) . "</td> - </tr> + </tr>"; + } + $form .= " $suppress <tr> <td></td> @@ -2446,14 +2800,25 @@ class Article { Xml::hidden( 'wpEditToken', $wgUser->editToken() ) . Xml::closeElement( 'form' ); - if( $wgUser->isAllowed( 'editinterface' ) ) { + if ( $wgUser->isAllowed( 'editinterface' ) ) { $skin = $wgUser->getSkin(); - $link = $skin->makeLink ( 'MediaWiki:Deletereason-dropdown', wfMsgHtml( 'delete-edit-reasonlist' ) ); + $title = Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' ); + $link = $skin->link( + $title, + wfMsgHtml( 'delete-edit-reasonlist' ), + array(), + array( 'action' => 'edit' ) + ); $form .= '<p class="mw-delete-editreasons">' . $link . '</p>'; } $wgOut->addHTML( $form ); - LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTitle->getPrefixedText() ); + $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); + LogEventsList::showLogExtract( + $wgOut, + 'delete', + $this->mTitle->getPrefixedText() + ); } /** @@ -2464,8 +2829,8 @@ class Article { $id = $this->mTitle->getArticleID( GAID_FOR_UPDATE ); $error = ''; - if( wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason, &$error)) ) { - if( $this->doDeleteArticle( $reason, $suppress, $id ) ) { + if ( wfRunHooks( 'ArticleDelete', array( &$this, &$wgUser, &$reason, &$error ) ) ) { + if ( $this->doDeleteArticle( $reason, $suppress, $id ) ) { $deleted = $this->mTitle->getPrefixedText(); $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); @@ -2475,15 +2840,25 @@ class Article { $wgOut->addWikiMsg( 'deletedtext', $deleted, $loglink ); $wgOut->returnToMain( false ); - wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason, $id)); + wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$wgUser, $reason, $id ) ); + } + } else { + if ( $error == '' ) { + $wgOut->showFatalError( + Html::rawElement( + 'div', + array( 'class' => 'error mw-error-cannotdelete' ), + wfMsgExt( 'cannotdelete', array( 'parse' ), $this->mTitle->getPrefixedText() ) + ) + ); + $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); + LogEventsList::showLogExtract( + $wgOut, + 'delete', + $this->mTitle->getPrefixedText() + ); } else { - if( $error == '' ) { - $wgOut->showFatalError( wfMsgExt( 'cannotdelete', array( 'parse' ) ) ); - $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); - LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTitle->getPrefixedText() ); - } else { - $wgOut->showFatalError( $error ); - } + $wgOut->showFatalError( $error ); } } } @@ -2497,22 +2872,22 @@ class Article { global $wgUseSquid, $wgDeferredUpdateList; global $wgUseTrackbacks; - wfDebug( __METHOD__."\n" ); + wfDebug( __METHOD__ . "\n" ); $dbw = wfGetDB( DB_MASTER ); $ns = $this->mTitle->getNamespace(); $t = $this->mTitle->getDBkey(); $id = $id ? $id : $this->mTitle->getArticleID( GAID_FOR_UPDATE ); - if( $t == '' || $id == 0 ) { + if ( $t == '' || $id == 0 ) { return false; } - $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getRawText() ), -1 ); + $u = new SiteStatsUpdate( 0, 1, - (int)$this->isCountable( $this->getRawText() ), -1 ); array_push( $wgDeferredUpdateList, $u ); // Bitfields to further suppress the content - if( $suppress ) { + if ( $suppress ) { $bitfield = 0; // This should be 15... $bitfield |= Revision::DELETED_TEXT; @@ -2560,26 +2935,26 @@ class Article { $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ ); # Now that it's safely backed up, delete it - $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__); + $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ ); $ok = ( $dbw->affectedRows() > 0 ); // getArticleId() uses slave, could be laggy - if( !$ok ) { + if ( !$ok ) { $dbw->rollback(); return false; } - + # 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; + foreach ( $res as $row ) { + $cats [] = $row->cl_to; } $this->updateCategoryCounts( array(), $cats ); # If using cascading deletes, we can skip some explicit deletes - if( !$dbw->cascadingDeletes() ) { + if ( !$dbw->cascadingDeletes() ) { $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); - if($wgUseTrackbacks) + if ( $wgUseTrackbacks ) $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ ); # Delete outgoing links @@ -2593,15 +2968,15 @@ class Article { } # If using cleanup triggers, we can skip some manual deletes - if( !$dbw->cleanupTriggers() ) { + if ( !$dbw->cleanupTriggers() ) { # Clean up recentchanges entries... $dbw->delete( 'recentchanges', - array( 'rc_type != '.RC_LOG, + array( 'rc_type != ' . RC_LOG, 'rc_namespace' => $this->mTitle->getNamespace(), - 'rc_title' => $this->mTitle->getDBKey() ), + 'rc_title' => $this->mTitle->getDBkey() ), __METHOD__ ); $dbw->delete( 'recentchanges', - array( 'rc_type != '.RC_LOG, 'rc_cur_id' => $id ), + array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ), __METHOD__ ); } @@ -2653,17 +3028,17 @@ class Article { $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ); $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) ); - if( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) + if ( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) $errors[] = array( 'sessionfailure' ); - if( $wgUser->pingLimiter( 'rollback' ) || $wgUser->pingLimiter() ) { + if ( $wgUser->pingLimiter( 'rollback' ) || $wgUser->pingLimiter() ) { $errors[] = array( 'actionthrottledtext' ); } # If there were errors, bail out now - if( !empty( $errors ) ) + if ( !empty( $errors ) ) return $errors; - return $this->commitRollback($fromP, $summary, $bot, $resultDetails); + return $this->commitRollback( $fromP, $summary, $bot, $resultDetails ); } /** @@ -2675,95 +3050,102 @@ class Article { * ly if you want to use custom permissions checks. If you don't, use * doRollback() instead. */ - public function commitRollback($fromP, $summary, $bot, &$resultDetails) { + public function commitRollback( $fromP, $summary, $bot, &$resultDetails ) { global $wgUseRCPatrol, $wgUser, $wgLang; $dbw = wfGetDB( DB_MASTER ); - if( wfReadOnly() ) { + if ( wfReadOnly() ) { return array( array( 'readonlytext' ) ); } # Get the last editor $current = Revision::newFromTitle( $this->mTitle ); - if( is_null( $current ) ) { + if ( is_null( $current ) ) { # Something wrong... no page? - return array(array('notanarticle')); + return array( array( 'notanarticle' ) ); } $from = str_replace( '_', ' ', $fromP ); - if( $from != $current->getUserText() ) { + # User name given should match up with the top revision. + # If the user was deleted then $from should be empty. + if ( $from != $current->getUserText() ) { $resultDetails = array( 'current' => $current ); - return array(array('alreadyrolled', - htmlspecialchars($this->mTitle->getPrefixedText()), - htmlspecialchars($fromP), - htmlspecialchars($current->getUserText()) - )); + return array( array( 'alreadyrolled', + htmlspecialchars( $this->mTitle->getPrefixedText() ), + htmlspecialchars( $fromP ), + htmlspecialchars( $current->getUserText() ) + ) ); } - # Get the last edit not by this guy - $user = intval( $current->getUser() ); - $user_text = $dbw->addQuotes( $current->getUserText() ); + # Get the last edit not by this guy... + # Note: these may not be public values + $user = intval( $current->getRawUser() ); + $user_text = $dbw->addQuotes( $current->getRawUserText() ); $s = $dbw->selectRow( 'revision', array( 'rev_id', 'rev_timestamp', 'rev_deleted' ), - array( 'rev_page' => $current->getPage(), + array( 'rev_page' => $current->getPage(), "rev_user != {$user} OR rev_user_text != {$user_text}" ), __METHOD__, - array( 'USE INDEX' => 'page_timestamp', + array( 'USE INDEX' => 'page_timestamp', 'ORDER BY' => 'rev_timestamp DESC' ) ); - if( $s === false ) { + 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 ) { + 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')); + return array( array( 'notvisiblerev' ) ); } $set = array(); - if( $bot && $wgUser->isAllowed('markbotedits') ) { + if ( $bot && $wgUser->isAllowed( 'markbotedits' ) ) { # Mark all reverted edits as bot $set['rc_bot'] = 1; } - if( $wgUseRCPatrol ) { + if ( $wgUseRCPatrol ) { # Mark all reverted edits as patrolled $set['rc_patrolled'] = 1; } - if( $set ) { + if ( count( $set ) ) { $dbw->update( 'recentchanges', $set, - array( /* WHERE */ - 'rc_cur_id' => $current->getPage(), - 'rc_user_text' => $current->getUserText(), - "rc_timestamp > '{$s->rev_timestamp}'", - ), __METHOD__ - ); + array( /* WHERE */ + 'rc_cur_id' => $current->getPage(), + 'rc_user_text' => $current->getUserText(), + "rc_timestamp > '{$s->rev_timestamp}'", + ), __METHOD__ + ); } # Generate the edit summary if necessary $target = Revision::newFromId( $s->rev_id ); - if( empty( $summary ) ){ - $summary = wfMsgForContent( 'revertpage' ); + if ( empty( $summary ) ) { + if ( $from == '' ) { // no public user name + $summary = wfMsgForContent( 'revertpage-nouser' ); + } else { + $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()) + $wgLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ), true ), + $current->getId(), $wgLang->timeanddate( $current->getTimestamp() ) ); $summary = wfMsgReplaceArgs( $summary, $args ); # Save $flags = EDIT_UPDATE; - if( $wgUser->isAllowed('minoredit') ) + if ( $wgUser->isAllowed( 'minoredit' ) ) $flags |= EDIT_MINOR; - if( $bot && ($wgUser->isAllowed('markbotedits') || $wgUser->isAllowed('bot')) ) + if ( $bot && ( $wgUser->isAllowed( 'markbotedits' ) || $wgUser->isAllowed( 'bot' ) ) ) $flags |= EDIT_FORCE_BOT; # Actually store the edit $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId() ); - if( !empty( $status->value['revision'] ) ) { + if ( !empty( $status->value['revision'] ) ) { $revId = $status->value['revision']->getId(); } else { $revId = false; @@ -2774,8 +3156,8 @@ class Article { $resultDetails = array( 'summary' => $summary, 'current' => $current, - 'target' => $target, - 'newid' => $revId + 'target' => $target, + 'newid' => $revId ); return array(); } @@ -2795,19 +3177,19 @@ class Article { $details ); - if( in_array( array( 'actionthrottledtext' ), $result ) ) { + if ( in_array( array( 'actionthrottledtext' ), $result ) ) { $wgOut->rateLimited(); return; } - if( isset( $result[0][0] ) && ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) ) { + 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'] ) ){ + if ( isset( $details['current'] ) ) { $current = $details['current']; - if( $current->getComment() != '' ) { - $wgOut->addWikiMsgArray( 'editcomment', array( + if ( $current->getComment() != '' ) { + $wgOut->addWikiMsgArray( 'editcomment', array( $wgUser->getSkin()->formatComment( $current->getComment() ) ), array( 'replaceafter' ) ); } } @@ -2816,19 +3198,19 @@ class Article { # Display permissions errors before read-only message -- there's no # point in misleading the user into thinking the inability to rollback # is only temporary. - if( !empty( $result ) && $result !== array( array( 'readonlytext' ) ) ) { + if ( !empty( $result ) && $result !== array( array( 'readonlytext' ) ) ) { # array_diff is completely broken for arrays of arrays, sigh. Re- # move any 'readonlytext' error manually. $out = array(); - foreach( $result as $error ) { - if( $error != array( 'readonlytext' ) ) { - $out []= $error; + foreach ( $result as $error ) { + if ( $error != array( 'readonlytext' ) ) { + $out [] = $error; } } $wgOut->showPermissionsErrorPage( $out ); return; } - if( $result == array( array( 'readonlytext' ) ) ) { + if ( $result == array( array( 'readonlytext' ) ) ) { $wgOut->readOnlyPage(); return; } @@ -2838,14 +3220,18 @@ class Article { $newId = $details['newid']; $wgOut->setPageTitle( wfMsg( 'actioncomplete' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() ) - . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() ); + if ( $current->getUserText() === '' ) { + $old = wfMsg( 'rev-deleted-user' ); + } else { + $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() ) + . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() ); + } $new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() ) . $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 ) && !$wgUser->getBoolOption( 'norollbackdiff', false ) ) { + if ( !$wgRequest->getBool( 'hidediff', false ) && !$wgUser->getBoolOption( 'norollbackdiff', false ) ) { $de = new DifferenceEngine( $this->mTitle, $current->getId(), $newId, false, true ); $de->showDiff( '', '' ); } @@ -2857,8 +3243,11 @@ class Article { */ public function viewUpdates() { global $wgDeferredUpdateList, $wgDisableCounters, $wgUser; + if ( wfReadOnly() ) { + return; + } # Don't update page view counters on views from bot users (bug 14044) - if( !$wgDisableCounters && !$wgUser->isAllowed('bot') && $this->getID() ) { + if ( !$wgDisableCounters && !$wgUser->isAllowed( 'bot' ) && $this->getID() ) { Article::incViewCount( $this->getID() ); $u = new SiteStatsUpdate( 1, 0, 0 ); array_push( $wgDeferredUpdateList, $u ); @@ -2871,8 +3260,8 @@ class Article { * Prepare text which is about to be saved. * Returns a stdclass with source, pst and output members */ - public function prepareTextForEdit( $text, $revid=null ) { - if( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid) { + public function prepareTextForEdit( $text, $revid = null ) { + if ( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid ) { // Already prepared return $this->mPreparedEdit; } @@ -2881,9 +3270,7 @@ class Article { $edit->revid = $revid; $edit->newText = $text; $edit->pst = $this->preSaveTransform( $text ); - $options = new ParserOptions; - $options->setTidy( true ); - $options->enableLimitReport(); + $options = $this->getParserOptions(); $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $options, true, true, $revid ); $edit->oldText = $this->getContent(); $this->mPreparedEdit = $edit; @@ -2905,13 +3292,13 @@ class Article { * @param $changed Whether or not the content actually changed */ public function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true ) { - global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser, $wgEnableParserCache; + global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgEnableParserCache; wfProfileIn( __METHOD__ ); # Parse the text # Be careful not to double-PST: $text is usually already PST-ed once - if( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { + if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); $editInfo = $this->prepareTextForEdit( $text, $newid ); } else { @@ -2920,10 +3307,8 @@ class Article { } # Save it to the parser cache - if( $wgEnableParserCache ) { - $popts = new ParserOptions; - $popts->setTidy( true ); - $popts->enableLimitReport(); + if ( $wgEnableParserCache ) { + $popts = $this->getParserOptions(); $parserCache = ParserCache::singleton(); $parserCache->save( $editInfo->output, $this, $popts ); } @@ -2931,11 +3316,11 @@ class Article { # Update the links tables $u = new LinksUpdate( $this->mTitle, $editInfo->output ); $u->doUpdate(); - + wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $changed ) ); - if( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { - if( 0 == mt_rand( 0, 99 ) ) { + if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { + if ( 0 == mt_rand( 0, 99 ) ) { // Flush old entries from the `recentchanges` table; we do this on // random requests so as to avoid an increase in writes for no good reason global $wgRCMaxAge; @@ -2951,7 +3336,7 @@ class Article { $title = $this->mTitle->getPrefixedDBkey(); $shortTitle = $this->mTitle->getDBkey(); - if( 0 == $id ) { + if ( 0 == $id ) { wfProfileOut( __METHOD__ ); return; } @@ -2965,24 +3350,24 @@ class Article { # Don't do this if $changed = false otherwise some idiot can null-edit a # load of user talk pages and piss people off, nor if it's a minor edit # by a properly-flagged bot. - if( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed + if ( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed && !( $minoredit && $wgUser->isAllowed( 'nominornewtalk' ) ) ) { - if( wfRunHooks('ArticleEditUpdateNewTalk', array( &$this ) ) ) { + if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this ) ) ) { $other = User::newFromName( $shortTitle, false ); - if( !$other ) { - wfDebug( __METHOD__.": invalid username\n" ); - } elseif( User::isIP( $shortTitle ) ) { + if ( !$other ) { + wfDebug( __METHOD__ . ": invalid username\n" ); + } elseif ( User::isIP( $shortTitle ) ) { // An anonymous user $other->setNewtalk( true ); - } elseif( $other->isLoggedIn() ) { + } elseif ( $other->isLoggedIn() ) { $other->setNewtalk( true ); } else { - wfDebug( __METHOD__. ": don't need to notify a nonexistent user\n" ); + wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" ); } } } - if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { $wgMessageCache->replace( $shortTitle, $text ); } @@ -3016,56 +3401,112 @@ class Article { public function setOldSubtitle( $oldid = 0 ) { global $wgLang, $wgOut, $wgUser, $wgRequest; - if( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) { + if ( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) { return; } + $unhide = $wgRequest->getInt( 'unhide' ) == 1 && + $wgUser->matchEditToken( $wgRequest->getVal( 'token' ), $oldid ); + # Cascade unhide param in links for easy deletion browsing + $extraParams = array(); + if ( $wgRequest->getVal( 'unhide' ) ) { + $extraParams['unhide'] = 1; + } $revision = Revision::newFromId( $oldid ); $current = ( $oldid == $this->mLatest ); $td = $wgLang->timeanddate( $this->mTimestamp, true ); + $tddate = $wgLang->date( $this->mTimestamp, true ); + $tdtime = $wgLang->time( $this->mTimestamp, true ); $sk = $wgUser->getSkin(); $lnk = $current ? wfMsgHtml( 'currentrevisionlink' ) - : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'currentrevisionlink' ) ); + : $sk->link( + $this->mTitle, + wfMsgHtml( 'currentrevisionlink' ), + array(), + $extraParams, + array( 'known', 'noclasses' ) + ); $curdiff = $current ? wfMsgHtml( 'diff' ) - : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'diff' ), 'diff=cur&oldid='.$oldid ); + : $sk->link( + $this->mTitle, + wfMsgHtml( 'diff' ), + array(), + array( + 'diff' => 'cur', + 'oldid' => $oldid + ) + $extraParams, + array( 'known', 'noclasses' ) + ); $prev = $this->mTitle->getPreviousRevisionID( $oldid ) ; $prevlink = $prev - ? $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousrevision' ), 'direction=prev&oldid='.$oldid ) + ? $sk->link( + $this->mTitle, + wfMsgHtml( 'previousrevision' ), + array(), + array( + 'direction' => 'prev', + 'oldid' => $oldid + ) + $extraParams, + array( 'known', 'noclasses' ) + ) : wfMsgHtml( 'previousrevision' ); $prevdiff = $prev - ? $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'diff' ), 'diff=prev&oldid='.$oldid ) + ? $sk->link( + $this->mTitle, + wfMsgHtml( 'diff' ), + array(), + array( + 'diff' => 'prev', + 'oldid' => $oldid + ) + $extraParams, + array( 'known', 'noclasses' ) + ) : wfMsgHtml( 'diff' ); $nextlink = $current ? wfMsgHtml( 'nextrevision' ) - : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextrevision' ), 'direction=next&oldid='.$oldid ); + : $sk->link( + $this->mTitle, + wfMsgHtml( 'nextrevision' ), + array(), + array( + 'direction' => 'next', + 'oldid' => $oldid + ) + $extraParams, + array( 'known', 'noclasses' ) + ); $nextdiff = $current ? wfMsgHtml( 'diff' ) - : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'diff' ), 'diff=next&oldid='.$oldid ); - - $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' ); + : $sk->link( + $this->mTitle, + wfMsgHtml( 'diff' ), + array(), + array( + 'diff' => 'next', + 'oldid' => $oldid + ) + $extraParams, + array( 'known', 'noclasses' ) + ); + + $cdel = ''; + // User can delete revisions or view deleted revisions... + $canHide = $wgUser->isAllowed( 'deleterevision' ); + if ( $canHide || ( $revision->getVisibility() && $wgUser->isAllowed( 'deletedhistory' ) ) ) { + if ( !$revision->userCan( Revision::DELETED_RESTRICTED ) ) { + $cdel = $sk->revDeleteLinkDisabled( $canHide ); // rev was hidden from Sysops } 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>"; + $query = array( + 'type' => 'revision', + 'target' => $this->mTitle->getPrefixedDbkey(), + 'ids' => $oldid + ); + $cdel = $sk->revDeleteLink( $query, $revision->isDeleted( File::DELETED_RESTRICTED ), $canHide ); } - $cdel = "(<small>$cdel</small>) "; + $cdel .= ' '; } - $unhide = $wgRequest->getInt('unhide') == 1 && $wgUser->matchEditToken( $wgRequest->getVal('token'), $oldid ); + # Show user links if allowed to see them. If hidden, then show them only if requested... $userlinks = $sk->revUserTools( $revision, !$unhide ); @@ -3074,11 +3515,20 @@ class Article { ? 'revision-info-current' : 'revision-info'; - $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . wfMsgExt( $infomsg, array( 'parseinline', 'replaceafter' ), - $td, $userlinks, $revision->getID() ) . "</div>\n" . - - "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsgExt( 'revision-nav', array( 'escapenoentities', 'parsemag', 'replaceafter' ), - $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t"; + $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . + wfMsgExt( + $infomsg, + array( 'parseinline', 'replaceafter' ), + $td, + $userlinks, + $revision->getID(), + $tddate, + $tdtime, + $revision->getUser() + ) . + "</div>\n" . + "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsgExt( 'revision-nav', array( 'escapenoentities', 'parsemag', 'replaceafter' ), + $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t"; $wgOut->setSubtitle( $r ); } @@ -3102,20 +3552,20 @@ class Article { */ protected function tryFileCache() { static $called = false; - if( $called ) { + if ( $called ) { wfDebug( "Article::tryFileCache(): called twice!?\n" ); return false; } $called = true; - if( $this->isFileCacheable() ) { + if ( $this->isFileCacheable() ) { $cache = new HTMLFileCache( $this->mTitle ); - if( $cache->isFileCacheGood( $this->mTouched ) ) { + if ( $cache->isFileCacheGood( $this->mTouched ) ) { wfDebug( "Article::tryFileCache(): about to load file\n" ); $cache->loadFromFileCache(); return true; } else { wfDebug( "Article::tryFileCache(): starting buffer\n" ); - ob_start( array(&$cache, 'saveToFileCache' ) ); + ob_start( array( &$cache, 'saveToFileCache' ) ); } } else { wfDebug( "Article::tryFileCache(): not cacheable\n" ); @@ -3129,10 +3579,10 @@ class Article { */ public function isFileCacheable() { $cacheable = false; - if( HTMLFileCache::useFileCache() ) { + if ( HTMLFileCache::useFileCache() ) { $cacheable = $this->getID() && !$this->mRedirectedFrom; // Extension may have reason to disable file caching on some pages. - if( $cacheable ) { + if ( $cacheable ) { $cacheable = wfRunHooks( 'IsFileCacheable', array( &$this ) ); } } @@ -3144,7 +3594,7 @@ class Article { * */ public function checkTouched() { - if( !$this->mDataLoaded ) { + if ( !$this->mDataLoaded ) { $this->loadPageData(); } return !$this->mIsRedirect; @@ -3155,7 +3605,7 @@ class Article { */ public function getTouched() { # Ensure that page data has been loaded - if( !$this->mDataLoaded ) { + if ( !$this->mDataLoaded ) { $this->loadPageData(); } return $this->mTouched; @@ -3165,7 +3615,7 @@ class Article { * Get the page_latest field */ public function getLatest() { - if( !$this->mDataLoaded ) { + if ( !$this->mDataLoaded ) { $this->loadPageData(); } return (int)$this->mLatest; @@ -3193,7 +3643,7 @@ class Article { $revision->insertOn( $dbw ); $this->updateRevisionOn( $dbw, $revision ); - wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false, $wgUser) ); + wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $wgUser ) ); wfProfileOut( __METHOD__ ); } @@ -3205,14 +3655,15 @@ class Article { */ public static function incViewCount( $id ) { $id = intval( $id ); - global $wgHitcounterUpdateFreq, $wgDBtype; + global $wgHitcounterUpdateFreq; $dbw = wfGetDB( DB_MASTER ); $pageTable = $dbw->tableName( 'page' ); $hitcounterTable = $dbw->tableName( 'hitcounter' ); $acchitsTable = $dbw->tableName( 'acchits' ); + $dbType = $dbw->getType(); - if( $wgHitcounterUpdateFreq <= 1 ) { + if ( $wgHitcounterUpdateFreq <= 1 || $dbType == 'sqlite' ) { $dbw->query( "UPDATE $pageTable SET page_counter = page_counter + 1 WHERE page_id = $id" ); return; } @@ -3222,37 +3673,36 @@ class Article { $dbw->query( "INSERT INTO $hitcounterTable (hc_id) VALUES ({$id})" ); - $checkfreq = intval( $wgHitcounterUpdateFreq/25 + 1 ); - if( (rand() % $checkfreq != 0) or ($dbw->lastErrno() != 0) ){ + $checkfreq = intval( $wgHitcounterUpdateFreq / 25 + 1 ); + if ( ( rand() % $checkfreq != 0 ) or ( $dbw->lastErrno() != 0 ) ) { # Most of the time (or on SQL errors), skip row count check $dbw->ignoreErrors( $oldignore ); return; } - $res = $dbw->query("SELECT COUNT(*) as n FROM $hitcounterTable"); + $res = $dbw->query( "SELECT COUNT(*) as n FROM $hitcounterTable" ); $row = $dbw->fetchObject( $res ); $rown = intval( $row->n ); - if( $rown >= $wgHitcounterUpdateFreq ){ + if ( $rown >= $wgHitcounterUpdateFreq ) { wfProfileIn( 'Article::incViewCount-collect' ); $old_user_abort = ignore_user_abort( true ); - if($wgDBtype == 'mysql') - $dbw->query("LOCK TABLES $hitcounterTable WRITE"); - $tabletype = $wgDBtype == 'mysql' ? "ENGINE=HEAP " : ''; - $dbw->query("CREATE TEMPORARY TABLE $acchitsTable $tabletype AS ". - "SELECT hc_id,COUNT(*) AS hc_n FROM $hitcounterTable ". - 'GROUP BY hc_id'); - $dbw->query("DELETE FROM $hitcounterTable"); - if($wgDBtype == 'mysql') { - $dbw->query('UNLOCK TABLES'); - $dbw->query("UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n ". - 'WHERE page_id = hc_id'); + $dbw->lockTables( array(), array( 'hitcounter' ), __METHOD__, false ); + $tabletype = $dbType == 'mysql' ? "ENGINE=HEAP " : ''; + $dbw->query( "CREATE TEMPORARY TABLE $acchitsTable $tabletype AS " . + "SELECT hc_id,COUNT(*) AS hc_n FROM $hitcounterTable " . + 'GROUP BY hc_id', __METHOD__ ); + $dbw->delete( 'hitcounter', '*', __METHOD__ ); + $dbw->unlockTables( __METHOD__ ); + if ( $dbType == 'mysql' ) { + $dbw->query( "UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n " . + 'WHERE page_id = hc_id', __METHOD__ ); } else { - $dbw->query("UPDATE $pageTable SET page_counter=page_counter + hc_n ". - "FROM $acchitsTable WHERE page_id = hc_id"); + $dbw->query( "UPDATE $pageTable SET page_counter=page_counter + hc_n " . + "FROM $acchitsTable WHERE page_id = hc_id", __METHOD__ ); } - $dbw->query("DROP TABLE $acchitsTable"); + $dbw->query( "DROP TABLE $acchitsTable", __METHOD__ ); ignore_user_abort( $old_user_abort ); wfProfileOut( 'Article::incViewCount-collect' ); @@ -3271,10 +3721,9 @@ class Article { * * @param $title a title object */ - public static function onArticleCreate( $title ) { # Update existence markers on article/talk tabs... - if( $title->isTalkPage() ) { + if ( $title->isTalkPage() ) { $other = $title->getSubjectPage(); } else { $other = $title->getTalkPage(); @@ -3290,7 +3739,7 @@ class Article { public static function onArticleDelete( $title ) { global $wgMessageCache; # Update existence markers on article/talk tabs... - if( $title->isTalkPage() ) { + if ( $title->isTalkPage() ) { $other = $title->getSubjectPage(); } else { $other = $title->getTalkPage(); @@ -3305,16 +3754,16 @@ class Article { HTMLFileCache::clearFileCache( $title ); # Messages - if( $title->getNamespace() == NS_MEDIAWIKI ) { + if ( $title->getNamespace() == NS_MEDIAWIKI ) { $wgMessageCache->replace( $title->getDBkey(), false ); } # Images - if( $title->getNamespace() == NS_FILE ) { + if ( $title->getNamespace() == NS_FILE ) { $update = new HTMLCacheUpdate( $title, 'imagelinks' ); $update->doUpdate(); } # User talk pages - if( $title->getNamespace() == NS_USER_TALK ) { + if ( $title->getNamespace() == NS_USER_TALK ) { $user = User::newFromName( $title->getText(), false ); $user->setNewtalk( false ); } @@ -3359,7 +3808,7 @@ class Article { public function info() { global $wgLang, $wgOut, $wgAllowPageInfo, $wgUser; - if( !$wgAllowPageInfo ) { + if ( !$wgAllowPageInfo ) { $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' ); return; } @@ -3370,9 +3819,9 @@ class Article { $wgOut->setPageTitleActionText( wfMsg( 'info_short' ) ); $wgOut->setSubtitle( wfMsgHtml( 'infosubtitle' ) ); - if( !$this->mTitle->exists() ) { + if ( !$this->mTitle->exists() ) { $wgOut->addHTML( '<div class="noarticletext">' ); - if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { // This doesn't quite make sense; the user is asking for // information about the _page_, not the message... -- RC $wgOut->addHTML( htmlspecialchars( wfMsgWeirdKey( $this->mTitle->getText() ) ) ); @@ -3398,14 +3847,14 @@ class Article { $pageInfo = $this->pageCountInfo( $page ); $talkInfo = $this->pageCountInfo( $page->getTalkPage() ); - $wgOut->addHTML( "<ul><li>" . wfMsg("numwatchers", $wgLang->formatNum( $numwatchers ) ) . '</li>' ); - $wgOut->addHTML( "<li>" . wfMsg('numedits', $wgLang->formatNum( $pageInfo['edits'] ) ) . '</li>'); - if( $talkInfo ) { - $wgOut->addHTML( '<li>' . wfMsg("numtalkedits", $wgLang->formatNum( $talkInfo['edits'] ) ) . '</li>'); + $wgOut->addHTML( "<ul><li>" . wfMsg( "numwatchers", $wgLang->formatNum( $numwatchers ) ) . '</li>' ); + $wgOut->addHTML( "<li>" . wfMsg( 'numedits', $wgLang->formatNum( $pageInfo['edits'] ) ) . '</li>' ); + if ( $talkInfo ) { + $wgOut->addHTML( '<li>' . wfMsg( "numtalkedits", $wgLang->formatNum( $talkInfo['edits'] ) ) . '</li>' ); } - $wgOut->addHTML( '<li>' . wfMsg("numauthors", $wgLang->formatNum( $pageInfo['authors'] ) ) . '</li>' ); - if( $talkInfo ) { - $wgOut->addHTML( '<li>' . wfMsg('numtalkauthors', $wgLang->formatNum( $talkInfo['authors'] ) ) . '</li>' ); + $wgOut->addHTML( '<li>' . wfMsg( "numauthors", $wgLang->formatNum( $pageInfo['authors'] ) ) . '</li>' ); + if ( $talkInfo ) { + $wgOut->addHTML( '<li>' . wfMsg( 'numtalkauthors', $wgLang->formatNum( $talkInfo['authors'] ) ) . '</li>' ); } $wgOut->addHTML( '</ul>' ); } @@ -3418,9 +3867,9 @@ class Article { * @param $title Title object * @return array */ - protected function pageCountInfo( $title ) { + public function pageCountInfo( $title ) { $id = $title->getArticleId(); - if( $id == 0 ) { + if ( $id == 0 ) { return false; } $dbr = wfGetDB( DB_SLAVE ); @@ -3451,7 +3900,7 @@ class Article { public function getUsedTemplates() { $result = array(); $id = $this->mTitle->getArticleID(); - if( $id == 0 ) { + if ( $id == 0 ) { return array(); } $dbr = wfGetDB( DB_SLAVE ); @@ -3459,8 +3908,8 @@ class Article { array( 'tl_namespace', 'tl_title' ), array( 'tl_from' => $id ), __METHOD__ ); - if( $res !== false ) { - foreach( $res as $row ) { + if ( $res !== false ) { + foreach ( $res as $row ) { $result[] = Title::makeTitle( $row->tl_namespace, $row->tl_title ); } } @@ -3477,17 +3926,17 @@ class Article { public function getHiddenCategories() { $result = array(); $id = $this->mTitle->getArticleID(); - if( $id == 0 ) { + if ( $id == 0 ) { return array(); } $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'), + 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ), __METHOD__ ); - if( $res !== false ) { - foreach( $res as $row ) { + if ( $res !== false ) { + foreach ( $res as $row ) { $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); } } @@ -3508,24 +3957,24 @@ class Article { # Redirect autosummaries $ot = Title::newFromRedirect( $oldtext ); $rt = Title::newFromRedirect( $newtext ); - if( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) { + if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) { return wfMsgForContent( 'autoredircomment', $rt->getFullText() ); } # New page autosummaries - if( $flags & EDIT_NEW && strlen( $newtext ) ) { + 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), + str_replace( "\n", ' ', $newtext ), max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) ); return wfMsgForContent( 'autosumm-new', $truncatedtext ); } # Blanking autosummaries - if( $oldtext != '' && $newtext == '' ) { + if ( $oldtext != '' && $newtext == '' ) { return wfMsgForContent( 'autosumm-blank' ); - } elseif( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500) { + } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) { # Removing more than 90% of the article global $wgContLang; $truncatedtext = $wgContLang->truncate( @@ -3547,72 +3996,108 @@ class Article { * @param $text String * @param $cache Boolean */ - public function outputWikiText( $text, $cache = true ) { - global $wgParser, $wgUser, $wgOut, $wgEnableParserCache, $wgUseFileCache; - - $popts = $wgOut->parserOptions(); - $popts->setTidy(true); - $popts->enableLimitReport(); - $parserOutput = $wgParser->parse( $text, $this->mTitle, - $popts, true, true, $this->getRevIdFetched() ); - $popts->setTidy(false); - $popts->enableLimitReport( false ); - if( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) { + public function outputWikiText( $text, $cache = true, $parserOptions = false ) { + global $wgOut; + + $this->mParserOutput = $this->getOutputFromWikitext( $text, $cache, $parserOptions ); + $wgOut->addParserOutput( $this->mParserOutput ); + } + + /** + * This does all the heavy lifting for outputWikitext, except it returns the parser + * output instead of sending it straight to $wgOut. Makes things nice and simple for, + * say, embedding thread pages within a discussion system (LiquidThreads) + */ + public function getOutputFromWikitext( $text, $cache = true, $parserOptions = false ) { + global $wgParser, $wgOut, $wgEnableParserCache, $wgUseFileCache; + + if ( !$parserOptions ) { + $parserOptions = $this->getParserOptions(); + } + + $time = - wfTime(); + $this->mParserOutput = $wgParser->parse( $text, $this->mTitle, + $parserOptions, true, true, $this->getRevIdFetched() ); + $time += wfTime(); + + # Timing hack + if ( $time > 3 ) { + wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, + $this->mTitle->getPrefixedDBkey() ) ); + } + + if ( $wgEnableParserCache && $cache && $this && $this->mParserOutput->getCacheTime() != -1 ) { $parserCache = ParserCache::singleton(); - $parserCache->save( $parserOutput, $this, $popts ); + $parserCache->save( $this->mParserOutput, $this, $parserOptions ); } // Make sure file cache is not used on uncacheable content. // Output that has magic words in it can still use the parser cache // (if enabled), though it will generally expire sooner. - if( $parserOutput->getCacheTime() == -1 || $parserOutput->containsOldMagic() ) { + if ( $this->mParserOutput->getCacheTime() == -1 || $this->mParserOutput->containsOldMagic() ) { $wgUseFileCache = false; } + $this->doCascadeProtectionUpdates( $this->mParserOutput ); + return $this->mParserOutput; + } - if( $this->isCurrent() && !wfReadOnly() && $this->mTitle->areRestrictionsCascading() ) { - // templatelinks table may have become out of sync, - // especially if using variable-based transclusions. - // For paranoia, check if things have changed and if - // so apply updates to the database. This will ensure - // that cascaded protections apply as soon as the changes - // are visible. + /** + * Get parser options suitable for rendering the primary article wikitext + */ + public function getParserOptions() { + global $wgUser; + if ( !$this->mParserOptions ) { + $this->mParserOptions = new ParserOptions( $wgUser ); + $this->mParserOptions->setTidy( true ); + $this->mParserOptions->enableLimitReport(); + } + return $this->mParserOptions; + } - # Get templates from templatelinks - $id = $this->mTitle->getArticleID(); + protected function doCascadeProtectionUpdates( $parserOutput ) { + if ( !$this->isCurrent() || wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) { + return; + } - $tlTemplates = array(); + // templatelinks table may have become out of sync, + // especially if using variable-based transclusions. + // For paranoia, check if things have changed and if + // so apply updates to the database. This will ensure + // that cascaded protections apply as soon as the changes + // are visible. - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( array( 'templatelinks' ), - array( 'tl_namespace', 'tl_title' ), - array( 'tl_from' => $id ), - __METHOD__ ); + # Get templates from templatelinks + $id = $this->mTitle->getArticleID(); - global $wgContLang; - foreach( $res as $row ) { - $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true; - } + $tlTemplates = array(); - # Get templates from parser output. - $poTemplates = array(); - foreach ( $parserOutput->getTemplates() as $ns => $templates ) { - foreach ( $templates as $dbk => $id ) { - $key = $row->tl_namespace . ':'. $row->tl_title; - $poTemplates["$ns:$dbk"] = true; - } - } + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'templatelinks' ), + array( 'tl_namespace', 'tl_title' ), + array( 'tl_from' => $id ), + __METHOD__ ); - # Get the diff - # Note that we simulate array_diff_key in PHP <5.0.x - $templates_diff = array_diff_key( $poTemplates, $tlTemplates ); + global $wgContLang; + foreach ( $res as $row ) { + $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true; + } - if( count( $templates_diff ) > 0 ) { - # Whee, link updates time. - $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); - $u->doUpdate(); + # Get templates from parser output. + $poTemplates = array(); + foreach ( $parserOutput->getTemplates() as $ns => $templates ) { + foreach ( $templates as $dbk => $id ) { + $poTemplates["$ns:$dbk"] = true; } } - $wgOut->addParserOutput( $parserOutput ); + # Get the diff + # Note that we simulate array_diff_key in PHP <5.0.x + $templates_diff = array_diff_key( $poTemplates, $tlTemplates ); + + if ( count( $templates_diff ) > 0 ) { + # Whee, link updates time. + $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); + $u->doUpdate(); + } } /** @@ -3634,27 +4119,30 @@ class Article { # # Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE. $insertCats = array_merge( $added, $deleted ); - if( !$insertCats ) { + if ( !$insertCats ) { # Okay, nothing to do return; } $insertRows = array(); - foreach( $insertCats as $cat ) { - $insertRows[] = array( 'cat_title' => $cat ); + foreach ( $insertCats as $cat ) { + $insertRows[] = array( + 'cat_id' => $dbw->nextSequenceValue( 'category_cat_id_seq' ), + '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 ) { + if ( $ns == NS_CATEGORY ) { $addFields[] = 'cat_subcats = cat_subcats + 1'; $removeFields[] = 'cat_subcats = cat_subcats - 1'; - } elseif( $ns == NS_FILE ) { + } elseif ( $ns == NS_FILE ) { $addFields[] = 'cat_files = cat_files + 1'; $removeFields[] = 'cat_files = cat_files - 1'; } - if( $added ) { + if ( $added ) { $dbw->update( 'category', $addFields, @@ -3662,7 +4150,7 @@ class Article { __METHOD__ ); } - if( $deleted ) { + if ( $deleted ) { $dbw->update( 'category', $removeFields, @@ -3671,4 +4159,37 @@ class Article { ); } } + + /** Lightweight method to get the parser output for a page, checking the parser cache + * and so on. Doesn't consider most of the stuff that Article::view is forced to + * consider, so it's not appropriate to use there. + */ + function getParserOutput( $oldid = null ) { + global $wgEnableParserCache, $wgUser, $wgOut; + + // Should the parser cache be used? + $useParserCache = $wgEnableParserCache && + intval( $wgUser->getOption( 'stubthreshold' ) ) == 0 && + $this->exists() && + $oldid === null; + + wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); + if ( $wgUser->getOption( 'stubthreshold' ) ) { + wfIncrStats( 'pcache_miss_stub' ); + } + + $parserOutput = false; + if ( $useParserCache ) { + $parserOutput = ParserCache::singleton()->get( $this, $this->getParserOptions() ); + } + + if ( $parserOutput === false ) { + // Cache miss; parse and output it. + $rev = Revision::newFromTitle( $this->getTitle(), $oldid ); + + return $this->getOutputFromWikitext( $rev->getText(), $useParserCache ); + } else { + return $parserOutput; + } + } } diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php index b29e13f2..87ac8adb 100644 --- a/includes/AuthPlugin.php +++ b/includes/AuthPlugin.php @@ -63,8 +63,9 @@ class AuthPlugin { * Modify options in the login template. * * @param $template UserLoginTemplate object. + * @param $type String 'signup' or 'login'. */ - public function modifyUITemplate( &$template ) { + public function modifyUITemplate( &$template, &$type ) { # Override this! $template->set( 'usedomain', false ); } @@ -97,7 +98,7 @@ class AuthPlugin { * The User object is passed by reference so it can be modified; don't * forget the & on your function declaration. * - * @param User $user + * @param $user User object */ public function updateUser( &$user ) { # Override this and do something @@ -116,13 +117,32 @@ class AuthPlugin { * * This is just a question, and shouldn't perform any actions. * - * @return bool + * @return Boolean */ public function autoCreate() { return false; } /** + * Allow a property change? Properties are the same as preferences + * and use the same keys. 'Realname' 'Emailaddress' and 'Nickname' + * all reference this. + * + * @return Boolean + */ + public function allowPropChange( $prop = '' ) { + if( $prop == 'realname' && is_callable( array( $this, 'allowRealNameChange' ) ) ) { + return $this->allowRealNameChange(); + } elseif( $prop == 'emailaddress' && is_callable( array( $this, 'allowEmailChange' ) ) ) { + return $this->allowEmailChange(); + } elseif( $prop == 'nickname' && is_callable( array( $this, 'allowNickChange' ) ) ) { + return $this->allowNickChange(); + } else { + return true; + } + } + + /** * Can users change their passwords? * * @return bool @@ -152,7 +172,7 @@ class AuthPlugin { * Return true if successful. * * @param $user User object. - * @return bool + * @return Boolean */ public function updateExternalDB( $user ) { return true; @@ -161,7 +181,7 @@ class AuthPlugin { /** * Check to see if external accounts can be created. * Return true if external accounts can be created. - * @return bool + * @return Boolean */ public function canCreateAccounts() { return false; @@ -171,11 +191,11 @@ class AuthPlugin { * Add a user to the external authentication database. * Return true if successful. * - * @param User $user - only the name should be assumed valid at this point - * @param string $password - * @param string $email - * @param string $realname - * @return bool + * @param $user User: only the name should be assumed valid at this point + * @param $password String + * @param $email String + * @param $realname String + * @return Boolean */ public function addUser( $user, $password, $email='', $realname='' ) { return true; @@ -188,7 +208,7 @@ class AuthPlugin { * * This is just a question, and shouldn't perform any actions. * - * @return bool + * @return Boolean */ public function strict() { return false; @@ -199,7 +219,7 @@ class AuthPlugin { * If either this or strict() returns true, local authentication is not used. * * @param $username String: username. - * @return bool + * @return Boolean */ public function strictUserAuth( $username ) { return false; @@ -214,7 +234,7 @@ class AuthPlugin { * forget the & on your function declaration. * * @param $user User object. - * @param $autocreate bool True if user is being autocreated on login + * @param $autocreate Boolean: True if user is being autocreated on login */ public function initUser( &$user, $autocreate=false ) { # Override this to do something. @@ -232,7 +252,6 @@ class AuthPlugin { * Get an instance of a User object * * @param $user User - * @public */ public function getUserInstance( User &$user ) { return new AuthPluginUser( $user ); diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 85e7e668..cecb53f9 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -1,9 +1,6 @@ <?php - /* This defines autoloading handler for whole MediaWiki framework */ -ini_set('unserialize_callback_func', '__autoload' ); - # Locations of core classes # Extension classes are specified with $wgAutoloadClasses # This array is a global instead of a static member of AutoLoader to work around a bug in APC @@ -27,11 +24,23 @@ $wgAutoloadLocalClasses = array( 'Categoryfinder' => 'includes/Categoryfinder.php', 'CategoryPage' => 'includes/CategoryPage.php', 'CategoryViewer' => 'includes/CategoryPage.php', + 'CdbFunctions' => 'includes/Cdb_PHP.php', + 'CdbReader' => 'includes/Cdb.php', + 'CdbReader_DBA' => 'includes/Cdb.php', + 'CdbReader_PHP' => 'includes/Cdb_PHP.php', + 'CdbWriter' => 'includes/Cdb.php', + 'CdbWriter_DBA' => 'includes/Cdb.php', + 'CdbWriter_PHP' => 'includes/Cdb_PHP.php', 'ChangesList' => 'includes/ChangesList.php', 'ChangesFeed' => 'includes/ChangesFeed.php', 'ChangeTags' => 'includes/ChangeTags.php', 'ChannelFeed' => 'includes/Feed.php', + 'Cookie' => 'includes/HttpFunctions.php', + 'CookieJar' => 'includes/HttpFunctions.php', 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php', + 'ConfEditor' => 'includes/ConfEditor.php', + 'ConfEditorParseError' => 'includes/ConfEditor.php', + 'ConfEditorToken' => 'includes/ConfEditor.php', 'ConstantDependency' => 'includes/CacheDependency.php', 'CreativeCommonsRdf' => 'includes/Metadata.php', 'Credits' => 'includes/Credits.php', @@ -66,33 +75,58 @@ $wgAutoloadLocalClasses = array( 'ExternalStoreDB' => 'includes/ExternalStoreDB.php', 'ExternalStoreHttp' => 'includes/ExternalStoreHttp.php', 'ExternalStore' => 'includes/ExternalStore.php', + 'ExternalUser' => 'includes/ExternalUser.php', + 'ExternalUser_Hardcoded' => 'includes/extauth/Hardcoded.php', + 'ExternalUser_MediaWiki' => 'includes/extauth/MediaWiki.php', + 'ExternalUser_vB' => 'includes/extauth/vB.php', 'FatalError' => 'includes/Exception.php', 'FakeTitle' => 'includes/FakeTitle.php', + 'FakeMemCachedClient' => 'includes/ObjectCache.php', 'FauxRequest' => 'includes/WebRequest.php', + 'FauxResponse' => 'includes/WebResponse.php', 'FeedItem' => 'includes/Feed.php', 'FeedUtils' => 'includes/FeedUtils.php', 'FileDeleteForm' => 'includes/FileDeleteForm.php', 'FileDependency' => 'includes/CacheDependency.php', 'FileRevertForm' => 'includes/FileRevertForm.php', - 'FileStore' => 'includes/FileStore.php', 'ForkController' => 'includes/ForkController.php', 'FormatExif' => 'includes/Exif.php', 'FormOptions' => 'includes/FormOptions.php', - 'FSException' => 'includes/FileStore.php', - 'FSTransaction' => 'includes/FileStore.php', + 'GIFMetadataExtractor' => 'includes/media/GIFMetadataExtractor.php', + 'GIFHandler' => 'includes/media/GIF.php', 'GlobalDependency' => 'includes/CacheDependency.php', 'HashBagOStuff' => 'includes/BagOStuff.php', 'HashtableReplacer' => 'includes/StringUtils.php', 'HistoryBlobCurStub' => 'includes/HistoryBlob.php', 'HistoryBlob' => 'includes/HistoryBlob.php', 'HistoryBlobStub' => 'includes/HistoryBlob.php', + 'HistoryPage' => 'includes/HistoryPage.php', + 'HistoryPager' => 'includes/HistoryPage.php', + 'Html' => 'includes/Html.php', 'HTMLCacheUpdate' => 'includes/HTMLCacheUpdate.php', 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php', 'HTMLFileCache' => 'includes/HTMLFileCache.php', + 'HTMLForm' => 'includes/HTMLForm.php', + 'HTMLFormField' => 'includes/HTMLForm.php', + 'HTMLTextField' => 'includes/HTMLForm.php', + 'HTMLIntField' => 'includes/HTMLForm.php', + 'HTMLTextAreaField' => 'includes/HTMLForm.php', + 'HTMLFloatField' => 'includes/HTMLForm.php', + 'HTMLHiddenField' => 'includes/HTMLForm.php', + 'HTMLSubmitField' => 'includes/HTMLForm.php', + 'HTMLEditTools' => 'includes/HTMLForm.php', + 'HTMLCheckField' => 'includes/HTMLForm.php', + 'HTMLSelectField' => 'includes/HTMLForm.php', + 'HTMLSelectOrOtherField' => 'includes/HTMLForm.php', + 'HTMLMultiSelectField' => 'includes/HTMLForm.php', + 'HTMLRadioField' => 'includes/HTMLForm.php', + 'HTMLInfoField' => 'includes/HTMLForm.php', 'Http' => 'includes/HttpFunctions.php', + 'HttpRequest' => 'includes/HttpFunctions.php', 'IEContentAnalyzer' => 'includes/IEContentAnalyzer.php', 'ImageGallery' => 'includes/ImageGallery.php', 'ImageHistoryList' => 'includes/ImagePage.php', + 'ImageHistoryPseudoPager' => 'includes/ImagePage.php', 'ImagePage' => 'includes/ImagePage.php', 'ImageQueryPage' => 'includes/ImageQueryPage.php', 'IncludableSpecialPage' => 'includes/SpecialPage.php', @@ -100,6 +134,10 @@ $wgAutoloadLocalClasses = array( 'Interwiki' => 'includes/Interwiki.php', 'IP' => 'includes/IP.php', 'Job' => 'includes/JobQueue.php', + 'JSMin' => 'includes/JSMin.php', + 'LCStore_DB' => 'includes/LocalisationCache.php', + 'LCStore_CDB' => 'includes/LocalisationCache.php', + 'LCStore_Null' => 'includes/LocalisationCache.php', 'License' => 'includes/Licenses.php', 'Licenses' => 'includes/Licenses.php', 'LinkBatch' => 'includes/LinkBatch.php', @@ -107,6 +145,8 @@ $wgAutoloadLocalClasses = array( 'Linker' => 'includes/Linker.php', 'LinkFilter' => 'includes/LinkFilter.php', 'LinksUpdate' => 'includes/LinksUpdate.php', + 'LocalisationCache' => 'includes/LocalisationCache.php', + 'LocalisationCache_BulkLoad' => 'includes/LocalisationCache.php', 'LogPage' => 'includes/LogPage.php', 'LogPager' => 'includes/LogEventsList.php', 'LogEventsList' => 'includes/LogEventsList.php', @@ -122,24 +162,24 @@ $wgAutoloadLocalClasses = array( 'MediaWikiBagOStuff' => 'includes/BagOStuff.php', 'MediaWiki_I18N' => 'includes/SkinTemplate.php', 'MediaWiki' => 'includes/Wiki.php', - 'memcached' => 'includes/memcached-client.php', + 'MemCachedClientforWiki' => 'includes/memcached-client.php', 'MessageCache' => 'includes/MessageCache.php', 'MimeMagic' => 'includes/MimeMagic.php', 'MWException' => 'includes/Exception.php', + 'MWMemcached' => 'includes/memcached-client.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', - 'PageHistoryPager' => 'includes/PageHistory.php', 'PageQueryPage' => 'includes/PageQueryPage.php', + 'PageHistory' => 'includes/HistoryPage.php', + 'PageHistoryPager' => 'includes/HistoryPage.php', 'Pager' => 'includes/Pager.php', 'PasswordError' => 'includes/User.php', 'PatrolLog' => 'includes/PatrolLog.php', - 'PostgresSearchResult' => 'includes/SearchPostgres.php', - 'PostgresSearchResultSet' => 'includes/SearchPostgres.php', + 'PoolCounter' => 'includes/PoolCounter.php', + 'PoolCounter_Stub' => 'includes/PoolCounter.php', + 'Preferences' => 'includes/Preferences.php', 'PrefixSearch' => 'includes/PrefixSearch.php', 'Profiler' => 'includes/Profiler.php', 'ProfilerSimple' => 'includes/ProfilerSimple.php', @@ -161,20 +201,9 @@ $wgAutoloadLocalClasses = array( 'Revision' => 'includes/Revision.php', 'RSSFeed' => 'includes/Feed.php', 'Sanitizer' => 'includes/Sanitizer.php', - 'SearchEngineDummy' => 'includes/SearchEngine.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', 'SiteConfiguration' => 'includes/SiteConfiguration.php', 'SiteStats' => 'includes/SiteStats.php', + 'SiteStatsInit' => 'includes/SiteStats.php', 'SiteStatsUpdate' => 'includes/SiteStats.php', 'Skin' => 'includes/Skin.php', 'SkinTemplate' => 'includes/SkinTemplate.php', @@ -185,7 +214,13 @@ $wgAutoloadLocalClasses = array( 'SpecialRedirectToSpecial' => 'includes/SpecialPage.php', 'SqlBagOStuff' => 'includes/BagOStuff.php', 'SquidUpdate' => 'includes/SquidUpdate.php', + 'SquidPurgeClient' => 'includes/SquidPurgeClient.php', + 'SquidPurgeClientPool' => 'includes/SquidPurgeClient.php', 'Status' => 'includes/Status.php', + 'StubContLang' => 'includes/StubObject.php', + 'StubUser' => 'includes/StubObject.php', + 'StubUserLang' => 'includes/StubObject.php', + 'StubObject' => 'includes/StubObject.php', 'StringUtils' => 'includes/StringUtils.php', 'TablePager' => 'includes/Pager.php', 'ThumbnailImage' => 'includes/MediaTransformOutput.php', @@ -193,15 +228,20 @@ $wgAutoloadLocalClasses = array( 'TitleDependency' => 'includes/CacheDependency.php', 'Title' => 'includes/Title.php', 'TitleArray' => 'includes/TitleArray.php', + 'TitleArrayFromResult' => 'includes/TitleArray.php', 'TitleListDependency' => 'includes/CacheDependency.php', 'TransformParameterError' => 'includes/MediaTransformOutput.php', - 'TurckBagOStuff' => 'includes/BagOStuff.php', 'UnlistedSpecialPage' => 'includes/SpecialPage.php', + 'UploadBase' => 'includes/upload/UploadBase.php', + 'UploadFromStash' => 'includes/upload/UploadFromStash.php', + 'UploadFromFile' => 'includes/upload/UploadFromFile.php', + 'UploadFromUrl' => 'includes/upload/UploadFromUrl.php', 'User' => 'includes/User.php', 'UserArray' => 'includes/UserArray.php', 'UserArrayFromResult' => 'includes/UserArray.php', 'UserMailer' => 'includes/UserMailer.php', 'UserRightsProxy' => 'includes/UserRightsProxy.php', + 'WantedQueryPage' => 'includes/QueryPage.php', 'WatchedItem' => 'includes/WatchedItem.php', 'WatchlistEditor' => 'includes/WatchlistEditor.php', 'WebRequest' => 'includes/WebRequest.php', @@ -209,6 +249,8 @@ $wgAutoloadLocalClasses = array( 'WikiError' => 'includes/WikiError.php', 'WikiErrorMsg' => 'includes/WikiError.php', 'WikiExporter' => 'includes/Export.php', + 'WikiMap' => 'includes/WikiMap.php', + 'WikiReference' => 'includes/WikiMap.php', 'WikiXmlError' => 'includes/WikiError.php', 'XCacheBagOStuff' => 'includes/BagOStuff.php', 'XmlDumpWriter' => 'includes/Export.php', @@ -282,6 +324,7 @@ $wgAutoloadLocalClasses = array( 'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php', 'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php', 'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php', + 'ApiQueryTags' => 'includes/api/ApiQueryTags.php', 'ApiQueryUserInfo' => 'includes/api/ApiQueryUserInfo.php', 'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php', 'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php', @@ -290,56 +333,58 @@ $wgAutoloadLocalClasses = array( 'ApiRollback' => 'includes/api/ApiRollback.php', 'ApiUnblock' => 'includes/api/ApiUnblock.php', 'ApiUndelete' => 'includes/api/ApiUndelete.php', + 'ApiUserrights' => 'includes/api/ApiUserrights.php', + 'ApiUpload' => 'includes/api/ApiUpload.php', 'ApiWatch' => 'includes/api/ApiWatch.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', + # includes/json + 'Services_JSON' => 'includes/json/Services_JSON.php', + 'Services_JSON_Error' => 'includes/json/Services_JSON.php', + 'FormatJson' => 'includes/json/FormatJson.php', + # includes/db 'Blob' => 'includes/db/Database.php', 'ChronologyProtector' => 'includes/db/LBFactory.php', - 'Database' => 'includes/db/Database.php', + 'Database' => 'includes/db/DatabaseMysql.php', + 'DatabaseBase' => 'includes/db/Database.php', 'DatabaseMssql' => 'includes/db/DatabaseMssql.php', - 'DatabaseMysql' => 'includes/db/Database.php', + 'DatabaseMysql' => 'includes/db/DatabaseMysql.php', 'DatabaseOracle' => 'includes/db/DatabaseOracle.php', 'DatabasePostgres' => 'includes/db/DatabasePostgres.php', 'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php', + 'DatabaseSqliteStandalone' => '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', + 'IBM_DB2Blob' => 'includes/db/DatabaseIbm_db2.php', 'LBFactory' => 'includes/db/LBFactory.php', 'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php', 'LBFactory_Simple' => 'includes/db/LBFactory.php', + 'LikeMatch' => 'includes/db/Database.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', + 'MySQLMasterPos' => 'includes/db/DatabaseMysql.php', 'ORABlob' => 'includes/db/DatabaseOracle.php', + 'ORAField' => 'includes/db/DatabaseOracle.php', 'ORAResult' => 'includes/db/DatabaseOracle.php', 'PostgresField' => 'includes/db/DatabasePostgres.php', 'ResultWrapper' => 'includes/db/Database.php', 'SQLiteField' => 'includes/db/DatabaseSqlite.php', - 'DatabaseIbm_db2' => 'includes/db/DatabaseIbm_db2.php', 'IBM_DB2Field' => 'includes/db/DatabaseIbm_db2.php', - 'IBM_DB2SearchResultSet' => 'includes/SearchIBM_DB2.php', - 'SearchIBM_DB2' => 'includes/SearchIBM_DB2.php', # includes/diff - 'AncestorComparator' => 'includes/diff/HTMLDiff.php', - 'AnchorToString' => 'includes/diff/HTMLDiff.php', 'ArrayDiffFormatter' => 'includes/diff/DifferenceEngine.php', - 'BodyNode' => 'includes/diff/Nodes.php', - 'ChangeText' => 'includes/diff/HTMLDiff.php', - 'ChangeTextGenerator' => 'includes/diff/HTMLDiff.php', - 'DelegatingContentHandler' => 'includes/diff/HTMLDiff.php', '_DiffEngine' => 'includes/diff/DifferenceEngine.php', - 'DifferenceEngine' => 'includes/diff/DifferenceEngine.php', + 'DifferenceEngine' => 'includes/diff/DifferenceInterface.php', 'DiffFormatter' => 'includes/diff/DifferenceEngine.php', 'Diff' => 'includes/diff/DifferenceEngine.php', '_DiffOp_Add' => 'includes/diff/DifferenceEngine.php', @@ -347,34 +392,17 @@ $wgAutoloadLocalClasses = array( '_DiffOp_Copy' => 'includes/diff/DifferenceEngine.php', '_DiffOp_Delete' => 'includes/diff/DifferenceEngine.php', '_DiffOp' => 'includes/diff/DifferenceEngine.php', - 'DomTreeBuilder' => 'includes/diff/HTMLDiff.php', - 'DummyNode' => 'includes/diff/Nodes.php', - 'HTMLDiffer' => 'includes/diff/HTMLDiff.php', - 'HTMLOutput' => 'includes/diff/HTMLDiff.php', '_HWLDF_WordAccumulator' => 'includes/diff/DifferenceEngine.php', - 'ImageNode' => 'includes/diff/Nodes.php', - 'LastCommonParentResult' => 'includes/diff/HTMLDiff.php', 'MappedDiff' => 'includes/diff/DifferenceEngine.php', - 'Modification' => 'includes/diff/HTMLDiff.php', - 'NoContentTagToString' => 'includes/diff/HTMLDiff.php', - 'Node' => 'includes/diff/Nodes.php', 'RangeDifference' => 'includes/diff/Diff.php', 'TableDiffFormatter' => 'includes/diff/DifferenceEngine.php', - 'TagNode' => 'includes/diff/Nodes.php', - 'TagToString' => 'includes/diff/HTMLDiff.php', - 'TagToStringFactory' => 'includes/diff/HTMLDiff.php', - 'TextNode' => 'includes/diff/Nodes.php', - 'TextNodeDiffer' => 'includes/diff/HTMLDiff.php', - 'TextOnlyComparator' => 'includes/diff/HTMLDiff.php', 'UnifiedDiffFormatter' => 'includes/diff/DifferenceEngine.php', - 'WhiteSpaceNode' => 'includes/diff/Nodes.php', 'WikiDiff3' => 'includes/diff/Diff.php', 'WordLevelDiff' => 'includes/diff/DifferenceEngine.php', # includes/filerepo 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php', 'File' => 'includes/filerepo/File.php', - 'FileCache' => 'includes/filerepo/FileCache.php', 'FileRepo' => 'includes/filerepo/FileRepo.php', 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php', 'ForeignAPIFile' => 'includes/filerepo/ForeignAPIFile.php', @@ -408,10 +436,13 @@ $wgAutoloadLocalClasses = array( # includes/parser 'CoreLinkFunctions' => 'includes/parser/CoreLinkFunctions.php', 'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php', + 'CoreTagHooks' => 'includes/parser/CoreTagHooks.php', 'DateFormatter' => 'includes/parser/DateFormatter.php', 'LinkHolderArray' => 'includes/parser/LinkHolderArray.php', - 'LinkMarkerReplacer' => 'includes/parser/LinkMarkerReplacer.php', + 'LinkMarkerReplacer' => 'includes/parser/Parser_LinkHooks.php', 'OnlyIncludeReplacer' => 'includes/parser/Parser.php', + 'PPCustomFrame_Hash' => 'includes/parser/Preprocessor_Hash.php', + 'PPCustomFrame_DOM' => 'includes/parser/Preprocessor_DOM.php', 'PPDAccum_Hash' => 'includes/parser/Preprocessor_Hash.php', 'PPDPart' => 'includes/parser/Preprocessor_DOM.php', 'PPDPart_Hash' => 'includes/parser/Preprocessor_Hash.php', @@ -442,7 +473,31 @@ $wgAutoloadLocalClasses = array( 'StripState' => 'includes/parser/Parser.php', 'MWTidy' => 'includes/parser/Tidy.php', + # includes/search + 'MySQLSearchResultSet' => 'includes/search/SearchMySQL.php', + 'PostgresSearchResult' => 'includes/search/SearchPostgres.php', + 'PostgresSearchResultSet' => 'includes/search/SearchPostgres.php', + 'SearchEngineDummy' => 'includes/search/SearchEngine.php', + 'SearchEngine' => 'includes/search/SearchEngine.php', + 'SearchHighlighter' => 'includes/search/SearchEngine.php', + 'SearchIBM_DB2' => 'includes/search/SearchIBM_DB2.php', + 'SearchMySQL4' => 'includes/search/SearchMySQL4.php', + 'SearchMySQL' => 'includes/search/SearchMySQL.php', + 'SearchOracle' => 'includes/search/SearchOracle.php', + 'SearchPostgres' => 'includes/search/SearchPostgres.php', + 'SearchResult' => 'includes/search/SearchEngine.php', + 'SearchResultSet' => 'includes/search/SearchEngine.php', + 'SearchResultTooMany' => 'includes/search/SearchEngine.php', + 'SearchSqlite' => 'includes/search/SearchSqlite.php', + 'SearchUpdate' => 'includes/search/SearchUpdate.php', + 'SearchUpdateMyISAM' => 'includes/search/SearchUpdate.php', + 'SqliteSearchResultSet' => 'includes/search/SearchSqlite.php', + 'SqlSearchResultSet' => 'includes/search/SearchEngine.php', + # includes/specials + 'SpecialAllmessages' => 'includes/specials/SpecialAllmessages.php', + 'ActiveUsersPager' => 'includes/specials/SpecialActiveusers.php', + 'AllmessagesTablePager' => 'includes/specials/SpecialAllmessages.php', 'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php', 'BrokenRedirectsPage' => 'includes/specials/SpecialBrokenRedirects.php', 'ContribsPager' => 'includes/specials/SpecialContributions.php', @@ -456,6 +511,7 @@ $wgAutoloadLocalClasses = array( 'EmailConfirmation' => 'includes/specials/SpecialConfirmemail.php', 'EmailInvalidation' => 'includes/specials/SpecialConfirmemail.php', 'EmailUserForm' => 'includes/specials/SpecialEmailuser.php', + 'FakeResultWrapper' => 'includes/specials/SpecialAllmessages.php', 'FewestrevisionsPage' => 'includes/specials/SpecialFewestrevisions.php', 'FileDuplicateSearchPage' => 'includes/specials/SpecialFileDuplicateSearch.php', 'IPBlockForm' => 'includes/specials/SpecialBlockip.php', @@ -482,26 +538,41 @@ $wgAutoloadLocalClasses = array( 'PageArchive' => 'includes/specials/SpecialUndelete.php', 'SpecialResetpass' => 'includes/specials/SpecialResetpass.php', 'PopularPagesPage' => 'includes/specials/SpecialPopularpages.php', - 'PreferencesForm' => 'includes/specials/SpecialPreferences.php', + 'PreferencesForm' => 'includes/Preferences.php', 'RandomPage' => 'includes/specials/SpecialRandompage.php', 'SpecialRevisionDelete' => 'includes/specials/SpecialRevisiondelete.php', 'RevisionDeleter' => 'includes/specials/SpecialRevisiondelete.php', + 'RevDel_RevisionList' => 'includes/specials/SpecialRevisiondelete.php', + 'RevDel_RevisionItem' => 'includes/specials/SpecialRevisiondelete.php', + 'RevDel_ArchiveList' => 'includes/specials/SpecialRevisiondelete.php', + 'RevDel_ArchiveItem' => 'includes/specials/SpecialRevisiondelete.php', + 'RevDel_FileList' => 'includes/specials/SpecialRevisiondelete.php', + 'RevDel_FileItem' => 'includes/specials/SpecialRevisiondelete.php', + 'RevDel_ArchivedFileList' => 'includes/specials/SpecialRevisiondelete.php', + 'RevDel_ArchivedFileItem' => 'includes/specials/SpecialRevisiondelete.php', + 'RevDel_LogList' => 'includes/specials/SpecialRevisiondelete.php', + 'RevDel_LogItem' => 'includes/specials/SpecialRevisiondelete.php', 'ShortPagesPage' => 'includes/specials/SpecialShortpages.php', + 'SpecialActiveUsers' => 'includes/specials/SpecialActiveusers.php', 'SpecialAllpages' => 'includes/specials/SpecialAllpages.php', + 'SpecialBlankpage' => 'includes/specials/SpecialBlankpage.php', 'SpecialBookSources' => 'includes/specials/SpecialBooksources.php', 'SpecialExport' => 'includes/specials/SpecialExport.php', 'SpecialImport' => 'includes/specials/SpecialImport.php', 'SpecialListGroupRights' => 'includes/specials/SpecialListgrouprights.php', 'SpecialMostlinkedtemplates' => 'includes/specials/SpecialMostlinkedtemplates.php', + 'SpecialPreferences' => 'includes/specials/SpecialPreferences.php', 'SpecialPrefixindex' => 'includes/specials/SpecialPrefixindex.php', 'SpecialRandomredirect' => 'includes/specials/SpecialRandomredirect.php', - 'SpecialRecentchanges' => 'includes/specials/SpecialRecentchanges.php', + 'SpecialRecentChanges' => 'includes/specials/SpecialRecentchanges.php', 'SpecialRecentchangeslinked' => 'includes/specials/SpecialRecentchangeslinked.php', 'SpecialSearch' => 'includes/specials/SpecialSearch.php', - 'SpecialSearchOld' => 'includes/specials/SpecialSearch.php', 'SpecialStatistics' => 'includes/specials/SpecialStatistics.php', 'SpecialTags' => 'includes/specials/SpecialTags.php', + 'SpecialUpload' => 'includes/specials/SpecialUpload.php', 'SpecialVersion' => 'includes/specials/SpecialVersion.php', + 'SpecialWhatlinkshere' => 'includes/specials/SpecialWhatlinkshere.php', + 'SpecialWhatLinksHere' => 'includes/specials/SpecialWhatlinkshere.php', 'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php', 'UncategorizedPagesPage' => 'includes/specials/SpecialUncategorizedpages.php', 'UncategorizedTemplatesPage' => 'includes/specials/SpecialUncategorizedtemplates.php', @@ -511,7 +582,7 @@ $wgAutoloadLocalClasses = array( 'UnusedtemplatesPage' => 'includes/specials/SpecialUnusedtemplates.php', 'UnwatchedpagesPage' => 'includes/specials/SpecialUnwatchedpages.php', 'UploadForm' => 'includes/specials/SpecialUpload.php', - 'UploadFormMogile' => 'includes/specials/SpecialUploadMogile.php', + 'UploadSourceField' => 'includes/specials/SpecialUpload.php', 'UserrightsPage' => 'includes/specials/SpecialUserrights.php', 'UsersPager' => 'includes/specials/SpecialListusers.php', 'WantedCategoriesPage' => 'includes/specials/SpecialWantedcategories.php', @@ -530,13 +601,14 @@ $wgAutoloadLocalClasses = array( # languages 'Language' => 'languages/Language.php', 'FakeConverter' => 'languages/Language.php', + 'LanguageConverter' => 'languages/LanguageConverter.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', + 'SevenZipStream' => 'maintenance/7zip.inc', ); @@ -544,7 +616,7 @@ class AutoLoader { /** * autoload - take a class name and attempt to load it * - * @param string $className Name of class we're looking for. + * @param $className String: 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. @@ -567,7 +639,7 @@ class AutoLoader { } } if ( !$filename ) { - if( function_exists( 'wfDebug' ) ) + if( function_exists( 'wfDebug' ) ) wfDebug( "Class {$className} not found; skipped loading\n" ); # Give up return false; @@ -592,6 +664,17 @@ class AutoLoader { } } } + + /** + * Force a class to be run through the autoloader, helpful for things like + * Sanitizer that have define()s outside of their class definition. Of course + * this wouldn't be necessary if everything in MediaWiki was class-based. Sigh. + * + * @return Boolean Return the results of class_exists() so we know if we were successful + */ + static function loadClass( $class ) { + return class_exists( $class ); + } } function wfLoadAllExtensions() { @@ -604,4 +687,6 @@ if ( function_exists( 'spl_autoload_register' ) ) { function __autoload( $class ) { AutoLoader::autoload( $class ); } + + ini_set( 'unserialize_callback_func', '__autoload' ); } diff --git a/includes/Autopromote.php b/includes/Autopromote.php index c8a4c03b..c0adff43 100644 --- a/includes/Autopromote.php +++ b/includes/Autopromote.php @@ -1,5 +1,4 @@ <?php - /** * This class checks if user can get extra rights * because of conditions specified in $wgAutopromote @@ -18,9 +17,9 @@ class Autopromote { if( self::recCheckCondition( $cond, $user ) ) $promote[] = $group; } - + wfRunHooks( 'GetAutoPromoteGroups', array( $user, &$promote ) ); - + return $promote; } @@ -116,6 +115,8 @@ class Autopromote { return $cond[1] == wfGetIP(); case APCOND_IPINRANGE: return IP::isInRange( wfGetIP(), $cond[1] ); + case APCOND_BLOCKED: + return $user->isBlocked(); default: $result = null; wfRunHooks( 'AutopromoteCondition', array( $cond[0], array_slice( $cond, 1 ), $user, &$result ) ); diff --git a/includes/BacklinkCache.php b/includes/BacklinkCache.php index a7bcd858..53f92dd9 100644 --- a/includes/BacklinkCache.php +++ b/includes/BacklinkCache.php @@ -1,10 +1,9 @@ <?php - /** * Class for fetching backlink lists, approximate backlink counts and partitions. * Instances of this class should typically be fetched with $title->getBacklinkCache(). * - * Ideally you should only get your backlinks from here when you think there is some + * Ideally you should only get your backlinks from here when you think there is some * advantage in caching them. Otherwise it's just a waste of memory. */ class BacklinkCache { @@ -47,44 +46,53 @@ class BacklinkCache { /** * Get the backlinks for a given table. Cached in process memory only. - * @param string $table + * @param $table String + * @param $startId Integer or false + * @param $endId Integer or false * @return TitleArray */ public function getLinks( $table, $startId = false, $endId = false ) { wfProfileIn( __METHOD__ ); + $fromField = $this->getPrefix( $table ) . '_from'; + if ( $startId || $endId ) { // Partial range, not cached - wfDebug( __METHOD__.": from DB (uncacheable range)\n" ); + wfDebug( __METHOD__ . ": from DB (uncacheable range)\n" ); $conds = $this->getConditions( $table ); // Use the from field in the condition rather than the joined page_id, // because databases are stupid and don't necessarily propagate indexes. - $fromField = $this->getPrefix( $table ) . '_from'; if ( $startId ) { $conds[] = "$fromField >= " . intval( $startId ); } if ( $endId ) { $conds[] = "$fromField <= " . intval( $endId ); } - $res = $this->getDB()->select( + $res = $this->getDB()->select( array( $table, 'page' ), - array( 'page_namespace', 'page_title', 'page_id'), + array( 'page_namespace', 'page_title', 'page_id' ), $conds, __METHOD__, - array('STRAIGHT_JOIN') ); + array( + 'STRAIGHT_JOIN', + 'ORDER BY' => $fromField + ) ); $ta = TitleArray::newFromResult( $res ); wfProfileOut( __METHOD__ ); return $ta; } if ( !isset( $this->fullResultCache[$table] ) ) { - wfDebug( __METHOD__.": from DB\n" ); - $res = $this->getDB()->select( + wfDebug( __METHOD__ . ": from DB\n" ); + $res = $this->getDB()->select( array( $table, 'page' ), array( 'page_namespace', 'page_title', 'page_id' ), $this->getConditions( $table ), __METHOD__, - array('STRAIGHT_JOIN') ); + array( + 'STRAIGHT_JOIN', + 'ORDER BY' => $fromField, + ) ); $this->fullResultCache[$table] = $res; } $ta = TitleArray::newFromResult( $this->fullResultCache[$table] ); @@ -103,6 +111,7 @@ class BacklinkCache { 'templatelinks' => 'tl', 'redirect' => 'rd', ); + if ( isset( $prefixes[$table] ) ) { return $prefixes[$table]; } else { @@ -115,6 +124,7 @@ class BacklinkCache { */ protected function getConditions( $table ) { $prefix = $this->getPrefix( $table ); + switch ( $table ) { case 'pagelinks': case 'templatelinks': @@ -126,13 +136,13 @@ class BacklinkCache { ); break; case 'imagelinks': - $conds = array( + $conds = array( 'il_to' => $this->title->getDBkey(), 'page_id=il_from' ); break; case 'categorylinks': - $conds = array( + $conds = array( 'cl_to' => $this->title->getDBkey(), 'page_id=cl_from', ); @@ -150,10 +160,12 @@ class BacklinkCache { if ( isset( $this->fullResultCache[$table] ) ) { return $this->fullResultCache[$table]->numRows(); } + if ( isset( $this->partitionCache[$table] ) ) { $entry = reset( $this->partitionCache[$table] ); return $entry['numRows']; } + $titleArray = $this->getLinks( $table ); return $titleArray->count(); } @@ -163,33 +175,40 @@ class BacklinkCache { * Returns an array giving the start and end of each range. The first batch has * a start of false, and the last batch has an end of false. * - * @param string $table The links table name - * @param integer $batchSize - * @return array + * @param $table String: the links table name + * @param $batchSize Integer + * @return Array */ public function partition( $table, $batchSize ) { // Try cache if ( isset( $this->partitionCache[$table][$batchSize] ) ) { - wfDebug( __METHOD__.": got from partition cache\n" ); + wfDebug( __METHOD__ . ": got from partition cache\n" ); return $this->partitionCache[$table][$batchSize]['batches']; } + $this->partitionCache[$table][$batchSize] = false; $cacheEntry =& $this->partitionCache[$table][$batchSize]; // Try full result cache if ( isset( $this->fullResultCache[$table] ) ) { $cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize ); - wfDebug( __METHOD__.": got from full result cache\n" ); + wfDebug( __METHOD__ . ": got from full result cache\n" ); return $cacheEntry['batches']; } + // Try memcached global $wgMemc; - $memcKey = wfMemcKey( 'backlinks', md5( $this->title->getPrefixedDBkey() ), - $table, $batchSize ); + $memcKey = wfMemcKey( + 'backlinks', + md5( $this->title->getPrefixedDBkey() ), + $table, + $batchSize + ); $memcValue = $wgMemc->get( $memcKey ); + if ( is_array( $memcValue ) ) { $cacheEntry = $memcValue; - wfDebug( __METHOD__.": got from memcached $memcKey\n" ); + wfDebug( __METHOD__ . ": got from memcached $memcKey\n" ); return $cacheEntry['batches']; } // Fetch from database @@ -197,17 +216,18 @@ class BacklinkCache { $cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize ); // Save to memcached $wgMemc->set( $memcKey, $cacheEntry, self::CACHE_EXPIRY ); - wfDebug( __METHOD__.": got from database\n" ); + wfDebug( __METHOD__ . ": got from database\n" ); return $cacheEntry['batches']; } - /** + /** * Partition a DB result with backlinks in it into batches */ protected function partitionResult( $res, $batchSize ) { $batches = array(); $numRows = $res->numRows(); $numBatches = ceil( $numRows / $batchSize ); + for ( $i = 0; $i < $numBatches; $i++ ) { if ( $i == 0 ) { $start = false; @@ -217,6 +237,7 @@ class BacklinkCache { $row = $res->fetchObject(); $start = $row->page_id; } + if ( $i == $numBatches - 1 ) { $end = false; } else { @@ -225,6 +246,12 @@ class BacklinkCache { $row = $res->fetchObject(); $end = $row->page_id - 1; } + + # Sanity check order + if ( $start && $end && $start > $end ) { + throw new MWException( __METHOD__ . ': Internal error: query result out of order' ); + } + $batches[] = array( $start, $end ); } return array( 'numRows' => $numRows, 'batches' => $batches ); diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php index ffa8a0bb..ac0263d8 100644 --- a/includes/BagOStuff.php +++ b/includes/BagOStuff.php @@ -32,134 +32,129 @@ * backends for local hash array and SQL table included: * <code> * $bag = new HashBagOStuff(); - * $bag = new MediaWikiBagOStuff($tablename); # connect to db first + * $bag = new SqlBagOStuff(); # connect to db first * </code> * * @ingroup Cache */ -class BagOStuff { - var $debugmode; +abstract class BagOStuff { + var $debugMode = false; - function __construct() { - $this->set_debug( false ); - } - - function set_debug($bool) { - $this->debugmode = $bool; + public function set_debug( $bool ) { + $this->debugMode = $bool; } /* *** THE GUTS OF THE OPERATION *** */ /* Override these with functional things in subclasses */ - function get($key) { - /* stub */ - return false; - } + /** + * Get an item with the given key. Returns false if it does not exist. + * @param $key string + */ + abstract public function get( $key ); - function set($key, $value, $exptime=0) { - /* stub */ - return false; - } + /** + * Set an item. + * @param $key string + * @param $value mixed + * @param $exptime int Either an interval in seconds or a unix timestamp for expiry + */ + abstract public function set( $key, $value, $exptime = 0 ); - function delete($key, $time=0) { - /* stub */ - return false; - } + /* + * Delete an item. + * @param $key string + * @param $time int Amount of time to delay the operation (mostly memcached-specific) + */ + abstract public function delete( $key, $time = 0 ); - function lock($key, $timeout = 0) { + public function lock( $key, $timeout = 0 ) { /* stub */ return true; } - function unlock($key) { + public function unlock( $key ) { /* stub */ return true; } - function keys() { + public function keys() { /* stub */ return array(); } /* *** Emulated functions *** */ /* Better performance can likely be got with custom written versions */ - function get_multi($keys) { + public function get_multi( $keys ) { $out = array(); - foreach($keys as $key) - $out[$key] = $this->get($key); + + foreach ( $keys as $key ) { + $out[$key] = $this->get( $key ); + } + return $out; } - function set_multi($hash, $exptime=0) { - foreach($hash as $key => $value) - $this->set($key, $value, $exptime); + public function set_multi( $hash, $exptime = 0 ) { + foreach ( $hash as $key => $value ) { + $this->set( $key, $value, $exptime ); + } } - function add($key, $value, $exptime=0) { - if( $this->get($key) == false ) { - $this->set($key, $value, $exptime); + public function add( $key, $value, $exptime = 0 ) { + if ( $this->get( $key ) == false ) { + $this->set( $key, $value, $exptime ); return true; } } - function add_multi($hash, $exptime=0) { - foreach($hash as $key => $value) - $this->add($key, $value, $exptime); + public function add_multi( $hash, $exptime = 0 ) { + foreach ( $hash as $key => $value ) { + $this->add( $key, $value, $exptime ); + } } - function delete_multi($keys, $time=0) { - foreach($keys as $key) - $this->delete($key, $time); + public function delete_multi( $keys, $time = 0 ) { + foreach ( $keys as $key ) { + $this->delete( $key, $time ); + } } - function replace($key, $value, $exptime=0) { - if( $this->get($key) !== false ) - $this->set($key, $value, $exptime); + public function replace( $key, $value, $exptime = 0 ) { + if ( $this->get( $key ) !== false ) { + $this->set( $key, $value, $exptime ); + } } - function incr($key, $value=1) { - if ( !$this->lock($key) ) { + public function incr( $key, $value = 1 ) { + if ( !$this->lock( $key ) ) { return false; } - $value = intval($value); - if($value < 0) $value = 0; + $value = intval( $value ); $n = false; - if( ($n = $this->get($key)) !== false ) { + if ( ( $n = $this->get( $key ) ) !== false ) { $n += $value; - $this->set($key, $n); // exptime? + $this->set( $key, $n ); // exptime? } - $this->unlock($key); + $this->unlock( $key ); return $n; } - function decr($key, $value=1) { - if ( !$this->lock($key) ) { - return false; - } - $value = intval($value); - if($value < 0) $value = 0; - - $m = false; - if( ($n = $this->get($key)) !== false ) { - $m = $n - $value; - if($m < 0) $m = 0; - $this->set($key, $m); // exptime? - } - $this->unlock($key); - return $m; + public function decr( $key, $value = 1 ) { + return $this->incr( $key, - $value ); } - function _debug($text) { - if($this->debugmode) - wfDebug("BagOStuff debug: $text\n"); + public function debug( $text ) { + if ( $this->debugMode ) + wfDebug( "BagOStuff debug: $text\n" ); } /** * Convert an optionally relative time to an absolute time */ - static function convertExpiry( $exptime ) { - if(($exptime != 0) && ($exptime < 3600*24*30)) { + protected function convertExpiry( $exptime ) { + if ( ( $exptime != 0 ) && ( $exptime < 3600 * 24 * 30 ) ) { return time() + $exptime; } else { return $exptime; @@ -167,7 +162,6 @@ class BagOStuff { } } - /** * Functional versions! * This is a test of the interface, mainly. It stores things in an associative @@ -182,30 +176,34 @@ class HashBagOStuff extends BagOStuff { $this->bag = array(); } - function _expire($key) { + protected function expire( $key ) { $et = $this->bag[$key][1]; - if(($et == 0) || ($et > time())) + if ( ( $et == 0 ) || ( $et > time() ) ) { return false; - $this->delete($key); + } + $this->delete( $key ); return true; } - function get($key) { - if(!$this->bag[$key]) + function get( $key ) { + if ( !isset( $this->bag[$key] ) ) { return false; - if($this->_expire($key)) + } + if ( $this->expire( $key ) ) { return false; + } return $this->bag[$key][0]; } - function set($key,$value,$exptime=0) { - $this->bag[$key] = array( $value, BagOStuff::convertExpiry( $exptime ) ); + function set( $key, $value, $exptime = 0 ) { + $this->bag[$key] = array( $value, $this->convertExpiry( $exptime ) ); } - function delete($key,$time=0) { - if(!$this->bag[$key]) + function delete( $key, $time = 0 ) { + if ( !isset( $this->bag[$key] ) ) { return false; - unset($this->bag[$key]); + } + unset( $this->bag[$key] ); return true; } @@ -215,182 +213,196 @@ class HashBagOStuff extends BagOStuff { } /** - * Generic class to store objects in a database + * Class to store objects in the database * * @ingroup Cache */ -abstract class SqlBagOStuff extends BagOStuff { - var $table; - var $lastexpireall = 0; +class SqlBagOStuff extends BagOStuff { + var $lb, $db; + var $lastExpireAll = 0; - /** - * Constructor - * - * @param $tablename String: name of the table to use - */ - function __construct($tablename = 'objectcache') { - $this->table = $tablename; + protected function getDB() { + global $wgDBtype; + if ( !isset( $this->db ) ) { + /* We must keep a separate connection to MySQL in order to avoid deadlocks + * However, SQLite has an opposite behaviour. + * @todo Investigate behaviour for other databases + */ + if ( $wgDBtype == 'sqlite' ) { + $this->db = wfGetDB( DB_MASTER ); + } else { + $this->lb = wfGetLBFactory()->newMainLB(); + $this->db = $this->lb->getConnection( DB_MASTER ); + $this->db->clearFlag( DBO_TRX ); + } + } + return $this->db; } - function get($key) { - /* expire old entries if any */ + public function get( $key ) { + # expire old entries if any $this->garbageCollect(); - - $res = $this->_query( - "SELECT value,exptime FROM $0 WHERE keyname='$1'", $key); - if(!$res) { - $this->_debug("get: ** error: " . $this->_dberror($res) . " **"); + $db = $this->getDB(); + $row = $db->selectRow( 'objectcache', array( 'value', 'exptime' ), + array( 'keyname' => $key ), __METHOD__ ); + if ( !$row ) { + $this->debug( 'get: no matching rows' ); return false; } - 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() ) - { - $this->_debug("get: key has expired, deleting"); - $this->delete($key); - return false; + + $this->debug( "get: retrieved data; expiry time is " . $row->exptime ); + if ( $this->isExpired( $row->exptime ) ) { + $this->debug( "get: key has expired, deleting" ); + try { + $db->begin(); + # Put the expiry time in the WHERE condition to avoid deleting a + # newly-inserted value + $db->delete( 'objectcache', + array( + 'keyname' => $key, + 'exptime' => $row->exptime + ), __METHOD__ ); + $db->commit(); + } catch ( DBQueryError $e ) { + $this->handleWriteError( $e ); } - return $this->_unserialize($this->_blobdecode($row->value)); - } else { - $this->_debug('get: no matching rows'); + return false; } - return false; + return $this->unserialize( $db->decodeBlob( $row->value ) ); } - function set($key,$value,$exptime=0) { - if ( $this->_readonly() ) { - return false; - } - $exptime = intval($exptime); - if($exptime < 0) $exptime = 0; - if($exptime == 0) { - $exp = $this->_maxdatetime(); + public function set( $key, $value, $exptime = 0 ) { + $db = $this->getDB(); + $exptime = intval( $exptime ); + if ( $exptime < 0 ) $exptime = 0; + if ( $exptime == 0 ) { + $encExpiry = $this->getMaxDateTime(); } else { - if($exptime < 3.16e8) # ~10 years + if ( $exptime < 3.16e8 ) # ~10 years $exptime += time(); - $exp = $this->_fromunixtime($exptime); + $encExpiry = $db->timestamp( $exptime ); } - $this->_begin(); - $this->_query( - "DELETE FROM $0 WHERE keyname='$1'", $key ); - $this->_doinsert($this->getTableName(), array( + try { + $db->begin(); + $db->delete( 'objectcache', array( 'keyname' => $key ), __METHOD__ ); + $db->insert( 'objectcache', + array( 'keyname' => $key, - 'value' => $this->_blobencode($this->_serialize($value)), - 'exptime' => $exp - )); - $this->_commit(); - return true; /* ? */ + 'value' => $db->encodeBlob( $this->serialize( $value ) ), + 'exptime' => $encExpiry + ), __METHOD__ ); + $db->commit(); + } catch ( DBQueryError $e ) { + $this->handleWriteError( $e ); + return false; + } + return true; } - function delete($key,$time=0) { - if ( $this->_readonly() ) { + public function delete( $key, $time = 0 ) { + $db = $this->getDB(); + try { + $db->begin(); + $db->delete( 'objectcache', array( 'keyname' => $key ), __METHOD__ ); + $db->commit(); + } catch ( DBQueryError $e ) { + $this->handleWriteError( $e ); return false; } - $this->_begin(); - $this->_query( - "DELETE FROM $0 WHERE keyname='$1'", $key ); - $this->_commit(); - return true; /* ? */ + return true; } - function keys() { - $res = $this->_query( "SELECT keyname FROM $0" ); - if(!$res) { - $this->_debug("keys: ** error: " . $this->_dberror($res) . " **"); - return array(); + public function incr( $key, $step = 1 ) { + $db = $this->getDB(); + $step = intval( $step ); + + try { + $db->begin(); + $row = $db->selectRow( 'objectcache', array( 'value', 'exptime' ), + array( 'keyname' => $key ), __METHOD__, array( 'FOR UPDATE' ) ); + if ( $row === false ) { + // Missing + $db->commit(); + return false; + } + $db->delete( 'objectcache', array( 'keyname' => $key ), __METHOD__ ); + if ( $this->isExpired( $row->exptime ) ) { + // Expired, do not reinsert + $db->commit(); + return false; + } + + $oldValue = intval( $this->unserialize( $db->decodeBlob( $row->value ) ) ); + $newValue = $oldValue + $step; + $db->insert( 'objectcache', + array( + 'keyname' => $key, + 'value' => $db->encodeBlob( $this->serialize( $newValue ) ), + 'exptime' => $row->exptime + ), __METHOD__ ); + $db->commit(); + } catch ( DBQueryError $e ) { + $this->handleWriteError( $e ); + return false; } + return $newValue; + } + + public function keys() { + $db = $this->getDB(); + $res = $db->select( 'objectcache', array( 'keyname' ), false, __METHOD__ ); $result = array(); - while( $row = $this->_fetchobject($res) ) { + foreach ( $res as $row ) { $result[] = $row->keyname; } return $result; } - function getTableName() { - return $this->table; + protected function isExpired( $exptime ) { + return $exptime != $this->getMaxDateTime() && wfTimestamp( TS_UNIX, $exptime ) < time(); } - function _query($sql) { - $reps = func_get_args(); - $reps[0] = $this->getTableName(); - // ewwww - for($i=0;$i<count($reps);$i++) { - $sql = str_replace( - '$' . $i, - $i > 0 ? $this->_strencode($reps[$i]) : $reps[$i], - $sql); - } - $res = $this->_doquery($sql); - if($res == false) { - $this->_debug('query failed: ' . $this->_dberror($res)); + protected function getMaxDateTime() { + if ( time() > 0x7fffffff ) { + return $this->getDB()->timestamp( 1 << 62 ); + } else { + return $this->getDB()->timestamp( 0x7fffffff ); } - return $res; - } - - function _strencode($str) { - /* Protect strings in SQL */ - return str_replace( "'", "''", $str ); - } - function _blobencode($str) { - return $str; - } - function _blobdecode($str) { - return $str; - } - - abstract function _doinsert($table, $vals); - abstract function _doquery($sql); - - abstract function _readonly(); - - function _begin() {} - function _commit() {} - - function _freeresult($result) { - /* stub */ - return false; } - function _dberror($result) { - /* stub */ - return 'unknown error'; - } - - abstract function _maxdatetime(); - abstract function _fromunixtime($ts); - - function garbageCollect() { + protected function garbageCollect() { /* Ignore 99% of requests */ if ( !mt_rand( 0, 100 ) ) { - $nowtime = time(); + $now = time(); /* Avoid repeating the delete within a few seconds */ - if ( $nowtime > ($this->lastexpireall + 1) ) { - $this->lastexpireall = $nowtime; - $this->expireall(); + if ( $now > ( $this->lastExpireAll + 1 ) ) { + $this->lastExpireAll = $now; + $this->expireAll(); } } } - function expireall() { - /* Remove any items that have expired */ - if ( $this->_readonly() ) { - return false; + public function expireAll() { + $db = $this->getDB(); + $now = $db->timestamp(); + try { + $db->begin(); + $db->delete( 'objectcache', array( 'exptime < ' . $db->addQuotes( $now ) ), __METHOD__ ); + $db->commit(); + } catch ( DBQueryError $e ) { + $this->handleWriteError( $e ); } - $now = $this->_fromunixtime( time() ); - $this->_begin(); - $this->_query( "DELETE FROM $0 WHERE exptime < '$now'" ); - $this->_commit(); } - function deleteall() { - /* Clear *all* items from cache table */ - if ( $this->_readonly() ) { - return false; + public function deleteAll() { + $db = $this->getDB(); + try { + $db->begin(); + $db->delete( 'objectcache', '*', __METHOD__ ); + $db->commit(); + } catch ( DBQueryError $e ) { + $this->handleWriteError( $e ); } - $this->_begin(); - $this->_query( "DELETE FROM $0" ); - $this->_commit(); } /** @@ -401,9 +413,9 @@ abstract class SqlBagOStuff extends BagOStuff { * @param $data mixed * @return string */ - function _serialize( &$data ) { + protected function serialize( &$data ) { $serial = serialize( $data ); - if( function_exists( 'gzdeflate' ) ) { + if ( function_exists( 'gzdeflate' ) ) { return gzdeflate( $serial ); } else { return $serial; @@ -415,156 +427,39 @@ abstract class SqlBagOStuff extends BagOStuff { * @param $serial string * @return mixed */ - function _unserialize( $serial ) { - if( function_exists( 'gzinflate' ) ) { + protected function unserialize( $serial ) { + if ( function_exists( 'gzinflate' ) ) { $decomp = @gzinflate( $serial ); - if( false !== $decomp ) { + if ( false !== $decomp ) { $serial = $decomp; } } $ret = unserialize( $serial ); return $ret; } -} -/** - * Stores objects in the main database of the wiki - * - * @ingroup Cache - */ -class MediaWikiBagOStuff extends SqlBagOStuff { - var $tableInitialised = false; - var $lb, $db; - - function _getDB(){ - global $wgDBtype; - if ( !isset( $this->db ) ) { - /* We must keep a separate connection to MySQL in order to avoid deadlocks - * However, SQLite has an opposite behaviour. - * @todo Investigate behaviour for other databases - */ - if ( $wgDBtype == 'sqlite' ) { - $this->db = wfGetDB( DB_MASTER ); - } else { - $this->lb = wfGetLBFactory()->newMainLB(); - $this->db = $this->lb->getConnection( DB_MASTER ); - $this->db->clearFlag( DBO_TRX ); - } - } - return $this->db; - } - function _begin() { - $this->_getDB()->begin(); - } - function _commit() { - $this->_getDB()->commit(); - } - function _doquery($sql) { - return $this->_getDB()->query( $sql, __METHOD__ ); - } - function _doinsert($t, $v) { - return $this->_getDB()->insert($t, $v, __METHOD__, array( 'IGNORE' ) ); - } - function _fetchobject($result) { - return $this->_getDB()->fetchObject($result); - } - function _freeresult($result) { - return $this->_getDB()->freeResult($result); - } - function _dberror($result) { - return $this->_getDB()->lastError(); - } - function _maxdatetime() { - if ( time() > 0x7fffffff ) { - return $this->_fromunixtime( 1<<62 ); - } else { - return $this->_fromunixtime( 0x7fffffff ); - } - } - function _fromunixtime($ts) { - return $this->_getDB()->timestamp($ts); - } - /*** - * Note -- this should *not* check wfReadOnly(). - * Read-only mode has been repurposed from the original - * "nothing must write to the database" to "users should not - * be able to edit or alter anything user-visible". - * - * Backend bits like the object cache should continue - * to work in this mode, otherwise things will blow up - * like the message cache failing to save its state, - * causing long delays (bug 11533). + /** + * Handle a DBQueryError which occurred during a write operation. + * Ignore errors which are due to a read-only database, rethrow others. */ - function _readonly(){ - return false; - } - function _strencode($s) { - return $this->_getDB()->strencode($s); - } - function _blobencode($s) { - return $this->_getDB()->encodeBlob($s); - } - function _blobdecode($s) { - return $this->_getDB()->decodeBlob($s); - } - function getTableName() { - if ( !$this->tableInitialised ) { - $dbw = $this->_getDB(); - /* This is actually a hack, we should be able - to use Language classes here... or not */ - if (!$dbw) - throw new MWException("Could not connect to database"); - $this->table = $dbw->tableName( $this->table ); - $this->tableInitialised = true; + protected function handleWriteError( $exception ) { + $db = $this->getDB(); + if ( !$db->wasReadOnlyError() ) { + throw $exception; } - return $this->table; + try { + $db->rollback(); + } catch ( DBQueryError $e ) { + } + wfDebug( __METHOD__ . ": ignoring query error\n" ); + $db->ignoreErrors( false ); } } /** - * This is a wrapper for Turck MMCache's shared memory functions. - * - * You can store objects with mmcache_put() and mmcache_get(), but Turck seems - * to use a weird custom serializer that randomly segfaults. So we wrap calls - * with serialize()/unserialize(). - * - * The thing I noticed about the Turck serialized data was that unlike ordinary - * serialize(), it contained the names of methods, and judging by the amount of - * binary data, perhaps even the bytecode of the methods themselves. It may be - * that Turck's serializer is faster, so a possible future extension would be - * to use it for arrays but not for objects. - * - * @ingroup Cache + * Backwards compatibility alias */ -class TurckBagOStuff extends BagOStuff { - function get($key) { - $val = mmcache_get( $key ); - if ( is_string( $val ) ) { - $val = unserialize( $val ); - } - return $val; - } - - function set($key, $value, $exptime=0) { - mmcache_put( $key, serialize( $value ), $exptime ); - return true; - } - - function delete($key, $time=0) { - mmcache_rm( $key ); - return true; - } - - function lock($key, $waitTimeout = 0 ) { - mmcache_lock( $key ); - return true; - } - - function unlock($key) { - mmcache_unlock( $key ); - return true; - } -} +class MediaWikiBagOStuff extends SqlBagOStuff { } /** * This is a wrapper for APC's shared memory functions @@ -572,36 +467,45 @@ class TurckBagOStuff extends BagOStuff { * @ingroup Cache */ class APCBagOStuff extends BagOStuff { - function get($key) { - $val = apc_fetch($key); + public function get( $key ) { + $val = apc_fetch( $key ); if ( is_string( $val ) ) { $val = unserialize( $val ); } return $val; } - function set($key, $value, $exptime=0) { - apc_store($key, serialize($value), $exptime); + public function set( $key, $value, $exptime = 0 ) { + apc_store( $key, serialize( $value ), $exptime ); return true; } - function delete($key, $time=0) { - apc_delete($key); + public function delete( $key, $time = 0 ) { + apc_delete( $key ); return true; } -} + public function keys() { + $info = apc_cache_info( 'user' ); + $list = $info['cache_list']; + $keys = array(); + foreach ( $list as $entry ) { + $keys[] = $entry['info']; + } + return $keys; + } +} /** * This is a wrapper for eAccelerator's shared memory functions. * - * This is basically identical to the Turck MMCache version, + * This is basically identical to the deceased Turck MMCache version, * mostly because eAccelerator is based on Turck MMCache. * * @ingroup Cache */ class eAccelBagOStuff extends BagOStuff { - function get($key) { + public function get( $key ) { $val = eaccelerator_get( $key ); if ( is_string( $val ) ) { $val = unserialize( $val ); @@ -609,22 +513,22 @@ class eAccelBagOStuff extends BagOStuff { return $val; } - function set($key, $value, $exptime=0) { + public function set( $key, $value, $exptime = 0 ) { eaccelerator_put( $key, serialize( $value ), $exptime ); return true; } - function delete($key, $time=0) { + public function delete( $key, $time = 0 ) { eaccelerator_rm( $key ); return true; } - function lock($key, $waitTimeout = 0 ) { + public function lock( $key, $waitTimeout = 0 ) { eaccelerator_lock( $key ); return true; } - function unlock($key) { + public function unlock( $key ) { eaccelerator_unlock( $key ); return true; } @@ -646,7 +550,7 @@ class XCacheBagOStuff extends BagOStuff { */ public function get( $key ) { $val = xcache_get( $key ); - if( is_string( $val ) ) + if ( is_string( $val ) ) $val = unserialize( $val ); return $val; } @@ -675,25 +579,29 @@ class XCacheBagOStuff extends BagOStuff { xcache_unset( $key ); return true; } - } /** - * @todo document + * Cache that uses DBA as a backend. + * Slow due to the need to constantly open and close the file to avoid holding + * writer locks. Intended for development use only, as a memcached workalike + * for systems that don't have it. + * * @ingroup Cache */ class DBABagOStuff extends BagOStuff { var $mHandler, $mFile, $mReader, $mWriter, $mDisabled; - function __construct( $handler = 'db3', $dir = false ) { + public function __construct( $dir = false ) { + global $wgDBAhandler; if ( $dir === false ) { global $wgTmpDirectory; $dir = $wgTmpDirectory; } $this->mFile = "$dir/mw-cache-" . wfWikiID(); $this->mFile .= '.db'; - wfDebug( __CLASS__.": using cache file {$this->mFile}\n" ); - $this->mHandler = $handler; + wfDebug( __CLASS__ . ": using cache file {$this->mFile}\n" ); + $this->mHandler = $wgDBAhandler; } /** @@ -701,7 +609,7 @@ class DBABagOStuff extends BagOStuff { */ function encode( $value, $expiry ) { # Convert to absolute time - $expiry = BagOStuff::convertExpiry( $expiry ); + $expiry = $this->convertExpiry( $expiry ); return sprintf( '%010u', intval( $expiry ) ) . ' ' . serialize( $value ); } @@ -715,7 +623,7 @@ class DBABagOStuff extends BagOStuff { return array( unserialize( substr( $blob, 11 ) ), intval( substr( $blob, 0, 10 ) ) - ); + ); } } @@ -741,7 +649,7 @@ class DBABagOStuff extends BagOStuff { function get( $key ) { wfProfileIn( __METHOD__ ); - wfDebug( __METHOD__."($key)\n" ); + wfDebug( __METHOD__ . "($key)\n" ); $handle = $this->getReader(); if ( !$handle ) { return null; @@ -756,16 +664,16 @@ class DBABagOStuff extends BagOStuff { $handle = $this->getWriter(); dba_delete( $key, $handle ); dba_close( $handle ); - wfDebug( __METHOD__.": $key expired\n" ); + wfDebug( __METHOD__ . ": $key expired\n" ); $val = null; } wfProfileOut( __METHOD__ ); return $val; } - function set( $key, $value, $exptime=0 ) { + function set( $key, $value, $exptime = 0 ) { wfProfileIn( __METHOD__ ); - wfDebug( __METHOD__."($key)\n" ); + wfDebug( __METHOD__ . "($key)\n" ); $blob = $this->encode( $value, $exptime ); $handle = $this->getWriter(); if ( !$handle ) { @@ -779,7 +687,7 @@ class DBABagOStuff extends BagOStuff { function delete( $key, $time = 0 ) { wfProfileIn( __METHOD__ ); - wfDebug( __METHOD__."($key)\n" ); + wfDebug( __METHOD__ . "($key)\n" ); $handle = $this->getWriter(); if ( !$handle ) { return false; @@ -817,11 +725,11 @@ class DBABagOStuff extends BagOStuff { function keys() { $reader = $this->getReader(); $k1 = dba_firstkey( $reader ); - if( !$k1 ) { + if ( !$k1 ) { return array(); } $result[] = $k1; - while( $key = dba_nextkey( $reader ) ) { + while ( $key = dba_nextkey( $reader ) ) { $result[] = $key; } return $result; diff --git a/includes/Block.php b/includes/Block.php index a44941f1..187ff2db 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -34,7 +34,7 @@ class Block { $this->mUser = $user; $this->mBy = $by; $this->mReason = $reason; - $this->mTimestamp = wfTimestamp(TS_MW,$timestamp); + $this->mTimestamp = wfTimestamp( TS_MW, $timestamp ); $this->mAuto = $auto; $this->mAnonOnly = $anonOnly; $this->mCreateAccount = $createAccount; @@ -54,7 +54,7 @@ class Block { * Load a block from the database, using either the IP address or * user ID. Tries the user ID first, and if that doesn't work, tries * the address. - * + * * @param $address String: IP address of user/anon * @param $user Integer: user id of user * @param $killExpired Boolean: delete expired blocks on load @@ -87,14 +87,14 @@ class Block { return null; } } - + /** * Check if two blocks are effectively equal * * @return Boolean */ public function equals( Block $block ) { - return ( + return ( $this->mAddress == $block->mAddress && $this->mUser == $block->mUser && $this->mAuto == $block->mAuto @@ -130,9 +130,10 @@ class Block { */ protected function &getDBOptions( &$options ) { global $wgAntiLockFlags; + if ( $this->mForUpdate || $this->mFromMaster ) { $db = wfGetDB( DB_MASTER ); - if ( !$this->mForUpdate || ($wgAntiLockFlags & ALF_NO_BLOCK_LOCK) ) { + if ( !$this->mForUpdate || ( $wgAntiLockFlags & ALF_NO_BLOCK_LOCK ) ) { $options = array(); } else { $options = array( 'FOR UPDATE' ); @@ -180,12 +181,13 @@ class Block { if ( $address ) { $conds = array( 'ipb_address' => $address, 'ipb_auto' => 0 ); $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); + if ( $this->loadFromResult( $res, $killExpired ) ) { if ( $user && $this->mAnonOnly ) { # Block is marked anon-only # Whitelist this IP address against autoblocks and range blocks # (but not account creation blocks -- bug 13611) - if( !$this->mCreateAccount ) { + if ( !$this->mCreateAccount ) { $this->clear(); } return false; @@ -199,7 +201,7 @@ class Block { if ( $this->loadRange( $address, $killExpired, $user ) ) { if ( $user && $this->mAnonOnly ) { # Respect account creation blocks on logged-in users -- bug 13611 - if( !$this->mCreateAccount ) { + if ( !$this->mCreateAccount ) { $this->clear(); } return false; @@ -211,10 +213,13 @@ class Block { # Try autoblock if ( $address ) { $conds = array( 'ipb_address' => $address, 'ipb_auto' => 1 ); + if ( $user ) { $conds['ipb_anon_only'] = 0; } + $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); + if ( $this->loadFromResult( $res, $killExpired ) ) { return true; } @@ -234,6 +239,7 @@ class Block { */ protected function loadFromResult( ResultWrapper $res, $killExpired = true ) { $ret = false; + if ( 0 != $res->numRows() ) { # Get first block $row = $res->fetchObject(); @@ -274,6 +280,7 @@ class Block { */ public function loadRange( $address, $killExpired = true, $user = 0 ) { $iaddr = IP::toHex( $address ); + if ( $iaddr === false ) { # Invalid address return false; @@ -286,7 +293,7 @@ class Block { $options = array(); $db =& $this->getDBOptions( $options ); $conds = array( - "ipb_range_start LIKE '$range%'", + 'ipb_range_start' . $db->buildLike( $range, $db->anyString() ), "ipb_range_start <= '$iaddr'", "ipb_range_end >= '$iaddr'" ); @@ -309,7 +316,7 @@ class Block { public function initFromRow( $row ) { $this->mAddress = $row->ipb_address; $this->mReason = $row->ipb_reason; - $this->mTimestamp = wfTimestamp(TS_MW,$row->ipb_timestamp); + $this->mTimestamp = wfTimestamp( TS_MW, $row->ipb_timestamp ); $this->mUser = $row->ipb_user; $this->mBy = $row->ipb_by; $this->mAuto = $row->ipb_auto; @@ -321,17 +328,19 @@ class Block { $this->mHideName = $row->ipb_deleted; $this->mId = $row->ipb_id; $this->mExpiry = self::decodeExpiry( $row->ipb_expiry ); + if ( isset( $row->user_name ) ) { $this->mByName = $row->user_name; } else { $this->mByName = $row->ipb_by_text; } + $this->mRangeStart = $row->ipb_range_start; $this->mRangeEnd = $row->ipb_range_end; } /** - * Once $mAddress has been set, get the range they came from. + * Once $mAddress has been set, get the range they came from. * Wrapper for IP::parseRange */ protected function initialiseRange() { @@ -352,6 +361,7 @@ class Block { if ( wfReadOnly() ) { return false; } + if ( !$this->mId ) { throw new MWException( "Block::delete() now requires that the mId member be filled\n" ); } @@ -377,8 +387,9 @@ class Block { # Don't collide with expired blocks Block::purgeExpired(); - $ipb_id = $dbw->nextSequenceValue('ipblocks_ipb_id_val'); - $dbw->insert( 'ipblocks', + $ipb_id = $dbw->nextSequenceValue( 'ipblocks_ipb_id_seq' ); + $dbw->insert( + 'ipblocks', array( 'ipb_id' => $ipb_id, 'ipb_address' => $this->mAddress, @@ -386,7 +397,7 @@ class Block { 'ipb_by' => $this->mBy, 'ipb_by_text' => $this->mByName, 'ipb_reason' => $this->mReason, - 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), + 'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ), 'ipb_auto' => $this->mAuto, 'ipb_anon_only' => $this->mAnonOnly, 'ipb_create_account' => $this->mCreateAccount, @@ -394,14 +405,16 @@ class Block { 'ipb_expiry' => self::encodeExpiry( $this->mExpiry, $dbw ), 'ipb_range_start' => $this->mRangeStart, 'ipb_range_end' => $this->mRangeEnd, - 'ipb_deleted' => $this->mHideName, + 'ipb_deleted' => intval( $this->mHideName ), // typecast required for SQLite 'ipb_block_email' => $this->mBlockEmail, 'ipb_allow_usertalk' => $this->mAllowUsertalk - ), 'Block::insert', array( 'IGNORE' ) + ), + 'Block::insert', + array( 'IGNORE' ) ); $affected = $dbw->affectedRows(); - if ($affected) + if ( $affected ) $this->doRetroactiveAutoblock(); return (bool)$affected; @@ -417,13 +430,14 @@ class Block { $this->validateBlockParams(); - $dbw->update( 'ipblocks', + $dbw->update( + 'ipblocks', array( 'ipb_user' => $this->mUser, 'ipb_by' => $this->mBy, 'ipb_by_text' => $this->mByName, 'ipb_reason' => $this->mReason, - 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), + 'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ), 'ipb_auto' => $this->mAuto, 'ipb_anon_only' => $this->mAnonOnly, 'ipb_create_account' => $this->mCreateAccount, @@ -433,13 +447,15 @@ class Block { 'ipb_range_end' => $this->mRangeEnd, 'ipb_deleted' => $this->mHideName, 'ipb_block_email' => $this->mBlockEmail, - 'ipb_allow_usertalk' => $this->mAllowUsertalk ), + 'ipb_allow_usertalk' => $this->mAllowUsertalk + ), array( 'ipb_id' => $this->mId ), - 'Block::update' ); + 'Block::update' + ); return $dbw->affectedRows(); } - + /** * Make sure all the proper members are set to sane values * before adding/updating a block @@ -453,11 +469,14 @@ class Block { # Unset ipb_enable_autoblock for IP blocks, makes no sense if ( !$this->mUser ) { $this->mEnableAutoblock = 0; - $this->mBlockEmail = 0; //Same goes for email... } - if( !$this->mByName ) { - if( $this->mBy ) { + # bug 18860: non-anon-only IP blocks should be allowed to block email + if ( !$this->mUser && $this->mAnonOnly ) { + $this->mBlockEmail = 0; + } + if ( !$this->mByName ) { + if ( $this->mBy ) { $this->mByName = User::whoIs( $this->mBy ); } else { global $wgUser; @@ -465,28 +484,27 @@ class Block { } } } - - + /** - * Retroactively autoblocks the last IP used by the user (if it is a user) - * blocked by this Block. - * - * @return Boolean: whether or not a retroactive autoblock was made. - */ + * Retroactively autoblocks the last IP used by the user (if it is a user) + * blocked by this Block. + * + * @return Boolean: whether or not a retroactive autoblock was made. + */ public function doRetroactiveAutoblock() { $dbr = wfGetDB( DB_SLAVE ); - #If autoblock is enabled, autoblock the LAST IP used + # If autoblock is enabled, autoblock the LAST IP used # - stolen shamelessly from CheckUser_body.php - if ($this->mEnableAutoblock && $this->mUser) { - wfDebug("Doing retroactive autoblocks for " . $this->mAddress . "\n"); - + 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) { + + 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) ) ); + $conds[] = 'rc_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( time() - ( 7 * 86400 ) ) ); $options['LIMIT'] = 5; } else { // Just the last IP used. @@ -494,11 +512,11 @@ class Block { } $res = $dbr->select( 'recentchanges', array( 'rc_ip' ), $conds, - __METHOD__ , $options); + __METHOD__ , $options ); if ( !$dbr->numRows( $res ) ) { - #No results, don't autoblock anything - wfDebug("No IP found to retroactively autoblock\n"); + # No results, don't autoblock anything + wfDebug( "No IP found to retroactively autoblock\n" ); } else { while ( $row = $dbr->fetchObject( $res ) ) { if ( $row->rc_ip ) @@ -507,7 +525,7 @@ class Block { } } } - + /** * Checks whether a given IP is on the autoblock whitelist. * @@ -516,7 +534,7 @@ class Block { */ public static function isWhitelistedFromAutoblocks( $ip ) { global $wgMemc; - + // Try to get the autoblock_whitelist from the cache, as it's faster // than getting the msg raw and explode()'ing it. $key = wfMemcKey( 'ipb', 'autoblock', 'whitelist' ); @@ -526,28 +544,28 @@ class Block { $wgMemc->set( $key, $lines, 3600 * 24 ); } - wfDebug("Checking the autoblock whitelist..\n"); + wfDebug( "Checking the autoblock whitelist..\n" ); - foreach( $lines as $line ) { + foreach ( $lines as $line ) { # List items only if ( substr( $line, 0, 1 ) !== '*' ) { continue; } - $wlEntry = substr($line, 1); - $wlEntry = trim($wlEntry); + $wlEntry = substr( $line, 1 ); + $wlEntry = trim( $wlEntry ); - wfDebug("Checking $ip against $wlEntry..."); + wfDebug( "Checking $ip against $wlEntry..." ); # Is the IP in this range? - if (IP::isInRange( $ip, $wlEntry )) { - wfDebug(" IP $ip matches $wlEntry, not autoblocking\n"); + if ( IP::isInRange( $ip, $wlEntry ) ) { + wfDebug( " IP $ip matches $wlEntry, not autoblocking\n" ); return true; } else { wfDebug( " No match\n" ); } } - + return false; } @@ -565,12 +583,12 @@ class Block { } # Check for presence on the autoblock whitelist - if (Block::isWhitelistedFromAutoblocks($autoblockIP)) { + if ( Block::isWhitelistedFromAutoblocks( $autoblockIP ) ) { return; } - - ## Allow hooks to cancel the autoblock. - if (!wfRunHooks( 'AbortAutoblock', array( $autoblockIP, &$this ) )) { + + # # Allow hooks to cancel the autoblock. + if ( !wfRunHooks( 'AbortAutoblock', array( $autoblockIP, &$this ) ) ) { wfDebug( "Autoblock aborted by hook.\n" ); return false; } @@ -582,8 +600,8 @@ class Block { # If the user is already blocked. Then check if the autoblock would # exceed the user block. If it would exceed, then do nothing, else # prolong block time - if ($this->mExpiry && - ($this->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) { + if ( $this->mExpiry && + ( $this->mExpiry < Block::getAutoblockExpiry( $ipblock->mTimestamp ) ) ) { return; } # Just update the timestamp @@ -610,8 +628,8 @@ class Block { $ipblock->mAllowUsertalk = $this->mAllowUsertalk; # If the user is already blocked with an expiry date, we don't # want to pile on top of that! - if($this->mExpiry) { - $ipblock->mExpiry = min ( $this->mExpiry, Block::getAutoblockExpiry( $this->mTimestamp )); + if ( $this->mExpiry ) { + $ipblock->mExpiry = min( $this->mExpiry, Block::getAutoblockExpiry( $this->mTimestamp ) ); } else { $ipblock->mExpiry = Block::getAutoblockExpiry( $this->mTimestamp ); } @@ -624,8 +642,7 @@ class Block { * @return Boolean */ public function deleteIfExpired() { - $fname = 'Block::deleteIfExpired'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); if ( $this->isExpired() ) { wfDebug( "Block::deleteIfExpired() -- deleting\n" ); $this->delete(); @@ -634,7 +651,7 @@ class Block { wfDebug( "Block::deleteIfExpired() -- not expired\n" ); $retVal = false; } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $retVal; } @@ -660,7 +677,7 @@ class Block { } /** - * Update the timestamp on autoblocks. + * Update the timestamp on autoblocks. */ public function updateTimestamp() { if ( $this->mAuto ) { @@ -670,8 +687,8 @@ class Block { $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'ipblocks', array( /* SET */ - 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), - 'ipb_expiry' => $dbw->timestamp($this->mExpiry), + 'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ), + 'ipb_expiry' => $dbw->timestamp( $this->mExpiry ), ), array( /* WHERE */ 'ipb_address' => $this->mAddress ), 'Block::updateTimestamp' @@ -700,14 +717,14 @@ class Block { /** * Get/set the SELECT ... FOR UPDATE flag */ - public function forUpdate( $x = NULL ) { + public function forUpdate( $x = null ) { return wfSetVar( $this->mForUpdate, $x ); } /** * Get/set a flag determining whether the master is used for reads */ - public function fromMaster( $x = NULL ) { + public function fromMaster( $x = null ) { return wfSetVar( $this->mFromMaster, $x ); } @@ -726,7 +743,7 @@ class Block { /** * Encode expiry for DB * - * @param $expiry String: timestamp for expiry, or + * @param $expiry String: timestamp for expiry, or * @param $db Database object * @return String */ @@ -773,10 +790,10 @@ class Block { $parts = explode( '/', $range ); if ( count( $parts ) == 2 ) { // IPv6 - if ( IP::isIPv6($range) && $parts[1] >= 64 && $parts[1] <= 128 ) { + if ( IP::isIPv6( $range ) && $parts[1] >= 64 && $parts[1] <= 128 ) { $bits = $parts[1]; $ipint = IP::toUnsigned6( $parts[0] ); - # Native 32 bit functions WONT work here!!! + # Native 32 bit functions WON'T work here!!! # Convert to a padded binary number $network = wfBaseConvert( $ipint, 10, 2, 128 ); # Truncate the last (128-$bits) bits and replace them with zeros @@ -787,7 +804,7 @@ class Block { $newip = IP::toOctet( $network ); $range = "$newip/{$parts[1]}"; } // IPv4 - else if ( IP::isIPv4($range) && $parts[1] >= 16 && $parts[1] <= 32 ) { + elseif ( IP::isIPv4( $range ) && $parts[1] >= 16 && $parts[1] <= 32 ) { $shift = 32 - $parts[1]; $ipint = IP::toUnsigned( $parts[0] ); $ipint = $ipint >> $shift << $shift; @@ -808,7 +825,7 @@ class Block { /** * Get a value to insert into expiry field of the database when infinite expiry - * is desired. In principle this could be DBMS-dependant, but currently all + * is desired. In principle this could be DBMS-dependant, but currently all * supported DBMS's support the string "infinity", so we just use that. * * @return String @@ -818,35 +835,36 @@ class Block { # works with CHAR(14) as well because "i" sorts after all numbers. return 'infinity'; } - + /** * Convert a DB-encoded expiry into a real string that humans can read. * * @param $encoded_expiry String: Database encoded expiry time - * @return String + * @return Html-escaped String */ public static function formatExpiry( $encoded_expiry ) { static $msg = null; - - if( is_null( $msg ) ) { + + if ( is_null( $msg ) ) { $msg = array(); $keys = array( 'infiniteblock', 'expiringblock' ); - foreach( $keys as $key ) { + foreach ( $keys as $key ) { $msg[$key] = wfMsgHtml( $key ); } } - + $expiry = Block::decodeExpiry( $encoded_expiry ); - if ($expiry == 'infinity') { + if ( $expiry == 'infinity' ) { $expirystr = $msg['infiniteblock']; } else { global $wgLang; - $expiretimestr = $wgLang->timeanddate( $expiry, true ); - $expirystr = wfMsgReplaceArgs( $msg['expiringblock'], array($expiretimestr) ); + $expiredatestr = htmlspecialchars( $wgLang->date( $expiry, true ) ); + $expiretimestr = htmlspecialchars( $wgLang->time( $expiry, true ) ); + $expirystr = wfMsgReplaceArgs( $msg['expiringblock'], array( $expiredatestr, $expiretimestr ) ); } return $expirystr; } - + /** * Convert a typed-in expiry time into something we can put into the database. * @param $expiry_input String: whatever was typed into the form @@ -857,7 +875,8 @@ class Block { $expiry = 'infinity'; } else { $expiry = strtotime( $expiry_input ); - if ($expiry < 0 || $expiry === false) { + + if ( $expiry < 0 || $expiry === false ) { return false; } } diff --git a/includes/CacheDependency.php b/includes/CacheDependency.php index b050c46d..11e70738 100644 --- a/includes/CacheDependency.php +++ b/includes/CacheDependency.php @@ -1,5 +1,4 @@ <?php - /** * This class stores an arbitrary value along with its dependencies. * Users should typically only use DependencyWrapper::getFromCache(), rather @@ -12,8 +11,8 @@ class DependencyWrapper { /** * Create an instance. - * @param mixed $value The user-supplied value - * @param mixed $deps A dependency or dependency array. All dependencies + * @param $value Mixed: the user-supplied value + * @param $deps Mixed: a dependency or dependency array. All dependencies * must be objects implementing CacheDependency. */ function __construct( $value = false, $deps = array() ) { @@ -66,12 +65,12 @@ class DependencyWrapper { * it will be generated with the callback function (if present), and the newly * 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 $cache Object: a cache object such as $wgMemc + * @param $key String: the cache key + * @param $expiry Integer: the expiry timestamp or interval in seconds + * @param $callback Mixed: the callback for generating the value, or false + * @param $callbackParams Array: the function parameters for the callback + * @param $deps Array: 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. * @@ -108,7 +107,7 @@ abstract class CacheDependency { /** * Hook to perform any expensive pre-serialize loading of dependency values. */ - function loadDependencyValues() {} + function loadDependencyValues() { } } /** @@ -120,8 +119,8 @@ class FileDependency extends CacheDependency { /** * Create a file dependency * - * @param string $filename The name of the file, preferably fully qualified - * @param mixed $timestamp The unix last modified timestamp, or false if the + * @param $filename String: the name of the file, preferably fully qualified + * @param $timestamp Mixed: the unix last modified timestamp, or false if the * file does not exist. If omitted, the timestamp will be loaded from * the file. * @@ -134,6 +133,11 @@ class FileDependency extends CacheDependency { $this->timestamp = $timestamp; } + function __sleep() { + $this->loadDependencyValues(); + return array( 'filename', 'timestamp' ); + } + function loadDependencyValues() { if ( is_null( $this->timestamp ) ) { if ( !file_exists( $this->filename ) ) { @@ -180,7 +184,7 @@ class TitleDependency extends CacheDependency { /** * Construct a title dependency - * @param Title $title + * @param $title Title */ function __construct( Title $title ) { $this->titleObj = $title; @@ -208,6 +212,7 @@ class TitleDependency extends CacheDependency { function isExpired() { $touched = $this->getTitle()->getTouched(); + if ( $this->touched === false ) { if ( $touched === false ) { # Still missing @@ -246,6 +251,7 @@ class TitleListDependency extends CacheDependency { function calculateTimestamps() { # Initialise values to false $timestamps = array(); + foreach ( $this->getLinkBatch()->data as $ns => $dbks ) { if ( count( $dbks ) > 0 ) { $timestamps[$ns] = array(); @@ -259,9 +265,13 @@ 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__ ); + $where, + __METHOD__ + ); + while ( $row = $dbr->fetchObject( $res ) ) { $timestamps[$row->page_namespace][$row->page_title] = $row->page_touched; } @@ -278,7 +288,7 @@ class TitleListDependency extends CacheDependency { } function getLinkBatch() { - if ( !isset( $this->linkBatch ) ){ + if ( !isset( $this->linkBatch ) ) { $this->linkBatch = new LinkBatch; $this->linkBatch->setArray( $this->timestamps ); } @@ -290,6 +300,7 @@ class TitleListDependency extends CacheDependency { foreach ( $this->timestamps as $ns => $dbks ) { foreach ( $dbks as $dbk => $oldTimestamp ) { $newTimestamp = $newTimestamps[$ns][$dbk]; + if ( $oldTimestamp === false ) { if ( $newTimestamp === false ) { # Still missing diff --git a/includes/Category.php b/includes/Category.php index 50efdbc1..e9ffaecf 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -1,6 +1,6 @@ <?php /** - * Category objects are immutable, strictly speaking. If you call methods that change the database, + * 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. * @@ -18,21 +18,21 @@ class Category { /** Counts of membership (cat_pages, cat_subcats, cat_files) */ private $mPages = null, $mSubcats = null, $mFiles = null; - private function __construct() {} + 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->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 ) { + 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 ) { + } elseif ( $this->mName === null ) { $where = array( 'cat_id' => $this->mID ); } else { # Already initialized @@ -45,12 +45,13 @@ class Category { $where, __METHOD__ ); - if( !$row ) { + + 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->mName = $this->mTitle->getDBkey(); $this->mPages = 0; $this->mSubcats = 0; $this->mFiles = 0; @@ -60,6 +61,7 @@ class Category { return false; # Fail } } + $this->mID = $row->cat_id; $this->mName = $row->cat_title; $this->mPages = $row->cat_pages; @@ -69,7 +71,7 @@ class Category { # (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 ) { + if ( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) { $this->refreshCounts(); } @@ -86,12 +88,13 @@ class Category { public static function newFromName( $name ) { $cat = new self(); $title = Title::makeTitleSafe( NS_CATEGORY, $name ); - if( !is_object( $title ) ) { + + if ( !is_object( $title ) ) { return false; } $cat->mTitle = $title; - $cat->mName = $title->getDBKey(); + $cat->mName = $title->getDBkey(); return $cat; } @@ -106,7 +109,7 @@ class Category { $cat = new self(); $cat->mTitle = $title; - $cat->mName = $title->getDBKey(); + $cat->mName = $title->getDBkey(); return $cat; } @@ -126,7 +129,7 @@ class Category { /** * 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, + * @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. @@ -137,8 +140,7 @@ class Category { $cat = new self(); $cat->mTitle = $title; - - # NOTE: the row often results from a LEFT JOIN on categorylinks. This may result in + # 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. @@ -149,7 +151,7 @@ class Category { # 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->mName = $title->getDBkey(); # if we have a title object, fetch the category name from there } $cat->mID = false; @@ -169,12 +171,16 @@ class Category { /** @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' ); } @@ -182,9 +188,9 @@ class Category { * @return mixed The Title for this category, or false on failure. */ public function getTitle() { - if( $this->mTitle ) return $this->mTitle; - - if( !$this->initialize() ) { + if ( $this->mTitle ) return $this->mTitle; + + if ( !$this->initialize() ) { return false; } @@ -204,13 +210,19 @@ class Category { $conds = array( 'cl_to' => $this->getName(), 'cl_from = page_id' ); $options = array( 'ORDER BY' => 'cl_sortkey' ); - if( $limit ) $options[ 'LIMIT' ] = $limit; - if( $offset !== '' ) $conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset ); + + if ( $limit ) { + $options[ 'LIMIT' ] = $limit; + } + + if ( $offset !== '' ) { + $conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset ); + } return TitleArray::newFromResult( $dbr->select( array( 'page', 'categorylinks' ), - array( 'page_id', 'page_namespace','page_title', 'page_len', + array( 'page_id', 'page_namespace', 'page_title', 'page_len', 'page_is_redirect', 'page_latest' ), $conds, __METHOD__, @@ -221,10 +233,10 @@ class Category { /** Generic accessor */ private function getX( $key ) { - if( !$this->initialize() ) { + if ( !$this->initialize() ) { return false; } - return $this->{$key}; + return $this-> { $key } ; } /** @@ -233,29 +245,33 @@ class Category { * @return bool True on success, false on failure */ public function refreshCounts() { - if( wfReadOnly() ) { + 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() ) { + 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! + $seqVal = $dbw->nextSequenceValue( 'category_cat_id_seq' ); $dbw->insert( 'category', - array( 'cat_title' => $this->mName ), + array( + 'cat_id' => $seqVal, + 'cat_title' => $this->mName + ), __METHOD__, 'IGNORE' ); } - $cond1 = $dbw->conditional( 'page_namespace='.NS_CATEGORY, 1, 'NULL' ); - $cond2 = $dbw->conditional( 'page_namespace='.NS_FILE, 1, 'NULL' ); + $cond1 = $dbw->conditional( 'page_namespace=' . NS_CATEGORY, 1, 'NULL' ); + $cond2 = $dbw->conditional( 'page_namespace=' . NS_FILE, 1, 'NULL' ); $result = $dbw->selectRow( array( 'categorylinks', 'page' ), array( 'COUNT(*) AS pages', diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php index 03ecb5dc..56f85faa 100644 --- a/includes/CategoryPage.php +++ b/includes/CategoryPage.php @@ -5,7 +5,7 @@ * */ -if( !defined( 'MEDIAWIKI' ) ) +if ( !defined( 'MEDIAWIKI' ) ) die( 1 ); /** @@ -20,7 +20,7 @@ class CategoryPage extends Article { if ( isset( $diff ) && $diffOnly ) return Article::view(); - if( !wfRunHooks( 'CategoryPageView', array( &$this ) ) ) + if ( !wfRunHooks( 'CategoryPageView', array( &$this ) ) ) return; if ( NS_CATEGORY == $this->mTitle->getNamespace() ) { @@ -33,18 +33,17 @@ class CategoryPage extends Article { $this->closeShowCategory(); } } - + /** * Don't return a 404 for categories in use. */ function hasViewableContent() { - if( parent::hasViewableContent() ) { + if ( parent::hasViewableContent() ) { return true; } else { $cat = Category::newFromTitle( $this->mTitle ); return $cat->getId() != 0; } - } function openShowCategory() { @@ -86,7 +85,7 @@ class CategoryViewer { * @private */ function getHTML() { - global $wgOut, $wgCategoryMagicGallery, $wgCategoryPagingLimit; + global $wgOut, $wgCategoryMagicGallery, $wgCategoryPagingLimit, $wgContLang; wfProfileIn( __METHOD__ ); $this->showGallery = $wgCategoryMagicGallery && !$wgOut->mNoGallery; @@ -95,11 +94,22 @@ class CategoryViewer { $this->doCategoryQuery(); $this->finaliseCategoryState(); - $r = $this->getCategoryTop() . - $this->getSubcategorySection() . + $r = $this->getSubcategorySection() . $this->getPagesSection() . - $this->getImageSection() . - $this->getCategoryBottom(); + $this->getImageSection(); + + if ( $r == '' ) { + // If there is no category content to display, only + // show the top part of the navigation links. + // FIXME: cannot be completely suppressed because it + // is unknown if 'until' or 'from' makes this + // give 0 results. + $r = $r . $this->getCategoryTop(); + } else { + $r = $this->getCategoryTop() . + $r . + $this->getCategoryBottom(); + } // Give a proper message if category is empty if ( $r == '' ) { @@ -107,7 +117,7 @@ class CategoryViewer { } wfProfileOut( __METHOD__ ); - return $r; + return $wgContLang->convert( $r ); } function clearCategoryState() { @@ -115,7 +125,7 @@ class CategoryViewer { $this->articles_start_char = array(); $this->children = array(); $this->children_start_char = array(); - if( $this->showGallery ) { + if ( $this->showGallery ) { $this->gallery = new ImageGallery(); $this->gallery->setHideBadImages(); } @@ -138,14 +148,18 @@ class CategoryViewer { } /** - * Add a subcategory to the internal lists, using a title object + * Add a subcategory to the internal lists, using a title object * @deprecated 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( - $title, $wgContLang->convertHtml( $title->getText() ) ); + $this->children[] = $this->getSkin()->link( + $title, + null, + array(), + array(), + array( 'known', 'noclasses' ) + ); $this->children_start_char[] = $this->getSubcategorySortChar( $title, $sortkey ); } @@ -160,7 +174,7 @@ class CategoryViewer { function getSubcategorySortChar( $title, $sortkey ) { global $wgContLang; - if( $title->getPrefixedText() == $sortkey ) { + if ( $title->getPrefixedText() == $sortkey ) { $firstChar = $wgContLang->firstChar( $title->getDBkey() ); } else { $firstChar = $wgContLang->firstChar( $sortkey ); @@ -174,7 +188,7 @@ class CategoryViewer { */ function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) { if ( $this->showGallery ) { - if( $this->flip ) { + if ( $this->flip ) { $this->gallery->insert( $title ); } else { $this->gallery->add( $title ); @@ -189,15 +203,21 @@ class CategoryViewer { */ function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) { global $wgContLang; - $titletext = $wgContLang->convert( $title->getPrefixedText() ); $this->articles[] = $isRedirect - ? '<span class="redirect-in-category">' . $this->getSkin()->makeKnownLinkObj( $title, $titletext ) . '</span>' - : $this->getSkin()->makeSizeLinkObj( $pageLength, $title, $titletext ); + ? '<span class="redirect-in-category">' . + $this->getSkin()->link( + $title, + null, + array(), + array(), + array( 'known', 'noclasses' ) + ) . '</span>' + : $this->getSkin()->makeSizeLinkObj( $pageLength, $title ); $this->articles_start_char[] = $wgContLang->convert( $wgContLang->firstChar( $sortkey ) ); } function finaliseCategoryState() { - if( $this->flip ) { + if ( $this->flip ) { $this->children = array_reverse( $this->children ); $this->children_start_char = array_reverse( $this->children_start_char ); $this->articles = array_reverse( $this->articles ); @@ -207,16 +227,17 @@ class CategoryViewer { function doCategoryQuery() { $dbr = wfGetDB( DB_SLAVE, 'category' ); - if( $this->from != '' ) { + if ( $this->from != '' ) { $pageCondition = 'cl_sortkey >= ' . $dbr->addQuotes( $this->from ); $this->flip = false; - } elseif( $this->until != '' ) { + } elseif ( $this->until != '' ) { $pageCondition = 'cl_sortkey < ' . $dbr->addQuotes( $this->until ); $this->flip = true; } else { $pageCondition = '1 = 1'; $this->flip = false; } + $res = $dbr->select( array( 'page', 'categorylinks', 'category' ), array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey', @@ -232,8 +253,9 @@ class CategoryViewer { $count = 0; $this->nextPage = null; - while( $x = $dbr->fetchObject ( $res ) ) { - if( ++$count > $this->limit ) { + + while ( $x = $dbr->fetchObject ( $res ) ) { + if ( ++$count > $this->limit ) { // We've reached the one extra which shows that there are // additional pages to be had. Stop here... $this->nextPage = $x->cl_sortkey; @@ -242,26 +264,20 @@ class CategoryViewer { $title = Title::makeTitle( $x->page_namespace, $x->page_title ); - if( $title->getNamespace() == NS_CATEGORY ) { + if ( $title->getNamespace() == NS_CATEGORY ) { $cat = Category::newFromRow( $x, $title ); $this->addSubcategoryObject( $cat, $x->cl_sortkey, $x->page_len ); - } elseif( $this->showGallery && $title->getNamespace() == NS_FILE ) { + } elseif ( $this->showGallery && $title->getNamespace() == NS_FILE ) { $this->addImage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect ); } else { $this->addPage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect ); } } - $dbr->freeResult( $res ); } function getCategoryTop() { - $r = ''; - if( $this->until != '' ) { - $r .= $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit ); - } elseif( $this->nextPage != '' || $this->from != '' ) { - $r .= $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit ); - } - return $r == '' + $r = $this->getCategoryBottom(); + return $r === '' ? $r : "<br style=\"clear:both;\"/>\n" . $r; } @@ -272,7 +288,8 @@ class CategoryViewer { $rescnt = count( $this->children ); $dbcnt = $this->cat->getSubcatCount(); $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' ); - if( $rescnt > 0 ) { + + if ( $rescnt > 0 ) { # Showing subcategories $r .= "<div id=\"mw-subcategories\">\n"; $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n"; @@ -297,7 +314,7 @@ class CategoryViewer { $rescnt = count( $this->articles ); $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' ); - if( $rescnt > 0 ) { + if ( $rescnt > 0 ) { $r = "<div id=\"mw-pages\">\n"; $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n"; $r .= $countmsg; @@ -308,7 +325,7 @@ class CategoryViewer { } function getImageSection() { - if( $this->showGallery && ! $this->gallery->isEmpty() ) { + if ( $this->showGallery && ! $this->gallery->isEmpty() ) { $dbcnt = $this->cat->getFileCount(); $rescnt = $this->gallery->count(); $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' ); @@ -322,9 +339,9 @@ class CategoryViewer { } function getCategoryBottom() { - if( $this->until != '' ) { + if ( $this->until != '' ) { return $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit ); - } elseif( $this->nextPage != '' || $this->from != '' ) { + } elseif ( $this->nextPage != '' || $this->from != '' ) { return $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit ); } else { return ''; @@ -344,7 +361,7 @@ class CategoryViewer { function formatList( $articles, $articles_start_char, $cutoff = 6 ) { if ( count ( $articles ) > $cutoff ) { return $this->columnList( $articles, $articles_start_char ); - } elseif ( count($articles) > 0) { + } elseif ( count( $articles ) > 0 ) { // for short lists of articles in categories. return $this->shortList( $articles, $articles_start_char ); } @@ -355,61 +372,60 @@ class CategoryViewer { * Format a list of articles chunked by letter in a three-column * list, ordered vertically. * + * TODO: Take the headers into account when creating columns, so they're + * more visually equal. + * + * More distant TODO: Scrap this and use CSS columns, whenever IE finally + * supports those. + * * @param $articles Array * @param $articles_start_char Array * @return String * @private */ function columnList( $articles, $articles_start_char ) { - // divide list into three equal chunks - $chunk = (int) (count ( $articles ) / 3); + $columns = array_combine( $articles, $articles_start_char ); + # Split into three columns + $columns = array_chunk( $columns, ceil( count( $columns ) / 3 ), true /* preserve keys */ ); - // get and display header - $r = '<table width="100%"><tr valign="top">'; + $ret = '<table width="100%"><tr valign="top"><td>'; + $prevchar = null; - $prev_start_char = 'none'; + foreach ( $columns as $column ) { + $colContents = array(); - // loop through the chunks - for($startChunk = 0, $endChunk = $chunk, $chunkIndex = 0; - $chunkIndex < 3; - $chunkIndex++, $startChunk = $endChunk, $endChunk += $chunk + 1) - { - $r .= "<td>\n"; - $atColumnTop = true; + # Kind of like array_flip() here, but we keep duplicates in an + # array instead of dropping them. + foreach ( $column as $article => $char ) { + if ( !isset( $colContents[$char] ) ) { + $colContents[$char] = array(); + } + $colContents[$char][] = $article; + } - // output all articles in category - for ($index = $startChunk ; - $index < $endChunk && $index < count($articles); - $index++ ) - { - // check for change of starting letter or begining of chunk - if ( ($index == $startChunk) || - ($articles_start_char[$index] != $articles_start_char[$index - 1]) ) - - { - if( $atColumnTop ) { - $atColumnTop = false; - } else { - $r .= "</ul>\n"; - } - $cont_msg = ""; - if ( $articles_start_char[$index] == $prev_start_char ) - $cont_msg = ' ' . wfMsgHtml( 'listingcontinuesabbrev' ); - $r .= "<h3>" . htmlspecialchars( $articles_start_char[$index] ) . "$cont_msg</h3>\n<ul>"; - $prev_start_char = $articles_start_char[$index]; + $first = true; + foreach ( $colContents as $char => $articles ) { + $ret .= '<h3>' . htmlspecialchars( $char ); + if ( $first && $char === $prevchar ) { + # We're continuing a previous chunk at the top of a new + # column, so add " cont." after the letter. + $ret .= ' ' . wfMsgHtml( 'listingcontinuesabbrev' ); } + $ret .= "</h3>\n"; - $r .= "<li>{$articles[$index]}</li>"; - } - if( !$atColumnTop ) { - $r .= "</ul>\n"; - } - $r .= "</td>\n"; + $ret .= '<ul><li>'; + $ret .= implode( "</li>\n<li>", $articles ); + $ret .= '</li></ul>'; + $first = false; + $prevchar = $char; + } + $ret .= "</td>\n<td>"; } - $r .= '</tr></table>'; - return $r; + + $ret .= '</td></tr></table>'; + return $ret; } /** @@ -421,10 +437,10 @@ class CategoryViewer { */ function shortList( $articles, $articles_start_char ) { $r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n"; - $r .= '<ul><li>'.$articles[0].'</li>'; - for ($index = 1; $index < count($articles); $index++ ) + $r .= '<ul><li>' . $articles[0] . '</li>'; + for ( $index = 1; $index < count( $articles ); $index++ ) { - if ($articles_start_char[$index] != $articles_start_char[$index - 1]) + if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) { $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>"; } @@ -450,14 +466,29 @@ class CategoryViewer { $limitText = $wgLang->formatNum( $limit ); $prevLink = wfMsgExt( 'prevn', array( 'escape', 'parsemag' ), $limitText ); - if( $first != '' ) { - $prevLink = $sk->makeLinkObj( $title, $prevLink, - wfArrayToCGI( $query + array( 'until' => $first ) ) ); + + if ( $first != '' ) { + $prevQuery = $query; + $prevQuery['until'] = $first; + $prevLink = $sk->linkKnown( + $title, + $prevLink, + array(), + $prevQuery + ); } + $nextLink = wfMsgExt( 'nextn', array( 'escape', 'parsemag' ), $limitText ); - if( $last != '' ) { - $nextLink = $sk->makeLinkObj( $title, $nextLink, - wfArrayToCGI( $query + array( 'from' => $last ) ) ); + + if ( $last != '' ) { + $lastQuery = $query; + $lastQuery['from'] = $last; + $nextLink = $sk->linkKnown( + $title, + $nextLink, + array(), + $lastQuery + ); } return "($prevLink) ($nextLink)"; @@ -490,12 +521,14 @@ class CategoryViewer { # 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)){ + ( $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){ + } elseif ( $totalrescnt < $this->limit && !$this->from && !$this->until ) { # Case 2: not sane, but salvageable. Use the number of results. # Since there are fewer than 200, we can also take this opportunity # to refresh the incorrect category table entry -- which should be @@ -504,10 +537,14 @@ class CategoryViewer { $this->cat->refreshCounts(); } else { # Case 3: hopeless. Don't give a total count at all. - return wfMsgExt("category-$type-count-limited", 'parse', + return wfMsgExt( "category-$type-count-limited", 'parse', $wgLang->formatNum( $rescnt ) ); } - return wfMsgExt( "category-$type-count", 'parse', $wgLang->formatNum( $rescnt ), - $wgLang->formatNum( $totalcnt ) ); + return wfMsgExt( + "category-$type-count", + 'parse', + $wgLang->formatNum( $rescnt ), + $wgLang->formatNum( $totalcnt ) + ); } } diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php index 7c1c2856..5ac8a9be 100644 --- a/includes/Categoryfinder.php +++ b/includes/Categoryfinder.php @@ -1,5 +1,4 @@ <?php - /** * The "Categoryfinder" class takes a list of articles, creates an internal * representation of all their parent categories (as well as parents of @@ -23,15 +22,14 @@ * */ class Categoryfinder { - - var $articles = array () ; # The original article IDs passed to the seed function - var $deadend = array () ; # Array of DBKEY category names for categories that don't have a page - var $parents = array () ; # Array of [ID => array()] - var $next = array () ; # Array of article/category IDs - var $targets = array () ; # Array of DBKEY category names - var $name2id = array () ; - var $mode ; # "AND" or "OR" - var $dbr ; # Read-DB slave + var $articles = array(); # The original article IDs passed to the seed function + var $deadend = array(); # Array of DBKEY category names for categories that don't have a page + var $parents = array(); # Array of [ID => array()] + var $next = array(); # Array of article/category IDs + var $targets = array(); # Array of DBKEY category names + var $name2id = array(); + var $mode; # "AND" or "OR" + var $dbr; # Read-DB slave /** * Constructor (currently empty). @@ -45,16 +43,16 @@ class Categoryfinder { * @param $categories FIXME * @param $mode String: FIXME, default 'AND'. */ - function seed ( $article_ids , $categories , $mode = "AND" ) { - $this->articles = $article_ids ; - $this->next = $article_ids ; - $this->mode = $mode ; + function seed( $article_ids, $categories, $mode = "AND" ) { + $this->articles = $article_ids; + $this->next = $article_ids; + $this->mode = $mode; # Set the list of target categories; convert them to DBKEY form first - $this->targets = array () ; - foreach ( $categories AS $c ) { + $this->targets = array(); + foreach ( $categories as $c ) { $ct = Title::makeTitleSafe( NS_CATEGORY, $c ); - if( $ct ) { + if ( $ct ) { $c = $ct->getDBkey(); $this->targets[$c] = $c; } @@ -69,19 +67,20 @@ class Categoryfinder { function run () { $this->dbr = wfGetDB( DB_SLAVE ); while ( count ( $this->next ) > 0 ) { - $this->scan_next_layer () ; + $this->scan_next_layer(); } # Now check if this applies to the individual articles - $ret = array () ; - foreach ( $this->articles AS $article ) { - $conds = $this->targets ; - if ( $this->check ( $article , $conds ) ) { + $ret = array(); + + foreach ( $this->articles as $article ) { + $conds = $this->targets; + if ( $this->check( $article, $conds ) ) { # Matches the conditions - $ret[] = $article ; + $ret[] = $article; } } - return $ret ; + return $ret; } /** @@ -91,108 +90,111 @@ class Categoryfinder { * @param $path used to check for recursion loops * @return bool Does this match the conditions? */ - function check ( $id , &$conds, $path=array() ) { + function check( $id , &$conds, $path = array() ) { // Check for loops and stop! - if( in_array( $id, $path ) ) + if ( in_array( $id, $path ) ) { return false; + } + $path[] = $id; # Shortcut (runtime paranoia): No contitions=all matched - if ( count ( $conds ) == 0 ) return true ; + if ( count( $conds ) == 0 ) { + return true; + } - if ( !isset ( $this->parents[$id] ) ) return false ; + if ( !isset( $this->parents[$id] ) ) { + return false; + } # iterate through the parents - foreach ( $this->parents[$id] AS $p ) { + foreach ( $this->parents[$id] as $p ) { $pname = $p->cl_to ; # Is this a condition? - if ( isset ( $conds[$pname] ) ) { + if ( isset( $conds[$pname] ) ) { # This key is in the category list! if ( $this->mode == "OR" ) { # One found, that's enough! - $conds = array () ; - return true ; + $conds = array(); + return true; } else { # Assuming "AND" as default - unset ( $conds[$pname] ) ; - if ( count ( $conds ) == 0 ) { + unset( $conds[$pname] ) ; + if ( count( $conds ) == 0 ) { # All conditions met, done - return true ; + return true; } } } # Not done yet, try sub-parents - if ( !isset ( $this->name2id[$pname] ) ) { + if ( !isset( $this->name2id[$pname] ) ) { # No sub-parent continue ; } - $done = $this->check ( $this->name2id[$pname] , $conds, $path ); - if ( $done OR count ( $conds ) == 0 ) { + $done = $this->check( $this->name2id[$pname], $conds, $path ); + if ( $done || count( $conds ) == 0 ) { # Subparents have done it! - return true ; + return true; } } - return false ; + return false; } /** * Scans a "parent layer" of the articles/categories in $this->next */ - function scan_next_layer () { - $fname = "Categoryfinder::scan_next_layer" ; - + function scan_next_layer() { # Find all parents of the article currently in $this->next - $layer = array () ; + $layer = array(); $res = $this->dbr->select( - /* FROM */ 'categorylinks', - /* SELECT */ '*', - /* WHERE */ array( 'cl_from' => $this->next ), - $fname."-1" + /* FROM */ 'categorylinks', + /* SELECT */ '*', + /* WHERE */ array( 'cl_from' => $this->next ), + __METHOD__ . "-1" ); while ( $o = $this->dbr->fetchObject( $res ) ) { $k = $o->cl_to ; # Update parent tree - if ( !isset ( $this->parents[$o->cl_from] ) ) { - $this->parents[$o->cl_from] = array () ; + if ( !isset( $this->parents[$o->cl_from] ) ) { + $this->parents[$o->cl_from] = array(); } - $this->parents[$o->cl_from][$k] = $o ; + $this->parents[$o->cl_from][$k] = $o; # Ignore those we already have - if ( in_array ( $k , $this->deadend ) ) continue ; - if ( isset ( $this->name2id[$k] ) ) continue ; + if ( in_array ( $k , $this->deadend ) ) continue; + + if ( isset ( $this->name2id[$k] ) ) continue; # Hey, new category! - $layer[$k] = $k ; + $layer[$k] = $k; } - $this->dbr->freeResult( $res ) ; - $this->next = array() ; + $this->next = array(); # Find the IDs of all category pages in $layer, if they exist if ( count ( $layer ) > 0 ) { $res = $this->dbr->select( - /* FROM */ 'page', - /* SELECT */ 'page_id,page_title', - /* WHERE */ array( 'page_namespace' => NS_CATEGORY , 'page_title' => $layer ), - $fname."-2" + /* FROM */ 'page', + /* SELECT */ array( 'page_id', 'page_title' ), + /* WHERE */ array( 'page_namespace' => NS_CATEGORY , 'page_title' => $layer ), + __METHOD__ . "-2" ); while ( $o = $this->dbr->fetchObject( $res ) ) { - $id = $o->page_id ; - $name = $o->page_title ; - $this->name2id[$name] = $id ; - $this->next[] = $id ; - unset ( $layer[$name] ) ; - } - $this->dbr->freeResult( $res ) ; + $id = $o->page_id; + $name = $o->page_title; + $this->name2id[$name] = $id; + $this->next[] = $id; + unset( $layer[$name] ); } + } # Mark dead ends - foreach ( $layer AS $v ) { - $this->deadend[$v] = $v ; + foreach ( $layer as $v ) { + $this->deadend[$v] = $v; } } -} # END OF CLASS "Categoryfinder" +} diff --git a/includes/Cdb.php b/includes/Cdb.php new file mode 100644 index 00000000..ab429872 --- /dev/null +++ b/includes/Cdb.php @@ -0,0 +1,149 @@ +<?php + +/** + * Read from a CDB file. + * Native and pure PHP implementations are provided. + * http://cr.yp.to/cdb.html + */ +abstract class CdbReader { + /** + * Open a file and return a subclass instance + */ + public static function open( $fileName ) { + if ( self::haveExtension() ) { + return new CdbReader_DBA( $fileName ); + } else { + wfDebug( "Warning: no dba extension found, using emulation.\n" ); + return new CdbReader_PHP( $fileName ); + } + } + + /** + * Returns true if the native extension is available + */ + public static function haveExtension() { + if ( !function_exists( 'dba_handlers' ) ) { + return false; + } + $handlers = dba_handlers(); + if ( !in_array( 'cdb', $handlers ) || !in_array( 'cdb_make', $handlers ) ) { + return false; + } + return true; + } + + /** + * Construct the object and open the file + */ + abstract function __construct( $fileName ); + + /** + * Close the file. Optional, you can just let the variable go out of scope. + */ + abstract function close(); + + /** + * Get a value with a given key. Only string values are supported. + */ + abstract public function get( $key ); +} + +/** + * Write to a CDB file. + * Native and pure PHP implementations are provided. + */ +abstract class CdbWriter { + /** + * Open a writer and return a subclass instance. + * The user must have write access to the directory, for temporary file creation. + */ + public static function open( $fileName ) { + if ( CdbReader::haveExtension() ) { + return new CdbWriter_DBA( $fileName ); + } else { + wfDebug( "Warning: no dba extension found, using emulation.\n" ); + return new CdbWriter_PHP( $fileName ); + } + } + + /** + * Create the object and open the file + */ + abstract function __construct( $fileName ); + + /** + * Set a key to a given value. The value will be converted to string. + */ + abstract public function set( $key, $value ); + + /** + * Close the writer object. You should call this function before the object + * goes out of scope, to write out the final hashtables. + */ + abstract public function close(); +} + + +/** + * Reader class which uses the DBA extension + */ +class CdbReader_DBA { + var $handle; + + function __construct( $fileName ) { + $this->handle = dba_open( $fileName, 'r-', 'cdb' ); + if ( !$this->handle ) { + throw new MWException( 'Unable to open DB file "' . $fileName . '"' ); + } + } + + function close() { + if( isset($this->handle) ) + dba_close( $this->handle ); + unset( $this->handle ); + } + + function get( $key ) { + return dba_fetch( $key, $this->handle ); + } +} + + +/** + * Writer class which uses the DBA extension + */ +class CdbWriter_DBA { + var $handle, $realFileName, $tmpFileName; + + function __construct( $fileName ) { + $this->realFileName = $fileName; + $this->tmpFileName = $fileName . '.tmp.' . mt_rand( 0, 0x7fffffff ); + $this->handle = dba_open( $this->tmpFileName, 'n', 'cdb_make' ); + if ( !$this->handle ) { + throw new MWException( 'Unable to open DB file for write "' . $fileName . '"' ); + } + } + + function set( $key, $value ) { + return dba_insert( $key, $value, $this->handle ); + } + + function close() { + if( isset($this->handle) ) + dba_close( $this->handle ); + if ( wfIsWindows() ) { + unlink( $this->realFileName ); + } + if ( !rename( $this->tmpFileName, $this->realFileName ) ) { + throw new MWException( 'Unable to move the new CDB file into place.' ); + } + unset( $this->handle ); + } + + function __destruct() { + if ( isset( $this->handle ) ) { + $this->close(); + } + } +} + diff --git a/includes/Cdb_PHP.php b/includes/Cdb_PHP.php new file mode 100644 index 00000000..49294f71 --- /dev/null +++ b/includes/Cdb_PHP.php @@ -0,0 +1,374 @@ +<?php + +/** + * This is a port of D.J. Bernstein's CDB to PHP. It's based on the copy that + * appears in PHP 5.3. Changes are: + * * Error returns replaced with exceptions + * * Exception thrown if sizes or offsets are between 2GB and 4GB + * * Some variables renamed + */ + +/** + * Common functions for readers and writers + */ +class CdbFunctions { + /** + * Take a modulo of a signed integer as if it were an unsigned integer. + * $b must be less than 0x40000000 and greater than 0 + */ + public static function unsignedMod( $a, $b ) { + if ( $a & 0x80000000 ) { + $m = ( $a & 0x7fffffff ) % $b + 2 * ( 0x40000000 % $b ); + return $m % $b; + } else { + return $a % $b; + } + } + + /** + * Shift a signed integer right as if it were unsigned + */ + public static function unsignedShiftRight( $a, $b ) { + if ( $b == 0 ) { + return $a; + } + if ( $a & 0x80000000 ) { + return ( ( $a & 0x7fffffff ) >> $b ) | ( 0x40000000 >> ( $b - 1 ) ); + } else { + return $a >> $b; + } + } + + /** + * The CDB hash function. + */ + public static function hash( $s ) { + $h = 5381; + for ( $i = 0; $i < strlen( $s ); $i++ ) { + $h5 = ($h << 5) & 0xffffffff; + // Do a 32-bit sum + // Inlined here for speed + $sum = ($h & 0x3fffffff) + ($h5 & 0x3fffffff); + $h = + ( + ( $sum & 0x40000000 ? 1 : 0 ) + + ( $h & 0x80000000 ? 2 : 0 ) + + ( $h & 0x40000000 ? 1 : 0 ) + + ( $h5 & 0x80000000 ? 2 : 0 ) + + ( $h5 & 0x40000000 ? 1 : 0 ) + ) << 30 + | ( $sum & 0x3fffffff ); + $h ^= ord( $s[$i] ); + $h &= 0xffffffff; + } + return $h; + } +} + +/** + * CDB reader class + */ +class CdbReader_PHP extends CdbReader { + /** The file handle */ + var $handle; + + /* number of hash slots searched under this key */ + var $loop; + + /* initialized if loop is nonzero */ + var $khash; + + /* initialized if loop is nonzero */ + var $kpos; + + /* initialized if loop is nonzero */ + var $hpos; + + /* initialized if loop is nonzero */ + var $hslots; + + /* initialized if findNext() returns true */ + var $dpos; + + /* initialized if cdb_findnext() returns 1 */ + var $dlen; + + function __construct( $fileName ) { + $this->handle = fopen( $fileName, 'rb' ); + if ( !$this->handle ) { + throw new MWException( 'Unable to open DB file "' . $fileName . '"' ); + } + $this->findStart(); + } + + function close() { + if( isset($this->handle) ) + fclose( $this->handle ); + unset( $this->handle ); + } + + public function get( $key ) { + // strval is required + if ( $this->find( strval( $key ) ) ) { + return $this->read( $this->dlen, $this->dpos ); + } else { + return false; + } + } + + protected function match( $key, $pos ) { + $buf = $this->read( strlen( $key ), $pos ); + return $buf === $key; + } + + protected function findStart() { + $this->loop = 0; + } + + protected function read( $length, $pos ) { + if ( fseek( $this->handle, $pos ) == -1 ) { + // This can easily happen if the internal pointers are incorrect + throw new MWException( __METHOD__.': seek failed, file may be corrupted.' ); + } + + if ( $length == 0 ) { + return ''; + } + + $buf = fread( $this->handle, $length ); + if ( $buf === false || strlen( $buf ) !== $length ) { + throw new MWException( __METHOD__.': read from cdb file failed, file may be corrupted' ); + } + return $buf; + } + + /** + * Unpack an unsigned integer and throw an exception if it needs more than 31 bits + */ + protected function unpack31( $s ) { + $data = unpack( 'V', $s ); + if ( $data[1] > 0x7fffffff ) { + throw new MWException( __METHOD__.': error in CDB file, integer too big' ); + } + return $data[1]; + } + + /** + * Unpack a 32-bit signed integer + */ + protected function unpackSigned( $s ) { + $data = unpack( 'va/vb', $s ); + return $data['a'] | ( $data['b'] << 16 ); + } + + protected function findNext( $key ) { + if ( !$this->loop ) { + $u = CdbFunctions::hash( $key ); + $buf = $this->read( 8, ( $u << 3 ) & 2047 ); + $this->hslots = $this->unpack31( substr( $buf, 4 ) ); + if ( !$this->hslots ) { + return false; + } + $this->hpos = $this->unpack31( substr( $buf, 0, 4 ) ); + $this->khash = $u; + $u = CdbFunctions::unsignedShiftRight( $u, 8 ); + $u = CdbFunctions::unsignedMod( $u, $this->hslots ); + $u <<= 3; + $this->kpos = $this->hpos + $u; + } + + while ( $this->loop < $this->hslots ) { + $buf = $this->read( 8, $this->kpos ); + $pos = $this->unpack31( substr( $buf, 4 ) ); + if ( !$pos ) { + return false; + } + $this->loop += 1; + $this->kpos += 8; + if ( $this->kpos == $this->hpos + ( $this->hslots << 3 ) ) { + $this->kpos = $this->hpos; + } + $u = $this->unpackSigned( substr( $buf, 0, 4 ) ); + if ( $u === $this->khash ) { + $buf = $this->read( 8, $pos ); + $keyLen = $this->unpack31( substr( $buf, 0, 4 ) ); + if ( $keyLen == strlen( $key ) && $this->match( $key, $pos + 8 ) ) { + // Found + $this->dlen = $this->unpack31( substr( $buf, 4 ) ); + $this->dpos = $pos + 8 + $keyLen; + return true; + } + } + } + return false; + } + + protected function find( $key ) { + $this->findStart(); + return $this->findNext( $key ); + } +} + +/** + * CDB writer class + */ +class CdbWriter_PHP extends CdbWriter { + var $handle, $realFileName, $tmpFileName; + + var $hplist; + var $numEntries, $pos; + + function __construct( $fileName ) { + $this->realFileName = $fileName; + $this->tmpFileName = $fileName . '.tmp.' . mt_rand( 0, 0x7fffffff ); + $this->handle = fopen( $this->tmpFileName, 'wb' ); + if ( !$this->handle ) { + throw new MWException( 'Unable to open DB file for write "' . $fileName . '"' ); + } + $this->hplist = array(); + $this->numentries = 0; + $this->pos = 2048; // leaving space for the pointer array, 256 * 8 + if ( fseek( $this->handle, $this->pos ) == -1 ) { + throw new MWException( __METHOD__.': fseek failed' ); + } + } + + function __destruct() { + if ( isset( $this->handle ) ) { + $this->close(); + } + } + + public function set( $key, $value ) { + if ( strval( $key ) === '' ) { + // DBA cross-check hack + return; + } + $this->addbegin( strlen( $key ), strlen( $value ) ); + $this->write( $key ); + $this->write( $value ); + $this->addend( strlen( $key ), strlen( $value ), CdbFunctions::hash( $key ) ); + } + + public function close() { + $this->finish(); + if( isset($this->handle) ) + fclose( $this->handle ); + if ( wfIsWindows() && file_exists($this->realFileName) ) { + unlink( $this->realFileName ); + } + if ( !rename( $this->tmpFileName, $this->realFileName ) ) { + throw new MWException( 'Unable to move the new CDB file into place.' ); + } + unset( $this->handle ); + } + + protected function write( $buf ) { + $len = fwrite( $this->handle, $buf ); + if ( $len !== strlen( $buf ) ) { + throw new MWException( 'Error writing to CDB file.' ); + } + } + + protected function posplus( $len ) { + $newpos = $this->pos + $len; + if ( $newpos > 0x7fffffff ) { + throw new MWException( 'A value in the CDB file is too large' ); + } + $this->pos = $newpos; + } + + protected function addend( $keylen, $datalen, $h ) { + $this->hplist[] = array( + 'h' => $h, + 'p' => $this->pos + ); + + $this->numentries++; + $this->posplus( 8 ); + $this->posplus( $keylen ); + $this->posplus( $datalen ); + } + + protected function addbegin( $keylen, $datalen ) { + if ( $keylen > 0x7fffffff ) { + throw new MWException( __METHOD__.': key length too long' ); + } + if ( $datalen > 0x7fffffff ) { + throw new MWException( __METHOD__.': data length too long' ); + } + $buf = pack( 'VV', $keylen, $datalen ); + $this->write( $buf ); + } + + protected function finish() { + // Hack for DBA cross-check + $this->hplist = array_reverse( $this->hplist ); + + // Calculate the number of items that will be in each hashtable + $counts = array_fill( 0, 256, 0 ); + foreach ( $this->hplist as $item ) { + ++ $counts[ 255 & $item['h'] ]; + } + + // Fill in $starts with the *end* indexes + $starts = array(); + $pos = 0; + for ( $i = 0; $i < 256; ++$i ) { + $pos += $counts[$i]; + $starts[$i] = $pos; + } + + // Excessively clever and indulgent code to simultaneously fill $packedTables + // with the packed hashtables, and adjust the elements of $starts + // to actually point to the starts instead of the ends. + $packedTables = array_fill( 0, $this->numentries, false ); + foreach ( $this->hplist as $item ) { + $packedTables[--$starts[255 & $item['h']]] = $item; + } + + $final = ''; + for ( $i = 0; $i < 256; ++$i ) { + $count = $counts[$i]; + + // The size of the hashtable will be double the item count. + // The rest of the slots will be empty. + $len = $count + $count; + $final .= pack( 'VV', $this->pos, $len ); + + $hashtable = array(); + for ( $u = 0; $u < $len; ++$u ) { + $hashtable[$u] = array( 'h' => 0, 'p' => 0 ); + } + + // Fill the hashtable, using the next empty slot if the hashed slot + // is taken. + for ( $u = 0; $u < $count; ++$u ) { + $hp = $packedTables[$starts[$i] + $u]; + $where = CdbFunctions::unsignedMod( + CdbFunctions::unsignedShiftRight( $hp['h'], 8 ), $len ); + while ( $hashtable[$where]['p'] ) + if ( ++$where == $len ) + $where = 0; + $hashtable[$where] = $hp; + } + + // Write the hashtable + for ( $u = 0; $u < $len; ++$u ) { + $buf = pack( 'vvV', + $hashtable[$u]['h'] & 0xffff, + CdbFunctions::unsignedShiftRight( $hashtable[$u]['h'], 16 ), + $hashtable[$u]['p'] ); + $this->write( $buf ); + $this->posplus( 8 ); + } + } + + // Write the pointer array at the start of the file + rewind( $this->handle ); + if ( ftell( $this->handle ) != 0 ) { + throw new MWException( __METHOD__.': Error rewinding to start of file' ); + } + $this->write( $final ); + } +} diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php index de804c5c..8dce679b 100644 --- a/includes/ChangeTags.php +++ b/includes/ChangeTags.php @@ -1,56 +1,63 @@ <?php -if (!defined( 'MEDIAWIKI' )) +if( !defined( 'MEDIAWIKI' ) ) die; class ChangeTags { static function formatSummaryRow( $tags, $page ) { - if (!$tags) - return array('',array()); + if( !$tags ) + return array( '', array() ); $classes = array(); - + $tags = explode( ',', $tags ); $displayTags = array(); foreach( $tags as $tag ) { - $displayTags[] = self::tagDescription( $tag ); - $classes[] = "mw-tag-$tag"; + $displayTags[] = Xml::tags( + 'span', + array( 'class' => 'mw-tag-marker ' . + Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ), + self::tagDescription( $tag ) + ); + $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" ); } - return array( '(' . implode( ', ', $displayTags ) . ')', $classes ); + $markers = '(' . implode( ', ', $displayTags ) . ')'; + $markers = Xml::tags( 'span', array( 'class' => 'mw-tag-markers' ), $markers ); + return array( $markers, $classes ); } static function tagDescription( $tag ) { $msg = wfMsgExt( "tag-$tag", 'parseinline' ); if ( wfEmptyMsg( "tag-$tag", $msg ) ) { - return htmlspecialchars($tag); + return htmlspecialchars( $tag ); } return $msg; } ## Basic utility method to add tags to a particular change, given its rc_id, rev_id and/or log_id. - static function addTags( $tags, $rc_id=null, $rev_id=null, $log_id=null, $params = null ) { - if ( !is_array($tags) ) { + static function addTags( $tags, $rc_id = null, $rev_id = null, $log_id = null, $params = null ) { + if ( !is_array( $tags ) ) { $tags = array( $tags ); } $tags = array_filter( $tags ); // Make sure we're submitting all tags... - if (!$rc_id && !$rev_id && !$log_id) { + if( !$rc_id && !$rev_id && !$log_id ) { throw new MWException( "At least one of: RCID, revision ID, and log ID MUST be specified when adding a tag to a change!" ); } $dbr = wfGetDB( DB_SLAVE ); // Might as well look for rcids and so on. - if (!$rc_id) { + if( !$rc_id ) { $dbr = wfGetDB( DB_MASTER ); // Info might be out of date, somewhat fractionally, on slave. - if ($log_id) { + if( $log_id ) { $rc_id = $dbr->selectField( 'recentchanges', 'rc_id', array( 'rc_logid' => $log_id ), __METHOD__ ); - } elseif ($rev_id) { + } elseif( $rev_id ) { $rc_id = $dbr->selectField( 'recentchanges', 'rc_id', array( 'rc_this_oldid' => $rev_id ), __METHOD__ ); } - } elseif (!$log_id && !$rev_id) { + } elseif( !$log_id && !$rev_id ) { $dbr = wfGetDB( DB_MASTER ); // Info might be out of date, somewhat fractionally, on slave. $log_id = $dbr->selectField( 'recentchanges', 'rc_logid', array( 'rc_id' => $rc_id ), __METHOD__ ); $rev_id = $dbr->selectField( 'recentchanges', 'rc_this_oldid', array( 'rc_id' => $rc_id ), __METHOD__ ); @@ -63,8 +70,8 @@ class ChangeTags { $prevTags = $prevTags ? $prevTags : ''; $prevTags = array_filter( explode( ',', $prevTags ) ); $newTags = array_unique( array_merge( $prevTags, $tags ) ); - sort($prevTags); - sort($newTags); + sort( $prevTags ); + sort( $newTags ); if ( $prevTags == $newTags ) { // No change. @@ -72,15 +79,28 @@ class ChangeTags { } $dbw = wfGetDB( DB_MASTER ); - $dbw->replace( 'tag_summary', array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ), array_filter( array_merge( $tsConds, array( 'ts_tags' => implode( ',', $newTags ) ) ) ), __METHOD__ ); + $dbw->replace( + 'tag_summary', + array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ), + array_filter( array_merge( $tsConds, array( 'ts_tags' => implode( ',', $newTags ) ) ) ), + __METHOD__ + ); // Insert the tags rows. $tagsRows = array(); foreach( $tags as $tag ) { // Filter so we don't insert NULLs as zero accidentally. - $tagsRows[] = array_filter( array( 'ct_tag' => $tag, 'ct_rc_id' => $rc_id, 'ct_log_id' => $log_id, 'ct_rev_id' => $rev_id, 'ct_params' => $params ) ); + $tagsRows[] = array_filter( + array( + 'ct_tag' => $tag, + 'ct_rc_id' => $rc_id, + 'ct_log_id' => $log_id, + 'ct_rev_id' => $rev_id, + 'ct_params' => $params + ) + ); } - $dbw->insert( 'change_tag', $tagsRows, __METHOD__, array('IGNORE') ); + $dbw->insert( 'change_tag', $tagsRows, __METHOD__, array( 'IGNORE' ) ); return true; } @@ -89,38 +109,40 @@ class ChangeTags { * Applies all tags-related changes to a query. * Handles selecting tags, and filtering. * Needs $tables to be set up properly, so we can figure out which join conditions to use. - */ + */ static function modifyDisplayQuery( &$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag = false ) { global $wgRequest, $wgUseTagFilter; - - if ($filter_tag === false) { + + if( $filter_tag === false ) { $filter_tag = $wgRequest->getVal( 'tagfilter' ); } // Figure out which conditions can be done. $join_field = ''; - if ( in_array('recentchanges', $tables) ) { + if ( in_array( 'recentchanges', $tables ) ) { $join_cond = 'rc_id'; - } elseif( in_array('logging', $tables) ) { + } elseif( in_array( 'logging', $tables ) ) { $join_cond = 'log_id'; - } elseif ( in_array('revision', $tables) ) { + } elseif ( in_array( 'revision', $tables ) ) { $join_cond = 'rev_id'; } else { - throw new MWException( "Unable to determine appropriate JOIN condition for tagging." ); + throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' ); } // JOIN on tag_summary $tables[] = 'tag_summary'; $join_conds['tag_summary'] = array( 'LEFT JOIN', "ts_$join_cond=$join_cond" ); $fields[] = 'ts_tags'; - - if ($wgUseTagFilter && $filter_tag) { + + if( $wgUseTagFilter && $filter_tag ) { // Somebody wants to filter on a tag. // Add an INNER JOIN on change_tag // FORCE INDEX -- change_tags will almost ALWAYS be the correct query plan. - $options['USE INDEX'] = array( 'change_tag' => 'change_tag_tag_id' ); + global $wgOldChangeTagsIndex; + $index = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id'; + $options['USE INDEX'] = array( 'change_tag' => $index ); unset( $options['FORCE INDEX'] ); $tables[] = 'change_tag'; $join_conds['change_tag'] = array( 'INNER JOIN', "ct_$join_cond=$join_cond" ); @@ -134,15 +156,15 @@ class ChangeTags { */ static function buildTagFilterSelector( $selected='', $fullForm = false /* used to put a full form around the selector */ ) { global $wgUseTagFilter; - + if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) ) return $fullForm ? '' : array(); - + global $wgTitle; - + $data = array( wfMsgExt( 'tag-filter', 'parseinline' ), Xml::input( 'tagfilter', 20, $selected ) ); - if (!$fullForm) { + if ( !$fullForm ) { return $data; } @@ -160,9 +182,9 @@ class ChangeTags { global $wgMemc; $key = wfMemcKey( 'valid-tags' ); - if ($tags = $wgMemc->get( $key )) + if ( $tags = $wgMemc->get( $key ) ) return $tags; - + $emptyTags = array(); // Some DB stuff @@ -171,8 +193,8 @@ class ChangeTags { while( $row = $res->fetchObject() ) { $emptyTags[] = $row->vt_tag; } - - wfRunHooks( 'ListDefinedTags', array(&$emptyTags) ); + + wfRunHooks( 'ListDefinedTags', array( &$emptyTags ) ); $emptyTags = array_filter( array_unique( $emptyTags ) ); diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php index a0c2767a..bc50fe02 100644 --- a/includes/ChangesFeed.php +++ b/includes/ChangesFeed.php @@ -1,14 +1,31 @@ <?php +/** + * Feed to Special:RecentChanges and Special:RecentChangesLiked + * + * @ingroup Feed + */ class ChangesFeed { - public $format, $type, $titleMsg, $descMsg; + /** + * Constructor + * + * @param $format String: feed's format (either 'rss' or 'atom') + * @param $type String: type of feed (for cache keys) + */ public function __construct( $format, $type ) { $this->format = $format; $this->type = $type; } + /** + * Get a ChannelFeed subclass object to use + * + * @param $title String: feed's title + * @param $description String: feed's description + * @return ChannelFeed subclass or false on failure + */ public function getFeedObject( $title, $description ) { global $wgSitename, $wgContLanguageCode, $wgFeedClasses, $wgTitle; $feedTitle = "$wgSitename - {$title} [$wgContLanguageCode]"; @@ -18,16 +35,26 @@ class ChangesFeed { $feedTitle, htmlspecialchars( $description ), $wgTitle->getFullUrl() ); } - public function execute( $feed, $rows, $limit=0, $hideminor=false, $lastmod=false, $target='' ) { + /** + * Generates feed's content + * + * @param $feed ChannelFeed subclass object (generally the one returned by getFeedObject()) + * @param $rows ResultWrapper object with rows in recentchanges table + * @param $lastmod Integer: timestamp of the last item in the recentchanges table (only used for the cache key) + * @param $opts FormOptions as in SpecialRecentChanges::getDefaultOptions() + * @return null or true + */ + public function execute( $feed, $rows, $lastmod, $opts ) { global $messageMemc, $wgFeedCacheTimeout; - global $wgSitename, $wgContLanguageCode; + global $wgSitename, $wgLang; if ( !FeedUtils::checkFeedOutput( $this->format ) ) { return; } $timekey = wfMemcKey( $this->type, $this->format, 'timestamp' ); - $key = wfMemcKey( $this->type, $this->format, $limit, $hideminor, $target ); + $optionsHash = md5( serialize( $opts->getAllValues() ) ); + $key = wfMemcKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash ); FeedUtils::checkPurge($timekey, $key); @@ -52,13 +79,28 @@ class ChangesFeed { return true; } + /** + * Save to feed result to $messageMemc + * + * @param $feed String: feed's content + * @param $timekey String: memcached key of the last modification + * @param $key String: memcached key of the content + */ public function saveToCache( $feed, $timekey, $key ) { global $messageMemc; $expire = 3600 * 24; # One day - $messageMemc->set( $key, $feed ); + $messageMemc->set( $key, $feed, $expire ); $messageMemc->set( $timekey, wfTimestamp( TS_MW ), $expire ); } + /** + * Try to load the feed result from $messageMemc + * + * @param $lastmod Integer: timestamp of the last item in the recentchanges table + * @param $timekey String: memcached key of the last modification + * @param $key String: memcached key of the content + * @return feed's content on cache hit or false on cache miss + */ public function loadFromCache( $lastmod, $timekey, $key ) { global $wgFeedCacheTimeout, $messageMemc; $feedLastmod = $messageMemc->get( $timekey ); @@ -86,10 +128,10 @@ class ChangesFeed { } /** - * Generate the feed items given a row from the database. - * @param $rows Database resource with recentchanges rows - * @param $feed Feed object - */ + * Generate the feed items given a row from the database. + * @param $rows DatabaseBase resource with recentchanges rows + * @param $feed Feed object + */ public static function generateFeed( $rows, &$feed ) { wfProfileIn( __METHOD__ ); @@ -113,18 +155,20 @@ class ChangesFeed { foreach( $sorted as $obj ) { $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title ); $talkpage = $title->getTalkPage(); + // Skip items with deleted content (avoids partially complete/inconsistent output) + if( $obj->rc_deleted ) continue; $item = new FeedItem( $title->getPrefixedText(), FeedUtils::formatDiff( $obj ), - $title->getFullURL( 'diff=' . $obj->rc_this_oldid . '&oldid=prev' ), + $obj->rc_this_oldid ? $title->getFullURL( 'diff=' . $obj->rc_this_oldid . '&oldid=prev' ) : $title->getFullURL(), $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 4eda1dbd..9f092991 100644 --- a/includes/ChangesList.php +++ b/includes/ChangesList.php @@ -25,13 +25,14 @@ class RCCacheEntry extends RecentChange { class ChangesList { # Called by history lists and recent changes public $skin; + protected $watchlist = false; /** * Changeslist contructor - * @param Skin $skin + * @param $skin Skin */ - public function __construct( &$skin ) { - $this->skin =& $skin; + public function __construct( $skin ) { + $this->skin = $skin; $this->preCacheMessages(); } @@ -44,7 +45,7 @@ class ChangesList { */ public static function newFromUser( &$user ) { $sk = $user->getSkin(); - $list = NULL; + $list = null; if( wfRunHooks( 'FetchChangesList', array( &$user, &$sk, &$list ) ) ) { return $user->getOption( 'usenewrc' ) ? new EnhancedChangesList( $sk ) : new OldChangesList( $sk ); @@ -52,6 +53,14 @@ class ChangesList { return $list; } } + + /** + * Sets the list to use a <li class="watchlist-(namespace)-(page)"> tag + * @param $value Boolean + */ + public function setWatchlistDivs( $value = true ) { + $this->watchlist = $value; + } /** * As we use the same small set of messages in various methods and that @@ -59,8 +68,8 @@ class ChangesList { */ private function preCacheMessages() { if( !isset( $this->message ) ) { - foreach( explode(' ', 'cur diff hist minoreditletter newpageletter last '. - 'blocklink history boteditletter semicolon-separator' ) as $msg ) { + foreach ( explode( ' ', 'cur diff hist last blocklink history ' . + 'semicolon-separator pipe-separator' ) as $msg ) { $this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) ); } } @@ -69,26 +78,95 @@ 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 + * @param $new Boolean + * @param $minor Boolean + * @param $patrolled Boolean + * @param $nothing String to use for empty space + * @param $bot Boolean + * @return String */ 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>' : $nothing; - $f .= $bot ? '<span class="bot">' . $this->message['boteditletter'] . '</span>' : $nothing; - $f .= $patrolled ? '<span class="unpatrolled">!</span>' : $nothing; + $f = $new ? self::flag( 'newpage' ) : $nothing; + $f .= $minor ? self::flag( 'minor' ) : $nothing; + $f .= $bot ? self::flag( 'bot' ) : $nothing; + $f .= $patrolled ? self::flag( 'unpatrolled' ) : $nothing; return $f; } /** + * Provide the <abbr> element appropriate to a given abbreviated flag, + * namely the flag indicating a new page, a minor edit, a bot edit, or an + * unpatrolled edit. By default in English it will contain "N", "m", "b", + * "!" respectively, plus it will have an appropriate title and class. + * + * @param $key String: 'newpage', 'unpatrolled', 'minor', or 'bot' + * @return String: Raw HTML + */ + public static function flag( $key ) { + static $messages = null; + if ( is_null( $messages ) ) { + foreach ( explode( ' ', 'minoreditletter boteditletter newpageletter ' . + 'unpatrolledletter recentchanges-label-minor recentchanges-label-bot ' . + 'recentchanges-label-newpage recentchanges-label-unpatrolled' ) as $msg ) { + $messages[$msg] = wfMsgExt( $msg, 'escapenoentities' ); + } + } + # Inconsistent naming, bleh + if ( $key == 'newpage' || $key == 'unpatrolled' ) { + $key2 = $key; + } else { + $key2 = $key . 'edit'; + } + return "<abbr class=\"$key\" title=\"" + . $messages["recentchanges-label-$key"] . "\">" + . $messages["${key2}letter"] + . '</abbr>'; + } + + /** + * Some explanatory wrapper text for the given flag, to be used in a legend + * explaining what the flags mean. For instance, "N - new page". See + * also flag(). + * + * @param $key String: 'newpage', 'unpatrolled', 'minor', or 'bot' + * @return String: Raw HTML + */ + private static function flagLine( $key ) { + return wfMsgExt( "recentchanges-legend-$key", array( 'escapenoentities', + 'replaceafter' ), self::flag( $key ) ); + } + + /** + * A handy legend to tell users what the little "m", "b", and so on mean. + * + * @return String: Raw HTML + */ + public static function flagLegend() { + global $wgGroupPermissions, $wgLang; + + $flags = array( self::flagLine( 'newpage' ), + self::flagLine( 'minor' ) ); + + # Don't show info on bot edits unless there's a bot group of some kind + foreach ( $wgGroupPermissions as $rights ) { + if ( isset( $rights['bot'] ) && $rights['bot'] ) { + $flags[] = self::flagLine( 'bot' ); + break; + } + } + + if ( self::usePatrol() ) { + $flags[] = self::flagLine( 'unpatrolled' ); + } + + return '<div class="mw-rc-label-legend">' . + wfMsgExt( 'recentchanges-label-legend', 'parseinline', + $wgLang->commaList( $flags ) ) . '</div>'; + } + + /** * Returns text for the start of the tabular part of RC - * @return string + * @return String */ public function beginRecentChangesList() { $this->rc_cache = array(); @@ -101,14 +179,26 @@ class ChangesList { /** * Show formatted char difference - * @param int $old bytes - * @param int $new bytes - * @returns string + * @param $old Integer: bytes + * @param $new Integer: bytes + * @returns String */ public static function showCharacterDifference( $old, $new ) { - global $wgRCChangedSizeThreshold, $wgLang; + global $wgRCChangedSizeThreshold, $wgLang, $wgMiserMode; $szdiff = $new - $old; - $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape' ), $wgLang->formatNum( $szdiff ) ); + + $code = $wgLang->getCode(); + static $fastCharDiff = array(); + if ( !isset($fastCharDiff[$code]) ) { + $fastCharDiff[$code] = $wgMiserMode || wfMsgNoTrans( 'rc-change-size' ) === '$1'; + } + + $formatedSize = $wgLang->formatNum($szdiff); + + if ( !$fastCharDiff[$code] ) { + $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape' ), $formatedSize ); + } + if( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) { $tag = 'strong'; } else { @@ -125,7 +215,7 @@ class ChangesList { /** * Returns text for the end of RC - * @return string + * @return String */ public function endRecentChangesList() { if( $this->rclistOpen ) { @@ -135,73 +225,130 @@ class ChangesList { } } - protected function insertMove( &$s, $rc ) { + public function insertMove( &$s, $rc ) { # Diff $s .= '(' . $this->message['diff'] . ') ('; # Hist - $s .= $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), $this->message['hist'], - 'action=history' ) . ') . . '; + $s .= $this->skin->link( + $rc->getMovedToTitle(), + $this->message['hist'], + array(), + array( 'action' => 'history' ), + array( 'known', 'noclasses' ) + ) . ') . . '; # "[[x]] moved to [[y]]" $msg = ( $rc->mAttribs['rc_type'] == RC_MOVE ) ? '1movedto2' : '1movedto2_redir'; - $s .= wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ), - $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) ); + $s .= wfMsg( + $msg, + $this->skin->link( + $rc->getTitle(), + null, + array(), + array( 'redirect' => 'no' ), + array( 'known', 'noclasses' ) + ), + $this->skin->link( + $rc->getMovedToTitle(), + null, + array(), + array(), + array( 'known', 'noclasses' ) + ) + ); } - protected function insertDateHeader( &$s, $rc_timestamp ) { + public function insertDateHeader( &$s, $rc_timestamp ) { global $wgLang; # Make date header if necessary $date = $wgLang->date( $rc_timestamp, true, true ); if( $date != $this->lastdate ) { - if( '' != $this->lastdate ) { + if( $this->lastdate != '' ) { $s .= "</ul>\n"; } - $s .= '<h4>'.$date."</h4>\n<ul class=\"special\">"; + $s .= Xml::element( 'h4', null, $date ) . "\n<ul class=\"special\">"; $this->lastdate = $date; $this->rclistOpen = true; } } - protected function insertLog( &$s, $title, $logtype ) { + public function insertLog( &$s, $title, $logtype ) { $logname = LogPage::logName( $logtype ); - $s .= '(' . $this->skin->makeKnownLinkObj($title, $logname ) . ')'; + $s .= '(' . $this->skin->link( + $title, + $logname, + array(), + array(), + array( 'known', 'noclasses' ) + ) . ')'; } - protected function insertDiffHist( &$s, &$rc, $unpatrolled ) { + public function insertDiffHist( &$s, &$rc, $unpatrolled ) { # Diff link if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) { $diffLink = $this->message['diff']; - } else if( !$this->userCan($rc,Revision::DELETED_TEXT) ) { + } else if( !self::userCan($rc,Revision::DELETED_TEXT) ) { $diffLink = $this->message['diff']; } else { - $rcidparam = $unpatrolled ? array( 'rcid' => $rc->mAttribs['rc_id'] ) : array(); - $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'], - wfArrayToCGI( array( - 'curid' => $rc->mAttribs['rc_cur_id'], - 'diff' => $rc->mAttribs['rc_this_oldid'], - 'oldid' => $rc->mAttribs['rc_last_oldid'] ), - $rcidparam ), - '', '', ' tabindex="'.$rc->counter.'"'); - } - $s .= '('.$diffLink.') ('; + $query = array( + 'curid' => $rc->mAttribs['rc_cur_id'], + 'diff' => $rc->mAttribs['rc_this_oldid'], + 'oldid' => $rc->mAttribs['rc_last_oldid'] + ); + + if( $unpatrolled ) { + $query['rcid'] = $rc->mAttribs['rc_id']; + }; + + $diffLink = $this->skin->link( + $rc->getTitle(), + $this->message['diff'], + array( 'tabindex' => $rc->counter ), + $query, + array( 'known', 'noclasses' ) + ); + } + $s .= '(' . $diffLink . $this->message['pipe-separator']; # History link - $s .= $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['hist'], - wfArrayToCGI( array( + $s .= $this->skin->link( + $rc->getTitle(), + $this->message['hist'], + array(), + array( 'curid' => $rc->mAttribs['rc_cur_id'], - 'action' => 'history' ) ) ); + 'action' => 'history' + ), + array( 'known', 'noclasses' ) + ); $s .= ') . . '; } - protected function insertArticleLink( &$s, &$rc, $unpatrolled, $watched ) { + public function insertArticleLink( &$s, &$rc, $unpatrolled, $watched ) { global $wgContLang; # 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'] : ''; + $params = array(); + + if ( $unpatrolled && $rc->mAttribs['rc_type'] == RC_NEW ) { + $params['rcid'] = $rc->mAttribs['rc_id']; + } + if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) { - $articlelink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params ); - $articlelink = '<span class="history-deleted">'.$articlelink.'</span>'; + $articlelink = $this->skin->link( + $rc->getTitle(), + null, + array(), + $params, + array( 'known', 'noclasses' ) + ); + $articlelink = '<span class="history-deleted">' . $articlelink . '</span>'; } else { - $articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params ); + $articlelink = ' '. $this->skin->link( + $rc->getTitle(), + null, + array(), + $params, + array( 'known', 'noclasses' ) + ); } # Bolden pages watched by this user if( $watched ) { @@ -216,7 +363,7 @@ class ChangesList { $s .= " $articlelink"; } - protected function insertTimestamp( &$s, $rc ) { + public function insertTimestamp( &$s, $rc ) { global $wgLang; $s .= $this->message['semicolon-separator'] . $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; @@ -233,7 +380,7 @@ class ChangesList { } /** insert a formatted action */ - protected function insertAction( &$s, &$rc ) { + public function insertAction( &$s, &$rc ) { if( $rc->mAttribs['rc_type'] == RC_LOG ) { if( $this->isDeleted( $rc, LogPage::DELETED_ACTION ) ) { $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-event' ) . '</span>'; @@ -245,7 +392,7 @@ class ChangesList { } /** insert a formatted comment */ - protected function insertComment( &$s, &$rc ) { + public function insertComment( &$s, &$rc ) { if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) { if( $this->isDeleted( $rc, Revision::DELETED_COMMENT ) ) { $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span>'; @@ -257,7 +404,7 @@ class ChangesList { /** * Check whether to enable recent changes patrol features - * @return bool + * @return Boolean */ public static function usePatrol() { global $wgUser; @@ -283,9 +430,9 @@ class ChangesList { /** * Determine if said field of a revision is hidden - * @param RCCacheEntry $rc - * @param int $field one of DELETED_* bitfield constants - * @return bool + * @param $rc RCCacheEntry + * @param $field Integer: one of DELETED_* bitfield constants + * @return Boolean */ public static function isDeleted( $rc, $field ) { return ( $rc->mAttribs['rc_deleted'] & $field ) == $field; @@ -294,24 +441,19 @@ class ChangesList { /** * 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 + * @param $rc RCCacheEntry + * @param $field Integer + * @return Boolean */ 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 ); + if( $rc->mAttribs['rc_type'] == RC_LOG ) { + return LogEventsList::userCanBitfield( $rc->mAttribs['rc_deleted'], $field ); } else { - return true; + return Revision::userCanBitfield( $rc->mAttribs['rc_deleted'], $field ); } } - protected function maybeWatchedLink( $link, $watched=false ) { + protected function maybeWatchedLink( $link, $watched = false ) { if( $watched ) { return '<strong class="mw-watched">' . $link . '</strong>'; } else { @@ -320,7 +462,7 @@ class ChangesList { } /** Inserts a rollback link */ - protected function insertRollback( &$s, &$rc ) { + public function insertRollback( &$s, &$rc ) { global $wgUser; if( !$rc->mAttribs['rc_new'] && $rc->mAttribs['rc_this_oldid'] && $rc->mAttribs['rc_cur_id'] ) { $page = $rc->getTitle(); @@ -340,7 +482,7 @@ class ChangesList { } } - protected function insertTags( &$s, &$rc, &$classes ) { + public function insertTags( &$s, &$rc, &$classes ) { if ( empty($rc->mAttribs['ts_tags']) ) return; @@ -349,7 +491,7 @@ class ChangesList { $s .= ' ' . $tagSummary; } - protected function insertExtra( &$s, &$rc, &$classes ) { + public function insertExtra( &$s, &$rc, &$classes ) { ## Empty, used for subclassers to add anything special. } } @@ -362,8 +504,8 @@ class OldChangesList extends ChangesList { /** * Format a line using the old system (aka without any javascript). */ - public function recentChangesLine( &$rc, $watched = false, $linenumber = NULL ) { - global $wgContLang, $wgLang, $wgRCShowChangedSize, $wgUser; + public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) { + global $wgLang, $wgRCShowChangedSize, $wgUser; wfProfileIn( __METHOD__ ); # Should patrol-related stuff be shown? $unpatrolled = $wgUser->useRCPatrol() && !$rc->mAttribs['rc_patrolled']; @@ -426,20 +568,20 @@ class OldChangesList extends ChangesList { # For subclasses $this->insertExtra( $s, $rc, $classes ); - # Mark revision as deleted if so - if( !$rc->mAttribs['rc_log_type'] && $this->isDeleted($rc,Revision::DELETED_TEXT) ) { - $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>'; - } # How many users watch this page if( $rc->numberofWatchingusers > 0 ) { $s .= ' ' . wfMsgExt( 'number_of_watching_users_RCview', array( 'parsemag', 'escape' ), $wgLang->formatNum( $rc->numberofWatchingusers ) ); } - + + if( $this->watchlist ) { + $classes[] = Sanitizer::escapeClass( 'watchlist-'.$rc->mAttribs['rc_namespace'].'-'.$rc->mAttribs['rc_title'] ); + } + wfRunHooks( 'OldChangesListRecentChangesLine', array(&$this, &$s, $rc) ); wfProfileOut( __METHOD__ ); - return "$dateheader<li class=\"".implode( ' ', $classes )."\">$s</li>\n"; + return "$dateheader<li class=\"".implode( ' ', $classes )."\">".$s."</li>\n"; } } @@ -449,26 +591,24 @@ class OldChangesList extends ChangesList { */ class EnhancedChangesList extends ChangesList { /** - * Add the JavaScript file for enhanced changeslist - * @ return string - */ + * Add the JavaScript file for enhanced changeslist + * @return String + */ public function beginRecentChangesList() { - global $wgStylePath, $wgJsMimeType, $wgStyleVersion; + global $wgStylePath, $wgStyleVersion; $this->rc_cache = array(); $this->rcMoveIndex = 0; $this->rcCacheIndex = 0; $this->lastdate = ''; $this->rclistOpen = false; - $script = Xml::tags( 'script', array( - 'type' => $wgJsMimeType, - 'src' => $wgStylePath . "/common/enhancedchanges.js?$wgStyleVersion" ), '' ); + $script = Html::linkedScript( $wgStylePath . "/common/enhancedchanges.js?$wgStyleVersion" ); return $script; } /** * Format a line for enhanced recentchange (aka with javascript and block of lines). */ public function recentChangesLine( &$baseRC, $watched = false ) { - global $wgLang, $wgContLang, $wgUser; + global $wgLang, $wgUser; wfProfileIn( __METHOD__ ); @@ -479,7 +619,7 @@ class EnhancedChangesList extends ChangesList { // FIXME: Would be good to replace this extract() call with something // that explicitly initializes variables. extract( $rc->mAttribs ); - $curIdEq = 'curid=' . $rc_cur_id; + $curIdEq = array( 'curid' => $rc_cur_id ); # If it's a new day, add the headline and flush the cache $date = $wgLang->date( $rc_timestamp, true ); @@ -488,7 +628,7 @@ class EnhancedChangesList extends ChangesList { # Process current cache $ret = $this->recentChangesBlock(); $this->rc_cache = array(); - $ret .= "<h4>{$date}</h4>\n"; + $ret .= Xml::element( 'h4', null, $date ); $this->lastdate = $date; } @@ -504,19 +644,21 @@ class EnhancedChangesList extends ChangesList { // 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(), '' ) ); + $clink = wfMsg( $msg, $this->skin->linkKnown( $rc->getTitle(), null, + array(), array( 'redirect' => 'no' ) ), + $this->skin->linkKnown( $rc->getMovedToTitle() ) ); // New unpatrolled pages } else if( $rc->unpatrolled && $rc_type == RC_NEW ) { - $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', "rcid={$rc_id}" ); + $clink = $this->skin->linkKnown( $rc->getTitle(), null, array(), + array( '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, + $clink = '(' . $this->skin->linkKnown( $logtitle, LogPage::logName($rc_log_type) ) . ')'; } else { - $clink = $this->skin->makeLinkObj( $rc->getTitle(), '' ); + $clink = $this->skin->link( $rc->getTitle() ); } $watched = false; // Log entries (old format) and special pages @@ -525,14 +667,14 @@ class EnhancedChangesList extends ChangesList { if ( $specialName == 'Log' ) { # Log updates, etc $logname = LogPage::logName( $logtype ); - $clink = '(' . $this->skin->makeKnownLinkObj( $rc->getTitle(), $logname ) . ')'; + $clink = '(' . $this->skin->linkKnown( $rc->getTitle(), $logname ) . ')'; } else { wfDebug( "Unexpected special page in recentchanges\n" ); $clink = ''; } // Edits } else { - $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '' ); + $clink = $this->skin->linkKnown( $rc->getTitle() ); } # Don't show unusable diff links @@ -540,44 +682,49 @@ class EnhancedChangesList extends ChangesList { $showdifflinks = false; } - $time = $wgContLang->time( $rc_timestamp, true, true ); + $time = $wgLang->time( $rc_timestamp, true, true ); $rc->watched = $watched; $rc->link = $clink; $rc->timestamp = $time; $rc->numberofWatchingusers = $baseRC->numberofWatchingusers; - # Make "cur" and "diff" links + # Make "cur" and "diff" links. Do not use link(), it is too slow if + # called too many times (50% of CPU time on RecentChanges!). if( $rc->unpatrolled ) { - $rcIdQuery = "&rcid={$rc_id}"; + $rcIdQuery = array( 'rcid' => $rc_id ); } else { - $rcIdQuery = ''; + $rcIdQuery = array(); } - $querycur = $curIdEq."&diff=0&oldid=$rc_this_oldid"; - $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 ); + $querycur = $curIdEq + array( 'diff' => '0', 'oldid' => $rc_this_oldid ); + $querydiff = $curIdEq + array( 'diff' => $rc_this_oldid, 'oldid' => + $rc_last_oldid ) + $rcIdQuery; - # Make "diff" an "cur" links if( !$showdifflinks ) { - $curLink = $this->message['cur']; - $diffLink = $this->message['diff']; + $curLink = $this->message['cur']; + $diffLink = $this->message['diff']; } else if( in_array( $rc_type, array(RC_NEW,RC_LOG,RC_MOVE,RC_MOVE_OVER_REDIRECT) ) ) { - $curLink = ($rc_type != RC_NEW) ? $this->message['cur'] : $curLink; + if ( $rc_type != RC_NEW ) { + $curLink = $this->message['cur']; + } else { + $curUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querycur ) ); + $curLink = "<a href=\"$curUrl\" tabindex=\"{$baseRC->counter}\">{$this->message['cur']}</a>"; + } $diffLink = $this->message['diff']; } else { - $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'], - $querydiff, '' ,'', $aprops ); + $diffUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querydiff ) ); + $curUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querycur ) ); + $diffLink = "<a href=\"$diffUrl\" tabindex=\"{$baseRC->counter}\">{$this->message['diff']}</a>"; + $curLink = "<a href=\"$curUrl\" tabindex=\"{$baseRC->counter}\">{$this->message['cur']}</a>"; } # Make "last" link if( !$showdifflinks || !$rc_last_oldid ) { - $lastLink = $this->message['last']; + $lastLink = $this->message['last']; } else if( $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 ); + $lastLink = $this->skin->linkKnown( $rc->getTitle(), $this->message['last'], + array(), $curIdEq + array('diff' => $rc_this_oldid, 'oldid' => $rc_last_oldid) + $rcIdQuery ); } # Make user links @@ -624,7 +771,7 @@ class EnhancedChangesList extends ChangesList { wfProfileIn( __METHOD__ ); - $r = '<table cellpadding="0" cellspacing="0" border="0" style="background: none"><tr>'; + $r = '<table class="mw-enhanced-rc"><tr>'; # Collate list of users $userlinks = array(); @@ -694,13 +841,13 @@ class EnhancedChangesList extends ChangesList { $tl = "<span id='mw-rc-openarrow-$jsid' class='mw-changeslist-expanded' style='visibility:hidden'><a href='#' $toggleLink title='$expandTitle'>" . $this->sideArrow() . "</a></span>"; $tl .= "<span id='mw-rc-closearrow-$jsid' class='mw-changeslist-hidden' style='display:none'><a href='#' $toggleLink title='$closeTitle'>" . $this->downArrow() . "</a></span>"; - $r .= '<td valign="top" style="white-space: nowrap"><tt>'.$tl.' '; + $r .= '<td class="mw-enhanced-rc">'.$tl.' '; # Main line $r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, ' ', $bot ); # Timestamp - $r .= ' '.$block[0]->timestamp.' </tt></td><td>'; + $r .= ' '.$block[0]->timestamp.' </td><td style="padding:0px;">'; # Article link if( $namehidden ) { @@ -713,7 +860,7 @@ class EnhancedChangesList extends ChangesList { $r .= $wgContLang->getDirMark(); - $curIdEq = 'curid=' . $curId; + $queryParams['curid'] = $curId; # Changes message $n = count($block); static $nchanges = array(); @@ -729,8 +876,17 @@ class EnhancedChangesList extends ChangesList { } else if( $isnew ) { $r .= $nchanges[$n]; } else { - $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(), - $nchanges[$n], $curIdEq."&diff=$currentRevision&oldid=$oldid" ); + $params = $queryParams; + $params['diff'] = $currentRevision; + $params['oldid'] = $oldid; + + $r .= $this->skin->link( + $block[0]->getTitle(), + $nchanges[$n], + array(), + $params, + array( 'known', 'noclasses' ) + ); } } @@ -738,10 +894,19 @@ class EnhancedChangesList extends ChangesList { if( $allLogs ) { // don't show history link for logs } else if( $namehidden || !$block[0]->getTitle()->exists() ) { - $r .= $this->message['semicolon-separator'] . $this->message['hist'] . ')'; + $r .= $this->message['pipe-separator'] . $this->message['hist'] . ')'; } else { - $r .= $this->message['semicolon-separator'] . $this->skin->makeKnownLinkObj( $block[0]->getTitle(), - $this->message['hist'], $curIdEq . '&action=history' ) . ')'; + $params = $queryParams; + $params['action'] = 'history'; + + $r .= $this->message['pipe-separator'] . + $this->skin->link( + $block[0]->getTitle(), + $this->message['hist'], + array(), + $params, + array( 'known', 'noclasses' ) + ) . ')'; } $r .= ' . . '; @@ -750,10 +915,10 @@ class EnhancedChangesList extends ChangesList { $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 ) { + while( $last < $first && $block[$last]->mAttribs['rc_new_len'] === null ) { $last++; } - while( $first > $last && $block[$first]->mAttribs['rc_old_len'] === NULL ) { + while( $first > $last && $block[$first]->mAttribs['rc_old_len'] === null ) { $first--; } # Get net change @@ -774,7 +939,7 @@ class EnhancedChangesList extends ChangesList { # Sub-entries $r .= '<div id="mw-rc-subentries-'.$jsid.'" class="mw-changeslist-hidden">'; - $r .= '<table cellpadding="0" cellspacing="0" border="0" style="background: none">'; + $r .= '<table class="mw-enhanced-rc">'; foreach( $block as $rcObj ) { # Extract fields from DB into the function scope (rc_xxxx variables) // FIXME: Would be good to replace this extract() call with something @@ -784,35 +949,44 @@ class EnhancedChangesList extends ChangesList { extract( $rcObj->mAttribs ); #$r .= '<tr><td valign="top">'.$this->spacerArrow(); - $r .= '<tr><td valign="top">'; - $r .= '<tt>'.$this->spacerIndent() . $this->spacerIndent(); + $r .= '<tr><td style="vertical-align:top;font-family:monospace; padding:0px;">'; + $r .= $this->spacerIndent() . $this->spacerIndent(); $r .= $this->recentChangesFlags( $rc_new, $rc_minor, $rcObj->unpatrolled, ' ', $rc_bot ); - $r .= ' </tt></td><td valign="top">'; + $r .= ' </td><td style="vertical-align:top; padding:0px;"><span style="font-family:monospace">'; + + $params = $queryParams; - $o = ''; if( $rc_this_oldid != 0 ) { - $o = 'oldid='.$rc_this_oldid; + $params['oldid'] = $rc_this_oldid; } + # Log timestamp if( $rc_type == RC_LOG ) { - $link = '<tt>'.$rcObj->timestamp.'</tt> '; + $link = $rcObj->timestamp; # Revision link } else if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) { - $link = '<span class="history-deleted"><tt>'.$rcObj->timestamp.'</tt></span> '; + $link = '<span class="history-deleted">'.$rcObj->timestamp.'</span> '; } else { - $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 ( $rcObj->unpatrolled && $rc_type == RC_NEW) { + $params['rcid'] = $rcObj->mAttribs['rc_id']; + } + + $link = $this->skin->link( + $rcObj->getTitle(), + $rcObj->timestamp, + array(), + $params, + array( 'known', 'noclasses' ) + ); if( $this->isDeleted($rcObj,Revision::DELETED_TEXT) ) $link = '<span class="history-deleted">'.$link.'</span> '; } - $r .= $link; + $r .= $link . '</span>'; if ( !$rc_type == RC_LOG || $rc_type == RC_NEW ) { $r .= ' ('; $r .= $rcObj->curlink; - $r .= $this->message['semicolon-separator']; + $r .= $this->message['pipe-separator']; $r .= $rcObj->lastlink; $r .= ')'; } @@ -833,11 +1007,6 @@ class EnhancedChangesList extends ChangesList { $this->insertRollback( $r, $rcObj ); # Tags $this->insertTags( $r, $rcObj, $classes ); - - # Mark revision as deleted - if( !$rc_log_type && $this->isDeleted($rcObj,Revision::DELETED_TEXT) ) { - $r .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>'; - } $r .= "</td></tr>\n"; } @@ -852,10 +1021,10 @@ class EnhancedChangesList extends ChangesList { /** * Generate HTML for an arrow or placeholder graphic - * @param string $dir one of '', 'd', 'l', 'r' - * @param string $alt text - * @param string $title text - * @return string HTML <img> tag + * @param $dir String: one of '', 'd', 'l', 'r' + * @param $alt String: text + * @param $title String: text + * @return String: HTML <img> tag */ protected function arrow( $dir, $alt='', $title='' ) { global $wgStylePath; @@ -868,7 +1037,7 @@ class EnhancedChangesList extends ChangesList { /** * Generate HTML for a right- or left-facing arrow, * depending on language direction. - * @return string HTML <img> tag + * @return String: HTML <img> tag */ protected function sideArrow() { global $wgContLang; @@ -879,7 +1048,7 @@ class EnhancedChangesList extends ChangesList { /** * Generate HTML for a down-facing arrow * depending on language direction. - * @return string HTML <img> tag + * @return String: HTML <img> tag */ protected function downArrow() { return $this->arrow( 'd', '-', wfMsg( 'rc-enhanced-hide' ) ); @@ -887,7 +1056,7 @@ class EnhancedChangesList extends ChangesList { /** * Generate HTML for a spacer image - * @return string HTML <img> tag + * @return String: HTML <img> tag */ protected function spacerArrow() { return $this->arrow( '', codepointToUtf8( 0xa0 ) ); // non-breaking space @@ -895,7 +1064,7 @@ class EnhancedChangesList extends ChangesList { /** * Add a set of spaces - * @return string HTML <td> tag + * @return String: HTML <td> tag */ protected function spacerIndent() { return ' '; @@ -903,10 +1072,10 @@ class EnhancedChangesList extends ChangesList { /** * Enhanced RC ungrouped line. - * @return string a HTML formated line (generated using $r) + * @return String: a HTML formated line (generated using $r) */ protected function recentChangesBlockLine( $rcObj ) { - global $wgContLang, $wgRCShowChangedSize; + global $wgRCShowChangedSize; wfProfileIn( __METHOD__ ); @@ -915,30 +1084,42 @@ class EnhancedChangesList extends ChangesList { // that explicitly initializes variables. $classes = array(); // TODO implement extract( $rcObj->mAttribs ); - $curIdEq = "curid={$rc_cur_id}"; + $query['curid'] = $rc_cur_id; - $r = '<table cellspacing="0" cellpadding="0" border="0" style="background: none"><tr>'; - $r .= '<td valign="top" style="white-space: nowrap"><tt>' . $this->spacerArrow() . ' '; + $r = '<table class="mw-enhanced-rc"><tr>'; + $r .= '<td class="mw-enhanced-rc">' . $this->spacerArrow() . ' '; # Flag and Timestamp if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { $r .= ' '; // 4 flags -> 4 spaces } else { $r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, ' ', $rc_bot ); } - $r .= ' '.$rcObj->timestamp.' </tt></td><td>'; + $r .= ' '.$rcObj->timestamp.' </td><td style="padding:0px;">'; # 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 ) . ')'; + $r .= '(' . $this->skin->link( + $logtitle, + $logname, + array(), + array(), + array( 'known', 'noclasses' ) + ) . ')'; } else { $this->insertArticleLink( $r, $rcObj, $rcObj->unpatrolled, $rcObj->watched ); } # Diff and hist links if ( $rc_type != RC_LOG ) { - $r .= ' ('. $rcObj->difflink . $this->message['semicolon-separator']; - $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), $this->message['hist'], - $curIdEq.'&action=history' ) . ')'; + $r .= ' ('. $rcObj->difflink . $this->message['pipe-separator']; + $query['action'] = 'history'; + $r .= $this->skin->link( + $rcObj->getTitle(), + $this->message['hist'], + array(), + $query, + array( 'known', 'noclasses' ) + ) . ')'; } $r .= ' . . '; # Character diff diff --git a/includes/ConfEditor.php b/includes/ConfEditor.php new file mode 100644 index 00000000..f862ebb7 --- /dev/null +++ b/includes/ConfEditor.php @@ -0,0 +1,1058 @@ +<?php + +/** + * This is a state machine style parser with two internal stacks: + * * A next state stack, which determines the state the machine will progress to next + * * A path stack, which keeps track of the logical location in the file. + * + * Reference grammar: + * + * file = T_OPEN_TAG *statement + * statement = T_VARIABLE "=" expression ";" + * expression = array / scalar / T_VARIABLE + * array = T_ARRAY "(" [ element *( "," element ) [ "," ] ] ")" + * element = assoc-element / expression + * assoc-element = scalar T_DOUBLE_ARROW expression + * scalar = T_LNUMBER / T_DNUMBER / T_STRING / T_CONSTANT_ENCAPSED_STRING + */ +class ConfEditor { + /** The text to parse */ + var $text; + + /** The token array from token_get_all() */ + var $tokens; + + /** The current position in the token array */ + var $pos; + + /** The current 1-based line number */ + var $lineNum; + + /** The current 1-based column number */ + var $colNum; + + /** The current 0-based byte number */ + var $byteNum; + + /** The current ConfEditorToken object */ + var $currentToken; + + /** The previous ConfEditorToken object */ + var $prevToken; + + /** + * The state machine stack. This is an array of strings where the topmost + * element will be popped off and become the next parser state. + */ + var $stateStack; + + + /** + * The path stack is a stack of associative arrays with the following elements: + * name The name of top level of the path + * level The level (number of elements) of the path + * startByte The byte offset of the start of the path + * startToken The token offset of the start + * endByte The byte offset of thee + * endToken The token offset of the end, plus one + * valueStartToken The start token offset of the value part + * valueStartByte The start byte offset of the value part + * valueEndToken The end token offset of the value part, plus one + * valueEndByte The end byte offset of the value part, plus one + * nextArrayIndex The next numeric array index at this level + * hasComma True if the array element ends with a comma + * arrowByte The byte offset of the "=>", or false if there isn't one + */ + var $pathStack; + + /** + * The elements of the top of the pathStack for every path encountered, indexed + * by slash-separated path. + */ + var $pathInfo; + + /** + * Next serial number for whitespace placeholder paths (@extra-N) + */ + var $serial; + + /** + * Editor state. This consists of the internal copy/insert operations which + * are applied to the source string to obtain the destination string. + */ + var $edits; + + /** + * Simple entry point for command-line testing + */ + static function test( $text ) { + try { + $ce = new self( $text ); + $ce->parse(); + } catch ( ConfEditorParseError $e ) { + return $e->getMessage() . "\n" . $e->highlight( $text ); + } + return "OK"; + } + + /** + * Construct a new parser + */ + public function __construct( $text ) { + $this->text = $text; + } + + /** + * Edit the text. Returns the edited text. + * @param array $ops Array of operations. + * + * Operations are given as an associative array, with members: + * type: One of delete, set, append or insert (required) + * path: The path to operate on (required) + * key: The array key to insert/append, with PHP quotes + * value: The value, with PHP quotes + * + * delete + * Deletes an array element or statement with the specified path. + * e.g. + * array('type' => 'delete', 'path' => '$foo/bar/baz' ) + * is equivalent to the runtime PHP code: + * unset( $foo['bar']['baz'] ); + * + * set + * Sets the value of an array element. If the element doesn't exist, it + * is appended to the array. If it does exist, the value is set, with + * comments and indenting preserved. + * + * append + * Appends a new element to the end of the array. Adds a trailing comma. + * e.g. + * array( 'type' => 'append', 'path', '$foo/bar', + * 'key' => 'baz', 'value' => "'x'" ) + * is like the PHP code: + * $foo['bar']['baz'] = 'x'; + * + * insert + * Insert a new element at the start of the array. + * + */ + public function edit( $ops ) { + $this->parse(); + + $this->edits = array( + array( 'copy', 0, strlen( $this->text ) ) + ); + foreach ( $ops as $op ) { + $type = $op['type']; + $path = $op['path']; + $value = isset( $op['value'] ) ? $op['value'] : null; + $key = isset( $op['key'] ) ? $op['key'] : null; + + switch ( $type ) { + case 'delete': + list( $start, $end ) = $this->findDeletionRegion( $path ); + $this->replaceSourceRegion( $start, $end, false ); + break; + case 'set': + if ( isset( $this->pathInfo[$path] ) ) { + list( $start, $end ) = $this->findValueRegion( $path ); + $encValue = $value; // var_export( $value, true ); + $this->replaceSourceRegion( $start, $end, $encValue ); + break; + } + // No existing path, fall through to append + $slashPos = strrpos( $path, '/' ); + $key = var_export( substr( $path, $slashPos + 1 ), true ); + $path = substr( $path, 0, $slashPos ); + // Fall through + case 'append': + // Find the last array element + $lastEltPath = $this->findLastArrayElement( $path ); + if ( $lastEltPath === false ) { + throw new MWException( "Can't find any element of array \"$path\"" ); + } + $lastEltInfo = $this->pathInfo[$lastEltPath]; + + // Has it got a comma already? + if ( strpos( $lastEltPath, '@extra' ) === false && !$lastEltInfo['hasComma'] ) { + // No comma, insert one after the value region + list( $start, $end ) = $this->findValueRegion( $lastEltPath ); + $this->replaceSourceRegion( $end - 1, $end - 1, ',' ); + } + + // Make the text to insert + list( $start, $end ) = $this->findDeletionRegion( $lastEltPath ); + + if ( $key === null ) { + list( $indent, $arrowIndent ) = $this->getIndent( $start ); + $textToInsert = "$indent$value,"; + } else { + list( $indent, $arrowIndent ) = + $this->getIndent( $start, $key, $lastEltInfo['arrowByte'] ); + $textToInsert = "$indent$key$arrowIndent=> $value,"; + } + $textToInsert .= ( $indent === false ? ' ' : "\n" ); + + // Insert the item + $this->replaceSourceRegion( $end, $end, $textToInsert ); + break; + case 'insert': + // Find first array element + $firstEltPath = $this->findFirstArrayElement( $path ); + if ( $firstEltPath === false ) { + throw new MWException( "Can't find array element of \"$path\"" ); + } + list( $start, $end ) = $this->findDeletionRegion( $firstEltPath ); + $info = $this->pathInfo[$firstEltPath]; + + // Make the text to insert + if ( $key === null ) { + list( $indent, $arrowIndent ) = $this->getIndent( $start ); + $textToInsert = "$indent$value,"; + } else { + list( $indent, $arrowIndent ) = + $this->getIndent( $start, $key, $info['arrowByte'] ); + $textToInsert = "$indent$key$arrowIndent=> $value,"; + } + $textToInsert .= ( $indent === false ? ' ' : "\n" ); + + // Insert the item + $this->replaceSourceRegion( $start, $start, $textToInsert ); + break; + default: + throw new MWException( "Unrecognised operation: \"$type\"" ); + } + } + + // Do the edits + $out = ''; + foreach ( $this->edits as $edit ) { + if ( $edit[0] == 'copy' ) { + $out .= substr( $this->text, $edit[1], $edit[2] - $edit[1] ); + } else { // if ( $edit[0] == 'insert' ) + $out .= $edit[1]; + } + } + + // Do a second parse as a sanity check + $this->text = $out; + try { + $this->parse(); + } catch ( ConfEditorParseError $e ) { + throw new MWException( + "Sorry, ConfEditor broke the file during editing and it won't parse anymore: " . + $e->getMessage() ); + } + return $out; + } + + /** + * Get the variables defined in the text + * @return array( varname => value ) + */ + function getVars() { + $vars = array(); + $this->parse(); + foreach( $this->pathInfo as $path => $data ) { + if ( $path[0] != '$' ) + continue; + $trimmedPath = substr( $path, 1 ); + $name = $data['name']; + if ( $name[0] == '@' ) + continue; + if ( $name[0] == '$' ) + $name = substr( $name, 1 ); + $parentPath = substr( $trimmedPath, 0, + strlen( $trimmedPath ) - strlen( $name ) ); + if( substr( $parentPath, -1 ) == '/' ) + $parentPath = substr( $parentPath, 0, -1 ); + + $value = substr( $this->text, $data['valueStartByte'], + $data['valueEndByte'] - $data['valueStartByte'] + ); + $this->setVar( $vars, $parentPath, $name, + $this->parseScalar( $value ) ); + } + return $vars; + } + + /** + * Set a value in an array, unless it's set already. For instance, + * setVar( $arr, 'foo/bar', 'baz', 3 ); will set + * $arr['foo']['bar']['baz'] = 3; + * @param $array array + * @param $path string slash-delimited path + * @param $key mixed Key + * @param $value mixed Value + */ + function setVar( &$array, $path, $key, $value ) { + $pathArr = explode( '/', $path ); + $target =& $array; + if ( $path !== '' ) { + foreach ( $pathArr as $p ) { + if( !isset( $target[$p] ) ) + $target[$p] = array(); + $target =& $target[$p]; + } + } + if ( !isset( $target[$key] ) ) + $target[$key] = $value; + } + + /** + * Parse a scalar value in PHP + * @return mixed Parsed value + */ + function parseScalar( $str ) { + if ( $str !== '' && $str[0] == '\'' ) + // Single-quoted string + // @todo Fixme: trim() call is due to mystery bug where whitespace gets + // appended to the token; without it we ended up reading in the + // extra quote on the end! + return strtr( substr( trim( $str ), 1, -1 ), + array( '\\\'' => '\'', '\\\\' => '\\' ) ); + if ( $str !== '' && @$str[0] == '"' ) + // Double-quoted string + // @todo Fixme: trim() call is due to mystery bug where whitespace gets + // appended to the token; without it we ended up reading in the + // extra quote on the end! + return stripcslashes( substr( trim( $str ), 1, -1 ) ); + if ( substr( $str, 0, 4 ) == 'true' ) + return true; + if ( substr( $str, 0, 5 ) == 'false' ) + return false; + if ( substr( $str, 0, 4 ) == 'null' ) + return null; + // Must be some kind of numeric value, so let PHP's weak typing + // be useful for a change + return $str; + } + + /** + * Replace the byte offset region of the source with $newText. + * Works by adding elements to the $this->edits array. + */ + function replaceSourceRegion( $start, $end, $newText = false ) { + // Split all copy operations with a source corresponding to the region + // in question. + $newEdits = array(); + foreach ( $this->edits as $i => $edit ) { + if ( $edit[0] !== 'copy' ) { + $newEdits[] = $edit; + continue; + } + $copyStart = $edit[1]; + $copyEnd = $edit[2]; + if ( $start >= $copyEnd || $end <= $copyStart ) { + // Outside this region + $newEdits[] = $edit; + continue; + } + if ( ( $start < $copyStart && $end > $copyStart ) + || ( $start < $copyEnd && $end > $copyEnd ) + ) { + throw new MWException( "Overlapping regions found, can't do the edit" ); + } + // Split the copy + $newEdits[] = array( 'copy', $copyStart, $start ); + if ( $newText !== false ) { + $newEdits[] = array( 'insert', $newText ); + } + $newEdits[] = array( 'copy', $end, $copyEnd ); + } + $this->edits = $newEdits; + } + + /** + * Finds the source byte region which you would want to delete, if $pathName + * was to be deleted. Includes the leading spaces and tabs, the trailing line + * break, and any comments in between. + */ + function findDeletionRegion( $pathName ) { + if ( !isset( $this->pathInfo[$pathName] ) ) { + throw new MWException( "Can't find path \"$pathName\"" ); + } + $path = $this->pathInfo[$pathName]; + // Find the start + $this->firstToken(); + while ( $this->pos != $path['startToken'] ) { + $this->nextToken(); + } + $regionStart = $path['startByte']; + for ( $offset = -1; $offset >= -$this->pos; $offset-- ) { + $token = $this->getTokenAhead( $offset ); + if ( !$token->isSkip() ) { + // If there is other content on the same line, don't move the start point + // back, because that will cause the regions to overlap. + $regionStart = $path['startByte']; + break; + } + $lfPos = strrpos( $token->text, "\n" ); + if ( $lfPos === false ) { + $regionStart -= strlen( $token->text ); + } else { + // The line start does not include the LF + $regionStart -= strlen( $token->text ) - $lfPos - 1; + break; + } + } + // Find the end + while ( $this->pos != $path['endToken'] ) { + $this->nextToken(); + } + $regionEnd = $path['endByte']; // past the end + for ( $offset = 0; $offset < count( $this->tokens ) - $this->pos; $offset++ ) { + $token = $this->getTokenAhead( $offset ); + if ( !$token->isSkip() ) { + break; + } + $lfPos = strpos( $token->text, "\n" ); + if ( $lfPos === false ) { + $regionEnd += strlen( $token->text ); + } else { + // This should point past the LF + $regionEnd += $lfPos + 1; + break; + } + } + return array( $regionStart, $regionEnd ); + } + + /** + * Find the byte region in the source corresponding to the value part. + * This includes the quotes, but does not include the trailing comma + * or semicolon. + * + * The end position is the past-the-end (end + 1) value as per convention. + */ + function findValueRegion( $pathName ) { + if ( !isset( $this->pathInfo[$pathName] ) ) { + throw new MWEXception( "Can't find path \"$pathName\"" ); + } + $path = $this->pathInfo[$pathName]; + if ( $path['valueStartByte'] === false || $path['valueEndByte'] === false ) { + throw new MWException( "Can't find value region for path \"$pathName\"" ); + } + return array( $path['valueStartByte'], $path['valueEndByte'] ); + } + + /** + * Find the path name of the last element in the array. + * If the array is empty, this will return the @extra interstitial element. + * If the specified path is not found or is not an array, it will return false. + */ + function findLastArrayElement( $path ) { + // Try for a real element + $lastEltPath = false; + foreach ( $this->pathInfo as $candidatePath => $info ) { + $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); + $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); + if ( $part2 == '@' ) { + // Do nothing + } elseif ( $part1 == "$path/" ) { + $lastEltPath = $candidatePath; + } elseif ( $lastEltPath !== false ) { + break; + } + } + if ( $lastEltPath !== false ) { + return $lastEltPath; + } + + // Try for an interstitial element + $extraPath = false; + foreach ( $this->pathInfo as $candidatePath => $info ) { + $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); + if ( $part1 == "$path/" ) { + $extraPath = $candidatePath; + } elseif ( $extraPath !== false ) { + break; + } + } + return $extraPath; + } + + /* + * Find the path name of first element in the array. + * If the array is empty, this will return the @extra interstitial element. + * If the specified path is not found or is not an array, it will return false. + */ + function findFirstArrayElement( $path ) { + // Try for an ordinary element + foreach ( $this->pathInfo as $candidatePath => $info ) { + $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); + $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); + if ( $part1 == "$path/" && $part2 != '@' ) { + return $candidatePath; + } + } + + // Try for an interstitial element + foreach ( $this->pathInfo as $candidatePath => $info ) { + $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); + if ( $part1 == "$path/" ) { + return $candidatePath; + } + } + return false; + } + + /** + * Get the indent string which sits after a given start position. + * Returns false if the position is not at the start of the line. + */ + function getIndent( $pos, $key = false, $arrowPos = false ) { + $arrowIndent = ' '; + if ( $pos == 0 || $this->text[$pos-1] == "\n" ) { + $indentLength = strspn( $this->text, " \t", $pos ); + $indent = substr( $this->text, $pos, $indentLength ); + } else { + $indent = false; + } + if ( $indent !== false && $arrowPos !== false ) { + $textToInsert = "$indent$key "; + $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key ); + if ( $arrowIndentLength > 0 ) { + $arrowIndent = str_repeat( ' ', $arrowIndentLength ); + } + } + return array( $indent, $arrowIndent ); + } + + /** + * Run the parser on the text. Throws an exception if the string does not + * match our defined subset of PHP syntax. + */ + public function parse() { + $this->initParse(); + $this->pushState( 'file' ); + $this->pushPath( '@extra-' . ($this->serial++) ); + $token = $this->firstToken(); + + while ( !$token->isEnd() ) { + $state = $this->popState(); + if ( !$state ) { + $this->error( 'internal error: empty state stack' ); + } + + switch ( $state ) { + case 'file': + $token = $this->expect( T_OPEN_TAG ); + $token = $this->skipSpace(); + if ( $token->isEnd() ) { + break 2; + } + $this->pushState( 'statement', 'file 2' ); + break; + case 'file 2': + $token = $this->skipSpace(); + if ( $token->isEnd() ) { + break 2; + } + $this->pushState( 'statement', 'file 2' ); + break; + case 'statement': + $token = $this->skipSpace(); + if ( !$this->validatePath( $token->text ) ) { + $this->error( "Invalid variable name \"{$token->text}\"" ); + } + $this->nextPath( $token->text ); + $this->expect( T_VARIABLE ); + $this->skipSpace(); + $arrayAssign = false; + if ( $this->currentToken()->type == '[' ) { + $this->nextToken(); + $token = $this->skipSpace(); + if ( !$token->isScalar() ) { + $this->error( "expected a string or number for the array key" ); + } + if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { + $text = $this->parseScalar( $token->text ); + } else { + $text = $token->text; + } + if ( !$this->validatePath( $text ) ) { + $this->error( "Invalid associative array name \"$text\"" ); + } + $this->pushPath( $text ); + $this->nextToken(); + $this->skipSpace(); + $this->expect( ']' ); + $this->skipSpace(); + $arrayAssign = true; + } + $this->expect( '=' ); + $this->skipSpace(); + $this->startPathValue(); + if ( $arrayAssign ) + $this->pushState( 'expression', 'array assign end' ); + else + $this->pushState( 'expression', 'statement end' ); + break; + case 'array assign end': + case 'statement end': + $this->endPathValue(); + if ( $state == 'array assign end' ) + $this->popPath(); + $this->skipSpace(); + $this->expect( ';' ); + $this->nextPath( '@extra-' . ($this->serial++) ); + break; + case 'expression': + $token = $this->skipSpace(); + if ( $token->type == T_ARRAY ) { + $this->pushState( 'array' ); + } elseif ( $token->isScalar() ) { + $this->nextToken(); + } elseif ( $token->type == T_VARIABLE ) { + $this->nextToken(); + } else { + $this->error( "expected simple expression" ); + } + break; + case 'array': + $this->skipSpace(); + $this->expect( T_ARRAY ); + $this->skipSpace(); + $this->expect( '(' ); + $this->skipSpace(); + $this->pushPath( '@extra-' . ($this->serial++) ); + if ( $this->isAhead( ')' ) ) { + // Empty array + $this->pushState( 'array end' ); + } else { + $this->pushState( 'element', 'array end' ); + } + break; + case 'array end': + $this->skipSpace(); + $this->popPath(); + $this->expect( ')' ); + break; + case 'element': + $token = $this->skipSpace(); + // Look ahead to find the double arrow + if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW, 1 ) ) { + // Found associative element + $this->pushState( 'assoc-element', 'element end' ); + } else { + // Not associative + $this->nextPath( '@next' ); + $this->startPathValue(); + $this->pushState( 'expression', 'element end' ); + } + break; + case 'element end': + $token = $this->skipSpace(); + if ( $token->type == ',' ) { + $this->endPathValue(); + $this->markComma(); + $this->nextToken(); + $this->nextPath( '@extra-' . ($this->serial++) ); + // Look ahead to find ending bracket + if ( $this->isAhead( ")" ) ) { + // Found ending bracket, no continuation + $this->skipSpace(); + } else { + // No ending bracket, continue to next element + $this->pushState( 'element' ); + } + } elseif ( $token->type == ')' ) { + // End array + $this->endPathValue(); + } else { + $this->error( "expected the next array element or the end of the array" ); + } + break; + case 'assoc-element': + $token = $this->skipSpace(); + if ( !$token->isScalar() ) { + $this->error( "expected a string or number for the array key" ); + } + if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { + $text = $this->parseScalar( $token->text ); + } else { + $text = $token->text; + } + if ( !$this->validatePath( $text ) ) { + $this->error( "Invalid associative array name \"$text\"" ); + } + $this->nextPath( $text ); + $this->nextToken(); + $this->skipSpace(); + $this->markArrow(); + $this->expect( T_DOUBLE_ARROW ); + $this->skipSpace(); + $this->startPathValue(); + $this->pushState( 'expression' ); + break; + } + } + if ( count( $this->stateStack ) ) { + $this->error( 'unexpected end of file' ); + } + $this->popPath(); + } + + /** + * Initialise a parse. + */ + protected function initParse() { + $this->tokens = token_get_all( $this->text ); + $this->stateStack = array(); + $this->pathStack = array(); + $this->firstToken(); + $this->pathInfo = array(); + $this->serial = 1; + } + + /** + * Set the parse position. Do not call this except from firstToken() and + * nextToken(), there is more to update than just the position. + */ + protected function setPos( $pos ) { + $this->pos = $pos; + if ( $this->pos >= count( $this->tokens ) ) { + $this->currentToken = ConfEditorToken::newEnd(); + } else { + $this->currentToken = $this->newTokenObj( $this->tokens[$this->pos] ); + } + return $this->currentToken; + } + + /** + * Create a ConfEditorToken from an element of token_get_all() + */ + function newTokenObj( $internalToken ) { + if ( is_array( $internalToken ) ) { + return new ConfEditorToken( $internalToken[0], $internalToken[1] ); + } else { + return new ConfEditorToken( $internalToken, $internalToken ); + } + } + + /** + * Reset the parse position + */ + function firstToken() { + $this->setPos( 0 ); + $this->prevToken = ConfEditorToken::newEnd(); + $this->lineNum = 1; + $this->colNum = 1; + $this->byteNum = 0; + return $this->currentToken; + } + + /** + * Get the current token + */ + function currentToken() { + return $this->currentToken; + } + + /** + * Advance the current position and return the resulting next token + */ + function nextToken() { + if ( $this->currentToken ) { + $text = $this->currentToken->text; + $lfCount = substr_count( $text, "\n" ); + if ( $lfCount ) { + $this->lineNum += $lfCount; + $this->colNum = strlen( $text ) - strrpos( $text, "\n" ); + } else { + $this->colNum += strlen( $text ); + } + $this->byteNum += strlen( $text ); + } + $this->prevToken = $this->currentToken; + $this->setPos( $this->pos + 1 ); + return $this->currentToken; + } + + /** + * Get the token $offset steps ahead of the current position. + * $offset may be negative, to get tokens behind the current position. + */ + function getTokenAhead( $offset ) { + $pos = $this->pos + $offset; + if ( $pos >= count( $this->tokens ) || $pos < 0 ) { + return ConfEditorToken::newEnd(); + } else { + return $this->newTokenObj( $this->tokens[$pos] ); + } + } + + /** + * Advances the current position past any whitespace or comments + */ + function skipSpace() { + while ( $this->currentToken && $this->currentToken->isSkip() ) { + $this->nextToken(); + } + return $this->currentToken; + } + + /** + * Throws an error if the current token is not of the given type, and + * then advances to the next position. + */ + function expect( $type ) { + if ( $this->currentToken && $this->currentToken->type == $type ) { + return $this->nextToken(); + } else { + $this->error( "expected " . $this->getTypeName( $type ) . + ", got " . $this->getTypeName( $this->currentToken->type ) ); + } + } + + /** + * Push a state or two on to the state stack. + */ + function pushState( $nextState, $stateAfterThat = null ) { + if ( $stateAfterThat !== null ) { + $this->stateStack[] = $stateAfterThat; + } + $this->stateStack[] = $nextState; + } + + /** + * Pop a state from the state stack. + */ + function popState() { + return array_pop( $this->stateStack ); + } + + /** + * Returns true if the user input path is valid. + * This exists to allow "/" and "@" to be reserved for string path keys + */ + function validatePath( $path ) { + return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@'; + } + + /** + * Internal function to update some things at the end of a path region. Do + * not call except from popPath() or nextPath(). + */ + function endPath() { + $i = count( $this->pathStack ) - 1; + $key = ''; + foreach ( $this->pathStack as $pathInfo ) { + if ( $key !== '' ) { + $key .= '/'; + } + $key .= $pathInfo['name']; + } + $pathInfo['endByte'] = $this->byteNum; + $pathInfo['endToken'] = $this->pos; + $this->pathInfo[$key] = $pathInfo; + } + + /** + * Go up to a new path level, for example at the start of an array. + */ + function pushPath( $path ) { + $this->pathStack[] = array( + 'name' => $path, + 'level' => count( $this->pathStack ) + 1, + 'startByte' => $this->byteNum, + 'startToken' => $this->pos, + 'valueStartToken' => false, + 'valueStartByte' => false, + 'valueEndToken' => false, + 'valueEndByte' => false, + 'nextArrayIndex' => 0, + 'hasComma' => false, + 'arrowByte' => false + ); + } + + /** + * Go down a path level, for example at the end of an array. + */ + function popPath() { + $this->endPath(); + array_pop( $this->pathStack ); + } + + /** + * Go to the next path on the same level. This ends the current path and + * starts a new one. If $path is @next, the new path is set to the next + * numeric array element. + */ + function nextPath( $path ) { + $this->endPath(); + $i = count( $this->pathStack ) - 1; + if ( $path == '@next' ) { + $nextArrayIndex =& $this->pathStack[$i]['nextArrayIndex']; + $this->pathStack[$i]['name'] = $nextArrayIndex; + $nextArrayIndex++; + } else { + $this->pathStack[$i]['name'] = $path; + } + $this->pathStack[$i] = + array( + 'startByte' => $this->byteNum, + 'startToken' => $this->pos, + 'valueStartToken' => false, + 'valueStartByte' => false, + 'valueEndToken' => false, + 'valueEndByte' => false, + 'hasComma' => false, + 'arrowByte' => false, + ) + $this->pathStack[$i]; + } + + /** + * Mark the start of the value part of a path. + */ + function startPathValue() { + $path =& $this->pathStack[count( $this->pathStack ) - 1]; + $path['valueStartToken'] = $this->pos; + $path['valueStartByte'] = $this->byteNum; + } + + /** + * Mark the end of the value part of a path. + */ + function endPathValue() { + $path =& $this->pathStack[count( $this->pathStack ) - 1]; + $path['valueEndToken'] = $this->pos; + $path['valueEndByte'] = $this->byteNum; + } + + /** + * Mark the comma separator in an array element + */ + function markComma() { + $path =& $this->pathStack[count( $this->pathStack ) - 1]; + $path['hasComma'] = true; + } + + /** + * Mark the arrow separator in an associative array element + */ + function markArrow() { + $path =& $this->pathStack[count( $this->pathStack ) - 1]; + $path['arrowByte'] = $this->byteNum; + } + + /** + * Generate a parse error + */ + function error( $msg ) { + throw new ConfEditorParseError( $this, $msg ); + } + + /** + * Get a readable name for the given token type. + */ + function getTypeName( $type ) { + if ( is_int( $type ) ) { + return token_name( $type ); + } else { + return "\"$type\""; + } + } + + /** + * Looks ahead to see if the given type is the next token type, starting + * from the current position plus the given offset. Skips any intervening + * whitespace. + */ + function isAhead( $type, $offset = 0 ) { + $ahead = $offset; + $token = $this->getTokenAhead( $offset ); + while ( !$token->isEnd() ) { + if ( $token->isSkip() ) { + $ahead++; + $token = $this->getTokenAhead( $ahead ); + continue; + } elseif ( $token->type == $type ) { + // Found the type + return true; + } else { + // Not found + return false; + } + } + return false; + } + + /** + * Get the previous token object + */ + function prevToken() { + return $this->prevToken; + } + + /** + * Echo a reasonably readable representation of the tokenizer array. + */ + function dumpTokens() { + $out = ''; + foreach ( $this->tokens as $token ) { + $obj = $this->newTokenObj( $token ); + $out .= sprintf( "%-28s %s\n", + $this->getTypeName( $obj->type ), + addcslashes( $obj->text, "\0..\37" ) ); + } + echo "<pre>" . htmlspecialchars( $out ) . "</pre>"; + } +} + +/** + * Exception class for parse errors + */ +class ConfEditorParseError extends MWException { + var $lineNum, $colNum; + function __construct( $editor, $msg ) { + $this->lineNum = $editor->lineNum; + $this->colNum = $editor->colNum; + parent::__construct( "Parse error on line {$editor->lineNum} " . + "col {$editor->colNum}: $msg" ); + } + + function highlight( $text ) { + $lines = StringUtils::explode( "\n", $text ); + foreach ( $lines as $lineNum => $line ) { + if ( $lineNum == $this->lineNum - 1 ) { + return "$line\n" .str_repeat( ' ', $this->colNum - 1 ) . "^\n"; + } + } + } + +} + +/** + * Class to wrap a token from the tokenizer. + */ +class ConfEditorToken { + var $type, $text; + + static $scalarTypes = array( T_LNUMBER, T_DNUMBER, T_STRING, T_CONSTANT_ENCAPSED_STRING ); + static $skipTypes = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ); + + static function newEnd() { + return new self( 'END', '' ); + } + + function __construct( $type, $text ) { + $this->type = $type; + $this->text = $text; + } + + function isSkip() { + return in_array( $this->type, self::$skipTypes ); + } + + function isScalar() { + return in_array( $this->type, self::$scalarTypes ); + } + + function isEnd() { + return $this->type == 'END'; + } +} + diff --git a/includes/Credits.php b/includes/Credits.php index ae9377f2..91ba3f16 100644 --- a/includes/Credits.php +++ b/includes/Credits.php @@ -55,13 +55,13 @@ class Credits { * @param $showIfMax Bool: whether to contributors if there more than $cnt * @return String: html */ - public static function getCredits($article, $cnt, $showIfMax=true) { + public static function getCredits( Article $article, $cnt, $showIfMax = true ) { wfProfileIn( __METHOD__ ); $s = ''; if( isset( $cnt ) && $cnt != 0 ){ $s = self::getAuthor( $article ); - if ($cnt > 1 || $cnt < 0) { + if ( $cnt > 1 || $cnt < 0 ) { $s .= ' ' . self::getContributors( $article, $cnt - 1, $showIfMax ); } } @@ -75,7 +75,7 @@ class Credits { * @param $article Article object */ protected static function getAuthor( Article $article ){ - global $wgLang, $wgAllowRealName; + global $wgLang; $user = User::newFromId( $article->getUser() ); @@ -87,7 +87,7 @@ class Credits { $d = ''; $t = ''; } - return wfMsg( 'lastmodifiedatby', $d, $t, self::userLink( $user ) ); + return wfMsgExt( 'lastmodifiedatby', 'parsemag', $d, $t, self::userLink( $user ), $user->getName() ); } /** @@ -98,11 +98,11 @@ class Credits { * @return String: html */ protected static function getContributors( Article $article, $cnt, $showIfMax ) { - global $wgLang, $wgAllowRealName; + global $wgLang, $wgHiddenPrefs; $contributors = $article->getContributors(); - $others_link = ''; + $others_link = false; # Hmm... too many to fit! if( $cnt > 0 && $contributors->count() > $cnt ){ @@ -113,38 +113,48 @@ class Credits { $real_names = array(); $user_names = array(); - $anon = 0; + $anon_ips = array(); # Sift for real versus user names foreach( $contributors as $user ) { $cnt--; if( $user->isLoggedIn() ){ $link = self::link( $user ); - if( $wgAllowRealName && $user->getRealName() ) + if( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) $real_names[] = $link; else $user_names[] = $link; } else { - $anon++; + $anon_ips[] = self::link( $user ); } if( $cnt == 0 ) break; } - # Two strings: real names, and user names - $real = $wgLang->listToText( $real_names ); - $user = $wgLang->listToText( $user_names ); - if( $anon ) - $anon = wfMsgExt( 'anonymous', array( 'parseinline' ), $anon ); + if ( count( $real_names ) ) { + $real = $wgLang->listToText( $real_names ); + } else { + $real = false; + } # "ThisSite user(s) A, B and C" - if( !empty( $user ) ){ - $user = wfMsgExt( 'siteusers', array( 'parsemag' ), $user, count( $user_names ) ); + if( count( $user_names ) ){ + $user = wfMsgExt( 'siteusers', array( 'parsemag' ), + $wgLang->listToText( $user_names ), count( $user_names ) ); + } else { + $user = false; + } + + if( count( $anon_ips ) ){ + $anon = wfMsgExt( 'anonusers', array( 'parsemag' ), + $wgLang->listToText( $anon_ips ), count( $anon_ips ) ); + } else { + $anon = false; } # This is the big list, all mooshed together. We sift for blank strings $fulllist = array(); foreach( array( $real, $user, $anon, $others_link ) as $s ){ - if( !empty( $s ) ){ + if( $s ){ array_push( $fulllist, $s ); } } @@ -153,40 +163,42 @@ class Credits { $creds = $wgLang->listToText( $fulllist ); # "Based on work by ..." - return empty( $creds ) ? '' : wfMsg( 'othercontribs', $creds ); + return strlen( $creds ) ? wfMsg( 'othercontribs', $creds ) : ''; } /** - * Get a link to $user_name page + * Get a link to $user's user page * @param $user User object * @return String: html */ protected static function link( User $user ) { - global $wgUser, $wgAllowRealName; - if( $wgAllowRealName ) + global $wgUser, $wgHiddenPrefs; + if( !in_array( 'realname', $wgHiddenPrefs ) && !$user->isAnon() ) $real = $user->getRealName(); else $real = false; $skin = $wgUser->getSkin(); - $page = $user->getUserPage(); - + $page = $user->isAnon() ? + SpecialPage::getTitleFor( 'Contributions', $user->getName() ) : + $user->getUserPage(); + return $skin->link( $page, htmlspecialchars( $real ? $real : $user->getName() ) ); } /** - * Get a link to $user_name page + * Get a link to $user's user page * @param $user_name String: user name * @param $linkText String: optional display * @return String: html */ protected static function userLink( User $user ) { - global $wgUser, $wgAllowRealName; + $link = self::link( $user ); if( $user->isAnon() ){ - return wfMsgExt( 'anonymous', array( 'parseinline' ), 1 ); + return wfMsgExt( 'anonuser', array( 'parseinline', 'replaceafter' ), $link ); } else { - $link = self::link( $user ); - if( $wgAllowRealName && $user->getRealName() ) + global $wgHiddenPrefs; + if( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) return $link; else return wfMsgExt( 'siteuser', array( 'parseinline', 'replaceafter' ), $link ); @@ -203,4 +215,4 @@ class Credits { $skin = $wgUser->getSkin(); return $skin->link( $article->getTitle(), wfMsgHtml( 'others' ), array(), array( 'action' => 'credits' ), array( 'known' ) ); } -}
\ No newline at end of file +} diff --git a/includes/DatabaseFunctions.php b/includes/DatabaseFunctions.php index 52e9a8c8..2df56115 100644 --- a/includes/DatabaseFunctions.php +++ b/includes/DatabaseFunctions.php @@ -58,7 +58,7 @@ function wfIgnoreSQLErrors( $newstate, $dbi = DB_LAST ) { if ( $db !== false ) { return $db->ignoreErrors( $newstate ); } else { - return NULL; + return null; } } diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 160273b8..a369fccd 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -33,7 +33,7 @@ if ( !defined( 'MW_PHP4' ) ) { } /** MediaWiki version number */ -$wgVersion = '1.15.5'; +$wgVersion = '1.16.0'; /** Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; @@ -142,16 +142,17 @@ $wgRedirectScript = false; ///< defaults to "{$wgScriptPath}/redirect{$wgScrip * splitting style sheets or images outside the main document root. */ /** - * style path as seen by users + * asset paths as seen by users */ $wgStylePath = false; ///< defaults to "{$wgScriptPath}/skins" +$wgExtensionAssetsPath = false; ///< defaults to "{$wgScriptPath}/extensions" + /** * filesystem stylesheets directory */ $wgStyleDirectory = false; ///< defaults to "{$IP}/skins" $wgStyleSheetPath = &$wgStylePath; $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" $wgHashedUploadDirectory = true; @@ -165,6 +166,16 @@ $wgUploadBaseUrl = ""; /**@}*/ /** + * Directory for caching data in the local filesystem. Should not be accessible + * from the web. Set this to false to not use any local caches. + * + * Note: if multiple wikis share the same localisation cache directory, they + * must all have the same set of extensions. You can set a directory just for + * the localisation cache using $wgLocalisationCacheConf['storeDirectory']. + */ +$wgCacheDirectory = false; + +/** * Default value for chmoding of new directories. */ $wgDirectoryMode = 0777; @@ -181,11 +192,14 @@ $wgFileStore['deleted']['directory'] = false;///< Defaults to $wgUploadDirectory $wgFileStore['deleted']['url'] = null; ///< Private $wgFileStore['deleted']['hash'] = 3; ///< 3-level subdirectory split +$wgImgAuthDetails = false; ///< defaults to false - only set to true if you use img_auth and want the user to see details on why access failed +$wgImgAuthPublicTest = true; ///< defaults to true - if public read is turned on, no need for img_auth, config error unless other access is used + /**@{ * File repository structures * - * $wgLocalFileRepo is a single repository structure, and $wgForeignFileRepo is - * a an array of such structures. Each repository structure is an associative + * $wgLocalFileRepo is a single repository structure, and $wgForeignFileRepos is + * an array of such structures. Each repository structure is an associative * array of properties configuring the repository. * * Properties required for all repos: @@ -194,20 +208,27 @@ $wgFileStore['deleted']['hash'] = 3; ///< 3-level subdirectory split * * name A unique name for the repository. * - * For all core repos: + * For most 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 * handler instead. - * 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 - * are mismatched. + * initialCapital Equivalent to $wgCapitalLinks (or $wgCapitalLinkOverrides[NS_FILE], + * determines whether filenames implicitly start with a capital letter. + * The current implementation may give incorrect 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 * placeholders. Default for LocalRepo is 'simple'. + * fileMode This allows wikis to set the file mode when uploading/moving files. Default + * is 0644. + * directory The local filesystem directory where public files are stored. Not used for + * some remote repos. + * thumbDir The base thumbnail directory. Defaults to <directory>/thumb. + * thumbUrl The base thumbnail URL. Defaults to <url>/thumb. + * * * These settings describe a foreign MediaWiki installation. They are optional, and will be ignored * for local repositories: @@ -224,7 +245,7 @@ $wgFileStore['deleted']['hash'] = 3; ///< 3-level subdirectory split * equivalent to the corresponding member of $wgDBservers * tablePrefix Table prefix, the foreign wiki's $wgDBprefix * hasSharedCache True if the wiki's shared cache is accessible via the local $wgMemc - * + * * ForeignAPIRepo: * apibase Use for the foreign API's URL * apiThumbCacheExpiry How long to locally cache thumbs for @@ -237,6 +258,13 @@ $wgForeignFileRepos = array(); /**@}*/ /** + * Use Commons as a remote file repository. Essentially a wrapper, when this + * is enabled $wgForeignFileRepos will point at Commons with a set of default + * settings + */ +$wgUseInstantCommons = false; + +/** * Allowed title characters -- regex character class * Don't change this unless you know what you're doing * @@ -263,6 +291,7 @@ $wgForeignFileRepos = array(); * this breaks interlanguage links */ $wgLegalTitleChars = " %!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+"; +$wgIllegalFileChars = ":"; // These are additional characters that should be replaced with '-' in file names /** @@ -285,7 +314,7 @@ $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. */ -$wgAntivirus= NULL; +$wgAntivirus= null; /** Configuration for different virus scanners. This an associative array of associative arrays: * it contains on setup array per known scanner type. The entry is selected by $wgAntivirus, i.e. @@ -352,11 +381,11 @@ $wgVerifyMimeType= true; /** 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. +#$wgMimeTypeFile= null; #use built-in defaults only. /** Sets the mime type info file to use by MimeMagic.php. */ $wgMimeInfoFile= "includes/mime.info"; -#$wgMimeInfoFile= NULL; #use built-in defaults only. +#$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 @@ -369,7 +398,7 @@ $wgLoadFileinfoExtension= false; * 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= 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 @@ -426,8 +455,12 @@ $wgSharedUploadDBname = false; $wgSharedUploadDBprefix = ''; /** Cache shared metadata in memcached. Don't do this if the commons wiki is in a different memcached domain */ $wgCacheSharedUploads = true; -/** Allow for upload to be copied from an URL. Requires Special:Upload?source=web */ +/** +* Allow for upload to be copied from an URL. Requires Special:Upload?source=web +* The timeout for copy uploads is set by $wgHTTPTimeout. +*/ $wgAllowCopyUploads = false; + /** * Max size for uploads, in bytes. Currently only works for uploads from URL * via CURL (see $wgAllowCopyUploads). The only way to impose limits on @@ -440,6 +473,9 @@ $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'; + * + * This also affects images inline images that do not exist. In that case the URL will get + * (?|&)wpDestFile=<filename> appended to it as appropriate. */ $wgUploadNavigationUrl = false; @@ -564,6 +600,10 @@ $wgDBpassword = ''; /** Database type */ $wgDBtype = 'mysql'; +/** Separate username and password for maintenance tasks. Leave as null to use the default */ +$wgDBadminuser = null; +$wgDBadminpassword = null; + /** Search type * Leave as null to select the default search engine for the * selected database type (eg SearchMySQL), or set to a class @@ -610,6 +650,8 @@ $wgCheckDBSchema = true; * 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. + * The user_properties table is also added so that users will continue to have their + * preferences shared (preferences were stored in the user table prior to 1.16) * * $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 @@ -618,7 +660,7 @@ $wgCheckDBSchema = true; */ $wgSharedDB = null; $wgSharedPrefix = false; # Defaults to $wgDBprefix -$wgSharedTables = array( 'user' ); +$wgSharedTables = array( 'user', 'user_properties' ); /** * Database load balancer @@ -732,8 +774,17 @@ $wgParserCacheType = CACHE_ANYTHING; $wgParserCacheExpireTime = 86400; +// Select which DBA handler <http://www.php.net/manual/en/dba.requirements.php> to use as CACHE_DBA backend +$wgDBAhandler = 'db3'; + $wgSessionsInMemcached = false; +/** This is used for setting php's session.save_handler. In practice, you will + * almost never need to change this ever. Other options might be 'user' or + * 'session_mysql.' Setting to null skips setting this entirely (which might be + * useful if you're doing cross-application sessions, see bug 11381) */ +$wgSessionHandler = 'files'; + /**@{ * Memcached-specific settings * See docs/memcached.txt @@ -742,12 +793,15 @@ $wgUseMemCached = false; $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; +$wgMemCachedTimeout = 100000; //Data timeout in microseconds /**@}*/ /** - * Directory for local copy of message cache, for use in addition to memcached + * Set this to true to make a local copy of the message cache, for use in + * addition to memcached. The files will be put in $wgCacheDirectory. */ -$wgLocalMessageCache = false; +$wgUseLocalMessageCache = false; + /** * Defines format of local cache * true - Serialized object @@ -755,6 +809,34 @@ $wgLocalMessageCache = false; */ $wgLocalMessageCacheSerialized = true; +/** + * Localisation cache configuration. Associative array with keys: + * class: The class to use. May be overridden by extensions. + * + * store: The location to store cache data. May be 'files', 'db' or + * 'detect'. If set to "files", data will be in CDB files. If set + * to "db", data will be stored to the database. If set to + * "detect", files will be used if $wgCacheDirectory is set, + * otherwise the database will be used. + * + * storeClass: The class name for the underlying storage. If set to a class + * name, it overrides the "store" setting. + * + * storeDirectory: If the store class puts its data in files, this is the + * directory it will use. If this is false, $wgCacheDirectory + * will be used. + * + * manualRecache: Set this to true to disable cache updates on web requests. + * Use maintenance/rebuildLocalisationCache.php instead. + */ +$wgLocalisationCacheConf = array( + 'class' => 'LocalisationCache', + 'store' => 'detect', + 'storeClass' => false, + 'storeDirectory' => false, + 'manualRecache' => false, +); + # Language settings # /** Site language code, should be one of ./languages/Language(.*).php */ @@ -776,14 +858,42 @@ $wgHideInterlanguageLinks = false; /** List of language names or overrides for default names in Names.php */ $wgExtraLanguageNames = array(); +/** + * List of language codes that don't correspond to an actual language. + * These codes are leftoffs from renames, or other legacy things. + * Also, qqq is a dummy "language" for documenting messages. + */ +$wgDummyLanguageCodes = array( 'qqq', 'als', 'be-x-old', 'dk', 'fiu-vro', 'iu', 'nb', 'simple', 'tp' ); + /** We speak UTF-8 all the time now, unless some oddities happen */ $wgInputEncoding = 'UTF-8'; $wgOutputEncoding = 'UTF-8'; $wgEditEncoding = ''; /** + * Set this to true to replace Arabic presentation forms with their standard + * forms in the U+0600-U+06FF block. This only works if $wgLanguageCode is + * set to "ar". + * + * Note that pages with titles containing presentation forms will become + * inaccessible, run maintenance/cleanupTitles.php to fix this. + */ +$wgFixArabicUnicode = true; + +/** + * Set this to true to replace ZWJ-based chillu sequences in Malayalam text + * with their Unicode 5.1 equivalents. This only works if $wgLanguageCode is + * set to "ml". Note that some clients (even new clients as of 2010) do not + * support these characters. + * + * If you enable this on an existing wiki, run maintenance/cleanupTitles.php to + * fix any ZWJ sequences in existing page titles. + */ +$wgFixMalayalamUnicode = true; + +/** * Locale for LC_CTYPE, to work around http://bugs.php.net/bug.php?id=45132 - * For Unix-like operating systems, set this to to a locale that has a UTF-8 + * For Unix-like operating systems, set this to to a locale that has a UTF-8 * character set. Only the character set is relevant. */ $wgShellLocale = 'en_US.utf8'; @@ -817,11 +927,54 @@ $wgLegacyEncoding = false; */ $wgLegacySchemaConversion = false; -$wgMimeType = 'text/html'; -$wgJsMimeType = 'text/javascript'; -$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'; +$wgMimeType = 'text/html'; +$wgJsMimeType = 'text/javascript'; +$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'; + +/** + * Should we output an HTML5 doctype? This mode is still experimental, but + * all indications are that it should be usable, so it's enabled by default. + * If all goes well, it will be removed and become always true before the 1.16 + * release. + */ +$wgHtml5 = true; + +/** + * Defines the value of the version attribute in the <html> tag, if any. + * Will be initialized later if not set explicitly. + */ +$wgHtml5Version = null; + +/** + * Enabled RDFa attributes for use in wikitext. + * NOTE: Interaction with HTML5 is somewhat underspecified. + */ +$wgAllowRdfaAttributes = false; + +/** + * Enabled HTML5 microdata attributes for use in wikitext, if $wgHtml5 is also true. + */ +$wgAllowMicrodataAttributes = false; + +/** + * Should we try to make our HTML output well-formed XML? If set to false, + * output will be a few bytes shorter, and the HTML will arguably be more + * readable. If set to true, life will be much easier for the authors of + * screen-scraping bots, and the HTML will arguably be more readable. + * + * Setting this to false may omit quotation marks on some attributes, omit + * slashes from some self-closing tags, omit some ending tags, etc., where + * permitted by HTML5. Setting it to true will not guarantee that all pages + * will be well-formed, although non-well-formed pages should be rare and it's + * a bug if you find one. Conversely, setting it to false doesn't mean that + * all XML-y constructs will be omitted, just that they might be. + * + * Because of compatibility with screen-scraping bots, and because it's + * controversial, this is currently left to true by default. + */ +$wgWellFormedXml = true; /** * Permit other namespaces in addition to the w3.org default. @@ -831,7 +984,7 @@ $wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml'; * Normally we wouldn't have to define this in the root <html> * element, but IE needs it there in some circumstances. */ -$wgXhtmlNamespaces = array(); +$wgXhtmlNamespaces = array(); /** Enable to allow rewriting dates in page text. * DOES NOT FORMAT CORRECTLY FOR MOST LANGUAGES */ @@ -885,6 +1038,32 @@ $wgDisableTitleConversion = false; /** Default variant code, if false, the default will be the language code */ $wgDefaultLanguageVariant = false; +/** Disabled variants array of language variant conversion. + * example: + * $wgDisabledVariants[] = 'zh-mo'; + * $wgDisabledVariants[] = 'zh-my'; + * + * or: + * $wgDisabledVariants = array('zh-mo', 'zh-my'); + */ +$wgDisabledVariants = array(); + +/** + * Like $wgArticlePath, but on multi-variant wikis, this provides a + * path format that describes which parts of the URL contain the + * language variant. For Example: + * + * $wgLanguageCode = 'sr'; + * $wgVariantArticlePath = '/$2/$1'; + * $wgArticlePath = '/wiki/$1'; + * + * A link to /wiki/ would be redirected to /sr/Главна_страна + * + * It is important that $wgArticlePath not overlap with possible values + * of $wgVariantArticlePath. + */ +$wgVariantArticlePath = false;///< defaults to false + /** * Show a bar of language selection links in the user login and user * registration forms; edit the "loginlanguagelinks" message to @@ -970,11 +1149,11 @@ $wgExtraSubtitle = ''; $wgSiteSupportPage = ''; # A page where you users can receive donations /** - * Set this to a string to put the wiki into read-only mode. The text will be - * used as an explanation to users. + * Set this to a string to put the wiki into read-only mode. The text will be + * used as an explanation to users. * - * This prevents most write operations via the web interface. Cache updates may - * still be possible. To prevent database writes completely, use the read_only + * This prevents most write operations via the web interface. Cache updates may + * still be possible. To prevent database writes completely, use the read_only * option in MySQL. */ $wgReadOnly = null; @@ -989,7 +1168,7 @@ $wgReadOnlyFile = false; ///< defaults to "{$wgUploadDirectory}/lock_yBg /** * Filename for debug logging. See http://www.mediawiki.org/wiki/How_to_debug * The debug log file should be not be publicly accessible if it is used, as it - * may contain private data. + * may contain private data. */ $wgDebugLogFile = ''; @@ -999,14 +1178,14 @@ $wgDebugLogFile = ''; $wgDebugLogPrefix = ''; /** - * If true, instead of redirecting, show a page with a link to the redirect + * If true, instead of redirecting, show a page with a link to the redirect * destination. This allows for the inspection of PHP error messages, and easy * resubmission of form data. For developer use only. */ $wgDebugRedirects = false; /** - * If true, log debugging data from action=raw. + * If true, log debugging data from action=raw. * This is normally false to avoid overlapping debug entries due to gen=css and * gen=js requests. */ @@ -1017,14 +1196,11 @@ $wgDebugRawPage = false; * * This may occasionally be useful when supporting a non-technical end-user. It's * more secure than exposing the debug log file to the web, since the output only - * contains private data for the current user. But it's not ideal for development + * contains private data for the current user. But it's not ideal for development * use since data is lost on fatal errors and redirects. */ $wgDebugComments = false; -/** Does nothing. Obsolete? */ -$wgLogQueries = false; - /** * Write SQL queries to the debug log */ @@ -1046,6 +1222,16 @@ $wgDebugLogGroups = array(); $wgShowDebug = false; /** + * Prefix debug messages with relative timestamp. Very-poor man's profiler. + */ +$wgDebugTimestamps = false; + +/** + * Print HTTP headers for every request in the debug information. + */ +$wgDebugPrintHttpHeaders = true; + +/** * Show the contents of $wgHooks in Special:Version */ $wgSpecialVersionShowHooks = false; @@ -1073,22 +1259,33 @@ $wgColorErrors = true; $wgShowExceptionDetails = false; /** + * If true, show a backtrace for database errors + */ +$wgShowDBErrorBacktrace = false; + +/** * Expose backend server host names through the API and various HTML comments */ $wgShowHostnames = false; /** + * If set to true MediaWiki will throw notices for some possible error + * conditions and for deprecated functions. + */ +$wgDevelopmentWarnings = false; + +/** * Use experimental, DMOZ-like category browser */ $wgUseCategoryBrowser = false; /** - * Keep parsed pages in a cache (objectcache table, turck, or memcached) + * Keep parsed pages in a cache (objectcache table or memcached) * to speed up output of the same page viewed by another user with the * same options. * * This can provide a significant speedup for medium to large pages, - * so you probably want to keep it on. Extensions that conflict with the + * so you probably want to keep it on. Extensions that conflict with the * parser cache should disable the cache on a per-page basis instead. */ $wgEnableParserCache = true; @@ -1120,7 +1317,7 @@ $wgSidebarCacheExpiry = 86400; * as a valid article? If $wgUseCommaCount is set to true, it will be * counted if it contains at least one comma. If it is set to false * (default), it will only be counted if it contains at least one [[wiki - * link]]. See http://meta.wikimedia.org/wiki/Help:Article_count + * link]]. See http://www.mediawiki.org/wiki/Manual:Article_count * * Retroactively changing this variable will not affect * the existing count (cf. maintenance/recount.sql). @@ -1142,6 +1339,19 @@ $wgSysopRangeBans = true; # Allow sysops to ban IP ranges $wgAutoblockExpiry = 86400; # Number of seconds before autoblock entries expire $wgBlockAllowsUTEdit = false; # Default setting for option on block form to allow self talkpage editing whilst blocked $wgSysopEmailBans = true; # Allow sysops to ban users from accessing Emailuser +$wgBlockCIDRLimit = array( + 'IPv4' => 16, # Blocks larger than a /16 (64k addresses) will not be allowed + 'IPv6' => 64, # 2^64 = ~1.8x10^19 addresses +); + +/** + * If true, blocked users will not be allowed to login. When using this with + * a public wiki, the effect of logging out blocked users may actually be + * avers: unless the user's address is also blocked (e.g. auto-block), + * logging the user out will again allow reading and editing, just as for + * anonymous visitors. + */ +$wgBlockDisablesLogin = false; # # Pages anonymous user may see as an array, e.g.: # array ( "Main Page", "Wikipedia:Help"); @@ -1186,6 +1396,7 @@ $wgGroupPermissions['*']['edit'] = true; $wgGroupPermissions['*']['createpage'] = true; $wgGroupPermissions['*']['createtalk'] = true; $wgGroupPermissions['*']['writeapi'] = true; +//$wgGroupPermissions['*']['patrolmarks'] = false; // let anons see what was patrolled // Implicit group for all logged-in accounts $wgGroupPermissions['user']['move'] = true; @@ -1202,6 +1413,7 @@ $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']['sendemail'] = true; // Implicit group for accounts that pass $wgAutoConfirmAge $wgGroupPermissions['autoconfirmed']['autoconfirmed'] = true; @@ -1223,9 +1435,11 @@ $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']['deletedtext'] = true; // can view deleted revision text $wgGroupPermissions['sysop']['undelete'] = true; $wgGroupPermissions['sysop']['editinterface'] = true; -$wgGroupPermissions['sysop']['editusercssjs'] = true; +$wgGroupPermissions['sysop']['editusercss'] = true; +$wgGroupPermissions['sysop']['edituserjs'] = true; $wgGroupPermissions['sysop']['import'] = true; $wgGroupPermissions['sysop']['importupload'] = true; $wgGroupPermissions['sysop']['move'] = true; @@ -1249,6 +1463,7 @@ $wgGroupPermissions['sysop']['markbotedits'] = true; $wgGroupPermissions['sysop']['apihighlimits'] = true; $wgGroupPermissions['sysop']['browsearchive'] = true; $wgGroupPermissions['sysop']['noratelimit'] = true; +$wgGroupPermissions['sysop']['versiondetail'] = true; $wgGroupPermissions['sysop']['movefile'] = true; #$wgGroupPermissions['sysop']['mergehistory'] = true; @@ -1276,6 +1491,15 @@ $wgGroupPermissions['bureaucrat']['noratelimit'] = true; */ # $wgGroupPermissions['developer']['siteadmin'] = true; +/** + * Permission keys revoked from users in each group. + * This acts the same way as wgGroupPermissions above, except that + * if the user is in a group here, the permission will be removed from them. + * + * Improperly setting this could mean that your users will be unable to perform + * certain essential tasks, so use at your own risk! + */ +$wgRevokePermissions = array(); /** * Implicit groups, aren't shown on Special:Listusers or somewhere else @@ -1287,7 +1511,7 @@ $wgImplicitGroups = array( '*', 'user', 'autoconfirmed' ); * are allowed to add or revoke. * * Setting the list of groups to add or revoke to true is equivalent to "any group". - * + * * For example, to allow sysops to add themselves to the "bot" group: * * $wgGroupsAddToSelf = array( 'sysop' => array( 'bot' ) ); @@ -1298,7 +1522,7 @@ $wgImplicitGroups = array( '*', 'user', 'autoconfirmed' ); * * This allows users in the '*' group (i.e. any user) to remove themselves from * any group that they happen to be in. - * + * */ $wgGroupsAddToSelf = array(); $wgGroupsRemoveFromSelf = array(); @@ -1372,6 +1596,7 @@ $wgAutoConfirmCount = 0; * array( APCOND_ISIP, ip ), *OR* * array( APCOND_IPINRANGE, range ), *OR* * array( APCOND_AGE_FROM_EDIT, seconds since first edit ), *OR* + * array( APCOND_BLOCKED ), *OR* * similar constructs defined by extensions. * * If $wgEmailAuthentication is off, APCOND_EMAILCONFIRMED will be true for any @@ -1412,14 +1637,6 @@ $wgAvailableRights = array(); */ $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 # @@ -1466,10 +1683,10 @@ $wgCacheEpoch = '20030516000000'; /** * Bump this number when changing the global style sheets and JavaScript. * It should be appended in the query string of static CSS and JS includes, - * to ensure that client-side caches don't keep obsolete copies of global + * to ensure that client-side caches do not keep obsolete copies of global * styles. */ -$wgStyleVersion = '207'; +$wgStyleVersion = '270'; # Server-side caching: @@ -1482,7 +1699,7 @@ $wgStyleVersion = '207'; $wgUseFileCache = false; /** Directory where the cached page will be saved */ -$wgFileCacheDirectory = false; ///< defaults to "{$wgUploadDirectory}/cache"; +$wgFileCacheDirectory = false; ///< defaults to "$wgCacheDirectory/html"; /** * When using the file cache, we can store the cached HTML gzipped to save disk @@ -1581,6 +1798,9 @@ $wgUseSquid = false; /** If you run Squid3 with ESI support, enable this (default:false): */ $wgUseESI = false; +/** Send X-Vary-Options header for better caching (requires patched Squid) */ +$wgUseXVO = false; + /** Internal server name as known to Squid, if different */ # $wgInternalServer = 'http://yourinternal.tld:8000'; $wgInternalServer = $wgServer; @@ -1692,7 +1912,7 @@ $wgAllowExternalImagesFrom = ''; * Or false to disable it */ $wgEnableImageWhitelist = true; - + /** Allows to move images and other media files */ $wgAllowImageMoving = true; @@ -1738,6 +1958,26 @@ $wgSpecialPageCacheUpdates = array( $wgUseTeX = false; /** Location of the texvc binary */ $wgTexvc = './math/texvc'; +/** + * Texvc background color + * use LaTeX color format as used in \special function + * for transparent background use value 'Transparent' for alpha transparency or + * 'transparent' for binary transparency. + */ +$wgTexvcBackgroundColor = 'transparent'; + +/** + * Normally when generating math images, we double-check that the + * directories we want to write to exist, and that files that have + * been generated still exist when we need to bring them up again. + * + * This lets us give useful error messages in case of permission + * problems, and automatically rebuild images that have been lost. + * + * On a big site with heavy NFS traffic this can be slow and flaky, + * so sometimes we want to short-circuit it by setting this to false. + */ +$wgMathCheckFiles = true; # # Profiling / debugging @@ -1766,8 +2006,6 @@ $wgUDPProfilerPort = '3811'; $wgDebugProfiling = false; /** Output debug message on every wfProfileIn/wfProfileOut */ $wgDebugFunctionEntry = 0; -/** Lots of debugging output from SquidUpdate.php */ -$wgDebugSquid = false; /* * Destination for wfIncrStats() data... @@ -1800,6 +2038,18 @@ $wgSearchHighlightBoundaries = version_compare("5.1", PHP_VERSION, "<")? '[\p{Z} : '[ ,.;:!?~!@#$%\^&*\(\)+=\-\\|\[\]"\'<>\n\r\/{}]'; // PHP 5.0 workaround /** + * Set to true to have the search engine count total + * search matches to present in the Special:Search UI. + * Not supported by every search engine shipped with MW. + * + * This could however be slow on larger wikis, and is pretty flaky + * with the current title vs content split. Recommend avoiding until + * that's been worked out cleanly; but this may aid in testing the + * search UI and API to confirm that the result count works. + */ +$wgCountTotalSearchHits = false; + +/** * Template for OpenSearch suggestions, defaults to API action=opensearch * * Sites with heavy load would tipically have these point to a custom @@ -1813,10 +2063,23 @@ $wgOpenSearchTemplate = false; /** * Enable suggestions while typing in search boxes * (results are passed around in OpenSearch format) + * Requires $wgEnableOpenSearchSuggest = true; */ $wgEnableMWSuggest = false; /** + * Enable OpenSearch suggestions requested by MediaWiki. Set this to + * false if you've disabled MWSuggest or another suggestion script and + * want reduce load caused by cached scripts pulling suggestions. + */ +$wgEnableOpenSearchSuggest = true; + +/** + * Expiry time for search suggestion responses + */ +$wgSearchSuggestCacheExpiry = 1200; + +/** * Template for internal MediaWiki suggestion engine, defaults to API action=opensearch * * Placeholders: {searchTerms}, {namespaces}, {dbname} @@ -1850,6 +2113,11 @@ $wgShowEXIF = function_exists( 'exif_read_data' ); * uploads do work. */ $wgRemoteUploads = false; + +/** + * Disable links to talk pages of anonymous users (IPs) in listings on special + * pages like page history, Special:Recentchanges, etc. + */ $wgDisableAnonTalk = false; /** * Do DELETE/INSERT for link updates instead of incremental @@ -1900,7 +2168,7 @@ $wgFileExtensions = array( 'png', 'gif', 'jpg', 'jpeg' ); /** Files with these extensions will never be allowed as uploads. */ $wgFileBlacklist = array( # HTML may contain cookie-stealing JavaScript and web bugs - 'html', 'htm', 'js', 'jsb', 'mhtml', 'mht', + 'html', 'htm', 'js', 'jsb', 'mhtml', 'mht', 'xhtml', 'xht', # PHP scripts may execute arbitrary code on the server 'php', 'phtml', 'php3', 'php4', 'php5', 'phps', # Other types that may be interpreted by some servers @@ -1951,37 +2219,50 @@ $wgNamespacesWithSubpages = array( NS_USER_TALK => true, NS_PROJECT_TALK => true, NS_FILE_TALK => true, + NS_MEDIAWIKI => true, NS_MEDIAWIKI_TALK => true, NS_TEMPLATE_TALK => true, NS_HELP_TALK => true, NS_CATEGORY_TALK => true ); +/** + * Which namespaces have special treatment where they should be preview-on-open + * Internaly only Category: pages apply, but using this extensions (e.g. Semantic MediaWiki) + * can specify namespaces of pages they have special treatment for + */ +$wgPreviewOnOpenNamespaces = array( + NS_CATEGORY => true +); + $wgNamespacesToBeSearchedDefault = array( NS_MAIN => true, ); /** - * Additional namespaces to those in $wgNamespacesToBeSearchedDefault that - * will be added to default search for "project" page inclusive searches - * + * Namespaces to be searched when user clicks the "Help" tab + * on Special:Search + * * Same format as $wgNamespacesToBeSearchedDefault - */ -$wgNamespacesToBeSearchedProject = array( - NS_USER => true, - NS_PROJECT => true, + */ +$wgNamespacesToBeSearchedHelp = array( + NS_PROJECT => true, NS_HELP => true, - NS_CATEGORY => true, ); -$wgUseOldSearchUI = true; // temp testing variable +/** + * If set to true the 'searcheverything' preference will be effective only for logged-in users. + * Useful for big wikis to maintain different search profiles for anonymous and logged-in users. + * + */ +$wgSearchEverythingOnlyLoggedIn = false; /** * Site notice shown at the top of each page * - * This message can contain wiki text, and can also be set through the - * MediaWiki:Sitenotice page. You can also provide a separate message for - * logged-out users using the MediaWiki:Anonnotice page. + * MediaWiki:Sitenotice page, which will override this. You can also + * provide a separate message for logged-out users using the + * MediaWiki:Anonnotice page. */ $wgSiteNotice = ''; @@ -1996,7 +2277,7 @@ $wgSiteNotice = ''; $wgMediaHandlers = array( 'image/jpeg' => 'BitmapHandler', 'image/png' => 'BitmapHandler', - 'image/gif' => 'BitmapHandler', + 'image/gif' => 'GIFHandler', 'image/tiff' => 'TiffHandler', 'image/x-ms-bmp' => 'BmpHandler', 'image/x-bmp' => 'BmpHandler', @@ -2026,8 +2307,8 @@ $wgSharpenParameter = '0x0.4'; /** Reduction in linear dimensions below which sharpening will be enabled */ $wgSharpenReductionThreshold = 0.85; -/** - * Temporary directory used for ImageMagick. The directory must exist. Leave +/** + * Temporary directory used for ImageMagick. The directory must exist. Leave * this set to false to let ImageMagick decide for itself. */ $wgImageMagickTempDir = false; @@ -2084,7 +2365,8 @@ $wgMaxAnimatedGifArea = 1.0e6; * // JPEG is good for photos, but has no transparency support. Bad for diagrams. * $wgTiffThumbnailType = array( 'jpg', 'image/jpeg' ); */ -$wgTiffThumbnailType = false; + $wgTiffThumbnailType = false; + /** * If rendered thumbnail files are older than this timestamp, they * will be rerendered on demand as if the file didn't already exist. @@ -2115,9 +2397,15 @@ $wgIgnoreImageErrors = false; */ $wgGenerateThumbnailOnParse = true; -/** Whether or not to use image resizing */ +/** +* Show thumbnails for old images on the image description page +*/ +$wgShowArchiveThumbnails = true; + +/** Obsolete, always true, kept for compatibility with extensions */ $wgUseImageResize = true; + /** Set $wgCommandLineMode if it's not set already, to avoid notices */ if( !isset( $wgCommandLineMode ) ) { $wgCommandLineMode = false; @@ -2126,6 +2414,13 @@ if( !isset( $wgCommandLineMode ) ) { /** For colorized maintenance script output, is your terminal background dark ? */ $wgCommandLineDarkBg = false; +/** + * Array for extensions to register their maintenance scripts with the + * system. The key is the name of the class and the value is the full + * path to the file + */ +$wgMaintenanceScripts = array(); + # # Recent changes settings # @@ -2136,9 +2431,9 @@ $wgPutIPinRC = true; /** * Recentchanges items are periodically purged; entries older than this many * seconds will go. - * For one week : 7 * 24 * 3600 + * Default: 13 weeks = about three months */ -$wgRCMaxAge = 7 * 24 * 3600; +$wgRCMaxAge = 13 * 7 * 24 * 3600; /** * Filter $wgRCLinkDays by $wgRCMaxAge to avoid showing links for numbers higher than what will be stored. @@ -2167,19 +2462,19 @@ $wgRC2UDPPort = false; /** * Prefix to prepend to each UDP packet. * This can be used to identify the wiki. A script is available called - * mxircecho.py which listens on a UDP port, and uses a prefix ending in a + * mxircecho.py which listens on a UDP port, and uses a prefix ending in a * tab to identify the IRC channel to send the log line to. */ $wgRC2UDPPrefix = ''; /** - * If this is set to true, $wgLocalInterwiki will be prepended to links in the + * If this is set to true, $wgLocalInterwiki will be prepended to links in the * IRC feed. If this is set to a string, that string will be used as the prefix. */ $wgRC2UDPInterwikiPrefix = false; /** - * Set to true to omit "bot" edits (by users with the bot permission) from the + * Set to true to omit "bot" edits (by users with the bot permission) from the * UDP feed. */ $wgRC2UDPOmitBots = false; @@ -2191,16 +2486,6 @@ $wgRC2UDPOmitBots = false; */ $wgEnableNewpagesUserFilter = true; -/** - * Whether to use metadata edition - * This will put categories, language links and allowed templates in a separate text box - * while editing pages - * EXPERIMENTAL - */ -$wgUseMetadataEdit = false; -/** Full name (including namespace) of the page containing templates names that will be allowed as metadata */ -$wgMetadataWhitelist = ''; - # # Copyright and credits settings # @@ -2212,13 +2497,13 @@ $wgEnableCreativeCommonsRdf = false; /** Override for copyright metadata. * TODO: these options need documentation */ -$wgRightsPage = NULL; -$wgRightsUrl = NULL; -$wgRightsText = NULL; -$wgRightsIcon = NULL; +$wgRightsPage = null; +$wgRightsUrl = null; +$wgRightsText = null; +$wgRightsIcon = null; /** Set this to some HTML to override the rights icon with an arbitrary logo */ -$wgCopyrightIcon = NULL; +$wgCopyrightIcon = null; /** Set this to true if you want detailed copyright information forms on Upload. */ $wgUseCopyrightUpload = false; @@ -2251,6 +2536,18 @@ $wgShowCreditsIfMax = true; $wgCapitalLinks = true; /** + * @since 1.16 - This can now be set per-namespace. Some special namespaces (such + * as Special, see MWNamespace::$alwaysCapitalizedNamespaces for the full list) must be + * true by default (and setting them has no effect), due to various things that + * require them to be so. Also, since Talk namespaces need to directly mirror their + * associated content namespaces, the values for those are ignored in favor of the + * subject namespace's setting. Setting for NS_MEDIA is taken automatically from + * NS_FILE. + * EX: $wgCapitalLinkOverrides[ NS_FILE ] = false; + */ +$wgCapitalLinkOverrides = array(); + +/** * List of interwiki prefixes for wikis we'll accept as sources for * Special:Import (for sysops). Since complete page history can be imported, * these should be 'trusted'. @@ -2283,6 +2580,9 @@ $wgExportAllowHistory = true; */ $wgExportMaxHistory = 0; +/** +* Return distinct author list (when not returning full history) +*/ $wgExportAllowListContributors = false ; /** @@ -2299,8 +2599,8 @@ $wgExportAllowListContributors = false ; $wgExportMaxLinkDepth = 0; /** - * Whether to allow the "export all pages in namespace" option - */ +* Whether to allow the "export all pages in namespace" option +*/ $wgExportFromNamespaces = false; /** @@ -2311,6 +2611,7 @@ $wgExportFromNamespaces = false; * May be an array of regexes or a single string for backwards compatibility. * * See http://en.wikipedia.org/wiki/Regular_expression + * Note that each regex needs a beginning/end delimiter, eg: # or / */ $wgSpamRegex = array(); @@ -2375,7 +2676,10 @@ $wgValidateAllHtml = false; /** See list of skins and their symbolic names in languages/Language.php */ $wgDefaultSkin = 'monobook'; -/** Should we allow the user's to select their own skin that will override the default? */ +/** +* Should we allow the user's to select their own skin that will override the default? +* @deprecated in 1.16, use $wgHiddenPrefs[] = 'skin' to disable it +*/ $wgAllowUserSkin = true; /** @@ -2475,11 +2779,21 @@ $wgDefaultUserOptions = array( 'watchdeletion' => 0, 'noconvertlink' => 0, 'gender' => 'unknown', + 'ccmeonemails' => 0, + 'disablemail' => 0, + 'editfont' => 'default', ); -/** Whether or not to allow and use real name fields. Defaults to true. */ +/** + * Whether or not to allow and use real name fields. + * @deprecated in 1.16, use $wgHiddenPrefs[] = 'realname' below to disable real + * names + */ $wgAllowRealName = true; +/** An array of preferences to not show for the user */ +$wgHiddenPrefs = array(); + /***************************************************************************** * Extensions */ @@ -2496,10 +2810,15 @@ $wgExtensionFunctions = array(); $wgSkinExtensionFunctions = array(); /** - * Extension messages files - * Associative array mapping extension name to the filename where messages can be found. - * The file must create a variable called $messages. - * When the messages are needed, the extension should call wfLoadExtensionMessages(). + * Extension messages files. + * + * Associative array mapping extension name to the filename where messages can be + * found. The file should contain variable assignments. Any of the variables + * present in languages/messages/MessagesEn.php may be defined, but $messages + * is the most common. + * + * Variables defined in extensions will override conflicting variables defined + * in the core. * * Example: * $wgExtensionMessagesFiles['ConfirmEdit'] = dirname(__FILE__).'/ConfirmEdit.i18n.php'; @@ -2509,13 +2828,7 @@ $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'; + * @deprecated Use $specialPageAliases in a file referred to by $wgExtensionMessagesFiles */ $wgExtensionAliasesFiles = array(); @@ -2560,8 +2873,8 @@ $wgAutoloadClasses = array(); * <code> * $wgExtensionCredits[$type][] = array( * 'name' => 'Example extension', - * 'version' => 1.9, - * 'svn-revision' => '$LastChangedRevision: 70070 $', + * 'version' => 1.9, + * 'path' => __FILE__, * 'author' => 'Foo Barstein', * 'url' => 'http://wwww.example.com/Example%20Extension/', * 'description' => 'An example extension', @@ -2570,6 +2883,8 @@ $wgAutoloadClasses = array(); * </code> * * Where $type is 'specialpage', 'parserhook', 'variable', 'media' or 'other'. + * Where 'descriptionmsg' can be an array with message key and parameters: + * 'descriptionmsg' => array( 'exampleextension-desc', param1, param2, ... ), */ $wgExtensionCredits = array(); /* @@ -2596,7 +2911,11 @@ $wgUseSiteJs = true; /** Use the site's Cascading Style Sheets (CSS)? */ $wgUseSiteCss = true; -/** Filter for Special:Randompage. Part of a WHERE clause */ +/** + * Filter for Special:Randompage. Part of a WHERE clause + * @deprecated as of 1.16, use the SpecialRandomGetRandomTitle hook +*/ + $wgExtraRandompageSQL = false; /** Allow the "info" action, very inefficient at the moment */ @@ -2608,9 +2927,6 @@ $wgMaxTocLevel = 999; /** Name of the external diff engine to use */ $wgExternalDiffEngine = false; -/** Whether to use inline diff */ -$wgEnableHtmlDiff = false; - /** Use RC Patrolling to check for vandalism */ $wgUseRCPatrol = true; @@ -2646,6 +2962,12 @@ $wgFeedDiffCutoff = 32768; $wgOverrideSiteFeed = array(); /** + * Which feed types should we provide by default? This can include 'rss', + * 'atom', neither, or both. + */ +$wgAdvertisedFeedTypes = array( 'atom' ); + +/** * Additional namespaces. If the namespaces defined in Language.php and * Namespace.php are insufficient, you can create new ones here, for example, * to import Help files in other languages. @@ -2662,7 +2984,7 @@ $wgOverrideSiteFeed = array(); # 102 => "Aide", # 103 => "Discussion_Aide" # ); -$wgExtraNamespaces = NULL; +$wgExtraNamespaces = null; /** * Namespace aliases @@ -2777,10 +3099,10 @@ $wgBrowserBlackList = array( /** * Fake out the timezone that the server thinks it's in. This will be used for * date display and not for what's stored in the DB. Leave to null to retain - * your server's OS-based timezone value. This is the same as the timezone. + * your server's OS-based timezone value. * - * This variable is currently used ONLY for signature formatting, not for - * anything else. + * This variable is currently used only for signature formatting and for local + * time/date parser variables ({{LOCALTIME}} etc.) * * Timezones can be translated by editing MediaWiki messages of type * timezone-nameinlowercase like timezone-utc. @@ -2802,10 +3124,10 @@ $wgLocaltimezone = null; * $wgLocalTZoffset = date("Z") / 60; * * If your server is not configured for the timezone you want, you can set - * this in conjunction with the signature timezone and override the TZ - * environment variable like so: + * this in conjunction with the signature timezone and override the PHP default + * timezone like so: * $wgLocaltimezone="Europe/Berlin"; - * putenv("TZ=$wgLocaltimezone"); + * date_default_timezone_set( $wgLocaltimezone ); * $wgLocalTZoffset = date("Z") / 60; * * Leave at NULL to show times in universal time (UTC/GMT). @@ -2871,6 +3193,7 @@ $wgLogTypes = array( '', * 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 + * Format: logtype => permissiontype */ $wgLogRestrictions = array( 'suppress' => 'suppressionlog' @@ -2881,7 +3204,7 @@ $wgLogRestrictions = array( * * This is associative array of log type => boolean "hide by default" * - * See $wgLogTypes for a list of available log types. + * See $wgLogTypes for a list of available log types. * * For example: * $wgFilterLogTypes => array( @@ -2890,7 +3213,7 @@ $wgLogRestrictions = array( * ); * * Will display show/hide links for the move and import logs. Move logs will be - * hidden by default unless the link is clicked. Import logs will be shown by + * hidden by default unless the link is clicked. Import logs will be shown by * default, and hidden when the link is clicked. * * A message of the form log-show-hide-<type> should be added, and will be used @@ -3025,7 +3348,7 @@ $wgSpecialPageGroups = array( 'Newimages' => 'changes', 'Newpages' => 'changes', 'Log' => 'changes', - 'Tags' => 'changes', + 'Tags' => 'changes', 'Upload' => 'media', 'Listfiles' => 'media', @@ -3034,6 +3357,7 @@ $wgSpecialPageGroups = array( 'Filepath' => 'media', 'Listusers' => 'users', + 'Activeusers' => 'users', 'Listgrouprights' => 'users', 'Ipblocklist' => 'users', 'Contributions' => 'users', @@ -3088,14 +3412,6 @@ $wgSpecialPageGroups = array( ); /** - * Experimental preview feature to fetch rendered text - * over an XMLHttpRequest from JavaScript instead of - * forcing a submit and reload of the whole page. - * Leave disabled unless you're testing it. - */ -$wgLivePreview = false; - -/** * Disable the internal MySQL-based search, to allow it to be * implemented by an extension instead. */ @@ -3180,7 +3496,7 @@ $wgNamespaceRobotPolicies = array(); * 'Main_Page' => 'noindex,follow', * # "Project", not the actual project name! * 'Project:X' => 'index,follow', - * # Needs to be "Abc", not "abc" (unless $wgCapitalLinks is false)! + * # Needs to be "Abc", not "abc" (unless $wgCapitalLinks is false for that namespace)! * 'abc' => 'noindex,nofollow' * ); */ @@ -3199,11 +3515,11 @@ $wgExemptFromUserRobotsControl = null; * Specifies the minimal length of a user password. If set to 0, empty pass- * words are allowed. */ -$wgMinimalPasswordLength = 0; +$wgMinimalPasswordLength = 1; /** * Activate external editor interface for files and pages - * See http://meta.wikimedia.org/wiki/Help:External_editors + * See http://www.mediawiki.org/wiki/Manual:External_editors */ $wgUseExternalEditor = true; @@ -3231,10 +3547,35 @@ $wgDisabledActions = array(); $wgDisableHardRedirects = false; /** - * Use http.dnsbl.sorbs.net to check for open proxies + * Set to false to disable application of access keys and tooltips, + * eg to avoid keyboard conflicts with system keys or as a low-level + * optimization. + */ +$wgEnableTooltipsAndAccesskeys = true; + +/** + * Whether to use DNS blacklists in $wgDnsBlacklistUrls to check for open proxies + * @since 1.16 + */ +$wgEnableDnsBlacklist = false; + +/** + * @deprecated Use $wgEnableDnsBlacklist instead, only kept for backward + * compatibility */ $wgEnableSorbs = false; -$wgSorbsUrl = 'http.dnsbl.sorbs.net.'; + +/** + * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true + * @since 1.16 + */ +$wgDnsBlacklistUrls = array( 'http.dnsbl.sorbs.net.' ); + +/** + * @deprecated Use $wgDnsBlacklistUrls instead, only kept for backward + * compatibility + */ +$wgSorbsUrl = array(); /** * Proxy whitelist, list of addresses that are assumed to be non-proxy despite @@ -3266,7 +3607,7 @@ $wgRateLimits = array( 'subnet' => null, ), 'mailpassword' => array( - 'anon' => NULL, + 'anon' => null, ), 'emailuser' => array( 'user' => null, @@ -3364,9 +3705,14 @@ $wgTrustedMediaFormats= array( $wgAllowSpecialInclusion = true; /** - * Timeout for HTTP requests done via CURL + * Timeout for HTTP requests done internally + */ +$wgHTTPTimeout = 25; + +/** + * Timeout for Asynchronous (background) HTTP requests */ -$wgHTTPTimeout = 3; +$wgAsyncHTTPTimeout = 25; /** * Proxy to use for CURL requests. @@ -3409,7 +3755,7 @@ $wgUpdateRowsPerJob = 500; /** * Number of rows to update per query */ -$wgUpdateRowsPerQuery = 10; +$wgUpdateRowsPerQuery = 100; /** * Enable AJAX framework @@ -3435,7 +3781,7 @@ $wgAjaxWatch = true; $wgAjaxUploadDestCheck = true; /** - * Enable previewing licences via AJAX + * Enable previewing licences via AJAX. Also requires $wgEnableAPI to be true. */ $wgAjaxLicensePreview = true; @@ -3495,9 +3841,9 @@ $wgMaxShellFileSize = 102400; $wgMaxShellTime = 180; /** -* Executable name of PHP cli client (php/php5) -*/ -$wgPhpCli = 'php'; + * Executable path of the PHP cli binary (php/php5). Should be set up on install. + */ +$wgPhpCli = '/usr/bin/php'; /** * DJVU settings @@ -3515,6 +3861,13 @@ $wgDjvuDump = null; $wgDjvuRenderer = null; /** + * Path of the djvutxt DJVU text extraction utility + * Enable this and $wgDjvuDump to enable text layer extraction from djvu files + */ +# $wgDjvuTxt = 'djvutxt'; +$wgDjvuTxt = null; + +/** * Path of the djvutoxml executable * This works like djvudump except much, much slower as of version 3.5. * @@ -3582,6 +3935,24 @@ $wgAPIMaxResultSize = 8388608; $wgAPIMaxUncachedDiffs = 1; /** + * Log file or URL (TCP or UDP) to log API requests to, or false to disable + * API request logging + */ +$wgAPIRequestLog = false; + +/** + * Cache the API help text for up to an hour. Disable this during API + * debugging and development + */ +$wgAPICacheHelp = true; + +/** + * Set the timeout for the API help text cache. Ignored if $wgAPICacheHelp + * is false. + */ +$wgAPICacheHelpTimeout = 60*60; + +/** * Parser test suite files to be run by parserTests.php when no specific * filename is passed to it. * @@ -3595,6 +3966,21 @@ $wgParserTestFiles = array( ); /** + * If configured, specifies target CodeReview installation to send test + * result data from 'parserTests.php --upload' + * + * Something like this: + * $wgParserTestRemote = array( + * 'api-url' => 'http://www.mediawiki.org/w/api.php', + * 'repo' => 'MediaWiki', + * 'suite' => 'ParserTests', + * 'path' => '/trunk/phase3', // not used client-side; for reference + * 'secret' => 'qmoicj3mc4mcklmqw', // Shared secret used in HMAC validation + * ); + */ +$wgParserTestRemote = false; + +/** * Break out of framesets. This can be used to prevent external sites from * framing your site with ads. */ @@ -3652,6 +4038,12 @@ $wgParserConf = array( $wgLinkHolderBatchSize = 1000; /** + * By default MediaWiki does not register links pointing to same server in externallinks dataset, + * use this value to override: + */ +$wgRegisterInternalExternals = false; + +/** * Hooks that are used for outputting exceptions. Format is: * $wgExceptionHooks[] = $funcname * or: @@ -3661,8 +4053,11 @@ $wgLinkHolderBatchSize = 1000; $wgExceptionHooks = array(); /** - * Page property link table invalidation lists. Should only be set by exten- - * sions. + * Page property link table invalidation lists. When a page property + * changes, this may require other link tables to be updated (eg + * adding __HIDDENCAT__ means the hiddencat tracking category will + * have been added, so the categorylinks table needs to be rebuilt). + * This array can be added to by extensions. */ $wgPagePropLinkInvalidations = array( 'hiddencat' => 'categorylinks', @@ -3687,7 +4082,7 @@ $wgMaximumMovedPages = 100; /** * Fix double redirects after a page move. - * Tends to conflict with page move vandalism, use only on a private wiki. + * Tends to conflict with page move vandalism, use only on a private wiki. */ $wgFixDoubleRedirects = false; @@ -3709,7 +4104,7 @@ $wgMaxRedirects = 1; * other namespaces cannot be invalidated by this variable. */ $wgInvalidRedirectTargets = array( 'Filepath', 'Mypage', 'Mytalk' ); - + /** * Array of namespaces to generate a sitemap for when the * maintenance/generateSitemap.php script is run, or false if one is to be ge- @@ -3744,11 +4139,15 @@ $wgEdititis = false; $wgUniversalEditButton = true; /** - * Allow id's that don't conform to HTML4 backward compatibility requirements. - * This is currently for testing; if all goes well, this option will be removed - * and the functionality will be enabled universally. + * Should we allow a broader set of characters in id attributes, per HTML5? If + * not, use only HTML 4-compatible IDs. This option is for testing -- when the + * functionality is ready, it will be on by default with no option. + * + * Currently this appears to work fine in Chrome 4 and 5, Firefox 3.5 and 3.6, IE6 + * and 8, and Opera 10.50, but it fails in Opera 10.10: Unicode IDs don't seem + * to work as anchors. So not quite ready for general use yet. */ -$wgEnforceHtmlIds = true; +$wgExperimentalHtmlIds = false; /** * Search form behavior @@ -3758,6 +4157,28 @@ $wgEnforceHtmlIds = true; $wgUseTwoButtonsSearchForm = true; /** + * Search form behavior for Vector skin only + * true = use an icon search button + * false = use Go & Search buttons + */ +$wgVectorUseSimpleSearch = false; + +/** + * Watch and unwatch as an icon rather than a link for Vector skin only + * true = use an icon watch/unwatch button + * false = use watch/unwatch text link + */ +$wgVectorUseIconWatch = false; + +/** + * Add extra stylesheets for Vector - This is only being used so that we can play around with different options while + * keeping our CSS code in the SVN and not having to change the main Vector styles. This will probably go away later on. + * null = add no extra styles + * array = list of style paths relative to skins/vector/ + */ +$wgVectorExtraStyles = null; + +/** * Preprocessor caching threshold */ $wgPreprocessorCacheThreshold = 1000; @@ -3791,3 +4212,122 @@ $wgInvalidUsernameCharacters = '@'; * modify the user rights of those users via Special:UserRights */ $wgUserrightsInterwikiDelimiter = '@'; + +/** + * Configuration for processing pool control, for use in high-traffic wikis. + * An implementation is provided in the PoolCounter extension. + * + * This configuration array maps pool types to an associative array. The only + * defined key in the associative array is "class", which gives the class name. + * The remaining elements are passed through to the class as constructor + * parameters. Example: + * + * $wgPoolCounterConf = array( 'Article::view' => array( + * 'class' => 'PoolCounter_Client', + * ... any extension-specific options... + * ); + */ +$wgPoolCounterConf = null; + +/** + * Use some particular type of external authentication. The specific + * authentication module you use will normally require some extra settings to + * be specified. + * + * null indicates no external authentication is to be used. Otherwise, + * $wgExternalAuthType must be the name of a non-abstract class that extends + * ExternalUser. + * + * Core authentication modules can be found in includes/extauth/. + */ +$wgExternalAuthType = null; + +/** + * Configuration for the external authentication. This may include arbitrary + * keys that depend on the authentication mechanism. For instance, + * authentication against another web app might require that the database login + * info be provided. Check the file where your auth mechanism is defined for + * info on what to put here. + */ +$wgExternalAuthConfig = array(); + +/** + * When should we automatically create local accounts when external accounts + * already exist, if using ExternalAuth? Can have three values: 'never', + * 'login', 'view'. 'view' requires the external database to support cookies, + * and implies 'login'. + * + * TODO: Implement 'view' (currently behaves like 'login'). + */ +$wgAutocreatePolicy = 'login'; + +/** + * Policies for how each preference is allowed to be changed, in the presence + * of external authentication. The keys are preference keys, e.g., 'password' + * or 'emailaddress' (see Preferences.php et al.). The value can be one of the + * following: + * + * - local: Allow changes to this pref through the wiki interface but only + * apply them locally (default). + * - semiglobal: Allow changes through the wiki interface and try to apply them + * to the foreign database, but continue on anyway if that fails. + * - global: Allow changes through the wiki interface, but only let them go + * through if they successfully update the foreign database. + * - message: Allow no local changes for linked accounts; replace the change + * form with a message provided by the auth plugin, telling the user how to + * change the setting externally (maybe providing a link, etc.). If the auth + * plugin provides no message for this preference, hide it entirely. + * + * Accounts that are not linked to an external account are never affected by + * this setting. You may want to look at $wgHiddenPrefs instead. + * $wgHiddenPrefs supersedes this option. + * + * TODO: Implement message, global. + */ +$wgAllowPrefChange = array(); + + +/** + * Settings for incoming cross-site AJAX requests: + * Newer browsers support cross-site AJAX when the target resource allows requests + * from the origin domain by the Access-Control-Allow-Origin header. + * This is currently only used by the API (requests to api.php) + * $wgCrossSiteAJAXdomains can be set using a wildcard syntax: + * + * '*' matches any number of characters + * '?' matches any 1 character + * + * Example: + $wgCrossSiteAJAXdomains = array( + 'www.mediawiki.org', + '*.wikipedia.org', + '*.wikimedia.org', + '*.wiktionary.org', + ); + * + */ +$wgCrossSiteAJAXdomains = array(); + +/** + * Domains that should not be allowed to make AJAX requests, + * even if they match one of the domains allowed by $wgCrossSiteAJAXdomains + * Uses the same syntax as $wgCrossSiteAJAXdomains + */ + +$wgCrossSiteAJAXdomainExceptions = array(); + +/** + * The minimum amount of memory that MediaWiki "needs"; MediaWiki will try to raise PHP's memory limit if it's below this amount. + */ +$wgMemoryLimit = "50M"; + +/** + * To disable file delete/restore temporarily + */ +$wgUploadMaintenance = false; + +/** + * Use old names for change_tags indices. + */ +$wgOldChangeTagsIndex = false; + diff --git a/includes/Defines.php b/includes/Defines.php index 8de6c5a1..7be569af 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -18,6 +18,7 @@ define( 'DBO_IGNORE', 4 ); define( 'DBO_TRX', 8 ); define( 'DBO_DEFAULT', 16 ); define( 'DBO_PERSISTENT', 32 ); +define( 'DBO_SYSDBA', 64 ); //for oracle maintenance /**#@-*/ # Valid database indexes @@ -102,7 +103,7 @@ define( 'CACHE_ANYTHING', -1 ); // Use anything, as long as it works define( 'CACHE_NONE', 0 ); // Do not cache define( 'CACHE_DB', 1 ); // Store cache objects in the DB define( 'CACHE_MEMCACHED', 2 ); // MemCached, must specify servers in $wgMemCacheServers -define( 'CACHE_ACCEL', 3 ); // eAccelerator or Turck, whichever is available +define( 'CACHE_ACCEL', 3 ); // eAccelerator define( 'CACHE_DBA', 4 ); // Use PHP's DBA extension to store in a DBM-style database /**#@-*/ @@ -200,6 +201,7 @@ require_once dirname(__FILE__).'/normal/UtfNormalDefines.php'; # Hook support constants define( 'MW_SUPPORTS_EDITFILTERMERGED', 1 ); define( 'MW_SUPPORTS_PARSERFIRSTCALLINIT', 1 ); +define( 'MW_SUPPORTS_LOCALISATIONCACHE', 1 ); # Allowed values for Parser::$mOutputType # Parameter to Parser::startExternalParse(). @@ -227,3 +229,4 @@ define( 'APCOND_INGROUPS', 4 ); define( 'APCOND_ISIP', 5 ); define( 'APCOND_IPINRANGE', 6 ); define( 'APCOND_AGE_FROM_EDIT', 7 ); +define( 'APCOND_BLOCKED', 8 ); diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php index 8e7caf63..75df0fd5 100644 --- a/includes/DjVuImage.php +++ b/includes/DjVuImage.php @@ -224,7 +224,7 @@ class DjVuImage { * @return string */ function retrieveMetaData() { - global $wgDjvuToXML, $wgDjvuDump; + global $wgDjvuToXML, $wgDjvuDump, $wgDjvuTxt; if ( isset( $wgDjvuDump ) ) { # djvudump is faster as of version 3.5 # http://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583 @@ -242,6 +242,30 @@ class DjVuImage { } else { $xml = null; } + # Text layer + if ( isset( $wgDjvuTxt ) ) { + wfProfileIn( 'djvutxt' ); + $cmd = wfEscapeShellArg( $wgDjvuTxt ) . ' --detail=page ' . wfEscapeShellArg( $this->mFilename ) ; + wfDebug( __METHOD__.": $cmd\n" ); + $txt = wfShellExec( $cmd, $retval ); + wfProfileOut( 'djvutxt' ); + if( $retval == 0) { + # Get rid of invalid UTF-8, strip control characters + if( is_callable( 'iconv' ) ) { + wfSuppressWarnings(); + $txt = iconv( "UTF-8","UTF-8//IGNORE", $txt ); + wfRestoreWarnings(); + } else { + $txt = UtfNormal::cleanUp( $txt ); + } + $txt = preg_replace( "/[\013\035\037]/", "", $txt ); + $txt = htmlspecialchars($txt); + $txt = preg_replace( "/\((page\s[\d-]*\s[\d-]*\s[\d-]*\s[\d-]*\s*\"([^<]*?)\"\s*|)\)/s", "<PAGE value=\"$2\" />", $txt ); + $txt = "<DjVuTxt>\n<HEAD></HEAD>\n<BODY>\n" . $txt . "</BODY>\n</DjVuTxt>\n"; + $xml = preg_replace( "/<DjVuXML>/", "<mw-djvu><DjVuXML>", $xml ); + $xml = $xml . $txt. '</mw-djvu>' ; + } + } return $xml; } diff --git a/includes/DoubleRedirectJob.php b/includes/DoubleRedirectJob.php index 889beecf..0857408a 100644 --- a/includes/DoubleRedirectJob.php +++ b/includes/DoubleRedirectJob.php @@ -1,13 +1,19 @@ <?php +/** + * Job to fix double redirects after moving a page + * + * @ingroup JobQueue + */ 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 + * @param $reason String: the reason for the fix, see message double-redirect-fixed-<reason> + * @param $redirTitle Title: the title which has changed, redirects pointing to this title are fixed + * @param $destTitle Not used */ public static function fixRedirects( $reason, $redirTitle, $destTitle = false ) { # Need to use the master to get the redirect table updated in the same transaction @@ -116,7 +122,7 @@ class DoubleRedirectJob extends Job { /** * Get the final destination of a redirect - * Returns false if the specified title is not a redirect, or if it is a circular redirect + * @return 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 ); diff --git a/includes/EditPage.php b/includes/EditPage.php index 3589b52d..b4cbf0de 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -17,37 +17,38 @@ * usually the same, but they are now allowed to be different. */ class EditPage { - const AS_SUCCESS_UPDATE = 200; - const AS_SUCCESS_NEW_ARTICLE = 201; - const AS_HOOK_ERROR = 210; - const AS_FILTERING = 211; - const AS_HOOK_ERROR_EXPECTED = 212; - const AS_BLOCKED_PAGE_FOR_USER = 215; - const AS_CONTENT_TOO_BIG = 216; - const AS_USER_CANNOT_EDIT = 217; - const AS_READ_ONLY_PAGE_ANON = 218; - const AS_READ_ONLY_PAGE_LOGGED = 219; - const AS_READ_ONLY_PAGE = 220; - const AS_RATE_LIMITED = 221; - const AS_ARTICLE_WAS_DELETED = 222; - const AS_NO_CREATE_PERMISSION = 223; - const AS_BLANK_ARTICLE = 224; - const AS_CONFLICT_DETECTED = 225; - const AS_SUMMARY_NEEDED = 226; - const AS_TEXTBOX_EMPTY = 228; - const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229; - const AS_OK = 230; - const AS_END = 231; - const AS_SPAM_ERROR = 232; - const AS_IMAGE_REDIRECT_ANON = 233; - const AS_IMAGE_REDIRECT_LOGGED = 234; + const AS_SUCCESS_UPDATE = 200; + const AS_SUCCESS_NEW_ARTICLE = 201; + const AS_HOOK_ERROR = 210; + const AS_FILTERING = 211; + const AS_HOOK_ERROR_EXPECTED = 212; + const AS_BLOCKED_PAGE_FOR_USER = 215; + const AS_CONTENT_TOO_BIG = 216; + const AS_USER_CANNOT_EDIT = 217; + const AS_READ_ONLY_PAGE_ANON = 218; + const AS_READ_ONLY_PAGE_LOGGED = 219; + const AS_READ_ONLY_PAGE = 220; + const AS_RATE_LIMITED = 221; + const AS_ARTICLE_WAS_DELETED = 222; + const AS_NO_CREATE_PERMISSION = 223; + const AS_BLANK_ARTICLE = 224; + const AS_CONFLICT_DETECTED = 225; + const AS_SUMMARY_NEEDED = 226; + const AS_TEXTBOX_EMPTY = 228; + const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229; + const AS_OK = 230; + const AS_END = 231; + const AS_SPAM_ERROR = 232; + const AS_IMAGE_REDIRECT_ANON = 233; + const AS_IMAGE_REDIRECT_LOGGED = 234; var $mArticle; var $mTitle; var $action; - var $mMetaData = ''; var $isConflict = false; var $isCssJsSubpage = false; + var $isCssSubpage = false; + var $isJsSubpage = false; var $deletedSinceEdit = false; var $formtype; var $firsttime; @@ -65,13 +66,14 @@ class EditPage { #var $mPreviewTemplates; var $mParserOutput; var $mBaseRevision = false; + var $mShowSummaryField = true; # Form values var $save = false, $preview = false, $diff = false; var $minoredit = false, $watchthis = false, $recreate = false; - var $textbox1 = '', $textbox2 = '', $summary = ''; + var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false; var $edittime = '', $section = '', $starttime = ''; - var $oldid = 0, $editintro = '', $scrolltop = null; + var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true; # Placeholders for text injection by hooks (must be HTML) # extensions should take care to _append_ to the present value @@ -81,6 +83,8 @@ class EditPage { public $editFormTextAfterWarn; public $editFormTextAfterTools; public $editFormTextBottom; + public $editFormTextAfterContent; + public $previewTextAfterContent; /* $didSave should be set to true whenever an article was succesfully altered. */ public $didSave = false; @@ -103,15 +107,20 @@ class EditPage { $this->editFormTextBeforeContent = $this->editFormTextAfterWarn = $this->editFormTextAfterTools = - $this->editFormTextBottom = ""; + $this->editFormTextBottom = + $this->editFormTextAfterContent = + $this->previewTextAfterContent = + $this->mPreloadText = ""; } - + function getArticle() { return $this->mArticle; } + /** * Fetch initial editing page content. + * @returns mixed string on success, $def_text for invalid sections * @private */ function getContent( $def_text = '' ) { @@ -120,7 +129,10 @@ class EditPage { wfProfileIn( __METHOD__ ); # Get variables from query string :P $section = $wgRequest->getVal( 'section' ); - $preload = $wgRequest->getVal( 'preload' ); + + $preload = $wgRequest->getVal( 'preload', + // Custom preload text for new sections + $section === 'new' ? 'MediaWiki:addsection-preload' : '' ); $undoafter = $wgRequest->getVal( 'undoafter' ); $undo = $wgRequest->getVal( 'undo' ); @@ -134,7 +146,7 @@ class EditPage { $wgMessageCache->loadAllMessages( $lang ); $text = wfMsgGetKey( $message, false, $lang, false ); if( wfEmptyMsg( $message, $text ) ) - $text = ''; + $text = $this->getPreloadedText( $preload ); } else { # If requested, preload some text. $text = $this->getPreloadedText( $preload ); @@ -150,10 +162,10 @@ class EditPage { # Undoing a specific edit overrides section editing; section-editing # doesn't work with undoing. if ( $undoafter ) { - $undorev = Revision::newFromId($undo); - $oldrev = Revision::newFromId($undoafter); + $undorev = Revision::newFromId( $undo ); + $oldrev = Revision::newFromId( $undoafter ); } else { - $undorev = Revision::newFromId($undo); + $undorev = Revision::newFromId( $undo ); $oldrev = $undorev ? $undorev->getPrevious() : null; } @@ -165,7 +177,7 @@ class EditPage { $undorev->getPage() == $this->mArticle->getID() && !$undorev->isDeleted( Revision::DELETED_TEXT ) && !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) { - + $undotext = $this->mArticle->getUndoText( $undorev, $oldrev ); if ( $undotext === false ) { # Warn the user that something went wrong @@ -192,6 +204,7 @@ class EditPage { if ( $section == 'new' ) { $text = $this->getPreloadedText( $preload ); } else { + // Get section edit text (returns $def_text for invalid sections) $text = $wgParser->getSection( $text, $section, $def_text ); } } @@ -201,6 +214,11 @@ class EditPage { return $text; } + /** Use this method before edit() to preload some text into the edit box */ + public function setPreloadedText( $text ) { + $this->mPreloadText = $text; + } + /** * Get the contents of a page from its title and remove includeonly tags * @@ -208,12 +226,14 @@ class EditPage { * @return string The contents of the page. */ protected function getPreloadedText( $preload ) { - if ( $preload === '' ) { + if ( !empty( $this->mPreloadText ) ) { + return $this->mPreloadText; + } elseif ( $preload === '' ) { return ''; } else { $preloadTitle = Title::newFromText( $preload ); if ( isset( $preloadTitle ) && $preloadTitle->userCanRead() ) { - $rev = Revision::newFromTitle($preloadTitle); + $rev = Revision::newFromTitle( $preloadTitle ); if ( is_object( $rev ) ) { $text = $rev->getText(); // TODO FIXME: AAAAAAAAAAA, this shouldn't be implementing @@ -226,96 +246,7 @@ class EditPage { } } - /** - * This is the function that extracts metadata from the article body on the first view. - * To turn the feature on, set $wgUseMetadataEdit = true ; in LocalSettings - * and set $wgMetadataWhitelist to the *full* title of the template whitelist - */ - function extractMetaDataFromArticle () { - global $wgUseMetadataEdit, $wgMetadataWhitelist, $wgContLang; - $this->mMetaData = ''; - if ( !$wgUseMetadataEdit ) return; - if ( $wgMetadataWhitelist == '' ) return; - $s = ''; - $t = $this->getContent(); - - # MISSING : <nowiki> filtering - - # Categories and language links - $t = explode ( "\n" , $t ); - $catlow = strtolower ( $wgContLang->getNsText( NS_CATEGORY ) ); - $cat = $ll = array(); - foreach ( $t AS $key => $x ) { - $y = trim ( strtolower ( $x ) ); - while ( substr ( $y , 0 , 2 ) == '[[' ) { - $y = explode ( ']]' , trim ( $x ) ); - $first = array_shift ( $y ); - $first = explode ( ':' , $first ); - $ns = array_shift ( $first ); - $ns = trim ( str_replace ( '[' , '' , $ns ) ); - if ( $wgContLang->getLanguageName( $ns ) || strtolower ( $ns ) == $catlow ) { - $add = '[[' . $ns . ':' . implode ( ':' , $first ) . ']]'; - if ( strtolower ( $ns ) == $catlow ) $cat[] = $add; - else $ll[] = $add; - $x = implode ( ']]' , $y ); - $t[$key] = $x; - $y = trim ( strtolower ( $x ) ); - } else { - $x = implode ( ']]' , $y ); - $y = trim ( strtolower ( $x ) ); - } - } - } - if ( count ( $cat ) ) $s .= implode ( ' ' , $cat ) . "\n"; - if ( count ( $ll ) ) $s .= implode ( ' ' , $ll ) . "\n"; - $t = implode ( "\n" , $t ); - - # Load whitelist - $sat = array () ; # stand-alone-templates; must be lowercase - $wl_title = Title::newFromText ( $wgMetadataWhitelist ); - $wl_article = new Article ( $wl_title ); - $wl = explode ( "\n" , $wl_article->getContent() ); - foreach ( $wl AS $x ) { - $isentry = false; - $x = trim ( $x ); - while ( substr ( $x , 0 , 1 ) == '*' ) { - $isentry = true; - $x = trim ( substr ( $x , 1 ) ); - } - if ( $isentry ) { - $sat[] = strtolower ( $x ); - } - - } - - # Templates, but only some - $t = explode ( '{{' , $t ); - $tl = array () ; - foreach ( $t AS $key => $x ) { - $y = explode ( '}}' , $x , 2 ); - if ( count ( $y ) == 2 ) { - $z = $y[0]; - $z = explode ( '|' , $z ); - $tn = array_shift ( $z ); - if ( in_array ( strtolower ( $tn ) , $sat ) ) { - $tl[] = '{{' . $y[0] . '}}'; - $t[$key] = $y[1]; - $y = explode ( '}}' , $y[1] , 2 ); - } - else $t[$key] = '{{' . $x; - } - else if ( $key != 0 ) $t[$key] = '{{' . $x; - else $t[$key] = $x; - } - if ( count ( $tl ) ) $s .= implode ( ' ' , $tl ); - $t = implode ( '' , $t ); - - $t = str_replace ( "\n\n\n" , "\n" , $t ); - $this->mArticle->mContent = $t; - $this->mMetaData = $s; - } - - /* + /* * Check if a page was deleted while the user was editing it, before submit. * Note that we rely on the logging table, which hasn't been always there, * but that doesn't matter, because this only applies to brand new @@ -352,9 +283,9 @@ class EditPage { * the newly-edited page. */ function edit() { - global $wgOut, $wgUser, $wgRequest; + global $wgOut, $wgRequest, $wgUser; // Allow extensions to modify/prevent this form or submission - if ( !wfRunHooks( 'AlternateEdit', array( &$this ) ) ) { + if ( !wfRunHooks( 'AlternateEdit', array( $this ) ) ) { return; } @@ -374,16 +305,24 @@ class EditPage { } if ( wfReadOnly() && $this->save ) { - // Force preview - $this->save = false; - $this->preview = true; + // Force preview + $this->save = false; + $this->preview = true; } $wgOut->addScriptFile( 'edit.js' ); + + if ( $wgUser->getOption( 'uselivepreview', false ) ) { + $wgOut->includeJQuery(); + $wgOut->addScriptFile( 'preview.js' ); + } + // Bug #19334: textarea jumps when editing articles in IE8 + $wgOut->addStyle( 'common/IE80Fixes.css', 'screen', 'IE 8' ); + $permErrors = $this->getEditPermissionErrors(); if ( $permErrors ) { - wfDebug( __METHOD__.": User can't edit\n" ); - $this->readOnlyPage( $this->getContent(), true, $permErrors, 'edit' ); + wfDebug( __METHOD__ . ": User can't edit\n" ); + $this->readOnlyPage( $this->getContent( false ), true, $permErrors, 'edit' ); wfProfileOut( __METHOD__ ); return; } else { @@ -398,12 +337,11 @@ class EditPage { if ( $this->previewOnOpen() ) { $this->formtype = 'preview'; } else { - $this->extractMetaDataFromArticle () ; $this->formtype = 'initial'; } } } - + // If they used redlink=1 and the page exists, redirect to the main article if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) { $wgOut->redirect( $this->mTitle->getFullURL() ); @@ -414,6 +352,8 @@ class EditPage { $this->isConflict = false; // css / js subpages of user pages get a special treatment $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage(); + $this->isCssSubpage = $this->mTitle->isCssSubpage(); + $this->isJsSubpage = $this->mTitle->isJsSubpage(); $this->isValidCssJsSubpage = $this->mTitle->isValidCssJsSubpage(); # Show applicable editing introductions @@ -456,7 +396,7 @@ class EditPage { # First time through: get contents, set time for conflict # checking, etc. if ( 'initial' == $this->formtype || $this->firsttime ) { - if ( $this->initialiseForm() === false) { + if ( $this->initialiseForm() === false ) { $this->noSuchSectionPage(); wfProfileOut( __METHOD__."-business-end" ); wfProfileOut( __METHOD__ ); @@ -464,13 +404,15 @@ class EditPage { } if ( !$this->mTitle->getArticleId() ) wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) ); + else + wfRunHooks( 'EditFormInitialText', array( $this ) ); } $this->showEditForm(); wfProfileOut( __METHOD__."-business-end" ); wfProfileOut( __METHOD__ ); } - + protected function getEditPermissionErrors() { global $wgUser; $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ); @@ -482,8 +424,8 @@ class EditPage { # Ignore some permissions errors when a user is just previewing/viewing diffs $remove = array(); foreach( $permErrors as $error ) { - if ( ($this->preview || $this->diff) && - ($error[0] == 'blockedtext' || $error[0] == 'autoblockedtext') ) + if ( ( $this->preview || $this->diff ) && + ( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' ) ) { $remove[] = $error; } @@ -515,7 +457,7 @@ class EditPage { * @return bool */ protected function previewOnOpen() { - global $wgRequest, $wgUser; + global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces; if ( $wgRequest->getVal( 'preview' ) == 'yes' ) { // Explicit override from request return true; @@ -528,7 +470,10 @@ class EditPage { } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) { // Standard preference behaviour return true; - } elseif ( !$this->mTitle->exists() && $this->mTitle->getNamespace() == NS_CATEGORY ) { + } elseif ( !$this->mTitle->exists() && + isset($wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()]) && + $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] ) + { // Categories are special return true; } else { @@ -537,13 +482,36 @@ class EditPage { } /** + * Does this EditPage class support section editing? + * This is used by EditPage subclasses to indicate their ui cannot handle section edits + * + * @return bool + */ + protected function isSectionEditSupported() { + return true; + } + + /** + * Returns the URL to use in the form's action attribute. + * This is used by EditPage subclasses when simply customizing the action + * variable in the constructor is not enough. This can be used when the + * EditPage lives inside of a Special page rather than a custom page action. + * + * @param Title $title The title for which is being edited (where we go to for &action= links) + * @return string + */ + protected function getActionURL( Title $title ) { + return $title->getLocalURL( array( 'action' => $this->action ) ); + } + + /** * @todo document * @param $request */ function importFormData( &$request ) { global $wgLang, $wgUser; - $fname = 'EditPage::importFormData'; - wfProfileIn( $fname ); + + wfProfileIn( __METHOD__ ); # Section edit can come from either the form or a link $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) ); @@ -553,8 +521,17 @@ class EditPage { # 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' ) ); + if ( !$request->getCheck('wpTextbox2') ) { + // Skip this if wpTextbox2 has input, it indicates that we came + // from a conflict page with raw page text, not a custom form + // modified by subclasses + wfProfileIn( get_class($this)."::importContentFormData" ); + $textbox1 = $this->importContentFormData( $request ); + if ( isset($textbox1) ) + $this->textbox1 = $textbox1; + wfProfileOut( get_class($this)."::importContentFormData" ); + } + # Truncate for whole multibyte characters. +5 bytes for ellipsis $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250, '' ); @@ -568,7 +545,7 @@ class EditPage { if ( is_null( $this->edittime ) ) { # If the form is incomplete, force to preview. - wfDebug( "$fname: Form data appears to be incomplete\n" ); + wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" ); wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" ); $this->preview = true; } else { @@ -585,23 +562,23 @@ class EditPage { # if the user hits enter in the comment box. # The unmarked state will be assumed to be a save, # if the form seems otherwise complete. - wfDebug( "$fname: Passed token check.\n" ); + wfDebug( __METHOD__ . ": Passed token check.\n" ); } else if ( $this->diff ) { # Failed token check, but only requested "Show Changes". - wfDebug( "$fname: Failed token check; Show Changes requested.\n" ); + wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" ); } else { # Page might be a hack attempt posted from # an external site. Preview instead of saving. - wfDebug( "$fname: Failed token check; forcing preview\n" ); + wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" ); $this->preview = true; } } $this->save = !$this->preview && !$this->diff; - if ( !preg_match( '/^\d{14}$/', $this->edittime )) { + if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) { $this->edittime = null; } - if ( !preg_match( '/^\d{14}$/', $this->starttime )) { + if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) { $this->starttime = null; } @@ -611,8 +588,8 @@ class EditPage { $this->watchthis = $request->getCheck( 'wpWatchthis' ); # Don't force edit summaries when a user is editing their own user or talk page - if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) && - $this->mTitle->getText() == $wgUser->getName() ) + if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) && + $this->mTitle->getText() == $wgUser->getName() ) { $this->allowBlankSummary = true; } else { @@ -622,10 +599,8 @@ class EditPage { $this->autoSumm = $request->getText( 'wpAutoSummary' ); } else { # Not a posted form? Start with nothing. - wfDebug( "$fname: Not a posted form.\n" ); + wfDebug( __METHOD__ . ": Not a posted form.\n" ); $this->textbox1 = ''; - $this->textbox2 = ''; - $this->mMetaData = ''; $this->summary = ''; $this->edittime = ''; $this->starttime = wfTimestampNow(); @@ -634,7 +609,7 @@ class EditPage { $this->save = false; $this->diff = false; $this->minoredit = false; - $this->watchthis = false; + $this->watchthis = $request->getBool( 'watchthis', false ); // Watch may be overriden by request parameters $this->recreate = false; if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) { @@ -643,18 +618,39 @@ class EditPage { elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) { $this->summary = $request->getText( 'summary' ); } - + if ( $request->getVal( 'minor' ) ) { $this->minoredit = true; } } + $this->bot = $request->getBool( 'bot', true ); + $this->nosummary = $request->getBool( 'nosummary' ); + + // FIXME: unused variable? $this->oldid = $request->getInt( 'oldid' ); $this->live = $request->getCheck( 'live' ); - $this->editintro = $request->getText( 'editintro' ); + $this->editintro = $request->getText( 'editintro', + // Custom edit intro for new sections + $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' ); + + wfProfileOut( __METHOD__ ); - wfProfileOut( $fname ); + // Allow extensions to modify form data + wfRunHooks( 'EditPage::importFormData', array( $this, $request ) ); + } + + /** + * Subpage overridable method for extracting the page content data from the + * posted form to be placed in $this->textbox1, if using customized input + * this method should be overrided and return the page text that will be used + * for saving, preview parsing and so on... + * + * @praram WebRequest $request + */ + protected function importContentFormData( &$request ) { + return; // Don't do anything, EditPage already extracted wpTextbox1 } /** @@ -688,28 +684,49 @@ class EditPage { $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1</div>", 'editinginterface' ); } - # Show a warning message when someone creates/edits a user (talk) page but the user does not exists + # Show a warning message when someone creates/edits a user (talk) page but the user does not exist + # Show log extract when the user is currently blocked if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) { $parts = explode( '/', $this->mTitle->getText(), 2 ); $username = $parts[0]; - $id = User::idFromName( $username ); + $user = User::newFromName( $username, false /* allow IP users*/ ); $ip = User::isIP( $username ); - if ( $id == 0 && !$ip ) { - $wgOut->wrapWikiMsg( '<div class="mw-userpage-userdoesnotexist error">$1</div>', + if ( !$user->isLoggedIn() && !$ip ) { # User does not exist + $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1</div>", array( 'userpage-userdoesnotexist', $username ) ); + } else if ( $user->isBlocked() ) { # Show log extract if the user is currently blocked + LogEventsList::showLogExtract( + $wgOut, + 'block', + $user->getUserPage()->getPrefixedText(), + '', + array( + 'lim' => 1, + 'showIfEmpty' => false, + 'msgKey' => array( + 'blocked-notice-logextract', + $user->getName() # Support GENDER in notice + ) + ) + ); } } # Try to add a custom edit intro, or use the standard one if this is not possible. if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) { if ( $wgUser->isLoggedIn() ) { - $wgOut->wrapWikiMsg( '<div class="mw-newarticletext">$1</div>', 'newarticletext' ); + $wgOut->wrapWikiMsg( "<div class=\"mw-newarticletext\">\n$1</div>", 'newarticletext' ); } else { - $wgOut->wrapWikiMsg( '<div class="mw-newarticletextanon">$1</div>', 'newarticletextanon' ); + $wgOut->wrapWikiMsg( "<div class=\"mw-newarticletextanon\">\n$1</div>", 'newarticletextanon' ); } } - # Give a notice if the user is editing a deleted page... + # Give a notice if the user is editing a deleted/moved page... if ( !$this->mTitle->exists() ) { - $this->showDeletionLog( $wgOut ); + LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->mTitle->getPrefixedText(), + '', array( 'lim' => 10, + 'conds' => array( "log_action != 'revision'" ), + 'showIfEmpty' => false, + 'msgKey' => array( 'recreate-moveddeleted-warn') ) + ); } } @@ -742,12 +759,10 @@ class EditPage { global $wgFilterCallback, $wgUser, $wgOut, $wgParser; global $wgMaxArticleSize; - $fname = 'EditPage::attemptSave'; - wfProfileIn( $fname ); - wfProfileIn( "$fname-checks" ); + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-checks' ); - if ( !wfRunHooks( 'EditPage::attemptSave', array( &$this ) ) ) - { + if ( !wfRunHooks( 'EditPage::attemptSave', array( $this ) ) ) { wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" ); return self::AS_HOOK_ERROR; } @@ -763,10 +778,6 @@ class EditPage { } } - # Reintegrate metadata - if ( $this->mMetaData != '' ) $this->textbox1 .= "\n" . $this->mMetaData ; - $this->mMetaData = '' ; - # Check for spam $match = self::matchSummarySpamRegex( $this->summary ); if ( $match === false ) { @@ -778,104 +789,107 @@ class EditPage { $pdbk = $this->mTitle->getPrefixedDBkey(); $match = str_replace( "\n", '', $match ); wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" ); - wfProfileOut( "$fname-checks" ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ . '-checks' ); + wfProfileOut( __METHOD__ ); return self::AS_SPAM_ERROR; } 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 ); + wfProfileOut( __METHOD__ . '-checks' ); + wfProfileOut( __METHOD__ ); return self::AS_FILTERING; } 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 ); + wfProfileOut( __METHOD__ . '-checks' ); + wfProfileOut( __METHOD__ ); return self::AS_HOOK_ERROR; } elseif ( $this->hookError != '' ) { # ...or the hook could be expecting us to produce an error - wfProfileOut( "$fname-checks" ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ . '-checks' ); + wfProfileOut( __METHOD__ ); return self::AS_HOOK_ERROR_EXPECTED; } if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) { # Check block state against master, thus 'false'. - wfProfileOut( "$fname-checks" ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ . '-checks' ); + wfProfileOut( __METHOD__ ); return self::AS_BLOCKED_PAGE_FOR_USER; } - $this->kblength = (int)(strlen( $this->textbox1 ) / 1024); + $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 ); if ( $this->kblength > $wgMaxArticleSize ) { // Error will be displayed by showEditForm() $this->tooBig = true; - wfProfileOut( "$fname-checks" ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ . '-checks' ); + wfProfileOut( __METHOD__ ); return self::AS_CONTENT_TOO_BIG; } - if ( !$wgUser->isAllowed('edit') ) { + if ( !$wgUser->isAllowed( 'edit' ) ) { if ( $wgUser->isAnon() ) { - wfProfileOut( "$fname-checks" ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ . '-checks' ); + wfProfileOut( __METHOD__ ); return self::AS_READ_ONLY_PAGE_ANON; - } - else { - wfProfileOut( "$fname-checks" ); - wfProfileOut( $fname ); + } else { + wfProfileOut( __METHOD__ . '-checks' ); + wfProfileOut( __METHOD__ ); return self::AS_READ_ONLY_PAGE_LOGGED; } } if ( wfReadOnly() ) { - wfProfileOut( "$fname-checks" ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ . '-checks' ); + wfProfileOut( __METHOD__ ); return self::AS_READ_ONLY_PAGE; } if ( $wgUser->pingLimiter() ) { - wfProfileOut( "$fname-checks" ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ . '-checks' ); + wfProfileOut( __METHOD__ ); return self::AS_RATE_LIMITED; } # If the article has been deleted while editing, don't save it without # confirmation if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) { - wfProfileOut( "$fname-checks" ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ . '-checks' ); + wfProfileOut( __METHOD__ ); return self::AS_ARTICLE_WAS_DELETED; } - wfProfileOut( "$fname-checks" ); + wfProfileOut( __METHOD__ . '-checks' ); # If article is new, insert it. $aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE ); if ( 0 == $aid ) { // Late check for create permission, just in case *PARANOIA* if ( !$this->mTitle->userCan( 'create' ) ) { - wfDebug( "$fname: no create permission\n" ); - wfProfileOut( $fname ); + wfDebug( __METHOD__ . ": no create permission\n" ); + wfProfileOut( __METHOD__ ); return self::AS_NO_CREATE_PERMISSION; } # Don't save a new article if it's blank. - if ( '' == $this->textbox1 ) { - wfProfileOut( $fname ); + if ( $this->textbox1 == '' ) { + wfProfileOut( __METHOD__ ); return self::AS_BLANK_ARTICLE; } // Run post-section-merge edit filter if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) { # Error messages etc. could be handled within the hook... - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return self::AS_HOOK_ERROR; + } elseif ( $this->hookError != '' ) { + # ...or the hook could be expecting us to produce an error + wfProfileOut( __METHOD__ ); + return self::AS_HOOK_ERROR_EXPECTED; } - + # Handle the user preference to force summaries here. Check if it's not a redirect. if ( !$this->allowBlankSummary && !Title::newFromRedirect( $this->textbox1 ) ) { if ( md5( $this->summary ) == $this->autoSumm ) { $this->missingSummary = true; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return self::AS_SUMMARY_NEEDED; } } @@ -885,7 +899,7 @@ class EditPage { $this->mArticle->insertNewArticle( $this->textbox1, $this->summary, $this->minoredit, $this->watchthis, false, $isComment, $bot ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return self::AS_SUCCESS_NEW_ARTICLE; } @@ -894,7 +908,7 @@ class EditPage { $this->mArticle->clear(); # Force reload of dates, etc. $this->mArticle->forUpdate( true ); # Lock the article - wfDebug("timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n"); + wfDebug( "timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n" ); if ( $this->mArticle->getTimestamp() != $this->edittime ) { $this->isConflict = true; @@ -904,32 +918,32 @@ class EditPage { // Probably a duplicate submission of a new comment. // This can happen when squid resends a request after // a timeout but the first one actually went through. - wfDebug( "EditPage::editForm duplicate new section submission; trigger edit conflict!\n" ); + wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" ); } else { // New comment; suppress conflict. $this->isConflict = false; - wfDebug( "EditPage::editForm conflict suppressed; new section\n" ); + wfDebug( __METHOD__ .": conflict suppressed; new section\n" ); } } } $userid = $wgUser->getId(); - + # Suppress edit conflict with self, except for section edits where merging is required. - if ( $this->isConflict && $this->section == '' && $this->userWasLastToEdit($userid,$this->edittime) ) { - wfDebug( "EditPage::editForm Suppressing edit conflict, same user.\n" ); + if ( $this->isConflict && $this->section == '' && $this->userWasLastToEdit( $userid, $this->edittime ) ) { + wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); $this->isConflict = false; } if ( $this->isConflict ) { - wfDebug( "EditPage::editForm conflict! getting section '$this->section' for time '$this->edittime' (article time '" . + wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '" . $this->mArticle->getTimestamp() . "')\n" ); $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime ); } else { - wfDebug( "EditPage::editForm getting section '$this->section'\n" ); + wfDebug( __METHOD__ . ": getting section '$this->section'\n" ); $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary ); } if ( is_null( $text ) ) { - wfDebug( "EditPage::editForm activating conflict; section replace failed.\n" ); + wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" ); $this->isConflict = true; $text = $this->textbox1; // do not try to merge here! } else if ( $this->isConflict ) { @@ -937,16 +951,16 @@ class EditPage { if ( $this->mergeChangesInto( $text ) ) { // Successful merge! Maybe we should tell the user the good news? $this->isConflict = false; - wfDebug( "EditPage::editForm Suppressing edit conflict, successful merge.\n" ); + wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" ); } else { $this->section = ''; $this->textbox1 = $text; - wfDebug( "EditPage::editForm Keeping edit conflict, failed merge.\n" ); + wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" ); } } if ( $this->isConflict ) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return self::AS_CONFLICT_DETECTED; } @@ -955,36 +969,42 @@ class EditPage { // Run post-section-merge edit filter if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) { # Error messages etc. could be handled within the hook... - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return self::AS_HOOK_ERROR; + } elseif ( $this->hookError != '' ) { + # ...or the hook could be expecting us to produce an error + wfProfileOut( __METHOD__ ); + return self::AS_HOOK_ERROR_EXPECTED; } # Handle the user preference to force summaries here, but not for null edits - if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp($oldtext,$text) + if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp( $oldtext, $text ) && !Title::newFromRedirect( $text ) ) # check if it's not a redirect { if ( md5( $this->summary ) == $this->autoSumm ) { $this->missingSummary = true; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return self::AS_SUMMARY_NEEDED; } } # And a similar thing for new sections if ( $this->section == 'new' && !$this->allowBlankSummary ) { - if (trim($this->summary) == '') { + if ( trim( $this->summary ) == '' ) { $this->missingSummary = true; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return self::AS_SUMMARY_NEEDED; } } # All's well - wfProfileIn( "$fname-sectionanchor" ); + wfProfileIn( __METHOD__ . '-sectionanchor' ); $sectionanchor = ''; if ( $this->section == 'new' ) { if ( $this->textbox1 == '' ) { $this->missingComment = true; + wfProfileOut( __METHOD__ . '-sectionanchor' ); + wfProfileOut( __METHOD__ ); return self::AS_TEXTBOX_EMPTY; } if ( $this->summary != '' ) { @@ -1001,11 +1021,11 @@ class EditPage { $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); # we can't deal with anchors, includes, html etc in the header for now, # headline would need to be parsed to improve this - if ( $hasmatch and strlen($matches[2]) > 0 ) { + if ( $hasmatch and strlen( $matches[2] ) > 0 ) { $sectionanchor = $wgParser->guessSectionNameFromWikiText( $matches[2] ); } } - wfProfileOut( "$fname-sectionanchor" ); + wfProfileOut( __METHOD__ . '-sectionanchor' ); // Save errors may fall down to the edit form, but we've now // merged the section into full text. Clear the section field @@ -1015,26 +1035,26 @@ class EditPage { $this->section = ''; // Check for length errors again now that the section is merged in - $this->kblength = (int)(strlen( $text ) / 1024); + $this->kblength = (int)( strlen( $text ) / 1024 ); if ( $this->kblength > $wgMaxArticleSize ) { $this->tooBig = true; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return self::AS_MAX_ARTICLE_SIZE_EXCEEDED; } # update the article here if ( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit, - $this->watchthis, $bot, $sectionanchor ) ) + $this->watchthis, $bot, $sectionanchor ) ) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return self::AS_SUCCESS_UPDATE; } else { $this->isConflict = true; } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return self::AS_END; } - + /** * Check if no edits were made by other users since * the time a user started editing the page. Limit to @@ -1045,7 +1065,7 @@ class EditPage { $dbw = wfGetDB( DB_MASTER ); $res = $dbw->select( 'revision', 'rev_user', - array( + array( 'rev_page' => $this->mArticle->getId(), 'rev_timestamp > '.$dbw->addQuotes( $dbw->timestamp($edittime) ) ), @@ -1058,7 +1078,7 @@ class EditPage { } return true; } - + /** * Check given input text against $wgSpamRegex, and return the text of the first match. * @return mixed -- matching string or false @@ -1069,7 +1089,7 @@ class EditPage { $regexes = (array)$wgSpamRegex; return self::matchSpamRegexInternal( $text, $regexes ); } - + /** * Check given input text against $wgSpamRegex, and return the text of the first match. * @return mixed -- matching string or false @@ -1079,7 +1099,7 @@ class EditPage { $regexes = (array)$wgSummarySpamRegex; return self::matchSpamRegexInternal( $text, $regexes ); } - + protected static function matchSpamRegexInternal( $text, $regexes ) { foreach( $regexes as $regex ) { $matches = array(); @@ -1093,10 +1113,25 @@ class EditPage { /** * Initialise form fields in the object * Called on the first invocation, e.g. when a user clicks an edit link + * @returns bool -- if the requested section is valid */ function initialiseForm() { + global $wgUser; $this->edittime = $this->mArticle->getTimestamp(); $this->textbox1 = $this->getContent( false ); + // activate checkboxes if user wants them to be always active + # Sort out the "watch" checkbox + if ( $wgUser->getOption( 'watchdefault' ) ) { + # Watch all edits + $this->watchthis = true; + } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { + # Watch creations + $this->watchthis = true; + } elseif ( $this->mTitle->userIsWatching() ) { + # Already watched + $this->watchthis = true; + } + if ( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true; if ( $this->textbox1 === false ) return false; wfProxyCheck(); return true; @@ -1115,7 +1150,7 @@ class EditPage { $wgOut->setPageTitle( wfMsg( $msg, $wgTitle->getPrefixedText() ) ); } else { # Use the title defined by DISPLAYTITLE magic word when present - if ( isset($this->mParserOutput) + if ( isset( $this->mParserOutput ) && ( $dt = $this->mParserOutput->getDisplayTitle() ) !== false ) { $title = $dt; } else { @@ -1132,22 +1167,19 @@ class EditPage { * near the top, for captchas and the like. */ function showEditForm( $formCallback=null ) { - global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize, $wgTitle, $wgRequest; + global $wgOut, $wgUser, $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)) + if ( is_null( $wgTitle ) ) return; - $fname = 'EditPage::showEditForm'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $sk = $wgUser->getSkin(); - wfRunHooks( 'EditPage::showEditForm:initial', array( &$this ) ) ; - #need to parse the preview early so that we know which templates are used, #otherwise users with "show preview after edit box" will get a blank list #we parse this near the beginning so that setHeaders can do the title @@ -1157,18 +1189,158 @@ class EditPage { $previewOutput = $this->getPreviewText(); } + wfRunHooks( 'EditPage::showEditForm:initial', array( &$this ) ); + $this->setHeaders(); # Enabled article-related sidebar, toplinks, etc. $wgOut->setArticleRelated( true ); + if ( $this->showHeader() === false ) + return; + + $action = htmlspecialchars($this->getActionURL($wgTitle)); + + if ( $wgUser->getOption( 'showtoolbar' ) and !$this->isCssJsSubpage ) { + # prepare toolbar for edit buttons + $toolbar = EditPage::getEditToolbar(); + } else { + $toolbar = ''; + } + + + $wgOut->addHTML( $this->editFormPageTop ); + + if ( $wgUser->getOption( 'previewontop' ) ) { + $this->displayPreviewArea( $previewOutput, true ); + } + + $wgOut->addHTML( $this->editFormTextTop ); + + $templates = $this->getTemplates(); + $formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != ''); + + $hiddencats = $this->mArticle->getHiddenCategories(); + $formattedhiddencats = $sk->formatHiddenCategories( $hiddencats ); + + if ( $this->wasDeletedSinceLastEdit() && 'save' != $this->formtype ) { + $wgOut->wrapWikiMsg( + "<div class='error mw-deleted-while-editing'>\n$1</div>", + 'deletedwhileediting' ); + } elseif ( $this->wasDeletedSinceLastEdit() ) { + // Hide the toolbar and edit area, user can click preview to get it back + // Add an confirmation checkbox and explanation. + $toolbar = ''; + // @todo move this to a cleaner conditional instead of blanking a variable + } + $wgOut->addHTML( <<<HTML +{$toolbar} +<form id="editform" name="editform" method="post" action="$action" enctype="multipart/form-data"> +HTML +); + + if ( is_callable( $formCallback ) ) { + call_user_func_array( $formCallback, array( &$wgOut ) ); + } + + wfRunHooks( 'EditPage::showEditForm:fields', array( &$this, &$wgOut ) ); + + // Put these up at the top to ensure they aren't lost on early form submission + $this->showFormBeforeText(); + + if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) { + $wgOut->addHTML( + '<div class="mw-confirm-recreate">' . + $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment ) ) . + Xml::checkLabel( wfMsg( 'recreate' ), 'wpRecreate', 'wpRecreate', false, + array( 'title' => $sk->titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ) + ) . + '</div>' + ); + } + + # 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. + if ( $this->missingSummary || + ( $this->section == 'new' && $this->nosummary ) ) + $wgOut->addHTML( Xml::hidden( 'wpIgnoreBlankSummary', true ) ); + $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); + $wgOut->addHTML( Xml::hidden( 'wpAutoSummary', $autosumm ) ); + + $wgOut->addHTML( Xml::hidden( 'oldid', $this->mArticle->getOldID() ) ); + + if ( $this->section == 'new' ) { + $this->showSummaryInput( true, $this->summary ); + $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) ); + } + + $wgOut->addHTML( $this->editFormTextBeforeContent ); + if ( $this->isConflict ) { - $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1</div>", 'explainconflict' ); + // In an edit conflict bypass the overrideable content form method + // and fallback to the raw wpTextbox1 since editconflicts can't be + // resolved between page source edits and custom ui edits using the + // custom edit ui. + $this->showTextbox1( null, $this->getContent() ); + } else { + $this->showContentForm(); + } + + $wgOut->addHTML( $this->editFormTextAfterContent ); + + $wgOut->addWikiText( $this->getCopywarn() ); + if ( isset($this->editFormTextAfterWarn) && $this->editFormTextAfterWarn !== '' ) + $wgOut->addHTML( $this->editFormTextAfterWarn ); + + $this->showStandardInputs(); + + $this->showFormAfterText(); + + $this->showTosSummary(); + $this->showEditTools(); + + $wgOut->addHTML( <<<HTML +{$this->editFormTextAfterTools} +<div class='templatesUsed'> +{$formattedtemplates} +</div> +<div class='hiddencats'> +{$formattedhiddencats} +</div> +HTML +); + + if ( $this->isConflict ) + $this->showConflict(); + + $wgOut->addHTML( $this->editFormTextBottom ); + $wgOut->addHTML( "</form>\n" ); + if ( !$wgUser->getOption( 'previewontop' ) ) { + $this->displayPreviewArea( $previewOutput, false ); + } - $this->textbox2 = $this->textbox1; - $this->textbox1 = $this->getContent(); + wfProfileOut( __METHOD__ ); + } + + protected function showHeader() { + global $wgOut, $wgUser, $wgTitle, $wgMaxArticleSize, $wgLang; + if ( $this->isConflict ) { + $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1</div>", 'explainconflict' ); $this->edittime = $this->mArticle->getTimestamp(); } else { + if ( $this->section != '' && !$this->isSectionEditSupported() ) { + // We use $this->section to much before this and getVal('wgSection') directly in other places + // at this point we can't reset $this->section to '' to fallback to non-section editing. + // Someone is welcome to try refactoring though + $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' ); + return false; + } + if ( $this->section != '' && $this->section != 'new' ) { $matches = array(); if ( !$this->summary && !$this->preview && !$this->diff ) { @@ -1183,15 +1355,15 @@ class EditPage { } if ( $this->missingComment ) { - $wgOut->wrapWikiMsg( '<div id="mw-missingcommenttext">$1</div>', 'missingcommenttext' ); + $wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1</div>", 'missingcommenttext' ); } if ( $this->missingSummary && $this->section != 'new' ) { - $wgOut->wrapWikiMsg( '<div id="mw-missingsummary">$1</div>', 'missingsummary' ); + $wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1</div>", 'missingsummary' ); } if ( $this->missingSummary && $this->section == 'new' ) { - $wgOut->wrapWikiMsg( '<div id="mw-missingcommentheader">$1</div>', 'missingcommentheader' ); + $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1</div>", 'missingcommentheader' ); } if ( $this->hookError !== '' ) { @@ -1201,6 +1373,7 @@ class EditPage { if ( !$this->checkUnicodeCompliantBrowser() ) { $wgOut->addWikiMsg( 'nonunicodebrowser' ); } + if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) { // Let sysop know that this will make private content public if saved @@ -1220,41 +1393,37 @@ class EditPage { if ( wfReadOnly() ) { $wgOut->wrapWikiMsg( "<div id=\"mw-read-only-warning\">\n$1\n</div>", array( 'readonlywarning', wfReadOnlyReason() ) ); } elseif ( $wgUser->isAnon() && $this->formtype != 'preview' ) { - $wgOut->wrapWikiMsg( '<div id="mw-anon-edit-warning">$1</div>', 'anoneditwarning' ); + $wgOut->wrapWikiMsg( "<div id=\"mw-anon-edit-warning\">\n$1</div>", 'anoneditwarning' ); } else { if ( $this->isCssJsSubpage ) { # Check the skin exists - if ( $this->isValidCssJsSubpage ) { - if ( $this->formtype !== 'preview' ) { - $wgOut->addWikiMsg( 'usercssjsyoucanpreview' ); - } - } else { + if ( !$this->isValidCssJsSubpage ) { $wgOut->addWikiMsg( 'userinvalidcssjstitle', $wgTitle->getSkinFromCssJsSubpage() ); } + if ( $this->formtype !== 'preview' ) { + if ( $this->isCssSubpage ) + $wgOut->addWikiMsg( 'usercssyoucanpreview' ); + if ( $this->isJsSubpage ) + $wgOut->addWikiMsg( 'userjsyoucanpreview' ); + } } } - $classes = array(); // Textarea CSS - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - } elseif ( $this->mTitle->isProtected( 'edit' ) ) { + if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) { # Is the title semi-protected? if ( $this->mTitle->isSemiProtected() ) { $noticeMsg = 'semiprotectedpagewarning'; - $classes[] = 'mw-textarea-sprotected'; } else { # Then it must be protected based on static groups (regular) $noticeMsg = 'protectedpagewarning'; - $classes[] = 'mw-textarea-protected'; } - $wgOut->addHTML( "<div class='mw-warning-with-logexcerpt'>\n" ); - $wgOut->addWikiMsg( $noticeMsg ); - LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle->getPrefixedText(), '', 1 ); - $wgOut->addHTML( "</div>\n" ); + LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle->getPrefixedText(), '', + array( 'lim' => 1, 'msgKey' => array( $noticeMsg ) ) ); } if ( $this->mTitle->isCascadeProtected() ) { # Is this page under cascading protection from some source pages? list($cascadeSources, /* $restrictions */) = $this->mTitle->getCascadeProtectionSources(); - $notice = "<div class='mw-cascadeprotectedwarning'>$1\n"; + $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n"; $cascadeSourcesCount = count( $cascadeSources ); if ( $cascadeSourcesCount > 0 ) { # Explain, and list the titles responsible @@ -1266,12 +1435,17 @@ class EditPage { $wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', $cascadeSourcesCount ) ); } if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) { - $wgOut->wrapWikiMsg( '<div class="mw-titleprotectedwarning">$1</div>', 'titleprotectedwarning' ); + LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle->getPrefixedText(), '', + array( 'lim' => 1, + 'showIfEmpty' => false, + 'msgKey' => array( 'titleprotectedwarning' ), + 'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ) ); } if ( $this->kblength === false ) { - $this->kblength = (int)(strlen( $this->textbox1 ) / 1024); + $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 ); } + if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) { $wgOut->addHTML( "<div class='error' id='mw-edit-longpageerror'>\n" ); $wgOut->addWikiMsg( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgLang->formatNum( $wgMaxArticleSize ) ); @@ -1281,245 +1455,109 @@ class EditPage { $wgOut->addWikiMsg( 'longpagewarning', $wgLang->formatNum( $this->kblength ) ); $wgOut->addHTML( "</div>\n" ); } + } - $q = 'action='.$this->action; - #if ( "no" == $redirect ) { $q .= "&redirect=no"; } - $action = $wgTitle->escapeLocalURL( $q ); - - $summary = wfMsg( 'summary' ); - $subject = wfMsg( 'subject' ); - - $cancel = $sk->makeKnownLink( $wgTitle->getPrefixedText(), - wfMsgExt('cancel', array('parseinline')) ); - $separator = wfMsgExt( 'pipe-separator' , 'escapenoentities' ); - $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' )); - $edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'. - htmlspecialchars( wfMsg( 'edithelp' ) ).'</a> '. - htmlspecialchars( wfMsg( 'newwindow' ) ); - - global $wgRightsText; - if ( $wgRightsText ) { - $copywarnMsg = array( 'copyrightwarning', - '[[' . wfMsgForContent( 'copyrightpage' ) . ']]', - $wgRightsText ); - } else { - $copywarnMsg = array( 'copyrightwarning2', - '[[' . wfMsgForContent( 'copyrightpage' ) . ']]' ); - } - - if ( $wgUser->getOption('showtoolbar') and !$this->isCssJsSubpage ) { - # prepare toolbar for edit buttons - $toolbar = EditPage::getEditToolbar(); - } else { - $toolbar = ''; - } - - // activate checkboxes if user wants them to be always active - if ( !$this->preview && !$this->diff ) { - # Sort out the "watch" checkbox - if ( $wgUser->getOption( 'watchdefault' ) ) { - # Watch all edits - $this->watchthis = true; - } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { - # Watch creations - $this->watchthis = true; - } elseif ( $this->mTitle->userIsWatching() ) { - # Already watched - $this->watchthis = true; - } - - # May be overriden by request parameters - if( $wgRequest->getBool( 'watchthis' ) ) { - $this->watchthis = true; - } - - if ( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true; - } - - $wgOut->addHTML( $this->editFormPageTop ); + /** + * Standard summary input and label (wgSummary), abstracted so EditPage + * subclasses may reorganize the form. + * Note that you do not need to worry about the label's for=, it will be + * inferred by the id given to the input. You can remove them both by + * passing array( 'id' => false ) to $userInputAttrs. + * + * @param $summary The value of the summary input + * @param $labelText The html to place inside the label + * @param $userInputAttrs An array of attrs to use on the input + * @param $userSpanAttrs An array of attrs to use on the span inside the label + * + * @return array An array in the format array( $label, $input ) + */ + function getSummaryInput($summary = "", $labelText = null, $inputAttrs = null, $spanLabelAttrs = null) { + $inputAttrs = ( is_array($inputAttrs) ? $inputAttrs : array() ) + array( + 'id' => 'wpSummary', + 'maxlength' => '200', + 'tabindex' => '1', + 'size' => 60, + 'spellcheck' => 'true', + ); + + $spanLabelAttrs = ( is_array($spanLabelAttrs) ? $spanLabelAttrs : array() ) + array( + 'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary', + 'id' => "wpSummaryLabel" + ); - if ( $wgUser->getOption( 'previewontop' ) ) { - $this->displayPreviewArea( $previewOutput, true ); + $label = null; + if ( $labelText ) { + $label = Xml::tags( 'label', $inputAttrs['id'] ? array( 'for' => $inputAttrs['id'] ) : null, $labelText ); + $label = Xml::tags( 'span', $spanLabelAttrs, $label ); } + $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs ); - $wgOut->addHTML( $this->editFormTextTop ); - - # 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 = $wgContLang->recodeForEdit( $this->summary ); + return array( $label, $input ); + } - # 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 = ''; - if ( !$wgRequest->getBool( 'nosummary' ) ) { - # Add a class if 'missingsummary' is triggered to allow styling of the summary line - $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; - - $commentsubject = - Xml::tags( 'label', array( 'for' => 'wpSummary' ), $subject ); - $commentsubject = - Xml::tags( 'span', array( 'class' => $summaryClass, 'id' => "wpSummaryLabel" ), - $commentsubject ); - $commentsubject .= ' '; - $commentsubject .= Xml::input( 'wpSummary', - 60, - $summarytext, - array( - 'id' => 'wpSummary', - 'maxlength' => '200', - 'tabindex' => '1' - ) ); - } - $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 = ''; + /** + * @param bool $isSubjectPreview true if this is the section subject/title + * up top, or false if this is the comment + * summary down below the textarea + * @param string $summary The text of the summary to display + * @return string + */ + protected function showSummaryInput( $isSubjectPreview, $summary = "" ) { + global $wgOut, $wgContLang; + # Add a class if 'missingsummary' is triggered to allow styling of the summary line + $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; + if ( $isSubjectPreview ) { + if ( $this->nosummary ) + return; } else { - $commentsubject = ''; - - # Add a class if 'missingsummary' is triggered to allow styling of the summary line - $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; - - $editsummary = Xml::tags( 'label', array( 'for' => 'wpSummary' ), $summary ); - $editsummary = Xml::tags( 'span', array( 'class' => $summaryClass, 'id' => "wpSummaryLabel" ), - $editsummary ) . ' '; - - $editsummary .= Xml::input( 'wpSummary', - 60, - $summarytext, - array( - 'id' => 'wpSummary', - 'maxlength' => '200', - 'tabindex' => '1' - ) ); - - // No idea where this is closed. - $editsummary = Xml::openElement( 'div', array( 'class' => 'editOptions' ) ) - . $editsummary . '<br/>'; - - $summarypreview = ''; - if ( $summarytext && $this->preview ) { - $summarypreview = - Xml::tags( 'div', - array( 'class' => 'mw-summary-preview' ), - wfMsg( 'summary-preview' ) . - $sk->commentBlock( $this->summary, $this->mTitle ) - ); - } - $subjectpreview = ''; - } - $commentsubject .= $summaryhiddens; - - # Set focus to the edit box on load, except on preview or diff, where it would interfere with the display - if ( !$this->preview && !$this->diff ) { - $wgOut->setOnloadHandler( 'document.editform.wpTextbox1.focus()' ); - } - $templates = $this->getTemplates(); - $formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != ''); - - $hiddencats = $this->mArticle->getHiddenCategories(); - $formattedhiddencats = $sk->formatHiddenCategories( $hiddencats ); - - global $wgUseMetadataEdit ; - if ( $wgUseMetadataEdit ) { - $metadata = $this->mMetaData ; - $metadata = htmlspecialchars( $wgContLang->recodeForEdit( $metadata ) ) ; - $top = wfMsgWikiHtml( 'metadata_help' ); - /* ToDo: Replace with clean code */ - $ew = $wgUser->getOption( 'editwidth' ); - if ( $ew ) $ew = " style=\"width:100%\""; - else $ew = ''; - $cols = $wgUser->getIntOption( 'cols' ); - /* /ToDo */ - $metadata = $top . "<textarea name='metadata' rows='3' cols='{$cols}'{$ew}>{$metadata}</textarea>" ; - } - else $metadata = "" ; - - $recreate = ''; - if ( $this->wasDeletedSinceLastEdit() ) { - if ( 'save' != $this->formtype ) { - $wgOut->wrapWikiMsg( - "<div class='error mw-deleted-while-editing'>\n$1</div>", - 'deletedwhileediting' ); - } else { - // Hide the toolbar and edit area, user can click preview to get it back - // Add an confirmation checkbox and explanation. - $toolbar = ''; - $recreate = '<div class="mw-confirm-recreate">' . - $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment ) ) . - Xml::checkLabel( wfMsg( 'recreate' ), 'wpRecreate', 'wpRecreate', false, - array( 'title' => $sk->titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ) - ) . '</div>'; - } - } - - $tabindex = 2; - - $checkboxes = $this->getCheckboxes( $tabindex, $sk, - array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) ); - - $checkboxhtml = implode( $checkboxes, "\n" ); - - $buttons = $this->getEditButtons( $tabindex ); - $buttonshtml = implode( $buttons, "\n" ); - - $safemodehtml = $this->checkUnicodeCompliantBrowser() - ? '' : Xml::hidden( 'safemode', '1' ); - - $wgOut->addHTML( <<<END -{$toolbar} -<form id="editform" name="editform" method="post" action="$action" enctype="multipart/form-data"> -END -); - - if ( is_callable( $formCallback ) ) { - call_user_func_array( $formCallback, array( &$wgOut ) ); + if ( !$this->mShowSummaryField ) + return; } + $summary = $wgContLang->recodeForEdit( $summary ); + $labelText = wfMsgExt( $isSubjectPreview ? 'subject' : 'summary', 'parseinline' ); + list($label, $input) = $this->getSummaryInput($summary, $labelText, array( 'class' => $summaryClass ), array()); + $wgOut->addHTML("{$label} {$input}"); + } - wfRunHooks( 'EditPage::showEditForm:fields', array( &$this, &$wgOut ) ); - - // Put these up at the top to ensure they aren't lost on early form submission - $this->showFormBeforeText(); - - $wgOut->addHTML( <<<END -{$recreate} -{$commentsubject} -{$subjectpreview} -{$this->editFormTextBeforeContent} -END -); - $this->showTextbox1( $classes ); - - $wgOut->wrapWikiMsg( "<div id=\"editpage-copywarn\">\n$1\n</div>", $copywarnMsg ); - $wgOut->addHTML( <<<END -{$this->editFormTextAfterWarn} -{$metadata} -{$editsummary} -{$summarypreview} -{$checkboxhtml} -{$safemodehtml} -END -); + /** + * @param bool $isSubjectPreview true if this is the section subject/title + * up top, or false if this is the comment + * summary down below the textarea + * @param string $summary The text of the summary to display + * @return string + */ + protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) { + if ( !$summary || ( !$this->preview && !$this->diff ) ) + return ""; + + global $wgParser, $wgUser; + $sk = $wgUser->getSkin(); + + if ( $isSubjectPreview ) + $summary = wfMsgForContent( 'newsectionsummary', $wgParser->stripSectionName( $summary ) ); - $wgOut->addHTML( -"<div class='editButtons'> -{$buttonshtml} - <span class='editHelp'>{$cancel}{$separator}{$edithelp}</span> -</div><!-- editButtons --> -</div><!-- editOptions -->"); + $summary = wfMsgExt( 'subject-preview', 'parseinline' ) . $sk->commentBlock( $summary, $this->mTitle, !!$isSubjectPreview ); + return Xml::tags( 'div', array( 'class' => 'mw-summary-preview' ), $summary ); + } + protected function showFormBeforeText() { + global $wgOut; + $section = htmlspecialchars( $this->section ); + $wgOut->addHTML( <<<INPUTS +<input type='hidden' value="{$section}" name="wpSection" /> +<input type='hidden' value="{$this->starttime}" name="wpStarttime" /> +<input type='hidden' value="{$this->edittime}" name="wpEdittime" /> +<input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" /> + +INPUTS + ); + if ( !$this->checkUnicodeCompliantBrowser() ) + $wgOut->addHTML(Xml::hidden( 'safemode', '1' )); + } + + protected function showFormAfterText() { + global $wgOut, $wgUser; /** * To make it harder for someone to slip a user a page * which submits an edit form to the wiki without their @@ -1532,68 +1570,64 @@ END * include the constant suffix to prevent editing from * broken text-mangling proxies. */ - $token = htmlspecialchars( $wgUser->editToken() ); - $wgOut->addHTML( "\n<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" ); - - $this->showEditTools(); - - $wgOut->addHTML( <<<END -{$this->editFormTextAfterTools} -<div class='templatesUsed'> -{$formattedtemplates} -</div> -<div class='hiddencats'> -{$formattedhiddencats} -</div> -END -); - - if ( $this->isConflict && wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) { - $wgOut->wrapWikiMsg( '==$1==', "yourdiff" ); - - $de = new DifferenceEngine( $this->mTitle ); - $de->setText( $this->textbox2, $this->textbox1 ); - $de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) ); - - $wgOut->wrapWikiMsg( '==$1==', "yourtext" ); - $this->showTextbox2(); - } - $wgOut->addHTML( $this->editFormTextBottom ); - $wgOut->addHTML( "</form>\n" ); - if ( !$wgUser->getOption( 'previewontop' ) ) { - $this->displayPreviewArea( $previewOutput, false ); - } - - wfProfileOut( $fname ); + $wgOut->addHTML( "\n" . Xml::hidden( "wpEditToken", $wgUser->editToken() ) . "\n" ); } - protected function showFormBeforeText() { - global $wgOut; - $wgOut->addHTML( " -<input type='hidden' value=\"" . htmlspecialchars( $this->section ) . "\" name=\"wpSection\" /> -<input type='hidden' value=\"{$this->starttime}\" name=\"wpStarttime\" />\n -<input type='hidden' value=\"{$this->edittime}\" name=\"wpEdittime\" />\n -<input type='hidden' value=\"{$this->scrolltop}\" name=\"wpScrolltop\" id=\"wpScrolltop\" />\n" ); + /** + * Subpage overridable method for printing the form for page content editing + * By default this simply outputs wpTextbox1 + * Subclasses can override this to provide a custom UI for editing; + * be it a form, or simply wpTextbox1 with a modified content that will be + * reverse modified when extracted from the post data. + * Note that this is basically the inverse for importContentFormData + * + * @praram WebRequest $request + */ + protected function showContentForm() { + $this->showTextbox1(); } - - protected function showTextbox1( $classes ) { + + /** + * Method to output wpTextbox1 + * The $textoverride method can be used by subclasses overriding showContentForm + * to pass back to this method. + * + * @param array $customAttribs An array of html attributes to use in the textarea + * @param string $textoverride Optional text to override $this->textarea1 with + */ + protected function showTextbox1($customAttribs = null, $textoverride = null) { + $classes = array(); // Textarea CSS + if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) { + # Is the title semi-protected? + if ( $this->mTitle->isSemiProtected() ) { + $classes[] = 'mw-textarea-sprotected'; + } else { + # Then it must be protected based on static groups (regular) + $classes[] = 'mw-textarea-protected'; + } + } $attribs = array( 'tabindex' => 1 ); - + if ( is_array($customAttribs) ) + $attribs += $customAttribs; + if ( $this->wasDeletedSinceLastEdit() ) $attribs['type'] = 'hidden'; - if ( !empty($classes) ) - $attribs['class'] = implode(' ',$classes); + if ( !empty( $classes ) ) { + if ( isset($attribs['class']) ) + $classes[] = $attribs['class']; + $attribs['class'] = implode( ' ', $classes ); + } - $this->showTextbox( $this->textbox1, 'wpTextbox1', $attribs ); + $this->showTextbox( isset($textoverride) ? $textoverride : $this->textbox1, 'wpTextbox1', $attribs ); } - + protected function showTextbox2() { $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6 ) ); } - - protected function showTextbox( $content, $name, $attribs = array() ) { + + protected function showTextbox( $content, $name, $customAttribs = array() ) { global $wgOut, $wgUser; - + $wikitext = $this->safeUnicodeOutput( $content ); if ( $wikitext !== '' ) { // Ensure there's a newline at the end, otherwise adding lines @@ -1602,18 +1636,19 @@ END // mode will show an extra newline. A bit annoying. $wikitext .= "\n"; } - - $attribs['accesskey'] = ','; - $attribs['id'] = $name; - + + $attribs = $customAttribs + array( + 'accesskey' => ',', + 'id' => $name, + 'cols' => $wgUser->getIntOption( 'cols' ), + 'rows' => $wgUser->getIntOption( 'rows' ), + 'style' => '' // avoid php notices when appending for editwidth preference (appending allows customAttribs['style'] to still work + ); + if ( $wgUser->getOption( 'editwidth' ) ) - $attribs['style'] = 'width: 100%'; - - $wgOut->addHTML( Xml::textarea( - $name, - $wikitext, - $wgUser->getIntOption( 'cols' ), $wgUser->getIntOption( 'rows' ), - $attribs ) ); + $attribs['style'] .= 'width: 100%'; + + $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) ); } protected function displayPreviewArea( $previewOutput, $isOnTop = false ) { @@ -1660,23 +1695,22 @@ END } } - /** - * Live Preview lets us fetch rendered preview page content and - * add it to the page without refreshing the whole page. - * If not supported by the browser it will fall through to the normal form - * submission method. - * - * This function outputs a script tag to support live preview, and - * returns an onclick handler which should be added to the attributes - * of the preview button - */ - function doLivePreviewScript() { - global $wgOut, $wgTitle; - $wgOut->addScriptFile( 'preview.js' ); - $liveAction = $wgTitle->getLocalUrl( "action={$this->action}&wpPreview=true&live=true" ); - return "return !lpDoPreview(" . - "editform.wpTextbox1.value," . - '"' . $liveAction . '"' . ")"; + protected function showTosSummary() { + $msg = 'editpage-tos-summary'; + // Give a chance for site and per-namespace customizations of + // terms of service summary link that might exist separately + // from the copyright notice. + // + // This will display between the save button and the edit tools, + // so should remain short! + wfRunHooks( 'EditPageTosSummary', array( $this->mTitle, &$msg ) ); + $text = wfMsg( $msg ); + if( !wfEmptyMsg( $msg, $text ) && $text !== '-' ) { + global $wgOut; + $wgOut->addHTML( '<div class="mw-tos-summary">' ); + $wgOut->addWikiMsgArray( $msg, array() ); + $wgOut->addHTML( '</div>' ); + } } protected function showEditTools() { @@ -1685,6 +1719,67 @@ END $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) ); $wgOut->addHTML( '</div>' ); } + + protected function getCopywarn() { + global $wgRightsText; + if ( $wgRightsText ) { + $copywarnMsg = array( 'copyrightwarning', + '[[' . wfMsgForContent( 'copyrightpage' ) . ']]', + $wgRightsText ); + } else { + $copywarnMsg = array( 'copyrightwarning2', + '[[' . wfMsgForContent( 'copyrightpage' ) . ']]' ); + } + // Allow for site and per-namespace customization of contribution/copyright notice. + wfRunHooks( 'EditPageCopyrightWarning', array( $this->mTitle, &$copywarnMsg ) ); + + return "<div id=\"editpage-copywarn\">\n" . call_user_func_array("wfMsgNoTrans", $copywarnMsg) . "\n</div>"; + } + + protected function showStandardInputs( &$tabindex = 2 ) { + global $wgOut, $wgUser; + $wgOut->addHTML( "<div class='editOptions'>\n" ); + + if ( $this->section != 'new' ) { + $this->showSummaryInput( false, $this->summary ); + $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) ); + } + + $checkboxes = $this->getCheckboxes( $tabindex, $wgUser->getSkin(), + array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) ); + $wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" ); + $wgOut->addHTML( "<div class='editButtons'>\n" ); + $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" ); + + $cancel = $this->getCancelLink(); + $separator = wfMsgExt( 'pipe-separator' , 'escapenoentities' ); + $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' ) ); + $edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'. + htmlspecialchars( wfMsg( 'edithelp' ) ).'</a> '. + htmlspecialchars( wfMsg( 'newwindow' ) ); + $wgOut->addHTML( " <span class='editHelp'>{$cancel}{$separator}{$edithelp}</span>\n" ); + $wgOut->addHTML( "</div><!-- editButtons -->\n</div><!-- editOptions -->\n" ); + } + + /* + * Show an edit conflict. textbox1 is already shown in showEditForm(). + * If you want to use another entry point to this function, be careful. + */ + protected function showConflict() { + global $wgOut; + $this->textbox2 = $this->textbox1; + $this->textbox1 = $this->getContent(); + if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) { + $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" ); + + $de = new DifferenceEngine( $this->mTitle ); + $de->setText( $this->textbox2, $this->textbox1 ); + $de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) ); + + $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" ); + $this->showTextbox2(); + } + } protected function getLastDelete() { $dbr = wfGetDB( DB_SLAVE ); @@ -1698,7 +1793,7 @@ END 'log_title', 'log_comment', 'log_params', - 'log_deleted', + 'log_deleted', 'user_name' ), array( 'log_namespace' => $this->mTitle->getNamespace(), 'log_title' => $this->mTitle->getDBkey(), @@ -1709,11 +1804,11 @@ END array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ) ); // Quick paranoid permission checks... - if( is_object($data) ) { + if( is_object( $data ) ) { if( $data->log_deleted & LogPage::DELETED_USER ) - $data->user_name = wfMsgHtml('rev-deleted-user'); + $data->user_name = wfMsgHtml( 'rev-deleted-user' ); if( $data->log_deleted & LogPage::DELETED_COMMENT ) - $data->log_comment = wfMsgHtml('rev-deleted-comment'); + $data->log_comment = wfMsgHtml( 'rev-deleted-comment' ); } return $data; } @@ -1754,12 +1849,12 @@ END # XXX: stupid php bug won't let us use $wgTitle->isCssJsSubpage() here if ( $this->isCssJsSubpage ) { - if (preg_match("/\\.css$/", $this->mTitle->getText() ) ) { - $previewtext = wfMsg('usercsspreview'); - } else if (preg_match("/\\.js$/", $this->mTitle->getText() ) ) { - $previewtext = wfMsg('userjspreview'); + if (preg_match( "/\\.css$/", $this->mTitle->getText() ) ) { + $previewtext = wfMsg( 'usercsspreview' ); + } else if (preg_match( "/\\.js$/", $this->mTitle->getText() ) ) { + $previewtext = wfMsg( 'userjspreview' ); } - $parserOptions->setTidy(true); + $parserOptions->setTidy( true ); $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions ); $previewHTML = $parserOutput->mText; } elseif ( $rt = Title::newFromRedirectArray( $this->textbox1 ) ) { @@ -1769,11 +1864,11 @@ END # If we're adding a comment, we need to show the # summary as the headline - if ( $this->section=="new" && $this->summary!="" ) { - $toparse="== {$this->summary} ==\n\n".$toparse; + if ( $this->section == "new" && $this->summary != "" ) { + $toparse = "== {$this->summary} ==\n\n" . $toparse; } - if ( $this->mMetaData != "" ) $toparse .= "\n" . $this->mMetaData; + wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) ); // Parse mediawiki messages with correct target language if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { @@ -1783,7 +1878,7 @@ END } - $parserOptions->setTidy(true); + $parserOptions->setTidy( true ); $parserOptions->enableLimitReport(); $parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ), $this->mTitle, $parserOptions ); @@ -1797,20 +1892,24 @@ END } } - $previewhead = '<h2>' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>\n" . - "<div class='previewnote'>" . $wgOut->parse( $note ) . "</div>\n"; - if ( $this->isConflict ) { - $previewhead .='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n"; + if( $this->isConflict ) { + $conflict = '<h2 id="mw-previewconflict">' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n"; + } else { + $conflict = '<hr />'; } + $previewhead = "<div class='previewnote'>\n" . + '<h2 id="mw-previewheader">' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>" . + $wgOut->parse( $note ) . $conflict . "</div>\n"; + wfProfileOut( __METHOD__ ); - return $previewhead . $previewHTML; + return $previewhead . $previewHTML . $this->previewTextAfterContent; } - + function getTemplates() { if ( $this->preview || $this->section != '' ) { $templates = array(); - if ( !isset($this->mParserOutput) ) return $templates; + if ( !isset( $this->mParserOutput ) ) return $templates; foreach( $this->mParserOutput->getTemplates() as $ns => $template) { foreach( array_keys( $template ) as $dbk ) { $templates[] = Title::makeTitle($ns, $dbk); @@ -1826,7 +1925,7 @@ END * Call the stock "user is blocked" page */ function blockedPage() { - global $wgOut, $wgUser; + global $wgOut; $wgOut->blockedPage( false ); # Standard block notice on the top, don't 'return' # If the user made changes, preserve them when showing the markup @@ -1840,14 +1939,9 @@ END # Spit out the source or the user's modified version if ( $source !== false ) { - $rows = $wgUser->getIntOption( 'rows' ); - $cols = $wgUser->getIntOption( 'cols' ); - $attribs = array( 'id' => 'wpTextbox1', 'name' => 'wpTextbox1', 'cols' => $cols, 'rows' => $rows, 'readonly' => 'readonly' ); $wgOut->addHTML( '<hr />' ); $wgOut->addWikiMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() ); - # 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' ) ); + $this->showTextbox1( array( 'readonly' ), $source ); } } @@ -1859,7 +1953,13 @@ END $skin = $wgUser->getSkin(); $loginTitle = SpecialPage::getTitleFor( 'Userlogin' ); - $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $wgTitle->getPrefixedUrl() ); + $loginLink = $skin->link( + $loginTitle, + wfMsgHtml( 'loginreqlink' ), + array(), + array( 'returnto' => $wgTitle->getPrefixedText() ), + array( 'known', 'noclasses' ) + ); $wgOut->setPageTitle( wfMsg( 'whitelistedittitle' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); @@ -1874,14 +1974,17 @@ END * they have attempted to edit a nonexistent section. */ function noSuchSectionPage() { - global $wgOut, $wgTitle; + global $wgOut; $wgOut->setPageTitle( wfMsg( 'nosuchsectiontitle' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); - $wgOut->addWikiMsg( 'nosuchsectiontext', $this->section ); - $wgOut->returnToMain( false, $wgTitle ); + $res = wfMsgExt( 'nosuchsectiontext', 'parse', $this->section ); + wfRunHooks( 'EditPageNoSuchSection', array( &$this, &$res ) ); + $wgOut->addHTML( $res ); + + $wgOut->returnToMain( false, $this->mTitle ); } /** @@ -1910,15 +2013,14 @@ END * @todo document */ function mergeChangesInto( &$editText ){ - $fname = 'EditPage::mergeChangesInto'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $db = wfGetDB( DB_MASTER ); // This is the revision the editor started from $baseRevision = $this->getBaseRevision(); if ( is_null( $baseRevision ) ) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return false; } $baseText = $baseRevision->getText(); @@ -1926,7 +2028,7 @@ END // The current state, we want to merge updates into it $currentRevision = Revision::loadFromTitle( $db, $this->mTitle ); if ( is_null( $currentRevision ) ) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return false; } $currentText = $currentRevision->getText(); @@ -1934,10 +2036,10 @@ END $result = ''; if ( wfMerge( $baseText, $editText, $currentText, $result ) ) { $editText = $result; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return true; } else { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return false; } } @@ -1987,13 +2089,14 @@ 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 skins/common/edit.js. - * + * * @return string */ static function getEditToolbar() { - global $wgStylePath, $wgContLang, $wgLang, $wgJsMimeType; + global $wgStylePath, $wgContLang, $wgLang; /** + * toolarray an array of arrays which each include the filename of * the button image (without path), the opening tag, the closing tag, * and optionally a sample text that is inserted between the two when no @@ -2006,111 +2109,111 @@ END */ $toolarray = array( array( - 'image' => $wgLang->getImageFile('button-bold'), + 'image' => $wgLang->getImageFile( 'button-bold' ), 'id' => 'mw-editbutton-bold', 'open' => '\'\'\'', 'close' => '\'\'\'', - 'sample' => wfMsg('bold_sample'), - 'tip' => wfMsg('bold_tip'), + 'sample' => wfMsg( 'bold_sample' ), + 'tip' => wfMsg( 'bold_tip' ), 'key' => 'B' ), array( - 'image' => $wgLang->getImageFile('button-italic'), + 'image' => $wgLang->getImageFile( 'button-italic' ), 'id' => 'mw-editbutton-italic', 'open' => '\'\'', 'close' => '\'\'', - 'sample' => wfMsg('italic_sample'), - 'tip' => wfMsg('italic_tip'), + 'sample' => wfMsg( 'italic_sample' ), + 'tip' => wfMsg( 'italic_tip' ), 'key' => 'I' ), array( - 'image' => $wgLang->getImageFile('button-link'), + 'image' => $wgLang->getImageFile( 'button-link' ), 'id' => 'mw-editbutton-link', 'open' => '[[', 'close' => ']]', - 'sample' => wfMsg('link_sample'), - 'tip' => wfMsg('link_tip'), + 'sample' => wfMsg( 'link_sample' ), + 'tip' => wfMsg( 'link_tip' ), 'key' => 'L' ), array( - 'image' => $wgLang->getImageFile('button-extlink'), + 'image' => $wgLang->getImageFile( 'button-extlink' ), 'id' => 'mw-editbutton-extlink', 'open' => '[', 'close' => ']', - 'sample' => wfMsg('extlink_sample'), - 'tip' => wfMsg('extlink_tip'), + 'sample' => wfMsg( 'extlink_sample' ), + 'tip' => wfMsg( 'extlink_tip' ), 'key' => 'X' ), array( - 'image' => $wgLang->getImageFile('button-headline'), + 'image' => $wgLang->getImageFile( 'button-headline' ), 'id' => 'mw-editbutton-headline', 'open' => "\n== ", 'close' => " ==\n", - 'sample' => wfMsg('headline_sample'), - 'tip' => wfMsg('headline_tip'), + 'sample' => wfMsg( 'headline_sample' ), + 'tip' => wfMsg( 'headline_tip' ), 'key' => 'H' ), array( - 'image' => $wgLang->getImageFile('button-image'), + 'image' => $wgLang->getImageFile( 'button-image' ), 'id' => 'mw-editbutton-image', - 'open' => '[['.$wgContLang->getNsText(NS_FILE).':', + 'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':', 'close' => ']]', - 'sample' => wfMsg('image_sample'), - 'tip' => wfMsg('image_tip'), + 'sample' => wfMsg( 'image_sample' ), + 'tip' => wfMsg( 'image_tip' ), 'key' => 'D' ), array( - 'image' => $wgLang->getImageFile('button-media'), + 'image' => $wgLang->getImageFile( 'button-media' ), 'id' => 'mw-editbutton-media', - 'open' => '[['.$wgContLang->getNsText(NS_MEDIA).':', + 'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':', 'close' => ']]', - 'sample' => wfMsg('media_sample'), - 'tip' => wfMsg('media_tip'), + 'sample' => wfMsg( 'media_sample' ), + 'tip' => wfMsg( 'media_tip' ), 'key' => 'M' ), array( - 'image' => $wgLang->getImageFile('button-math'), + 'image' => $wgLang->getImageFile( 'button-math' ), 'id' => 'mw-editbutton-math', 'open' => "<math>", 'close' => "</math>", - 'sample' => wfMsg('math_sample'), - 'tip' => wfMsg('math_tip'), + 'sample' => wfMsg( 'math_sample' ), + 'tip' => wfMsg( 'math_tip' ), 'key' => 'C' ), array( - 'image' => $wgLang->getImageFile('button-nowiki'), + 'image' => $wgLang->getImageFile( 'button-nowiki' ), 'id' => 'mw-editbutton-nowiki', 'open' => "<nowiki>", 'close' => "</nowiki>", - 'sample' => wfMsg('nowiki_sample'), - 'tip' => wfMsg('nowiki_tip'), + 'sample' => wfMsg( 'nowiki_sample' ), + 'tip' => wfMsg( 'nowiki_tip' ), 'key' => 'N' ), array( - 'image' => $wgLang->getImageFile('button-sig'), + 'image' => $wgLang->getImageFile( 'button-sig' ), 'id' => 'mw-editbutton-signature', 'open' => '--~~~~', 'close' => '', 'sample' => '', - 'tip' => wfMsg('sig_tip'), + 'tip' => wfMsg( 'sig_tip' ), 'key' => 'Y' ), array( - 'image' => $wgLang->getImageFile('button-hr'), + 'image' => $wgLang->getImageFile( 'button-hr' ), 'id' => 'mw-editbutton-hr', 'open' => "\n----\n", 'close' => '', 'sample' => '', - 'tip' => wfMsg('hr_tip'), + 'tip' => wfMsg( 'hr_tip' ), 'key' => 'R' ) ); $toolbar = "<div id='toolbar'>\n"; - $toolbar.="<script type='$wgJsMimeType'>\n/*<![CDATA[*/\n"; - foreach($toolarray as $tool) { + $script = ''; + foreach ( $toolarray as $tool ) { $params = array( - $image = $wgStylePath.'/common/images/'.$tool['image'], + $image = $wgStylePath . '/common/images/' . $tool['image'], // Note that we use the tip both for the ALT tag and the TITLE tag of the image. // Older browsers show a "speedtip" type message only for ALT. // Ideally these should be different, realistically they @@ -2124,11 +2227,14 @@ END $paramList = implode( ',', array_map( array( 'Xml', 'encodeJsVar' ), $params ) ); - $toolbar.="addButton($paramList);\n"; + $script .= "addButton($paramList);\n"; } + $toolbar .= Html::inlineScript( "\n$script\n" ); + + $toolbar .= "\n</div>"; + + wfRunHooks( 'EditPageBeforeEditToolbar', array( &$toolbar ) ); - $toolbar.="/*]]>*/\n</script>"; - $toolbar.="\n</div>"; return $toolbar; } @@ -2149,8 +2255,8 @@ END $checkboxes = array(); $checkboxes['minor'] = ''; - $minorLabel = wfMsgExt('minoredit', array('parseinline')); - if ( $wgUser->isAllowed('minoredit') ) { + $minorLabel = wfMsgExt( 'minoredit', array( 'parseinline' ) ); + if ( $wgUser->isAllowed( 'minoredit' ) ) { $attribs = array( 'tabindex' => ++$tabindex, 'accesskey' => wfMsg( 'accesskey-minoredit' ), @@ -2158,10 +2264,10 @@ END ); $checkboxes['minor'] = Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) . - " <label for='wpMinoredit'".$skin->tooltip('minoredit', 'withaccess').">{$minorLabel}</label>"; + " <label for='wpMinoredit'" . $skin->tooltip( 'minoredit', 'withaccess' ) . ">{$minorLabel}</label>"; } - $watchLabel = wfMsgExt('watchthis', array('parseinline')); + $watchLabel = wfMsgExt( 'watchthis', array( 'parseinline' ) ); $checkboxes['watch'] = ''; if ( $wgUser->isLoggedIn() ) { $attribs = array( @@ -2171,7 +2277,7 @@ END ); $checkboxes['watch'] = Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) . - " <label for='wpWatchthis'".$skin->tooltip('watch', 'withaccess').">{$watchLabel}</label>"; + " <label for='wpWatchthis'" . $skin->tooltip( 'watch', 'withaccess' ) . ">{$watchLabel}</label>"; } wfRunHooks( 'EditPageBeforeEditChecks', array( &$this, &$checkboxes, &$tabindex ) ); return $checkboxes; @@ -2186,8 +2292,6 @@ END * @return array */ public function getEditButtons(&$tabindex) { - global $wgLivePreview, $wgUser; - $buttons = array(); $temp = array( @@ -2195,61 +2299,35 @@ END 'name' => 'wpSave', 'type' => 'submit', 'tabindex' => ++$tabindex, - 'value' => wfMsg('savearticle'), - 'accesskey' => wfMsg('accesskey-save'), + 'value' => wfMsg( 'savearticle' ), + 'accesskey' => wfMsg( 'accesskey-save' ), 'title' => wfMsg( 'tooltip-save' ).' ['.wfMsg( 'accesskey-save' ).']', ); |