diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2007-05-16 20:58:53 +0000 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2007-05-16 20:58:53 +0000 |
commit | cecb985bee3bdd252e1b8dc0bd500b37cd52be01 (patch) | |
tree | 17266aa237742640aabee7856f0202317a45d540 /includes | |
parent | 0bac06c301f2a83edb0236e4c2434da16848d549 (diff) |
Aktualisierung auf MediaWiki 1.10.0
Plugins angepasst und verbessert
kleine Korrekturen am Design
Diffstat (limited to 'includes')
233 files changed, 10573 insertions, 6034 deletions
diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index 39ec19f8..ca129029 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -1,7 +1,8 @@ <?php -if( !defined( 'MEDIAWIKI' ) ) - die( 1 ); +if( !defined( 'MEDIAWIKI' ) ) { + die( 1 ); +} if ( ! $wgUseAjax ) { die( 1 ); @@ -9,12 +10,16 @@ if ( ! $wgUseAjax ) { require_once( 'AjaxFunctions.php' ); +/** + * Object-Oriented Ajax functions. + * @addtogroup Ajax + */ class AjaxDispatcher { var $mode; var $func_name; var $args; - function AjaxDispatcher() { + function __construct() { wfProfileIn( __METHOD__ ); $this->mode = ""; @@ -28,14 +33,14 @@ class AjaxDispatcher { } if ($this->mode == "get") { - $this->func_name = $_GET["rs"]; + $this->func_name = isset( $_GET["rs"] ) ? $_GET["rs"] : ''; if (! empty($_GET["rsargs"])) { $this->args = $_GET["rsargs"]; } else { $this->args = array(); } } else { - $this->func_name = $_POST["rs"]; + $this->func_name = isset( $_POST["rs"] ) ? $_POST["rs"] : ''; if (! empty($_POST["rsargs"])) { $this->args = $_POST["rsargs"]; } else { @@ -47,7 +52,7 @@ class AjaxDispatcher { function performAction() { global $wgAjaxExportList, $wgOut; - + if ( empty( $this->mode ) ) { return; } @@ -59,7 +64,7 @@ class AjaxDispatcher { } else { try { $result = call_user_func_array($this->func_name, $this->args); - + if ( $result === false || $result === NULL ) { wfHttpError( 500, 'Internal Error', "{$this->func_name} returned no data" ); @@ -68,7 +73,7 @@ class AjaxDispatcher { if ( is_string( $result ) ) { $result= new AjaxResponse( $result ); } - + $result->sendHeaders(); $result->printText(); } @@ -82,7 +87,7 @@ class AjaxDispatcher { } } } - + wfProfileOut( __METHOD__ ); $wgOut = null; } diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php index eee2a1a4..86f853db 100644 --- a/includes/AjaxFunctions.php +++ b/includes/AjaxFunctions.php @@ -1,7 +1,13 @@ <?php -if( !defined( 'MEDIAWIKI' ) ) - die( 1 ); +/** + * @package MediaWiki + * @addtogroup Ajax + */ + +if( !defined( 'MEDIAWIKI' ) ) { + die( 1 ); +} /** * Function converts an Javascript escaped string back into a string with @@ -13,40 +19,39 @@ if( !defined( 'MEDIAWIKI' ) ) * @return string */ function js_unescape($source, $iconv_to = 'UTF-8') { - $decodedStr = ''; - $pos = 0; - $len = strlen ($source); - while ($pos < $len) { - $charAt = substr ($source, $pos, 1); - if ($charAt == '%') { - $pos++; - $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); - $pos += 4; - } - else { - // we have an escaped ascii character - $hexVal = substr ($source, $pos, 2); - $decodedStr .= chr (hexdec ($hexVal)); - $pos += 2; - } - } - else { - $decodedStr .= $charAt; - $pos++; - } - } - - if ($iconv_to != "UTF-8") { - $decodedStr = iconv("UTF-8", $iconv_to, $decodedStr); - } - - return $decodedStr; + $decodedStr = ''; + $pos = 0; + $len = strlen ($source); + + while ($pos < $len) { + $charAt = substr ($source, $pos, 1); + if ($charAt == '%') { + $pos++; + $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); + $pos += 4; + } else { + // we have an escaped ascii character + $hexVal = substr ($source, $pos, 2); + $decodedStr .= chr (hexdec ($hexVal)); + $pos += 2; + } + } else { + $decodedStr .= $charAt; + $pos++; + } + } + + if ($iconv_to != "UTF-8") { + $decodedStr = iconv("UTF-8", $iconv_to, $decodedStr); + } + + return $decodedStr; } /** @@ -71,7 +76,7 @@ function code2utf($num){ function wfSajaxSearch( $term ) { global $wgContLang, $wgOut; $limit = 16; - + $l = new Linker; $term = str_replace( ' ', '_', $wgContLang->ucfirst( @@ -81,7 +86,7 @@ function wfSajaxSearch( $term ) { if ( strlen( str_replace( '_', '', $term ) )<3 ) return; - $db =& wfGetDB( DB_SLAVE ); + $db = wfGetDB( DB_SLAVE ); $res = $db->select( 'page', 'page_title', array( 'page_namespace' => 0, "page_title LIKE '". $db->strencode( $term) ."%'" ), @@ -108,8 +113,8 @@ function wfSajaxSearch( $term ) { $subtitlemsg = ( Title::newFromText($term) ? 'searchsubtitle' : 'searchsubtitleinvalid' ); $subtitle = $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ); #FIXME: parser is missing mTitle ! - $term = htmlspecialchars( $term ); - $html = '<div style="float:right; border:solid 1px black;background:gainsboro;padding:2px;"><a onclick="Searching_Hide_Results();">' + $term = urlencode( $term ); + $html = '<div style="float:right; border:solid 1px black;background:gainsboro;padding:2px;"><a onclick="Searching_Hide_Results();">' . wfMsg( 'hideresults' ) . '</a></div>' . '<h1 class="firstHeading">'.wfMsg('search') . '</h1><div id="contentSub">'. $subtitle . '</div><ul><li>' @@ -121,11 +126,11 @@ function wfSajaxSearch( $term ) { "search=$term&go=Go" ) . "</li></ul><h2>" . wfMsg( 'articletitles', $term ) . "</h2>" . '<ul>' .$r .'</ul>'.$more; - + $response = new AjaxResponse( $html ); - + $response->setCacheDuration( 30*60 ); - + return $response; } @@ -152,14 +157,14 @@ function wfAjaxWatch($pageID = "", $watch = "") { if($watch) { if(!$watching) { - $dbw =& wfGetDB(DB_MASTER); + $dbw = wfGetDB(DB_MASTER); $dbw->begin(); $article->doWatch(); $dbw->commit(); } } else { if($watching) { - $dbw =& wfGetDB(DB_MASTER); + $dbw = wfGetDB(DB_MASTER); $dbw->begin(); $article->doUnwatch(); $dbw->commit(); diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index a59c73bb..cb4af1b5 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -1,28 +1,32 @@ <?php +if( !defined( 'MEDIAWIKI' ) ) { + die( 1 ); +} -if( !defined( 'MEDIAWIKI' ) ) - die( 1 ); - +/** + * @todo document + * @addtogroup Ajax + */ class AjaxResponse { var $mCacheDuration; var $mVary; - + var $mDisabled; var $mText; var $mResponseCode; var $mLastModified; var $mContentType; - function AjaxResponse( $text = 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= 'text/html; charset=utf-8'; - + if ( $text ) { $this->addText( $text ); } @@ -39,15 +43,15 @@ class AjaxResponse { function setResponseCode( $code ) { $this->mResponseCode = $code; } - + function setContentType( $type ) { $this->mContentType = $type; } - + function disable() { $this->mDisabled = true; } - + function addText( $text ) { if ( ! $this->mDisabled && $text ) { $this->mText .= $text; @@ -59,62 +63,62 @@ class AjaxResponse { print $this->mText; } } - + function sendHeaders() { global $wgUseSquid, $wgUseESI; - + if ( $this->mResponseCode ) { $n = preg_replace( '/^ *(\d+)/', '\1', $this->mResponseCode ); header( "Status: " . $this->mResponseCode, true, (int)$n ); } - + header ("Content-Type: " . $this->mContentType ); - + if ( $this->mLastModified ) { header ("Last-Modified: " . $this->mLastModified ); } else { header ("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); } - + if ( $this->mCacheDuration ) { - + # If squid caches are configured, tell them to cache the response, # and tell the client to always check with the squid. Otherwise, # tell the client to use a cached copy, without a way to purge it. - + 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( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); } else { 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}"); } - + } 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 } - + if ( $this->mVary ) { header ( "Vary: " . $this->mVary ); } } - + /** * checkLastModified tells the client to use the client-cached response if * possible. If sucessful, the AjaxResponse is disabled so that @@ -154,9 +158,9 @@ class AjaxResponse { $this->setResponseCode( "304 Not Modified" ); $this->disable(); $this->mLastModified = $lastmod; - + wfDebug( "$fname: CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false ); - + return true; } else { wfDebug( "$fname: READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false ); @@ -167,11 +171,11 @@ class AjaxResponse { $this->mLastModified = $lastmod; } } - + function loadFromMemcached( $mckey, $touched ) { global $wgMemc; if ( !$touched ) return false; - + $mcvalue = $wgMemc->get( $mckey ); if ( $mcvalue ) { # Check to see if the value has been invalidated @@ -183,20 +187,20 @@ class AjaxResponse { wfDebug( "$mckey has expired\n" ); } } - + return false; } - + function storeInMemcached( $mckey, $expiry = 86400 ) { global $wgMemc; - - $wgMemc->set( $mckey, + + $wgMemc->set( $mckey, array( 'timestamp' => wfTimestampNow(), 'value' => $this->mText ), $expiry ); - + return true; } } diff --git a/includes/Article.php b/includes/Article.php index 6b4f5270..0130ceba 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -1,7 +1,6 @@ <?php /** * File for articles - * @package MediaWiki */ /** @@ -11,7 +10,6 @@ * Note: edit user interface and cache support functions have been * moved to separate EditPage and HTMLFileCache classes. * - * @package MediaWiki */ class Article { /**@{{ @@ -43,7 +41,7 @@ class Article { * @param $title Reference to a Title object. * @param $oldId Integer revision ID, null to fetch from request, zero for current */ - function Article( &$title, $oldId = null ) { + function __construct( &$title, $oldId = null ) { $this->mTitle =& $title; $this->mOldId = $oldId; $this->clear(); @@ -57,14 +55,14 @@ class Article { function setRedirectedFrom( $from ) { $this->mRedirectedFrom = $from; } - + /** * @return mixed false, Title of in-wiki target, or string with URL */ function followRedirect() { $text = $this->getContent(); $rt = Title::newFromRedirect( $text ); - + # process if title object is valid and not special:userlogout if( $rt ) { if( $rt->getInterwiki() != '' ) { @@ -73,7 +71,7 @@ class Article { // // This can be hard to reverse and may produce loops, // so they may be disabled in the site configuration. - + $source = $this->mTitle->getFullURL( 'redirect=no' ); return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) ); } @@ -84,7 +82,7 @@ class Article { // the rest of the page we're on. // // This can be hard to reverse, so they may be disabled. - + if( $rt->isSpecial( 'Userlogout' ) ) { // rolleyes } else { @@ -94,7 +92,7 @@ class Article { return $rt; } } - + // No or invalid redirect return false; } @@ -247,7 +245,7 @@ class Article { * @param array $conditions * @private */ - function pageData( &$dbr, $conditions ) { + function pageData( $dbr, $conditions ) { $fields = array( 'page_id', 'page_namespace', @@ -273,7 +271,7 @@ class Article { * @param Database $dbr * @param Title $title */ - function pageDataFromTitle( &$dbr, $title ) { + function pageDataFromTitle( $dbr, $title ) { return $this->pageData( $dbr, array( 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDBkey() ) ); @@ -283,7 +281,7 @@ class Article { * @param Database $dbr * @param int $id */ - function pageDataFromId( &$dbr, $id ) { + function pageDataFromId( $dbr, $id ) { return $this->pageData( $dbr, array( 'page_id' => $id ) ); } @@ -296,17 +294,18 @@ class Article { */ function loadPageData( $data = 'fromdb' ) { if ( $data === 'fromdb' ) { - $dbr =& $this->getDB(); + $dbr = $this->getDB(); $data = $this->pageDataFromId( $dbr, $this->getId() ); } - + $lc =& LinkCache::singleton(); if ( $data ) { $lc->addGoodLinkObj( $data->page_id, $this->mTitle ); $this->mTitle->mArticleID = $data->page_id; + + # Old-fashioned restrictions. $this->mTitle->loadRestrictions( $data->page_restrictions ); - $this->mTitle->mRestrictionsLoaded = true; $this->mCounter = $data->page_counter; $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); @@ -333,7 +332,7 @@ class Article { return $this->mContent; } - $dbr =& $this->getDB(); + $dbr = $this->getDB(); # Pre-fill content with error message so that if something # fails we'll have something telling us what we intended. @@ -405,9 +404,8 @@ class Article { * * @return Database */ - function &getDB() { - $ret =& wfGetDB( DB_MASTER ); - return $ret; + function getDB() { + return wfGetDB( DB_MASTER ); } /** @@ -455,7 +453,7 @@ class Article { if ( $id == 0 ) { $this->mCounter = 0; } else { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $this->mCounter = $dbr->selectField( 'page', 'page_counter', array( 'page_id' => $id ), 'Article::getCount', $this->getSelectOptions() ); } @@ -471,12 +469,12 @@ class Article { * @return bool */ function isCountable( $text ) { - global $wgUseCommaCount, $wgContentNamespaces; + global $wgUseCommaCount; $token = $wgUseCommaCount ? ',' : '[['; return - array_search( $this->mTitle->getNamespace(), $wgContentNamespaces ) !== false - && ! $this->isRedirect( $text ) + $this->mTitle->isContentPage() + && !$this->isRedirect( $text ) && in_string( $token, $text ); } @@ -573,7 +571,7 @@ class Article { # XXX: this is expensive; cache this info somewhere. $contribs = array(); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $revTable = $dbr->tableName( 'revision' ); $userTable = $dbr->tableName( 'user' ); $user = $this->getUser(); @@ -613,7 +611,7 @@ class Article { $parserCache =& ParserCache::singleton(); $ns = $this->mTitle->getNamespace(); # shortcut - + # Get variables from query string $oldid = $this->getOldID(); @@ -627,16 +625,21 @@ class Article { $diff = $wgRequest->getVal( 'diff' ); $rcid = $wgRequest->getVal( 'rcid' ); $rdfrom = $wgRequest->getVal( 'rdfrom' ); + $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); $wgOut->setArticleFlag( true ); - if ( isset( $wgNamespaceRobotPolicies[$ns] ) ) { + + # Discourage indexing of printable versions, but encourage following + if( $wgOut->isPrintable() ) { + $policy = 'noindex,follow'; + } elseif( isset( $wgNamespaceRobotPolicies[$ns] ) ) { + # Honour customised robot policies for this namespace $policy = $wgNamespaceRobotPolicies[$ns]; } else { - # The default policy. Dev note: make sure you change the documentation - # in DefaultSettings.php before changing it. + # Default to encourage indexing and following links $policy = 'index,follow'; } - $wgOut->setRobotpolicy( $policy ); + $wgOut->setRobotPolicy( $policy ); # If we got diff and oldid in the query, we want to see a # diff page instead of the article. @@ -647,8 +650,8 @@ class Article { $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid ); // DifferenceEngine directly fetched the revision: $this->mRevIdFetched = $de->mNewid; - $de->showDiffPage(); - + $de->showDiffPage( $diffOnly ); + // Needed to get the page's current revision $this->loadPageData(); if( $diff == 0 || $diff == $this->mLatest ) { @@ -658,7 +661,7 @@ class Article { wfProfileOut( __METHOD__ ); return; } - + if ( empty( $oldid ) && $this->checkTouched() ) { $wgOut->setETag($parserCache->getETag($this, $wgUser)); @@ -713,11 +716,11 @@ class Article { $wasRedirected = true; } } - + $outputDone = false; + wfRunHooks( 'ArticleViewHeader', array( &$this ) ); if ( $pcache ) { if ( $wgOut->tryParserCache( $this, $wgUser ) ) { - wfRunHooks( 'ArticleViewHeader', array( &$this ) ); $outputDone = true; } } @@ -764,17 +767,12 @@ class Article { } } if( !$outputDone ) { - /** - * @fixme: this hook doesn't work most of the time, as it doesn't - * trigger when the parser cache is used. - */ - wfRunHooks( 'ArticleViewHeader', array( &$this ) ) ; $wgOut->setRevisionId( $this->getRevIdFetched() ); # wrap user css and user js in pre and don't parse # XXX: use $this->mTitle->usCssJsSubpage() when php is fixed/ a workaround is found if ( $ns == NS_USER && - preg_match('/\\/[\\w]+\\.(css|js)$/', $this->mTitle->getDBkey()) + preg_match('/\\/[\\w]+\\.(?:css|js)$/', $this->mTitle->getDBkey()) ) { $wgOut->addWikiText( wfMsg('clearyourcache')); $wgOut->addHTML( '<pre>'.htmlspecialchars($this->mContent)."\n</pre>" ); @@ -795,7 +793,7 @@ class Article { $wgOut->addParserOutputNoText( $parseout ); } else if ( $pcache ) { # Display content and save to parser cache - $wgOut->addPrimaryWikiText( $text, $this ); + $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. @@ -803,11 +801,21 @@ class Article { $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false ); } # Display content and don't save to parser cache - $wgOut->addPrimaryWikiText( $text, $this, false ); + # 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 ); } + } } /* title may have been set from the cache */ @@ -827,8 +835,9 @@ class Article { if ( $wgUseRCPatrol && !is_null( $rcid ) && $rcid != 0 && $wgUser->isAllowed( 'patrol' ) ) { $wgOut->addHTML( "<div class='patrollink'>" . - wfMsg ( 'markaspatrolledlink', - $sk->makeKnownLinkObj( $this->mTitle, wfMsg('markaspatrolledtext'), "action=markpatrolled&rcid=$rcid" ) + wfMsgHtml( 'markaspatrolledlink', + $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml('markaspatrolledtext'), + "action=markpatrolled&rcid=$rcid" ) ) . '</div>' ); @@ -845,7 +854,7 @@ class Article { function addTrackbacks() { global $wgOut, $wgUser; - $dbr =& wfGetDB(DB_SLAVE); + $dbr = wfGetDB(DB_SLAVE); $tbs = $dbr->select( /* FROM */ 'trackbacks', /* SELECT */ array('tb_id', 'tb_title', 'tb_url', 'tb_ex', 'tb_name'), @@ -891,7 +900,7 @@ class Article { return; } - $db =& wfGetDB(DB_MASTER); + $db = wfGetDB(DB_MASTER); $db->delete('trackbacks', array('tb_id' => $wgRequest->getInt('tbid'))); $wgTitle->invalidateCache(); $wgOut->addWikiText(wfMsg('trackbackdeleteok')); @@ -910,7 +919,7 @@ class Article { function purge() { global $wgUser, $wgRequest, $wgOut; - if ( $wgUser->isLoggedIn() || $wgRequest->wasPosted() ) { + if ( $wgUser->isAllowed( 'purge' ) || $wgRequest->wasPosted() ) { if( wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { $this->doPurge(); } @@ -928,7 +937,7 @@ class Article { $wgOut->addHTML( $msg ); } } - + /** * Perform the actions of a page purging */ @@ -957,11 +966,10 @@ class Article { * Best if all done inside a transaction. * * @param Database $dbw - * @param string $restrictions * @return int The newly created page_id key * @private */ - function insertOn( &$dbw, $restrictions = '' ) { + function insertOn( $dbw ) { wfProfileIn( __METHOD__ ); $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' ); @@ -970,7 +978,7 @@ class Article { 'page_namespace' => $this->mTitle->getNamespace(), 'page_title' => $this->mTitle->getDBkey(), 'page_counter' => 0, - 'page_restrictions' => $restrictions, + 'page_restrictions' => '', 'page_is_redirect' => 0, # Will set this shortly... 'page_is_new' => 1, 'page_random' => wfRandom(), @@ -996,7 +1004,7 @@ class Article { * when different from the currently set value. * Giving 0 indicates the new page flag should * be set on. - * @param bool $lastRevIsRedirect If given, will optimize adding and + * @param bool $lastRevIsRedirect If given, will optimize adding and * removing rows in redirect table. * @return bool true on success, false on failure * @private @@ -1006,7 +1014,7 @@ class Article { $text = $revision->getText(); $rt = Title::newFromRedirect( $text ); - + $conditions = array( 'page_id' => $this->getId() ); if( !is_null( $lastRevision ) ) { # An extra check against threads stepping on each other @@ -1028,20 +1036,20 @@ class Article { if ($result) { // FIXME: Should the result from updateRedirectOn() be returned instead? - $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); + $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); } - + wfProfileOut( __METHOD__ ); return $result; } /** - * Add row to the redirect table if this is a redirect, remove otherwise. + * Add row to the redirect table if this is a redirect, remove otherwise. * * @param Database $dbw * @param $redirectTitle a title object pointing to the redirect target, - * or NULL if this is not a redirect - * @param bool $lastRevIsRedirect If given, will optimize adding and + * or NULL if this is not a redirect + * @param bool $lastRevIsRedirect If given, will optimize adding and * removing rows in redirect table. * @return bool true on success, false on failure * @private @@ -1067,7 +1075,7 @@ class Article { $dbw->replace( 'redirect', array( 'rd_from' ), $set, __METHOD__ ); } else { - // This is not a redirect, remove row from redirect table + // This is not a redirect, remove row from redirect table $where = array( 'rd_from' => $this->getId() ); $dbw->delete( 'redirect', $where, __METHOD__); } @@ -1075,7 +1083,7 @@ class Article { wfProfileOut( __METHOD__ ); return ( $dbw->affectedRows() != 0 ); } - + return true; } @@ -1119,14 +1127,14 @@ class Article { */ function replaceSection($section, $text, $summary = '', $edittime = NULL) { wfProfileIn( __METHOD__ ); - + if( $section == '' ) { // Whole-page edit; let the text through unmolested. } else { if( is_null( $edittime ) ) { $rev = Revision::newFromTitle( $this->mTitle ); } else { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); } if( is_null( $rev ) ) { @@ -1166,10 +1174,10 @@ class Article { if ( $comment && $summary != "" ) { $text = "== {$summary} ==\n\n".$text; } - + $this->doEdit( $text, $summary, $flags ); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); if ($watchthis) { if (!$this->mTitle->userIsWatching()) { $dbw->begin(); @@ -1196,7 +1204,7 @@ class Article { $good = $this->doEdit( $text, $summary, $flags ); if ( $good ) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); if ($watchthis) { if (!$this->mTitle->userIsWatching()) { $dbw->begin(); @@ -1219,7 +1227,7 @@ class Article { /** * Article::doEdit() * - * Change an existing article or create a new article. Updates RC and all necessary caches, + * Change an existing article or create a new article. Updates RC and all necessary caches, * optionally via the deferred update array. * * $wgUser must be set before calling this function. @@ -1241,9 +1249,9 @@ class Article { * Defer some of the updates until the end of index.php * EDIT_AUTOSUMMARY * 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 return false. If + * + * 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 return false. If * EDIT_NEW is specified and the article does exist, a duplicate key error will cause an exception * to be thrown from the Database. These two conditions are also possible with auto-detection due * to MediaWiki's performance-optimised locking strategy. @@ -1267,7 +1275,7 @@ class Article { if( !wfRunHooks( 'ArticleSave', array( &$this, &$wgUser, &$text, &$summary, $flags & EDIT_MINOR, - null, null, &$flags ) ) ) + null, null, &$flags ) ) ) { wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" ); wfProfileOut( __METHOD__ ); @@ -1288,9 +1296,9 @@ class Article { $text = $this->preSaveTransform( $text ); $newsize = strlen( $text ); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $now = wfTimestampNow(); - + if ( $flags & EDIT_UPDATE ) { # Update article, but only if changed. @@ -1316,7 +1324,7 @@ class Article { wfProfileOut( __METHOD__ ); return false; } - + $revision = new Revision( array( 'page' => $this->getId(), 'comment' => $summary, @@ -1340,10 +1348,11 @@ class Article { $rcid = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $wgUser, $summary, $lastRevision, $this->getTimestamp(), $bot, '', $oldsize, $newsize, $revisionId ); - + # Mark as patrolled if the user can do so - if( $wgUser->isAllowed( 'autopatrol' ) ) { + if( $GLOBALS['wgUseRCPatrol'] && $wgUser->isAllowed( 'autopatrol' ) ) { RecentChange::markPatrolled( $rcid ); + PatrolLog::record( $rcid, true ); } } $wgUser->incEditCount(); @@ -1362,19 +1371,19 @@ class Article { } if ( $good ) { - # Invalidate cache of this article and all pages using this article + # Invalidate cache of this article and all pages using this article # as a template. Partly deferred. Article::onArticleEdit( $this->mTitle ); - + # Update links tables, site stats, etc. $changed = ( strcmp( $oldtext, $text ) != 0 ); $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed ); } } else { # Create new article - + # Set statistics members - # We work out if it's countable after PST to avoid counter drift + # We work out if it's countable after PST to avoid counter drift # when articles are created with {{subst:}} $this->mGoodAdjustment = (int)$this->isCountable( $text ); $this->mTotalAdjustment = 1; @@ -1403,8 +1412,9 @@ class Article { $rcid = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, $bot, '', strlen( $text ), $revisionId ); # Mark as patrolled if the user can - if( $wgUser->isAllowed( 'autopatrol' ) ) { + if( $GLOBALS['wgUseRCPatrol'] && $wgUser->isAllowed( 'autopatrol' ) ) { RecentChange::markPatrolled( $rcid ); + PatrolLog::record( $rcid, true ); } } $wgUser->incEditCount(); @@ -1429,7 +1439,7 @@ class Article { array( &$this, &$wgUser, $text, $summary, $flags & EDIT_MINOR, null, null, &$flags ) ); - + wfProfileOut( __METHOD__ ); return $good; } @@ -1457,7 +1467,7 @@ class Article { } $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $sectionAnchor ); } - + /** * Mark this particular edit as patrolled */ @@ -1470,25 +1480,25 @@ class Article { $wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); return; } - + # Check permissions if( !$wgUser->isAllowed( 'patrol' ) ) { $wgOut->permissionRequired( 'patrol' ); return; } - + # If we haven't been given an rc_id value, we can't do anything $rcid = $wgRequest->getVal( 'rcid' ); if( !$rcid ) { $wgOut->errorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); return; } - + # Handle the 'MarkPatrolled' hook if( !wfRunHooks( 'MarkPatrolled', array( $rcid, &$wgUser, false ) ) ) { return; } - + $return = SpecialPage::getTitleFor( 'Recentchanges' ); # If it's left up to us, check that the user is allowed to patrol this edit # If the user has the "autopatrol" right, then we'll assume there are no @@ -1507,11 +1517,12 @@ class Article { return; } } - + # Mark the edit as patrolled RecentChange::markPatrolled( $rcid ); + PatrolLog::record( $rcid ); wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) ); - + # Inform the user $wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) ); $wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrolledtext' ) ); @@ -1534,7 +1545,7 @@ class Article { $wgOut->readOnlyPage(); return; } - + if( $this->doWatch() ) { $wgOut->setPagetitle( wfMsg( 'addedwatch' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); @@ -1546,7 +1557,7 @@ class Article { $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() ); } - + /** * Add this page to $wgUser's watchlist * @return bool true on successful watch operation @@ -1556,13 +1567,13 @@ class Article { if( $wgUser->isAnon() ) { return false; } - + if (wfRunHooks('WatchArticle', array(&$wgUser, &$this))) { $wgUser->addWatch( $this->mTitle ); return wfRunHooks('WatchArticleComplete', array(&$wgUser, &$this)); } - + return false; } @@ -1581,7 +1592,7 @@ class Article { $wgOut->readOnlyPage(); return; } - + if( $this->doUnwatch() ) { $wgOut->setPagetitle( wfMsg( 'removedwatch' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); @@ -1593,7 +1604,7 @@ class Article { $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() ); } - + /** * Stop watching a page * @return bool true on successful unwatch @@ -1609,7 +1620,7 @@ class Article { return wfRunHooks('UnwatchArticleComplete', array(&$wgUser, &$this)); } - + return false; } @@ -1618,7 +1629,7 @@ class Article { */ function protect() { $form = new ProtectionForm( $this ); - $form->show(); + $form->execute(); } /** @@ -1635,14 +1646,21 @@ class Article { * @param string $reason * @return bool true on success */ - function updateRestrictions( $limit = array(), $reason = '' ) { + function updateRestrictions( $limit = array(), $reason = '', $cascade = 0, $expiry = null ) { global $wgUser, $wgRestrictionTypes, $wgContLang; - + $id = $this->mTitle->getArticleID(); if( !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $id == 0 ) { return false; } + if (!$cascade) { + $cascade = false; + } + + // Take this opportunity to purge out expired restrictions + Title::purgeExpiredRestrictions(); + # FIXME: Same limitations as described in ProtectionForm.php (line 37); # we expect a single selection, but the schema allows otherwise. $current = array(); @@ -1651,48 +1669,89 @@ class Article { $current = Article::flattenRestrictions( $current ); $updated = Article::flattenRestrictions( $limit ); - + $changed = ( $current != $updated ); + $changed = $changed || ($this->mTitle->areRestrictionsCascading() != $cascade); + $changed = $changed || ($this->mTitle->mRestrictionsExpiry != $expiry); $protect = ( $updated != '' ); - + # If nothing's changed, do nothing if( $changed ) { + global $wgGroupPermissions; if( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) { - $dbw =& wfGetDB( DB_MASTER ); - + $dbw = wfGetDB( DB_MASTER ); + + $encodedExpiry = Block::encodeExpiry($expiry, $dbw ); + + $expiry_description = ''; + if ( $encodedExpiry != 'infinity' ) { + $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) ).')'; + } + # Prepare a null revision to be added to the history $comment = $wgContLang->ucfirst( wfMsgForContent( $protect ? 'protectedarticle' : 'unprotectedarticle', $this->mTitle->getPrefixedText() ) ); + + foreach( $limit as $action => $restrictions ) { + # Check if the group level required to edit also can protect pages + # Otherwise, people who cannot normally protect can "protect" pages via transclusion + $cascade = ( $cascade && isset($wgGroupPermissions[$restrictions]['protect']) && $wgGroupPermissions[$restrictions]['protect'] ); + } + + $cascade_description = ''; + if ($cascade) { + $cascade_description = ' ['.wfMsg('protect-summary-cascade').']'; + } + if( $reason ) $comment .= ": $reason"; if( $protect ) $comment .= " [$updated]"; + if ( $expiry_description && $protect ) + $comment .= "$expiry_description"; + if ( $cascade ) + $comment .= "$cascade_description"; + $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true ); $nullRevId = $nullRevision->insertOn( $dbw ); - + + # 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 ? 1 : 0 + , 'pr_expiry' => $encodedExpiry ), __METHOD__ ); + } else { + $dbw->delete( 'page_restrictions', array( 'pr_page' => $id, + 'pr_type' => $action ), __METHOD__ ); + } + } + # Update page record $dbw->update( 'page', array( /* SET */ 'page_touched' => $dbw->timestamp(), - 'page_restrictions' => $updated, + 'page_restrictions' => '', 'page_latest' => $nullRevId ), array( /* WHERE */ 'page_id' => $id ), 'Article::protect' ); wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) ); - + # Update the protection log $log = new LogPage( 'protect' ); + if( $protect ) { - $log->addEntry( 'protect', $this->mTitle, trim( $reason . " [$updated]" ) ); + $log->addEntry( 'protect', $this->mTitle, trim( $reason . " [$updated]$cascade_description$expiry_description" ) ); } else { $log->addEntry( 'unprotect', $this->mTitle, $reason ); } - + } # End hook } # End "changed" check - + return true; } @@ -1745,9 +1804,9 @@ class Article { } $wgOut->setPagetitle( wfMsg( 'confirmdelete' ) ); - + # Better double-check that it hasn't been deleted yet! - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $conds = $this->mTitle->pageCond(); $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); if ( $latest === false ) { @@ -1769,7 +1828,7 @@ class Article { # and insert a warning if it does $maxRevisions = 20; $authors = $this->getLastNAuthors( $maxRevisions, $latest ); - + if( count( $authors ) > 1 && !$confirm ) { $skin=$wgUser->getSkin(); $wgOut->addHTML( '<strong>' . wfMsg( 'historywarning' ) . ' ' . $skin->historyLink() . '</strong>' ); @@ -1813,7 +1872,7 @@ class Article { $reason = wfMsgForContent( 'exblank' ); } - if( $length < 500 && $reason === '' ) { + if( $reason === '' ) { # comment field=255, let's grep the first 150 to have some user # space left global $wgContLang; @@ -1849,7 +1908,7 @@ class Article { // First try the slave // If that doesn't have the latest revision, try the master $continue = 2; - $db =& wfGetDB( DB_SLAVE ); + $db = wfGetDB( DB_SLAVE ); do { $res = $db->select( array( 'page', 'revision' ), array( 'rev_id', 'rev_user_text' ), @@ -1868,7 +1927,7 @@ class Article { } $row = $db->fetchObject( $res ); if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { - $db =& wfGetDB( DB_MASTER ); + $db = wfGetDB( DB_MASTER ); $continue--; } else { $continue = 0; @@ -1882,7 +1941,7 @@ class Article { wfProfileOut( __METHOD__ ); return $authors; } - + /** * Output deletion confirmation dialog */ @@ -1929,6 +1988,23 @@ class Article { </form>\n" ); $wgOut->returnToMain( false ); + + $this->showLogExtract( $wgOut ); + } + + + /** + * Fetch deletion log + */ + function showLogExtract( &$out ) { + # Show relevant lines from the deletion log: + $out->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" ); + $logViewer = new LogViewer( + new LogReader( + new FauxRequest( + array( 'page' => $this->mTitle->getPrefixedText(), + 'type' => 'delete' ) ) ) ); + $logViewer->showList( $out ); } @@ -1969,7 +2045,7 @@ class Article { wfDebug( __METHOD__."\n" ); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $ns = $this->mTitle->getNamespace(); $t = $this->mTitle->getDBkey(); $id = $this->mTitle->getArticleID(); @@ -2004,12 +2080,16 @@ class Article { 'ar_text_id' => 'rev_text_id', 'ar_text' => '\'\'', // Be explicit to appease 'ar_flags' => '\'\'', // MySQL's "strict mode"... + 'ar_len' => 'rev_len' ), array( 'page_id' => $id, 'page_id = rev_page' ), __METHOD__ ); + # Delete restrictions for it + $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__); @@ -2078,7 +2158,7 @@ class Article { $wgOut->addWikiText( wfMsg( 'sessionfailure' ) ); return; } - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); # Enhanced rollback, marks edits rc_bot=1 $bot = $wgRequest->getBool( 'bot' ); @@ -2103,7 +2183,7 @@ class Article { if( $current->getComment() != '') { $wgOut->addHTML( wfMsg( 'editcomment', - htmlspecialchars( $current->getComment() ) ) ); + $wgUser->getSkin()->formatComment( $current->getComment() ) ) ); } return; } @@ -2189,7 +2269,7 @@ class Article { * Do standard deferred updates after page edit. * Update links tables, site stats, search index and message cache. * Every 1000th edit, prune the recent changes table. - * + * * @private * @param $text New text of the article * @param $summary Edit summary @@ -2222,7 +2302,7 @@ class Article { # Periodically flush old entries from the recentchanges table. global $wgRCMaxAge; - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $cutoff = $dbw->timestamp( time() - $wgRCMaxAge ); $recentchanges = $dbw->tableName( 'recentchanges' ); $sql = "DELETE FROM $recentchanges WHERE rc_timestamp < '{$cutoff}'"; @@ -2269,13 +2349,13 @@ class Article { wfProfileOut( __METHOD__ ); } - + /** * Perform article updates on a special page creation. * * @param Revision $rev * - * @fixme This is a shitty interface function. Kill it and replace the + * @todo This is a shitty interface function. Kill it and replace the * other shitty functions like editUpdates and such so it's not needed * anymore. */ @@ -2299,8 +2379,8 @@ class Article { global $wgLang, $wgOut, $wgUser; if ( !wfRunHooks( 'DisplayOldSubtitle', array(&$this, &$oldid) ) ) { - return; - } + return; + } $revision = Revision::newFromId( $oldid ); @@ -2326,10 +2406,10 @@ class Article { $nextdiff = $current ? wfMsg( 'diff' ) : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=next&oldid='.$oldid ); - + $userlinks = $sk->userLink( $revision->getUser(), $revision->getUserText() ) . $sk->userToolLinks( $revision->getUser(), $revision->getUserText() ); - + $r = "\n\t\t\t\t<div id=\"mw-revision-info\">" . wfMsg( 'revision-info', $td, $userlinks ) . "</div>\n" . "\n\t\t\t\t<div id=\"mw-revision-nav\">" . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t"; $wgOut->setSubtitle( $r ); @@ -2381,25 +2461,40 @@ class Article { * @return bool */ function isFileCacheable() { - global $wgUser, $wgUseFileCache, $wgShowIPinHeader, $wgRequest; + global $wgUser, $wgUseFileCache, $wgShowIPinHeader, $wgRequest, $wgLang, $wgContLang; $action = $wgRequest->getVal( 'action' ); $oldid = $wgRequest->getVal( 'oldid' ); $diff = $wgRequest->getVal( 'diff' ); $redirect = $wgRequest->getVal( 'redirect' ); $printable = $wgRequest->getVal( 'printable' ); + $page = $wgRequest->getVal( 'page' ); + + //check for non-standard user language; this covers uselang, + //and extensions for auto-detecting user language. + $ulang = $wgLang->getCode(); + $clang = $wgContLang->getCode(); - return $wgUseFileCache - and (!$wgShowIPinHeader) - and ($this->getID() != 0) - and ($wgUser->isAnon()) - and (!$wgUser->getNewtalk()) - and ($this->mTitle->getNamespace() != NS_SPECIAL ) - and (empty( $action ) || $action == 'view') - and (!isset($oldid)) - and (!isset($diff)) - and (!isset($redirect)) - and (!isset($printable)) - and (!$this->mRedirectedFrom); + $cacheable = $wgUseFileCache + && (!$wgShowIPinHeader) + && ($this->getID() != 0) + && ($wgUser->isAnon()) + && (!$wgUser->getNewtalk()) + && ($this->mTitle->getNamespace() != NS_SPECIAL ) + && (empty( $action ) || $action == 'view') + && (!isset($oldid)) + && (!isset($diff)) + && (!isset($redirect)) + && (!isset($printable)) + && !isset($page) + && (!$this->mRedirectedFrom) + && ($ulang === $clang); + + if ( $cacheable ) { + //extension may have reason to disable file caching on some pages. + $cacheable = wfRunHooks( 'IsFileCacheable', array( $this ) ); + } + + return $cacheable; } /** @@ -2446,7 +2541,7 @@ class Article { function quickEdit( $text, $comment = '', $minor = 0 ) { wfProfileIn( __METHOD__ ); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->begin(); $revision = new Revision( array( 'page' => $this->getId(), @@ -2471,7 +2566,7 @@ class Article { $id = intval( $id ); global $wgHitcounterUpdateFreq, $wgDBtype; - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $pageTable = $dbw->tableName( 'page' ); $hitcounterTable = $dbw->tableName( 'hitcounter' ); $acchitsTable = $dbw->tableName( 'acchits' ); @@ -2555,7 +2650,7 @@ class Article { $title->touchLinks(); $title->purgeSquid(); - + # File cache if ( $wgUseFileCache ) { $cm = new HTMLFileCache( $title ); @@ -2617,7 +2712,7 @@ class Article { $wgOut->addHTML(wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' ) ); } } else { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $wl_clause = array( 'wl_title' => $page->getDBkey(), 'wl_namespace' => $page->getNamespace() ); @@ -2659,7 +2754,7 @@ class Article { return false; } - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $rev_clause = array( 'rev_page' => $id ); @@ -2693,7 +2788,7 @@ class Article { return array(); } - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( array( 'templatelinks' ), array( 'tl_namespace', 'tl_title' ), array( 'tl_from' => $id ), @@ -2708,7 +2803,7 @@ class Article { $dbr->freeResult( $res ); return $result; } - + /** * Return an auto-generated summary if the text provided is a redirect. * @@ -2785,6 +2880,84 @@ class Article { return $summary; } + + /** + * Add the primary page-view wikitext to the output buffer + * Saves the text into the parser cache if possible. + * Updates templatelinks if it is out of date. + * + * @param string $text + * @param bool $cache + */ + public function outputWikiText( $text, $cache = true ) { + global $wgParser, $wgUser, $wgOut; + + $popts = $wgOut->parserOptions(); + $popts->setTidy(true); + $parserOutput = $wgParser->parse( $text, $this->mTitle, + $popts, true, true, $this->getRevIdFetched() ); + $popts->setTidy(false); + if ( $cache && $this && $parserOutput->getCacheTime() != -1 ) { + $parserCache =& ParserCache::singleton(); + $parserCache->save( $parserOutput, $this, $wgUser ); + } + + if ( !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 templates from templatelinks + $id = $this->mTitle->getArticleID(); + + $tlTemplates = array(); + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'templatelinks' ), + array( 'tl_namespace', 'tl_title' ), + array( 'tl_from' => $id ), + 'Article:getUsedTemplates' ); + + global $wgContLang; + + if ( false !== $res ) { + if ( $dbr->numRows( $res ) ) { + while ( $row = $dbr->fetchObject( $res ) ) { + $tlTemplates[] = $wgContLang->getNsText( $row->tl_namespace ) . ':' . $row->tl_title ; + } + } + } + + # Get templates from parser output. + $poTemplates_allns = $parserOutput->getTemplates(); + + $poTemplates = array (); + foreach ( $poTemplates_allns as $ns_templates ) { + $poTemplates = array_merge( $poTemplates, $ns_templates ); + } + + # Get the diff + $templates_diff = array_diff( $poTemplates, $tlTemplates ); + + if ( count( $templates_diff ) > 0 ) { + # Whee, link updates time. + $u = new LinksUpdate( $this->mTitle, $parserOutput ); + + $dbw = wfGetDb( DB_MASTER ); + $dbw->begin(); + + $u->doUpdate(); + + $dbw->commit(); + } + } + + $wgOut->addParserOutput( $parserOutput ); + } + } ?> diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php index e33ef1bf..9395032f 100644 --- a/includes/AuthPlugin.php +++ b/includes/AuthPlugin.php @@ -1,6 +1,5 @@ <?php /** - * @package MediaWiki */ # Copyright (C) 2004 Brion Vibber <brion@pobox.com> # http://www.mediawiki.org/ @@ -33,7 +32,6 @@ * This interface is new, and might change a bit before 1.4.0 final is * done... * - * @package MediaWiki */ class AuthPlugin { /** @@ -187,12 +185,14 @@ class AuthPlugin { * Add a user to the external authentication database. * Return true if successful. * - * @param User $user + * @param User $user - only the name should be assumed valid at this point * @param string $password + * @param string $email + * @param string $realname * @return bool * @public */ - function addUser( $user, $password ) { + function addUser( $user, $password, $email='', $realname='' ) { return true; } diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 8de5608f..72a71c71 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -8,9 +8,11 @@ function __autoload($className) { global $wgAutoloadClasses; static $localClasses = array( + # Includes 'AjaxDispatcher' => 'includes/AjaxDispatcher.php', 'AjaxCachePolicy' => 'includes/AjaxFunctions.php', 'AjaxResponse' => 'includes/AjaxResponse.php', + 'AlphabeticPager' => 'includes/Pager.php', 'Article' => 'includes/Article.php', 'AuthPlugin' => 'includes/AuthPlugin.php', 'BagOStuff' => 'includes/BagOStuff.php', @@ -39,9 +41,8 @@ function __autoload($className) { 'Database' => 'includes/Database.php', 'DatabaseMysql' => 'includes/Database.php', 'ResultWrapper' => 'includes/Database.php', - 'OracleBlob' => 'includes/DatabaseOracle.php', - 'DatabaseOracle' => 'includes/DatabaseOracle.php', 'DatabasePostgres' => 'includes/DatabasePostgres.php', + 'DatabaseOracle' => 'includes/DatabaseOracle.php', 'DateFormatter' => 'includes/DateFormatter.php', 'DifferenceEngine' => 'includes/DifferenceEngine.php', '_DiffOp' => 'includes/DifferenceEngine.php', @@ -95,6 +96,7 @@ function __autoload($className) { 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php', 'Http' => 'includes/HttpFunctions.php', 'Image' => 'includes/Image.php', + 'ArchivedFile' => 'includes/Image.php', 'IP' => 'includes/IP.php', 'ThumbnailImage' => 'includes/Image.php', 'ImageGallery' => 'includes/ImageGallery.php', @@ -114,6 +116,10 @@ function __autoload($className) { 'MacBinary' => 'includes/MacBinary.php', 'MagicWord' => 'includes/MagicWord.php', 'MathRenderer' => 'includes/Math.php', + 'MediaTransformOutput' => 'includes/MediaTransformOutput.php', + 'ThumbnailImage' => 'includes/MediaTransformOutput.php', + 'MediaTransformError' => 'includes/MediaTransformOutput.php', + 'TransformParameterError' => 'includes/MediaTransformOutput.php', 'MessageCache' => 'includes/MessageCache.php', 'MimeMagic' => 'includes/MimeMagic.php', 'Namespace' => 'includes/Namespace.php', @@ -124,16 +130,18 @@ function __autoload($className) { 'ReverseChronologicalPager' => 'includes/Pager.php', 'TablePager' => 'includes/Pager.php', 'Parser' => 'includes/Parser.php', - 'ParserOutput' => 'includes/Parser.php', - 'ParserOptions' => 'includes/Parser.php', + 'ParserOutput' => 'includes/ParserOutput.php', + 'ParserOptions' => 'includes/ParserOptions.php', 'ParserCache' => 'includes/ParserCache.php', + 'PatrolLog' => 'includes/PatrolLog.php', 'ProfilerSimple' => 'includes/ProfilerSimple.php', 'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php', 'Profiler' => 'includes/Profiler.php', 'ProxyTools' => 'includes/ProxyTools.php', 'ProtectionForm' => 'includes/ProtectionForm.php', 'QueryPage' => 'includes/QueryPage.php', - 'PageQueryPage' => 'includes/QueryPage.php', + 'PageQueryPage' => 'includes/PageQueryPage.php', + 'ImageQueryPage' => 'includes/ImageQueryPage.php', 'RawPage' => 'includes/RawPage.php', 'RecentChange' => 'includes/RecentChange.php', 'Revision' => 'includes/Revision.php', @@ -148,6 +156,7 @@ function __autoload($className) { 'SearchPostgres' => 'includes/SearchPostgres.php', 'SearchUpdate' => 'includes/SearchUpdate.php', 'SearchUpdateMyISAM' => 'includes/SearchUpdate.php', + 'SearchOracle' => 'includes/SearchOracle.php', 'SiteConfiguration' => 'includes/SiteConfiguration.php', 'SiteStats' => 'includes/SiteStats.php', 'SiteStatsUpdate' => 'includes/SiteStats.php', @@ -160,7 +169,6 @@ function __autoload($className) { 'IPBlockForm' => 'includes/SpecialBlockip.php', 'SpecialBookSources' => 'includes/SpecialBooksources.php', 'BrokenRedirectsPage' => 'includes/SpecialBrokenRedirects.php', - 'CategoriesPage' => 'includes/SpecialCategories.php', 'EmailConfirmation' => 'includes/SpecialConfirmemail.php', 'ContributionsPage' => 'includes/SpecialContributions.php', 'DeadendPagesPage' => 'includes/SpecialDeadendpages.php', @@ -173,7 +181,6 @@ function __autoload($className) { 'ImportStreamSource' => 'includes/SpecialImport.php', 'IPUnblockForm' => 'includes/SpecialIpblocklist.php', 'ListredirectsPage' => 'includes/SpecialListredirects.php', - 'ListUsersPage' => 'includes/SpecialListusers.php', 'DBLockForm' => 'includes/SpecialLockdb.php', 'LogReader' => 'includes/SpecialLog.php', 'LogViewer' => 'includes/SpecialLog.php', @@ -185,6 +192,7 @@ function __autoload($className) { 'MostlinkedPage' => 'includes/SpecialMostlinked.php', 'MostlinkedCategoriesPage' => 'includes/SpecialMostlinkedcategories.php', 'MostrevisionsPage' => 'includes/SpecialMostrevisions.php', + 'FewestrevisionsPage' => 'includes/SpecialFewestrevisions.php', 'MovePageForm' => 'includes/SpecialMovepage.php', 'NewbieContributionsPage' => 'includes/SpecialNewbieContributions.php', 'NewPagesPage' => 'includes/SpecialNewpages.php', @@ -194,6 +202,7 @@ function __autoload($className) { 'PopularPagesPage' => 'includes/SpecialPopularpages.php', 'PreferencesForm' => 'includes/SpecialPreferences.php', 'SpecialPrefixindex' => 'includes/SpecialPrefixindex.php', + 'PasswordResetForm' => 'includes/SpecialResetpass.php', 'RevisionDeleteForm' => 'includes/SpecialRevisiondelete.php', 'RevisionDeleter' => 'includes/SpecialRevisiondelete.php', 'SpecialSearch' => 'includes/SpecialSearch.php', @@ -215,6 +224,7 @@ function __autoload($className) { 'WantedCategoriesPage' => 'includes/SpecialWantedcategories.php', 'WantedPagesPage' => 'includes/SpecialWantedpages.php', 'WhatLinksHerePage' => 'includes/SpecialWhatlinkshere.php', + 'WithoutInterwikiPage' => 'includes/SpecialWithoutinterwiki.php', 'SquidUpdate' => 'includes/SquidUpdate.php', 'ReplacementArray' => 'includes/StringUtils.php', 'Replacer' => 'includes/StringUtils.php', @@ -237,13 +247,27 @@ function __autoload($className) { 'Xml' => 'includes/Xml.php', 'ZhClient' => 'includes/ZhClient.php', 'memcached' => 'includes/memcached-client.php', + + # Media + 'BitmapHandler' => 'includes/media/Bitmap.php', + 'BmpHandler' => 'includes/media/BMP.php', + 'DjVuHandler' => 'includes/media/DjVu.php', + 'MediaHandler' => 'includes/media/Generic.php', + 'ImageHandler' => 'includes/media/Generic.php', + 'SvgHandler' => 'includes/media/SVG.php', + + # Normal 'UtfNormal' => 'includes/normal/UtfNormal.php', + + # Templates 'UsercreateTemplate' => 'includes/templates/Userlogin.php', 'UserloginTemplate' => 'includes/templates/Userlogin.php', + + # Languages 'Language' => 'languages/Language.php', - 'PasswordResetForm' => 'includes/SpecialResetpass.php', + 'RandomPage' => 'includes/SpecialRandompage.php', - // API classes + # API 'ApiBase' => 'includes/api/ApiBase.php', 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php', 'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php', @@ -274,6 +298,7 @@ function __autoload($className) { 'ApiResult' => 'includes/api/ApiResult.php', ); + wfProfileIn( __METHOD__ ); if ( isset( $localClasses[$className] ) ) { $filename = $localClasses[$className]; } elseif ( isset( $wgAutoloadClasses[$className] ) ) { @@ -290,6 +315,7 @@ function __autoload($className) { } if ( !$filename ) { # Give up + wfProfileOut( __METHOD__ ); return; } } @@ -300,6 +326,7 @@ function __autoload($className) { $filename = "$IP/$filename"; } require( $filename ); + wfProfileOut( __METHOD__ ); } function wfLoadAllExtensions() { @@ -311,10 +338,10 @@ function wfLoadAllExtensions() { # guaranteed by entering special pages via SpecialPage members such as # executePath(), but here we have to take a more explicit measure. - require_once( 'SpecialPage.php' ); + require_once( dirname(__FILE__) . '/SpecialPage.php' ); foreach( $wgAutoloadClasses as $class => $file ) { - if ( ! class_exists( $class ) ) { + if( !( class_exists( $class ) || interface_exists( $class ) ) ) { require( $file ); } } diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php index c720807d..2a04b9dd 100644 --- a/includes/BagOStuff.php +++ b/includes/BagOStuff.php @@ -19,7 +19,6 @@ # http://www.gnu.org/copyleft/gpl.html /** * - * @package MediaWiki */ /** @@ -29,15 +28,16 @@ * the PHP memcached client. * * backends for local hash array and SQL table included: - * $bag = new HashBagOStuff(); - * $bag = new MysqlBagOStuff($tablename); # connect to db first + * <code> + * $bag = new HashBagOStuff(); + * $bag = new MysqlBagOStuff($tablename); # connect to db first + * </code> * - * @package MediaWiki */ class BagOStuff { var $debugmode; - function BagOStuff() { + function __construct() { $this->set_debug( false ); } @@ -163,7 +163,6 @@ class BagOStuff { /** * Functional versions! * @todo document - * @package MediaWiki */ class HashBagOStuff extends BagOStuff { /* @@ -218,7 +217,6 @@ CREATE TABLE objectcache ( /** * @todo document * @abstract - * @package MediaWiki */ abstract class SqlBagOStuff extends BagOStuff { var $table; @@ -386,34 +384,32 @@ abstract class SqlBagOStuff extends BagOStuff { /** * @todo document - * @package MediaWiki */ class MediaWikiBagOStuff extends SqlBagOStuff { var $tableInitialised = false; function _doquery($sql) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); return $dbw->query($sql, 'MediaWikiBagOStuff::_doquery'); } function _doinsert($t, $v) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); return $dbw->insert($t, $v, 'MediaWikiBagOStuff::_doinsert', array( 'IGNORE' ) ); } function _fetchobject($result) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); return $dbw->fetchObject($result); } function _freeresult($result) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); return $dbw->freeResult($result); } function _dberror($result) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); return $dbw->lastError(); } function _maxdatetime() { - $dbw =& wfGetDB(DB_MASTER); if ( time() > 0x7fffffff ) { return $this->_fromunixtime( 1<<62 ); } else { @@ -421,24 +417,24 @@ class MediaWikiBagOStuff extends SqlBagOStuff { } } function _fromunixtime($ts) { - $dbw =& wfGetDB(DB_MASTER); + $dbw = wfGetDB(DB_MASTER); return $dbw->timestamp($ts); } function _strencode($s) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); return $dbw->strencode($s); } function _blobencode($s) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); return $dbw->encodeBlob($s); } function _blobdecode($s) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); return $dbw->decodeBlob($s); } function getTableName() { if ( !$this->tableInitialised ) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); /* This is actually a hack, we should be able to use Language classes here... or not */ if (!$dbw) @@ -463,7 +459,6 @@ class MediaWikiBagOStuff extends SqlBagOStuff { * that Turck's serializer is faster, so a possible future extension would be * to use it for arrays but not for objects. * - * @package MediaWiki */ class TurckBagOStuff extends BagOStuff { function get($key) { @@ -498,9 +493,7 @@ class TurckBagOStuff extends BagOStuff { /** * This is a wrapper for APC's shared memory functions * - * @package MediaWiki */ - class APCBagOStuff extends BagOStuff { function get($key) { $val = apc_fetch($key); @@ -528,7 +521,6 @@ class APCBagOStuff extends BagOStuff { * This is basically identical to the Turck MMCache version, * mostly because eAccelerator is based on Turck MMCache. * - * @package MediaWiki */ class eAccelBagOStuff extends BagOStuff { function get($key) { @@ -560,6 +552,9 @@ class eAccelBagOStuff extends BagOStuff { } } +/** + * @todo document + */ class DBABagOStuff extends BagOStuff { var $mHandler, $mFile, $mReader, $mWriter, $mDisabled; diff --git a/includes/Block.php b/includes/Block.php index ff813ba3..94bfa5b4 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -1,7 +1,6 @@ <?php /** * Blocks and bans object - * @package MediaWiki */ /** @@ -12,22 +11,24 @@ * Globals used: $wgAutoblockExpiry, $wgAntiLockFlags * * @todo This could be used everywhere, but it isn't. - * @package MediaWiki */ class Block { /* public*/ var $mAddress, $mUser, $mBy, $mReason, $mTimestamp, $mAuto, $mId, $mExpiry, - $mRangeStart, $mRangeEnd, $mAnonOnly, $mEnableAutoblock; + $mRangeStart, $mRangeEnd, $mAnonOnly, $mEnableAutoblock, $mHideName; /* private */ var $mNetworkBits, $mIntegerAddr, $mForUpdate, $mFromMaster, $mByName; const EB_KEEP_EXPIRED = 1; const EB_FOR_UPDATE = 2; const EB_RANGE_ONLY = 4; - function Block( $address = '', $user = 0, $by = 0, $reason = '', - $timestamp = '' , $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0 ) + function __construct( $address = '', $user = 0, $by = 0, $reason = '', + $timestamp = '' , $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0, + $hideName = 0 ) { $this->mId = 0; + # Expand valid IPv6 addresses + $address = IP::sanitizeIP( $address ); $this->mAddress = $address; $this->mUser = $user; $this->mBy = $by; @@ -38,6 +39,7 @@ class Block $this->mCreateAccount = $createAccount; $this->mExpiry = self::decodeExpiry( $expiry ); $this->mEnableAutoblock = $enableAutoblock; + $this->mHideName = $hideName; $this->mForUpdate = false; $this->mFromMaster = false; @@ -58,7 +60,7 @@ class Block static function newFromID( $id ) { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->resultObject( $dbr->select( 'ipblocks', '*', array( 'ipb_id' => $id ), __METHOD__ ) ); $block = new Block; @@ -74,7 +76,7 @@ class Block $this->mAddress = $this->mReason = $this->mTimestamp = ''; $this->mId = $this->mAnonOnly = $this->mCreateAccount = $this->mEnableAutoblock = $this->mAuto = $this->mUser = - $this->mBy = 0; + $this->mBy = $this->mHideName = 0; $this->mByName = false; } @@ -85,14 +87,14 @@ class Block { global $wgAntiLockFlags; if ( $this->mForUpdate || $this->mFromMaster ) { - $db =& wfGetDB( DB_MASTER ); + $db = wfGetDB( DB_MASTER ); if ( !$this->mForUpdate || ($wgAntiLockFlags & ALF_NO_BLOCK_LOCK) ) { $options = array(); } else { $options = array( 'FOR UPDATE' ); } } else { - $db =& wfGetDB( DB_SLAVE ); + $db = wfGetDB( DB_SLAVE ); $options = array(); } return $db; @@ -147,7 +149,7 @@ class Block } # Try range block - if ( $this->loadRange( $address, $killExpired, $user == 0 ) ) { + if ( $this->loadRange( $address, $killExpired, $user ) ) { if ( $user && $this->mAnonOnly ) { $this->clear(); return false; @@ -176,7 +178,8 @@ class Block /** * Fill in member variables from a result wrapper */ - function loadFromResult( ResultWrapper $res, $killExpired = true ) { + function loadFromResult( ResultWrapper $res, $killExpired = true ) + { $ret = false; if ( 0 != $res->numRows() ) { # Get first block @@ -211,7 +214,7 @@ class Block * Search the database for any range blocks matching the given address, and * load the row if one is found. */ - function loadRange( $address, $killExpired = true ) + function loadRange( $address, $killExpired = true, $user = 0 ) { $iaddr = IP::toHex( $address ); if ( $iaddr === false ) { @@ -230,6 +233,10 @@ class Block "ipb_range_start <= '$iaddr'", "ipb_range_end >= '$iaddr'" ); + + if ( $user ) { + $conds['ipb_anon_only'] = 0; + } $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); $success = $this->loadFromResult( $res, $killExpired ); @@ -255,6 +262,7 @@ class Block $this->mAnonOnly = $row->ipb_anon_only; $this->mCreateAccount = $row->ipb_create_account; $this->mEnableAutoblock = $row->ipb_enable_autoblock; + $this->mHideName = $row->ipb_deleted; $this->mId = $row->ipb_id; $this->mExpiry = self::decodeExpiry( $row->ipb_expiry ); if ( isset( $row->user_name ) ) { @@ -286,7 +294,7 @@ class Block $block = new Block(); if ( $flags & Block::EB_FOR_UPDATE ) { - $db =& wfGetDB( DB_MASTER ); + $db = wfGetDB( DB_MASTER ); if ( $wgAntiLockFlags & ALF_NO_BLOCK_LOCK ) { $options = ''; } else { @@ -294,7 +302,7 @@ class Block } $block->forUpdate( true ); } else { - $db =& wfGetDB( DB_SLAVE ); + $db = wfGetDB( DB_SLAVE ); $options = ''; } if ( $flags & Block::EB_RANGE_ONLY ) { @@ -341,7 +349,7 @@ class Block throw new MWException( "Block::delete() now requires that the mId member be filled\n" ); } - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->delete( 'ipblocks', array( 'ipb_id' => $this->mId ), __METHOD__ ); return $dbw->affectedRows() > 0; } @@ -353,8 +361,7 @@ class Block function insert() { wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" ); - $dbw =& wfGetDB( DB_MASTER ); - $dbw->begin(); + $dbw = wfGetDB( DB_MASTER ); # Unset ipb_anon_only for user blocks, makes no sense if ( $this->mUser ) { @@ -385,6 +392,7 @@ class Block 'ipb_expiry' => self::encodeExpiry( $this->mExpiry, $dbw ), 'ipb_range_start' => $this->mRangeStart, 'ipb_range_end' => $this->mRangeEnd, + 'ipb_deleted' => $this->mHideName ), 'Block::insert', array( 'IGNORE' ) ); $affected = $dbw->affectedRows(); @@ -418,20 +426,20 @@ class Block } else { #Limit is 1, so no loop needed. $retroblockip = $row->rc_ip; - return $this->doAutoblock($retroblockip); + return $this->doAutoblock( $retroblockip, true ); } } } /** * Autoblocks the given IP, referring to this Block. - * @param $autoblockip The IP to autoblock. + * @param string $autoblockip The IP to autoblock. + * @param bool $justInserted The main block was just inserted * @return bool Whether or not an autoblock was inserted. */ - function doAutoblock( $autoblockip ) { + function doAutoblock( $autoblockip, $justInserted = false ) { # Check if this IP address is already blocked - $dbw =& wfGetDB( DB_MASTER ); - $dbw->begin(); + $dbw = wfGetDB( DB_MASTER ); # If autoblocks are disabled, go away. if ( !$this->mEnableAutoblock ) { @@ -480,7 +488,9 @@ class Block return; } # Just update the timestamp - $ipblock->updateTimestamp(); + if ( !$justInserted ) { + $ipblock->updateTimestamp(); + } return; } else { $ipblock = new Block; @@ -495,6 +505,8 @@ class Block $ipblock->mTimestamp = wfTimestampNow(); $ipblock->mAuto = 1; $ipblock->mCreateAccount = $this->mCreateAccount; + # Continue suppressing the name if needed + $ipblock->mHideName = $this->mHideName; # If the user is already blocked with an expiry date, we don't # want to pile on top of that! @@ -544,7 +556,7 @@ class Block $this->mTimestamp = wfTimestamp(); $this->mExpiry = Block::getAutoblockExpiry( $this->mTimestamp ); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'ipblocks', array( /* SET */ 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), @@ -628,16 +640,36 @@ class Block global $wgAutoblockExpiry; return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry ); } - - static function normaliseRange( $range ) - { + + /** + * Gets rid of uneeded numbers in quad-dotted/octet IP strings + * For example, 127.111.113.151/24 -> 127.111.113.0/24 + */ + static function normaliseRange( $range ) { $parts = explode( '/', $range ); if ( count( $parts ) == 2 ) { - $shift = 32 - $parts[1]; - $ipint = IP::toUnsigned( $parts[0] ); - $ipint = $ipint >> $shift << $shift; - $newip = long2ip( $ipint ); - $range = "$newip/{$parts[1]}"; + // IPv6 + 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!!! + # Convert to a padded binary number + $network = wfBaseConvert( $ipint, 10, 2, 128 ); + # Truncate the last (128-$bits) bits and replace them with zeros + $network = str_pad( substr( $network, 0, $bits ), 128, 0, STR_PAD_RIGHT ); + # Convert back to an integer + $network = wfBaseConvert( $network, 2, 10 ); + # Reform octet address + $newip = IP::toOctet( $network ); + $range = "$newip/{$parts[1]}"; + } // IPv4 + else if ( IP::isIPv4($range) && $parts[1] >= 16 && $parts[1] <= 32 ) { + $shift = 32 - $parts[1]; + $ipint = IP::toUnsigned( $parts[0] ); + $ipint = $ipint >> $shift << $shift; + $newip = long2ip( $ipint ); + $range = "$newip/{$parts[1]}"; + } } return $range; } @@ -646,7 +678,7 @@ class Block * Purge expired blocks from the ipblocks table */ static function purgeExpired() { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->delete( 'ipblocks', array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), __METHOD__ ); } @@ -658,7 +690,7 @@ class Block /* static $infinity; if ( !isset( $infinity ) ) { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $infinity = $dbr->bigTimestamp(); } return $infinity; diff --git a/includes/CacheDependency.php b/includes/CacheDependency.php index 4bb3d328..bb5c5437 100644 --- a/includes/CacheDependency.php +++ b/includes/CacheDependency.php @@ -4,6 +4,7 @@ * This class stores an arbitrary value along with its dependencies. * Users should typically only use DependencyWrapper::getFromCache(), rather * than instantiating one of these objects directly. + * @addtogroup Cache */ class DependencyWrapper { var $value; @@ -95,6 +96,9 @@ class DependencyWrapper { } } +/** + * @addtogroup Cache + */ abstract class CacheDependency { /** * Returns true if the dependency is expired, false otherwise @@ -107,6 +111,9 @@ abstract class CacheDependency { function loadDependencyValues() {} } +/** + * @addtogroup Cache + */ class FileDependency extends CacheDependency { var $filename, $timestamp; @@ -163,6 +170,9 @@ class FileDependency extends CacheDependency { } } +/** + * @addtogroup Cache + */ class TitleDependency extends CacheDependency { var $titleObj; var $ns, $dbk; @@ -219,6 +229,9 @@ class TitleDependency extends CacheDependency { } } +/** + * @addtogroup Cache + */ class TitleListDependency extends CacheDependency { var $linkBatch; var $timestamps; @@ -244,7 +257,7 @@ class TitleListDependency extends CacheDependency { # Do the query if ( count( $timestamps ) ) { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $where = $this->getLinkBatch()->constructSet( 'page', $dbr ); $res = $dbr->select( 'page', array( 'page_namespace', 'page_title', 'page_touched' ), @@ -299,6 +312,9 @@ class TitleListDependency extends CacheDependency { } } +/** + * @addtogroup Cache + */ class GlobalDependency extends CacheDependency { var $name, $value; @@ -312,6 +328,9 @@ class GlobalDependency extends CacheDependency { } } +/** + * @addtogroup Cache + */ class ConstantDependency extends CacheDependency { var $name, $value; diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php index 0086a2f9..356f9ea2 100644 --- a/includes/CategoryPage.php +++ b/includes/CategoryPage.php @@ -3,17 +3,23 @@ * Special handling for category description pages * Modelled after ImagePage.php * - * @package MediaWiki */ if( !defined( 'MEDIAWIKI' ) ) die( 1 ); /** - * @package MediaWiki */ class CategoryPage extends Article { function view() { + global $wgRequest, $wgUser; + + $diff = $wgRequest->getVal( 'diff' ); + $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); + + if ( isset( $diff ) && $diffOnly ) + return Article::view(); + if(!wfRunHooks('CategoryPageView', array(&$this))) return; if ( NS_CATEGORY == $this->mTitle->getNamespace() ) { @@ -175,7 +181,7 @@ class CategoryViewer { } function doCategoryQuery() { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); if( $this->from != '' ) { $pageCondition = 'cl_sortkey >= ' . $dbr->addQuotes( $this->from ); $this->flip = false; @@ -196,6 +202,7 @@ class CategoryViewer { #+ $pageCondition, __METHOD__, array( 'ORDER BY' => $this->flip ? 'cl_sortkey DESC' : 'cl_sortkey', + 'USE INDEX' => 'cl_sortkey', 'LIMIT' => $this->limit + 1 ) ); $count = 0; @@ -234,11 +241,12 @@ class CategoryViewer { function getSubcategorySection() { # Don't show subcategories section if there are none. $r = ''; - if( count( $this->children ) > 0 ) { + $c = count( $this->children ); + if( $c > 0 ) { # Showing subcategories $r .= "<div id=\"mw-subcategories\">\n"; $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n"; - $r .= wfMsgExt( 'subcategorycount', array( 'parse' ), count( $this->children) ); + $r .= wfMsgExt( 'subcategorycount', array( 'parse' ), $c ); $r .= $this->formatList( $this->children, $this->children_start_char ); $r .= "\n</div>"; } @@ -247,11 +255,16 @@ class CategoryViewer { function getPagesSection() { $ti = htmlspecialchars( $this->title->getText() ); - $r = "<div id=\"mw-pages\">\n"; - $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n"; - $r .= wfMsgExt( 'categoryarticlecount', array( 'parse' ), count( $this->articles) ); - $r .= $this->formatList( $this->articles, $this->articles_start_char ); - $r .= "\n</div>"; + # Don't show articles section if there are none. + $r = ''; + $c = count( $this->articles ); + if( $c > 0 ) { + $r = "<div id=\"mw-pages\">\n"; + $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n"; + $r .= wfMsgExt( 'categoryarticlecount', array( 'parse' ), $c ); + $r .= $this->formatList( $this->articles, $this->articles_start_char ); + $r .= "\n</div>"; + } return $r; } @@ -391,7 +404,7 @@ class CategoryViewer { */ function pagingLinks( $title, $first, $last, $limit, $query = array() ) { global $wgUser, $wgLang; - $sk =& $this->getSkin(); + $sk = $this->getSkin(); $limitText = $wgLang->formatNum( $limit ); $prevLink = htmlspecialchars( wfMsg( 'prevn', $limitText ) ); diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php index a8cdf3ce..7faae935 100644 --- a/includes/Categoryfinder.php +++ b/includes/Categoryfinder.php @@ -1,26 +1,27 @@ <?php -/* -The "Categoryfinder" class takes a list of articles, creates an internal representation of all their parent -categories (as well as parents of parents etc.). From this representation, it determines which of these articles -are in one or all of a given subset of categories. - -Example use : - - # Determines wether the article with the page_id 12345 is in both - # "Category 1" and "Category 2" or their subcategories, respectively - - $cf = new Categoryfinder ; - $cf->seed ( - array ( 12345 ) , - array ( "Category 1","Category 2" ) , - "AND" - ) ; - $a = $cf->run() ; - print implode ( "," , $a ) ; - -*/ - +/** + * The "Categoryfinder" class takes a list of articles, creates an internal + * representation of all their parent categories (as well as parents of + * parents etc.). From this representation, it determines which of these + * articles are in one or all of a given subset of categories. + * + * Example use : + * <code> + * # Determines whether the article with the page_id 12345 is in both + * # "Category 1" and "Category 2" or their subcategories, respectively + * + * $cf = new Categoryfinder ; + * $cf->seed ( + * array ( 12345 ) , + * array ( "Category 1","Category 2" ) , + * "AND" + * ) ; + * $a = $cf->run() ; + * print implode ( "," , $a ) ; + * </code> + * + */ class Categoryfinder { var $articles = array () ; # The original article IDs passed to the seed function @@ -34,8 +35,8 @@ class Categoryfinder { /** * Constructor (currently empty). - */ - function Categoryfinder () { + */ + function __construct() { } /** @@ -61,10 +62,10 @@ class Categoryfinder { /** * Iterates through the parent tree starting with the seed values, * then checks the articles if they match the conditions - @return array of page_ids (those given to seed() that match the conditions) - */ + * @return array of page_ids (those given to seed() that match the conditions) + */ function run () { - $this->dbr =& wfGetDB( DB_SLAVE ); + $this->dbr = wfGetDB( DB_SLAVE ); while ( count ( $this->next ) > 0 ) { $this->scan_next_layer () ; } @@ -83,20 +84,20 @@ class Categoryfinder { /** * This functions recurses through the parent representation, trying to match the conditions - @param $id The article/category to check - @param $conds The array of categories to match - @return bool Does this match the conditions? - */ + * @param $id The article/category to check + * @param $conds The array of categories to match + * @return bool Does this match the conditions? + */ function check ( $id , &$conds ) { # Shortcut (runtime paranoia): No contitions=all matched if ( count ( $conds ) == 0 ) return true ; - + if ( !isset ( $this->parents[$id] ) ) return false ; # iterate through the parents foreach ( $this->parents[$id] AS $p ) { $pname = $p->cl_to ; - + # Is this a condition? if ( isset ( $conds[$pname] ) ) { # This key is in the category list! @@ -113,7 +114,7 @@ class Categoryfinder { } } } - + # Not done yet, try sub-parents if ( !isset ( $this->name2id[$pname] ) ) { # No sub-parent @@ -130,10 +131,10 @@ class Categoryfinder { /** * Scans a "parent layer" of the articles/categories in $this->next - */ + */ function scan_next_layer () { $fname = "Categoryfinder::scan_next_layer" ; - + # Find all parents of the article currently in $this->next $layer = array () ; $res = $this->dbr->select( @@ -161,7 +162,7 @@ class Categoryfinder { $this->dbr->freeResult( $res ) ; $this->next = array() ; - + # Find the IDs of all category pages in $layer, if they exist if ( count ( $layer ) > 0 ) { $res = $this->dbr->select( diff --git a/includes/ChangesList.php b/includes/ChangesList.php index a2c1a265..bc141579 100644 --- a/includes/ChangesList.php +++ b/includes/ChangesList.php @@ -1,15 +1,7 @@ <?php -/** - * @package MediaWiki - * Contain class to show various lists of change: - * - what's link here - * - related changes - * - recent changes - */ /** * @todo document - * @package MediaWiki */ class RCCacheEntry extends RecentChange { @@ -17,8 +9,7 @@ class RCCacheEntry extends RecentChange var $curlink , $difflink, $lastlink , $usertalklink , $versionlink ; var $userlink, $timestamp, $watched; - function newFromParent( $rc ) - { + function newFromParent( $rc ) { $rc2 = new RCCacheEntry; $rc2->mAttribs = $rc->mAttribs; $rc2->mExtra = $rc->mExtra; @@ -27,14 +18,17 @@ class RCCacheEntry extends RecentChange } ; /** - * @package MediaWiki + * Class to show various lists of changes: + * - what links here + * - related changes + * - recent changes */ class ChangesList { # Called by history lists and recent changes # /** @todo document */ - function ChangesList( &$skin ) { + function __construct( &$skin ) { $this->skin =& $skin; $this->preCacheMessages(); } @@ -47,7 +41,7 @@ class ChangesList { * @return ChangesList derivative */ public static function newFromUser( &$user ) { - $sk =& $user->getSkin(); + $sk = $user->getSkin(); $list = NULL; if( wfRunHooks( 'FetchChangesList', array( &$user, &$sk, &$list ) ) ) { return $user->getOption( 'usenewrc' ) ? new EnhancedChangesList( $sk ) : new OldChangesList( $sk ); @@ -64,7 +58,7 @@ class ChangesList { // Precache various messages if( !isset( $this->message ) ) { foreach( explode(' ', 'cur diff hist minoreditletter newpageletter last '. - 'blocklink changes history boteditletter' ) as $msg ) { + 'blocklink history boteditletter' ) as $msg ) { $this->message[$msg] = wfMsgExt( $msg, array( 'escape') ); } } @@ -212,6 +206,23 @@ class ChangesList { global $wgUseRCPatrol, $wgUser; return( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ); } + + /** + * Returns the string which indicates the number of watching users + */ + function numberofWatchingusers( $count ) { + global $wgLang; + static $cache = array(); + if ( $count > 0 ) { + if ( !isset( $cache[$count] ) ) { + $cache[$count] = wfMsgExt('number_of_watching_users_RCview', + array('parsemag', 'escape'), $wgLang->formatNum($count)); + } + return $cache[$count]; + } else { + return ''; + } + } } @@ -229,6 +240,7 @@ class OldChangesList extends ChangesList { wfProfileIn( $fname ); # Extract DB fields into local scope + // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( $rc->mAttribs ); # Should patrol-related stuff be shown? @@ -273,9 +285,7 @@ class OldChangesList extends ChangesList { $this->insertUserRelatedLinks($s,$rc); $this->insertComment($s, $rc); - if($rc->numberofWatchingusers > 0) { - $s .= ' ' . wfMsg('number_of_watching_users_RCview', $wgContLang->formatNum($rc->numberofWatchingusers)); - } + $s .= rtrim(' ' . $this->numberofWatchingusers($rc->numberofWatchingusers)); $s .= "</li>\n"; @@ -301,6 +311,7 @@ class EnhancedChangesList extends ChangesList { $rc = RCCacheEntry::newFromParent( $baseRC ); # Extract fields from DB into the function scope (rc_xxxx variables) + // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( $rc->mAttribs ); $curIdEq = 'curid=' . $rc_cur_id; @@ -405,7 +416,7 @@ class EnhancedChangesList extends ChangesList { * Enhanced RC group */ function recentChangesBlockGroup( $block ) { - global $wgContLang, $wgRCShowChangedSize; + global $wgLang, $wgContLang, $wgRCShowChangedSize; $r = ''; # Collate list of users @@ -467,22 +478,32 @@ class EnhancedChangesList extends ChangesList { $currentRevision = $block[0]->mAttribs['rc_this_oldid']; if( $block[0]->mAttribs['rc_type'] != RC_LOG ) { # Changes - $r .= ' ('.count($block).' '; + + $n = count($block); + static $nchanges = array(); + if ( !isset( $nchanges[$n] ) ) { + $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape'), + $wgLang->formatNum( $n ) ); + } + + $r .= ' ('; if( $isnew ) { - $r .= $this->message['changes']; + $r .= $nchanges[$n]; } else { $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(), - $this->message['changes'], $curIdEq."&diff=$currentRevision&oldid=$oldid" ); + $nchanges[$n], $curIdEq."&diff=$currentRevision&oldid=$oldid" ); } + $r .= ') . . '; + # Character difference $chardiff = $rcObj->getCharacterDifference( $block[ count( $block ) - 1 ]->mAttribs['rc_old_len'], $block[0]->mAttribs['rc_new_len'] ); if( $chardiff == '' ) { - $r .= '; '; + $r .= ' ('; } else { - $r .= '; ' . $chardiff . ' '; + $r .= ' ' . $chardiff. ' . . ('; } @@ -494,16 +515,14 @@ class EnhancedChangesList extends ChangesList { $r .= $users; - if($block[0]->numberofWatchingusers > 0) { - global $wgContLang; - $r .= wfMsg('number_of_watching_users_RCview', $wgContLang->formatNum($block[0]->numberofWatchingusers)); - } + $r .= $this->numberofWatchingusers($block[0]->numberofWatchingusers); $r .= "<br />\n"; # Sub-entries $r .= '<div id="'.$rci.'" style="display:none">'; foreach( $block as $rcObj ) { # Get rc_xxxx variables + // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( $rcObj->mAttribs ); $r .= $this->spacerArrow(); @@ -607,6 +626,7 @@ class EnhancedChangesList extends ChangesList { global $wgContLang, $wgRCShowChangedSize; # Get rc_xxxx variables + // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( $rcObj->mAttribs ); $curIdEq = 'curid='.$rc_cur_id; @@ -647,9 +667,7 @@ class EnhancedChangesList extends ChangesList { $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() ); } - if( $rcObj->numberofWatchingusers > 0 ) { - $r .= wfMsg('number_of_watching_users_RCview', $wgContLang->formatNum($rcObj->numberofWatchingusers)); - } + $r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers); $r .= "<br />\n"; return $r; diff --git a/includes/CoreParserFunctions.php b/includes/CoreParserFunctions.php index 402a3ba9..72ceb45f 100644 --- a/includes/CoreParserFunctions.php +++ b/includes/CoreParserFunctions.php @@ -2,8 +2,8 @@ /** * Various core parser functions, registered in Parser::firstCallInit() + * @addtogroup Parser */ - class CoreParserFunctions { static function intFunction( $parser, $part1 = '' /*, ... */ ) { if ( strval( $part1 ) !== '' ) { @@ -87,7 +87,7 @@ class CoreParserFunctions { static function formatNum( $parser, $num = '' ) { return $parser->getFunctionLang()->formatNum( $num ); } - + static function grammar( $parser, $case = '', $word = '' ) { return $parser->getFunctionLang()->convertGrammar( $word, $case ); } @@ -135,6 +135,7 @@ class CoreParserFunctions { static function numberofarticles( $parser, $raw = null ) { return self::statisticsFunction( 'articles', $raw ); } static function numberoffiles( $parser, $raw = null ) { return self::statisticsFunction( 'images', $raw ); } static function numberofadmins( $parser, $raw = null ) { return self::statisticsFunction( 'admins', $raw ); } + static function numberofedits( $parser, $raw = null ) { return self::statisticsFunction( 'edits', $raw ); } static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) { $count = SiteStats::pagesInNs( intval( $namespace ) ); @@ -151,7 +152,7 @@ class CoreParserFunctions { $lang = $wgContLang->getLanguageName( strtolower( $arg ) ); return $lang != '' ? $lang : $arg; } - + static function pad( $string = '', $length = 0, $char = 0, $direction = STR_PAD_RIGHT ) { $length = min( max( $length, 0 ), 500 ); $char = substr( $char, 0, 1 ); @@ -159,17 +160,21 @@ class CoreParserFunctions { ? str_pad( $string, $length, (string)$char, $direction ) : $string; } - + static function padleft( $parser, $string = '', $length = 0, $char = 0 ) { return self::pad( $string, $length, $char, STR_PAD_LEFT ); } - + static function padright( $parser, $string = '', $length = 0, $char = 0 ) { return self::pad( $string, $length, $char ); } - + static function anchorencode( $parser, $text ) { - return strtr( urlencode( $text ) , array( '%' => '.' , '+' => '_' ) ); + $a = urlencode( $text ); + $a = strtr( $a, array( '%' => '.', '+' => '_' ) ); + # leave colons alone, however + $a = str_replace( '.3A', ':', $a ); + return $a; } static function special( $parser, $text ) { @@ -180,14 +185,12 @@ class CoreParserFunctions { return wfMsgForContent( 'nosuchspecialpage' ); } } - + public static function defaultsort( $parser, $text ) { $text = trim( $text ); if( strlen( $text ) > 0 ) $parser->setDefaultSort( $text ); return ''; } - } - ?> diff --git a/includes/Credits.php b/includes/Credits.php index 62f0b256..87382a86 100644 --- a/includes/Credits.php +++ b/includes/Credits.php @@ -18,7 +18,6 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * * @author <evan@wikitravel.org> - * @package MediaWiki */ /** diff --git a/includes/Database.php b/includes/Database.php index eb1ee135..3fd6ad16 100644 --- a/includes/Database.php +++ b/includes/Database.php @@ -2,7 +2,6 @@ /** * This file deals with MySQL interface functions * and query specifics/optimisations - * @package MediaWiki */ /** Number of times to re-try an operation in case of deadlock */ @@ -16,6 +15,10 @@ define( 'DEADLOCK_DELAY_MAX', 1500000 ); * Utility classes *****************************************************************************/ +/** + * Utility class. + * @addtogroup Database + */ class DBObject { public $mData; @@ -32,12 +35,66 @@ class DBObject { } }; +/** + * Utility class. + * @addtogroup Database + */ +class MySQLField { + private $name, $tablename, $default, $max_length, $nullable, + $is_pk, $is_unique, $is_key, $type; + function __construct ($info) { + $this->name = $info->name; + $this->tablename = $info->table; + $this->default = $info->def; + $this->max_length = $info->max_length; + $this->nullable = !$info->not_null; + $this->is_pk = $info->primary_key; + $this->is_unique = $info->unique_key; + $this->is_multiple = $info->multiple_key; + $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple); + $this->type = $info->type; + } + + function name() { + return $this->name; + } + + function tableName() { + return $this->tableName; + } + + function defaultValue() { + return $this->default; + } + + function maxLength() { + return $this->max_length; + } + + function nullable() { + return $this->nullable; + } + + function isKey() { + return $this->is_key; + } + + function isMultipleKey() { + return $this->is_multiple; + } + + function type() { + return $this->type; + } +} + /****************************************************************************** * Error classes *****************************************************************************/ /** * Database error base class + * @addtogroup Database */ class DBError extends MWException { public $db; @@ -53,6 +110,9 @@ class DBError extends MWException { } } +/** + * @addtogroup Database + */ class DBConnectionError extends DBError { public $error; @@ -154,6 +214,7 @@ border=\"0\" ALT=\"Google\"></A> $cache = new HTMLFileCache( $t ); if( $cache->isFileCached() ) { + // FIXME: $msg is not defined on the next line. $msg = '<p style="color: red"><b>'.$msg."<br />\n" . $cachederror . "</b></p>\n"; @@ -169,6 +230,9 @@ border=\"0\" ALT=\"Google\"></A> } } +/** + * @addtogroup Database + */ class DBQueryError extends DBError { public $error, $errno, $sql, $fname; @@ -222,13 +286,16 @@ class DBQueryError extends DBError { } } +/** + * @addtogroup Database + */ class DBUnexpectedError extends DBError {} /******************************************************************************/ /** * Database abstraction object - * @package MediaWiki + * @addtogroup Database */ class Database { @@ -247,9 +314,6 @@ class Database { protected $mTrxLevel = 0; protected $mErrorCount = 0; protected $mLBInfo = array(); - protected $mCascadingDeletes = false; - protected $mCleanupTriggers = false; - protected $mStrictIPs = false; #------------------------------------------------------------------------------ # Accessors @@ -344,14 +408,14 @@ class Database { * Returns true if this database supports (and uses) cascading deletes */ function cascadingDeletes() { - return $this->mCascadingDeletes; + return false; } /** * Returns true if this database supports (and uses) triggers (e.g. on the page table) */ function cleanupTriggers() { - return $this->mCleanupTriggers; + return false; } /** @@ -359,7 +423,7 @@ class Database { * Specifically, it uses a NULL value instead of an empty string. */ function strictIPs() { - return $this->mStrictIPs; + return false; } /** @@ -376,6 +440,14 @@ class Database { return true; } + /** + * Returns true if this database can do a native search on IP columns + * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32'; + */ + function searchableIPs() { + return false; + } + /**#@+ * Get function */ @@ -407,13 +479,11 @@ class Database { #------------------------------------------------------------------------------ /**@{{ + * Constructor. * @param string $server database server host * @param string $user database user name * @param string $password database user password * @param string $dbname database name - */ - - /** * @param failFunction * @param $flags * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php @@ -463,8 +533,7 @@ class Database { * @param failFunction * @param $flags */ - static function newFromParams( $server, $user, $password, $dbName, - $failFunction = false, $flags = 0 ) + static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 ) { return new Database( $server, $user, $password, $dbName, $failFunction, $flags ); } @@ -514,7 +583,7 @@ class Database { } if ($this->mConn === false) { $iplus = $i + 1; - wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); + #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); } } @@ -541,12 +610,19 @@ class Database { } if ( $success ) { - global $wgDBmysql5; - if( $wgDBmysql5 ) { + $version = $this->getServerVersion(); + if ( version_compare( $version, '4.1' ) >= 0 ) { // Tell the server we're communicating with it in UTF-8. // This may engage various charset conversions. - $this->query( 'SET NAMES utf8' ); + global $wgDBmysql5; + if( $wgDBmysql5 ) { + $this->query( 'SET NAMES utf8', __METHOD__ ); + } + // Turn off strict mode + $this->query( "SET sql_mode = ''", __METHOD__ ); } + + // Turn off strict mode if it is on } else { $this->reportConnectionError(); } @@ -599,10 +675,15 @@ class Database { } /** - * Usually aborts on failure - * If errors are explicitly ignored, returns success + * Usually aborts on failure. If errors are explicitly ignored, returns success. + * + * @param $sql String: SQL query + * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST comment (you can use __METHOD__ or add some extra info) + * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors... maybe best to catch the exception instead? + * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure if $tempIgnore set + * @throws DBQueryError Thrown when the database returns an error of any kind */ - function query( $sql, $fname = '', $tempIgnore = false ) { + public function query( $sql, $fname = '', $tempIgnore = false ) { global $wgProfiling; if ( $wgProfiling ) { @@ -626,11 +707,21 @@ class Database { $this->mLastQuery = $sql; # Add a comment for easy SHOW PROCESSLIST interpretation - if ( $fname ) { - $commentedSql = preg_replace('/\s/', " /* $fname */ ", $sql, 1); - } else { - $commentedSql = $sql; - } + #if ( $fname ) { + global $wgUser; + if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) { + $userName = $wgUser->getName(); + if ( strlen( $userName ) > 15 ) { + $userName = substr( $userName, 0, 15 ) . '...'; + } + $userName = str_replace( '/', '', $userName ); + } else { + $userName = ''; + } + $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1); + #} else { + # $commentedSql = $sql; + #} # If DBO_TRX is set, start a transaction if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() && @@ -655,6 +746,11 @@ class Database { wfDebug( "Connection lost, reconnecting...\n" ); if ( $this->ping() ) { wfDebug( "Reconnected\n" ); + $sqlx = substr( $commentedSql, 0, 500 ); + $sqlx = strtr( $sqlx, "\t\n", ' ' ); + global $wgRequestTime; + $elapsed = round( microtime(true) - $wgRequestTime, 3 ); + wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" ); $ret = $this->doQuery( $commentedSql ); } else { wfDebug( "Failed\n" ); @@ -674,9 +770,11 @@ class Database { /** * The DBMS-dependent part of query() - * @param string $sql SQL query. + * @param $sql String: SQL query. + * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure + * @access private */ - function doQuery( $sql ) { + /*private*/ function doQuery( $sql ) { if( $this->bufferResults() ) { $ret = mysql_query( $sql, $this->mConn ); } else { @@ -817,7 +915,13 @@ class Database { } /** - * Fetch the next row from the given result object, in object form + * Fetch the next row from the given result object, in object form. + * Fields can be retrieved with $row->fieldname, with fields acting like + * member variables. + * + * @param $res SQL result object as returned from Database::query(), etc. + * @return MySQL row object + * @throws DBUnexpectedError Thrown if the database returns an error */ function fetchObject( $res ) { @/**/$row = mysql_fetch_object( $res ); @@ -828,8 +932,12 @@ class Database { } /** - * Fetch the next row from the given result object - * Returns an array + * Fetch the next row from the given result object, in associative array + * form. Fields are retrieved with $row['fieldname']. + * + * @param $res SQL result object as returned from Database::query(), etc. + * @return MySQL row object + * @throws DBUnexpectedError Thrown if the database returns an error */ function fetchRow( $res ) { @/**/$row = mysql_fetch_array( $res ); @@ -972,7 +1080,7 @@ class Database { * @return array */ function makeSelectOptions( $options ) { - $tailOpts = ''; + $preLimitTail = $postLimitTail = ''; $startOpts = ''; $noKeyOptions = array(); @@ -982,16 +1090,17 @@ class Database { } } - if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}"; - if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}"; + if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; + if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; - if (isset($options['LIMIT'])) { - $tailOpts .= $this->limitResult('', $options['LIMIT'], - isset($options['OFFSET']) ? $options['OFFSET'] : false); - } - - if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE'; - if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE'; + //if (isset($options['LIMIT'])) { + // $tailOpts .= $this->limitResult('', $options['LIMIT'], + // isset($options['OFFSET']) ? $options['OFFSET'] + // : false); + //} + + if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE'; + if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE'; if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT'; # Various MySQL extensions @@ -1010,7 +1119,7 @@ class Database { $useIndex = ''; } - return array( $startOpts, $useIndex, $tailOpts ); + return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail ); } /** @@ -1038,20 +1147,33 @@ class Database { else $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) ); } elseif ($table!='') { - $from = ' FROM ' . $this->tableName( $table ); + if ($table{0}==' ') { + $from = ' FROM ' . $table; + } else { + $from = ' FROM ' . $this->tableName( $table ); + } } else { $from = ''; } - list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $options ); + list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options ); if( !empty( $conds ) ) { if ( is_array( $conds ) ) { $conds = $this->makeList( $conds, LIST_AND ); } - $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $tailOpts"; + $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail"; } else { - $sql = "SELECT $startOpts $vars $from $useIndex $tailOpts"; + $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail"; + } + + if (isset($options['LIMIT'])) + $sql = $this->limitResult($sql, $options['LIMIT'], + isset($options['OFFSET']) ? $options['OFFSET'] : false); + $sql = "$sql $postLimitTail"; + + if (isset($options['EXPLAIN'])) { + $sql = 'EXPLAIN ' . $sql; } return $this->query( $sql, $fname ); @@ -1085,6 +1207,33 @@ class Database { return $obj; } + + /** + * Estimate rows in dataset + * Returns estimated count, based on EXPLAIN output + * Takes same arguments as Database::select() + */ + + function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) { + $options['EXPLAIN']=true; + $res = $this->select ($table, $vars, $conds, $fname, $options ); + if ( $res === false ) + return false; + if (!$this->numRows($res)) { + $this->freeResult($res); + return 0; + } + + $rows=1; + + while( $plan = $this->fetchObject( $res ) ) { + $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero + } + + $this->freeResult($res); + return $rows; + } + /** * Removes most variables from an SQL query and replaces them with X or N for numbers. @@ -1207,7 +1356,7 @@ class Database { for( $i = 0; $i < $n; $i++ ) { $meta = mysql_fetch_field( $res, $i ); if( $field == $meta->name ) { - return $meta; + return new MySQLField($meta); } } return false; @@ -1417,7 +1566,7 @@ class Database { } /** - * @desc: Fetch a number of table names into an zero-indexed numerical array + * Fetch a number of table names into an zero-indexed numerical array * This is handy when you need to construct SQL for joins * * Example: @@ -1952,20 +2101,50 @@ class Database { } /** + * Override database's default connection timeout. + * May be useful for very long batch queries such as + * full-wiki dumps, where a single query reads out + * over hours or days. + * @param int $timeout in seconds + */ + public function setTimeout( $timeout ) { + $this->query( "SET net_read_timeout=$timeout" ); + $this->query( "SET net_write_timeout=$timeout" ); + } + + /** * Read and execute SQL commands from a file. * Returns true on success, error string on failure + * @param string $filename File name to open + * @param callback $lineCallback Optional function called before reading each line + * @param callback $resultCallback Optional function called for each MySQL result */ - function sourceFile( $filename ) { + function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) { $fp = fopen( $filename, 'r' ); if ( false === $fp ) { return "Could not open \"{$filename}\".\n"; } + $error = $this->sourceStream( $fp, $lineCallback, $resultCallback ); + fclose( $fp ); + return $error; + } + /** + * Read and execute commands from an open file handle + * Returns true on success, error string on failure + * @param string $fp File handle + * @param callback $lineCallback Optional function called before reading each line + * @param callback $resultCallback Optional function called for each MySQL result + */ + function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) { $cmd = ""; $done = false; $dollarquote = false; while ( ! feof( $fp ) ) { + if ( $lineCallback ) { + call_user_func( $lineCallback ); + } $line = trim( fgets( $fp, 1024 ) ); $sl = strlen( $line ) - 1; @@ -1995,7 +2174,10 @@ class Database { if ( $done ) { $cmd = str_replace(';;', ";", $cmd); $cmd = $this->replaceVars( $cmd ); - $res = $this->query( $cmd, 'dbsource', true ); + $res = $this->query( $cmd, __METHOD__, true ); + if ( $resultCallback ) { + call_user_func( $resultCallback, $this->resultObject( $res ) ); + } if ( false === $res ) { $err = $this->lastError(); @@ -2006,10 +2188,10 @@ class Database { $done = false; } } - fclose( $fp ); return true; } + /** * Replace variables in sourced SQL */ @@ -2017,7 +2199,7 @@ class Database { $varnames = array( 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser', 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword', - 'wgDBadminuser', 'wgDBadminpassword', + 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions', ); // Ordinary variables @@ -2050,7 +2232,7 @@ class Database { * Database abstraction object for mySQL * Inherit all methods and properties of Database::Database() * - * @package MediaWiki + * @addtogroup Database * @see Database */ class DatabaseMysql extends Database { @@ -2060,8 +2242,7 @@ class DatabaseMysql extends Database { /** * Result wrapper for grabbing data queried by someone else - * - * @package MediaWiki + * @addtogroup Database */ class ResultWrapper { var $db, $result; @@ -2107,6 +2288,12 @@ class ResultWrapper { function seek( $row ) { $this->db->dataSeek( $this->result, $row ); } + + function rewind() { + if ($this->numRows()) { + $this->db->dataSeek($this->result, 0); + } + } } diff --git a/includes/DatabaseFunctions.php b/includes/DatabaseFunctions.php index ca83b9e5..4b31b4f0 100644 --- a/includes/DatabaseFunctions.php +++ b/includes/DatabaseFunctions.php @@ -3,7 +3,6 @@ * Legacy database functions, for compatibility with pre-1.3 code * NOTE: this file is no longer loaded by default. * - * @package MediaWiki */ /** @@ -18,7 +17,7 @@ function wfQuery( $sql, $db, $fname = '' ) { # Someone has tried to call this the old way throw new FatalError( wfMsgNoDB( 'wrong_wfQuery_params', $db, $sql ) ); } - $c =& wfGetDB( $db ); + $c = wfGetDB( $db ); if ( $c !== false ) { return $c->query( $sql, $fname ); } else { @@ -34,7 +33,7 @@ function wfQuery( $sql, $db, $fname = '' ) { * @return Array: first row from the database */ function wfSingleQuery( $sql, $dbi, $fname = '' ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); $res = $db->query($sql, $fname ); $row = $db->fetchRow( $res ); $ret = $row[0]; @@ -54,7 +53,7 @@ function wfSingleQuery( $sql, $dbi, $fname = '' ) { * @return Returns the previous state. */ function wfIgnoreSQLErrors( $newstate, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->ignoreErrors( $newstate ); } else { @@ -73,7 +72,7 @@ function wfIgnoreSQLErrors( $newstate, $dbi = DB_LAST ) { */ function wfFreeResult( $res, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { $db->freeResult( $res ); return true; @@ -87,7 +86,7 @@ function wfFreeResult( $res, $dbi = DB_LAST ) * @return object|false object we requested */ function wfFetchObject( $res, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->fetchObject( $res, $dbi = DB_LAST ); } else { @@ -100,7 +99,7 @@ function wfFetchObject( $res, $dbi = DB_LAST ) { * @return object|false row we requested */ function wfFetchRow( $res, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->fetchRow ( $res, $dbi = DB_LAST ); } else { @@ -113,7 +112,7 @@ function wfFetchRow( $res, $dbi = DB_LAST ) { * @return integer|false number of rows */ function wfNumRows( $res, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->numRows( $res, $dbi = DB_LAST ); } else { @@ -126,7 +125,7 @@ function wfNumRows( $res, $dbi = DB_LAST ) { * @return integer|false number of fields */ function wfNumFields( $res, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->numFields( $res ); } else { @@ -143,7 +142,7 @@ function wfNumFields( $res, $dbi = DB_LAST ) { */ function wfFieldName( $res, $n, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->fieldName( $res, $n, $dbi = DB_LAST ); } else { @@ -156,7 +155,7 @@ function wfFieldName( $res, $n, $dbi = DB_LAST ) * @todo document function */ function wfInsertId( $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->insertId(); } else { @@ -168,7 +167,7 @@ function wfInsertId( $dbi = DB_LAST ) { * @todo document function */ function wfDataSeek( $res, $row, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->dataSeek( $res, $row ); } else { @@ -180,7 +179,7 @@ function wfDataSeek( $res, $row, $dbi = DB_LAST ) { * @todo document function */ function wfLastErrno( $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->lastErrno(); } else { @@ -192,7 +191,7 @@ function wfLastErrno( $dbi = DB_LAST ) { * @todo document function */ function wfLastError( $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->lastError(); } else { @@ -204,7 +203,7 @@ function wfLastError( $dbi = DB_LAST ) { * @todo document function */ function wfAffectedRows( $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->affectedRows(); } else { @@ -216,7 +215,7 @@ function wfAffectedRows( $dbi = DB_LAST ) { * @todo document function */ function wfLastDBquery( $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->lastQuery(); } else { @@ -235,7 +234,7 @@ function wfLastDBquery( $dbi = DB_LAST ) { */ function wfSetSQL( $table, $var, $value, $cond, $dbi = DB_MASTER ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->set( $table, $var, $value, $cond ); } else { @@ -254,7 +253,7 @@ function wfSetSQL( $table, $var, $value, $cond, $dbi = DB_MASTER ) */ function wfGetSQL( $table, $var, $cond='', $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->selectField( $table, $var, $cond ); } else { @@ -271,7 +270,7 @@ function wfGetSQL( $table, $var, $cond='', $dbi = DB_LAST ) * @return Result of Database::fieldExists() or false. */ function wfFieldExists( $table, $field, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->fieldExists( $table, $field ); } else { @@ -288,7 +287,7 @@ function wfFieldExists( $table, $field, $dbi = DB_LAST ) { * @return Result of Database::indexExists() or false. */ function wfIndexExists( $table, $index, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->indexExists( $table, $index ); } else { @@ -306,7 +305,7 @@ function wfIndexExists( $table, $index, $dbi = DB_LAST ) { * @return result of Database::insert() or false. */ function wfInsertArray( $table, $array, $fname = 'wfInsertArray', $dbi = DB_MASTER ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->insert( $table, $array, $fname ); } else { @@ -325,7 +324,7 @@ function wfInsertArray( $table, $array, $fname = 'wfInsertArray', $dbi = DB_MAST * @return result of Database::getArray() or false. */ function wfGetArray( $table, $vars, $conds, $fname = 'wfGetArray', $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->getArray( $table, $vars, $conds, $fname ); } else { @@ -344,7 +343,7 @@ function wfGetArray( $table, $vars, $conds, $fname = 'wfGetArray', $dbi = DB_LAS * @todo document function */ function wfUpdateArray( $table, $values, $conds, $fname = 'wfUpdateArray', $dbi = DB_MASTER ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { $db->update( $table, $values, $conds, $fname ); return true; @@ -357,7 +356,7 @@ function wfUpdateArray( $table, $values, $conds, $fname = 'wfUpdateArray', $dbi * @todo document function */ function wfTableName( $name, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->tableName( $name ); } else { @@ -369,7 +368,7 @@ function wfTableName( $name, $dbi = DB_LAST ) { * @todo document function */ function wfStrencode( $s, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->strencode( $s ); } else { @@ -381,7 +380,7 @@ function wfStrencode( $s, $dbi = DB_LAST ) { * @todo document function */ function wfNextSequenceValue( $seqName, $dbi = DB_MASTER ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->nextSequenceValue( $seqName ); } else { @@ -393,7 +392,7 @@ function wfNextSequenceValue( $seqName, $dbi = DB_MASTER ) { * @todo document function */ function wfUseIndexClause( $index, $dbi = DB_SLAVE ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->useIndexClause( $index ); } else { diff --git a/includes/DatabaseOracle.php b/includes/DatabaseOracle.php index 1a6f62f2..2b720df7 100644 --- a/includes/DatabaseOracle.php +++ b/includes/DatabaseOracle.php @@ -1,44 +1,141 @@ <?php /** - * Oracle. - * - * @package MediaWiki + * This is the Oracle database abstraction layer. + * @addtogroup Database */ +class ORABlob { + var $mData; -class OracleBlob extends DBObject { - function isLOB() { - return true; + function __construct($data) { + $this->mData = $data; } - function data() { + + function getData() { return $this->mData; } -}; +} + +/** + * The oci8 extension is fairly weak and doesn't support oci_num_rows, among + * other things. We use a wrapper class to handle that and other + * Oracle-specific bits, like converting column names back to lowercase. + * @addtogroup Database + */ +class ORAResult { + private $rows; + private $cursor; + private $stmt; + private $nrows; + private $db; + + function __construct(&$db, $stmt) { + $this->db =& $db; + if (($this->nrows = oci_fetch_all($stmt, $this->rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM)) === false) { + $e = oci_error($stmt); + $db->reportQueryError($e['message'], $e['code'], '', __FUNCTION__); + return; + } + + $this->cursor = 0; + $this->stmt = $stmt; + } + + function free() { + oci_free_statement($this->stmt); + } + + function seek($row) { + $this->cursor = min($row, $this->nrows); + } + + function numRows() { + return $this->nrows; + } + + function numFields() { + return oci_num_fields($this->stmt); + } + + function fetchObject() { + if ($this->cursor >= $this->nrows) + return false; + + $row = $this->rows[$this->cursor++]; + $ret = new stdClass(); + foreach ($row as $k => $v) { + $lc = strtolower(oci_field_name($this->stmt, $k + 1)); + $ret->$lc = $v; + } + + return $ret; + } + + function fetchAssoc() { + if ($this->cursor >= $this->nrows) + return false; + + $row = $this->rows[$this->cursor++]; + $ret = array(); + foreach ($row as $k => $v) { + $lc = strtolower(oci_field_name($this->stmt, $k + 1)); + $ret[$lc] = $v; + $ret[$k] = $v; + } + return $ret; + } +} /** - * - * @package MediaWiki + * @addtogroup Database */ class DatabaseOracle extends Database { var $mInsertId = NULL; var $mLastResult = NULL; - var $mFetchCache = array(); - var $mFetchID = array(); - var $mNcols = array(); - var $mFieldNames = array(), $mFieldTypes = array(); - var $mAffectedRows = array(); - var $mErr; + var $numeric_version = NULL; + var $lastResult = null; + var $cursor = 0; + var $mAffectedRows; function DatabaseOracle($server = false, $user = false, $password = false, $dbName = false, - $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) + $failFunction = false, $flags = 0 ) { - Database::Database( $server, $user, $password, $dbName, $failFunction, $flags, $tablePrefix ); + + global $wgOut; + # Can't get a reference if it hasn't been set yet + if ( !isset( $wgOut ) ) { + $wgOut = NULL; + } + $this->mOut =& $wgOut; + $this->mFailFunction = $failFunction; + $this->mFlags = $flags; + $this->open( $server, $user, $password, $dbName); + + } + + function cascadingDeletes() { + return true; + } + function cleanupTriggers() { + return true; + } + function strictIPs() { + return true; + } + function realTimestamps() { + return true; + } + function implicitGroupby() { + return false; + } + function searchableIPs() { + return true; } - /* static */ function newFromParams( $server = false, $user = false, $password = false, $dbName = false, - $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) + static function newFromParams( $server = false, $user = false, $password = false, $dbName = false, + $failFunction = false, $flags = 0) { - return new DatabaseOracle( $server, $user, $password, $dbName, $failFunction, $flags, $tablePrefix ); + return new DatabaseOracle( $server, $user, $password, $dbName, $failFunction, $flags ); } /** @@ -47,23 +144,33 @@ class DatabaseOracle extends Database { */ function open( $server, $user, $password, $dbName ) { if ( !function_exists( 'oci_connect' ) ) { - throw new DBConnectionError( $this, "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n" ); + throw new DBConnectionError( $this, "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" ); } + + # Needed for proper UTF-8 functionality + putenv("NLS_LANG=AMERICAN_AMERICA.AL32UTF8"); + $this->close(); $this->mServer = $server; $this->mUser = $user; $this->mPassword = $password; $this->mDBname = $dbName; - $this->mConn = oci_new_connect($user, $password, $dbName, "AL32UTF8"); - if ( $this->mConn === false ) { - wfDebug( "DB connection error\n" ); - wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " - . substr( $password, 0, 3 ) . "...\n" ); - wfDebug( $this->lastError()."\n" ); - } else { - $this->mOpened = true; + if (!strlen($user)) { ## e.g. the class is being loaded + return; + } + + error_reporting( E_ALL ); + $this->mConn = oci_connect($user, $password, $dbName); + + if ($this->mConn == false) { + wfDebug("DB connection error\n"); + wfDebug("Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n"); + wfDebug($this->lastError()."\n"); + return false; } + + $this->mOpened = true; return $this->mConn; } @@ -73,116 +180,67 @@ class DatabaseOracle extends Database { */ function close() { $this->mOpened = false; - if ($this->mConn) { - return oci_close($this->mConn); + if ( $this->mConn ) { + return oci_close( $this->mConn ); } else { return true; } } - function parseStatement($sql) { - $this->mErr = $this->mLastResult = false; - if (($stmt = oci_parse($this->mConn, $sql)) === false) { - $this->lastError(); - return $this->mLastResult = false; - } - $this->mAffectedRows[$stmt] = 0; - return $this->mLastResult = $stmt; + function execFlags() { + return $this->mTrxLevel ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS; } function doQuery($sql) { - if (($stmt = $this->parseStatement($sql)) === false) - return false; - return $this->executeStatement($stmt); - } + wfDebug("SQL: [$sql]\n"); + if (!mb_check_encoding($sql)) { + throw new MWException("SQL encoding is invalid"); + } - function executeStatement($stmt) { - if (!oci_execute($stmt, OCI_DEFAULT)) { - $this->lastError(); - oci_free_statement($stmt); - return false; + if (($this->mLastResult = $stmt = oci_parse($this->mConn, $sql)) === false) { + $e = oci_error($this->mConn); + $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__); } - $this->mAffectedRows[$stmt] = oci_num_rows($stmt); - $this->mFetchCache[$stmt] = array(); - $this->mFetchID[$stmt] = 0; - $this->mNcols[$stmt] = oci_num_fields($stmt); - if ($this->mNcols[$stmt] == 0) - return $this->mLastResult; - for ($i = 1; $i <= $this->mNcols[$stmt]; $i++) { - $this->mFieldNames[$stmt][$i] = oci_field_name($stmt, $i); - $this->mFieldTypes[$stmt][$i] = oci_field_type($stmt, $i); + + if (oci_execute($stmt, $this->execFlags()) == false) { + $e = oci_error($stmt); + $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__); } - while (($o = oci_fetch_array($stmt)) !== false) { - foreach ($o as $key => $value) { - if (is_object($value)) { - $o[$key] = $value->load(); - } - } - $this->mFetchCache[$stmt][] = $o; + if (oci_statement_type($stmt) == "SELECT") + return new ORAResult($this, $stmt); + else { + $this->mAffectedRows = oci_num_rows($stmt); + return true; } - return $this->mLastResult; } - function queryIgnore( $sql, $fname = '' ) { - return $this->query( $sql, $fname, true ); + function queryIgnore($sql, $fname = '') { + return $this->query($sql, $fname, true); } - function freeResult( $res ) { - if (!oci_free_statement($res)) { - throw new DBUnexpectedError( $this, "Unable to free Oracle result\n" ); - } - unset($this->mFetchID[$res]); - unset($this->mFetchCache[$res]); - unset($this->mNcols[$res]); - unset($this->mFieldNames[$res]); - unset($this->mFieldTypes[$res]); + function freeResult($res) { + $res->free(); } - function fetchAssoc($res) { - if ($this->mFetchID[$res] >= count($this->mFetchCache[$res])) - return false; - - for ($i = 1; $i <= $this->mNcols[$res]; $i++) { - $name = $this->mFieldNames[$res][$i]; - if (isset($this->mFetchCache[$res][$this->mFetchID[$res]][$name])) - $value = $this->mFetchCache[$res][$this->mFetchID[$res]][$name]; - else $value = NULL; - $key = strtolower($name); - wfdebug("'$key' => '$value'\n"); - $ret[$key] = $value; - } - $this->mFetchID[$res]++; - return $ret; + function fetchObject($res) { + return $res->fetchObject(); } function fetchRow($res) { - $r = $this->fetchAssoc($res); - if (!$r) - return false; - $i = 0; - $ret = array(); - foreach ($r as $value) { - wfdebug("ret[$i]=[$value]\n"); - $ret[$i++] = $value; - } - return $ret; + return $res->fetchAssoc(); } - function fetchObject($res) { - $row = $this->fetchAssoc($res); - if (!$row) - return false; - $ret = new stdClass; - foreach ($row as $key => $value) - $ret->$key = $value; - return $ret; + function numRows($res) { + return $res->numRows(); } - function numRows($res) { - return count($this->mFetchCache[$res]); + function numFields($res) { + return $res->numFields(); + } + + function fieldName($stmt, $n) { + return pg_field_name($stmt, $n); } - function numFields( $res ) { return pg_num_fields( $res ); } - function fieldName( $res, $n ) { return pg_field_name( $res, $n ); } /** * This must be called after nextSequenceVal @@ -192,139 +250,153 @@ class DatabaseOracle extends Database { } function dataSeek($res, $row) { - $this->mFetchID[$res] = $row; + $res->seek($row); } function lastError() { - if ($this->mErr === false) { - if ($this->mLastResult !== false) { - $what = $this->mLastResult; - } else if ($this->mConn !== false) { - $what = $this->mConn; - } else { - $what = false; - } - $err = ($what !== false) ? oci_error($what) : oci_error(); - if ($err === false) { - $this->mErr = 'no error'; - } else { - $this->mErr = $err['message']; - } - } - return str_replace("\n", '<br />', $this->mErr); + if ($this->mConn === false) + $e = oci_error(); + else + $e = oci_error($this->mConn); + return $e['message']; } + function lastErrno() { - return 0; + if ($this->mConn === false) + $e = oci_error(); + else + $e = oci_error($this->mConn); + return $e['code']; } function affectedRows() { - return $this->mAffectedRows[$this->mLastResult]; + return $this->mAffectedRows; } /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure */ - function indexInfo ($table, $index, $fname = 'Database::indexInfo' ) { - $table = $this->tableName($table, true); - if ($index == 'PRIMARY') - $index = "${table}_pk"; - $sql = "SELECT uniqueness FROM all_indexes WHERE table_name='" . - $table . "' AND index_name='" . - $this->strencode(strtoupper($index)) . "'"; - $res = $this->query($sql, $fname); - if (!$res) - return NULL; - if (($row = $this->fetchObject($res)) == NULL) - return false; - $this->freeResult($res); - $row->Non_unique = !$row->uniqueness; - return $row; - - // BUG: !!!! This code needs to be synced up with database.php - + function indexInfo( $table, $index, $fname = 'Database::indexExists' ) { + return false; } - function indexUnique ($table, $index, $fname = 'indexUnique') { - if (!($i = $this->indexInfo($table, $index, $fname))) - return $i; - return $i->uniqueness == 'UNIQUE'; + function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) { + return false; } - function fieldInfo( $table, $field ) { - $o = new stdClass; - $o->multiple_key = true; /* XXX */ - return $o; - } + function insert( $table, $a, $fname = 'Database::insert', $options = array() ) { + if (!is_array($options)) + $options = array($options); - function getColumnInformation($table, $field) { - $table = $this->tableName($table, true); - $field = strtoupper($field); + #if (in_array('IGNORE', $options)) + # $oldIgnore = $this->ignoreErrors(true); - $res = $this->doQuery("SELECT * FROM all_tab_columns " . - "WHERE table_name='".$table."' " . - "AND column_name='".$field."'"); - if (!$res) - return false; - $o = $this->fetchObject($res); - $this->freeResult($res); - return $o; - } + # IGNORE is performed using single-row inserts, ignoring errors in each + # FIXME: need some way to distiguish between key collision and other types of error + //$oldIgnore = $this->ignoreErrors(true); + if (!is_array(reset($a))) { + $a = array($a); + } + foreach ($a as $row) { + $this->insertOneRow($table, $row, $fname); + } + //$this->ignoreErrors($oldIgnore); + $retVal = true; - function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) { - $column = $this->getColumnInformation($table, $field); - if (!$column) - return false; - return true; + //if (in_array('IGNORE', $options)) + // $this->ignoreErrors($oldIgnore); + + return $retVal; } - function tableName($name, $forddl = false) { - # First run any transformations from the parent object - $name = parent::tableName( $name ); + function insertOneRow($table, $row, $fname) { + // "INSERT INTO tables (a, b, c)" + $sql = "INSERT INTO " . $this->tableName($table) . " (" . join(',', array_keys($row)) . ')'; + $sql .= " VALUES ("; + + // for each value, append ":key" + $first = true; + $returning = ''; + foreach ($row as $col => $val) { + if (is_object($val)) { + $what = "EMPTY_BLOB()"; + assert($returning === ''); + $returning = " RETURNING $col INTO :bval"; + $blobcol = $col; + } else + $what = ":$col"; + + if ($first) + $sql .= "$what"; + else + $sql.= ", $what"; + $first = false; + } + $sql .= ") $returning"; + + $stmt = oci_parse($this->mConn, $sql); + foreach ($row as $col => $val) { + if (!is_object($val)) { + if (oci_bind_by_name($stmt, ":$col", $row[$col]) === false) + $this->reportQueryError($this->lastErrno(), $this->lastError(), $sql, __METHOD__); + } + } + + if (($bval = oci_new_descriptor($this->mConn, OCI_D_LOB)) === false) { + $e = oci_error($stmt); + throw new DBUnexpectedError($this, "Cannot create LOB descriptor: " . $e['message']); + } + + if (strlen($returning)) + oci_bind_by_name($stmt, ":bval", $bval, -1, SQLT_BLOB); - # Replace backticks into empty - # Note: "foo" and foo are not the same in Oracle! - $name = str_replace('`', '', $name); + if (oci_execute($stmt, OCI_DEFAULT) === false) { + $e = oci_error($stmt); + $this->reportQueryError($e['message'], $e['code'], $sql, __METHOD__); + } + if (strlen($returning)) { + $bval->save($row[$blobcol]->getData()); + $bval->free(); + } + if (!$this->mTrxLevel) + oci_commit($this->mConn); - # Now quote Oracle reserved keywords + oci_free_statement($stmt); + } + + function tableName( $name ) { + # Replace reserved words with better ones switch( $name ) { case 'user': - case 'group': - case 'validate': - if ($forddl) - return $name; - else - return '"' . $name . '"'; - + return 'mwuser'; + case 'text': + return 'pagecontent'; default: - return strtoupper($name); + return $name; } } - function strencode( $s ) { - return str_replace("'", "''", $s); - } - /** * Return the next in a sequence, save the value for retrieval via insertId() */ - function nextSequenceValue( $seqName ) { - $r = $this->doQuery("SELECT $seqName.nextval AS val FROM dual"); - $o = $this->fetchObject($r); - $this->freeResult($r); - return $this->mInsertId = (int)$o->val; + function nextSequenceValue($seqName) { + $res = $this->query("SELECT $seqName.nextval FROM dual"); + $row = $this->fetchRow($res); + $this->mInsertId = $row[0]; + $this->freeResult($res); + return $this->mInsertId; } /** - * USE INDEX clause - * PostgreSQL doesn't have them and returns "" + * Oracle does not have a "USE INDEX" clause, so return an empty string */ - function useIndexClause( $index ) { + function useIndexClause($index) { return ''; } # REPLACE query wrapper - # PostgreSQL simulates this with a DELETE followed by INSERT + # Oracle simulates this with a DELETE followed by INSERT # $row is the row to insert, an associative array # $uniqueIndexes is an array of indexes. Each element may be either a # field name or an array of field names @@ -333,15 +405,15 @@ class DatabaseOracle extends Database { # However if you do this, you run the risk of encountering errors which wouldn't have # occurred in MySQL function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) { - $table = $this->tableName( $table ); + $table = $this->tableName($table); if (count($rows)==0) { return; } # Single row case - if ( !is_array( reset( $rows ) ) ) { - $rows = array( $rows ); + if (!is_array(reset($rows))) { + $rows = array($rows); } foreach( $rows as $row ) { @@ -377,14 +449,14 @@ class DatabaseOracle extends Database { # Now insert the row $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' . $this->makeList( $row, LIST_COMMA ) . ')'; - $this->query( $sql, $fname ); + $this->query($sql, $fname); } } # DELETE where the condition is a join function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) { if ( !$conds ) { - throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' ); + throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' ); } $delTable = $this->tableName( $delTable ); @@ -421,17 +493,14 @@ class DatabaseOracle extends Database { } function limitResult($sql, $limit, $offset) { - $ret = "SELECT * FROM ($sql) WHERE ROWNUM < " . ((int)$limit + (int)($offset+1)); - if (is_numeric($offset)) - $ret .= " AND ROWNUM >= " . (int)$offset; - return $ret; - } - function limitResultForUpdate($sql, $limit) { - return $sql; + if ($offset === false) + $offset = 0; + return "SELECT * FROM ($sql) WHERE rownum >= (1 + $offset) AND rownum < 1 + $limit + $offset"; } + /** * Returns an SQL expression for a simple conditional. - * Uses CASE on PostgreSQL. + * Uses CASE on Oracle * * @param string $cond SQL expression which will result in a boolean value * @param string $trueVal SQL expression to return if true @@ -442,15 +511,12 @@ class DatabaseOracle extends Database { return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; } - # FIXME: actually detecting deadlocks might be nice function wasDeadlock() { - return false; + return $this->lastErrno() == 'OCI-00060'; } - # Return DB-style timestamp used for MySQL schema function timestamp($ts = 0) { - return $this->strencode(wfTimestamp(TS_ORACLE, $ts)); -# return "TO_TIMESTAMP('" . $this->strencode(wfTimestamp(TS_DB, $ts)) . "', 'RRRR-MM-DD HH24:MI:SS')"; + return wfTimestamp(TS_ORACLE, $ts); } /** @@ -460,13 +526,25 @@ class DatabaseOracle extends Database { return $valuedata; } + function reportQueryError($error, $errno, $sql, $fname, $tempIgnore = false) { + # Ignore errors during error handling to avoid infinite + # recursion + $ignore = $this->ignoreErrors(true); + ++$this->mErrorCount; - function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { - $message = "A database error has occurred\n" . - "Query: $sql\n" . - "Function: $fname\n" . - "Error: $errno $error\n"; - throw new DBUnexpectedError($this, $message); + if ($ignore || $tempIgnore) { +echo "error ignored! query = [$sql]\n"; + wfDebug("SQL ERROR (ignored): $error\n"); + $this->ignoreErrors( $ignore ); + } + else { +echo "error!\n"; + $message = "A database error has occurred\n" . + "Query: $sql\n" . + "Function: $fname\n" . + "Error: $errno $error\n"; + throw new DBUnexpectedError($this, $message); + } } /** @@ -483,209 +561,125 @@ class DatabaseOracle extends Database { return oci_server_version($this->mConn); } - function setSchema($schema=false) { - $schemas=$this->mSchemas; - if ($schema) { array_unshift($schemas,$schema); } - $searchpath=$this->makeList($schemas,LIST_NAMES); - $this->query("SET search_path = $searchpath"); + /** + * Query whether a given table exists (in the given schema, or the default mw one if not given) + */ + function tableExists($table) { + $etable= $this->addQuotes($table); + $SQL = "SELECT 1 FROM user_tables WHERE table_name='$etable'"; + $res = $this->query($SQL); + $count = $res ? oci_num_rows($res) : 0; + if ($res) + $this->freeResult($res); + return $count; } - function begin() { + /** + * Query whether a given column exists in the mediawiki schema + */ + function fieldExists( $table, $field ) { + return true; // XXX } - function immediateCommit( $fname = 'Database::immediateCommit' ) { - oci_commit($this->mConn); - $this->mTrxLevel = 0; + function fieldInfo( $table, $field ) { + return false; // XXX } - function rollback( $fname = 'Database::rollback' ) { - oci_rollback($this->mConn); - $this->mTrxLevel = 0; + + function begin( $fname = '' ) { + $this->mTrxLevel = 1; } - function getLag() { - return false; + function immediateCommit( $fname = '' ) { + return true; } - function getStatus($which=null) { - $result = array('Threads_running' => 0, 'Threads_connected' => 0); - return $result; + function commit( $fname = '' ) { + oci_commit($this->mConn); + $this->mTrxLevel = 0; } - /** - * Returns an optional USE INDEX clause to go after the table, and a - * string to go at the end of the query - * - * @access private - * - * @param array $options an associative array of options to be turned into - * an SQL query, valid keys are listed in the function. - * @return array - */ - function makeSelectOptions($options) { - $tailOpts = ''; - - if (isset( $options['ORDER BY'])) { - $tailOpts .= " ORDER BY {$options['ORDER BY']}"; - } + /* Not even sure why this is used in the main codebase... */ + function limitResultForUpdate($sql, $num) { + return $sql; + } - return array('', $tailOpts); + function strencode($s) { + return str_replace("'", "''", $s); } - function maxListLen() { - return 1000; + function encodeBlob($b) { + return new ORABlob($b); + } + function decodeBlob($b) { + return $b; //return $b->load(); } - /** - * Query whether a given table exists - */ - function tableExists( $table ) { - $table = $this->tableName($table, true); - $res = $this->query( "SELECT COUNT(*) as NUM FROM user_tables WHERE table_name='" - . $table . "'" ); - if (!$res) - return false; - $row = $this->fetchObject($res); - $this->freeResult($res); - return $row->num >= 1; + function addQuotes( $s ) { + global $wgLang; + $s = $wgLang->checkTitleEncoding($s); + return "'" . $this->strencode($s) . "'"; } - /** - * UPDATE wrapper, takes a condition array and a SET array - */ - function update( $table, $values, $conds, $fname = 'Database::update' ) { - $table = $this->tableName( $table ); + function quote_ident( $s ) { + return $s; + } - $sql = "UPDATE $table SET "; - $first = true; - foreach ($values as $field => $v) { - if ($first) - $first = false; - else - $sql .= ", "; - $sql .= "$field = :n$field "; - } - if ( $conds != '*' ) { - $sql .= " WHERE " . $this->makeList( $conds, LIST_AND ); - } - $stmt = $this->parseStatement($sql); - if ($stmt === false) { - $this->reportQueryError( $this->lastError(), $this->lastErrno(), $stmt ); - return false; - } - if ($this->debug()) - wfDebug("SQL: $sql\n"); - $s = ''; - foreach ($values as $field => $v) { - oci_bind_by_name($stmt, ":n$field", $values[$field]); - if ($this->debug()) - $s .= " [$field] = [$v]\n"; - } - if ($this->debug()) - wfdebug(" PH: $s\n"); - $ret = $this->executeStatement($stmt); - return $ret; + /* For now, does nothing */ + function selectDB( $db ) { + return true; } /** - * INSERT wrapper, inserts an array into a table + * Returns an optional USE INDEX clause to go after the table, and a + * string to go at the end of the query * - * $a may be a single associative array, or an array of these with numeric keys, for - * multi-row insert. + * @private * - * Usually aborts on failure - * If errors are explicitly ignored, returns success + * @param array $options an associative array of options to be turned into + * an SQL query, valid keys are listed in the function. + * @return array */ - function insert( $table, $a, $fname = 'Database::insert', $options = array() ) { - # No rows to insert, easy just return now - if ( !count( $a ) ) { - return true; - } - - $table = $this->tableName( $table ); - if (!is_array($options)) - $options = array($options); - - $oldIgnore = false; - if (in_array('IGNORE', $options)) - $oldIgnore = $this->ignoreErrors( true ); - - if ( isset( $a[0] ) && is_array( $a[0] ) ) { - $multi = true; - $keys = array_keys( $a[0] ); - } else { - $multi = false; - $keys = array_keys( $a ); + function makeSelectOptions( $options ) { + $preLimitTail = $postLimitTail = ''; + $startOpts = ''; + + $noKeyOptions = array(); + foreach ( $options as $key => $option ) { + if ( is_numeric( $key ) ) { + $noKeyOptions[$option] = true; + } } - $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ('; - $return = ''; - $first = true; - foreach ($a as $key => $value) { - if ($first) - $first = false; - else - $sql .= ", "; - if (is_object($value) && $value->isLOB()) { - $sql .= "EMPTY_BLOB()"; - $return = "RETURNING $key INTO :bobj"; - } else - $sql .= ":$key"; + if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; + if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; + + if (isset($options['LIMIT'])) { + // $tailOpts .= $this->limitResult('', $options['LIMIT'], + // isset($options['OFFSET']) ? $options['OFFSET'] + // : false); } - $sql .= ") $return"; - if ($this->debug()) { - wfDebug("SQL: $sql\n"); - } + #if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE'; + #if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE'; + if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT'; - if (($stmt = $this->parseStatement($sql)) === false) { - $this->reportQueryError($this->lastError(), $this->lastErrno(), $sql, $fname); - $this->ignoreErrors($oldIgnore); - return false; + if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) { + $useIndex = $this->useIndexClause( $options['USE INDEX'] ); + } else { + $useIndex = ''; } + + return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail ); + } - /* - * If we're inserting multiple rows, parse the statement once and - * execute it for each set of values. Otherwise, convert it into an - * array and pretend. - */ - if (!$multi) - $a = array($a); - - foreach ($a as $key => $row) { - $blob = false; - $bdata = false; - $s = ''; - foreach ($row as $k => $value) { - if (is_object($value) && $value->isLOB()) { - $blob = oci_new_descriptor($this->mConn, OCI_D_LOB); - $bdata = $value->data(); - oci_bind_by_name($stmt, ":bobj", $blob, -1, OCI_B_BLOB); - } else - oci_bind_by_name($stmt, ":$k", $a[$key][$k], -1); - if ($this->debug()) - $s .= " [$k] = {$row[$k]}"; - } - if ($this->debug()) - wfDebug(" PH: $s\n"); - if (($s = $this->executeStatement($stmt)) === false) { - $this->reportQueryError($this->lastError(), $this->lastErrno(), $sql, $fname); - $this->ignoreErrors($oldIgnore); - return false; - } - - if ($blob) { - $blob->save($bdata); - } - } - $this->ignoreErrors($oldIgnore); - return $this->mLastResult = $s; + public function setTimeout( $timeout ) { + // @todo fixme no-op } function ping() { + wfDebug( "Function ping() not written for DatabasePostgres.php yet"); return true; } - function encodeBlob($b) { - return new OracleBlob($b); - } -} + +} // end DatabaseOracle class ?> diff --git a/includes/DatabasePostgres.php b/includes/DatabasePostgres.php index 803c0e26..7158e2d1 100644 --- a/includes/DatabasePostgres.php +++ b/includes/DatabasePostgres.php @@ -7,12 +7,69 @@ * than MySQL ones, some of them should be moved to parent * Database class. * - * @package MediaWiki + * @addtogroup Database */ +class PostgresField { + private $name, $tablename, $type, $nullable, $max_length; + + static function fromText($db, $table, $field) { + global $wgDBmwschema; + + $q = <<<END +SELECT typname, attnotnull, attlen +FROM pg_class, pg_namespace, pg_attribute, pg_type +WHERE relnamespace=pg_namespace.oid +AND relkind='r' +AND attrelid=pg_class.oid +AND atttypid=pg_type.oid +AND nspname=%s +AND relname=%s +AND attname=%s; +END; + $res = $db->query(sprintf($q, + $db->addQuotes($wgDBmwschema), + $db->addQuotes($table), + $db->addQuotes($field))); + $row = $db->fetchObject($res); + if (!$row) + return null; + $n = new PostgresField; + $n->type = $row->typname; + $n->nullable = ($row->attnotnull == 'f'); + $n->name = $field; + $n->tablename = $table; + $n->max_length = $row->attlen; + return $n; + } + + function name() { + return $this->name; + } + + function tableName() { + return $this->tablename; + } + + function type() { + return $this->type; + } + + function nullable() { + return $this->nullable; + } + function maxLength() { + return $this->max_length; + } +} + +/** + * @addtogroup Database + */ class DatabasePostgres extends Database { var $mInsertId = NULL; var $mLastResult = NULL; + var $numeric_version = NULL; function DatabasePostgres($server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0 ) @@ -25,24 +82,31 @@ class DatabasePostgres extends Database { } $this->mOut =& $wgOut; $this->mFailFunction = $failFunction; - $this->mCascadingDeletes = true; - $this->mCleanupTriggers = true; - $this->mStrictIPs = true; $this->mFlags = $flags; $this->open( $server, $user, $password, $dbName); } + function cascadingDeletes() { + return true; + } + function cleanupTriggers() { + return true; + } + function strictIPs() { + return true; + } function realTimestamps() { return true; } - function implicitGroupby() { return false; } + function searchableIPs() { + return true; + } - static function newFromParams( $server = false, $user = false, $password = false, $dbName = false, - $failFunction = false, $flags = 0) + static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0) { return new DatabasePostgres( $server, $user, $password, $dbName, $failFunction, $flags ); } @@ -57,9 +121,12 @@ class DatabasePostgres extends Database { throw new DBConnectionError( $this, "Postgres functions missing, have you compiled PHP with the --with-pgsql option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" ); } - global $wgDBport; + if (!strlen($user)) { ## e.g. the class is being loaded + return; + } + $this->close(); $this->mServer = $server; $port = $wgDBport; @@ -75,9 +142,6 @@ class DatabasePostgres extends Database { $hstring .= "port=$port "; } - if (!strlen($user)) { ## e.g. the class is being loaded - return; - } error_reporting( E_ALL ); @$this->mConn = pg_connect("$hstring dbname=$dbName user=$user password=$password"); @@ -94,21 +158,15 @@ class DatabasePostgres extends Database { if (defined('MEDIAWIKI_INSTALL')) { global $wgDBname, $wgDBuser, $wgDBpassword, $wgDBsuperuser, $wgDBmwschema, $wgDBts2schema; - print "OK</li>\n"; print "<li>Checking the version of Postgres..."; - $version = pg_fetch_result($this->doQuery("SELECT version()"),0,0); - $thisver = array(); - if (!preg_match('/PostgreSQL (\d+\.\d+)(\S+)/', $version, $thisver)) { - print "<b>FAILED</b> (could not determine the version)</li>\n"; - dieout("</ul>"); - } + $version = $this->getServerVersion(); $PGMINVER = "8.1"; - if ($thisver[1] < $PGMINVER) { - print "<b>FAILED</b>. Required version is $PGMINVER. You have $thisver[1]$thisver[2]</li>\n"; + if ($this->numeric_version < $PGMINVER) { + print "<b>FAILED</b>. Required version is $PGMINVER. You have $this->numeric_version ($version)</li>\n"; dieout("</ul>"); } - print "version $thisver[1]$thisver[2] is OK.</li>\n"; + print "version $this->numeric_version is OK.</li>\n"; $safeuser = $this->quote_ident($wgDBuser); ## Are we connecting as a superuser for the first time? @@ -232,7 +290,8 @@ class DatabasePostgres extends Database { $wgDBsuperuser = ''; return true; ## Reconnect as regular user - } + + } ## end superuser if (!defined('POSTGRES_SEARCHPATH')) { @@ -249,13 +308,24 @@ class DatabasePostgres extends Database { ## Does this user have the rights to the tsearch2 tables? $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0); print "<li>Checking tsearch2 permissions..."; + ## Let's check all four, just to be safe + error_reporting( 0 ); + $ts2tables = array('cfg','cfgmap','dict','parser'); + foreach ( $ts2tables AS $tname ) { + $SQL = "SELECT count(*) FROM $wgDBts2schema.pg_ts_$tname"; + $res = $this->doQuery($SQL); + if (!$res) { + print "<b>FAILED</b> to access pg_ts_$tname. Make sure that the user ". + "\"$wgDBuser\" has SELECT access to all four tsearch2 tables</li>\n"; + dieout("</ul>"); + } + } $SQL = "SELECT ts_name FROM $wgDBts2schema.pg_ts_cfg WHERE locale = '$ctype'"; $SQL .= " ORDER BY CASE WHEN ts_name <> 'default' THEN 1 ELSE 0 END"; - error_reporting( 0 ); $res = $this->doQuery($SQL); error_reporting( E_ALL ); if (!$res) { - print "<b>FAILED</b>. Make sure that the user \"$wgDBuser\" has SELECT access to the tsearch2 tables</li>\n"; + print "<b>FAILED</b>. Could not determine the tsearch2 locale information</li>\n"; dieout("</ul>"); } print "OK</li>"; @@ -282,7 +352,7 @@ class DatabasePostgres extends Database { $res = $this->doQuery($SQL); if (!$res) { print "<b>FAILED</b>. "; - print "Please make sure that the locale in pg_ts_cfg for \"default\" is set to \"ctype\"</li>\n"; + print "Please make sure that the locale in pg_ts_cfg for \"default\" is set to \"$ctype\"</li>\n"; dieout("</ul>"); } print "OK</li>"; @@ -325,9 +395,13 @@ class DatabasePostgres extends Database { $result = $this->schemaExists($wgDBmwschema); if (!$result) { print "<li>Creating schema <b>$wgDBmwschema</b> ..."; + error_reporting( 0 ); $result = $this->doQuery("CREATE SCHEMA $wgDBmwschema"); + error_reporting( E_ALL ); if (!$result) { - print "<b>FAILED</b>.</li>\n"; + print "<b>FAILED</b>. The user \"$wgDBuser\" must be able to access the schema. ". + "You can try making them the owner of the database, or try creating the schema with a ". + "different user, and then grant access to the \"$wgDBuser\" user.</li>\n"; dieout("</ul>"); } print "OK</li>\n"; @@ -339,6 +413,39 @@ class DatabasePostgres extends Database { print "<li>Schema \"$wgDBmwschema\" exists and is owned by \"$user\". Excellent.</li>\n"; } + ## Always return GMT time to accomodate the existing integer-based timestamp assumption + print "<li>Setting the timezone to GMT for user \"$user\" ..."; + $SQL = "ALTER USER $safeuser SET timezone = 'GMT'"; + $result = pg_query($this->mConn, $SQL); + if (!$result) { + print "<b>FAILED</b>.</li>\n"; + dieout("</ul>"); + } + print "OK</li>\n"; + ## Set for the rest of this session + $SQL = "SET timezone = 'GMT'"; + $result = pg_query($this->mConn, $SQL); + if (!$result) { + print "<li>Failed to set timezone</li>\n"; + dieout("</ul>"); + } + + print "<li>Setting the datestyle to ISO, YMD for user \"$user\" ..."; + $SQL = "ALTER USER $safeuser SET datestyle = 'ISO, YMD'"; + $result = pg_query($this->mConn, $SQL); + if (!$result) { + print "<b>FAILED</b>.</li>\n"; + dieout("</ul>"); + } + print "OK</li>\n"; + ## Set for the rest of this session + $SQL = "SET datestyle = 'ISO, YMD'"; + $result = pg_query($this->mConn, $SQL); + if (!$result) { + print "<li>Failed to set datestyle</li>\n"; + dieout("</ul>"); + } + ## Fix up the search paths if needed print "<li>Setting the search path for user \"$user\" ..."; $path = $this->quote_ident($wgDBmwschema); @@ -455,6 +562,30 @@ class DatabasePostgres extends Database { } /** + * Estimate rows in dataset + * Returns estimated count, based on EXPLAIN output + * This is not necessarily an accurate estimate, so use sparingly + * Returns -1 if count cannot be found + * Takes same arguments as Database::select() + */ + + function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) { + $options['EXPLAIN'] = true; + $res = $this->select( $table, $vars, $conds, $fname, $options ); + $rows = -1; + if ( $res ) { + $row = $this->fetchRow( $res ); + $count = array(); + if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) { + $rows = $count[1]; + } + $this->freeResult($res); + } + return $rows; + } + + + /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure */ @@ -645,7 +776,7 @@ class DatabasePostgres extends Database { return ''; } - function limitResult($sql, $limit,$offset) { + function limitResult($sql, $limit,$offset=false) { return "$sql LIMIT $limit ".(is_numeric($offset)?" OFFSET {$offset} ":""); } @@ -707,26 +838,31 @@ class DatabasePostgres extends Database { * @return string Version information from the database */ function getServerVersion() { - $res = $this->query( "SELECT version()" ); - $row = $this->fetchRow( $res ); - $version = $row[0]; - $this->freeResult( $res ); + $version = pg_fetch_result($this->doQuery("SELECT version()"),0,0); + $thisver = array(); + if (!preg_match('/PostgreSQL (\d+\.\d+)(\S+)/', $version, $thisver)) { + die("Could not determine the numeric version from $version!"); + } + $this->numeric_version = $thisver[1]; return $version; } /** - * Query whether a given table exists (in the given schema, or the default mw one if not given) + * Query whether a given relation exists (in the given schema, or the + * default mw one if not given) */ - function tableExists( $table, $schema = false ) { + function relationExists( $table, $types, $schema = false ) { global $wgDBmwschema; + if (!is_array($types)) + $types = array($types); if (! $schema ) $schema = $wgDBmwschema; - $etable = preg_replace("/'/", "''", $table); - $eschema = preg_replace("/'/", "''", $schema); + $etable = $this->addQuotes($table); + $eschema = $this->addQuotes($schema); $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n " - . "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema' " - . "AND c.relkind IN ('r','v')"; + . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema " + . "AND c.relkind IN ('" . implode("','", $types) . "')"; $res = $this->query( $SQL ); $count = $res ? pg_num_rows($res) : 0; if ($res) @@ -734,6 +870,61 @@ class DatabasePostgres extends Database { return $count; } + /* + * For backward compatibility, this function checks both tables and + * views. + */ + function tableExists ($table, $schema = false) { + return $this->relationExists($table, array('r', 'v'), $schema); + } + + function sequenceExists ($sequence, $schema = false) { + return $this->relationExists($sequence, 'S', $schema); + } + + function triggerExists($table, $trigger) { + global $wgDBmwschema; + + $q = <<<END + SELECT 1 FROM pg_class, pg_namespace, pg_trigger + WHERE relnamespace=pg_namespace.oid AND relkind='r' + AND tgrelid=pg_class.oid + AND nspname=%s AND relname=%s AND tgname=%s +END; + $res = $this->query(sprintf($q, + $this->addQuotes($wgDBmwschema), + $this->addQuotes($table), + $this->addQuotes($trigger))); + if (!$res) + return NULL; + $rows = pg_num_rows($res); + $this->freeResult($res); + return $rows; + } + + function ruleExists($table, $rule) { + global $wgDBmwschema; + $exists = $this->selectField("pg_rules", "rulename", + array( "rulename" => $rule, + "tablename" => $table, + "schemaname" => $wgDBmwschema)); + return $exists === $rule; + } + + function constraintExists($table, $constraint) { + global $wgDBmwschema; + $SQL = sprintf("SELECT 1 FROM information_schema.table_constraints ". + "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s", + $this->addQuotes($wgDBmwschema), + $this->addQuotes($table), + $this->addQuotes($constraint)); + $res = $this->query($SQL); + if (!$res) + return NULL; + $rows = pg_num_rows($res); + $this->freeResult($res); + return $rows; + } /** * Query whether a given schema exists. Returns the name of the owner @@ -752,7 +943,7 @@ class DatabasePostgres extends Database { /** * Query whether a given column exists in the mediawiki schema */ - function fieldExists( $table, $field ) { + function fieldExists( $table, $field, $fname = 'DatabasePostgres::fieldExists' ) { global $wgDBmwschema; $etable = preg_replace("/'/", "''", $table); $eschema = preg_replace("/'/", "''", $wgDBmwschema); @@ -760,7 +951,7 @@ class DatabasePostgres extends Database { $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n, pg_catalog.pg_attribute a " . "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema' " . "AND a.attrelid = c.oid AND a.attname = '$ecol'"; - $res = $this->query( $SQL ); + $res = $this->query( $SQL, $fname ); $count = $res ? pg_num_rows($res) : 0; if ($res) $this->freeResult( $res ); @@ -768,12 +959,10 @@ class DatabasePostgres extends Database { } function fieldInfo( $table, $field ) { - $res = $this->query( "SELECT $field FROM $table LIMIT 1" ); - $type = pg_field_type( $res, 0 ); - return $type; + return PostgresField::fromText($this, $table, $field); } - function begin( $fname = 'DatabasePostgrs::begin' ) { + function begin( $fname = 'DatabasePostgres::begin' ) { $this->query( 'BEGIN', $fname ); $this->mTrxLevel = 1; } @@ -791,10 +980,36 @@ class DatabasePostgres extends Database { } function setup_database() { - global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport; + global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser; + + ## Make sure that we can write to the correct schema + ## If not, Postgres will happily and silently go to the next search_path item + $ctest = "mw_test_table"; + if ($this->tableExists($ctest, $wgDBmwschema)) { + $this->doQuery("DROP TABLE $wgDBmwschema.$ctest"); + } + $SQL = "CREATE TABLE $wgDBmwschema.$ctest(a int)"; + error_reporting( 0 ); + $res = $this->doQuery($SQL); + error_reporting( E_ALL ); + if (!$res) { + print "<b>FAILED</b>. Make sure that the user \"$wgDBuser\" can write to the schema \"$wgDBmwschema\"</li>\n"; + dieout("</ul>"); + } + $this->doQuery("DROP TABLE $wgDBmwschema.mw_test_table"); dbsource( "../maintenance/postgres/tables.sql", $this); + ## Version-specific stuff + if ($this->numeric_version == 8.1) { + $this->doQuery("CREATE INDEX ts2_page_text ON pagecontent USING gist(textvector)"); + $this->doQuery("CREATE INDEX ts2_page_title ON page USING gist(titlevector)"); + } + else { + $this->doQuery("CREATE INDEX ts2_page_text ON pagecontent USING gin(textvector)"); + $this->doQuery("CREATE INDEX ts2_page_title ON page USING gin(titlevector)"); + } + ## Update version information $mwv = $this->addQuotes($wgVersion); $pgv = $this->addQuotes($this->getServerVersion()); @@ -827,6 +1042,8 @@ class DatabasePostgres extends Database { $this->query("$SQL $matches[1],$matches[2])"); } print " (table interwiki successfully populated)...\n"; + + $this->doQuery("COMMIT"); } function encodeBlob($b) { @@ -870,7 +1087,7 @@ class DatabasePostgres extends Database { * @return array */ function makeSelectOptions( $options ) { - $tailOpts = ''; + $preLimitTail = $postLimitTail = ''; $startOpts = ''; $noKeyOptions = array(); @@ -880,16 +1097,17 @@ class DatabasePostgres extends Database { } } - if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}"; - if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}"; + if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY " . $options['GROUP BY']; + if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY " . $options['ORDER BY']; - if (isset($options['LIMIT'])) { - $tailOpts .= $this->limitResult('', $options['LIMIT'], - isset($options['OFFSET']) ? $options['OFFSET'] : false); - } - - if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE'; - if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE'; + //if (isset($options['LIMIT'])) { + // $tailOpts .= $this->limitResult('', $options['LIMIT'], + // isset($options['OFFSET']) ? $options['OFFSET'] + // : false); + //} + + if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE'; + if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE'; if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT'; if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) { @@ -898,7 +1116,11 @@ class DatabasePostgres extends Database { $useIndex = ''; } - return array( $startOpts, $useIndex, $tailOpts ); + return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail ); + } + + public function setTimeout( $timeout ) { + // @todo fixme no-op } function ping() { diff --git a/includes/DateFormatter.php b/includes/DateFormatter.php index c795618a..88a64453 100644 --- a/includes/DateFormatter.php +++ b/includes/DateFormatter.php @@ -1,15 +1,9 @@ <?php -/** - * Date formatter, recognises dates in plain text and formats them accoding to user preferences. - * - * @package MediaWiki - * @subpackage Parser - */ /** + * Date formatter, recognises dates in plain text and formats them accoding to user preferences. * @todo preferences, OutputPage - * @package MediaWiki - * @subpackage Parser + * @addtogroup Parser */ class DateFormatter { diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 0692401d..169d67c9 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -15,7 +15,6 @@ * Documentation is in the source and on: * http://www.mediawiki.org/wiki/Help:Configuration_settings * - * @package MediaWiki */ # This is not a valid entry point, perform no further processing unless MEDIAWIKI is defined @@ -32,7 +31,7 @@ require_once( 'includes/SiteConfiguration.php' ); $wgConf = new SiteConfiguration; /** MediaWiki version number */ -$wgVersion = '1.9.3'; +$wgVersion = '1.10.0'; /** Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; @@ -163,7 +162,6 @@ $wgTmpDirectory = false; /// defaults to "{$wgUploadDirectory}/tmp" $wgUploadBaseUrl = ""; /**#@-*/ - /** * By default deleted files are simply discarded; to save them and * make it possible to undelete images, create a directory which @@ -192,6 +190,7 @@ $wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split * * Problematic punctuation: * []{}|# Are needed for link syntax, never enable these + * <> Causes problems with HTML escaping, don't use * % Enabled by default, minor problems with path to query rewrite rules, see below * + Enabled by default, but doesn't work with path to query rewrite rules, corrupted by apache * ? Enabled by default, but doesn't work with path to PATH_INFO rewrites @@ -307,8 +306,8 @@ $wgVerifyMimeType= true; /** Sets the mime type definition file to use by MimeMagic.php. * @global string $wgMimeTypeFile */ -#$wgMimeTypeFile= "/etc/mime.types"; $wgMimeTypeFile= "includes/mime.types"; +#$wgMimeTypeFile= "/etc/mime.types"; #$wgMimeTypeFile= NULL; #use built-in defaults only. /** Sets the mime type info file to use by MimeMagic.php. @@ -372,7 +371,11 @@ $wgSharedUploadDBprefix = ''; $wgCacheSharedUploads = true; /** Allow for upload to be copied from an URL. Requires Special:Upload?source=web */ $wgAllowCopyUploads = false; -/** Max size for uploads, in bytes */ +/** + * Max size for uploads, in bytes. Currently only works for uploads from URL + * via CURL (see $wgAllowCopyUploads). The only way to impose limits on + * normal uploads is currently to edit php.ini. + */ $wgMaxUploadSize = 1024*1024*100; # 100MB /** @@ -502,8 +505,12 @@ $wgDBtype = "mysql"; $wgSearchType = null; /** Table name prefix */ $wgDBprefix = ''; +/** MySQL table options to use during installation or update */ +$wgDBTableOptions = 'TYPE=InnoDB'; + /**#@-*/ + /** Live high performance sites should disable this - some checks acquire giant mysql locks */ $wgCheckDBSchema = true; @@ -964,6 +971,7 @@ $wgGroupPermissions['user' ]['upload'] = true; $wgGroupPermissions['user' ]['reupload'] = true; $wgGroupPermissions['user' ]['reupload-shared'] = true; $wgGroupPermissions['user' ]['minoredit'] = true; +$wgGroupPermissions['user' ]['purge'] = true; // can use ?action=purge without clicking "ok" // Implicit group for accounts that pass $wgAutoConfirmAge $wgGroupPermissions['autoconfirmed']['autoconfirmed'] = true; @@ -977,6 +985,7 @@ $wgGroupPermissions['emailconfirmed']['emailconfirmed'] = true; $wgGroupPermissions['bot' ]['bot'] = true; $wgGroupPermissions['bot' ]['autoconfirmed'] = true; $wgGroupPermissions['bot' ]['nominornewtalk'] = true; +$wgGroupPermissions['bot' ]['autopatrol'] = true; // Most extra permission abilities go to this group $wgGroupPermissions['sysop']['block'] = true; @@ -988,7 +997,7 @@ $wgGroupPermissions['sysop']['import'] = true; $wgGroupPermissions['sysop']['importupload'] = true; $wgGroupPermissions['sysop']['move'] = true; $wgGroupPermissions['sysop']['patrol'] = true; -$wgGroupPermissions['sysop']['autopatrol'] = true; +$wgGroupPermissions['sysop']['autopatrol'] = true; $wgGroupPermissions['sysop']['protect'] = true; $wgGroupPermissions['sysop']['proxyunbannable'] = true; $wgGroupPermissions['sysop']['rollback'] = true; @@ -1029,6 +1038,21 @@ $wgRestrictionTypes = array( 'edit', 'move' ); */ $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ); +/** + * Set the minimum permissions required to edit pages in each + * namespace. If you list more than one permission, a user must + * have all of them to edit pages in that namespace. + */ +$wgNamespaceProtection = array(); +$wgNamespaceProtection[ NS_MEDIAWIKI ] = array( 'editinterface' ); + +/** +* Pages in namespaces in this array can not be used as templates. +* Elements must be numeric namespace ids. +* Among other things, this may be useful to enforce read-restrictions +* which may otherwise be bypassed by using the template machanism. +*/ +$wgNonincludableNamespaces = array(); /** * Number of seconds an account is required to age before @@ -1045,6 +1069,11 @@ $wgAutoConfirmAge = 0; //$wgAutoConfirmAge = 600; // ten minutes //$wgAutoConfirmAge = 3600*24; // one day +# Number of edits an account requires before it is autoconfirmed +# Passing both this AND the time requirement is needed +$wgAutoConfirmCount = 0; +//$wgAutoConfirmCount = 50; + # Proxy scanner settings @@ -1096,7 +1125,7 @@ $wgCacheEpoch = '20030516000000'; * to ensure that client-side caches don't keep obsolete copies of global * styles. */ -$wgStyleVersion = '42b'; +$wgStyleVersion = '63'; # Server-side caching: @@ -1145,6 +1174,11 @@ $wgEnotifRevealEditorAddress = false; # UPO; reply-to address may be filled with $wgEnotifMinorEdits = true; # UPO; false: "minor edits" on pages do not trigger notification mails. # # Attention: _every_ change on a user_talk page trigger a notification mail (if the user is not yet notified) +/** + * Array of usernames who will be sent a notification email for every change which occurs on a wiki + */ +$wgUsersNotifedOnAllChanges = array(); + /** Show watching users in recent changes, watchlist and page history views */ $wgRCShowWatchingUsers = false; # UPO /** Show watching users in Page views */ @@ -1419,8 +1453,19 @@ $wgSiteNotice = ''; # Images settings # -/** dynamic server side image resizing ("Thumbnails") */ -$wgUseImageResize = false; +/** + * Plugins for media file type handling. + * Each entry in the array maps a MIME type to a class name + */ +$wgMediaHandlers = array( + 'image/jpeg' => 'BitmapHandler', + 'image/png' => 'BitmapHandler', + 'image/gif' => 'BitmapHandler', + 'image/x-ms-bmp' => 'BmpHandler', + 'image/svg+xml' => 'SvgHandler', + 'image/vnd.djvu' => 'DjVuHandler', +); + /** * Resizing can be done using PHP's internal image libraries or using @@ -1434,6 +1479,12 @@ $wgUseImageMagick = false; /** The convert command shipped with ImageMagick */ $wgImageMagickConvertCommand = '/usr/bin/convert'; +/** Sharpening parameter to ImageMagick */ +$wgSharpenParameter = '0x0.4'; + +/** Reduction in linear dimensions below which sharpening will be enabled */ +$wgSharpenReductionThreshold = 0.85; + /** * Use another resizing converter, e.g. GraphicMagick * %s will be replaced with the source path, %d with the destination @@ -1451,7 +1502,7 @@ $wgCustomConvertCommand = false; # # An external program is required to perform this conversion: $wgSVGConverters = array( - 'ImageMagick' => '$path/convert -background white -geometry $width $input $output', + 'ImageMagick' => '$path/convert -background white -geometry $width $input PNG:$output', 'sodipodi' => '$path/sodipodi -z -w $width -f $input -e $output', 'inkscape' => '$path/inkscape -z -w $width -f $input -e $output', 'batik' => 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input', @@ -1499,11 +1550,17 @@ $wgIgnoreImageErrors = false; */ $wgGenerateThumbnailOnParse = 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; } +/** For colorized maintenance script output, is your terminal background dark ? */ +$wgCommandLineDarkBg = false; # # Recent changes settings @@ -1613,18 +1670,22 @@ $wgExportAllowListContributors = false ; /** Text matching this regular expression will be recognised as spam * See http://en.wikipedia.org/wiki/Regular_expression */ $wgSpamRegex = false; -/** Similarly if this function returns true */ +/** Similarly you can get a function to do the job. The function will be given + * the following args: + * - a Title object for the article the edit is made on + * - the text submitted in the textarea (wpTextbox1) + * - the section number. + * The return should be boolean indicating whether the edit matched some evilness: + * - true : block it + * - false : let it through + * + * For a complete example, have a look at the SpamBlacklist extension. + */ $wgFilterCallback = false; /** Go button goes straight to the edit screen if the article doesn't exist. */ $wgGoToEdit = false; -/** Allow limited user-specified HTML in wiki pages? - * It will be run through a whitelist for security. Set this to false if you - * want wiki pages to consist only of wiki markup. Note that replacements do not - * yet exist for all HTML constructs.*/ -$wgUserHtml = true; - /** Allow raw, unchecked HTML in <html>...</html> sections. * THIS IS VERY DANGEROUS on a publically editable site, so USE wgGroupPermissions * TO RESTRICT EDITING to only those that you trust @@ -1633,8 +1694,7 @@ $wgRawHtml = false; /** * $wgUseTidy: use tidy to make sure HTML output is sane. - * This should only be enabled if $wgUserHtml is true. - * tidy is a free tool that fixes broken HTML. + * Tidy is a free tool that fixes broken HTML. * See http://www.w3.org/People/Raggett/tidy/ * $wgTidyBin should be set to the path of the binary and * $wgTidyConf to the path of the configuration file. @@ -1649,7 +1709,7 @@ $wgRawHtml = false; $wgUseTidy = false; $wgAlwaysUseTidy = false; $wgTidyBin = 'tidy'; -$wgTidyConf = $IP.'/extensions/tidy/tidy.conf'; +$wgTidyConf = $IP.'/includes/tidy.conf'; $wgTidyOpts = ''; $wgTidyInternal = function_exists( 'tidy_load_config' ); @@ -1660,7 +1720,7 @@ $wgDefaultSkin = 'monobook'; * Settings added to this array will override the default globals for the user * preferences used by anonymous visitors and newly created accounts. * For instance, to disable section editing links: - * $wgDefaultUserOptions ['editsection'] = 0; + * $wgDefaultUserOptions ['editsection'] = 0; * */ $wgDefaultUserOptions = array( @@ -1831,9 +1891,28 @@ $wgFeedDiffCutoff = 32768; $wgExtraNamespaces = NULL; /** + * Namespace aliases + * These are alternate names for the primary localised namespace names, which + * are defined by $wgExtraNamespaces and the language file. If a page is + * requested with such a prefix, the request will be redirected to the primary + * name. + * + * Set this to a map from namespace names to IDs. + * Example: + * $wgNamespaceAliases = array( + * 'Wikipedian' => NS_USER, + * 'Help' => 100, + * ); + */ +$wgNamespaceAliases = array(); + +/** * Limit images on image description pages to a user-selectable limit. In order - * to reduce disk usage, limits can only be selected from a list. This is the - * list of settings the user can choose from: + * to reduce disk usage, limits can only be selected from a list. + * The user preference is saved as an array offset in the database, by default + * the offset is set with $wgDefaultUserOptions['imagesize']. Make sure you + * change it if you alter the array (see bug 8858). + * This is the list of settings the user can choose from: */ $wgImageLimits = array ( array(320,240), @@ -1883,9 +1962,9 @@ $wgBrowserBlackList = array( * * Reference: http://www.psychedelix.com/agents/index.shtml */ - '/^Mozilla\/2\.[^ ]+ .*?\((?!compatible).*; [UIN]/', - '/^Mozilla\/3\.[^ ]+ .*?\((?!compatible).*; [UIN]/', - '/^Mozilla\/4\.[^ ]+ .*?\((?!compatible).*; [UIN]/', + '/^Mozilla\/2\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/', + '/^Mozilla\/3\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/', + '/^Mozilla\/4\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/', /** * MSIE on Mac OS 9 is teh sux0r, converts þ to <thorn>, ð to <eth>, Þ to <THORN> and Ð to <ETH> @@ -1985,7 +2064,9 @@ $wgLogTypes = array( '', 'delete', 'upload', 'move', - 'import' ); + 'import', + 'patrol', +); /** * Lists the message key string for each log type. The localized messages @@ -2001,7 +2082,9 @@ $wgLogNames = array( 'delete' => 'dellogpage', 'upload' => 'uploadlogpage', 'move' => 'movelogpage', - 'import' => 'importlogpage' ); + 'import' => 'importlogpage', + 'patrol' => 'patrol-log-page', +); /** * Lists the message key string for descriptive text to be shown at the @@ -2017,7 +2100,9 @@ $wgLogHeaders = array( 'delete' => 'dellogpagetext', 'upload' => 'uploadlogpagetext', 'move' => 'movelogpagetext', - 'import' => 'importlogpagetext', ); + 'import' => 'importlogpagetext', + 'patrol' => 'patrol-log-header', +); /** * Lists the message key string for formatting individual events of each @@ -2039,7 +2124,8 @@ $wgLogActions = array( 'move/move' => '1movedto2', 'move/move_redir' => '1movedto2_redir', 'import/upload' => 'import-logentry-upload', - 'import/interwiki' => 'import-logentry-interwiki' ); + 'import/interwiki' => 'import-logentry-interwiki', +); /** * Experimental preview feature to fetch rendered text @@ -2166,6 +2252,9 @@ $wgRateLimits = array( 'mailpassword' => array( 'anon' => NULL, ), + 'emailuser' => array( + 'user' => null, + ), ); /** @@ -2235,7 +2324,7 @@ $wgTrustedMediaFormats= array( MEDIATYPE_BITMAP, //all bitmap formats MEDIATYPE_AUDIO, //all audio formats MEDIATYPE_VIDEO, //all plain video formats - "image/svg", //svg (only needed if inline rendering of svg is not supported) + "image/svg+xml", //svg (only needed if inline rendering of svg is not supported) "application/pdf", //PDF files #"application/x-shockwave-flash", //flash/shockwave movie ); @@ -2330,7 +2419,7 @@ $wgAllowDisplayTitle = false ; $wgReservedUsernames = array( 'MediaWiki default', // Default 'Main Page' and MediaWiki: message pages 'Conversion script', // Used for the old Wikipedia software upgrade - 'Maintenance script', // ... maintenance/edit.php uses this? + 'Maintenance script', // Maintenance scripts which perform editing, image import script 'Template namespace initialisation script', // Used in 1.2->1.3 upgrade ); @@ -2338,7 +2427,7 @@ $wgReservedUsernames = array( * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic browsers which can't * perform basic stuff like MIME detection and which are vulnerable to further idiots uploading * crap files as images. When this directive is on, <title> will be allowed in files with - * an "image/svg" MIME type. You should leave this disabled if your web server is misconfigured + * an "image/svg+xml" MIME type. You should leave this disabled if your web server is misconfigured * and doesn't send appropriate MIME types for SVG images. */ $wgAllowTitlesInSVG = false; @@ -2364,25 +2453,42 @@ $wgMaxShellFileSize = 102400; /** * DJVU settings - * Path of the djvutoxml executable + * Path of the djvudump executable * Enable this and $wgDjvuRenderer to enable djvu rendering */ -# $wgDjvuToXML = 'djvutoxml'; -$wgDjvuToXML = null; +# $wgDjvuDump = 'djvudump'; +$wgDjvuDump = null; /** * Path of the ddjvu DJVU renderer - * Enable this and $wgDjvuToXML to enable djvu rendering + * Enable this and $wgDjvuDump to enable djvu rendering */ # $wgDjvuRenderer = 'ddjvu'; $wgDjvuRenderer = null; /** - * Path of the DJVU post processor - * May include command line options - * Default: ppmtojpeg, since ddjvu generates ppm output + * Path of the djvutoxml executable + * This works like djvudump except much, much slower as of version 3.5. + * + * For now I recommend you use djvudump instead. The djvuxml output is + * probably more stable, so we'll switch back to it as soon as they fix + * the efficiency problem. + * http://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583 + */ +# $wgDjvuToXML = 'djvutoxml'; +$wgDjvuToXML = null; + + +/** + * Shell command for the DJVU post processor + * Default: pnmtopng, since ddjvu generates ppm output + * Set this to false to output the ppm file directly. + */ +$wgDjvuPostProcessor = 'pnmtojpeg'; +/** + * File extension for the DJVU post processor output */ -$wgDjvuPostProcessor = 'ppmtojpeg'; +$wgDjvuOutputExtension = 'jpg'; /** * Enable direct access to the data API @@ -2416,4 +2522,14 @@ $wgBreakFrames = false; */ $wgDisableQueryPageUpdate = false; +/** + * Set this to false to disable cascading protection + */ +$wgEnableCascadingProtection = true; + +/** + * Disable output compression (enabled by default if zlib is available) + */ +$wgDisableOutputCompression = false; + ?> diff --git a/includes/Defines.php b/includes/Defines.php index 84bc4495..98e76277 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -1,7 +1,6 @@ <?php /** * A few constants that might be needed during LocalSettings.php - * @package MediaWiki */ /** diff --git a/includes/DifferenceEngine.php b/includes/DifferenceEngine.php index a72f0153..af65ce3a 100644 --- a/includes/DifferenceEngine.php +++ b/includes/DifferenceEngine.php @@ -1,15 +1,14 @@ <?php /** * See diff.doc - * @package MediaWiki - * @subpackage DifferenceEngine + * @todo indicate where diff.doc can be found. + * @addtogroup DifferenceEngine */ /** * @todo document * @public - * @package MediaWiki - * @subpackage DifferenceEngine + * @addtogroup DifferenceEngine */ class DifferenceEngine { /**#@+ @@ -63,8 +62,8 @@ class DifferenceEngine { $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer } - function showDiffPage() { - global $wgUser, $wgOut, $wgContLang, $wgUseExternalEditor, $wgUseRCPatrol; + function showDiffPage( $diffOnly = false ) { + global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol; $fname = 'DifferenceEngine::showDiffPage'; wfProfileIn( $fname ); @@ -118,6 +117,7 @@ CONTROL; # is the first version of that article. In that case, V' does not exist. if ( $this->mOldid === false ) { $this->showFirstRevision(); + $this->renderNewRevision(); // should we respect $diffOnly here or not? wfProfileOut( $fname ); return; } @@ -178,15 +178,34 @@ CONTROL; $oldHeader = "<strong>{$this->mOldtitle}</strong><br />" . $sk->revUserTools( $this->mOldRev ) . "<br />" . - $oldminor . $sk->revComment( $this->mOldRev, true ) . "<br />" . + $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly ) . "<br />" . $prevlink; $newHeader = "<strong>{$this->mNewtitle}</strong><br />" . $sk->revUserTools( $this->mNewRev ) . " $rollback<br />" . - $newminor . $sk->revComment( $this->mNewRev, true ) . "<br />" . + $newminor . $sk->revComment( $this->mNewRev, !$diffOnly ) . "<br />" . $nextlink . $patrol; $this->showDiff( $oldHeader, $newHeader ); + + if ( !$diffOnly ) + $this->renderNewRevision(); + + wfProfileOut( $fname ); + } + + /** + * Show the new revision of the page. + */ + function renderNewRevision() { + global $wgOut; + $fname = 'DifferenceEngine::renderNewRevision'; + wfProfileIn( $fname ); + $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" ); + #add deleted rev tag if needed + if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { + $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) ); + } if( !$this->mNewRev->isCurrent() ) { $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false ); @@ -196,7 +215,8 @@ CONTROL; if( is_object( $this->mNewRev ) ) { $wgOut->setRevisionId( $this->mNewRev->getId() ); } - $wgOut->addSecondaryWikiText( $this->mNewtext ); + + $wgOut->addWikiTextTidy( $this->mNewtext ); if( !$this->mNewRev->isCurrent() ) { $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); @@ -254,15 +274,6 @@ CONTROL; $wgOut->setSubtitle( wfMsg( 'difference' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); - - # Show current revision - # - $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" ); - if( is_object( $this->mNewRev ) ) { - $wgOut->setRevisionId( $this->mNewRev->getId() ); - } - $wgOut->addSecondaryWikiText( $this->mNewtext ); - wfProfileOut( $fname ); } @@ -322,9 +333,14 @@ CONTROL; } } + #loadtext is permission safe, this just clears out the diff if ( !$this->loadText() ) { wfProfileOut( $fname ); return false; + } else if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) { + return ''; + } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { + return ''; } $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); @@ -463,6 +479,14 @@ CONTROL; * Add the header to a diff body */ function addHeader( $diff, $otitle, $ntitle, $multi = '' ) { + global $wgOut; + + if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) { + $otitle = '<span class="history-deleted">'.$otitle.'</span>'; + } + if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { + $ntitle = '<span class="history-deleted">'.$ntitle.'</span>'; + } $header = " <table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'> <tr> @@ -523,21 +547,17 @@ CONTROL; $newLink = $this->mNewPage->escapeLocalUrl(); $this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) ); $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' ); - $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undo=' . $this->mNewid ); $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)" - . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)" - . " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)"; + . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; } else { $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid ); $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid ); - $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undo=' . $this->mNewid ); $this->mPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $timestamp ) ); $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>" - . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)" - . " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)"; + . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; } // Load the old revision object @@ -568,6 +588,9 @@ CONTROL; $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid ); $this->mOldtitle = "<a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) ) . "</a> (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + //now that we considered old rev, we can make undo link (bug 8133, multi-edit undo) + $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid); + $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)"; } return true; @@ -589,13 +612,13 @@ CONTROL; } if ( $this->mOldRev ) { // FIXME: permission tests - $this->mOldtext = $this->mOldRev->getText(); + $this->mOldtext = $this->mOldRev->revText(); if ( $this->mOldtext === false ) { return false; } } if ( $this->mNewRev ) { - $this->mNewtext = $this->mNewRev->getText(); + $this->mNewtext = $this->mNewRev->revText(); if ( $this->mNewtext === false ) { return false; } @@ -633,8 +656,7 @@ define('USE_ASSERTS', function_exists('assert')); /** * @todo document * @private - * @package MediaWiki - * @subpackage DifferenceEngine + * @addtogroup DifferenceEngine */ class _DiffOp { var $type; @@ -657,8 +679,7 @@ class _DiffOp { /** * @todo document * @private - * @package MediaWiki - * @subpackage DifferenceEngine + * @addtogroup DifferenceEngine */ class _DiffOp_Copy extends _DiffOp { var $type = 'copy'; @@ -678,8 +699,7 @@ class _DiffOp_Copy extends _DiffOp { /** * @todo document * @private - * @package MediaWiki - * @subpackage DifferenceEngine + * @addtogroup DifferenceEngine */ class _DiffOp_Delete extends _DiffOp { var $type = 'delete'; @@ -697,8 +717,7 @@ class _DiffOp_Delete extends _DiffOp { /** * @todo document * @private - * @package MediaWiki - * @subpackage DifferenceEngine + * @addtogroup DifferenceEngine */ class _DiffOp_Add extends _DiffOp { var $type = 'add'; @@ -716,8 +735,7 @@ class _DiffOp_Add extends _DiffOp { /** * @todo document * @private - * @package MediaWiki - * @subpackage DifferenceEngine + * @addtogroup DifferenceEngine */ class _DiffOp_Change extends _DiffOp { var $type = 'change'; @@ -754,8 +772,7 @@ class _DiffOp_Change extends _DiffOp { * * @author Geoffrey T. Dairiki, Tim Starling * @private - * @package MediaWiki - * @subpackage DifferenceEngine + * @addtogroup DifferenceEngine */ class _DiffEngine { @@ -1176,8 +1193,7 @@ class _DiffEngine * Class representing a 'diff' between two sequences of strings. * @todo document * @private - * @package MediaWiki - * @subpackage DifferenceEngine + * @addtogroup DifferenceEngine */ class Diff { @@ -1315,11 +1331,9 @@ class Diff } /** - * FIXME: bad name. - * @todo document + * @todo document, bad name. * @private - * @package MediaWiki - * @subpackage DifferenceEngine + * @addtogroup DifferenceEngine */ class MappedDiff extends Diff { @@ -1382,8 +1396,7 @@ class MappedDiff extends Diff * to obtain fancier outputs. * @todo document * @private - * @package MediaWiki - * @subpackage DifferenceEngine + * @addtogroup DifferenceEngine */ class DiffFormatter { @@ -1549,8 +1562,7 @@ define('NBSP', ' '); // iso-8859-x non-breaking space. /** * @todo document * @private - * @package MediaWiki - * @subpackage DifferenceEngine + * @addtogroup DifferenceEngine */ class _HWLDF_WordAccumulator { function _HWLDF_WordAccumulator () { @@ -1562,9 +1574,12 @@ class _HWLDF_WordAccumulator { function _flushGroup ($new_tag) { if ($this->_group !== '') { - if ($this->_tag == 'mark') - $this->_line .= '<span class="diffchange">' . - htmlspecialchars ( $this->_group ) . '</span>'; + if ($this->_tag == 'ins') + $this->_line .= '<ins class="diffchange">' . + htmlspecialchars ( $this->_group ) . '</ins>'; + elseif ($this->_tag == 'del') + $this->_line .= '<del class="diffchange">' . + htmlspecialchars ( $this->_group ) . '</del>'; else $this->_line .= htmlspecialchars ( $this->_group ); } @@ -1608,8 +1623,7 @@ class _HWLDF_WordAccumulator { /** * @todo document * @private - * @package MediaWiki - * @subpackage DifferenceEngine + * @addtogroup DifferenceEngine */ class WordLevelDiff extends MappedDiff { @@ -1669,7 +1683,7 @@ class WordLevelDiff extends MappedDiff if ($edit->type == 'copy') $orig->addWords($edit->orig); elseif ($edit->orig) - $orig->addWords($edit->orig, 'mark'); + $orig->addWords($edit->orig, 'del'); } $lines = $orig->getLines(); wfProfileOut( $fname ); @@ -1685,7 +1699,7 @@ class WordLevelDiff extends MappedDiff if ($edit->type == 'copy') $closing->addWords($edit->closing); elseif ($edit->closing) - $closing->addWords($edit->closing, 'mark'); + $closing->addWords($edit->closing, 'ins'); } $lines = $closing->getLines(); wfProfileOut( $fname ); @@ -1697,8 +1711,7 @@ class WordLevelDiff extends MappedDiff * Wikipedia Table style diff formatter. * @todo document * @private - * @package MediaWiki - * @subpackage DifferenceEngine + * @addtogroup DifferenceEngine */ class TableDiffFormatter extends DiffFormatter { diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php index 3b8a68ba..1e423565 100644 --- a/includes/DjVuImage.php +++ b/includes/DjVuImage.php @@ -1,11 +1,6 @@ <?php + /** - * Support for detecting/validating DjVu image files and getting - * some basic file metadata (resolution etc) - * - * File format docs are available in source package for DjVuLibre: - * http://djvulibre.djvuzone.org/ - * * * Copyright (C) 2006 Brion Vibber <brion@pobox.com> * http://www.mediawiki.org/ @@ -25,9 +20,17 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * - * @package MediaWiki */ +/** + * Support for detecting/validating DjVu image files and getting + * some basic file metadata (resolution etc) + * + * File format docs are available in source package for DjVuLibre: + * http://djvulibre.djvuzone.org/ + * + * @addtogroup Media + */ class DjVuImage { function __construct( $filename ) { $this->mFilename = $filename; @@ -68,6 +71,7 @@ class DjVuImage { function dump() { $file = fopen( $this->mFilename, 'rb' ); $header = fread( $file, 12 ); + // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4magic/a4chunk/NchunkLength', $header ) ); echo "$chunk $chunkLength\n"; $this->dumpForm( $file, $chunkLength, 1 ); @@ -83,6 +87,7 @@ class DjVuImage { if( $chunkHeader == '' ) { break; } + // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4chunk/NchunkLength', $chunkHeader ) ); echo str_repeat( ' ', $indent * 4 ) . "$chunk $chunkLength\n"; @@ -111,6 +116,7 @@ class DjVuImage { if( strlen( $header ) < 16 ) { wfDebug( __METHOD__ . ": too short file header\n" ); } else { + // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4magic/a4form/NformLength/a4subtype', $header ) ); if( $magic != 'AT&T' ) { @@ -134,6 +140,7 @@ class DjVuImage { if( strlen( $header ) < 8 ) { return array( false, 0 ); } else { + // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4chunk/Nlength', $header ) ); return array( $chunk, $length ); } @@ -192,6 +199,7 @@ class DjVuImage { return false; } + // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'nwidth/' . 'nheight/' . @@ -214,17 +222,121 @@ class DjVuImage { * @return string */ function retrieveMetaData() { - global $wgDjvuToXML; - if ( isset( $wgDjvuToXML ) ) { - $cmd = $wgDjvuToXML . ' --without-anno --without-text ' . + global $wgDjvuToXML, $wgDjvuDump; + 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 + wfProfileIn( 'djvudump' ); + $cmd = wfEscapeShellArg( $wgDjvuDump ) . ' ' . wfEscapeShellArg( $this->mFilename ); + $dump = wfShellExec( $cmd ); + $xml = $this->convertDumpToXML( $dump ); + wfProfileOut( 'djvudump' ); + } elseif ( isset( $wgDjvuToXML ) ) { + wfProfileIn( 'djvutoxml' ); + $cmd = wfEscapeShellArg( $wgDjvuToXML ) . ' --without-anno --without-text ' . wfEscapeShellArg( $this->mFilename ); $xml = wfShellExec( $cmd ); + wfProfileOut( 'djvutoxml' ); } else { $xml = null; } return $xml; } - + + /** + * Hack to temporarily work around djvutoxml bug + */ + function convertDumpToXML( $dump ) { + if ( strval( $dump ) == '' ) { + return false; + } + + $xml = <<<EOT +<?xml version="1.0" ?> +<!DOCTYPE DjVuXML PUBLIC "-//W3C//DTD DjVuXML 1.1//EN" "pubtext/DjVuXML-s.dtd"> +<DjVuXML> +<HEAD></HEAD> +<BODY> +EOT; + + $dump = str_replace( "\r", '', $dump ); + $line = strtok( $dump, "\n" ); + $m = false; + $good = false; + if ( preg_match( '/^( *)FORM:DJVU/', $line, $m ) ) { + # Single-page + if ( $this->parseFormDjvu( $line, $xml ) ) { + $good = true; + } else { + return false; + } + } elseif ( preg_match( '/^( *)FORM:DJVM/', $line, $m ) ) { + # Multi-page + $parentLevel = strlen( $m[1] ); + # Find DIRM + $line = strtok( "\n" ); + while ( $line !== false ) { + $childLevel = strspn( $line, ' ' ); + if ( $childLevel <= $parentLevel ) { + # End of chunk + break; + } + + if ( preg_match( '/^ *DIRM.*indirect/', $line ) ) { + wfDebug( "Indirect multi-page DjVu document, bad for server!\n" ); + return false; + } + if ( preg_match( '/^ *FORM:DJVU/', $line ) ) { + # Found page + if ( $this->parseFormDjvu( $line, $xml ) ) { + $good = true; + } else { + return false; + } + } + $line = strtok( "\n" ); + } + } + if ( !$good ) { + return false; + } + + $xml .= "</BODY>\n</DjVuXML>\n"; + return $xml; + } + + function parseFormDjvu( $line, &$xml ) { + $parentLevel = strspn( $line, ' ' ); + $line = strtok( "\n" ); + + # Find INFO + while ( $line !== false ) { + $childLevel = strspn( $line, ' ' ); + if ( $childLevel <= $parentLevel ) { + # End of chunk + break; + } + + if ( preg_match( '/^ *INFO *\[\d*\] *DjVu *(\d+)x(\d+), *\w*, *(\d+) *dpi, *gamma=([0-9.-]+)/', $line, $m ) ) { + $xml .= Xml::tags( 'OBJECT', + array( + #'data' => '', + #'type' => 'image/x.djvu', + 'height' => $m[2], + 'width' => $m[1], + #'usemap' => '', + ), + "\n" . + Xml::element( 'PARAM', array( 'name' => 'DPI', 'value' => $m[3] ) ) . "\n" . + Xml::element( 'PARAM', array( 'name' => 'GAMMA', 'value' => $m[4] ) ) . "\n" + ) . "\n"; + return true; + } + $line = strtok( "\n" ); + } + # Not found + return false; + } } diff --git a/includes/EditPage.php b/includes/EditPage.php index 7688a64a..bec6e300 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -1,18 +1,14 @@ <?php /** - * Contain the EditPage class - * @package MediaWiki + * Contains the EditPage class */ /** - * Splitting edit page/HTML interface from Article... + * The edit page/HTML interface (split from Article) * The actual database and text munging is still in Article, * but it should get easier to call those from alternate * interfaces. - * - * @package MediaWiki */ - class EditPage { var $mArticle; var $mTitle; @@ -69,22 +65,26 @@ class EditPage { /** * Fetch initial editing page content. */ - private function getContent() { + private function getContent( $def_text = '' ) { global $wgOut, $wgRequest, $wgParser; # Get variables from query string :P $section = $wgRequest->getVal( 'section' ); $preload = $wgRequest->getVal( 'preload' ); + $undoafter = $wgRequest->getVal( 'undoafter' ); $undo = $wgRequest->getVal( 'undo' ); wfProfileIn( __METHOD__ ); $text = ''; if( !$this->mTitle->exists() ) { - - # If requested, preload some text. - $text = $this->getPreloadedText( $preload ); - + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + # If this is a system message, get the default text. + $text = wfMsgWeirdKey ( $this->mTitle->getText() ) ; + } else { + # If requested, preload some text. + $text = $this->getPreloadedText( $preload ); + } # We used to put MediaWiki:Newarticletext here if # $text was empty at this point. # This is now shown above the edit box instead. @@ -94,51 +94,63 @@ class EditPage { // fetch the page record from the high-priority server, // which is needed to guarantee we don't pick up lagged // information. - + $text = $this->mArticle->getContent(); - if ( $undo > 0 ) { - #Undoing a specific edit overrides section editing; section-editing + if ( $undo > 0 && $undo > $undoafter ) { + # Undoing a specific edit overrides section editing; section-editing # doesn't work with undoing. - $undorev = Revision::newFromId($undo); + if ( $undoafter ) { + $undorev = Revision::newFromId($undo); + $oldrev = Revision::newFromId($undoafter); + } else { + $undorev = Revision::newFromId($undo); + $oldrev = $undorev ? $undorev->getPrevious() : null; + } #Sanity check, make sure it's the right page. # Otherwise, $text will be left as-is. - if (!is_null($undorev) && $undorev->getPage() == $this->mArticle->getID()) { - $oldrev = $undorev->getPrevious(); + if ( !is_null($undorev) && !is_null($oldrev) && $undorev->getPage()==$oldrev->getPage() && $undorev->getPage()==$this->mArticle->getID() ) { $undorev_text = $undorev->getText(); $oldrev_text = $oldrev->getText(); $currev_text = $text; #No use doing a merge if it's just a straight revert. - if ($currev_text != $undorev_text) { + if ( $currev_text != $undorev_text ) { $result = wfMerge($undorev_text, $oldrev_text, $currev_text, $text); } else { $text = $oldrev_text; $result = true; } - - if( $result ) { - # Inform the user of our success and set an automatic edit summary - $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-success' ) ); - $this->summary = wfMsgForContent( 'undo-summary', $undo, $undorev->getUserText() ); - $this->formtype = 'diff'; - } else { - # Warn the user that something went wrong - $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-failure' ) ); + } else { + // Failed basic sanity checks. + // Older revisions may have been removed since the link + // was created, or we may simply have got bogus input. + $result = false; + } + + if( $result ) { + # Inform the user of our success and set an automatic edit summary + $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-success' ) ); + $firstrev = $oldrev->getNext(); + # If we just undid one rev, use an autosummary + if ( $firstrev->mId == $undo ) { + $this->summary = wfMsgForContent('undo-summary', $undo, $undorev->getUserText()); } - + $this->formtype = 'diff'; + } else { + # Warn the user that something went wrong + $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-failure' ) ); } - } - else if( $section != '' ) { + } else if( $section != '' ) { if( $section == 'new' ) { $text = $this->getPreloadedText( $preload ); } else { - $text = $wgParser->getSection( $text, $section ); + $text = $wgParser->getSection( $text, $section, $def_text ); } } } - + wfProfileOut( __METHOD__ ); return $text; } @@ -282,7 +294,7 @@ class EditPage { global $wgOut, $wgUser, $wgRequest, $wgTitle; global $wgEmailConfirmToEdit; - if ( ! wfRunHooks( 'AlternateEdit', array( &$this ) ) ) + if ( ! wfRunHooks( 'AlternateEdit', array( &$this ) ) ) return; $fname = 'EditPage::edit'; @@ -301,7 +313,7 @@ class EditPage { return; } - if ( ! $this->mTitle->userCanEdit() ) { + if ( ! $this->mTitle->userCan( 'edit' ) ) { wfDebug( "$fname: user can't edit\n" ); $wgOut->readOnlyPage( $this->getContent(), true ); wfProfileOut( $fname ); @@ -335,7 +347,7 @@ class EditPage { wfProfileOut($fname); return; } - if ( !$this->mTitle->userCanCreate() && !$this->mTitle->exists() ) { + if ( !$this->mTitle->userCan( 'create' ) && !$this->mTitle->exists() ) { wfDebug( "$fname: no create permission\n" ); $this->noCreatePermission(); wfProfileOut( $fname ); @@ -421,7 +433,12 @@ class EditPage { # First time through: get contents, set time for conflict # checking, etc. if ( 'initial' == $this->formtype || $this->firsttime ) { - $this->initialiseForm(); + if ($this->initialiseForm() === false) { + $this->noSuchSectionPage(); + wfProfileOut( "$fname-business-end" ); + wfProfileOut( $fname ); + return; + } if( !$this->mTitle->getArticleId() ) wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) ); } @@ -482,7 +499,7 @@ class EditPage { // Remember whether a save was requested, so we can indicate // if we forced preview due to session failure. $this->mTriedSave = !$this->preview; - + if ( $this->tokenOk( $request ) ) { # Some browsers will not report any submit button # if the user hits enter in the comment box. @@ -519,8 +536,8 @@ class EditPage { } else { $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ); } - - $this->autoSumm = $request->getText( 'wpAutoSummary' ); + + $this->autoSumm = $request->getText( 'wpAutoSummary' ); } else { # Not a posted form? Start with nothing. wfDebug( "$fname: Not a posted form.\n" ); @@ -652,7 +669,7 @@ class EditPage { wfProfileOut( $fname ); return true; } - + if ( !$wgUser->isAllowed('edit') ) { if ( $wgUser->isAnon() ) { $this->userNotLoggedInPage(); @@ -696,7 +713,7 @@ class EditPage { if ( 0 == $aid ) { // Late check for create permission, just in case *PARANOIA* - if ( !$this->mTitle->userCanCreate() ) { + if ( !$this->mTitle->userCan( 'create' ) ) { wfDebug( "$fname: no create permission\n" ); $this->noCreatePermission(); wfProfileOut( $fname ); @@ -723,6 +740,8 @@ 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"); + if( $this->mArticle->getTimestamp() != $this->edittime ) { $this->isConflict = true; if( $this->section == 'new' ) { @@ -794,7 +813,7 @@ class EditPage { } #And a similar thing for new sections - if( $this->section == 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary' ) ) { + if( $this->section == 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary' ) ) { if (trim($this->summary) == '') { $this->missingSummary = true; wfProfileOut( $fname ); @@ -860,10 +879,13 @@ class EditPage { function initialiseForm() { $this->edittime = $this->mArticle->getTimestamp(); $this->summary = ''; - $this->textbox1 = $this->getContent(); + $this->textbox1 = $this->getContent(false); + if ($this->textbox1 === false) return false; + if ( !$this->mArticle->exists() && $this->mArticle->mTitle->getNamespace() == NS_MEDIAWIKI ) - $this->textbox1 = wfMsgWeirdKey( $this->mArticle->mTitle->getText() ) ; + $this->textbox1 = wfMsgWeirdKey( $this->mArticle->mTitle->getText() ); wfProxyCheck(); + return true; } /** @@ -878,7 +900,7 @@ class EditPage { $fname = 'EditPage::showEditForm'; wfProfileIn( $fname ); - $sk =& $wgUser->getSkin(); + $sk = $wgUser->getSkin(); wfRunHooks( 'EditPage::showEditForm:initial', array( &$this ) ) ; @@ -920,15 +942,15 @@ class EditPage { if ( $this->missingComment ) { $wgOut->addWikiText( wfMsg( 'missingcommenttext' ) ); } - + if( $this->missingSummary && $this->section != 'new' ) { $wgOut->addWikiText( wfMsg( 'missingsummary' ) ); } - if( $this->missingSummary && $this->section == 'new' ) { - $wgOut->addWikiText( wfMsg( 'missingcommentheader' ) ); - } - + if( $this->missingSummary && $this->section == 'new' ) { + $wgOut->addWikiText( wfMsg( 'missingcommentheader' ) ); + } + if( !$this->hookError == '' ) { $wgOut->addWikiText( $this->hookError ); } @@ -936,11 +958,15 @@ class EditPage { if ( !$this->checkUnicodeCompliantBrowser() ) { $wgOut->addWikiText( wfMsg( 'nonunicodebrowser') ); } - if ( isset( $this->mArticle ) - && isset( $this->mArticle->mRevision ) - && !$this->mArticle->mRevision->isCurrent() ) { - $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() ); - $wgOut->addWikiText( wfMsg( 'editingold' ) ); + if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) { + // Let sysop know that this will make private content public if saved + if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { + $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) ); + } + if( !$this->mArticle->mRevision->isCurrent() ) { + $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() ); + $wgOut->addWikiText( wfMsg( 'editingold' ) ); + } } } @@ -958,24 +984,33 @@ class EditPage { } } } - - if( $this->mTitle->isProtected( 'edit' ) ) { - # Is the protection due to the namespace, e.g. interface text? - if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - # Yes; remind the user - $notice = wfMsg( 'editinginterface' ); - } elseif( $this->mTitle->isSemiProtected() ) { - # No; semi protected + + if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + # Show a warning if editing an interface message + $wgOut->addWikiText( wfMsg( 'editinginterface' ) ); + } elseif( $this->mTitle->isProtected( 'edit' ) ) { + # Is the title semi-protected? + if( $this->mTitle->isSemiProtected() ) { $notice = wfMsg( 'semiprotectedpagewarning' ); - if( wfEmptyMsg( 'semiprotectedpagewarning', $notice ) || $notice == '-' ) { + if( wfEmptyMsg( 'semiprotectedpagewarning', $notice ) || $notice == '-' ) $notice = ''; - } } else { - # No; regular protection + # Then it must be protected based on static groups (regular) $notice = wfMsg( 'protectedpagewarning' ); } $wgOut->addWikiText( $notice ); } + if ( $this->mTitle->isCascadeProtected() ) { + # Is this page under cascading protection from some source pages? + list($cascadeSources, $restrictions) = $this->mTitle->getCascadeProtectionSources(); + if ( count($cascadeSources) > 0 ) { + # Explain, and list the titles responsible + $notice = wfMsgExt( 'cascadeprotectedwarning', array('parsemag'), count($cascadeSources) ) . "\n"; + foreach( $cascadeSources as $id => $page ) + $notice .= '* [[:' . $page->getPrefixedText() . "]]\n"; + } + $wgOut->addWikiText( $notice ); + } if ( $this->kblength === false ) { $this->kblength = (int)(strlen( $this->textbox1 ) / 1024); @@ -1005,8 +1040,6 @@ class EditPage { $summary = wfMsg('summary'); $subject = wfMsg('subject'); - $minor = wfMsgExt('minoredit', array('parseinline')); - $watchthis = wfMsgExt('watchthis', array('parseinline')); $cancel = $sk->makeKnownLink( $this->mTitle->getPrefixedText(), wfMsgExt('cancel', array('parseinline')) ); @@ -1041,31 +1074,10 @@ class EditPage { # Already watched $this->watchthis = true; } - - if( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true; - } - - $minoredithtml = ''; - - if ( $wgUser->isAllowed('minoredit') ) { - $minoredithtml = - "<input tabindex='3' type='checkbox' value='1' name='wpMinoredit'".($this->minoredit?" checked='checked'":""). - " accesskey='".wfMsg('accesskey-minoredit')."' id='wpMinoredit' />\n". - "<label for='wpMinoredit' title='".wfMsg('tooltip-minoredit')."'>{$minor}</label>\n"; - } - $watchhtml = ''; - - if ( $wgUser->isLoggedIn() ) { - $watchhtml = "<input tabindex='4' type='checkbox' name='wpWatchthis'". - ($this->watchthis?" checked='checked'":""). - " accesskey=\"".htmlspecialchars(wfMsg('accesskey-watch'))."\" id='wpWatchthis' />\n". - "<label for='wpWatchthis' title=\"" . - htmlspecialchars(wfMsg('tooltip-watch'))."\">{$watchthis}</label>\n"; + if( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true; } - $checkboxhtml = $minoredithtml . $watchhtml; - $wgOut->addHTML( $this->editFormPageTop ); if ( $wgUser->getOption( 'previewontop' ) ) { @@ -1132,68 +1144,18 @@ class EditPage { } } - $temp = array( - 'id' => 'wpSave', - 'name' => 'wpSave', - 'type' => 'submit', - 'tabindex' => '5', - 'value' => wfMsg('savearticle'), - 'accesskey' => wfMsg('accesskey-save'), - 'title' => wfMsg('tooltip-save'), - ); - $buttons['save'] = wfElement('input', $temp, ''); - $temp = array( - 'id' => 'wpDiff', - 'name' => 'wpDiff', - 'type' => 'submit', - 'tabindex' => '7', - 'value' => wfMsg('showdiff'), - 'accesskey' => wfMsg('accesskey-diff'), - 'title' => wfMsg('tooltip-diff'), - ); - $buttons['diff'] = wfElement('input', $temp, ''); + $tabindex = 2; - global $wgLivePreview; - if ( $wgLivePreview && $wgUser->getOption( 'uselivepreview' ) ) { - $temp = array( - 'id' => 'wpPreview', - 'name' => 'wpPreview', - 'type' => 'submit', - 'tabindex' => '6', - 'value' => wfMsg('showpreview'), - 'accesskey' => '', - 'title' => wfMsg('tooltip-preview'), - 'style' => 'display: none;', - ); - $buttons['preview'] = wfElement('input', $temp, ''); - $temp = array( - 'id' => 'wpLivePreview', - 'name' => 'wpLivePreview', - 'type' => 'submit', - 'tabindex' => '6', - 'value' => wfMsg('showlivepreview'), - 'accesskey' => wfMsg('accesskey-preview'), - 'title' => '', - 'onclick' => $this->doLivePreviewScript(), - ); - $buttons['live'] = wfElement('input', $temp, ''); - } else { - $temp = array( - 'id' => 'wpPreview', - 'name' => 'wpPreview', - 'type' => 'submit', - 'tabindex' => '6', - 'value' => wfMsg('showpreview'), - 'accesskey' => wfMsg('accesskey-preview'), - 'title' => wfMsg('tooltip-preview'), - ); - $buttons['preview'] = wfElement('input', $temp, ''); - $buttons['live'] = ''; - } + $checkboxes = self::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() - ? "" - : "<input type='hidden' name=\"safemode\" value='1' />\n"; + ? '' : Xml::hidden( 'safemode', '1' ); $wgOut->addHTML( <<<END {$toolbar} @@ -1205,6 +1167,8 @@ END 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 $wgOut->addHTML( " <input type='hidden' value=\"" . htmlspecialchars( $this->section ) . "\" name=\"wpSection\" /> @@ -1236,10 +1200,7 @@ END $wgOut->addHTML( "<div class='editButtons'> - {$buttons['save']} - {$buttons['preview']} - {$buttons['live']} - {$buttons['diff']} +{$buttonshtml} <span class='editHelp'>{$cancel} | {$edithelp}</span> </div><!-- editButtons --> </div><!-- editOptions -->"); @@ -1282,7 +1243,7 @@ END if( $this->missingSummary ) { $wgOut->addHTML( "<input type=\"hidden\" name=\"wpIgnoreBlankSummary\" value=\"1\" />\n" ); } - + # For a bit more sophisticated detection of blank summaries, hash the # automatic one and pass that in a hidden field. $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); @@ -1308,7 +1269,7 @@ END } else { $wgOut->addHTML( '<div id="wikiPreview"></div>' ); } - + if ( $this->formtype == 'diff') { $wgOut->addHTML( $this->getDiff() ); } @@ -1361,7 +1322,7 @@ END } function getLastDelete() { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $fname = 'EditPage::getLastDelete'; $res = $dbr->select( array( 'logging', 'user' ), @@ -1425,7 +1386,7 @@ END # don't parse user css/js, show message about preview # XXX: stupid php bug won't let us use $wgTitle->isCssJsSubpage() here - + if ( $this->isCssJsSubpage ) { if(preg_match("/\\.css$/", $wgTitle->getText() ) ) { $previewtext = wfMsg('usercsspreview'); @@ -1469,16 +1430,16 @@ END function blockedPage() { global $wgOut, $wgUser; $wgOut->blockedPage( false ); # Standard block notice on the top, don't 'return' - + # If the user made changes, preserve them when showing the markup - # (This happens when a user is blocked during edit, for instance) + # (This happens when a user is blocked during edit, for instance) $first = $this->firsttime || ( !$this->save && $this->textbox1 == '' ); if( $first ) { $source = $this->mTitle->exists() ? $this->getContent() : false; } else { $source = $this->textbox1; } - + # Spit out the source or the user's modified version if( $source !== false ) { $rows = $wgUser->getOption( 'rows' ); @@ -1496,14 +1457,14 @@ END function userNotLoggedInPage() { global $wgUser, $wgOut; $skin = $wgUser->getSkin(); - + $loginTitle = SpecialPage::getTitleFor( 'Userlogin' ); $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $this->mTitle->getPrefixedUrl() ); - + $wgOut->setPageTitle( wfMsg( 'whitelistedittitle' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); - + $wgOut->addHtml( wfMsgWikiHtml( 'whitelistedittext', $loginLink ) ); $wgOut->returnToMain( false, $this->mTitle->getPrefixedUrl() ); } @@ -1519,12 +1480,27 @@ END $wgOut->setPageTitle( wfMsg( 'confirmedittitle' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); - + $wgOut->addWikiText( wfMsg( 'confirmedittext' ) ); $wgOut->returnToMain( false ); } /** + * Creates a basic error page which informs the user that + * they have attempted to edit a nonexistant section. + */ + function noSuchSectionPage() { + global $wgOut; + + $wgOut->setPageTitle( wfMsg( 'nosuchsectiontitle' ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->setArticleRelated( false ); + + $wgOut->addWikiText( wfMsg( 'nosuchsectiontext', $this->section ) ); + $wgOut->returnToMain( false ); + } + + /** * Produce the stock "your edit contains spam" page * * @param $match Text which triggered one or more filters @@ -1539,7 +1515,7 @@ END $wgOut->addWikiText( wfMsg( 'spamprotectiontext' ) ); if ( $match ) $wgOut->addWikiText( wfMsg( 'spamprotectionmatch', "<nowiki>{$match}</nowiki>" ) ); - + $wgOut->returnToMain( false ); } @@ -1551,7 +1527,7 @@ END $fname = 'EditPage::mergeChangesInto'; wfProfileIn( $fname ); - $db =& wfGetDB( DB_MASTER ); + $db = wfGetDB( DB_MASTER ); // This is the revision the editor started from $baseRevision = Revision::loadFromTimestamp( @@ -1645,90 +1621,102 @@ END * can figure out a way to make them work in IE. However, we should make * sure these keys are not defined on the edit page. */ - $toolarray=array( - array( 'image'=>'button_bold.png', - 'open' => '\\\'\\\'\\\'', - 'close' => '\\\'\\\'\\\'', - 'sample'=> wfMsg('bold_sample'), - 'tip' => wfMsg('bold_tip'), - 'key' => 'B' - ), - array( 'image'=>'button_italic.png', - 'open' => '\\\'\\\'', - 'close' => '\\\'\\\'', - 'sample'=> wfMsg('italic_sample'), - 'tip' => wfMsg('italic_tip'), - 'key' => 'I' - ), - array( 'image'=>'button_link.png', - 'open' => '[[', - 'close' => ']]', - 'sample'=> wfMsg('link_sample'), - 'tip' => wfMsg('link_tip'), - 'key' => 'L' - ), - array( 'image'=>'button_extlink.png', - 'open' => '[', - 'close' => ']', - 'sample'=> wfMsg('extlink_sample'), - 'tip' => wfMsg('extlink_tip'), - 'key' => 'X' - ), - array( 'image'=>'button_headline.png', - 'open' => "\\n== ", - 'close' => " ==\\n", - 'sample'=> wfMsg('headline_sample'), - 'tip' => wfMsg('headline_tip'), - 'key' => 'H' - ), - array( 'image'=>'button_image.png', - 'open' => '[['.$wgContLang->getNsText(NS_IMAGE).":", - 'close' => ']]', - 'sample'=> wfMsg('image_sample'), - 'tip' => wfMsg('image_tip'), - 'key' => 'D' - ), - array( 'image' =>'button_media.png', - 'open' => '[['.$wgContLang->getNsText(NS_MEDIA).':', - 'close' => ']]', - 'sample'=> wfMsg('media_sample'), - 'tip' => wfMsg('media_tip'), - 'key' => 'M' - ), - array( 'image' =>'button_math.png', - 'open' => "<math>", - 'close' => "<\\/math>", - 'sample'=> wfMsg('math_sample'), - 'tip' => wfMsg('math_tip'), - 'key' => 'C' - ), - array( 'image' =>'button_nowiki.png', - 'open' => "<nowiki>", - 'close' => "<\\/nowiki>", - 'sample'=> wfMsg('nowiki_sample'), - 'tip' => wfMsg('nowiki_tip'), - 'key' => 'N' - ), - array( 'image' =>'button_sig.png', - 'open' => '--~~~~', - 'close' => '', - 'sample'=> '', - 'tip' => wfMsg('sig_tip'), - 'key' => 'Y' - ), - array( 'image' =>'button_hr.png', - 'open' => "\\n----\\n", - 'close' => '', - 'sample'=> '', - 'tip' => wfMsg('hr_tip'), - 'key' => 'R' - ) + $toolarray = array( + array( 'image' => 'button_bold.png', + 'id' => 'mw-editbutton-bold', + 'open' => '\\\'\\\'\\\'', + 'close' => '\\\'\\\'\\\'', + 'sample'=> wfMsg('bold_sample'), + 'tip' => wfMsg('bold_tip'), + 'key' => 'B' + ), + array( 'image' => 'button_italic.png', + 'id' => 'mw-editbutton-italic', + 'open' => '\\\'\\\'', + 'close' => '\\\'\\\'', + 'sample'=> wfMsg('italic_sample'), + 'tip' => wfMsg('italic_tip'), + 'key' => 'I' + ), + array( 'image' => 'button_link.png', + 'id' => 'mw-editbutton-link', + 'open' => '[[', + 'close' => ']]', + 'sample'=> wfMsg('link_sample'), + 'tip' => wfMsg('link_tip'), + 'key' => 'L' + ), + array( 'image' => 'button_extlink.png', + 'id' => 'mw-editbutton-extlink', + 'open' => '[', + 'close' => ']', + 'sample'=> wfMsg('extlink_sample'), + 'tip' => wfMsg('extlink_tip'), + 'key' => 'X' + ), + array( 'image' => 'button_headline.png', + 'id' => 'mw-editbutton-headline', + 'open' => "\\n== ", + 'close' => " ==\\n", + 'sample'=> wfMsg('headline_sample'), + 'tip' => wfMsg('headline_tip'), + 'key' => 'H' + ), + array( 'image' => 'button_image.png', + 'id' => 'mw-editbutton-image', + 'open' => '[['.$wgContLang->getNsText(NS_IMAGE).":", + 'close' => ']]', + 'sample'=> wfMsg('image_sample'), + 'tip' => wfMsg('image_tip'), + 'key' => 'D' + ), + array( 'image' => 'button_media.png', + 'id' => 'mw-editbutton-media', + 'open' => '[['.$wgContLang->getNsText(NS_MEDIA).':', + 'close' => ']]', + 'sample'=> wfMsg('media_sample'), + 'tip' => wfMsg('media_tip'), + 'key' => 'M' + ), + array( 'image' => 'button_math.png', + 'id' => 'mw-editbutton-math', + 'open' => "<math>", + 'close' => "<\\/math>", + 'sample'=> wfMsg('math_sample'), + 'tip' => wfMsg('math_tip'), + 'key' => 'C' + ), + array( 'image' => 'button_nowiki.png', + 'id' => 'mw-editbutton-nowiki', + 'open' => "<nowiki>", + 'close' => "<\\/nowiki>", + 'sample'=> wfMsg('nowiki_sample'), + 'tip' => wfMsg('nowiki_tip'), + 'key' => 'N' + ), + array( 'image' => 'button_sig.png', + 'id' => 'mw-editbutton-signature', + 'open' => '--~~~~', + 'close' => '', + 'sample'=> '', + 'tip' => wfMsg('sig_tip'), + 'key' => 'Y' + ), + array( 'image' => 'button_hr.png', + 'id' => 'mw-editbutton-hr', + 'open' => "\\n----\\n", + 'close' => '', + 'sample'=> '', + 'tip' => wfMsg('hr_tip'), + 'key' => 'R' + ) ); $toolbar = "<div id='toolbar'>\n"; $toolbar.="<script type='$wgJsMimeType'>\n/*<![CDATA[*/\n"; foreach($toolarray as $tool) { + $cssId = $tool['id']; $image=$wgStylePath.'/common/images/'.$tool['image']; $open=$tool['open']; $close=$tool['close']; @@ -1742,7 +1730,7 @@ END #$key = $tool["key"]; - $toolbar.="addButton('$image','$tip','$open','$close','$sample');\n"; + $toolbar.="addButton('$image','$tip','$open','$close','$sample','$cssId');\n"; } $toolbar.="/*]]>*/\n</script>"; @@ -1751,6 +1739,127 @@ END } /** + * Returns an array of html code of the following checkboxes: + * minor and watch + * + * @param $tabindex Current tabindex + * @param $skin Skin object + * @param $checked Array of checkbox => bool, where bool indicates the checked + * status of the checkbox + * + * @return array + */ + public static function getCheckboxes( &$tabindex, $skin, $checked ) { + global $wgUser; + + $checkboxes = array(); + + $checkboxes['minor'] = ''; + $minorLabel = wfMsgExt('minoredit', array('parseinline')); + if ( $wgUser->isAllowed('minoredit') ) { + $attribs = array( + 'tabindex' => ++$tabindex, + 'accesskey' => wfMsg( 'accesskey-minoredit' ), + 'id' => 'wpMinoredit', + ); + $checkboxes['minor'] = + Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) . + " <label for='wpMinoredit'".$skin->tooltipAndAccesskey('minoredit').">{$minorLabel}</label>"; + } + + $watchLabel = wfMsgExt('watchthis', array('parseinline')); + $checkboxes['watch'] = ''; + if ( $wgUser->isLoggedIn() ) { + $attribs = array( + 'tabindex' => ++$tabindex, + 'accesskey' => wfMsg( 'accesskey-watch' ), + 'id' => 'wpWatchthis', + ); + $checkboxes['watch'] = + Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) . + " <label for='wpWatchthis'".$skin->tooltipAndAccesskey('watch').">{$watchLabel}</label>"; + } + return $checkboxes; + } + + /** + * Returns an array of html code of the following buttons: + * save, diff, preview and live + * + * @param $tabindex Current tabindex + * + * @return array + */ + public function getEditButtons(&$tabindex) { + global $wgLivePreview, $wgUser; + + $buttons = array(); + + $temp = array( + 'id' => 'wpSave', + 'name' => 'wpSave', + 'type' => 'submit', + 'tabindex' => ++$tabindex, + 'value' => wfMsg('savearticle'), + 'accesskey' => wfMsg('accesskey-save'), + 'title' => wfMsg( 'tooltip-save' ).' ['.wfMsg( 'accesskey-save' ).']', + ); + $buttons['save'] = wfElement('input', $temp, ''); + + ++$tabindex; // use the same for preview and live preview + if ( $wgLivePreview && $wgUser->getOption( 'uselivepreview' ) ) { + $temp = array( + 'id' => 'wpPreview', + 'name' => 'wpPreview', + 'type' => 'submit', + 'tabindex' => $tabindex, + 'value' => wfMsg('showpreview'), + 'accesskey' => '', + 'title' => wfMsg( 'tooltip-preview' ).' ['.wfMsg( 'accesskey-preview' ).']', + 'style' => 'display: none;', + ); + $buttons['preview'] = wfElement('input', $temp, ''); + + $temp = array( + 'id' => 'wpLivePreview', + 'name' => 'wpLivePreview', + 'type' => 'submit', + 'tabindex' => $tabindex, + 'value' => wfMsg('showlivepreview'), + 'accesskey' => wfMsg('accesskey-preview'), + 'title' => '', + 'onclick' => $this->doLivePreviewScript(), + ); + $buttons['live'] = wfElement('input', $temp, ''); + } else { + $temp = array( + 'id' => 'wpPreview', + 'name' => 'wpPreview', + 'type' => 'submit', + 'tabindex' => $tabindex, + 'value' => wfMsg('showpreview'), + 'accesskey' => wfMsg('accesskey-preview'), + 'title' => wfMsg( 'tooltip-preview' ).' ['.wfMsg( 'accesskey-preview' ).']', + ); + $buttons['preview'] = wfElement('input', $temp, ''); + $buttons['live'] = ''; + } + + $temp = array( + 'id' => 'wpDiff', + 'name' => 'wpDiff', + 'type' => 'submit', + 'tabindex' => ++$tabindex, + 'value' => wfMsg('showdiff'), + 'accesskey' => wfMsg('accesskey-diff'), + 'title' => wfMsg( 'tooltip-diff' ).' ['.wfMsg( 'accesskey-diff' ).']', + ); + $buttons['diff'] = wfElement('input', $temp, ''); + + return $buttons; + } + + /** * Output preview text only. This can be sucked into the edit page * via JavaScript, and saves the server time rendering the skin as * well as theoretically being more robust on the client (doesn't @@ -1758,8 +1867,8 @@ END * failure, etc). * * @todo This doesn't include category or interlanguage links. - * Would need to enhance it a bit, maybe wrap them in XML - * or something... that might also require more skin + * Would need to enhance it a bit, <s>maybe wrap them in XML + * or something...</s> that might also require more skin * initialization, so check whether that's a problem. */ function livePreview() { @@ -1767,10 +1876,14 @@ END $wgOut->disable(); header( 'Content-type: text/xml; charset=utf-8' ); header( 'Cache-control: no-cache' ); - # FIXME - echo $this->getPreviewText( ); - /* To not shake screen up and down between preview and live-preview */ - echo "<br style=\"clear:both;\" />\n"; + + $s = + '<?xml version="1.0" encoding="UTF-8" ?>' . "\n" . + Xml::openElement( 'livepreview' ) . + Xml::element( 'preview', null, $this->getPreviewText() ) . + Xml::element( 'br', array( 'style' => 'clear: both;' ) ) . + Xml::closeElement( 'livepreview' ); + echo $s; } diff --git a/includes/Exception.php b/includes/Exception.php index ad7ec14a..4cf0b7ba 100644 --- a/includes/Exception.php +++ b/includes/Exception.php @@ -1,5 +1,9 @@ <?php +/** + * MediaWiki exception + * @addtogroup Exception + */ class MWException extends Exception { function useOutputPage() { @@ -12,6 +16,7 @@ class MWException extends Exception return is_object( $wgLang ); } + /** Get a message from i18n */ function msg( $key, $fallback /*[, params...] */ ) { $args = array_slice( func_get_args(), 2 ); if ( $this->useMessageCache() ) { @@ -21,6 +26,7 @@ class MWException extends Exception } } + /* If wgShowExceptionDetails, return a HTML message with a backtrace to the error. */ function getHTML() { global $wgShowExceptionDetails; if( $wgShowExceptionDetails ) { @@ -33,6 +39,7 @@ class MWException extends Exception } } + /* If wgShowExceptionDetails, return a text message with a backtrace to the error */ function getText() { global $wgShowExceptionDetails; if( $wgShowExceptionDetails ) { @@ -43,7 +50,8 @@ class MWException extends Exception "in LocalSettings.php to show detailed debugging information.</p>"; } } - + + /* Return titles of this error page */ function getPageTitle() { if ( $this->useMessageCache() ) { return wfMsg( 'internalerror' ); @@ -52,7 +60,10 @@ class MWException extends Exception return "$wgSitename error"; } } - + + /** Return the requested URL and point to file and line number from which the + * exception occured + */ function getLogMessage() { global $wgRequest; $file = $this->getFile(); @@ -60,7 +71,8 @@ class MWException extends Exception $message = $this->getMessage(); return $wgRequest->getRequestURL() . " Exception from line $line of $file: $message"; } - + + /** Output the exception report using HTML */ function reportHTML() { global $wgOut; if ( $this->useOutputPage() ) { @@ -78,11 +90,15 @@ class MWException extends Exception echo $this->htmlFooter(); } } - + + /** Print the exception report using text */ function reportText() { echo $this->getText(); } + /* Output a report about the exception and takes care of formatting. + * It will be either HTML or plain text based on $wgCommandLineMode. + */ function report() { global $wgCommandLineMode; if ( $wgCommandLineMode ) { @@ -125,6 +141,7 @@ class MWException extends Exception /** * Exception class which takes an HTML error message, and does not * produce a backtrace. Replacement for OutputPage::fatalError(). + * @addtogroup Exception */ class FatalError extends MWException { function getHTML() { @@ -136,6 +153,9 @@ class FatalError extends MWException { } } +/** + * @addtogroup Exception + */ class ErrorPageError extends MWException { public $title, $msg; @@ -203,7 +223,7 @@ function wfReportException( Exception $e ) { function wfExceptionHandler( $e ) { global $wgFullyInitialised; wfReportException( $e ); - + // Final cleanup, similar to wfErrorExit() if ( $wgFullyInitialised ) { try { diff --git a/includes/Exif.php b/includes/Exif.php index 0860d5f7..3a06ca1b 100644 --- a/includes/Exif.php +++ b/includes/Exif.php @@ -1,7 +1,6 @@ <?php /** - * @package MediaWiki - * @subpackage Metadata + * @addtogroup Media * * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com> * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason @@ -22,13 +21,12 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * - * @link http://exif.org/Exif2-2.PDF The Exif 2.2 specification - * @bug 1555, 1947 + * @see http://exif.org/Exif2-2.PDF The Exif 2.2 specification */ /** - * @package MediaWiki - * @subpackage Metadata + * @todo document (e.g. one-sentence class-overview description) + * @addtogroup Media */ class Exif { //@{ @@ -95,9 +93,9 @@ class Exif { var $basename; /** - * The private log to log to + * The private log to log to, e.g. 'exif' */ - var $log = 'exif'; + var $log = false; //@} @@ -106,7 +104,7 @@ class Exif { * * @param $file String: filename. */ - function Exif( $file ) { + function __construct( $file ) { /** * Page numbers here refer to pages in the EXIF 2.2 standard * @@ -563,7 +561,10 @@ class Exif { * @param $fname String: * @param $action Mixed: , default NULL. */ - function debug( $in, $fname, $action = NULL ) { + function debug( $in, $fname, $action = NULL ) { + if ( !$this->log ) { + return; + } $type = gettype( $in ); $class = ucfirst( __CLASS__ ); if ( $type === 'array' ) @@ -588,6 +589,9 @@ class Exif { * @param $io Boolean: Specify whether we're beginning or ending */ function debugFile( $fname, $io ) { + if ( !$this->log ) { + return; + } $class = ucfirst( __CLASS__ ); if ( $io ) { wfDebugLog( $this->log, "$class::$fname: begin processing: '{$this->basename}'\n" ); @@ -599,8 +603,8 @@ class Exif { } /** - * @package MediaWiki - * @subpackage Metadata + * @todo document (e.g. one-sentence class-overview description) + * @addtogroup Media */ class FormatExif { /** @@ -733,7 +737,7 @@ class FormatExif { case 'DateTimeDigitized': if( $val == '0000:00:00 00:00:00' ) { $tags[$tag] = wfMsg('exif-unknowndate'); - } elseif( preg_match( '/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/', $val ) ) { + } elseif( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/', $val ) ) { $tags[$tag] = $wgLang->timeanddate( wfTimestamp(TS_MW, $val) ); } break; diff --git a/includes/Export.php b/includes/Export.php index b7e0f9a1..9307795d 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -17,16 +17,15 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html + /** * - * @package MediaWiki - * @subpackage SpecialPage + * @addtogroup SpecialPage */ - class WikiExporter { var $list_authors = false ; # Return distinct author list (when not returning full history) var $author_list = "" ; - + const FULL = 0; const CURRENT = 1; @@ -44,14 +43,14 @@ class WikiExporter { * main query is still running. * * @param Database $db - * @param mixed $history one of WikiExporter::FULL or WikiExporter::CURRENT, or an + * @param mixed $history one of WikiExporter::FULL or WikiExporter::CURRENT, or an * associative array: * offset: non-inclusive offset at which to start the query * limit: maximum number of rows to return * dir: "asc" or "desc" timestamp order * @param int $buffer one of WikiExporter::BUFFER or WikiExporter::STREAM */ - function WikiExporter( &$db, $history = WikiExporter::CURRENT, + function __construct( &$db, $history = WikiExporter::CURRENT, $buffer = WikiExporter::BUFFER, $text = WikiExporter::TEXT ) { $this->db =& $db; $this->history = $history; @@ -140,7 +139,10 @@ class WikiExporter { $fname = "do_list_authors" ; wfProfileIn( $fname ); $this->author_list = "<contributors>"; - $sql = "SELECT DISTINCT rev_user_text,rev_user FROM {$page},{$revision} WHERE page_id=rev_page AND " . $cond ; + //rev_deleted + $nothidden = '(rev_deleted & '.Revision::DELETED_USER.') = 0'; + + $sql = "SELECT DISTINCT rev_user_text,rev_user FROM {$page},{$revision} WHERE page_id=rev_page AND $nothidden AND " . $cond ; $result = $this->db->query( $sql, $fname ); $resultset = $this->db->resultObject( $result ); while( $row = $resultset->fetchObject() ) { @@ -164,10 +166,10 @@ class WikiExporter { $page = $this->db->tableName( 'page' ); $revision = $this->db->tableName( 'revision' ); $text = $this->db->tableName( 'text' ); - + $order = 'ORDER BY page_id'; $limit = ''; - + if( $this->history == WikiExporter::FULL ) { $join = 'page_id=rev_page'; } elseif( $this->history == WikiExporter::CURRENT ) { @@ -185,7 +187,7 @@ class WikiExporter { $order .= ', rev_timestamp DESC'; } if ( !empty( $this->history['offset'] ) ) { - $join .= " AND rev_timestamp $op " . $this->db->addQuotes( + $join .= " AND rev_timestamp $op " . $this->db->addQuotes( $this->db->timestamp( $this->history['offset'] ) ); } if ( !empty( $this->history['limit'] ) ) { @@ -229,7 +231,7 @@ class WikiExporter { $result = $this->db->query( $sql, $fname ); $wrapper = $this->db->resultObject( $result ); $this->outputStream( $wrapper ); - + if ( $this->list_authors ) { $this->outputStream( $wrapper ); } @@ -279,6 +281,9 @@ class WikiExporter { } } +/** + * @addtogroup Dump + */ class XmlDumpWriter { /** @@ -461,6 +466,7 @@ class XmlDumpWriter { /** * Base class for output stream; prints to stdout or buffer or whereever. + * @addtogroup Dump */ class DumpOutput { function writeOpenStream( $string ) { @@ -494,6 +500,7 @@ class DumpOutput { /** * Stream outputter to send data to a file. + * @addtogroup Dump */ class DumpFileOutput extends DumpOutput { var $handle; @@ -511,6 +518,7 @@ class DumpFileOutput extends DumpOutput { * Stream outputter to send data to a file via some filter program. * Even if compression is available in a library, using a separate * program can allow us to make use of a multi-processor system. + * @addtogroup Dump */ class DumpPipeOutput extends DumpFileOutput { function DumpPipeOutput( $command, $file = null ) { @@ -523,6 +531,7 @@ class DumpPipeOutput extends DumpFileOutput { /** * Sends dump output via the gzip compressor. + * @addtogroup Dump */ class DumpGZipOutput extends DumpPipeOutput { function DumpGZipOutput( $file ) { @@ -532,6 +541,7 @@ class DumpGZipOutput extends DumpPipeOutput { /** * Sends dump output via the bgzip2 compressor. + * @addtogroup Dump */ class DumpBZip2Output extends DumpPipeOutput { function DumpBZip2Output( $file ) { @@ -541,6 +551,7 @@ class DumpBZip2Output extends DumpPipeOutput { /** * Sends dump output via the p7zip compressor. + * @addtogroup Dump */ class Dump7ZipOutput extends DumpPipeOutput { function Dump7ZipOutput( $file ) { @@ -558,6 +569,7 @@ class Dump7ZipOutput extends DumpPipeOutput { * Dump output filter class. * This just does output filtering and streaming; XML formatting is done * higher up, so be careful in what you do. + * @addtogroup Dump */ class DumpFilter { function DumpFilter( &$sink ) { @@ -603,6 +615,7 @@ class DumpFilter { /** * Simple dump output filter to exclude all talk pages. + * @addtogroup Dump */ class DumpNotalkFilter extends DumpFilter { function pass( $page ) { @@ -612,6 +625,7 @@ class DumpNotalkFilter extends DumpFilter { /** * Dump output filter to include or exclude pages in a given set of namespaces. + * @addtogroup Dump */ class DumpNamespaceFilter extends DumpFilter { var $invert = false; @@ -666,6 +680,7 @@ class DumpNamespaceFilter extends DumpFilter { /** * Dump output filter to include only the last revision in each page sequence. + * @addtogroup Dump */ class DumpLatestFilter extends DumpFilter { var $page, $pageString, $rev, $revString; @@ -697,6 +712,7 @@ class DumpLatestFilter extends DumpFilter { /** * Base class for output stream; prints to stdout or buffer or whereever. + * @addtogroup Dump */ class DumpMultiWriter { function DumpMultiWriter( $sinks ) { diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php index 14b55fdb..c8ed8bde 100644 --- a/includes/ExternalEdit.php +++ b/includes/ExternalEdit.php @@ -3,12 +3,10 @@ * License: Public domain * * @author Erik Moeller <moeller@scireview.de> - * @package MediaWiki */ /** * - * @package MediaWiki * * Support for external editors to modify both text and files * in external applications. It works as follows: MediaWiki @@ -22,7 +20,7 @@ class ExternalEdit { - function ExternalEdit ( $article, $mode ) { + function __construct( $article, $mode ) { global $wgInputEncoding; $this->mArticle =& $article; $this->mTitle =& $article->mTitle; diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php index 79f1a528..fb66b652 100644 --- a/includes/ExternalStore.php +++ b/includes/ExternalStore.php @@ -1,7 +1,6 @@ <?php /** * - * @package MediaWiki * * Constructor class for data kept in external repositories * diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php index 861a9939..7b4ffc2f 100644 --- a/includes/ExternalStoreDB.php +++ b/includes/ExternalStoreDB.php @@ -1,14 +1,12 @@ <?php /** * - * @package MediaWiki * * DB accessable external objects * */ -/** @package MediaWiki */ /** * External database storage will use one (or more) separate connection pools diff --git a/includes/ExternalStoreHttp.php b/includes/ExternalStoreHttp.php index daf62cc4..e6656986 100644 --- a/includes/ExternalStoreHttp.php +++ b/includes/ExternalStoreHttp.php @@ -1,7 +1,6 @@ <?php /** * - * @package MediaWiki * * Example class for HTTP accessable external objects * diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php index ae05385a..293bdaf0 100644 --- a/includes/FakeTitle.php +++ b/includes/FakeTitle.php @@ -14,7 +14,6 @@ class FakeTitle { function getInterwikiCached() { $this->error(); } function isLocal() { $this->error(); } function isTrans() { $this->error(); } - function touchArray( $titles, $timestamp = '' ) { $this->error(); } function getText() { $this->error(); } function getPartialURL() { $this->error(); } function getDBkey() { $this->error(); } @@ -41,6 +40,7 @@ class FakeTitle { function isProtected() { $this->error(); } function userIsWatching() { $this->error(); } function userCan() { $this->error(); } + function userCanCreate() { $this->error(); } function userCanEdit() { $this->error(); } function userCanMove() { $this->error(); } function isMovable() { $this->error(); } @@ -71,7 +71,6 @@ class FakeTitle { function moveOverExistingRedirect() { $this->error(); } function moveToNewTitle() { $this->error(); } function isValidMoveTarget() { $this->error(); } - function createRedirect() { $this->error(); } function getParentCategories() { $this->error(); } function getParentCategoryTree() { $this->error(); } function pageCond() { $this->error(); } diff --git a/includes/Feed.php b/includes/Feed.php index 5c14865d..ed4343c3 100644 --- a/includes/Feed.php +++ b/includes/Feed.php @@ -1,6 +1,5 @@ <?php -# Basic support for outputting syndication feeds in RSS, other formats -# + # Copyright (C) 2004 Brion Vibber <brion@pobox.com> # http://www.mediawiki.org/ # @@ -20,15 +19,13 @@ # http://www.gnu.org/copyleft/gpl.html /** + * Basic support for outputting syndication feeds in RSS, other formats. * Contain a feed class as well as classes to build rss / atom ... feeds * Available feeds are defined in Defines.php - * @package MediaWiki */ - /** - * @todo document - * @package MediaWiki + * A base class for basic support for outputting syndication feeds in RSS and other formats. */ class FeedItem { /**#@+ @@ -45,7 +42,7 @@ class FeedItem { /**#@+ * @todo document */ - function FeedItem( $Title, $Description, $Url, $Date = '', $Author = '', $Comments = '' ) { + function __construct( $Title, $Description, $Url, $Date = '', $Author = '', $Comments = '' ) { $this->Title = $Title; $this->Description = $Description; $this->Url = $Url; @@ -77,8 +74,7 @@ class FeedItem { } /** - * @todo document - * @package MediaWiki + * @todo document (needs one-sentence top-level class description). */ class ChannelFeed extends FeedItem { /**#@+ @@ -160,8 +156,6 @@ class ChannelFeed extends FeedItem { /** * Generate a RSS feed - * @todo document - * @package MediaWiki */ class RSSFeed extends ChannelFeed { @@ -221,8 +215,6 @@ class RSSFeed extends ChannelFeed { /** * Generate an Atom feed - * @todo document - * @package MediaWiki */ class AtomFeed extends ChannelFeed { /** diff --git a/includes/FileStore.php b/includes/FileStore.php index 1fd35b01..dcec71c5 100644 --- a/includes/FileStore.php +++ b/includes/FileStore.php @@ -1,5 +1,8 @@ <?php +/** + * @todo document (needs one-sentence top-level class description). + */ class FileStore { const DELETE_ORIGINAL = 1; @@ -33,7 +36,7 @@ class FileStore { * suffer an uncaught error the lock will be released when the * connection is closed. * - * @fixme Probably only works on MySQL. Abstract to the Database class? + * @todo Probably only works on MySQL. Abstract to the Database class? */ static function lock() { global $wgDBtype; @@ -106,7 +109,7 @@ class FileStore { private function copyFile( $sourcePath, $destPath, $flags=0 ) { if( !file_exists( $sourcePath ) ) { // Abort! Abort! - throw new FSException( "missing source file '$sourcePath'\n" ); + throw new FSException( "missing source file '$sourcePath'" ); } $transaction = new FSTransaction(); @@ -125,7 +128,7 @@ class FileStore { if( !$ok ) { throw new FSException( - "failed to create directory for '$destPath'\n" ); + "failed to create directory for '$destPath'" ); } } @@ -138,7 +141,7 @@ class FileStore { $transaction->addRollback( FSTransaction::DELETE_FILE, $destPath ); } else { throw new FSException( - __METHOD__." failed to copy '$sourcePath' to '$destPath'\n" ); + __METHOD__." failed to copy '$sourcePath' to '$destPath'" ); } } @@ -175,7 +178,7 @@ class FileStore { * @throws FSException if file can't be deleted * @return FSTransaction * - * @fixme Might be worth preliminary permissions check + * @todo Might be worth preliminary permissions check */ static function deleteFile( $path ) { if( file_exists( $path ) ) { @@ -368,6 +371,9 @@ class FSTransaction { } } +/** + * @addtogroup Exception + */ class FSException extends MWException { } ?> diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index de07b321..1ffde741 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -1,8 +1,11 @@ <?php +if ( !defined( 'MEDIAWIKI' ) ) { + die( "This file is part of MediaWiki, it is not a valid entry point" ); +} + /** * Global functions used everywhere - * @package MediaWiki */ /** @@ -19,9 +22,9 @@ $wgTotalViews = -1; $wgTotalEdits = -1; -require_once( 'LogPage.php' ); -require_once( 'normal/UtfNormalUtil.php' ); -require_once( 'XmlFunctions.php' ); +require_once dirname(__FILE__) . '/LogPage.php'; +require_once dirname(__FILE__) . '/normal/UtfNormalUtil.php'; +require_once dirname(__FILE__) . '/XmlFunctions.php'; /** * Compatibility functions @@ -57,6 +60,30 @@ if ( !function_exists( 'mb_substr' ) ) { } } +if ( !function_exists( 'mb_strlen' ) ) { + /** + * Fallback implementation of mb_strlen, hardcoded to UTF-8. + * @param string $str + * @param string $enc optional encoding; ignored + * @return int + */ + function mb_strlen( $str, $enc="" ) { + $counts = count_chars( $str ); + $total = 0; + + // Count ASCII bytes + for( $i = 0; $i < 0x80; $i++ ) { + $total += $counts[$i]; + } + + // Count multibyte sequence heads + for( $i = 0xc0; $i < 0xff; $i++ ) { + $total += $counts[$i]; + } + return $total; + } +} + if ( !function_exists( 'array_diff_key' ) ) { /** * Exists in PHP 5.1.0+ @@ -168,7 +195,7 @@ function wfDebug( $text, $logonly = false ) { # Strip unprintables; they can switch terminal modes when binary data # gets dumped, which is pretty annoying. $text = preg_replace( '![\x00-\x08\x0b\x0c\x0e-\x1f]!', ' ', $text ); - @error_log( $text, 3, $wgDebugLogFile ); + wfErrorLog( $text, $wgDebugLogFile ); } } @@ -187,7 +214,7 @@ function wfDebugLog( $logGroup, $text, $public = true ) { if( isset( $wgDebugLogGroups[$logGroup] ) ) { $time = wfTimestamp( TS_DB ); $wiki = wfWikiID(); - @error_log( "$time $wiki: $text", 3, $wgDebugLogGroups[$logGroup] ); + wfErrorLog( "$time $wiki: $text", $wgDebugLogGroups[$logGroup] ); } else if ( $public === true ) { wfDebug( $text, true ); } @@ -198,15 +225,28 @@ function wfDebugLog( $logGroup, $text, $public = true ) { * @param $text String: database error message. */ function wfLogDBError( $text ) { - global $wgDBerrorLog; + global $wgDBerrorLog, $wgDBname; if ( $wgDBerrorLog ) { $host = trim(`hostname`); - $text = date('D M j G:i:s T Y') . "\t$host\t".$text; - error_log( $text, 3, $wgDBerrorLog ); + $text = date('D M j G:i:s T Y') . "\t$host\t$wgDBname\t$text"; + wfErrorLog( $text, $wgDBerrorLog ); } } /** + * Log to a file without getting "file size exceeded" signals + */ +function wfErrorLog( $text, $file ) { + wfSuppressWarnings(); + $exists = file_exists( $file ); + $size = $exists ? filesize( $file ) : false; + if ( !$exists || ( $size !== false && $size + strlen( $text ) < 0x7fffffff ) ) { + error_log( $text, 3, $file ); + } + wfRestoreWarnings(); +} + +/** * @todo document */ function wfLogProfilingData() { @@ -232,7 +272,7 @@ function wfLogProfilingData() { gmdate( 'YmdHis' ), $elapsed, urldecode( $wgRequest->getRequestURL() . $forward ) ); if ( '' != $wgDebugLogFile && ( $wgRequest->getVal('action') != 'raw' || $wgDebugRawPage ) ) { - error_log( $log . $prof, 3, $wgDebugLogFile ); + wfErrorLog( $log . $prof, $wgDebugLogFile ); } } } @@ -376,8 +416,11 @@ function wfMsgNoDBForContent( $key ) { * @return String: the requested message. */ function wfMsgReal( $key, $args, $useDB = true, $forContent=false, $transform = true ) { + $fname = 'wfMsgReal'; + wfProfileIn( $fname ); $message = wfMsgGetKey( $key, $useDB, $forContent, $transform ); $message = wfMsgReplaceArgs( $message, $args ); + wfProfileOut( $fname ); return $message; } @@ -515,11 +558,11 @@ function wfMsgWikiHtml( $key ) { * Returns message in the requested format * @param string $key Key of the message * @param array $options Processing rules: - * <i>parse<i>: parses wikitext to html - * <i>parseinline<i>: parses wikitext to html and removes the surrounding p's added by parser or tidy - * <i>escape<i>: filters message trough htmlspecialchars - * <i>replaceafter<i>: parameters are substituted after parsing or escaping - * <i>parsemag<i>: ?? + * <i>parse</i>: parses wikitext to html + * <i>parseinline</i>: parses wikitext to html and removes the surrounding p's added by parser or tidy + * <i>escape</i>: filters message trough htmlspecialchars + * <i>replaceafter</i>: parameters are substituted after parsing or escaping + * <i>parsemag</i>: transform the message using magic phrases */ function wfMsgExt( $key, $options ) { global $wgOut, $wgParser; @@ -569,7 +612,7 @@ function wfMsgExt( $key, $options ) { * Just like exit() but makes a note of it. * Commits open transactions except if the error parameter is set * - * @obsolete Please return control to the caller or throw an exception + * @deprecated Please return control to the caller or throw an exception */ function wfAbruptExit( $error = false ){ global $wgLoadBalancer; @@ -599,7 +642,7 @@ function wfAbruptExit( $error = false ){ } /** - * @obsolete Please return control the caller or throw an exception + * @deprecated Please return control the caller or throw an exception */ function wfErrorExit() { wfAbruptExit( true ); @@ -736,7 +779,7 @@ function wfBacktrace() { */ function wfShowingResults( $offset, $limit ) { global $wgLang; - return wfMsg( 'showingresults', $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ) ); + return wfMsgExt( 'showingresults', array( 'parseinline' ), $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ) ); } /** @@ -744,7 +787,7 @@ function wfShowingResults( $offset, $limit ) { */ function wfShowingResultsNum( $offset, $limit, $num ) { global $wgLang; - return wfMsg( 'showingresultsnum', $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ), $wgLang->formatNum( $num ) ); + return wfMsgExt( 'showingresultsnum', array( 'parseinline' ), $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ), $wgLang->formatNum( $num ) ); } /** @@ -941,6 +984,26 @@ function wfArrayToCGI( $array1, $array2 = NULL ) } /** + * Append a query string to an existing URL, which may or may not already + * have query string parameters already. If so, they will be combined. + * + * @param string $url + * @param string $query + * @return string + */ +function wfAppendQuery( $url, $query ) { + if( $query != '' ) { + if( false === strpos( $url, '?' ) ) { + $url .= '?'; + } else { + $url .= '&'; + } + $url .= $query; + } + return $url; +} + +/** * This is obsolete, use SquidUpdate::purge() * @deprecated */ @@ -1104,9 +1167,15 @@ function wfHttpError( $code, $label, $desc ) { * Note that some PHP configuration options may add output buffer * layers which cannot be removed; these are left in place. * - * @parameter bool $resetGzipEncoding + * @param bool $resetGzipEncoding */ function wfResetOutputBuffers( $resetGzipEncoding=true ) { + if( $resetGzipEncoding ) { + // Suppress Content-Encoding and Content-Length + // headers from 1.10+s wfOutputHandler + global $wgDisableOutputCompression; + $wgDisableOutputCompression = true; + } while( $status = ob_get_status() ) { if( $status['type'] == 0 /* PHP_OUTPUT_HANDLER_INTERNAL */ ) { // Probably from zlib.output_compression or other @@ -1332,7 +1401,7 @@ define('TS_ISO_8601', 4); /** * An Exif timestamp (YYYY:MM:DD HH:MM:SS) * - * @url http://exif.org/Exif2-2.PDF The Exif 2.2 spec, see page 28 for the + * @see http://exif.org/Exif2-2.PDF The Exif 2.2 spec, see page 28 for the * DateTime tag and page 36 for the DateTimeOriginal and * DateTimeDigitized tags. */ @@ -1624,6 +1693,7 @@ function wfMkdirParents( $fullDir, $mode = 0777 ) { foreach ( $createList as $dir ) { # use chmod to override the umask, as suggested by the PHP manual if ( !mkdir( $dir, $mode ) || !chmod( $dir, $mode ) ) { + wfDebugLog( 'mkdir', "Unable to create directory $dir\n" ); return false; } } @@ -1750,14 +1820,14 @@ function wfShellExec( $cmd, &$retval=null ) { } if ( php_uname( 's' ) == 'Linux' ) { - $time = ini_get( 'max_execution_time' ); + $time = intval( ini_get( 'max_execution_time' ) ); $mem = intval( $wgMaxShellMemory ); $filesize = intval( $wgMaxShellFileSize ); if ( $time > 0 && $mem > 0 ) { - $script = "$IP/bin/ulimit-tvf.sh"; + $script = "$IP/bin/ulimit4.sh"; if ( is_executable( $script ) ) { - $cmd = escapeshellarg( $script ) . " $time $mem $filesize $cmd"; + $cmd = escapeshellarg( $script ) . " $time $mem $filesize " . escapeshellarg( $cmd ); } } } elseif ( php_uname( 's' ) == 'Windows NT' ) { @@ -1844,23 +1914,83 @@ function wfBaseName( $path ) { } /** + * Generate a relative path name to the given file. + * May explode on non-matching case-insensitive paths, + * funky symlinks, etc. + * + * @param string $path Absolute destination path including target filename + * @param string $from Absolute source path, directory only + * @return string + */ +function wfRelativePath( $path, $from ) { + // Normalize mixed input on Windows... + $path = str_replace( '/', DIRECTORY_SEPARATOR, $path ); + $from = str_replace( '/', DIRECTORY_SEPARATOR, $from ); + + $pieces = explode( DIRECTORY_SEPARATOR, dirname( $path ) ); + $against = explode( DIRECTORY_SEPARATOR, $from ); + + // Trim off common prefix + while( count( $pieces ) && count( $against ) + && $pieces[0] == $against[0] ) { + array_shift( $pieces ); + array_shift( $against ); + } + + // relative dots to bump us to the parent + while( count( $against ) ) { + array_unshift( $pieces, '..' ); + array_shift( $against ); + } + + array_push( $pieces, wfBaseName( $path ) ); + + return implode( DIRECTORY_SEPARATOR, $pieces ); +} + +/** * Make a URL index, appropriate for the el_index field of externallinks. */ function wfMakeUrlIndex( $url ) { - wfSuppressWarnings(); + global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php $bits = parse_url( $url ); + wfSuppressWarnings(); wfRestoreWarnings(); - if ( !$bits || $bits['scheme'] !== 'http' ) { + if ( !$bits ) { return false; } + // most of the protocols are followed by ://, but mailto: and sometimes news: not, check for it + $delimiter = ''; + if ( in_array( $bits['scheme'] . '://' , $wgUrlProtocols ) ) { + $delimiter = '://'; + } elseif ( in_array( $bits['scheme'] .':' , $wgUrlProtocols ) ) { + $delimiter = ':'; + // parse_url detects for news: and mailto: the host part of an url as path + // We have to correct this wrong detection + if ( isset ( $bits['path'] ) ) { + $bits['host'] = $bits['path']; + $bits['path'] = ''; + } + } else { + return false; + } + // Reverse the labels in the hostname, convert to lower case - $reversedHost = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) ); + // For emails reverse domainpart only + if ( $bits['scheme'] == 'mailto' ) { + $mailparts = explode( '@', $bits['host'] ); + $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) ); + $reversedHost = $domainpart . '@' . $mailparts[0]; + } else { + $reversedHost = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) ); + } // Add an extra dot to the end if ( substr( $reversedHost, -1, 1 ) !== '.' ) { $reversedHost .= '.'; } // Reconstruct the pseudo-URL - $index = "http://$reversedHost"; + $prot = $bits['scheme']; + $index = "$prot$delimiter$reversedHost"; // Leave out user and password. Add the port, path, query and fragment if ( isset( $bits['port'] ) ) $index .= ':' . $bits['port']; if ( isset( $bits['path'] ) ) { @@ -1908,9 +2038,11 @@ function wfExplodeMarkup( $separator, $text ) { * @param $sourceBase int 2-36 * @param $destBase int 2-36 * @param $pad int 1 or greater + * @param $lowercase bool * @return string or false on invalid input */ -function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1 ) { +function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1, $lowercase=true ) { + $input = strval( $input ); if( $sourceBase < 2 || $sourceBase > 36 || $destBase < 2 || @@ -1923,8 +2055,7 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1 ) { $input == '' ) { return false; } - - $digitChars = '0123456789abcdefghijklmnopqrstuvwxyz'; + $digitChars = ( $lowercase ) ? '0123456789abcdefghijklmnopqrstuvwxyz' : '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; $inDigits = array(); $outChars = ''; @@ -2024,7 +2155,7 @@ function wfIsLocalURL( $url ) { * Initialise php session */ function wfSetupSession() { - global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain; + global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain, $wgCookieSecure; if( $wgSessionsInMemcached ) { require_once( 'MemcachedSessions.php' ); } elseif( 'files' != ini_get( 'session.save_handler' ) ) { @@ -2032,7 +2163,7 @@ function wfSetupSession() { # application, it will end up failing. Try to recover. ini_set ( 'session.save_handler', 'files' ); } - session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain ); + session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure); session_cache_limiter( 'private, must-revalidate' ); @session_start(); } diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php index bda4720d..9a0b6a08 100644 --- a/includes/HTMLCacheUpdate.php +++ b/includes/HTMLCacheUpdate.php @@ -38,7 +38,7 @@ class HTMLCacheUpdate function doUpdate() { # Fetch the IDs $cond = $this->getToCondition(); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( $this->mTable, $this->getFromField(), $cond, __METHOD__ ); $resWrap = new ResultWrapper( $dbr, $res ); if ( $dbr->numRows( $res ) != 0 ) { @@ -136,7 +136,7 @@ class HTMLCacheUpdate return; } - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $timestamp = $dbw->timestamp(); $done = false; @@ -184,6 +184,9 @@ class HTMLCacheUpdate } } +/** + * @todo document (e.g. one-sentence top-level class description). + */ class HTMLCacheUpdateJob extends Job { var $table, $start, $end; @@ -218,7 +221,7 @@ class HTMLCacheUpdateJob extends Job { $conds[] = "$fromField <= {$this->end}"; } - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( $this->table, $fromField, $conds, __METHOD__ ); $update->invalidateIDs( new ResultWrapper( $dbr, $res ) ); $dbr->freeResult( $res ); diff --git a/includes/HTMLFileCache.php b/includes/HTMLFileCache.php index d85a4411..1d3778b2 100644 --- a/includes/HTMLFileCache.php +++ b/includes/HTMLFileCache.php @@ -1,8 +1,7 @@ <?php /** * Contain the HTMLFileCache class - * @package MediaWiki - * @subpackage Cache + * @addtogroup Cache */ /** @@ -16,7 +15,6 @@ * $wgUseFileCache * $wgFileCacheDirectory * $wgUseGzip - * @package MediaWiki */ class HTMLFileCache { var $mTitle, $mFileCache; diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php index 189e5c79..715c8c88 100644 --- a/includes/HTMLForm.php +++ b/includes/HTMLForm.php @@ -2,13 +2,11 @@ /** * This file contain a class to easily build HTML forms as well as custom * functions used by SpecialUserrights.php - * @package MediaWiki */ /** * Class to build various forms * - * @package MediaWiki * @author jeluf, hashar */ class HTMLForm { @@ -125,6 +123,7 @@ class HTMLForm { function HTMLSelectGroups($selectname, $selectmsg, $selected=array(), $multiple=false, $size=6, $reverse=false) { $groups = User::getAllGroups(); $out = htmlspecialchars( wfMsg( $selectmsg ) ); + $out .= "<br />"; if( $multiple ) { $attribs = array( @@ -134,6 +133,7 @@ function HTMLSelectGroups($selectname, $selectmsg, $selected=array(), $multiple= } else { $attribs = array( 'name' => $selectname ); } + $attribs['style'] = 'width: 100%'; $out .= wfElement( 'select', $attribs, null ); foreach( $groups as $group ) { diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php index a06b620d..9dfd6d61 100644 --- a/includes/HistoryBlob.php +++ b/includes/HistoryBlob.php @@ -1,82 +1,86 @@ <?php /** * - * @package MediaWiki */ /** * Pure virtual parent - * @package MediaWiki + * @todo document (needs a one-sentence top-level class description, that answers the question: "what is a HistoryBlob?") */ -class HistoryBlob +interface HistoryBlob { /** * setMeta and getMeta currently aren't used for anything, I just thought * they might be useful in the future. * @param $meta String: a single string. */ - function setMeta( $meta ) {} + public function setMeta( $meta ); /** * setMeta and getMeta currently aren't used for anything, I just thought * they might be useful in the future. * Gets the meta-value */ - function getMeta() {} + public function getMeta(); /** * Adds an item of text, returns a stub object which points to the item. * You must call setLocation() on the stub object before storing it to the * database */ - function addItem() {} + public function addItem( $text ); /** * Get item by hash */ - function getItem( $hash ) {} + public function getItem( $hash ); # Set the "default text" # This concept is an odd property of the current DB schema, whereby each text item has a revision # associated with it. The default text is the text of the associated revision. There may, however, # be other revisions in the same object - function setText() {} + public function setText( $text ); /** * Get default text. This is called from Revision::getRevisionText() */ - function getText() {} + function getText(); } /** * The real object - * @package MediaWiki + * @todo document (needs one-sentence top-level class description + function descriptions). */ -class ConcatenatedGzipHistoryBlob extends HistoryBlob +class ConcatenatedGzipHistoryBlob implements HistoryBlob { - /* private */ var $mVersion = 0, $mCompressed = false, $mItems = array(), $mDefaultHash = ''; - /* private */ var $mFast = 0, $mSize = 0; + public $mVersion = 0, $mCompressed = false, $mItems = array(), $mDefaultHash = ''; + public $mFast = 0, $mSize = 0; - function ConcatenatedGzipHistoryBlob() { + /** Constructor */ + public function ConcatenatedGzipHistoryBlob() { if ( !function_exists( 'gzdeflate' ) ) { throw new MWException( "Need zlib support to read or write this kind of history object (ConcatenatedGzipHistoryBlob)\n" ); } } + # + # HistoryBlob implementation: + # + /** @todo document */ - function setMeta( $metaData ) { + public function setMeta( $metaData ) { $this->uncompress(); $this->mItems['meta'] = $metaData; } /** @todo document */ - function getMeta() { + public function getMeta() { $this->uncompress(); return $this->mItems['meta']; } /** @todo document */ - function addItem( $text ) { + public function addItem( $text ) { $this->uncompress(); $hash = md5( $text ); $this->mItems[$hash] = $text; @@ -87,7 +91,7 @@ class ConcatenatedGzipHistoryBlob extends HistoryBlob } /** @todo document */ - function getItem( $hash ) { + public function getItem( $hash ) { $this->uncompress(); if ( array_key_exists( $hash, $this->mItems ) ) { return $this->mItems[$hash]; @@ -97,13 +101,29 @@ class ConcatenatedGzipHistoryBlob extends HistoryBlob } /** @todo document */ - function removeItem( $hash ) { + public function setText( $text ) { + $this->uncompress(); + $stub = $this->addItem( $text ); + $this->mDefaultHash = $stub->mHash; + } + + /** @todo document */ + public function getText() { + $this->uncompress(); + return $this->getItem( $this->mDefaultHash ); + } + + # HistoryBlob implemented. + + + /** @todo document */ + public function removeItem( $hash ) { $this->mSize -= strlen( $this->mItems[$hash] ); unset( $this->mItems[$hash] ); } /** @todo document */ - function compress() { + public function compress() { if ( !$this->mCompressed ) { $this->mItems = gzdeflate( serialize( $this->mItems ) ); $this->mCompressed = true; @@ -111,25 +131,13 @@ class ConcatenatedGzipHistoryBlob extends HistoryBlob } /** @todo document */ - function uncompress() { + public function uncompress() { if ( $this->mCompressed ) { $this->mItems = unserialize( gzinflate( $this->mItems ) ); $this->mCompressed = false; } } - /** @todo document */ - function getText() { - $this->uncompress(); - return $this->getItem( $this->mDefaultHash ); - } - - /** @todo document */ - function setText( $text ) { - $this->uncompress(); - $stub = $this->addItem( $text ); - $this->mDefaultHash = $stub->mHash; - } /** @todo document */ function __sleep() { @@ -145,7 +153,7 @@ class ConcatenatedGzipHistoryBlob extends HistoryBlob /** * Determines if this object is happy */ - function isHappy( $maxFactor, $factorThreshold ) { + public function isHappy( $maxFactor, $factorThreshold ) { if ( count( $this->mItems ) == 0 ) { return true; } @@ -179,7 +187,7 @@ $wgBlobCache = array(); /** - * @package MediaWiki + * @todo document (needs one-sentence top-level class description + some function descriptions). */ class HistoryBlobStub { var $mOldId, $mHash, $mRef; @@ -197,28 +205,28 @@ class HistoryBlobStub { $this->mOldId = $id; } - /** - * Sets the location (old_id) of the referring object - */ + /** + * Sets the location (old_id) of the referring object + */ function setReferrer( $id ) { $this->mRef = $id; } - /** - * Gets the location of the referring object - */ + /** + * Gets the location of the referring object + */ function getReferrer() { return $this->mRef; } /** @todo document */ function getText() { - $fname = 'HistoryBlob::getText'; + $fname = 'HistoryBlobStub::getText'; global $wgBlobCache; if( isset( $wgBlobCache[$this->mOldId] ) ) { $obj = $wgBlobCache[$this->mOldId]; } else { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'text', array( 'old_flags', 'old_text' ), array( 'old_id' => $this->mOldId ) ); if( !$row ) { return false; @@ -273,8 +281,6 @@ class HistoryBlobStub { * * Serialized HistoryBlobCurStub objects will be inserted into the text table * on conversion if $wgFastSchemaUpgrades is set to true. - * - * @package MediaWiki */ class HistoryBlobCurStub { var $mCurId; @@ -294,7 +300,7 @@ class HistoryBlobCurStub { /** @todo document */ function getText() { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'cur', array( 'cur_text' ), array( 'cur_id' => $this->mCurId ) ); if( !$row ) { return false; diff --git a/includes/Hooks.php b/includes/Hooks.php index 2eecfd72..b428b08d 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -18,7 +18,6 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * * @author Evan Prodromou <evan@wikitravel.org> - * @package MediaWiki * @see hooks.txt */ @@ -103,7 +102,7 @@ function wfRunHooks($event, $args = null) { if ( isset( $object ) ) { $func = get_class( $object ) . '::' . $method; $callback = array( $object, $method ); - } elseif ( false !== ( $pos = strpos( '::', $func ) ) ) { + } elseif ( false !== ( $pos = strpos( $func, '::' ) ) ) { $callback = array( substr( $func, 0, $pos ), substr( $func, $pos + 2 ) ); } else { $callback = $func; diff --git a/includes/IP.php b/includes/IP.php index edf4af7a..8a2756c9 100644 --- a/includes/IP.php +++ b/includes/IP.php @@ -1,8 +1,5 @@ <?php /* - * Collection of public static functions to play with IP address - * and IP blocks. - * * @Author "Ashar Voultoiz" <hashar@altern.org> * @License GPL v2 or later */ @@ -12,22 +9,231 @@ // An IP is made of 4 bytes from x00 to xFF which is d0 to d255 define( 'RE_IP_BYTE', '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])'); define( 'RE_IP_ADD' , RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE ); -// An IP block is an IP address and a prefix (d1 to d32) +// An IPv4 block is an IP address and a prefix (d1 to d32) define( 'RE_IP_PREFIX', '(3[0-2]|[12]?\d)'); define( 'RE_IP_BLOCK', RE_IP_ADD . '\/' . RE_IP_PREFIX); // For IPv6 canonicalization (NOT for strict validation; these are quite lax!) define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' ); define( 'RE_IPV6_GAP', ':(?:0+:)*(?::(?:0+:)*)?' ); define( 'RE_IPV6_V4_PREFIX', '0*' . RE_IPV6_GAP . '(?:ffff:)?' ); +// An IPv6 block is an IP address and a prefix (d1 to d128) +define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)'); +// An IPv6 IP is made up of 8 octets. However abbreviations like "::" can be used. This is lax! +define( 'RE_IPV6_ADD', '(:(:' . RE_IPV6_WORD . '){1,7}|' . RE_IPV6_WORD . '(:{1,2}' . RE_IPV6_WORD . '|::$){1,7})' ); +define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX ); +// This might be useful for regexps used elsewhere, matches any IPv6 or IPv6 address or network +define( 'IP_ADDRESS_STRING', RE_IP_ADD . '(\/' . RE_IP_PREFIX . '|)|' . RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)'); +/** + * A collection of public static functions to play with IP address + * and IP blocks. + */ class IP { + /** + * Given a string, determine if it as valid IP + * Unlike isValid(), this looks for networks too + * @param $ip IP address. + * @return string + */ + public static function isIPAddress( $ip ) { + if ( !$ip ) return false; + if ( is_array( $ip ) ) { + throw new MWException( "invalid value passed to " . __METHOD__ ); + } + // IPv6 IPs with two "::" strings are ambiguous and thus invalid + return preg_match( '/^' . IP_ADDRESS_STRING . '$/', $ip) && ( substr_count($ip, '::') < 2 ); + } + + public static function isIPv6( $ip ) { + if ( !$ip ) return false; + if( is_array( $ip ) ) { + throw new MWException( "invalid value passed to " . __METHOD__ ); + } + // IPv6 IPs with two "::" strings are ambiguous and thus invalid + return preg_match( '/^' . RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)$/', $ip) && ( substr_count($ip, '::') < 2); + } + + public static function isIPv4( $ip ) { + if ( !$ip ) return false; + return preg_match( '/^' . RE_IP_ADD . '(\/' . RE_IP_PREFIX . '|)$/', $ip); + } + + /** + * Given an IP address in dotted-quad notation, returns an IPv6 octet. + * See http://www.answers.com/topic/ipv4-compatible-address + * IPs with the first 92 bits as zeros are reserved from IPv6 + * @param $ip quad-dotted IP address. + * @return string + */ + public static function IPv4toIPv6( $ip ) { + if ( !$ip ) return null; + // Convert only if needed + if ( self::isIPv6( $ip ) ) return $ip; + // IPv4 CIDRs + if ( strpos( $ip, '/' ) !== false ) { + $parts = explode( '/', $ip, 2 ); + if ( count( $parts ) != 2 ) { + return false; + } + $network = self::toUnsigned( $parts[0] ); + if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 32 ) { + $bits = $parts[1] + 96; + return self::toOctet( $network ) . "/$bits"; + } else { + return false; + } + } + return self::toOctet( self::toUnsigned( $ip ) ); + } /** + * Given an IPv6 address in octet notation, returns an unsigned integer. + * @param $ip octet ipv6 IP address. + * @return string + */ + public static function toUnsigned6( $ip ) { + if ( !$ip ) return null; + $ip = explode(':', self::sanitizeIP( $ip ) ); + $r_ip = ''; + foreach ($ip as $v) { + $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT ); + } + $r_ip = wfBaseConvert( $r_ip, 16, 10 ); + return $r_ip; + } + + /** + * Given an IPv6 address in octet notation, returns the expanded octet. + * IPv4 IPs will be trimmed, thats it... + * @param $ip octet ipv6 IP address. + * @return string + */ + public static function sanitizeIP( $ip ) { + if ( !$ip ) return null; + // Trim and return IPv4 addresses + if ( self::isIPv4($ip) ) return trim($ip); + // Only IPv6 addresses can be expanded + if ( !self::isIPv6($ip) ) return $ip; + // Remove any whitespaces, convert to upper case + $ip = strtoupper( trim($ip) ); + // Expand zero abbreviations + if ( strpos( $ip, '::' ) !== false ) { + $ip = str_replace('::', str_repeat(':0', 8 - substr_count($ip, ':')) . ':', $ip); + } + // For IPs that start with "::", correct the final IP so that it starts with '0' and not ':' + if ( $ip[0] == ':' ) $ip = "0$ip"; + // Remove leading zereos from each bloc as needed + $ip = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip ); + return $ip; + } + + /** + * Given an unsigned integer, returns an IPv6 address in octet notation + * @param $ip integer IP address. + * @return string + */ + public static function toOctet( $ip_int ) { + // Convert to padded uppercase hex + $ip_hex = wfBaseConvert($ip_int, 10, 16, 32, false); + // Seperate into 8 octets + $ip_oct = substr( $ip_hex, 0, 4 ); + for ($n=1; $n < 8; $n++) { + $ip_oct .= ':' . substr($ip_hex, 4*$n, 4); + } + // NO leading zeroes + $ip_oct = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip_oct ); + return $ip_oct; + } + + /** + * Convert a network specification in IPv6 CIDR notation to an integer network and a number of bits + * @return array(string, int) + */ + public static function parseCIDR6( $range ) { + # Expand any IPv6 IP + $parts = explode( '/', IP::sanitizeIP( $range ), 2 ); + if ( count( $parts ) != 2 ) { + return array( false, false ); + } + $network = self::toUnsigned6( $parts[0] ); + if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 128 ) { + $bits = $parts[1]; + if ( $bits == 0 ) { + $network = 0; + } else { + # Native 32 bit functions WONT work here!!! + # Convert to a padded binary number + $network = wfBaseConvert( $network, 10, 2, 128 ); + # Truncate the last (128-$bits) bits and replace them with zeros + $network = str_pad( substr( $network, 0, $bits ), 128, 0, STR_PAD_RIGHT ); + # Convert back to an integer + $network = wfBaseConvert( $network, 2, 10 ); + } + } else { + $network = false; + $bits = false; + } + return array( $network, $bits ); + } + + /** + * Given a string range in a number of formats, return the start and end of + * the range in hexadecimal. For IPv6. + * + * Formats are: + * 2001:0db8:85a3::7344/96 CIDR + * 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range + * 2001:0db8:85a3::7344/96 Single IP + * @return array(string, int) + */ + public static function parseRange6( $range ) { + # Expand any IPv6 IP + $range = IP::sanitizeIP( $range ); + if ( strpos( $range, '/' ) !== false ) { + # CIDR + list( $network, $bits ) = self::parseCIDR6( $range ); + if ( $network === false ) { + $start = $end = false; + } else { + $start = wfBaseConvert( $network, 10, 16, 32, false ); + # Turn network to binary (again) + $end = wfBaseConvert( $network, 10, 2, 128 ); + # Truncate the last (128-$bits) bits and replace them with ones + $end = str_pad( substr( $end, 0, $bits ), 128, 1, STR_PAD_RIGHT ); + # Convert to hex + $end = wfBaseConvert( $end, 2, 16, 32, false ); + # see toHex() comment + $start = "v6-$start"; $end = "v6-$end"; + } + } elseif ( strpos( $range, '-' ) !== false ) { + # Explicit range + list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) ); + $start = self::toUnsigned6( $start ); $end = self::toUnsigned6( $end ); + if ( $start > $end ) { + $start = $end = false; + } else { + $start = wfBaseConvert( $start, 10, 16, 32, false ); + $end = wfBaseConvert( $end, 10, 16, 32, false ); + } + # see toHex() comment + $start = "v6-$start"; $end = "v6-$end"; + } else { + # Single IP + $start = $end = self::toHex( $range ); + } + if ( $start === false || $end === false ) { + return array( false, false ); + } else { + return array( $start, $end ); + } + } + + /** * Validate an IP address. * @return boolean True if it is valid. */ public static function isValid( $ip ) { - return preg_match( '/^' . RE_IP_ADD . '$/', $ip) ; + return ( preg_match( '/^' . RE_IP_ADD . '$/', $ip) || preg_match( '/^' . RE_IPV6_ADD . '$/', $ip) ); } /** @@ -44,7 +250,7 @@ class IP { * Comes from ProxyTools.php */ public static function isPublic( $ip ) { - $n = IP::toUnsigned( $ip ); + $n = self::toUnsigned( $ip ); if ( !$n ) { return false; } @@ -67,8 +273,8 @@ class IP { } foreach ( $privateRanges as $r ) { - $start = IP::toUnsigned( $r[0] ); - $end = IP::toUnsigned( $r[1] ); + $start = self::toUnsigned( $r[0] ); + $end = self::toUnsigned( $r[1] ); if ( $n >= $start && $n <= $end ) { return false; } @@ -80,15 +286,17 @@ class IP { * Split out an IP block as an array of 4 bytes and a mask, * return false if it can't be determined * - * @parameter $ip string A quad dotted IP address + * @param $ip string A quad dotted/octet IP address * @return array */ public static function toArray( $ipblock ) { $matches = array(); - if(! preg_match( '/^' . RE_IP_ADD . '(?:\/(?:'.RE_IP_PREFIX.'))?' . '$/', $ipblock, $matches ) ) { - return false; - } else { + if( preg_match( '/^' . RE_IP_ADD . '(?:\/(?:'.RE_IP_PREFIX.'))?' . '$/', $ipblock, $matches ) ) { + return $matches; + } else if ( preg_match( '/^' . RE_IPV6_ADD . '(?:\/(?:'.RE_IPV6_PREFIX.'))?' . '$/', $ipblock, $matches ) ) { return $matches; + } else { + return false; } } @@ -100,23 +308,29 @@ class IP { * function for an IPv6 address will be prefixed with "v6-", a non- * hexadecimal string which sorts after the IPv4 addresses. * - * @param $ip Quad dotted IP address. + * @param $ip Quad dotted/octet IP address. + * @return hexidecimal */ public static function toHex( $ip ) { $n = self::toUnsigned( $ip ); if ( $n !== false ) { - $n = sprintf( '%08X', $n ); + $n = ( self::isIPv6($ip) ) ? "v6-" . wfBaseConvert( $n, 10, 16, 32, false ) : wfBaseConvert( $n, 10, 16, 8, false ); } return $n; } /** - * Given an IP address in dotted-quad notation, returns an unsigned integer. + * Given an IP address in dotted-quad/octet notation, returns an unsigned integer. * Like ip2long() except that it actually works and has a consistent error return value. * Comes from ProxyTools.php * @param $ip Quad dotted IP address. + * @return integer */ public static function toUnsigned( $ip ) { + // Use IPv6 functions if needed + if ( self::isIPv6( $ip ) ) { + return self::toUnsigned6( $ip ); + } if ( $ip == '255.255.255.255' ) { $n = -1; } else { @@ -149,13 +363,14 @@ class IP { /** * Convert a network specification in CIDR notation to an integer network and a number of bits + * @return array(string, int) */ public static function parseCIDR( $range ) { $parts = explode( '/', $range, 2 ); if ( count( $parts ) != 2 ) { return array( false, false ); } - $network = IP::toSigned( $parts[0] ); + $network = self::toSigned( $parts[0] ); if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 32 ) { $bits = $parts[1]; if ( $bits == 0 ) { @@ -182,11 +397,20 @@ class IP { * 1.2.3.4/24 CIDR * 1.2.3.4 - 1.2.3.5 Explicit range * 1.2.3.4 Single IP + * + * 2001:0db8:85a3::7344/96 CIDR + * 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range + * 2001:0db8:85a3::7344 Single IP + * @return array(string, int) */ public static function parseRange( $range ) { + // Use IPv6 functions if needed + if ( self::isIPv6( $range ) ) { + return self::parseRange6( $range ); + } if ( strpos( $range, '/' ) !== false ) { # CIDR - list( $network, $bits ) = IP::parseCIDR( $range ); + list( $network, $bits ) = self::parseCIDR( $range ); if ( $network === false ) { $start = $end = false; } else { @@ -196,15 +420,16 @@ class IP { } elseif ( strpos( $range, '-' ) !== false ) { # Explicit range list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) ); + $start = self::toUnsigned( $start ); $end = self::toUnsigned( $end ); if ( $start > $end ) { $start = $end = false; } else { - $start = IP::toHex( $start ); - $end = IP::toHex( $end ); + $start = sprintf( '%08X', $start ); + $end = sprintf( '%08X', $end ); } } else { # Single IP - $start = $end = IP::toHex( $range ); + $start = $end = self::toHex( $range ); } if ( $start === false || $end === false ) { return array( false, false ); @@ -214,18 +439,15 @@ class IP { } /** - * Determine if a given integer IPv4 address is in a given CIDR network + * Determine if a given IPv4/IPv6 address is in a given CIDR network * @param $addr The address to check against the given range. * @param $range The range to check the given address against. * @return bool Whether or not the given address is in the given range. */ public static function isInRange( $addr, $range ) { - $unsignedIP = IP::toUnsigned($addr); - list( $start, $end ) = IP::parseRange($range); - - $start = hexdec($start); - $end = hexdec($end); - + // Convert to IPv6 if needed + $unsignedIP = self::toHex( $addr ); + list( $start, $end ) = self::parseRange( $range ); return (($unsignedIP >= $start) && ($unsignedIP <= $end)); } @@ -240,10 +462,11 @@ class IP { * @return valid dotted quad IPv4 address or null */ public static function canonicalize( $addr ) { - if ( IP::isValid( $addr ) ) + if ( self::isValid( $addr ) ) return $addr; // IPv6 loopback address + $m = array(); if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) return '127.0.0.1'; diff --git a/includes/Image.php b/includes/Image.php index 7a6442c3..09c2286e 100644 --- a/includes/Image.php +++ b/includes/Image.php @@ -1,6 +1,5 @@ <?php /** - * @package MediaWiki */ /** @@ -15,17 +14,24 @@ /** * Bump this number when serialized cache records may be incompatible. */ -define( 'MW_IMAGE_VERSION', 1 ); +define( 'MW_IMAGE_VERSION', 2 ); /** * Class to represent an image * * Provides methods to retrieve paths (physical, logical, URL), * to generate thumbnails or for uploading. - * @package MediaWiki + * + * @addtogroup Media */ class Image { + const DELETED_FILE = 1; + const DELETED_COMMENT = 2; + const DELETED_USER = 4; + const DELETED_RESTRICTED = 8; + const RENDER_NOW = 1; + /**#@+ * @private */ @@ -43,6 +49,7 @@ class Image $attr, # / $type, # MEDIATYPE_xxx (bitmap, drawing, audio...) $mime, # MIME type, determined by MimeMagic::guessMimeType + $extension, # The file extension (constructor) $size, # Size in bytes (loadFromXxx) $metadata, # Metadata $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx) @@ -81,18 +88,16 @@ class Image } $this->title =& $title; $this->name = $title->getDBkey(); - $this->metadata = serialize ( array() ) ; + $this->metadata = ''; $n = strrpos( $this->name, '.' ); $this->extension = Image::normalizeExtension( $n ? substr( $this->name, $n + 1 ) : '' ); $this->historyLine = 0; - $this->page = 1; $this->dataLoaded = false; } - /** * Normalize a file extension to the common form, and ensure it's clean. * Extensions with non-alphanumeric characters will be discarded. @@ -115,7 +120,7 @@ class Image return ''; } } - + /** * Get the memcached keys * Returns an array, first element is the local cache key, second is the shared cache key, if there is one @@ -264,32 +269,26 @@ class Image $this->mime = $magic->guessMimeType($this->imagePath,true); $this->type = $magic->getMediaType($this->imagePath,$this->mime); + $handler = MediaHandler::getHandler( $this->mime ); # Get size in bytes $this->size = filesize( $this->imagePath ); - $magic=& MimeMagic::singleton(); - - # Height and width - wfSuppressWarnings(); - if( $this->mime == 'image/svg' ) { - $gis = wfGetSVGsize( $this->imagePath ); - } elseif( $this->mime == 'image/vnd.djvu' ) { - $deja = new DjVuImage( $this->imagePath ); - $gis = $deja->getImageSize(); - } elseif ( !$magic->isPHPImageType( $this->mime ) ) { - # Don't try to get the width and height of sound and video files, that's bad for performance - $gis = false; + # Height, width and metadata + if ( $handler ) { + $gis = $handler->getImageSize( $this, $this->imagePath ); + $this->metadata = $handler->getMetadata( $this, $this->imagePath ); } else { - $gis = getimagesize( $this->imagePath ); + $gis = false; + $this->metadata = ''; } - wfRestoreWarnings(); wfDebug(__METHOD__.': '.$this->imagePath." loaded, ".$this->size." bytes, ".$this->mime.".\n"); } else { $this->mime = NULL; $this->type = MEDIATYPE_UNKNOWN; + $this->metadata = ''; wfDebug(__METHOD__.': '.$this->imagePath." NOT FOUND!\n"); } @@ -308,13 +307,6 @@ class Image # as ther's only one thread of execution, this should be safe anyway. $this->dataLoaded = true; - - if ( $this->mime == 'image/vnd.djvu' ) { - $this->metadata = $deja->retrieveMetaData(); - } else { - $this->metadata = serialize( $this->retrieveExifData( $this->imagePath ) ); - } - if ( isset( $gis['bits'] ) ) $this->bits = $gis['bits']; else $this->bits = 0; @@ -328,7 +320,7 @@ class Image global $wgUseSharedUploads, $wgSharedUploadDBname, $wgSharedUploadDBprefix, $wgContLang; wfProfileIn( __METHOD__ ); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $this->checkDBSchema($dbr); $row = $dbr->selectRow( 'image', @@ -341,15 +333,13 @@ class Image $this->loadFromRow( $row ); $this->imagePath = $this->getFullPath(); // Check for rows from a previous schema, quietly upgrade them - if ( is_null($this->type) ) { - $this->upgradeRow(); - } + $this->maybeUpgradeRow(); } elseif ( $wgUseSharedUploads && $wgSharedUploadDBname ) { # In case we're on a wgCapitalLinks=false wiki, we # capitalize the first letter of the filename before # looking it up in the shared repository. $name = $wgContLang->ucfirst($this->name); - $dbc =& wfGetDB( DB_SLAVE, 'commons' ); + $dbc = Image::getCommonsDB(); $row = $dbc->selectRow( "`$wgSharedUploadDBname`.{$wgSharedUploadDBprefix}image", array( @@ -364,9 +354,7 @@ class Image $this->loadFromRow( $row ); // Check for rows from a previous schema, quietly upgrade them - if ( is_null($this->type) ) { - $this->upgradeRow(); - } + $this->maybeUpgradeRow(); } } @@ -378,7 +366,7 @@ class Image $this->type = 0; $this->fileExists = false; $this->fromSharedDirectory = false; - $this->metadata = serialize ( array() ) ; + $this->metadata = ''; $this->mime = false; } @@ -405,9 +393,7 @@ class Image if (!$minor) $minor= "unknown"; $this->mime = $major.'/'.$minor; } - $this->metadata = $row->img_metadata; - if ( $this->metadata == "" ) $this->metadata = serialize ( array() ) ; $this->dataLoaded = true; } @@ -434,8 +420,21 @@ class Image } /** - * Metadata was loaded from the database, but the row had a marker indicating it needs to be - * upgraded from the 1.4 schema, which had no width, height, bits or type. Upgrade the row. + * Upgrade a row if it needs it + */ + function maybeUpgradeRow() { + if ( is_null($this->type) || $this->mime == 'image/svg' ) { + $this->upgradeRow(); + } else { + $handler = $this->getHandler(); + if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) { + $this->upgradeRow(); + } + } + } + + /** + * Fix assorted version-related problems with the image row by reloading it from the file */ function upgradeRow() { global $wgDBname, $wgSharedUploadDBname; @@ -451,17 +450,16 @@ class Image // Write to the other DB using selectDB, not database selectors // This avoids breaking replication in MySQL - $dbw =& wfGetDB( DB_MASTER, 'commons' ); - $dbw->selectDB( $wgSharedUploadDBname ); + $dbw = Image::getCommonsDB(); } else { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); } $this->checkDBSchema($dbw); list( $major, $minor ) = self::splitMime( $this->mime ); - wfDebug(__METHOD__.': upgrading '.$this->name." to 1.5 schema\n"); + wfDebug(__METHOD__.': upgrading '.$this->name." to the current schema\n"); $dbw->update( 'image', array( @@ -479,7 +477,7 @@ class Image } wfProfileOut( __METHOD__ ); } - + /** * Split an internet media type into its two components; if not * a two-part name, set the minor type to 'unknown'. @@ -554,23 +552,49 @@ class Image /** * Return the width of the image * - * Returns -1 if the file specified is not a known image type + * Returns false on error * @public */ - function getWidth() { + function getWidth( $page = 1 ) { $this->load(); - return $this->width; + if ( $this->isMultipage() ) { + $dim = $this->getHandler()->getPageDimensions( $this, $page ); + if ( $dim ) { + return $dim['width']; + } else { + return false; + } + } else { + return $this->width; + } } /** * Return the height of the image * - * Returns -1 if the file specified is not a known image type + * Returns false on error * @public */ - function getHeight() { + function getHeight( $page = 1 ) { $this->load(); - return $this->height; + if ( $this->isMultipage() ) { + $dim = $this->getHandler()->getPageDimensions( $this, $page ); + if ( $dim ) { + return $dim['height']; + } else { + return false; + } + } else { + return $this->height; + } + } + + /** + * Get handler-specific metadata + */ + function getMetadata() { + $this->load(); + return $this->metadata; } /** @@ -610,58 +634,10 @@ class Image * @todo remember the result of this check. */ function canRender() { - global $wgUseImageMagick, $wgDjvuRenderer; - - if( $this->getWidth()<=0 || $this->getHeight()<=0 ) return false; - - $mime= $this->getMimeType(); - - if (!$mime || $mime==='unknown' || $mime==='unknown/unknown') return false; - - #if it's SVG, check if there's a converter enabled - if ($mime === 'image/svg') { - global $wgSVGConverters, $wgSVGConverter; - - if ($wgSVGConverter && isset( $wgSVGConverters[$wgSVGConverter])) { - wfDebug( "Image::canRender: SVG is ready!\n" ); - return true; - } else { - wfDebug( "Image::canRender: SVG renderer missing\n" ); - } - } - - #image formats available on ALL browsers - if ( $mime === 'image/gif' - || $mime === 'image/png' - || $mime === 'image/jpeg' ) return true; - - #image formats that can be converted to the above formats - if ($wgUseImageMagick) { - #convertable by ImageMagick (there are more...) - if ( $mime === 'image/vnd.wap.wbmp' - || $mime === 'image/x-xbitmap' - || $mime === 'image/x-xpixmap' - #|| $mime === 'image/x-icon' #file may be split into multiple parts - || $mime === 'image/x-portable-anymap' - || $mime === 'image/x-portable-bitmap' - || $mime === 'image/x-portable-graymap' - || $mime === 'image/x-portable-pixmap' - #|| $mime === 'image/x-photoshop' #this takes a lot of CPU and RAM! - || $mime === 'image/x-rgb' - || $mime === 'image/x-bmp' - || $mime === 'image/tiff' ) return true; - } - else { - #convertable by the PHP GD image lib - if ( $mime === 'image/vnd.wap.wbmp' - || $mime === 'image/x-xbitmap' ) return true; - } - if ( $mime === 'image/vnd.djvu' && isset( $wgDjvuRenderer ) && $wgDjvuRenderer ) return true; - - return false; + $handler = $this->getHandler(); + return $handler && $handler->canRender(); } - /** * Return true if the file is of a type that can't be directly * rendered by typical browsers and needs to be re-rasterized. @@ -673,13 +649,8 @@ class Image * @return bool */ function mustRender() { - $mime= $this->getMimeType(); - - if ( $mime === "image/gif" - || $mime === "image/png" - || $mime === "image/jpeg" ) return false; - - return true; + $handler = $this->getHandler(); + return $handler && $handler->mustRender(); } /** @@ -745,15 +716,7 @@ class Image * @public */ function getEscapeLocalURL( $query=false) { - $this->getTitle(); - if ( $query === false ) { - if ( $this->page != 1 ) { - $query = 'page=' . $this->page; - } else { - $query = ''; - } - } - return $this->title->escapeLocalURL( $query ); + return $this->getTitle()->escapeLocalURL( $query ); } /** @@ -801,74 +764,83 @@ class Image * @todo document * @private */ - function thumbUrl( $width, $subdir='thumb') { + function thumbUrlFromName( $thumbName, $subdir = 'thumb' ) { global $wgUploadPath, $wgUploadBaseUrl, $wgSharedUploadPath; - global $wgSharedThumbnailScriptPath, $wgThumbnailScriptPath; + if($this->fromSharedDirectory) { + $base = ''; + $path = $wgSharedUploadPath; + } else { + $base = $wgUploadBaseUrl; + $path = $wgUploadPath; + } + if ( Image::isHashed( $this->fromSharedDirectory ) ) { + $hashdir = wfGetHashPath($this->name, $this->fromSharedDirectory) . + wfUrlencode( $this->name ); + } else { + $hashdir = ''; + } + $url = "{$base}{$path}/{$subdir}{$hashdir}/" . wfUrlencode( $thumbName ); + return $url; + } - // Generate thumb.php URL if possible - $script = false; - $url = false; + /** + * @deprecated Use $image->transform()->getUrl() or thumbUrlFromName() + */ + function thumbUrl( $width, $subdir = 'thumb' ) { + $name = $this->thumbName( array( 'width' => $width ) ); + if ( strval( $name ) !== '' ) { + return array( false, $this->thumbUrlFromName( $name, $subdir ) ); + } else { + return array( false, false ); + } + } + function getTransformScript() { + global $wgSharedThumbnailScriptPath, $wgThumbnailScriptPath; if ( $this->fromSharedDirectory ) { - if ( $wgSharedThumbnailScriptPath ) { - $script = $wgSharedThumbnailScriptPath; - } + $script = $wgSharedThumbnailScriptPath; } else { - if ( $wgThumbnailScriptPath ) { - $script = $wgThumbnailScriptPath; - } + $script = $wgThumbnailScriptPath; } if ( $script ) { - $url = $script . '?f=' . urlencode( $this->name ) . '&w=' . urlencode( $width ); - if( $this->mustRender() ) { - $url.= '&r=1'; - } + return "$script?f=" . urlencode( $this->name ); } else { - $name = $this->thumbName( $width ); - if($this->fromSharedDirectory) { - $base = ''; - $path = $wgSharedUploadPath; - } else { - $base = $wgUploadBaseUrl; - $path = $wgUploadPath; - } - if ( Image::isHashed( $this->fromSharedDirectory ) ) { - $url = "{$base}{$path}/{$subdir}" . - wfGetHashPath($this->name, $this->fromSharedDirectory) - . $this->name.'/'.$name; - $url = wfUrlencode( $url ); - } else { - $url = "{$base}{$path}/{$subdir}/{$name}"; - } + return false; } - return array( $script !== false, $url ); } /** - * Return the file name of a thumbnail of the specified width + * Get a ThumbnailImage which is the same size as the source + */ + function getUnscaledThumb( $page = false ) { + if ( $page ) { + $params = array( + 'page' => $page, + 'width' => $this->getWidth( $page ) + ); + } else { + $params = array( 'width' => $this->getWidth() ); + } + return $this->transform( $params ); + } + + /** + * Return the file name of a thumbnail with the specified parameters * - * @param integer $width Width of the thumbnail image - * @param boolean $shared Does the thumbnail come from the shared repository? + * @param array $params Handler-specific parameters * @private */ - function thumbName( $width ) { - $thumb = $width."px-".$this->name; - if ( $this->page != 1 ) { - $thumb = "page{$this->page}-$thumb"; + function thumbName( $params ) { + $handler = $this->getHandler(); + if ( !$handler ) { + return null; } - - if( $this->mustRender() ) { - if( $this->canRender() ) { - # Rasterize to PNG (for SVG vector images, etc) - $thumb .= '.png'; - } - else { - #should we use iconThumb here to get a symbolic thumbnail? - #or should we fail with an internal error? - return NULL; //can't make bitmap - } + list( $thumbExt, $thumbMime ) = self::getThumbType( $this->extension, $this->mime ); + $thumbName = $handler->makeParamString( $params ) . '-' . $this->name; + if ( $thumbExt != $this->extension ) { + $thumbName .= ".$thumbExt"; } - return $thumb; + return $thumbName; } /** @@ -887,9 +859,13 @@ class Image * @param integer $height maximum height of the image (optional) * @public */ - function createThumb( $width, $height=-1 ) { - $thumb = $this->getThumbnail( $width, $height ); - if( is_null( $thumb ) ) return ''; + function createThumb( $width, $height = -1 ) { + $params = array( 'width' => $width ); + if ( $height != -1 ) { + $params['height'] = $height; + } + $thumb = $this->transform( $params ); + if( is_null( $thumb ) || $thumb->isError() ) return ''; return $thumb->getUrl(); } @@ -907,149 +883,90 @@ class Image * * @return ThumbnailImage or null on failure * @public + * + * @deprecated use transform() */ function getThumbnail( $width, $height=-1, $render = true ) { - wfProfileIn( __METHOD__ ); - if ($this->canRender()) { - if ( $height > 0 ) { - $this->load(); - if ( $width > $this->width * $height / $this->height ) { - $width = wfFitBoxWidth( $this->width, $this->height, $height ); - } - } - if ( $render ) { - $thumb = $this->renderThumb( $width ); - } else { - // Don't render, just return the URL - if ( $this->validateThumbParams( $width, $height ) ) { - if ( !$this->mustRender() && $width == $this->width && $height == $this->height ) { - $url = $this->getURL(); - } else { - list( /* $isScriptUrl */, $url ) = $this->thumbUrl( $width ); - } - $thumb = new ThumbnailImage( $url, $width, $height ); - } else { - $thumb = null; - } - } - } else { - // not a bitmap or renderable image, don't try. - $thumb = $this->iconThumb(); + $params = array( 'width' => $width ); + if ( $height != -1 ) { + $params['height'] = $height; } - wfProfileOut( __METHOD__ ); - return $thumb; + $flags = $render ? self::RENDER_NOW : 0; + return $this->transform( $params, $flags ); } - + /** - * @return ThumbnailImage + * Transform a media file + * + * @param array $params An associative array of handler-specific parameters. Typical + * keys are width, height and page. + * @param integer $flags A bitfield, may contain self::RENDER_NOW to force rendering + * @return MediaTransformOutput */ - function iconThumb() { - global $wgStylePath, $wgStyleDirectory; + function transform( $params, $flags = 0 ) { + global $wgGenerateThumbnailOnParse, $wgUseSquid, $wgIgnoreImageErrors; - $try = array( 'fileicon-' . $this->extension . '.png', 'fileicon.png' ); - foreach( $try as $icon ) { - $path = '/common/images/icons/' . $icon; - $filepath = $wgStyleDirectory . $path; - if( file_exists( $filepath ) ) { - return new ThumbnailImage( $wgStylePath . $path, 120, 120 ); + wfProfileIn( __METHOD__ ); + do { + $handler = $this->getHandler(); + if ( !$handler || !$handler->canRender() ) { + // not a bitmap or renderable image, don't try. + $thumb = $this->iconThumb(); + break; } - } - return null; - } - /** - * Validate thumbnail parameters and fill in the correct height - * - * @param integer &$width Specified width (input/output) - * @param integer &$height Height (output only) - * @return false to indicate that an error should be returned to the user. - */ - function validateThumbParams( &$width, &$height ) { - global $wgSVGMaxSize, $wgMaxImageArea; - - $this->load(); + $script = $this->getTransformScript(); + if ( $script && !($flags & self::RENDER_NOW) ) { + // Use a script to transform on client request + $thumb = $handler->getScriptedTransform( $this, $script, $params ); + break; + } - if ( ! $this->exists() ) - { - # If there is no image, there will be no thumbnail - return false; - } - - $width = intval( $width ); - - # Sanity check $width - if( $width <= 0 || $this->width <= 0) { - # BZZZT - return false; - } + $normalisedParams = $params; + $handler->normaliseParams( $this, $normalisedParams ); + list( $thumbExt, $thumbMime ) = self::getThumbType( $this->extension, $this->mime ); + $thumbName = $this->thumbName( $normalisedParams ); + $thumbPath = wfImageThumbDir( $this->name, $this->fromSharedDirectory ) . "/$thumbName"; + $thumbUrl = $this->thumbUrlFromName( $thumbName ); - # Don't thumbnail an image so big that it will fill hard drives and send servers into swap - # JPEG has the handy property of allowing thumbnailing without full decompression, so we make - # an exception for it. - if ( $this->getMediaType() == MEDIATYPE_BITMAP && - $this->getMimeType() !== 'image/jpeg' && - $this->width * $this->height > $wgMaxImageArea ) - { - return false; - } + $this->migrateThumbFile( $thumbName ); - # Don't make an image bigger than the source, or wgMaxSVGSize for SVGs - if ( $this->mustRender() ) { - $width = min( $width, $wgSVGMaxSize ); - } elseif ( $width > $this->width - 1 ) { - $width = $this->width; - $height = $this->height; - return true; - } + if ( file_exists( $thumbPath ) ) { + $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); + break; + } - $height = round( $this->height * $width / $this->width ); - return true; + if ( !$wgGenerateThumbnailOnParse && !($flags & self::RENDER_NOW ) ) { + $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); + break; + } + $thumb = $handler->doTransform( $this, $thumbPath, $thumbUrl, $params ); + + // Ignore errors if requested + if ( !$thumb ) { + $thumb = null; + } elseif ( $thumb->isError() ) { + $this->lastError = $thumb->toText(); + if ( $wgIgnoreImageErrors && !($flags & self::RENDER_NOW) ) { + $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); + } + } + + if ( $wgUseSquid ) { + wfPurgeSquidServers( array( $thumbUrl ) ); + } + } while (false); + + wfProfileOut( __METHOD__ ); + return $thumb; } - + /** - * Create a thumbnail of the image having the specified width. - * The thumbnail will not be created if the width is larger than the - * image's width. Let the browser do the scaling in this case. - * The thumbnail is stored on disk and is only computed if the thumbnail - * file does not exist OR if it is older than the image. - * Returns an object which can return the pathname, URL, and physical - * pixel size of the thumbnail -- or null on failure. - * - * @return ThumbnailImage or null on failure - * @private + * Fix thumbnail files from 1.4 or before, with extreme prejudice */ - function renderThumb( $width, $useScript = true ) { - global $wgUseSquid, $wgThumbnailEpoch; - - wfProfileIn( __METHOD__ ); - - $this->load(); - $height = -1; - if ( !$this->validateThumbParams( $width, $height ) ) { - # Validation error - wfProfileOut( __METHOD__ ); - return null; - } - - if ( !$this->mustRender() && $width == $this->width && $height == $this->height ) { - # validateThumbParams (or the user) wants us to return the unscaled image - $thumb = new ThumbnailImage( $this->getURL(), $width, $height ); - wfProfileOut( __METHOD__ ); - return $thumb; - } - - list( $isScriptUrl, $url ) = $this->thumbUrl( $width ); - if ( $isScriptUrl && $useScript ) { - // Use thumb.php to render the image - $thumb = new ThumbnailImage( $url, $width, $height ); - wfProfileOut( __METHOD__ ); - return $thumb; - } - - $thumbName = $this->thumbName( $width, $this->fromSharedDirectory ); + function migrateThumbFile( $thumbName ) { $thumbDir = wfImageThumbDir( $this->name, $this->fromSharedDirectory ); - $thumbPath = $thumbDir.'/'.$thumbName; - + $thumbPath = "$thumbDir/$thumbName"; if ( is_dir( $thumbPath ) ) { // Directory where file should be // This happened occasionally due to broken migration code in 1.5 @@ -1062,254 +979,50 @@ class Image break; } } - // Code below will ask if it exists, and the answer is now no + // Doesn't exist anymore clearstatcache(); } - - $done = true; - if ( !file_exists( $thumbPath ) || - filemtime( $thumbPath ) < wfTimestamp( TS_UNIX, $wgThumbnailEpoch ) ) - { - // Create the directory if it doesn't exist - if ( is_file( $thumbDir ) ) { - // File where thumb directory should be, destroy if possible - @unlink( $thumbDir ); - } - wfMkdirParents( $thumbDir ); - - $oldThumbPath = wfDeprecatedThumbDir( $thumbName, 'thumb', $this->fromSharedDirectory ). - '/'.$thumbName; - $done = false; - - // Migration from old directory structure - if ( is_file( $oldThumbPath ) ) { - if ( filemtime($oldThumbPath) >= filemtime($this->imagePath) ) { - if ( file_exists( $thumbPath ) ) { - if ( !is_dir( $thumbPath ) ) { - // Old image in the way of rename - unlink( $thumbPath ); - } else { - // This should have been dealt with already - throw new MWException( "Directory where image should be: $thumbPath" ); - } - } - // Rename the old image into the new location - rename( $oldThumbPath, $thumbPath ); - $done = true; - } else { - unlink( $oldThumbPath ); - } - } - if ( !$done ) { - $this->lastError = $this->reallyRenderThumb( $thumbPath, $width, $height ); - if ( $this->lastError === true ) { - $done = true; - } elseif( $GLOBALS['wgIgnoreImageErrors'] ) { - // Log the error but output anyway. - // With luck it's a transitory error... - $done = true; - } - - # Purge squid - # This has to be done after the image is updated and present for all machines on NFS, - # or else the old version might be stored into the squid again - if ( $wgUseSquid ) { - $urlArr = array( $url ); - wfPurgeSquidServers($urlArr); - } - } - } - - if ( $done ) { - $thumb = new ThumbnailImage( $url, $width, $height, $thumbPath ); - } else { - $thumb = null; + if ( is_file( $thumbDir ) ) { + // File where directory should be + unlink( $thumbDir ); + // Doesn't exist anymore + clearstatcache(); } - wfProfileOut( __METHOD__ ); - return $thumb; - } // END OF function renderThumb + } /** - * Really render a thumbnail - * Call this only for images for which canRender() returns true. - * - * @param string $thumbPath Path to thumbnail - * @param int $width Desired width in pixels - * @param int $height Desired height in pixels - * @return bool True on error, false or error string on failure. - * @private + * Get a MediaHandler instance for this image */ - function reallyRenderThumb( $thumbPath, $width, $height ) { - global $wgSVGConverters, $wgSVGConverter; - global $wgUseImageMagick, $wgImageMagickConvertCommand; - global $wgCustomConvertCommand; - global $wgDjvuRenderer, $wgDjvuPostProcessor; - - $this->load(); - - $err = false; - $cmd = ""; - $retval = 0; - - if( $this->mime === "image/svg" ) { - #Right now we have only SVG - - global $wgSVGConverters, $wgSVGConverter; - if( isset( $wgSVGConverters[$wgSVGConverter] ) ) { - global $wgSVGConverterPath; - $cmd = str_replace( - array( '$path/', '$width', '$height', '$input', '$output' ), - array( $wgSVGConverterPath ? "$wgSVGConverterPath/" : "", - intval( $width ), - intval( $height ), - wfEscapeShellArg( $this->imagePath ), - wfEscapeShellArg( $thumbPath ) ), - $wgSVGConverters[$wgSVGConverter] ); - wfProfileIn( 'rsvg' ); - wfDebug( "reallyRenderThumb SVG: $cmd\n" ); - $err = wfShellExec( $cmd, $retval ); - wfProfileOut( 'rsvg' ); - } - } else { - if ( $this->mime === "image/vnd.djvu" && $wgDjvuRenderer ) { - // DJVU image - // The file contains several images. First, extract the - // page in hi-res, if it doesn't yet exist. Then, thumbnail - // it. - - $cmd = "{$wgDjvuRenderer} -page={$this->page} -size=${width}x${height} " . - wfEscapeShellArg( $this->imagePath ) . - " | {$wgDjvuPostProcessor} > " . wfEscapeShellArg($thumbPath); - wfProfileIn( 'ddjvu' ); - wfDebug( "reallyRenderThumb DJVU: $cmd\n" ); - $err = wfShellExec( $cmd, $retval ); - wfProfileOut( 'ddjvu' ); - - } elseif ( $wgUseImageMagick ) { - # use ImageMagick - - if ( $this->mime == 'image/jpeg' ) { - $quality = "-quality 80"; // 80% - } elseif ( $this->mime == 'image/png' ) { - $quality = "-quality 95"; // zlib 9, adaptive filtering - } else { - $quality = ''; // default - } - - # Specify white background color, will be used for transparent images - # in Internet Explorer/Windows instead of default black. - - # Note, we specify "-size {$width}" and NOT "-size {$width}x{$height}". - # It seems that ImageMagick has a bug wherein it produces thumbnails of - # the wrong size in the second case. - - $cmd = wfEscapeShellArg($wgImageMagickConvertCommand) . - " {$quality} -background white -size {$width} ". - wfEscapeShellArg($this->imagePath) . - // Coalesce is needed to scale animated GIFs properly (bug 1017). - ' -coalesce ' . - // For the -resize option a "!" is needed to force exact size, - // or ImageMagick may decide your ratio is wrong and slice off - // a pixel. - " -thumbnail " . wfEscapeShellArg( "{$width}x{$height}!" ) . - " -depth 8 " . - wfEscapeShellArg($thumbPath) . " 2>&1"; - wfDebug("reallyRenderThumb: running ImageMagick: $cmd\n"); - wfProfileIn( 'convert' ); - $err = wfShellExec( $cmd, $retval ); - wfProfileOut( 'convert' ); - } elseif( $wgCustomConvertCommand ) { - # Use a custom convert command - # Variables: %s %d %w %h - $src = wfEscapeShellArg( $this->imagePath ); - $dst = wfEscapeShellArg( $thumbPath ); - $cmd = $wgCustomConvertCommand; - $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames - $cmd = str_replace( '%h', $height, str_replace( '%w', $width, $cmd ) ); # Size - wfDebug( "reallyRenderThumb: Running custom convert command $cmd\n" ); - wfProfileIn( 'convert' ); - $err = wfShellExec( $cmd, $retval ); - wfProfileOut( 'convert' ); - } else { - # Use PHP's builtin GD library functions. - # - # First find out what kind of file this is, and select the correct - # input routine for this. - - $typemap = array( - 'image/gif' => array( 'imagecreatefromgif', 'palette', 'imagegif' ), - 'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', array( &$this, 'imageJpegWrapper' ) ), - 'image/png' => array( 'imagecreatefrompng', 'bits', 'imagepng' ), - 'image/vnd.wap.wmbp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ), - 'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ), - ); - if( !isset( $typemap[$this->mime] ) ) { - $err = 'Image type not supported'; - wfDebug( "$err\n" ); - return $err; - } - list( $loader, $colorStyle, $saveType ) = $typemap[$this->mime]; - - if( !function_exists( $loader ) ) { - $err = "Incomplete GD library configuration: missing function $loader"; - wfDebug( "$err\n" ); - return $err; - } - if( $colorStyle == 'palette' ) { - $truecolor = false; - } elseif( $colorStyle == 'truecolor' ) { - $truecolor = true; - } elseif( $colorStyle == 'bits' ) { - $truecolor = ( $this->bits > 8 ); - } + function getHandler() { + return MediaHandler::getHandler( $this->getMimeType() ); + } - $src_image = call_user_func( $loader, $this->imagePath ); - if ( $truecolor ) { - $dst_image = imagecreatetruecolor( $width, $height ); - } else { - $dst_image = imagecreate( $width, $height ); - } - imagecopyresampled( $dst_image, $src_image, - 0,0,0,0, - $width, $height, $this->width, $this->height ); - call_user_func( $saveType, $dst_image, $thumbPath ); - imagedestroy( $dst_image ); - imagedestroy( $src_image ); - } - } + /** + * Get a ThumbnailImage representing a file type icon + * @return ThumbnailImage + */ + function iconThumb() { + global $wgStylePath, $wgStyleDirectory; - # - # Check for zero-sized thumbnails. Those can be generated when - # no disk space is available or some other error occurs - # - if( file_exists( $thumbPath ) ) { - $thumbstat = stat( $thumbPath ); - if( $thumbstat['size'] == 0 || $retval != 0 ) { - wfDebugLog( 'thumbnail', - sprintf( 'Removing bad %d-byte thumbnail "%s"', - $thumbstat['size'], $thumbPath ) ); - unlink( $thumbPath ); + $try = array( 'fileicon-' . $this->extension . '.png', 'fileicon.png' ); + foreach( $try as $icon ) { + $path = '/common/images/icons/' . $icon; + $filepath = $wgStyleDirectory . $path; + if( file_exists( $filepath ) ) { + return new ThumbnailImage( $wgStylePath . $path, 120, 120 ); } } - if ( $retval != 0 ) { - wfDebugLog( 'thumbnail', - sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', - wfHostname(), $retval, trim($err), $cmd ) ); - return wfMsg( 'thumbnail_error', $err ); - } else { - return true; - } + return null; } + /** + * Get last thumbnailing error. + * Largely obsolete. + */ function getLastError() { return $this->lastError; } - function imageJpegWrapper( $dst_image, $thumbPath ) { - imageinterlace( $dst_image ); - imagejpeg( $dst_image, $thumbPath, 95 ); - } - /** * Get all thumbnail names previously generated for this image */ @@ -1319,16 +1032,17 @@ class Image $files = array(); $dir = wfImageThumbDir( $this->name, $shared ); - // This generates an error on failure, hence the @ - $handle = @opendir( $dir ); + if ( is_dir( $dir ) ) { + $handle = opendir( $dir ); - if ( $handle ) { - while ( false !== ( $file = readdir($handle) ) ) { - if ( $file{0} != '.' ) { - $files[] = $file; + if ( $handle ) { + while ( false !== ( $file = readdir($handle) ) ) { + if ( $file{0} != '.' ) { + $files[] = $file; + } } + closedir( $handle ); } - closedir( $handle ); } } else { $files = array(); @@ -1361,8 +1075,10 @@ class Image $urls = array(); foreach ( $files as $file ) { $m = array(); - if ( preg_match( '/^(\d+)px/', $file, $m ) ) { - list( /* $isScriptUrl */, $url ) = $this->thumbUrl( $m[1] ); + # Check that the base image name is part of the thumb name + # This is a basic sanity check to avoid erasing unrelated directories + if ( strpos( $file, $this->name ) !== false ) { + $url = $this->thumbUrlFromName( $file ); $urls[] = $url; @unlink( "$dir/$file" ); } @@ -1377,7 +1093,7 @@ class Image wfPurgeSquidServers( $urls ); } } - + /** * Purge the image description page, but don't go after * pages using the image. Use when modifying file history @@ -1388,7 +1104,7 @@ class Image $page->invalidateCache(); $page->purgeSquid(); } - + /** * Purge metadata and all affected pages when the image is created, * deleted, or majorly updated. A set of additional URLs may be @@ -1399,12 +1115,15 @@ class Image // Delete thumbnails and refresh image metadata cache $this->purgeCache(); $this->purgeDescription(); - + // Purge cache of all pages using this image $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ); $update->doUpdate(); } + /** + * Check the image table schema on the given connection for subtle problems + */ function checkDBSchema(&$db) { static $checkDone = false; global $wgCheckDBSchema; @@ -1445,7 +1164,7 @@ class Image * @public */ function nextHistoryLine() { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $this->checkDBSchema($dbr); @@ -1541,7 +1260,7 @@ class Image function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) { global $wgUser, $wgUseCopyrightUpload; - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $this->checkDBSchema($dbw); @@ -1670,6 +1389,9 @@ class Image $article->insertNewArticle( $textdesc, $desc, $minor, $watch, $suppressRC ); } + # Hooks, hooks, the magic of hooks... + wfRunHooks( 'FileUpload', array( $this ) ); + # Add the log entry $log = new LogPage( 'upload' ); $log->addEntry( 'upload', $descTitle, $desc ); @@ -1697,9 +1419,9 @@ class Image wfProfileIn( __METHOD__ ); if ( $options ) { - $db =& wfGetDB( DB_MASTER ); + $db = wfGetDB( DB_MASTER ); } else { - $db =& wfGetDB( DB_SLAVE ); + $db = wfGetDB( DB_SLAVE ); } $linkCache =& LinkCache::singleton(); @@ -1721,75 +1443,24 @@ class Image wfProfileOut( __METHOD__ ); return $retVal; } - - /** - * Retrive Exif data from the file and prune unrecognized tags - * and/or tags with invalid contents - * - * @param $filename - * @return array - */ - private function retrieveExifData( $filename ) { - global $wgShowEXIF; - - /* - if ( $this->getMimeType() !== "image/jpeg" ) - return array(); - */ - - if( $wgShowEXIF && file_exists( $filename ) ) { - $exif = new Exif( $filename ); - return $exif->getFilteredData(); - } - - return array(); - } function getExifData() { global $wgRequest; - if ( $this->metadata === '0' || $this->mime == 'image/vnd.djvu' ) + $handler = $this->getHandler(); + if ( !$handler || $handler->getMetadataType( $this ) != 'exif' ) { return array(); - - $purge = $wgRequest->getVal( 'action' ) == 'purge'; - $ret = unserialize( $this->metadata ); - - $oldver = isset( $ret['MEDIAWIKI_EXIF_VERSION'] ) ? $ret['MEDIAWIKI_EXIF_VERSION'] : 0; - $newver = Exif::version(); - - if ( !count( $ret ) || $purge || $oldver != $newver ) { - $this->purgeMetadataCache(); - $this->updateExifData( $newver ); } - if ( isset( $ret['MEDIAWIKI_EXIF_VERSION'] ) ) - unset( $ret['MEDIAWIKI_EXIF_VERSION'] ); - $format = new FormatExif( $ret ); - - return $format->getFormattedData(); - } - - function updateExifData( $version ) { - if ( $this->getImagePath() === false ) # Not a local image - return; - - # Get EXIF data from image - $exif = $this->retrieveExifData( $this->imagePath ); - if ( count( $exif ) ) { - $exif['MEDIAWIKI_EXIF_VERSION'] = $version; - $this->metadata = serialize( $exif ); - } else { - $this->metadata = '0'; + if ( !$this->metadata ) { + return array(); } + $exif = unserialize( $this->metadata ); + if ( !$exif ) { + return array(); + } + unset( $exif['MEDIAWIKI_EXIF_VERSION'] ); + $format = new FormatExif( $exif ); - # Update EXIF data in database - $dbw =& wfGetDB( DB_MASTER ); - - $this->checkDBSchema($dbw); - - $dbw->update( 'image', - array( 'img_metadata' => $this->metadata ), - array( 'img_name' => $this->name ), - __METHOD__ - ); + return $format->getFormattedData(); } /** @@ -1801,7 +1472,7 @@ class Image function isLocal() { return !$this->fromSharedDirectory; } - + /** * Was this image ever deleted from the wiki? * @@ -1811,7 +1482,7 @@ class Image $title = Title::makeTitle( NS_IMAGE, $this->name ); return ( $title->isDeleted() > 0 ); } - + /** * Delete all versions of the image. * @@ -1823,37 +1494,37 @@ class Image * @param $reason * @return true on success, false on some kind of failure */ - function delete( $reason ) { + function delete( $reason, $suppress=false ) { $transaction = new FSTransaction(); $urlArr = array( $this->getURL() ); - + if( !FileStore::lock() ) { wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" ); return false; } - + try { $dbw = wfGetDB( DB_MASTER ); $dbw->begin(); - + // Delete old versions $result = $dbw->select( 'oldimage', array( 'oi_archive_name' ), array( 'oi_name' => $this->name ) ); - + while( $row = $dbw->fetchObject( $result ) ) { $oldName = $row->oi_archive_name; - - $transaction->add( $this->prepareDeleteOld( $oldName, $reason ) ); - + + $transaction->add( $this->prepareDeleteOld( $oldName, $reason, $suppress ) ); + // We'll need to purge this URL from caches... $urlArr[] = wfImageArchiveUrl( $oldName ); } $dbw->freeResult( $result ); - + // And the current version... - $transaction->add( $this->prepareDeleteCurrent( $reason ) ); - + $transaction->add( $this->prepareDeleteCurrent( $reason, $suppress ) ); + $dbw->immediateCommit(); } catch( MWException $e ) { wfDebug( __METHOD__.": db error, rolling back file transactions\n" ); @@ -1861,22 +1532,22 @@ class Image FileStore::unlock(); throw $e; } - + wfDebug( __METHOD__.": deleted db items, applying file transactions\n" ); $transaction->commit(); FileStore::unlock(); - + // Update site_stats $site_stats = $dbw->tableName( 'site_stats' ); $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ ); - + $this->purgeEverything( $urlArr ); - + return true; } - - + + /** * Delete an old version of the image. * @@ -1889,20 +1560,20 @@ class Image * @throws MWException or FSException on database or filestore failure * @return true on success, false on some kind of failure */ - function deleteOld( $archiveName, $reason ) { + function deleteOld( $archiveName, $reason, $suppress=false ) { $transaction = new FSTransaction(); $urlArr = array(); - + if( !FileStore::lock() ) { wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" ); return false; } - + $transaction = new FSTransaction(); try { $dbw = wfGetDB( DB_MASTER ); $dbw->begin(); - $transaction->add( $this->prepareDeleteOld( $archiveName, $reason ) ); + $transaction->add( $this->prepareDeleteOld( $archiveName, $reason, $suppress ) ); $dbw->immediateCommit(); } catch( MWException $e ) { wfDebug( __METHOD__.": db error, rolling back file transaction\n" ); @@ -1910,11 +1581,11 @@ class Image FileStore::unlock(); throw $e; } - + wfDebug( __METHOD__.": deleted db items, applying file transaction\n" ); $transaction->commit(); FileStore::unlock(); - + $this->purgeDescription(); // Squid purging @@ -1927,13 +1598,13 @@ class Image } return true; } - + /** * Delete the current version of a file. * May throw a database error. * @return true on success, false on failure */ - private function prepareDeleteCurrent( $reason ) { + private function prepareDeleteCurrent( $reason, $suppress=false ) { return $this->prepareDeleteVersion( $this->getFullPath(), $reason, @@ -1954,6 +1625,7 @@ class Image 'fa_user_text' => 'img_user_text', 'fa_timestamp' => 'img_timestamp' ), array( 'img_name' => $this->name ), + $suppress, __METHOD__ ); } @@ -1962,7 +1634,7 @@ class Image * May throw a database error. * @return true on success, false on failure */ - private function prepareDeleteOld( $archiveName, $reason ) { + private function prepareDeleteOld( $archiveName, $reason, $suppress=false ) { $oldpath = wfImageArchiveDir( $this->name ) . DIRECTORY_SEPARATOR . $archiveName; return $this->prepareDeleteVersion( @@ -1987,6 +1659,7 @@ class Image array( 'oi_name' => $this->name, 'oi_archive_name' => $archiveName ), + $suppress, __METHOD__ ); } @@ -1999,14 +1672,14 @@ class Image * * @return FSTransaction */ - private function prepareDeleteVersion( $path, $reason, $table, $fieldMap, $where, $fname ) { + private function prepareDeleteVersion( $path, $reason, $table, $fieldMap, $where, $suppress=false, $fname ) { global $wgUser, $wgSaveDeletedFiles; - + // Dupe the file into the file store if( file_exists( $path ) ) { if( $wgSaveDeletedFiles ) { $group = 'deleted'; - + $store = FileStore::get( $group ); $key = FileStore::calculateKey( $path, $this->extension ); $transaction = $store->insert( $key, $path, @@ -2022,7 +1695,7 @@ class Image $key = null; $transaction = new FSTransaction(); // empty } - + if( $transaction === false ) { // Fail to restore? wfDebug( __METHOD__.": import to file store failed, aborting\n" ); @@ -2030,16 +1703,28 @@ class Image return false; } + // Bitfields to further supress the image content + // Note that currently, live images are stored elsewhere + // and cannot be partially deleted + $bitfield = 0; + if ( $suppress ) { + $bitfield |= self::DELETED_FILE; + $bitfield |= self::DELETED_COMMENT; + $bitfield |= self::DELETED_USER; + $bitfield |= self::DELETED_RESTRICTED; + } + $dbw = wfGetDB( DB_MASTER ); $storageMap = array( 'fa_storage_group' => $dbw->addQuotes( $group ), 'fa_storage_key' => $dbw->addQuotes( $key ), - + 'fa_deleted_user' => $dbw->addQuotes( $wgUser->getId() ), 'fa_deleted_timestamp' => $dbw->timestamp(), - 'fa_deleted_reason' => $dbw->addQuotes( $reason ) ); + 'fa_deleted_reason' => $dbw->addQuotes( $reason ), + 'fa_deleted' => $bitfield); $allFields = array_merge( $storageMap, $fieldMap ); - + try { if( $wgSaveDeletedFiles ) { $dbw->insertSelect( 'filearchive', $table, $allFields, $where, $fname ); @@ -2052,10 +1737,10 @@ class Image $transaction->rollback(); throw $e; } - + return $transaction; } - + /** * Restore all or specified deleted revisions to the given file. * Permissions and logging are left to the caller. @@ -2067,36 +1752,38 @@ class Image * @return the number of file revisions restored if successful, * or false on failure */ - function restore( $versions=array() ) { + function restore( $versions=array(), $Unsuppress=false ) { + global $wgUser; + if( !FileStore::lock() ) { wfDebug( __METHOD__." could not acquire filestore lock\n" ); return false; } - + $transaction = new FSTransaction(); try { $dbw = wfGetDB( DB_MASTER ); $dbw->begin(); - + // Re-confirm whether this image presently exists; // if no we'll need to create an image record for the // first item we restore. $exists = $dbw->selectField( 'image', '1', array( 'img_name' => $this->name ), __METHOD__ ); - + // Fetch all or selected archived revisions for the file, // sorted from the most recent to the oldest. $conditions = array( 'fa_name' => $this->name ); if( $versions ) { $conditions['fa_id'] = $versions; } - + $result = $dbw->select( 'filearchive', '*', $conditions, __METHOD__, array( 'ORDER BY' => 'fa_timestamp DESC' ) ); - + if( $dbw->numRows( $result ) < count( $versions ) ) { // There's some kind of conflict or confusion; // we can't restore everything we were asked to. @@ -2113,40 +1800,51 @@ class Image FileStore::unlock(); return true; } - + $revisions = 0; while( $row = $dbw->fetchObject( $result ) ) { + if ( $Unsuppress ) { + // Currently, fa_deleted flags fall off upon restore, lets be careful about this + } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) { + // Skip restoring file revisions that the user cannot restore + continue; + } $revisions++; $store = FileStore::get( $row->fa_storage_group ); if( !$store ) { wfDebug( __METHOD__.": skipping row with no file.\n" ); continue; } - + if( $revisions == 1 && !$exists ) { $destDir = wfImageDir( $row->fa_name ); if ( !is_dir( $destDir ) ) { wfMkdirParents( $destDir ); } $destPath = $destDir . DIRECTORY_SEPARATOR . $row->fa_name; - + // We may have to fill in data if this was originally // an archived file revision. if( is_null( $row->fa_metadata ) ) { $tempFile = $store->filePath( $row->fa_storage_key ); - $metadata = serialize( $this->retrieveExifData( $tempFile ) ); - + $magic = MimeMagic::singleton(); $mime = $magic->guessMimeType( $tempFile, true ); $media_type = $magic->getMediaType( $tempFile, $mime ); list( $major_mime, $minor_mime ) = self::splitMime( $mime ); + $handler = MediaHandler::getHandler( $mime ); + if ( $handler ) { + $metadata = $handler->getMetadata( false, $tempFile ); + } else { + $metadata = ''; + } } else { $metadata = $row->fa_metadata; $major_mime = $row->fa_major_mime; $minor_mime = $row->fa_minor_mime; $media_type = $row->fa_media_type; } - + $table = 'image'; $fields = array( 'img_name' => $row->fa_name, @@ -2177,7 +1875,7 @@ class Image wfMkdirParents( $destDir ); } $destPath = $destDir . DIRECTORY_SEPARATOR . $archiveName; - + $table = 'oldimage'; $fields = array( 'oi_name' => $row->fa_name, @@ -2191,13 +1889,13 @@ class Image 'oi_user_text' => $row->fa_user_text, 'oi_timestamp' => $row->fa_timestamp ); } - + $dbw->insert( $table, $fields, __METHOD__ ); - /// @fixme this delete is not totally safe, potentially + // @todo this delete is not totally safe, potentially $dbw->delete( 'filearchive', array( 'fa_id' => $row->fa_id ), __METHOD__ ); - + // Check if any other stored revisions use this file; // if so, we shouldn't remove the file from the deletion // archives so they will still work. @@ -2213,161 +1911,230 @@ class Image } else { $flags = 0; } - + $transaction->add( $store->export( $row->fa_storage_key, $destPath, $flags ) ); } - + $dbw->immediateCommit(); } catch( MWException $e ) { wfDebug( __METHOD__." caught error, aborting\n" ); $transaction->rollback(); throw $e; } - + $transaction->commit(); FileStore::unlock(); - + if( $revisions > 0 ) { if( !$exists ) { wfDebug( __METHOD__." restored $revisions items, creating a new current\n" ); - + // Update site_stats $site_stats = $dbw->tableName( 'site_stats' ); $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ ); - + $this->purgeEverything(); } else { wfDebug( __METHOD__." restored $revisions as archived versions\n" ); $this->purgeDescription(); } } - + return $revisions; } /** - * Select a page from a multipage document. Determines the page used for - * rendering thumbnails. + * Returns 'true' if this image is a multipage document, e.g. a DJVU + * document. * - * @param $page Integer: page number, starting with 1 + * @return Bool */ - function selectPage( $page ) { - wfDebug( __METHOD__." selecting page $page \n" ); - $this->page = $page; - if ( ! $this->dataLoaded ) { - $this->load(); - } - if ( ! isset( $this->multiPageXML ) ) { - $this->initializeMultiPageXML(); + function isMultipage() { + $handler = $this->getHandler(); + return $handler && $handler->isMultiPage(); + } + + /** + * Returns the number of pages of a multipage document, or NULL for + * documents which aren't multipage documents + */ + function pageCount() { + $handler = $this->getHandler(); + if ( $handler && $handler->isMultiPage() ) { + return $handler->pageCount( $this ); + } else { + return null; } - $o = $this->multiPageXML->BODY[0]->OBJECT[$page-1]; - $this->height = intval( $o['height'] ); - $this->width = intval( $o['width'] ); } - function initializeMultiPageXML() { - # - # Check for files uploaded prior to DJVU support activation - # They have a '0' in their metadata field. - # - if ( $this->metadata == '0' || $this->metadata == '' ) { - $deja = new DjVuImage( $this->imagePath ); - $this->metadata = $deja->retrieveMetaData(); - $this->purgeMetadataCache(); + static function getCommonsDB() { + static $dbc; + global $wgLoadBalancer, $wgSharedUploadDBname; + if ( !isset( $dbc ) ) { + $i = $wgLoadBalancer->getGroupIndex( 'commons' ); + $dbinfo = $wgLoadBalancer->mServers[$i]; + $dbc = new Database( $dbinfo['host'], $dbinfo['user'], + $dbinfo['password'], $wgSharedUploadDBname ); + } + return $dbc; + } - # Update metadata in the database - $dbw =& wfGetDB( DB_MASTER ); - $dbw->update( 'image', - array( 'img_metadata' => $this->metadata ), - array( 'img_name' => $this->name ), - __METHOD__ - ); + /** + * Calculate the height of a thumbnail using the source and destination width + */ + static function scaleHeight( $srcWidth, $srcHeight, $dstWidth ) { + // Exact integer multiply followed by division + if ( $srcWidth == 0 ) { + return 0; + } else { + return round( $srcHeight * $dstWidth / $srcWidth ); } - wfSuppressWarnings(); - $this->multiPageXML = new SimpleXMLElement( $this->metadata ); - wfRestoreWarnings(); } /** - * Returns 'true' if this image is a multipage document, e.g. a DJVU - * document. + * Get an image size array like that returned by getimagesize(), or false if it + * can't be determined. * - * @return Bool + * @param string $fileName The filename + * @return array */ - function isMultipage() { - return ( $this->mime == 'image/vnd.djvu' ); + function getImageSize( $fileName ) { + $handler = $this->getHandler(); + return $handler->getImageSize( $this, $fileName ); } /** - * Returns the number of pages of a multipage document, or NULL for - * documents which aren't multipage documents + * Get the thumbnail extension and MIME type for a given source MIME type + * @return array thumbnail extension and MIME type */ - function pageCount() { - if ( ! $this->isMultipage() ) { - return null; - } - if ( ! isset( $this->multiPageXML ) ) { - $this->initializeMultiPageXML(); + static function getThumbType( $ext, $mime ) { + $handler = MediaHandler::getHandler( $mime ); + if ( $handler ) { + return $handler->getThumbType( $ext, $mime ); + } else { + return array( $ext, $mime ); } - return count( $this->multiPageXML->xpath( '//OBJECT' ) ); } - + } //class + /** - * Wrapper class for thumbnail images - * @package MediaWiki + * @addtogroup Media */ -class ThumbnailImage { +class ArchivedFile +{ /** - * @param string $path Filesystem path to the thumb - * @param string $url URL path to the thumb - * @private + * Returns a file object from the filearchive table + * In the future, all current and old image storage + * may use FileStore. There will be a "old" storage + * for current and previous file revisions as well as + * the "deleted" group for archived revisions + * @param $title, the corresponding image page title + * @param $id, the image id, a unique key + * @param $key, optional storage key + * @return ResultWrapper */ - function ThumbnailImage( $url, $width, $height, $path = false ) { - $this->url = $url; - $this->width = round( $width ); - $this->height = round( $height ); - # These should be integers when they get here. - # If not, there's a bug somewhere. But let's at - # least produce valid HTML code regardless. - $this->path = $path; + function ArchivedFile( $title, $id=0, $key='' ) { + if( !is_object( $title ) ) { + throw new MWException( 'Image constructor given bogus title.' ); + } + $conds = ($id) ? "fa_id = $id" : "fa_storage_key = '$key'"; + if( $title->getNamespace() == NS_IMAGE ) { + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'filearchive', + array( + 'fa_id', + 'fa_name', + 'fa_storage_key', + 'fa_storage_group', + 'fa_size', + 'fa_bits', + 'fa_width', + 'fa_height', + 'fa_metadata', + 'fa_media_type', + 'fa_major_mime', + 'fa_minor_mime', + 'fa_description', + 'fa_user', + 'fa_user_text', + 'fa_timestamp', + 'fa_deleted' ), + array( + 'fa_name' => $title->getDbKey(), + $conds ), + __METHOD__, + array( 'ORDER BY' => 'fa_timestamp DESC' ) ); + + if ( $dbr->numRows( $res ) == 0 ) { + // this revision does not exist? + return; + } + $ret = $dbr->resultObject( $res ); + $row = $ret->fetchObject(); + + // initialize fields for filestore image object + $this->mId = intval($row->fa_id); + $this->mName = $row->fa_name; + $this->mGroup = $row->fa_storage_group; + $this->mKey = $row->fa_storage_key; + $this->mSize = $row->fa_size; + $this->mBits = $row->fa_bits; + $this->mWidth = $row->fa_width; + $this->mHeight = $row->fa_height; + $this->mMetaData = $row->fa_metadata; + $this->mMime = "$row->fa_major_mime/$row->fa_minor_mime"; + $this->mType = $row->fa_media_type; + $this->mDescription = $row->fa_description; + $this->mUser = $row->fa_user; + $this->mUserText = $row->fa_user_text; + $this->mTimestamp = $row->fa_timestamp; + $this->mDeleted = $row->fa_deleted; + } else { + throw new MWException( 'This title does not correspond to an image page.' ); + return; + } + return true; } /** - * @return string The thumbnail URL + * int $field one of DELETED_* bitfield constants + * for file or revision rows + * @return bool */ - function getUrl() { - return $this->url; + function isDeleted( $field ) { + return ($this->mDeleted & $field) == $field; } - + /** - * Return HTML <img ... /> tag for the thumbnail, will include - * width and height attributes and a blank alt text (as required). - * - * You can set or override additional attributes by passing an - * associative array of name => data pairs. The data will be escaped - * for HTML output, so should be in plaintext. - * - * @param array $attribs - * @return string - * @public + * Determine if the current user is allowed to view a particular + * field of this FileStore image file, if it's marked as deleted. + * @param int $field + * @return bool */ - function toHtml( $attribs = array() ) { - $attribs['src'] = $this->url; - $attribs['width'] = $this->width; - $attribs['height'] = $this->height; - if( !isset( $attribs['alt'] ) ) $attribs['alt'] = ''; - - $html = '<img '; - foreach( $attribs as $name => $data ) { - $html .= $name . '="' . htmlspecialchars( $data ) . '" '; + function userCan( $field ) { + if( isset($this->mDeleted) && ($this->mDeleted & $field) == $field ) { + // images + global $wgUser; + $permission = ( $this->mDeleted & Revision::DELETED_RESTRICTED ) == Revision::DELETED_RESTRICTED + ? 'hiderevision' + : 'deleterevision'; + wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" ); + return $wgUser->isAllowed( $permission ); + } else { + return true; } - $html .= '/>'; - return $html; } - } +/** + * Aliases for backwards compatibility with 1.6 + */ +define( 'MW_IMG_DELETED_FILE', Image::DELETED_FILE ); +define( 'MW_IMG_DELETED_COMMENT', Image::DELETED_COMMENT ); +define( 'MW_IMG_DELETED_USER', Image::DELETED_USER ); +define( 'MW_IMG_DELETED_RESTRICTED', Image::DELETED_RESTRICTED ); + ?> diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php index 931fdff1..d04110d4 100644 --- a/includes/ImageFunctions.php +++ b/includes/ImageFunctions.php @@ -21,7 +21,7 @@ function wfImageDir( $fname ) { } /** - * Returns the image directory of an image's thubnail + * Returns the image directory of an image's thumbnail * The result is an absolute path. * * This function is called from thumb.php before Setup.php is included diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php index 9d58b7f6..fba7714c 100644 --- a/includes/ImageGallery.php +++ b/includes/ImageGallery.php @@ -3,7 +3,6 @@ if ( ! defined( 'MEDIAWIKI' ) ) die( 1 ); /** - * @package MediaWiki */ /** @@ -11,23 +10,32 @@ if ( ! defined( 'MEDIAWIKI' ) ) * * Add images to the gallery using add(), then render that list to HTML using toHTML(). * - * @package MediaWiki + * @addtogroup Media */ class ImageGallery { var $mImages, $mShowBytes, $mShowFilename; var $mCaption = false; var $mSkin = false; - + /** * Is the gallery on a wiki page (i.e. not a special page) */ var $mParsing; /** + * Contextual title, used when images are being screened + * against the bad image list + */ + private $contextTitle = false; + + private $mPerRow = 4; // How many images wide should the gallery be? + private $mWidths = 120, $mHeights = 120; // How wide/tall each thumbnail should be + + /** * Create a new image gallery object. */ - function ImageGallery( ) { + function __construct( ) { $this->mImages = array(); $this->mShowBytes = true; $this->mShowFilename = true; @@ -40,7 +48,7 @@ class ImageGallery function setParsing( $val = true ) { $this->mParsing = $val; } - + /** * Set the caption (as plain text) * @@ -49,25 +57,58 @@ class ImageGallery function setCaption( $caption ) { $this->mCaption = htmlspecialchars( $caption ); } - + /** * Set the caption (as HTML) * * @param $caption Caption */ - function setCaptionHtml( $caption ) { + public function setCaptionHtml( $caption ) { $this->mCaption = $caption; } /** + * Set how many images will be displayed per row. + * + * @param int $num > 0; invalid numbers will be rejected + */ + public function setPerRow( $num ) { + if ($num > 0) { + $this->mPerRow = (int)$num; + } + } + + /** + * Set how wide each image will be, in pixels. + * + * @param int $num > 0; invalid numbers will be ignored + */ + public function setWidths( $num ) { + if ($num > 0) { + $this->mWidths = (int)$num; + } + } + + /** + * Set how high each image will be, in pixels. + * + * @param int $num > 0; invalid numbers will be ignored + */ + public function setHeights( $num ) { + if ($num > 0) { + $this->mHeights = (int)$num; + } + } + + /** * Instruct the class to use a specific skin for rendering * * @param $skin Skin object */ function useSkin( $skin ) { - $this->mSkin =& $skin; + $this->mSkin = $skin; } - + /** * Return the skin that should be used * @@ -76,9 +117,9 @@ class ImageGallery function getSkin() { if( !$this->mSkin ) { global $wgUser; - $skin =& $wgUser->getSkin(); + $skin = $wgUser->getSkin(); } else { - $skin =& $this->mSkin; + $skin = $this->mSkin; } return $skin; } @@ -143,14 +184,15 @@ class ImageGallery * */ function toHTML() { - global $wgLang, $wgGenerateThumbnailOnParse; + global $wgLang; $sk = $this->getSkin(); $s = '<table class="gallery" cellspacing="0" cellpadding="0">'; if( $this->mCaption ) - $s .= '<td class="galleryheader" colspan="4"><big>' . $this->mCaption . '</big></td>'; - + $s .= "\n\t<caption>{$this->mCaption}</caption>"; + + $params = array( 'width' => $this->mWidths, 'height' => $this->mHeights ); $i = 0; foreach ( $this->mImages as $pair ) { $img =& $pair[0]; @@ -160,20 +202,19 @@ class ImageGallery if( $nt->getNamespace() != NS_IMAGE ) { # We're dealing with a non-image, spit out the name and be done with it. - $thumbhtml = '<div style="height: 152px;">' . htmlspecialchars( $nt->getText() ) . '</div>'; - } - else if( $this->mParsing && wfIsBadImage( $nt->getDBkey() ) ) { + $thumbhtml = "\n\t\t\t".'<div style="height: '.($this->mHeights*1.25+2).'px;">' + . htmlspecialchars( $nt->getText() ) . '</div>'; + } elseif( $this->mParsing && wfIsBadImage( $nt->getDBkey(), $this->getContextTitle() ) ) { # The image is blacklisted, just show it as a text link. - $thumbhtml = '<div style="height: 152px;">' + $thumbhtml = "\n\t\t\t".'<div style="height: '.($this->mHeights*1.25+2).'px;">' . $sk->makeKnownLinkObj( $nt, htmlspecialchars( $nt->getText() ) ) . '</div>'; - } else if( !( $thumb = $img->getThumbnail( 120, 120, $wgGenerateThumbnailOnParse ) ) ) { + } elseif( !( $thumb = $img->transform( $params ) ) ) { # Error generating thumbnail. - $thumbhtml = '<div style="height: 152px;">' + $thumbhtml = "\n\t\t\t".'<div style="height: '.($this->mHeights*1.25+2).'px;">' . htmlspecialchars( $img->getLastError() ) . '</div>'; - } - else { - $vpad = floor( ( 150 - $thumb->height ) /2 ) - 2; - $thumbhtml = '<div class="thumb" style="padding: ' . $vpad . 'px 0;">' + } else { + $vpad = floor( ( 1.25*$this->mHeights - $thumb->height ) /2 ) - 2; + $thumbhtml = "\n\t\t\t".'<div class="thumb" style="padding: ' . $vpad . 'px 0; width: '.($this->mWidths+30).'px;">' . $sk->makeKnownLinkObj( $nt, $thumb->toHtml() ) . '</div>'; } @@ -200,27 +241,55 @@ class ImageGallery # in version 4.8.6 generated crackpot html in its absence, see: # http://bugzilla.wikimedia.org/show_bug.cgi?id=1765 -Ævar - $s .= ($i%4==0) ? '<tr>' : ''; - $s .= '<td><div class="gallerybox">' . $thumbhtml - . '<div class="gallerytext">' . "\n" . $textlink . $text . $nb - . "</div></div></td>\n"; - $s .= ($i%4==3) ? '</tr>' : ''; - $i++; + if ( $i % $this->mPerRow == 0 ) { + $s .= "\n\t<tr>"; + } + $s .= + "\n\t\t" . '<td><div class="gallerybox" style="width: '.($this->mWidths*1.25).'px;">' + . $thumbhtml + . "\n\t\t\t" . '<div class="gallerytext">' . "\n" + . $textlink . $text . $nb + . "\n\t\t\t</div>" + . "\n\t\t</div></td>"; + if ( $i % $this->mPerRow == $this->mPerRow - 1 ) { + $s .= "\n\t</tr>"; + } + ++$i; } - if( $i %4 != 0 ) { - $s .= "</tr>\n"; + if( $i % $this->mPerRow != 0 ) { + $s .= "\n\t</tr>"; } - $s .= '</table>'; + $s .= "\n</table>"; return $s; } - + /** * @return int Number of images in the gallery */ public function count() { return count( $this->mImages ); } + + /** + * Set the contextual title + * + * @param Title $title Contextual title + */ + public function setContextTitle( $title ) { + $this->contextTitle = $title; + } + + /** + * Get the contextual title, if applicable + * + * @return mixed Title or false + */ + public function getContextTitle() { + return is_object( $this->contextTitle ) && $this->contextTitle instanceof Title + ? $this->contextTitle + : false; + } } //class ?> diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 43b99130..13f8e46a 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -1,6 +1,5 @@ <?php /** - * @package MediaWiki */ /** @@ -11,7 +10,8 @@ if( !defined( 'MEDIAWIKI' ) ) /** * Special handling for image description pages - * @package MediaWiki + * + * @addtogroup Media */ class ImagePage extends Article { @@ -29,59 +29,62 @@ class ImagePage extends Article { } function view() { - global $wgOut, $wgShowEXIF; + global $wgOut, $wgShowEXIF, $wgRequest, $wgUser; $this->img = new Image( $this->mTitle ); - if( $this->mTitle->getNamespace() == NS_IMAGE ) { - if ($wgShowEXIF && $this->img->exists()) { - $exif = $this->img->getExifData(); - $showmeta = count($exif) ? true : false; - } else { - $exif = false; - $showmeta = false; - } + $diff = $wgRequest->getVal( 'diff' ); + $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); - if ($this->img->exists()) - $wgOut->addHTML($this->showTOC($showmeta)); + if ( $this->mTitle->getNamespace() != NS_IMAGE || ( isset( $diff ) && $diffOnly ) ) + return Article::view(); - $this->openShowImage(); + if ($wgShowEXIF && $this->img->exists()) { + $exif = $this->img->getExifData(); + $showmeta = count($exif) ? true : false; + } else { + $exif = false; + $showmeta = false; + } - # No need to display noarticletext, we use our own message, output in openShowImage() - if( $this->getID() ) { - Article::view(); - } else { - # Just need to set the right headers - $wgOut->setArticleFlag( true ); - $wgOut->setRobotpolicy( 'index,follow' ); - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - $this->viewUpdates(); - } + if ($this->img->exists()) + $wgOut->addHTML($this->showTOC($showmeta)); - # Show shared description, if needed - if( $this->mExtraDescription ) { - $fol = wfMsg( 'shareddescriptionfollows' ); - if( $fol != '-' ) { - $wgOut->addWikiText( $fol ); - } - $wgOut->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . '</div>' ); - } + $this->openShowImage(); - $this->closeShowImage(); - $this->imageHistory(); - $this->imageLinks(); - if( $exif ) { - global $wgStylePath, $wgStyleVersion; - $expand = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-expand' ) ) ); - $collapse = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-collapse' ) ) ); - $wgOut->addHTML( "<h2 id=\"metadata\">" . wfMsgHtml( 'metadata' ) . "</h2>\n" ); - $wgOut->addWikiText( $this->makeMetadataTable( $exif ) ); - $wgOut->addHTML( - "<script type=\"text/javascript\" src=\"$wgStylePath/common/metadata.js?$wgStyleVersion\"></script>\n" . - "<script type=\"text/javascript\">attachMetadataToggle('mw_metadata', '$expand', '$collapse');</script>\n" ); - } - } else { + # No need to display noarticletext, we use our own message, output in openShowImage() + if ( $this->getID() ) { Article::view(); + } else { + # Just need to set the right headers + $wgOut->setArticleFlag( true ); + $wgOut->setRobotpolicy( 'index,follow' ); + $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + $this->viewUpdates(); + } + + # Show shared description, if needed + if ( $this->mExtraDescription ) { + $fol = wfMsg( 'shareddescriptionfollows' ); + if( $fol != '-' && !wfEmptyMsg( 'shareddescriptionfollows', $fol ) ) { + $wgOut->addWikiText( $fol ); + } + $wgOut->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . '</div>' ); + } + + $this->closeShowImage(); + $this->imageHistory(); + $this->imageLinks(); + + if ( $exif ) { + global $wgStylePath, $wgStyleVersion; + $expand = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-expand' ) ) ); + $collapse = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-collapse' ) ) ); + $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ). "\n" ); + $wgOut->addWikiText( $this->makeMetadataTable( $exif ) ); + $wgOut->addHTML( + "<script type=\"text/javascript\" src=\"$wgStylePath/common/metadata.js?$wgStyleVersion\"></script>\n" . + "<script type=\"text/javascript\">attachMetadataToggle('mw_metadata', '$expand', '$collapse');</script>\n" ); } } @@ -165,15 +168,19 @@ class ImagePage extends Article { function openShowImage() { global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang; - global $wgUseImageResize, $wgGenerateThumbnailOnParse; $full_url = $this->img->getURL(); - $anchoropen = ''; - $anchorclose = ''; + $linkAttribs = false; $sizeSel = intval( $wgUser->getOption( 'imagesize') ); - if( !isset( $wgImageLimits[$sizeSel] ) ) { $sizeSel = User::getDefaultOption( 'imagesize' ); + + // The user offset might still be incorrect, specially if + // $wgImageLimits got changed (see bug #8858). + if( !isset( $wgImageLimits[$sizeSel] ) ) { + // Default to the first offset in $wgImageLimits + $sizeSel = 0; + } } $max = $wgImageLimits[$sizeSel]; $maxWidth = $max[0]; @@ -183,21 +190,25 @@ class ImagePage extends Article { if ( $this->img->exists() ) { # image $page = $wgRequest->getIntOrNull( 'page' ); - if ( ! is_null( $page ) ) { - $this->img->selectPage( $page ); - } else { + if ( is_null( $page ) ) { + $params = array(); $page = 1; + } else { + $params = array( 'page' => $page ); } - $width = $this->img->getWidth(); - $height = $this->img->getHeight(); + $width_orig = $this->img->getWidth(); + $width = $width_orig; + $height_orig = $this->img->getHeight(); + $height = $height_orig; + $mime = $this->img->getMimeType(); $showLink = false; + $linkAttribs = array( 'href' => $full_url ); if ( $this->img->allowInlineDisplay() and $width and $height) { # image # "Download high res version" link below the image - $msg = wfMsgHtml('showbigimage', $width, $height, intval( $this->img->getSize()/1024 ) ); - + $msgsize = wfMsgHtml('file-info-size', $width_orig, $height_orig, $sk->formatSize( $this->img->getSize() ), $mime ); # We'll show a thumbnail of this image if ( $width > $maxWidth || $height > $maxHeight ) { # Calculate the thumbnail size. @@ -213,38 +224,41 @@ class ImagePage extends Article { # Note that $height <= $maxHeight now, but might not be identical # because of rounding. } - - if( $wgUseImageResize ) { - $thumbnail = $this->img->getThumbnail( $width, -1, $wgGenerateThumbnailOnParse ); - if ( $thumbnail == null ) { - $url = $this->img->getViewURL(); - } else { - $url = $thumbnail->getURL(); - } - } else { - # No resize ability? Show the full image, but scale - # it down in the browser so it fits on the page. - $url = $this->img->getViewURL(); - } - $anchoropen = "<a href=\"{$full_url}\">"; - $anchorclose = "</a><br />"; - if( $this->img->mustRender() ) { - $showLink = true; - } else { - $anchorclose .= "\n$anchoropen{$msg}</a>"; - } + $msgbig = wfMsgHtml( 'show-big-image' ); + $msgsmall = wfMsgExt( 'show-big-image-thumb', + array( 'parseinline' ), $width, $height ); } else { - $url = $this->img->getViewURL(); + # Image is small enough to show full size on image page + $msgbig = htmlspecialchars( $this->img->getName() ); + $msgsmall = wfMsgExt( 'file-nohires', array( 'parseinline' ) ); + } + + $params['width'] = $width; + $thumbnail = $this->img->transform( $params ); + + $anchorclose = "<br />"; + if( $this->img->mustRender() ) { $showLink = true; + } else { + $anchorclose .= + $msgsmall . + '<br />' . Xml::tags( 'a', $linkAttribs, $msgbig ) . ' ' . $msgsize; } if ( $this->img->isMultipage() ) { $wgOut->addHTML( '<table class="multipageimage"><tr><td>' ); } - $wgOut->addHTML( '<div class="fullImageLink" id="file">' . $anchoropen . - "<img border=\"0\" src=\"{$url}\" width=\"{$width}\" height=\"{$height}\" alt=\"" . - htmlspecialchars( $this->img->getTitle()->getPrefixedText() ).'" />' . $anchorclose . '</div>' ); + $imgAttribs = array( + 'border' => 0, + 'alt' => $this->img->getTitle()->getPrefixedText() + ); + + if ( $thumbnail ) { + $wgOut->addHTML( '<div class="fullImageLink" id="file">' . + $thumbnail->toHtml( $imgAttribs, $linkAttribs ) . + $anchorclose . '</div>' ); + } if ( $this->img->isMultipage() ) { $count = $this->img->pageCount(); @@ -252,22 +266,26 @@ class ImagePage extends Article { if ( $page > 1 ) { $label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false ); $link = $sk->makeLinkObj( $this->mTitle, $label, 'page='. ($page-1) ); - $this->img->selectPage( $page - 1 ); - $thumb1 = $sk->makeThumbLinkObj( $this->img, $link, $label, 'none' ); + $thumb1 = $sk->makeThumbLinkObj( $this->img, $link, $label, 'none', + array( 'page' => $page - 1 ) ); } else { $thumb1 = ''; } if ( $page < $count ) { $label = wfMsg( 'imgmultipagenext' ); - $this->img->selectPage( $page + 1 ); $link = $sk->makeLinkObj( $this->mTitle, $label, 'page='. ($page+1) ); - $thumb2 = $sk->makeThumbLinkObj( $this->img, $link, $label, 'none' ); + $thumb2 = $sk->makeThumbLinkObj( $this->img, $link, $label, 'none', + array( 'page' => $page + 1 ) ); } else { $thumb2 = ''; } - $select = '<form name="pageselector" action="' . $this->img->getEscapeLocalUrl( '' ) . '" method="GET" onchange="document.pageselector.submit();">' ; + global $wgScript; + $select = '<form name="pageselector" action="' . + htmlspecialchars( $wgScript ) . + '" method="get" onchange="document.pageselector.submit();">' . + Xml::hidden( 'title', $this->getTitle()->getPrefixedDbKey() ); $select .= $wgOut->parse( wfMsg( 'imgmultigotopre' ), false ) . ' <select id="pageselector" name="page">'; for ( $i=1; $i <= $count; $i++ ) { @@ -279,7 +297,7 @@ class ImagePage extends Article { htmlspecialchars( wfMsg( 'imgmultigo' ) ) . '"></form>'; $wgOut->addHTML( '</td><td><div class="multipageimagenavbox">' . - "$select<hr />$thumb1\n$thumb2<br clear=\"all\" /></div></td></tr></table>" ); + "$select<hr />$thumb1\n$thumb2<br clear=\"all\" /></div></td></tr></table>" ); } } else { #if direct link is allowed but it's not a renderable image, show an icon. @@ -296,25 +314,26 @@ class ImagePage extends Article { if ($showLink) { - $filename = wfEscapeWikiText( $this->img->getName() ); - // Hacky workaround: for some reason we use the incorrect MIME type - // image/svg for SVG. This should be fixed internally, but at least - // make the displayed type right. - $mime = $this->img->getMimeType(); + // Workaround for incorrect MIME type on SVGs uploaded in previous versions if ($mime == 'image/svg') $mime = 'image/svg+xml'; - $info = wfMsg( 'fileinfo', - ceil($this->img->getSize()/1024.0), - $mime ); + $filename = wfEscapeWikiText( $this->img->getName() ); + $info = wfMsg( 'file-info', $sk->formatSize( $this->img->getSize() ), $mime ); + $infores = ''; + + // Check for MIME type. Other types may have more information in the future. + if (substr($mime,0,9) == 'image/svg' ) { + $infores = wfMsg('file-svg', $width_orig, $height_orig ) . '<br />'; + } global $wgContLang; $dirmark = $wgContLang->getDirMark(); if (!$this->img->isSafeFile()) { $warning = wfMsg( 'mediawarning' ); $wgOut->addWikiText( <<<END -<div class="fullMedia"> +<div class="fullMedia">$infores <span class="dangerousLink">[[Media:$filename|$filename]]</span>$dirmark -<span class="fileInfo"> ($info)</span> +<span class="fileInfo"> $info</span> </div> <div class="mediaWarning">$warning</div> @@ -322,8 +341,8 @@ END ); } else { $wgOut->addWikiText( <<<END -<div class="fullMedia"> -[[Media:$filename|$filename]]$dirmark <span class="fileInfo"> ($info)</span> +<div class="fullMedia">$infores +[[Media:$filename|$filename]]$dirmark <span class="fileInfo"> $info</span> </div> END ); @@ -360,7 +379,9 @@ END $wgOut->addHTML($sharedtext); if ($wgRepositoryBaseUrl && $wgFetchCommonsDescriptions) { - $text = Http::get($url . '?action=render'); + $renderUrl = wfAppendQuery( $url, 'action=render' ); + wfDebug( "Fetching shared description from $renderUrl\n" ); + $text = Http::get( $renderUrl ); if ($text) $this->mExtraDescription = $text; } @@ -389,11 +410,11 @@ END # "Upload a new version of this file" link if( $wgUser->isAllowed( 'reupload' ) ) { $ulink = $sk->makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) ); - $wgOut->addHtml( "<li><div>{$ulink}</div></li>" ); + $wgOut->addHtml( "<li><div class='plainlinks'>{$ulink}</div></li>" ); } # External editing link - $elink = $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'edit-externally' ), 'action=edit&externaledit=true&mode=file' ); + $elink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'edit-externally' ), 'action=edit&externaledit=true&mode=file' ); $wgOut->addHtml( '<li>' . $elink . '<div>' . wfMsgWikiHtml( 'edit-externally-help' ) . '</div></li>' ); $wgOut->addHtml( '</ul>' ); @@ -449,9 +470,9 @@ END { global $wgUser, $wgOut; - $wgOut->addHTML( '<h2 id="filelinks">' . wfMsg( 'imagelinks' ) . "</h2>\n" ); + $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'filelinks' ), wfMsg( 'imagelinks' ) ) . "\n" ); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $page = $dbr->tableName( 'page' ); $imagelinks = $dbr->tableName( 'imagelinks' ); @@ -619,7 +640,7 @@ END $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); return; } - if ( ! $this->mTitle->userCanEdit() ) { + if ( ! $this->mTitle->userCan( 'edit' ) ) { $wgOut->readOnlyPage( $this->getContent(), true ); return; } @@ -645,9 +666,6 @@ END } $oldver = wfTimestampNow() . "!{$name}"; - $dbr =& wfGetDB( DB_SLAVE ); - $size = $dbr->selectField( 'oldimage', 'oi_size', array( 'oi_archive_name' => $oldimage ) ); - if ( ! rename( $curfile, "${archive}/{$oldver}" ) ) { $wgOut->showFileRenameError( $curfile, "${archive}/{$oldver}" ); return; @@ -683,6 +701,7 @@ END wfDebug( "ImagePage::doPurge purging " . $this->img->getName() . "\n" ); $update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' ); $update->doUpdate(); + $this->img->upgradeRow(); $this->img->purgeCache(); } else { wfDebug( "ImagePage::doPurge no image\n" ); @@ -694,7 +713,7 @@ END /** * @todo document - * @package MediaWiki + * @addtogroup Media */ class ImageHistoryList { function ImageHistoryList( &$skin ) { @@ -702,8 +721,9 @@ class ImageHistoryList { } function beginImageHistoryList() { - $s = "\n<h2 id=\"filehistory\">" . wfMsg( 'imghistory' ) . "</h2>\n" . - "<p>" . wfMsg( 'imghistlegend' ) . "</p>\n".'<ul class="special">'; + $s = "\n" . + Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'imghistory' ) ) . + "\n<p>" . wfMsg( 'imghistlegend' ) . "</p>\n".'<ul class="special">'; return $s; } @@ -716,9 +736,9 @@ class ImageHistoryList { global $wgUser, $wgLang, $wgTitle, $wgContLang; $datetime = $wgLang->timeanddate( $timestamp, true ); - $del = wfMsg( 'deleteimg' ); - $delall = wfMsg( 'deleteimgcompletely' ); - $cur = wfMsg( 'cur' ); + $del = wfMsgHtml( 'deleteimg' ); + $delall = wfMsgHtml( 'deleteimgcompletely' ); + $cur = wfMsgHtml( 'cur' ); if ( $iscur ) { $url = Image::imageUrl( $img ); @@ -734,10 +754,10 @@ class ImageHistoryList { } } else { $url = htmlspecialchars( wfImageArchiveUrl( $img ) ); - if( $wgUser->getID() != 0 && $wgTitle->userCanEdit() ) { + if( $wgUser->getID() != 0 && $wgTitle->userCan( 'edit' ) ) { $token = urlencode( $wgUser->editToken( $img ) ); $rlink = $this->skin->makeKnownLinkObj( $wgTitle, - wfMsg( 'revertimg' ), 'action=revert&oldimage=' . + wfMsgHtml( 'revertimg' ), 'action=revert&oldimage=' . urlencode( $img ) . "&wpEditToken=$token" ); $dlink = $this->skin->makeKnownLinkObj( $wgTitle, $del, 'action=delete&oldimage=' . urlencode( $img ) . @@ -746,7 +766,7 @@ class ImageHistoryList { # Having live active links for non-logged in users # means that bots and spiders crawling our site can # inadvertently change content. Baaaad idea. - $rlink = wfMsg( 'revertimg' ); + $rlink = wfMsgHtml( 'revertimg' ); $dlink = $del; } } @@ -754,7 +774,7 @@ class ImageHistoryList { $userlink = $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext ); $nbytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( $size ) ); - $widthheight = wfMsg( 'widthheight', $width, $height ); + $widthheight = wfMsgHtml( 'widthheight', $width, $height ); $style = $this->skin->getInternalLinkAttributes( $url, $datetime ); $s = "<li> ({$dlink}) ({$rlink}) <a href=\"{$url}\"{$style}>{$datetime}</a> . . {$userlink} . . {$widthheight} ({$nbytes})"; diff --git a/includes/ImageQueryPage.php b/includes/ImageQueryPage.php new file mode 100644 index 00000000..93f090a1 --- /dev/null +++ b/includes/ImageQueryPage.php @@ -0,0 +1,68 @@ +<?php + +/** + * Variant of QueryPage which uses a gallery to output results, thus + * suited for reports generating images + * + * @package MediaWiki + * @addtogroup SpecialPage + * @author Rob Church <robchur@gmail.com> + */ +class ImageQueryPage extends QueryPage { + + /** + * Format and output report results using the given information plus + * OutputPage + * + * @param OutputPage $out OutputPage to print to + * @param Skin $skin User skin to use + * @param Database $dbr Database (read) connection to use + * @param int $res Result pointer + * @param int $num Number of available result rows + * @param int $offset Paging offset + */ + protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) { + if( $num > 0 ) { + $gallery = new ImageGallery(); + $gallery->useSkin( $skin ); + + # $res might contain the whole 1,000 rows, so we read up to + # $num [should update this to use a Pager] + for( $i = 0; $i < $num && $row = $dbr->fetchObject( $res ); $i++ ) { + $image = $this->prepareImage( $row ); + if( $image instanceof Image ) { + $gallery->add( $image, $this->getCellHtml( $row ) ); + } + } + + $out->addHtml( $gallery->toHtml() ); + } + } + + /** + * Prepare an image object given a result row + * + * @param object $row Result row + * @return Image + */ + private function prepareImage( $row ) { + $namespace = isset( $row->namespace ) ? $row->namespace : NS_IMAGE; + $title = Title::makeTitleSafe( $namespace, $row->title ); + return ( $title instanceof Title && $title->getNamespace() == NS_IMAGE ) + ? new Image( $title ) + : null; + } + + /** + * Get additional HTML to be shown in a results' cell + * + * @param object $row Result row + * @return string + */ + protected function getCellHtml( $row ) { + return ''; + } + +} + +?> diff --git a/includes/JobQueue.php b/includes/JobQueue.php index 746cf5de..140130fa 100644 --- a/includes/JobQueue.php +++ b/includes/JobQueue.php @@ -4,6 +4,9 @@ if ( !defined( 'MEDIAWIKI' ) ) { die( "This file is part of MediaWiki, it is not a valid entry point\n" ); } +/** + * Class to both describe a background job and handle jobs. + */ abstract class Job { var $command, $title, @@ -13,10 +16,20 @@ abstract class Job { $error; /*------------------------------------------------------------------------- + * Abstract functions + *------------------------------------------------------------------------*/ + + /** + * Run the job + * @return boolean success + */ + abstract function run(); + + /*------------------------------------------------------------------------- * Static functions *------------------------------------------------------------------------*/ - /** + /** * @deprecated use LinksUpdate::queueRecursiveJobs() */ /** @@ -26,25 +39,41 @@ abstract class Job { /** * Pop a job off the front of the queue * @static + * @param $offset Number of jobs to skip * @return Job or false if there's no jobs */ - static function pop() { + static function pop($offset=0) { wfProfileIn( __METHOD__ ); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); - // Get a job from the slave - $row = $dbr->selectRow( 'job', '*', '', __METHOD__, - array( 'ORDER BY' => 'job_id', 'LIMIT' => 1 ) - ); + /* Get a job from the slave, start with an offset, + scan full set afterwards, avoid hitting purged rows - if ( $row === false ) { - wfProfileOut( __METHOD__ ); - return false; + NB: If random fetch previously was used, offset + will always be ahead of few entries + */ + + $row = $dbr->selectRow( 'job', '*', "job_id >= ${offset}", __METHOD__, + array( 'ORDER BY' => 'job_id', 'LIMIT' => 1 )); + + // Refetching without offset is needed as some of job IDs could have had delayed commits + // and have lower IDs than jobs already executed, blame concurrency :) + // + if ( $row === false) { + if ($offset!=0) + $row = $dbr->selectRow( 'job', '*', '', __METHOD__, + array( 'ORDER BY' => 'job_id', 'LIMIT' => 1 )); + + if ($row === false ) { + wfProfileOut( __METHOD__ ); + return false; + } } + $offset = $row->job_id; // Try to delete it from the master - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ ); $affected = $dbw->affectedRows(); $dbw->immediateCommit(); @@ -53,7 +82,7 @@ abstract class Job { // Failed, someone else beat us to it // Try getting a random row $row = $dbw->selectRow( 'job', array( 'MIN(job_id) as minjob', - 'MAX(job_id) as maxjob' ), '', __METHOD__ ); + 'MAX(job_id) as maxjob' ), "job_id >= $offset", __METHOD__ ); if ( $row === false || is_null( $row->minjob ) || is_null( $row->maxjob ) ) { // No jobs to get wfProfileOut( __METHOD__ ); @@ -61,7 +90,7 @@ abstract class Job { } // Get the random row $row = $dbw->selectRow( 'job', '*', - array( 'job_id' => mt_rand( $row->minjob, $row->maxjob ) ), __METHOD__ ); + 'job_id >= ' . mt_rand( $row->minjob, $row->maxjob ), __METHOD__ ); if ( $row === false ) { // Random job gone before we got the chance to select it // Give up @@ -72,7 +101,7 @@ abstract class Job { $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ ); $affected = $dbw->affectedRows(); $dbw->immediateCommit(); - + if ( !$affected ) { // Random job gone before we exclusively deleted it // Give up @@ -80,22 +109,22 @@ abstract class Job { return false; } } - + // If execution got to here, there's a row in $row that has been deleted from the database // by this thread. Hence the concurrent pop was successful. $namespace = $row->job_namespace; $dbkey = $row->job_title; $title = Title::makeTitleSafe( $namespace, $dbkey ); $job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), $row->job_id ); - + // Remove any duplicates it may have later in the queue $dbw->delete( 'job', $job->insertFields(), __METHOD__ ); - + wfProfileOut( __METHOD__ ); return $job; } - /** + /** * Create an object of a subclass */ static function factory( $command, $title, $params = false, $id = 0 ) { @@ -126,6 +155,27 @@ abstract class Job { } } + /** + * Batch-insert a group of jobs into the queue. + * This will be wrapped in a transaction with a forced commit. + * + * This may add duplicate at insert time, but they will be + * removed later on, when the first one is popped. + * + * @param $jobs array of Job objects + */ + static function batchInsert( $jobs ) { + if( count( $jobs ) ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->begin(); + foreach( $jobs as $job ) { + $rows[] = $job->insertFields(); + } + $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' ); + $dbw->commit(); + } + } + /*------------------------------------------------------------------------- * Non-static functions *------------------------------------------------------------------------*/ @@ -147,8 +197,8 @@ abstract class Job { function insert() { $fields = $this->insertFields(); - $dbw =& wfGetDB( DB_MASTER ); - + $dbw = wfGetDB( DB_MASTER ); + if ( $this->removeDuplicates ) { $res = $dbw->select( 'job', array( '1' ), $fields, __METHOD__ ); if ( $dbw->numRows( $res ) ) { @@ -158,7 +208,7 @@ abstract class Job { $fields['job_id'] = $dbw->nextSequenceValue( 'job_job_id_seq' ); $dbw->insert( 'job', $fields, __METHOD__ ); } - + protected function insertFields() { return array( 'job_cmd' => $this->command, @@ -167,34 +217,7 @@ abstract class Job { 'job_params' => Job::makeBlob( $this->params ) ); } - - /** - * Batch-insert a group of jobs into the queue. - * This will be wrapped in a transaction with a forced commit. - * - * This may add duplicate at insert time, but they will be - * removed later on, when the first one is popped. - * - * @param $jobs array of Job objects - */ - static function batchInsert( $jobs ) { - if( count( $jobs ) ) { - $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); - foreach( $jobs as $job ) { - $rows[] = $job->insertFields(); - } - $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' ); - $dbw->commit(); - } - } - /** - * Run the job - * @return boolean success - */ - abstract function run(); - function toString() { $paramString = ''; if ( $this->params ) { @@ -222,6 +245,10 @@ abstract class Job { } } + +/** + * Background job to update links for a given title. + */ class RefreshLinksJob extends Job { function __construct( $title, $params = '', $id = 0 ) { parent::__construct( 'refreshLinks', $title, $params, $id ); @@ -237,7 +264,7 @@ class RefreshLinksJob extends Job { $linkCache =& LinkCache::singleton(); $linkCache->clear(); - + if ( is_null( $this->title ) ) { $this->error = "refreshLinks: Invalid title"; wfProfileOut( __METHOD__ ); diff --git a/includes/Licenses.php b/includes/Licenses.php index dd1308b4..f4586ae5 100644 --- a/includes/Licenses.php +++ b/includes/Licenses.php @@ -1,9 +1,8 @@ <?php /** * A License class for use on Special:Upload - * - * @package MediaWiki - * @subpackage SpecialPage + * + * @addtogroup SpecialPage * * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com> * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason @@ -31,12 +30,12 @@ class Licenses { /**#@-*/ /** - * Constrictor + * Constructor * * @param $str String: the string to build the licenses member from, will use * wfMsgForContent( 'licenses' ) if null (default: null) */ - function Licenses( $str = null ) { + function __construct( $str = null ) { // PHP sucks, this should be possible in the constructor $this->msg = is_null( $str ) ? wfMsgForContent( 'licenses' ) : $str; $this->html = ''; @@ -147,6 +146,9 @@ class Licenses { function getHtml() { return $this->html; } } +/** + * A License class for use on Special:Upload (represents a single type of license). + */ class License { /** * @var string diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php index 61e1c040..065c540a 100644 --- a/includes/LinkBatch.php +++ b/includes/LinkBatch.php @@ -4,8 +4,7 @@ * Class representing a list of titles * The execute() method checks them all for existence and adds them to a LinkCache object + - * @package MediaWiki - * @subpackage Cache + * @addtogroup Cache */ class LinkBatch { /** @@ -13,7 +12,7 @@ class LinkBatch { */ var $data = array(); - function LinkBatch( $arr = array() ) { + function __construct( $arr = array() ) { foreach( $arr as $item ) { $this->addObj( $item ); } @@ -120,7 +119,7 @@ class LinkBatch { // Construct query // This is very similar to Parser::replaceLinkHolders - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $page = $dbr->tableName( 'page' ); $set = $this->constructSet( 'page', $dbr ); if ( $set === false ) { diff --git a/includes/LinkCache.php b/includes/LinkCache.php index 8e56225b..53fb640a 100644 --- a/includes/LinkCache.php +++ b/includes/LinkCache.php @@ -1,13 +1,8 @@ <?php /** * Cache for article titles (prefixed DB keys) and ids linked from one source - * @package MediaWiki - * @subpackage Cache - */ - -/** - * @package MediaWiki - * @subpackage Cache + * + * @addtogroup Cache */ class LinkCache { // Increment $mClassVer whenever old serialized versions of this class @@ -29,7 +24,7 @@ class LinkCache { return $instance; } - function LinkCache() { + function __construct() { $this->mForUpdate = false; $this->mPageLinks = array(); $this->mGoodLinks = array(); @@ -135,14 +130,14 @@ class LinkCache { $id = $wgMemc->get( $key = $this->getKey( $title ) ); if( ! is_integer( $id ) ) { if ( $this->mForUpdate ) { - $db =& wfGetDB( DB_MASTER ); + $db = wfGetDB( DB_MASTER ); if ( !( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) ) { $options = array( 'FOR UPDATE' ); } else { $options = array(); } } else { - $db =& wfGetDB( DB_SLAVE ); + $db = wfGetDB( DB_SLAVE ); $options = array(); } diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php index e03b59dd..39341d5d 100644 --- a/includes/LinkFilter.php +++ b/includes/LinkFilter.php @@ -14,7 +14,7 @@ class LinkFilter { /** * @static */ - function matchEntry( $text, $filterEntry ) { + static function matchEntry( $text, $filterEntry ) { $regex = LinkFilter::makeRegex( $filterEntry ); return preg_match( $regex, $text ); } @@ -22,10 +22,10 @@ class LinkFilter { /** * @static */ - function makeRegex( $filterEntry ) { + private static function makeRegex( $filterEntry ) { $regex = '!http://'; if ( substr( $filterEntry, 0, 2 ) == '*.' ) { - $regex .= '([A-Za-z0-9.-]+\.|)'; + $regex .= '(?:[A-Za-z0-9.-]+\.|)'; $filterEntry = substr( $filterEntry, 2 ); } $regex .= preg_quote( $filterEntry, '!' ) . '!Si'; @@ -47,8 +47,10 @@ class LinkFilter { * Asterisks in any other location are considered invalid. * * @static + * @param $filterEntry String: domainparts + * @param $prot String: protocol */ - function makeLike( $filterEntry ) { + public static function makeLike( $filterEntry , $prot = 'http://' ) { if ( substr( $filterEntry, 0, 2 ) == '*.' ) { $subdomains = true; $filterEntry = substr( $filterEntry, 2 ); @@ -74,17 +76,31 @@ class LinkFilter { $path = '/'; $host = $filterEntry; } - $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) ); - if ( substr( $host, -1, 1 ) !== '.' ) { - $host .= '.'; - } - $like = "http://$host"; - - if ( $subdomains ) { - $like .= '%'; - } - if ( !$subdomains || $path !== '/' ) { - $like .= $path . '%'; + // Reverse the labels in the hostname, convert to lower case + // For emails reverse domainpart only + if ( $prot == 'mailto:' && strpos($host, '@') ) { + // complete email adress + $mailparts = explode( '@', $host ); + $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) ); + $host = $domainpart . '@' . $mailparts[0]; + $like = "$prot$host%"; + } elseif ( $prot == 'mailto:' ) { + // domainpart of email adress only. do not add '.' + $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) ); + $like = "$prot$host%"; + } else { + $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) ); + if ( substr( $host, -1, 1 ) !== '.' ) { + $host .= '.'; + } + $like = "$prot$host"; + + if ( $subdomains ) { + $like .= '%'; + } + if ( !$subdomains || $path !== '/' ) { + $like .= $path . '%'; + } } return $like; } diff --git a/includes/Linker.php b/includes/Linker.php index 0eabab2f..b12e2ad0 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -4,19 +4,15 @@ * These functions are used for primarily page content: * links, embedded images, table of contents. Links are * also used in the skin. - * @package MediaWiki - */ - -/** * For the moment, Skin is a descendent class of Linker. * In the future, it should probably be further split * so that ever other bit of the wiki doesn't have to * go loading up Skin to get at it. * - * @package MediaWiki + * @addtogroup Skins */ class Linker { - function Linker() {} + function __construct() {} /** * @deprecated @@ -229,7 +225,7 @@ class Linker { } else { $threshold = $wgUser->getOption('stubthreshold') ; if ( $threshold > 0 ) { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $s = $dbr->selectRow( array( 'page' ), array( 'page_len', @@ -358,16 +354,8 @@ class Linker { * the end of the link. */ function makeStubLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { - $u = $nt->escapeLocalURL( $query ); - - if ( '' == $text ) { - $text = htmlspecialchars( $nt->getPrefixedText() ); - } $style = $this->getInternalLinkAttributesObj( $nt, $text, 'stub' ); - - list( $inside, $trail ) = Linker::splitTrail( $trail ); - $s = "<a href=\"{$u}\"{$style}>{$prefix}{$text}{$inside}</a>{$trail}"; - return $s; + return $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix, '', $style ); } /** @@ -431,25 +419,19 @@ class Linker { } /** @todo document */ - function makeImageLinkObj( $nt, $label, $alt, $align = '', $width = false, $height = false, $framed = false, - $thumb = false, $manual_thumb = '', $page = null ) + function makeImageLinkObj( $nt, $label, $alt, $align = '', $params = array(), $framed = false, + $thumb = false, $manual_thumb = '', $valign = '' ) { - global $wgContLang, $wgUser, $wgThumbLimits, $wgGenerateThumbnailOnParse; + global $wgContLang, $wgUser, $wgThumbLimits; $img = new Image( $nt ); - if ( ! is_null( $page ) ) { - $img->selectPage( $page ); - } - if ( !$img->allowInlineDisplay() && $img->exists() ) { return $this->makeKnownLinkObj( $nt ); } - $url = $img->getViewURL(); $error = $prefix = $postfix = ''; - - wfDebug( "makeImageLinkObj: '$width'x'$height', \"$label\"\n" ); + $page = isset( $params['page'] ) ? $params['page'] : false; if ( 'center' == $align ) { @@ -458,6 +440,19 @@ class Linker { $align = 'none'; } + if ( !isset( $params['width'] ) ) { + $params['width'] = $img->getWidth( $page ); + if( $thumb || $framed ) { + $wopt = $wgUser->getOption( 'thumbsize' ); + + if( !isset( $wgThumbLimits[$wopt] ) ) { + $wopt = User::getDefaultOption( 'thumbsize' ); + } + + $params['width'] = min( $params['width'], $wgThumbLimits[$wopt] ); + } + } + if ( $thumb || $framed ) { # Create a thumbnail. Alignment depends on language @@ -470,70 +465,39 @@ class Linker { if ( $align == '' ) { $align = $wgContLang->isRTL() ? 'left' : 'right'; } - - - if ( $width === false ) { - $wopt = $wgUser->getOption( 'thumbsize' ); - - if( !isset( $wgThumbLimits[$wopt] ) ) { - $wopt = User::getDefaultOption( 'thumbsize' ); - } - - $width = min( $img->getWidth(), $wgThumbLimits[$wopt] ); - } - - return $prefix.$this->makeThumbLinkObj( $img, $label, $alt, $align, $width, $height, $framed, $manual_thumb ).$postfix; + return $prefix.$this->makeThumbLinkObj( $img, $label, $alt, $align, $params, $framed, $manual_thumb ).$postfix; } - if ( $width && $img->exists() ) { - - # Create a resized image, without the additional thumbnail - # features - - if ( $height == false ) - $height = -1; - if ( $manual_thumb == '') { - $thumb = $img->getThumbnail( $width, $height, $wgGenerateThumbnailOnParse ); - if ( $thumb ) { - // In most cases, $width = $thumb->width or $height = $thumb->height. - // If not, we're scaling the image larger than it can be scaled, - // so we send to the browser a smaller thumbnail, and let the client do the scaling. - - if ($height != -1 && $width > $thumb->width * $height / $thumb->height) { - // $height is the limiting factor, not $width - // set $width to the largest it can be, such that the resulting - // scaled height is at most $height - $width = floor($thumb->width * $height / $thumb->height); - } - $height = round($thumb->height * $width / $thumb->width); + if ( $params['width'] && $img->exists() ) { + # Create a resized image, without the additional thumbnail features + $thumb = $img->transform( $params ); + } else { + $thumb = false; + } - wfDebug( "makeImageLinkObj: client-size set to '$width x $height'\n" ); - $url = $thumb->getUrl(); - } else { - $error = htmlspecialchars( $img->getLastError() ); - // Do client-side scaling... - $height = intval( $img->getHeight() * $width / $img->getWidth() ); - } - } + if ( $page ) { + $query = 'page=' . urlencode( $page ); } else { - $width = $img->width; - $height = $img->height; + $query = ''; + } + $u = $nt->getLocalURL( $query ); + $imgAttribs = array( + 'alt' => $alt, + 'longdesc' => $u + ); + if ( $valign ) { + $imgAttribs['style'] = "vertical-align: $valign"; } + $linkAttribs = array( + 'href' => $u, + 'class' => 'image', + 'title' => $alt + ); - wfDebug( "makeImageLinkObj2: '$width'x'$height'\n" ); - $u = $nt->escapeLocalURL(); - if ( $error ) { - $s = $error; - } elseif ( $url == '' ) { + if ( !$thumb ) { $s = $this->makeBrokenImageLinkObj( $img->getTitle() ); - //$s .= "<br />{$alt}<br />{$url}<br />\n"; } else { - $s = '<a href="'.$u.'" class="image" title="'.$alt.'">' . - '<img src="'.$url.'" alt="'.$alt.'" ' . - ( $width - ? ( 'width="'.$width.'" height="'.$height.'" ' ) - : '' ) . - 'longdesc="'.$u.'" /></a>'; + $s = $thumb->toHtml( $imgAttribs, $linkAttribs ); } if ( '' != $align ) { $s = "<div class=\"float{$align}\"><span>{$s}</span></div>"; @@ -545,86 +509,64 @@ class Linker { * Make HTML for a thumbnail including image, border and caption * $img is an Image object */ - function makeThumbLinkObj( $img, $label = '', $alt, $align = 'right', $boxwidth = 180, $boxheight=false, $framed=false , $manual_thumb = "" ) { - global $wgStylePath, $wgContLang, $wgGenerateThumbnailOnParse; + function makeThumbLinkObj( $img, $label = '', $alt, $align = 'right', $params = array(), $framed=false , $manual_thumb = "" ) { + global $wgStylePath, $wgContLang; $thumbUrl = ''; $error = ''; - $width = $height = 0; - if ( $img->exists() ) { - $width = $img->getWidth(); - $height = $img->getHeight(); - } - if ( 0 == $width || 0 == $height ) { - $width = $height = 180; - } - if ( $boxwidth == 0 ) { - $boxwidth = 180; + $page = isset( $params['page'] ) ? $params['page'] : false; + + if ( empty( $params['width'] ) ) { + $params['width'] = 180; } - if ( $framed ) { + $thumb = false; + if ( $manual_thumb != '' ) { + # Use manually specified thumbnail + $manual_title = Title::makeTitleSafe( NS_IMAGE, $manual_thumb ); + if( $manual_title ) { + $manual_img = new Image( $manual_title ); + $thumb = $manual_img->getUnscaledThumb(); + } + } elseif ( $framed ) { // Use image dimensions, don't scale - $boxwidth = $width; - $boxheight = $height; - $thumbUrl = $img->getViewURL(); + $thumb = $img->getUnscaledThumb( $page ); } else { - if ( $boxheight === false ) - $boxheight = -1; - if ( '' == $manual_thumb ) { - $thumb = $img->getThumbnail( $boxwidth, $boxheight, $wgGenerateThumbnailOnParse ); - if ( $thumb ) { - $thumbUrl = $thumb->getUrl(); - $boxwidth = $thumb->width; - $boxheight = $thumb->height; - } else { - $error = $img->getLastError(); - } - } + $thumb = $img->transform( $params ); } - $oboxwidth = $boxwidth + 2; - if ( $manual_thumb != '' ) # Use manually specified thumbnail - { - $manual_title = Title::makeTitleSafe( NS_IMAGE, $manual_thumb ); #new Title ( $manual_thumb ) ; - if( $manual_title ) { - $manual_img = new Image( $manual_title ); - $thumbUrl = $manual_img->getViewURL(); - if ( $manual_img->exists() ) - { - $width = $manual_img->getWidth(); - $height = $manual_img->getHeight(); - $boxwidth = $width ; - $boxheight = $height ; - $oboxwidth = $boxwidth + 2 ; - } - } + if ( $thumb ) { + $outerWidth = $thumb->getWidth() + 2; + } else { + $outerWidth = $params['width'] + 2; } - $u = $img->getEscapeLocalURL(); + $query = $page ? 'page=' . urlencode( $page ) : ''; + $u = $img->getTitle()->getLocalURL( $query ); $more = htmlspecialchars( wfMsg( 'thumbnail-more' ) ); $magnifyalign = $wgContLang->isRTL() ? 'left' : 'right'; $textalign = $wgContLang->isRTL() ? ' style="text-align:right"' : ''; - $s = "<div class=\"thumb t{$align}\"><div class=\"thumbinner\" style=\"width:{$oboxwidth}px;\">"; - if( $thumbUrl == '' ) { - // Couldn't generate thumbnail? Scale the image client-side. - $thumbUrl = $img->getViewURL(); - if( $boxheight == -1 ) { - // Approximate... - $boxheight = intval( $height * $boxwidth / $width ); - } - } - if ( $error ) { - $s .= htmlspecialchars( $error ); + $s = "<div class=\"thumb t{$align}\"><div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">"; + if ( !$thumb ) { + $s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) ); $zoomicon = ''; } elseif( !$img->exists() ) { $s .= $this->makeBrokenImageLinkObj( $img->getTitle() ); $zoomicon = ''; } else { - $s .= '<a href="'.$u.'" class="internal" title="'.$alt.'">'. - '<img src="'.$thumbUrl.'" alt="'.$alt.'" ' . - 'width="'.$boxwidth.'" height="'.$boxheight.'" ' . - 'longdesc="'.$u.'" class="thumbimage" /></a>'; + $imgAttribs = array( + 'alt' => $alt, + 'longdesc' => $u, + 'class' => 'thumbimage' + ); + $linkAttribs = array( + 'href' => $u, + 'class' => 'internal', + 'title' => $alt + ); + + $s .= $thumb->toHtml( $imgAttribs, $linkAttribs ); if ( $framed ) { $zoomicon=""; } else { @@ -680,8 +622,6 @@ class Linker { * * @param $title Title object. * @param $text String: pre-sanitized HTML - * @param $nourl Boolean: Mask absolute URLs, so the parser doesn't - * linkify them (it is currently not context-aware) * @return string HTML * * @public @@ -756,10 +696,10 @@ class Linker { /** * @param $userId Integer: user id in database. * @param $userText String: user name in database. + * @param $redContribsWhenNoEdits Bool: return a red contribs link when the user had no edits and this is true. * @return string HTML fragment with talk and/or block links - * @private */ - function userToolLinks( $userId, $userText ) { + public function userToolLinks( $userId, $userText, $redContribsWhenNoEdits = false ) { global $wgUser, $wgDisableAnonTalk, $wgSysopUserBans; $talkable = !( $wgDisableAnonTalk && 0 == $userId ); $blockable = ( $wgSysopUserBans || 0 == $userId ); @@ -769,9 +709,15 @@ class Linker { $items[] = $this->userTalkLink( $userId, $userText ); } if( $userId ) { + // check if the user has an edit + if( $redContribsWhenNoEdits && User::edits( $userId ) == 0 ) { + $style = "class='new'"; + } else { + $style = ''; + } $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText ); - $items[] = $this->makeKnownLinkObj( $contribsPage , - wfMsgHtml( 'contribslink' ) ); + + $items[] = $this->makeKnownLinkObj( $contribsPage, wfMsgHtml( 'contribslink' ), '', '', '', '', $style ); } if( $blockable && $wgUser->isAllowed( 'block' ) ) { $items[] = $this->blockLink( $userId, $userText ); @@ -785,17 +731,22 @@ class Linker { } /** + * Alias for userToolLinks( $userId, $userText, true ); + */ + public function userToolLinksRedContribs( $userId, $userText ) { + return $this->userToolLinks( $userId, $userText, true ); + } + + + /** * @param $userId Integer: user id in database. * @param $userText String: user name in database. * @return string HTML fragment with user talk link * @private */ function userTalkLink( $userId, $userText ) { - global $wgLang; - $talkname = $wgLang->getNsText( NS_TALK ); # use the shorter name - $userTalkPage = Title::makeTitle( NS_USER_TALK, $userText ); - $userTalkLink = $this->makeLinkObj( $userTalkPage, $talkname ); + $userTalkLink = $this->makeLinkObj( $userTalkPage, wfMsgHtml( 'talkpagelinktext' ) ); return $userTalkLink; } @@ -860,7 +811,7 @@ class Linker { * Since you can't set a default parameter for a reference, I've turned it * temporarily to a value pass. Should be adjusted further. --brion * - * $param string $comment + * @param string $comment * @param mixed $title Title object (to generate link to the section in autocomment) or null * @param bool $local Whether section links should refer to local page */ @@ -1013,7 +964,7 @@ class Linker { /** @todo document */ function tocList($toc) { global $wgJsMimeType; - $title = wfMsgForContent('toc') ; + $title = wfMsgHtml('toc') ; return '<table id="toc" class="toc" summary="' . $title .'"><tr><td>' . '<div id="toctitle"><h2>' . $title . "</h2></div>\n" @@ -1023,8 +974,8 @@ class Linker { . "</ul>\n</td></tr></table>" . '<script type="' . $wgJsMimeType . '">' . ' if (window.showTocToggle) {' - . ' var tocShowText = "' . wfEscapeJsString( wfMsgForContent('showtoc') ) . '";' - . ' var tocHideText = "' . wfEscapeJsString( wfMsgForContent('hidetoc') ) . '";' + . ' var tocShowText = "' . wfEscapeJsString( wfMsg('showtoc') ) . '";' + . ' var tocHideText = "' . wfEscapeJsString( wfMsg('hidetoc') ) . '";' . ' showTocToggle();' . ' } ' . "</script>\n"; @@ -1134,7 +1085,7 @@ class Linker { global $wgUser; wfProfileIn( __METHOD__ ); - $sk =& $wgUser->getSkin(); + $sk = $wgUser->getSkin(); $outText = ''; if ( count( $templates ) > 0 ) { @@ -1182,10 +1133,14 @@ class Linker { */ public function formatSize( $size ) { global $wgLang; + // For small sizes no decimal places necessary + $round = 0; if( $size > 1024 ) { $size = $size / 1024; if( $size > 1024 ) { $size = $size / 1024; + // For MB and bigger two decimal places are smarter + $round = 2; if( $size > 1024 ) { $size = $size / 1024; $msg = 'size-gigabytes'; @@ -1198,10 +1153,59 @@ class Linker { } else { $msg = 'size-bytes'; } - $size = round( $size, 0 ); + $size = round( $size, $round ); return wfMsgHtml( $msg, $wgLang->formatNum( $size ) ); } - + + /** + * Given the id of an interface element, constructs the appropriate title + * and accesskey attributes from the system messages. (Note, this is usu- + * ally the id but isn't always, because sometimes the accesskey needs to + * go on a different element than the id, for reverse-compatibility, etc.) + * + * @param string $name Id of the element, minus prefixes. + * @return string title and accesskey attributes, ready to drop in an + * element (e.g., ' title="This does something [x]" accesskey="x"'). + */ + public function tooltipAndAccesskey($name) { + $out = ''; + + $tooltip = wfMsg('tooltip-'.$name); + if (!wfEmptyMsg('tooltip-'.$name, $tooltip) && $tooltip != '-') { + // Compatibility: formerly some tooltips had [alt-.] hardcoded + $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip ); + $out .= ' title="'.htmlspecialchars($tooltip); + } + $accesskey = wfMsg('accesskey-'.$name); + if ($accesskey && $accesskey != '-' && !wfEmptyMsg('accesskey-'.$name, $accesskey)) { + if ($out) $out .= " [$accesskey]\" accesskey=\"$accesskey\""; + else $out .= " title=\"[$accesskey]\" accesskey=\"$accesskey\""; + } elseif ($out) { + $out .= '"'; + } + return $out; + } + + /** + * Given the id of an interface element, constructs the appropriate title + * attribute from the system messages. (Note, this is usually the id but + * isn't always, because sometimes the accesskey needs to go on a different + * element than the id, for reverse-compatibility, etc.) + * + * @param string $name Id of the element, minus prefixes. + * @return string title attribute, ready to drop in an element + * (e.g., ' title="This does something"'). + */ + public function tooltip($name) { + $out = ''; + + $tooltip = wfMsg('tooltip-'.$name); + if (!wfEmptyMsg('tooltip-'.$name, $tooltip) && $tooltip != '-') { + $out = ' title="'.htmlspecialchars($tooltip).'"'; + } + + return $out; + } } ?> diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php index 9e25bf07..856c665d 100644 --- a/includes/LinksUpdate.php +++ b/includes/LinksUpdate.php @@ -1,19 +1,15 @@ <?php /** - * See deferred.txt - * @package MediaWiki - */ - -/** - * @todo document - * @package MediaWiki + * See docs/deferred.txt + * + * @todo document (e.g. one-sentence top-level class description). */ class LinksUpdate { /**@{{ * @private */ - var $mId, //!< Page ID of the article linked from + var $mId, //!< Page ID of the article linked from $mTitle, //!< Title object of the article linked from $mLinks, //!< Map of title strings to IDs for the links in the document $mImages, //!< DB keys of the images used, in the array key only @@ -41,7 +37,7 @@ class LinksUpdate { } else { $this->mOptions = array( 'FOR UPDATE' ); } - $this->mDb =& wfGetDB( DB_MASTER ); + $this->mDb = wfGetDB( DB_MASTER ); if ( !is_object( $title ) ) { throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " . @@ -172,7 +168,7 @@ class LinksUpdate { wfProfileIn( __METHOD__ ); $batchSize = 100; - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( array( 'templatelinks', 'page' ), array( 'page_namespace', 'page_title' ), array( diff --git a/includes/LoadBalancer.php b/includes/LoadBalancer.php index 396ef865..4ebe26c7 100644 --- a/includes/LoadBalancer.php +++ b/includes/LoadBalancer.php @@ -1,7 +1,6 @@ <?php /** * - * @package MediaWiki */ @@ -9,7 +8,6 @@ * Database load balancing object * * @todo document - * @package MediaWiki */ class LoadBalancer { /* private */ var $mServers, $mConnections, $mLoads, $mGroupLoads; @@ -24,7 +22,7 @@ class LoadBalancer { */ const AVG_STATUS_POLL = 2000; - function LoadBalancer( $servers, $failFunction = false, $waitTimeout = 10, $waitForMasterNow = false ) + function __construct( $servers, $failFunction = false, $waitTimeout = 10, $waitForMasterNow = false ) { $this->mServers = $servers; $this->mFailFunction = $failFunction; @@ -32,7 +30,7 @@ class LoadBalancer { $this->mWriteIndex = -1; $this->mForce = -1; $this->mConnections = array(); - $this->mLastIndex = 1; + $this->mLastIndex = -1; $this->mLoads = array(); $this->mWaitForFile = false; $this->mWaitForPos = false; @@ -97,7 +95,9 @@ class LoadBalancer { # Unset excessively lagged servers $lags = $this->getLagTimes(); foreach ( $lags as $i => $lag ) { - if ( isset( $this->mServers[$i]['max lag'] ) && $lag > $this->mServers[$i]['max lag'] ) { + if ( $i != 0 && isset( $this->mServers[$i]['max lag'] ) && + ( $lag === false || $lag > $this->mServers[$i]['max lag'] ) ) + { unset( $loads[$i] ); } } @@ -504,8 +504,7 @@ class LoadBalancer { * Save master pos to the session and to memcached, if the session exists */ function saveMasterPos() { - global $wgSessionStarted; - if ( $wgSessionStarted && count( $this->mServers ) > 1 ) { + if ( session_id() != '' && count( $this->mServers ) > 1 ) { # If this entire request was served from a slave without opening a connection to the # master (however unlikely that may be), then we can fetch the position from the slave. if ( empty( $this->mConnections[0] ) ) { diff --git a/includes/LogPage.php b/includes/LogPage.php index dd395126..af03bbba 100644 --- a/includes/LogPage.php +++ b/includes/LogPage.php @@ -21,7 +21,6 @@ /** * Contain log classes * - * @package MediaWiki */ /** @@ -29,7 +28,6 @@ * The logs are now kept in a table which is easier to manage and trim * than ever-growing wiki pages. * - * @package MediaWiki */ class LogPage { /* @access private */ @@ -44,7 +42,7 @@ class LogPage { * 'upload', 'move' * @param bool $rc Whether to update recent changes as well as the logging table */ - function LogPage( $type, $rc = true ) { + function __construct( $type, $rc = true ) { $this->type = $type; $this->updateRecentChanges = $rc; } @@ -55,7 +53,7 @@ class LogPage { global $wgUser; $fname = 'LogPage::saveContent'; - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $uid = $wgUser->getID(); $this->timestamp = $now = wfTimestampNow(); @@ -119,7 +117,7 @@ class LogPage { } /** - * @fixme: handle missing log types + * @todo handle missing log types * @static */ function logHeader( $type ) { @@ -134,6 +132,10 @@ class LogPage { global $wgLang, $wgContLang, $wgLogActions; $key = "$type/$action"; + + if( $key == 'patrol/patrol' ) + return PatrolLog::makeActionText( $title, $params, $skin ); + if( isset( $wgLogActions[$key] ) ) { if( is_null( $title ) ) { $rv=wfMsg( $wgLogActions[$key] ); @@ -183,8 +185,13 @@ class LogPage { } } else { array_unshift( $params, $titleLink ); - if ( $translate && $key == 'block/block' ) { - $params[1] = $wgLang->translateBlockExpiry($params[1]); + if ( $key == 'block/block' ) { + if ( $translate ) { + $params[1] = $wgLang->translateBlockExpiry( $params[1] ); + } + $params[2] = isset( $params[2] ) + ? self::formatBlockFlags( $params[2] ) + : ''; } $rv = wfMsgReal( $wgLogActions[$key], $params, true, !$skin ); } @@ -241,6 +248,41 @@ class LogPage { return explode( "\n", $blob ); } } + + /** + * Convert a comma-delimited list of block log flags + * into a more readable (and translated) form + * + * @param $flags Flags to format + * @return string + */ + public static function formatBlockFlags( $flags ) { + $flags = explode( ',', trim( $flags ) ); + if( count( $flags ) > 0 ) { + for( $i = 0; $i < count( $flags ); $i++ ) + $flags[$i] = self::formatBlockFlag( $flags[$i] ); + return '(' . implode( ', ', $flags ) . ')'; + } else { + return ''; + } + } + + /** + * Translate a block log flag if possible + * + * @param $flag Flag to translate + * @return string + */ + public static function formatBlockFlag( $flag ) { + static $messages = array(); + if( !isset( $messages[$flag] ) ) { + $k = 'block-log-flags-' . $flag; + $msg = wfMsg( $k ); + $messages[$flag] = htmlspecialchars( wfEmptyMsg( $k, $msg ) ? $flag : $msg ); + } + return $messages[$flag]; + } + } ?> diff --git a/includes/MacBinary.php b/includes/MacBinary.php index 05c3ce5c..2f6ad4f4 100644 --- a/includes/MacBinary.php +++ b/includes/MacBinary.php @@ -22,12 +22,11 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * - * @package MediaWiki - * @subpackage SpecialPage + * @addtogroup SpecialPage */ class MacBinary { - function MacBinary( $filename ) { + function __construct( $filename ) { $this->open( $filename ); $this->loadHeader(); } @@ -269,4 +268,4 @@ class MacBinary { } } -?>
\ No newline at end of file +?> diff --git a/includes/MagicWord.php b/includes/MagicWord.php index 60bfd0f4..bf72a0c8 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -1,8 +1,7 @@ <?php /** * File for magic words - * @package MediaWiki - * @subpackage Parser + * @addtogroup Parser */ /** @@ -21,7 +20,6 @@ * magic words which are also Parser variables, add a MagicWordwgVariableIDs * hook. Use string keys. * - * @package MediaWiki */ class MagicWord { /**#@+ @@ -55,6 +53,7 @@ class MagicWord { 'localhour', 'numberofarticles', 'numberoffiles', + 'numberofedits', 'sitename', 'server', 'servername', @@ -108,7 +107,7 @@ class MagicWord { /**#@-*/ - function MagicWord($id = 0, $syn = '', $cs = false) { + function __construct($id = 0, $syn = '', $cs = false) { $this->mId = $id; $this->mSynonyms = (array)$syn; $this->mCaseSensitive = $cs; diff --git a/includes/Math.php b/includes/Math.php index 9fa631f7..88934e5f 100644 --- a/includes/Math.php +++ b/includes/Math.php @@ -1,7 +1,6 @@ <?php /** * Contain everything related to <math> </math> parsing - * @package MediaWiki */ /** @@ -11,7 +10,6 @@ * * by Tomasz Wegrzanowski, with additions by Brion Vibber (2003, 2004) * - * @package MediaWiki */ class MathRenderer { var $mode = MW_MATH_MODERN; @@ -22,7 +20,7 @@ class MathRenderer { var $mathml = ''; var $conservativeness = 0; - function MathRenderer( $tex ) { + function __construct( $tex ) { $this->tex = $tex; } @@ -156,7 +154,7 @@ class MathRenderer { $md5_sql = pack('H32', $this->md5); # Binary packed, not hex - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->replace( 'math', array( 'math_inputhash' ), array( 'math_inputhash' => $md5_sql, @@ -185,7 +183,7 @@ class MathRenderer { $fname = 'MathRenderer::_recall'; $this->md5 = md5( $this->tex ); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $rpage = $dbr->selectRow( 'math', array( 'math_outputhash','math_html_conservativeness','math_html','math_mathml' ), array( 'math_inputhash' => pack("H32", $this->md5)), # Binary packed, not hex diff --git a/includes/MediaTransformOutput.php b/includes/MediaTransformOutput.php new file mode 100644 index 00000000..60057e3a --- /dev/null +++ b/includes/MediaTransformOutput.php @@ -0,0 +1,166 @@ +<?php + +/** + * Base class for the output of MediaHandler::doTransform() and Image::transform(). + * + * @addtogroup Media + */ +abstract class MediaTransformOutput { + /** + * Get the width of the output box + */ + function getWidth() { + return $this->width; + } + + /** + * Get the height of the output box + */ + function getHeight() { + return $this->height; + } + + /** + * @return string The thumbnail URL + */ + function getUrl() { + return $this->url; + } + + /** + * @return string Destination file path (local filesystem) + */ + function getPath() { + return $this->path; + } + + /** + * Fetch HTML for this transform output + * @param array $attribs Advisory associative array of HTML attributes supplied + * by the linker. These can be incorporated into the output in any way. + * @param array $linkAttribs Attributes of a suggested enclosing <a> tag. + * May be ignored. + */ + abstract function toHtml( $attribs = array() , $linkAttribs = false ); + + /** + * This will be overridden to return true in error classes + */ + function isError() { + return false; + } + + /** + * Wrap some XHTML text in an anchor tag with the given attributes + */ + protected function linkWrap( $linkAttribs, $contents ) { + if ( $linkAttribs ) { + return Xml::tags( 'a', $linkAttribs, $contents ); + } else { + return $contents; + } + } +} + + +/** + * Media transform output for images + * + * @addtogroup Media + */ +class ThumbnailImage extends MediaTransformOutput { + /** + * @param string $path Filesystem path to the thumb + * @param string $url URL path to the thumb + * @private + */ + function ThumbnailImage( $url, $width, $height, $path = false ) { + $this->url = $url; + # These should be integers when they get here. + # If not, there's a bug somewhere. But let's at + # least produce valid HTML code regardless. + $this->width = round( $width ); + $this->height = round( $height ); + $this->path = $path; + } + + /** + * Return HTML <img ... /> tag for the thumbnail, will include + * width and height attributes and a blank alt text (as required). + * + * You can set or override additional attributes by passing an + * associative array of name => data pairs. The data will be escaped + * for HTML output, so should be in plaintext. + * + * If $linkAttribs is given, the image will be enclosed in an <a> tag. + * + * @param array $attribs + * @param array $linkAttribs + * @return string + * @public + */ + function toHtml( $attribs = array(), $linkAttribs = false ) { + $attribs['src'] = $this->url; + $attribs['width'] = $this->width; + $attribs['height'] = $this->height; + if( !isset( $attribs['alt'] ) ) $attribs['alt'] = ''; + return $this->linkWrap( $linkAttribs, Xml::element( 'img', $attribs ) ); + } + +} + +/** + * Basic media transform error class + * + * @addtogroup Media + */ +class MediaTransformError extends MediaTransformOutput { + var $htmlMsg, $textMsg, $width, $height, $url, $path; + + function __construct( $msg, $width, $height /*, ... */ ) { + $args = array_slice( func_get_args(), 3 ); + $htmlArgs = array_map( 'htmlspecialchars', $args ); + $htmlArgs = array_map( 'nl2br', $htmlArgs ); + + $this->htmlMsg = wfMsgReplaceArgs( htmlspecialchars( wfMsgGetKey( $msg, true ) ), $htmlArgs ); + $this->textMsg = wfMsgReal( $msg, $args ); + $this->width = intval( $width ); + $this->height = intval( $height ); + $this->url = false; + $this->path = false; + } + + function toHtml( $attribs = array(), $linkAttribs = false ) { + return "<table class=\"MediaTransformError\" style=\"" . + "width: {$this->width}px; height: {$this->height}px;\"><tr><td>" . + $this->htmlMsg . + "</td></tr></table>"; + } + + function toText() { + return $this->textMsg; + } + + function getHtmlMsg() { + return $this->htmlMsg; + } + + function isError() { + return true; + } +} + +/** + * Shortcut class for parameter validation errors + * + * @addtogroup Media + */ +class TransformParameterError extends MediaTransformError { + function __construct( $params ) { + parent::__construct( 'thumbnail_error', + max( @$params['width'], 180 ), max( @$params['height'], 180 ), + wfMsg( 'thumbnail_invalid_params' ) ); + } +} + +?> diff --git a/includes/MemcachedSessions.php b/includes/MemcachedSessions.php index e2dc52ca..3bcf5535 100644 --- a/includes/MemcachedSessions.php +++ b/includes/MemcachedSessions.php @@ -6,7 +6,6 @@ * be necessary to change the cookie settings to work across hostnames. * See: http://www.php.net/manual/en/function.session-set-save-handler.php * - * @package MediaWiki */ /** diff --git a/includes/MessageCache.php b/includes/MessageCache.php index a269c620..e2cbf5f6 100644 --- a/includes/MessageCache.php +++ b/includes/MessageCache.php @@ -1,8 +1,7 @@ <?php /** * - * @package MediaWiki - * @subpackage Cache + * @addtogroup Cache */ /** @@ -17,7 +16,6 @@ define( 'MSG_CACHE_VERSION', 1 ); * Message cache * Performs various MediaWiki namespace-related functions * - * @package MediaWiki */ class MessageCache { var $mCache, $mUseCache, $mDisable, $mExpiry; @@ -298,10 +296,10 @@ class MessageCache { * Loads all or main part of cacheable messages from the database */ function loadFromDB() { - global $wgLang, $wgMaxMsgCacheEntrySize; + global $wgMaxMsgCacheEntrySize; wfProfileIn( __METHOD__ ); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $this->mCache = array(); # Load titles for all oversized pages in the MediaWiki namespace @@ -547,7 +545,7 @@ class MessageCache { if ( $type == ' ' ) { $message = substr( $entry, 1 ); - $this->mCache[$title] = $message; + $this->mCache[$title] = $entry; return $message; } elseif ( $entry == '!NONEXISTENT' ) { return false; diff --git a/includes/Metadata.php b/includes/Metadata.php index 4e0d91b7..b995b223 100644 --- a/includes/Metadata.php +++ b/includes/Metadata.php @@ -18,7 +18,6 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * * @author Evan Prodromou <evan@wikitravel.org> - * @package MediaWiki */ /** @@ -74,7 +73,9 @@ function wfCreativeCommonsRdf($article) { function rdfSetup() { global $wgOut, $_SERVER; - $rdftype = wfNegotiateType(wfAcceptToPrefs($_SERVER['HTTP_ACCEPT']), wfAcceptToPrefs(RDF_TYPE_PREFS)); + $httpaccept = isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : null; + + $rdftype = wfNegotiateType(wfAcceptToPrefs($httpaccept), wfAcceptToPrefs(RDF_TYPE_PREFS)); if (!$rdftype) { wfHttpError(406, "Not Acceptable", wfMsg("notacceptable")); diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index ca05dbb3..db35535d 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -1,7 +1,6 @@ <?php /** Module defining helper functions for detecting and dealing with mime types. * - * @package MediaWiki */ /** Defines a set of well known mime types @@ -23,9 +22,10 @@ image/x-bmp bmp image/gif gif image/jpeg jpeg jpg jpe image/png png -image/svg+xml svg +image/svg+xml image/svg svg image/tiff tiff tif image/vnd.djvu djvu +image/x-portable-pixmap ppm text/plain txt text/html html htm video/ogg ogm ogg @@ -51,9 +51,10 @@ image/x-bmp image/bmp [BITMAP] image/gif [BITMAP] image/jpeg [BITMAP] image/png [BITMAP] -image/svg image/svg+xml [DRAWING] +image/svg+xml [DRAWING] image/tiff [BITMAP] image/vnd.djvu [BITMAP] +image/x-portable-pixmap [BITMAP] text/plain [TEXT] text/html [TEXT] video/ogg [VIDEO] @@ -70,13 +71,13 @@ if ($wgLoadFileinfoExtension) { if(!extension_loaded('fileinfo')) dl('fileinfo.' . PHP_SHLIB_SUFFIX); } -/** Implements functions related to mime types such as detection and mapping to -* file extension, -* -* Instances of this class are stateles, there only needs to be one global instance -* of MimeMagic. Please use MimeMagic::singleton() to get that instance. -* @package MediaWiki -*/ +/** + * Implements functions related to mime types such as detection and mapping to + * file extension. + * + * Instances of this class are stateles, there only needs to be one global instance + * of MimeMagic. Please use MimeMagic::singleton() to get that instance. + */ class MimeMagic { /** @@ -105,7 +106,7 @@ class MimeMagic { * * This constructor parses the mime.types and mime.info files and build internal mappings. */ - function MimeMagic() { + function __construct() { /* * --- load mime.types --- */ @@ -149,7 +150,7 @@ class MimeMagic { if (empty($ext)) continue; - if (@$this->mMimeToExt[$mime]) $this->mMimeToExt[$mime] .= ' '.$ext; + if ( !empty($this->mMimeToExt[$mime])) $this->mMimeToExt[$mime] .= ' '.$ext; else $this->mMimeToExt[$mime]= $ext; $extensions= explode(' ',$ext); @@ -158,7 +159,7 @@ class MimeMagic { $e= trim($e); if (empty($e)) continue; - if (@$this->mExtToMime[$e]) $this->mExtToMime[$e] .= ' '.$mime; + if ( !empty($this->mExtToMime[$e])) $this->mExtToMime[$e] .= ' '.$mime; else $this->mExtToMime[$e]= $mime; } } @@ -262,7 +263,7 @@ class MimeMagic { function getTypesForExtension($ext) { $ext= strtolower($ext); - $r= @$this->mExtToMime[$ext]; + $r= isset( $this->mExtToMime[$ext] ) ? $this->mExtToMime[$ext] : null; return $r; } @@ -341,7 +342,7 @@ class MimeMagic { } - /** mime type detection. This uses detectMimeType to detect the mim type of the file, + /** mime type detection. This uses detectMimeType to detect the mime type of the file, * but applies additional checks to determine some well known file formats that may be missed * or misinterpreter by the default mime detection (namely xml based formats like XHTML or SVG). * @@ -399,8 +400,8 @@ class MimeMagic { #print "<br>ANALYSING $file ($mime): doctype= $doctype; tag= $tag<br>"; - if (strpos($doctype,"-//W3C//DTD SVG")===0) $mime= "image/svg"; - elseif ($tag==="svg") $mime= "image/svg"; + if (strpos($doctype,"-//W3C//DTD SVG")===0) $mime= "image/svg+xml"; + elseif ($tag==="svg") $mime= "image/svg+xml"; elseif (strpos($doctype,"-//W3C//DTD XHTML")===0) $mime= "text/html"; elseif ($tag==="html") $mime= "text/html"; } @@ -424,7 +425,9 @@ class MimeMagic { $match= array(); $prog= ""; - if (preg_match('%/?([^\s]+/)(w+)%sim',$head,$match)) $script= $match[2]; + if (preg_match('%/?([^\s]+/)(w+)%sim',$head,$match)) { + $script= $match[2]; // FIXME: $script variable not used; should this be "$prog = $match[2];" instead? + } $mime= "application/x-$prog"; } diff --git a/includes/Namespace.php b/includes/Namespace.php index 78493902..dd67b55a 100644 --- a/includes/Namespace.php +++ b/includes/Namespace.php @@ -1,7 +1,6 @@ <?php /** * Provide things related to namespaces - * @package MediaWiki */ /** @@ -41,7 +40,6 @@ if( is_array( $wgExtraNamespaces ) ) { * These are synonyms for the names given in the language file * Users and translators should not change them * - * @package MediaWiki */ class Namespace { @@ -125,5 +123,19 @@ class Namespace { static function canTalk( $index ) { return( $index >= NS_MAIN ); } + + /** + * Does this namespace contain content, for the purposes + * of calculating statistics, etc? + * + * @param $index Index to check + * @return bool + */ + public static function isContent( $index ) { + global $wgContentNamespaces; + return $index == NS_MAIN || in_array( $index, $wgContentNamespaces ); + } + } + ?> diff --git a/includes/ObjectCache.php b/includes/ObjectCache.php index 2b26cf4e..a493a75c 100644 --- a/includes/ObjectCache.php +++ b/includes/ObjectCache.php @@ -1,7 +1,6 @@ <?php /** - * @package MediaWiki - * @subpackage Cache + * @addtogroup Cache */ /** @@ -9,8 +8,7 @@ * It acts as a memcached server with no RAM, that is, all objects are * cleared the moment they are set. All set operations succeed and all * get operations return null. - * @package MediaWiki - * @subpackage Cache + * @addtogroup Cache */ class FakeMemCachedClient { function add ($key, $val, $exp = 0) { return true; } diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php new file mode 100644 index 00000000..d7e7c90f --- /dev/null +++ b/includes/OutputHandler.php @@ -0,0 +1,64 @@ +<?php + +/** + * Standard output handler for use with ob_start + */ +function wfOutputHandler( $s ) { + global $wgDisableOutputCompression; + $s = wfMangleFlashPolicy( $s ); + if ( !$wgDisableOutputCompression && !ini_get( 'zlib.output_compression' ) ) { + if ( !defined( 'MW_NO_OUTPUT_COMPRESSION' ) ) { + $s = wfGzipHandler( $s ); + } + if ( !ini_get( 'output_handler' ) ) { + wfDoContentLength( strlen( $s ) ); + } + } + return $s; +} + +/** + * Handler that compresses data with gzip if allowed by the Accept header. + * Unlike ob_gzhandler, it works for HEAD requests too. + */ +function wfGzipHandler( $s ) { + if ( function_exists( 'gzencode' ) && !headers_sent() ) { + $tokens = preg_split( '/[,; ]/', $_SERVER['HTTP_ACCEPT_ENCODING'] ); + if ( in_array( 'gzip', $tokens ) ) { + header( 'Content-Encoding: gzip' ); + $s = gzencode( $s, 3 ); + + # Set vary header if it hasn't been set already + $headers = headers_list(); + $foundVary = false; + foreach ( $headers as $header ) { + if ( substr( $header, 0, 5 ) == 'Vary:' ) { + $foundVary = true; + break; + } + } + if ( !$foundVary ) { + header( 'Vary: Accept-Encoding' ); + } + } + } + return $s; +} + +/** + * Mangle flash policy tags which open up the site to XSS attacks. + */ +function wfMangleFlashPolicy( $s ) { + return preg_replace( '/\<\s*cross-domain-policy\s*\>/i', '<NOT-cross-domain-policy>', $s ); +} + +/** + * Add a Content-Length header if possible. This makes it cooperate with squid better. + */ +function wfDoContentLength( $length ) { + if ( !headers_sent() && $_SERVER['SERVER_PROTOCOL'] == 'HTTP/1.0' ) { + header( "Content-Length: $length" ); + } +} + +?> diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 6d3cc0ac..03e832a4 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -2,12 +2,10 @@ if ( ! defined( 'MEDIAWIKI' ) ) die( 1 ); /** - * @package MediaWiki */ /** * @todo document - * @package MediaWiki */ class OutputPage { var $mMetatags, $mKeywords; @@ -34,7 +32,7 @@ class OutputPage { * Constructor * Initialise private variables */ - function OutputPage() { + function __construct() { $this->mMetatags = $this->mKeywords = $this->mLinktags = array(); $this->mHTMLtitle = $this->mPagetitle = $this->mBodytext = $this->mRedirect = $this->mLastModified = @@ -49,6 +47,7 @@ class OutputPage { $this->mParserOptions = null; $this->mSquidMaxage = 0; $this->mScripts = ''; + $this->mHeadItems = array(); $this->mETag = false; $this->mRevisionId = null; $this->mNewSectionLink = false; @@ -71,7 +70,7 @@ class OutputPage { # To add an http-equiv meta tag, precede the name with "http:" function addMeta( $name, $val ) { array_push( $this->mMetatags, array( $name, $val ) ); } function addKeyword( $text ) { array_push( $this->mKeywords, $text ); } - function addScript( $script ) { $this->mScripts .= $script; } + function addScript( $script ) { $this->mScripts .= "\t\t".$script; } /** * Add a self-contained script tag with the given contents @@ -79,10 +78,24 @@ class OutputPage { */ function addInlineScript( $script ) { global $wgJsMimeType; - $this->mScripts .= "<script type=\"$wgJsMimeType\"><!--\n$script\n--></script>"; + $this->mScripts .= "<script type=\"$wgJsMimeType\">/*<![CDATA[*/\n$script\n/*]]>*/</script>"; } - function getScript() { return $this->mScripts; } + function getScript() { + return $this->mScripts . $this->getHeadItems(); + } + + function getHeadItems() { + $s = ''; + foreach ( $this->mHeadItems as $item ) { + $s .= $item; + } + return $s; + } + + function addHeadItem( $name, $value ) { + $this->mHeadItems[$name] = $value; + } function setETag($tag) { $this->mETag = $tag; } function setArticleBodyOnly($only) { $this->mArticleBodyOnly = $only; } @@ -254,7 +267,7 @@ class OutputPage { $lb->setArray( $arr ); $lb->execute(); - $sk =& $wgUser->getSkin(); + $sk = $wgUser->getSkin(); foreach ( $categories as $category => $unused ) { $title = Title::makeTitleSafe( NS_CATEGORY, $category ); $text = $wgContLang->convertHtml( $title->getText() ); @@ -315,14 +328,26 @@ class OutputPage { $this->addWikiTextTitle($text, $title, $linestart); } - private function addWikiTextTitle($text, &$title, $linestart) { + function addWikiTextTitleTidy($text, &$title, $linestart = true) { + $this->addWikiTextTitle( $text, $title, $linestart, true ); + } + + public function addWikiTextTitle($text, &$title, $linestart, $tidy = false) { global $wgParser; + $fname = 'OutputPage:addWikiTextTitle'; wfProfileIn($fname); + wfIncrStats('pcache_not_possible'); - $parserOutput = $wgParser->parse( $text, $title, $this->parserOptions(), + + $popts = $this->parserOptions(); + $popts->setTidy($tidy); + + $parserOutput = $wgParser->parse( $text, $title, $popts, $linestart, true, $this->mRevisionId ); + $this->addParserOutput( $parserOutput ); + wfProfileOut($fname); } @@ -345,6 +370,7 @@ class OutputPage { $this->mSubtitle .= $parserOutput->mSubtitle ; } $this->mNoGallery = $parserOutput->getNoGallery(); + $this->mHeadItems = array_merge( $this->mHeadItems, (array)$parserOutput->mHeadItems ); wfRunHooks( 'OutputPageParserOutput', array( &$this, $parserOutput ) ); } @@ -366,6 +392,7 @@ class OutputPage { * @param string $text * @param Article $article * @param bool $cache + * @deprecated Use Article::outputWikitext */ public function addPrimaryWikiText( $text, $article, $cache = true ) { global $wgParser, $wgUser; @@ -384,17 +411,19 @@ class OutputPage { } /** - * For anything that isn't primary text or interface message - * - * @param string $text - * @param bool $linestart Is this the start of a line? + * @deprecated use addWikiTextTidy() */ public function addSecondaryWikiText( $text, $linestart = true ) { global $wgTitle; - $popts = $this->parserOptions(); - $popts->setTidy(true); - $this->addWikiTextTitle($text, $wgTitle, $linestart); - $popts->setTidy(false); + $this->addWikiTextTitleTidy($text, $wgTitle, $linestart); + } + + /** + * Add wikitext with tidy enabled + */ + public function addWikiTextTidy( $text, $linestart = true ) { + global $wgTitle; + $this->addWikiTextTitleTidy($text, $wgTitle, $linestart); } @@ -476,7 +505,7 @@ class OutputPage { # maintain different caches for logged-in users and non-logged in ones $wgRequest->response()->header( 'Vary: Accept-Encoding, Cookie' ); if( !$this->uncacheableBecauseRequestvars() && $this->mEnableClientCache ) { - if( $wgUseSquid && ! isset( $_COOKIE[ini_get( 'session.name') ] ) && + if( $wgUseSquid && session_id() == '' && ! $this->isPrintable() && $this->mSquidMaxage != 0 ) { if ( $wgUseESI ) { @@ -536,13 +565,16 @@ class OutputPage { if ( $wgUseAjax ) { $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajax.js?$wgStyleVersion\"></script>\n" ); + + wfRunHooks( 'AjaxAddScript', array( &$this ) ); + if( $wgAjaxSearch ) { - $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxsearch.js\"></script>\n" ); + $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxsearch.js?$wgStyleVersion\"></script>\n" ); $this->addScript( "<script type=\"{$wgJsMimeType}\">hookEvent(\"load\", sajax_onload);</script>\n" ); } if( $wgAjaxWatch && $wgUser->isLoggedIn() ) { - $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxwatch.js\"></script>\n" ); + $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxwatch.js?$wgStyleVersion\"></script>\n" ); } } @@ -750,7 +782,7 @@ class OutputPage { $this->returnToMain( false ); } - /** @obsolete */ + /** @deprecated */ public function errorpage( $title, $msg ) { throw new ErrorPageError( $title, $msg ); } @@ -792,10 +824,10 @@ class OutputPage { $groupName = User::getGroupName( $key ); $groupPage = User::getGroupPage( $key ); if( $groupPage ) { - $skin =& $wgUser->getSkin(); - $groups[] = '"'.$skin->makeLinkObj( $groupPage, $groupName ).'"'; + $skin = $wgUser->getSkin(); + $groups[] = $skin->makeLinkObj( $groupPage, $groupName ); } else { - $groups[] = '"'.$groupName.'"'; + $groups[] = $groupName; } } } @@ -860,7 +892,7 @@ class OutputPage { $this->returnToMain( true, $mainPage ); } - /** @obsolete */ + /** @deprecated */ public function databaseError( $fname, $sql, $error, $errno ) { throw new MWException( "OutputPage::databaseError is obsolete\n" ); } @@ -881,10 +913,22 @@ class OutputPage { $this->setPageTitle( wfMsg( 'viewsource' ) ); $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) ); + list( $cascadeSources, $restrictions ) = $wgTitle->getCascadeProtectionSources(); + # Determine if protection is due to the page being a system message # and show an appropriate explanation - if( $wgTitle->getNamespace() == NS_MEDIAWIKI && !$wgUser->isAllowed( 'editinterface' ) ) { + if( $wgTitle->getNamespace() == NS_MEDIAWIKI ) { $this->addWikiText( wfMsg( 'protectedinterface' ) ); + } if ( $cascadeSources && count($cascadeSources) > 0 ) { + $titles = ''; + + foreach ( $cascadeSources as $title ) { + $titles .= '* [[:' . $title->getPrefixedText() . "]]\n"; + } + + $notice = wfMsgExt( 'cascadeprotected', array('parsemag'), count($cascadeSources) ) . "\n$titles"; + + $this->addWikiText( $notice ); } else { $this->addWikiText( wfMsg( 'protectedpagetext' ) ); } @@ -900,17 +944,8 @@ class OutputPage { if( is_string( $source ) ) { $this->addWikiText( wfMsg( 'viewsourcetext' ) ); - if( $source === '' ) { - global $wgTitle; - if ( $wgTitle->getNamespace() == NS_MEDIAWIKI ) { - $source = wfMsgWeirdKey ( $wgTitle->getText() ); - } else { - $source = ''; - } - } $rows = $wgUser->getIntOption( 'rows' ); $cols = $wgUser->getIntOption( 'cols' ); - $text = "\n<textarea name='wpTextbox1' id='wpTextbox1' cols='$cols' rows='$rows' readonly='readonly'>" . htmlspecialchars( $source ) . "\n</textarea>"; $this->addHTML( $text ); @@ -921,32 +956,32 @@ class OutputPage { $this->returnToMain( false ); } - /** @obsolete */ + /** @deprecated */ public function fatalError( $message ) { throw new FatalError( $message ); } - /** @obsolete */ + /** @deprecated */ public function unexpectedValueError( $name, $val ) { throw new FatalError( wfMsg( 'unexpected', $name, $val ) ); } - /** @obsolete */ + /** @deprecated */ public function fileCopyError( $old, $new ) { throw new FatalError( wfMsg( 'filecopyerror', $old, $new ) ); } - /** @obsolete */ + /** @deprecated */ public function fileRenameError( $old, $new ) { throw new FatalError( wfMsg( 'filerenameerror', $old, $new ) ); } - /** @obsolete */ + /** @deprecated */ public function fileDeleteError( $name ) { throw new FatalError( wfMsg( 'filedeleteerror', $name ) ); } - /** @obsolete */ + /** @deprecated */ public function fileNotFoundError( $name ) { throw new FatalError( wfMsg( 'filenotfound', $name ) ); } @@ -1082,6 +1117,7 @@ class OutputPage { $ret .= $sk->getHeadScripts(); $ret .= $this->mScripts; $ret .= $sk->getUserStyles(); + $ret .= $this->getHeadItems(); if ($wgUseTrackbacks && $this->isArticleRelated()) $ret .= $wgTitle->trackbackRDF(); @@ -1118,11 +1154,11 @@ class OutputPage { "/<.*?>/" => '', "/_/" => ' ' ); - $ret .= "<meta name=\"keywords\" content=\"" . + $ret .= "\t\t<meta name=\"keywords\" content=\"" . htmlspecialchars(preg_replace(array_keys($strip), array_values($strip),implode( ",", $this->mKeywords ))) . "\" />\n"; } foreach ( $this->mLinktags as $tag ) { - $ret .= '<link'; + $ret .= "\t\t<link"; foreach( $tag as $attr => $val ) { $ret .= " $attr=\"" . htmlspecialchars( $val ) . "\""; } diff --git a/includes/PageHistory.php b/includes/PageHistory.php index aea0f0ed..b1cf41f0 100644 --- a/includes/PageHistory.php +++ b/includes/PageHistory.php @@ -3,7 +3,6 @@ * Page history * * Split off from Article.php and Skin.php, 2003-12-22 - * @package MediaWiki */ /** @@ -14,9 +13,7 @@ * Construct it by passing in an Article, and call $h->history() to print the * history. * - * @package MediaWiki */ - class PageHistory { const DIR_PREV = 0; const DIR_NEXT = 1; @@ -33,7 +30,7 @@ class PageHistory { * @param Article $article * @returns nothing */ - function PageHistory($article) { + function __construct($article) { global $wgUser; $this->mArticle =& $article; @@ -101,6 +98,8 @@ class PageHistory { $wgOut->redirect( $wgTitle->getLocalURL( "action=history&limit={$limit}&dir=prev" ) ); return; } + + wfRunHooks( 'PageHistoryBeforeList', array( &$this->mArticle ) ); /** * Do the list @@ -159,7 +158,7 @@ class PageHistory { 'class' => 'historysubmit', 'type' => 'submit', 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), - 'title' => wfMsg( 'tooltip-compareselectedversions' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ).' ['.wfMsg( 'accesskey-compareselectedversions' ).']', 'value' => wfMsg( 'compareselectedversions' ), ) ) ) : ''; @@ -179,7 +178,7 @@ class PageHistory { * @return string HTML output for the row */ function historyLine( $row, $next, $counter = '', $notificationtimestamp = false, $latest = false, $firstInList = false ) { - global $wgUser; + global $wgUser, $wgLang; $rev = new Revision( $row ); $rev->setTitle( $this->mTitle ); @@ -199,31 +198,58 @@ class PageHistory { if( $firstInList ) { // We don't currently handle well changing the top revision's settings $del = wfMsgHtml( 'rev-delundel' ); + } else if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) { + // If revision was hidden from sysops + $del = wfMsgHtml( 'rev-delundel' ); } else { $del = $this->mSkin->makeKnownLinkObj( $revdel, wfMsg( 'rev-delundel' ), 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) . '&oldid=' . urlencode( $rev->getId() ) ); } - $s .= "(<small>$del</small>) "; + $s .= " (<small>$del</small>) "; } - $s .= " $link <span class='history-user'>$user</span>"; + $s .= " $link"; + #getUser is safe, but this avoids making the invalid untargeted contribs links + if( $row->rev_deleted & Revision::DELETED_USER ) { + $user = '<span class="history-deleted">' . wfMsg('rev-deleted-user') . '</span>'; + } + $s .= " <span class='history-user'>$user</span>"; if( $row->rev_minor_edit ) { $s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') ); } - $s .= $this->mSkin->revComment( $rev ); + if (!is_null($size = $rev->getSize())) { + if ($size == 0) + $stxt = wfMsgHtml('historyempty'); + else + $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) ); + $s .= " <span class=\"history-size\">$stxt</span>"; + } + + #getComment is safe, but this is better formatted + if( $rev->isDeleted( Revision::DELETED_COMMENT ) ) { + $s .= " <span class=\"history-deleted\"><span class=\"comment\">" . + wfMsgHtml( 'rev-deleted-comment' ) . "</span></span>"; + } else { + $s .= $this->mSkin->revComment( $rev ); + } + if ($notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp)) { $s .= ' <span class="updatedmarker">' . wfMsgHtml( 'updatedmarker' ) . '</span>'; } + #add blurb about text having been deleted if( $row->rev_deleted & Revision::DELETED_TEXT ) { $s .= ' ' . wfMsgHtml( 'deletedrev' ); } if( $wgUser->isAllowed( 'rollback' ) && $latest ) { $s .= ' '.$this->mSkin->generateRollback( $rev ); } + + wfRunHooks( 'PageHistoryLineEnding', array( &$row , &$s ) ); + $s .= "</li>\n"; return $s; @@ -332,7 +358,7 @@ class PageHistory { function getLatestId() { if( is_null( $this->mLatestId ) ) { $id = $this->mTitle->getArticleID(); - $db =& wfGetDB(DB_SLAVE); + $db = wfGetDB(DB_SLAVE); $this->mLatestId = $db->selectField( 'page', "page_latest", array( 'page_id' => $id ), @@ -349,7 +375,7 @@ class PageHistory { function fetchRevisions($limit, $offset, $direction) { $fname = 'PageHistory::fetchRevisions'; - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); if ($direction == PageHistory::DIR_PREV) list($dirs, $oper) = array("ASC", ">="); @@ -365,8 +391,7 @@ class PageHistory { $res = $dbr->select( 'revision', - array('rev_id', 'rev_page', 'rev_text_id', 'rev_user', 'rev_comment', 'rev_user_text', - 'rev_timestamp', 'rev_minor_edit', 'rev_deleted'), + Revision::selectFields(), array_merge(array("rev_page=$page_id"), $offsets), $fname, array('ORDER BY' => "rev_timestamp $dirs", @@ -391,7 +416,7 @@ class PageHistory { if ($wgUser->isAnon() || !$wgShowUpdatedMarker) return $this->mNotificationTimestamp = false; - $dbr =& wfGetDB(DB_SLAVE); + $dbr = wfGetDB(DB_SLAVE); $this->mNotificationTimestamp = $dbr->selectField( 'watchlist', @@ -497,6 +522,9 @@ class PageHistory { } +/** + * @addtogroup Pager + */ class PageHistoryPager extends ReverseChronologicalPager { public $mLastRow = false, $mPageHistory; @@ -508,8 +536,7 @@ class PageHistoryPager extends ReverseChronologicalPager { function getQueryInfo() { return array( 'tables' => 'revision', - 'fields' => array('rev_id', 'rev_page', 'rev_text_id', 'rev_user', 'rev_comment', 'rev_user_text', - 'rev_timestamp', 'rev_minor_edit', 'rev_deleted'), + 'fields' => Revision::selectFields(), 'conds' => array('rev_page' => $this->mPageHistory->mTitle->getArticleID() ), 'options' => array( 'USE INDEX' => 'page_timestamp' ) ); diff --git a/includes/PageQueryPage.php b/includes/PageQueryPage.php new file mode 100644 index 00000000..5b82ebf6 --- /dev/null +++ b/includes/PageQueryPage.php @@ -0,0 +1,26 @@ +<?php + +/** + * Variant of QueryPage which formats the result as a simple link to the page + * + * @package MediaWiki + * @addtogroup SpecialPage + */ +class PageQueryPage extends QueryPage { + + /** + * Format the result as a simple link to the page + * + * @param Skin $skin + * @param object $row Result row + * @return string + */ + public function formatResult( $skin, $row ) { + global $wgContLang; + $title = Title::makeTitleSafe( $row->namespace, $row->title ); + return $skin->makeKnownLinkObj( $title, + htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) ); + } +} + +?> diff --git a/includes/Pager.php b/includes/Pager.php index 0987cc06..a475dc16 100644 --- a/includes/Pager.php +++ b/includes/Pager.php @@ -2,6 +2,7 @@ /** * Basic pager interface. + * @addtogroup Pager */ interface Pager { function getNavigationBar(); @@ -46,6 +47,8 @@ interface Pager { * please see the examples in PageHistory.php and SpecialIpblocklist.php. You just need * to override formatRow(), getQueryInfo() and getIndexField(). Don't forget to call the * parent constructor if you override it. + * + * @addtogroup Pager */ abstract class IndexPager implements Pager { public $mRequest; @@ -69,17 +72,18 @@ abstract class IndexPager implements Pager { public $mResult; function __construct() { - global $wgRequest; + global $wgRequest, $wgUser; $this->mRequest = $wgRequest; - + # NB: the offset is quoted, not validated. It is treated as an arbitrary string # to support the widest variety of index types. Be careful outputting it into # HTML! $this->mOffset = $this->mRequest->getText( 'offset' ); - $this->mLimit = $this->mRequest->getInt( 'limit', $this->mDefaultLimit ); - if ( $this->mLimit <= 0 || $this->mLimit > 50000 ) { - $this->mLimit = $this->mDefaultLimit; - } + + # Use consistent behavior for the limit options + $this->mDefaultLimit = intval( $wgUser->getOption( 'rclimit' ) ); + list( $this->mLimit, /* $offset */ ) = $this->mRequest->getLimitOffset(); + $this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' ); $this->mIndexField = $this->getIndexField(); $this->mDb = wfGetDB( DB_SLAVE ); @@ -386,8 +390,45 @@ abstract class IndexPager implements Pager { abstract function getIndexField(); } + +/** + * IndexPager with an alphabetic list and a formatted navigation bar + * @addtogroup Pager + */ +abstract class AlphabeticPager extends IndexPager { + public $mDefaultDirection = false; + + function __construct() { + parent::__construct(); + } + + /** + * Shamelessly stolen bits from ReverseChronologicalPager, d + * didn't want to do class magic as may be still revamped + */ + function getNavigationBar() { + global $wgLang; + + $linkTexts = array( + 'prev' => wfMsgHtml( "prevn", $this->mLimit ), + 'next' => wfMsgHtml( 'nextn', $this->mLimit ), + 'first' => wfMsgHtml('page_first'), /* Introduced the message */ + 'last' => wfMsgHtml( 'page_last' ) /* Introduced the message */ + ); + + $pagingLinks = $this->getPagingLinks( $linkTexts ); + $limitLinks = $this->getLimitLinks(); + $limits = implode( ' | ', $limitLinks ); + + $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits); + return $this->mNavigationBar; + + } +} + /** * IndexPager with a formatted navigation bar + * @addtogroup Pager */ abstract class ReverseChronologicalPager extends IndexPager { public $mDefaultDirection = true; @@ -413,13 +454,15 @@ abstract class ReverseChronologicalPager extends IndexPager { $limitLinks = $this->getLimitLinks(); $limits = implode( ' | ', $limitLinks ); - $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits); + $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . + wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits); return $this->mNavigationBar; } } /** * Table-based display with a user-selectable sort order + * @addtogroup Pager */ abstract class TablePager extends IndexPager { var $mSort; diff --git a/includes/Parser.php b/includes/Parser.php index 8d67279d..8e36e170 100644 --- a/includes/Parser.php +++ b/includes/Parser.php @@ -2,8 +2,7 @@ /** * File for Parser and related classes * - * @package MediaWiki - * @subpackage Parser + * @addtogroup Parser */ /** @@ -57,9 +56,10 @@ define( 'MW_COLON_STATE_COMMENTDASH', 6 ); define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 ); /** - * PHP Parser - * - * Processes wiki markup + * PHP Parser - Processes wiki markup (which uses a more user-friendly + * syntax, such as "[[link]]" for making links), and provides a one-way + * transformation of that wiki markup it into XHTML output / markup + * (which in turn the browser understands, and can display). * * <pre> * There are four main entry points into the Parser class: @@ -86,10 +86,11 @@ define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 ); * * only within ParserOptions * </pre> * - * @package MediaWiki + * @addtogroup Parser */ class Parser { + const VERSION = MW_PARSER_VERSION; /**#@+ * @private */ @@ -114,7 +115,7 @@ class Parser $ot, // Shortcut alias, see setOutputType() $mRevisionId, // ID to display in {{REVISIONID}} tags $mRevisionTimestamp, // The timestamp of the specified revision ID - $mRevIdForTs; // The revision ID which was used to fetch the timestamp + $mRevIdForTs; // The revision ID which was used to fetch the timestamp /**#@-*/ @@ -162,6 +163,7 @@ class Parser $this->setFunctionHook( 'numberofarticles', array( 'CoreParserFunctions', 'numberofarticles' ), SFH_NO_HASH ); $this->setFunctionHook( 'numberoffiles', array( 'CoreParserFunctions', 'numberoffiles' ), SFH_NO_HASH ); $this->setFunctionHook( 'numberofadmins', array( 'CoreParserFunctions', 'numberofadmins' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofedits', array( 'CoreParserFunctions', 'numberofedits' ), SFH_NO_HASH ); $this->setFunctionHook( 'language', array( 'CoreParserFunctions', 'language' ), SFH_NO_HASH ); $this->setFunctionHook( 'padleft', array( 'CoreParserFunctions', 'padleft' ), SFH_NO_HASH ); $this->setFunctionHook( 'padright', array( 'CoreParserFunctions', 'padright' ), SFH_NO_HASH ); @@ -211,7 +213,7 @@ class Parser 'titles' => array() ); $this->mRevisionTimestamp = $this->mRevisionId = null; - + /** * Prefix for temporary replacement strings for the multipass parser. * \x07 should never appear in input as it's disallowed in XML. @@ -262,7 +264,6 @@ class Parser * Convert wikitext to HTML * Do not call this function recursively. * - * @private * @param string $text Text we want to parse * @param Title &$title A title object * @param array $options @@ -271,7 +272,7 @@ class Parser * @param int $revid number to pass in {{REVISIONID}} * @return ParserOutput a ParserOutput */ - function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) { + public function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) { /** * First pass--just handle <nowiki> sections, pass the rest off * to internalParse() which does all the real work. @@ -724,7 +725,7 @@ class Parser $descriptorspec = array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), - 2 => array('file', '/dev/null', 'a') + 2 => array('file', '/dev/null', 'a') // FIXME: this line in UNIX-specific, it generates a warning on Windows, because /dev/null is not a valid Windows file. ); $pipes = array(); $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes); @@ -872,7 +873,7 @@ class Parser array_push ( $td_history , false ); array_push ( $last_tag_history , '' ); } - else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 ) == '|+' ) { + else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 ) == '|+' ) { // This might be cell elements, td, th or captions if ( substr ( $line , 0 , 2 ) == '|+' ) { $first_character = '+'; @@ -1002,6 +1003,7 @@ class Parser $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ) ); $text = $this->replaceVariables( $text, $args ); + wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) ); // Tables need to come after variable replacement for things to work // properly; putting them before other transformations should keep @@ -1086,7 +1088,7 @@ class Parser } $url = wfMsg( $urlmsg, $id); - $sk =& $this->mOptions->getSkin(); + $sk = $this->mOptions->getSkin(); $la = $sk->getExternalLinkAttributes( $url, $keyword.$id ); $text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>"; } @@ -1287,7 +1289,8 @@ class Parser $output .= '</i>'; if ($state == 'bi') $output .= '</b>'; - if ($state == 'both') + # There might be lonely ''''', so make sure we have a buffer + if ($state == 'both' && $buffer) $output .= '<b><i>'.$buffer.'</i></b>'; return $output; } @@ -1306,7 +1309,7 @@ class Parser $fname = 'Parser::replaceExternalLinks'; wfProfileIn( $fname ); - $sk =& $this->mOptions->getSkin(); + $sk = $this->mOptions->getSkin(); $bits = preg_split( EXT_LINK_BRACKETED, $text, -1, PREG_SPLIT_DELIM_CAPTURE ); @@ -1395,7 +1398,7 @@ class Parser $s = array_shift( $bits ); $i = 0; - $sk =& $this->mOptions->getSkin(); + $sk = $this->mOptions->getSkin(); while ( $i < count( $bits ) ){ $protocol = $bits[$i++]; @@ -1468,7 +1471,7 @@ class Parser * @param string * @return string * @static - * @fixme This can merge genuinely required bits in the path or query string, + * @todo This can merge genuinely required bits in the path or query string, * breaking legit URLs. A proper fix would treat the various parts of * the URL differently; as a workaround, just use the output for * statistical records, not for actual linking/output. @@ -1503,7 +1506,7 @@ class Parser * @private */ function maybeMakeExternalImage( $url ) { - $sk =& $this->mOptions->getSkin(); + $sk = $this->mOptions->getSkin(); $imagesfrom = $this->mOptions->getAllowExternalImagesFrom(); $imagesexception = !empty($imagesfrom); $text = false; @@ -1533,7 +1536,7 @@ class Parser # the % is needed to support urlencoded titles as well if ( !$tc ) { $tc = Title::legalChars() . '#%'; } - $sk =& $this->mOptions->getSkin(); + $sk = $this->mOptions->getSkin(); #split the entire text string on occurences of [[ $a = explode( '[[', ' ' . $s ); @@ -1552,7 +1555,6 @@ class Parser $e2 = wfMsgForContent( 'linkprefix' ); $useLinkPrefixExtension = $wgContLang->linkPrefixExtension(); - if( is_null( $this->mTitle ) ) { throw new MWException( __METHOD__.": \$this->mTitle is null\n" ); } @@ -1569,10 +1571,11 @@ class Parser $prefix = ''; } - if($wgContLang->hasVariants()) + if($wgContLang->hasVariants()) { $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText()); - else + } else { $selflink = array($this->mTitle->getPrefixedText()); + } $useSubpages = $this->areSubpagesAllowed(); wfProfileOut( $fname.'-setup' ); @@ -1626,7 +1629,7 @@ class Parser $might_be_img = true; $text = $m[2]; if ( strpos( $m[1], '%' ) !== false ) { - $m[1] = urldecode($m[1]); + $m[1] = urldecode($m[1]); } $trail = ""; } else { # Invalid form; output directly @@ -1640,7 +1643,7 @@ class Parser # Don't allow internal links to pages containing # PROTO: where PROTO is a valid URL protocol; these # should be external links. - if (preg_match('/^(\b(?:' . wfUrlProtocols() . '))/', $m[1])) { + if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) { $s .= $prefix . '[[' . $line ; continue; } @@ -1723,8 +1726,8 @@ class Parser wfProfileIn( "$fname-interwiki" ); if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) { $this->mOutput->addLanguageLink( $nt->getFullText() ); - $s = rtrim($s . "\n"); - $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail; + $s = rtrim($s . $prefix); + $s .= trim($trail, "\n") == '' ? '': $prefix . $trail; wfProfileOut( "$fname-interwiki" ); continue; } @@ -1778,11 +1781,12 @@ class Parser } } - if( ( in_array( $nt->getPrefixedText(), $selflink ) ) && - ( $nt->getFragment() === '' ) ) { - # Self-links are handled specially; generally de-link and change to bold. - $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail ); - continue; + # Self-link checking + if( $nt->getFragment() === '' ) { + if( in_array( $nt->getPrefixedText(), $selflink, true ) ) { + $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail ); + continue; + } } # Special and Media are pseudo-namespaces; no pages actually exist in them @@ -1862,7 +1866,7 @@ class Parser */ function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { list( $inside, $trail ) = Linker::splitTrail( $trail ); - $sk =& $this->mOptions->getSkin(); + $sk = $this->mOptions->getSkin(); $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix ); return $this->armorLinks( $link ) . $trail; } @@ -1923,9 +1927,9 @@ class Parser # Look at the first character if( $target != '' && $target{0} == '/' ) { # / at end means we don't want the slash to be shown - if( substr( $target, -1, 1 ) == '/' ) { - $target = substr( $target, 1, -1 ); - $noslash = $target; + $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m ); + if( $trailingSlashes ) { + $noslash = $target = substr( $target, 1, -strlen($m[0][0]) ); } else { $noslash = substr( $target, 1 ); } @@ -2134,9 +2138,9 @@ class Parser wfProfileIn( "$fname-paragraph" ); # No prefix (not in list)--go to paragraph mode // XXX: use a stack for nestable elements like span, table and div - $openmatch = preg_match('/(<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t ); + $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t ); $closematch = preg_match( - '/(<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'. + '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'. '<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t ); if ( $openmatch or $closematch ) { $paragraphStack = false; @@ -2538,6 +2542,8 @@ class Parser return $varCache[$index] = $wgContLang->formatNum( SiteStats::pages() ); case 'numberofadmins': return $varCache[$index] = $wgContLang->formatNum( SiteStats::admins() ); + case 'numberofedits': + return $varCache[$index] = $wgContLang->formatNum( SiteStats::edits() ); case 'currenttimestamp': return $varCache[$index] = wfTimestampNow(); case 'localtimestamp': @@ -2852,7 +2858,7 @@ class Parser return $text; } - + /// Clean up argument array - refactored in 1.9 so parserfunctions can use it, too. static function createAssocArgs( $args ) { $assocArgs = array(); @@ -2872,10 +2878,10 @@ class Parser } } } - + return $assocArgs; } - + /** * Return the text of a template, after recursively * replacing any variables or templates within the template. @@ -2888,7 +2894,7 @@ class Parser * @private */ function braceSubstitution( $piece ) { - global $wgContLang, $wgLang, $wgAllowDisplayTitle; + global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces; $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/; wfProfileIn( $fname ); wfProfileIn( __METHOD__.'-setup' ); @@ -3031,6 +3037,19 @@ class Parser } else { # set $text to cached message. $text = $linestart . $this->mTemplates[$piece['title']]; + #treat title for cached page the same as others + $ns = NS_TEMPLATE; + $subpage = ''; + $part1 = $this->maybeDoSubpageLink( $part1, $subpage ); + if ($subpage !== '') { + $ns = $this->mTitle->getNamespace(); + } + $title = Title::newFromText( $part1, $ns ); + //used by include size checking + $titleText = $title->getPrefixedText(); + //used by edit section links + $replaceHeadings = true; + } } @@ -3066,6 +3085,9 @@ class Parser $isHTML = true; $this->disableCache(); } + } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) { + $found = false; //access denied + wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() ); } else { $articleContent = $this->fetchTemplate( $title ); if ( $articleContent !== false ) { @@ -3155,7 +3177,7 @@ class Parser # If the template begins with a table or block-level # element, it should be treated as beginning a new line. - if (!$piece['lineStart'] && preg_match('/^({\\||:|;|#|\*)/', $text)) /*}*/{ + if (!$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) /*}*/{ $text = "\n" . $text; } } elseif ( !$noargs ) { @@ -3271,7 +3293,7 @@ class Parser return wfMsg('scarytranscludedisabled'); $url = $title->getFullUrl( "action=$action" ); - + if (strlen($url) > 255) return wfMsg('scarytranscludetoolong'); return $this->fetchScaryTemplateMaybeFromCache($url); @@ -3279,7 +3301,7 @@ class Parser function fetchScaryTemplateMaybeFromCache($url) { global $wgTranscludeCacheExpiry; - $dbr =& wfGetDB(DB_SLAVE); + $dbr = wfGetDB(DB_SLAVE); $obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'), array('tc_url' => $url)); if ($obj) { @@ -3294,7 +3316,7 @@ class Parser if (!$text) return wfMsg('scarytranscludefailed', $url); - $dbw =& wfGetDB(DB_MASTER); + $dbw = wfGetDB(DB_MASTER); $dbw->replace('transcache', array('tc_url'), array( 'tc_url' => $url, 'tc_time' => time(), @@ -3395,7 +3417,7 @@ class Parser global $wgMaxTocLevel, $wgContLang; $doNumberHeadings = $this->mOptions->getNumberHeadings(); - if( !$this->mTitle->userCanEdit() ) { + if( !$this->mTitle->quickUserCan( 'edit' ) ) { $showEditLink = 0; } else { $showEditLink = $this->mOptions->getEditSection(); @@ -3437,7 +3459,7 @@ class Parser } # We need this to perform operations on the HTML - $sk =& $this->mOptions->getSkin(); + $sk = $this->mOptions->getSkin(); # headline counter $headlineCount = 0; @@ -3723,11 +3745,7 @@ class Parser } # Trim trailing whitespace - # __END__ tag allows for trailing - # whitespace to be deliberately included $text = rtrim( $text ); - $mw =& MagicWord::get( 'end' ); - $mw->matchAndRemove( $text ); return $text; } @@ -3847,7 +3865,7 @@ class Parser wfProfileIn($fname); - if ( $wgTitle ) { + if ( $wgTitle && !( $wgTitle instanceof FakeTitle ) ) { $this->mTitle = $wgTitle; } else { $this->mTitle = Title::newFromText('msg'); @@ -3966,12 +3984,12 @@ class Parser $pdbks = array(); $colours = array(); - $sk =& $this->mOptions->getSkin(); + $sk = $this->mOptions->getSkin(); $linkCache =& LinkCache::singleton(); if ( !empty( $this->mLinkHolders['namespaces'] ) ) { wfProfileIn( $fname.'-check' ); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $page = $dbr->tableName( 'page' ); $threshold = $wgUser->getOption('stubthreshold'); @@ -4161,8 +4179,8 @@ class Parser if(isset($categoryMap[$vardbk])){ $oldkey = $categoryMap[$vardbk]; if($oldkey != $vardbk) - $varCategories[$oldkey]=$vardbk; - } + $varCategories[$oldkey]=$vardbk; + } } // rebuild the categories in original order (if there are replacements) @@ -4303,6 +4321,7 @@ class Parser */ function renderImageGallery( $text, $params ) { $ig = new ImageGallery(); + $ig->setContextTitle( $this->mTitle ); $ig->setShowBytes( false ); $ig->setShowFilename( false ); $ig->setParsing(); @@ -4314,6 +4333,15 @@ class Parser $caption = $this->replaceInternalLinks( $caption ); $ig->setCaptionHtml( $caption ); } + if( isset( $params['perrow'] ) ) { + $ig->setPerRow( $params['perrow'] ); + } + if( isset( $params['widths'] ) ) { + $ig->setWidths( $params['widths'] ); + } + if( isset( $params['heights'] ) ) { + $ig->setHeights( $params['heights'] ); + } $lines = explode( "\n", $text ); foreach ( $lines as $line ) { @@ -4359,10 +4387,8 @@ class Parser * Parse image options text and use it to make an image */ function makeImage( $nt, $options ) { - global $wgUseImageResize, $wgDjvuRenderer; - - $align = ''; - + # @TODO: let the MediaHandler specify its transform parameters + # # Check if the options text is of the form "options|alt text" # Options are: # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang @@ -4372,61 +4398,74 @@ class Parser # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox # * center center the image # * framed Keep original image size, no magnify-button. - - $part = explode( '|', $options); - + # vertical-align values (no % or length right now): + # * baseline + # * sub + # * super + # * top + # * text-top + # * middle + # * bottom + # * text-bottom + + + $part = array_map( 'trim', explode( '|', $options) ); + + $mwAlign = array(); + $alignments = array( 'left', 'right', 'center', 'none', 'baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom' ); + foreach ( $alignments as $alignment ) { + $mwAlign[$alignment] =& MagicWord::get( 'img_'.$alignment ); + } $mwThumb =& MagicWord::get( 'img_thumbnail' ); $mwManualThumb =& MagicWord::get( 'img_manualthumb' ); - $mwLeft =& MagicWord::get( 'img_left' ); - $mwRight =& MagicWord::get( 'img_right' ); - $mwNone =& MagicWord::get( 'img_none' ); $mwWidth =& MagicWord::get( 'img_width' ); - $mwCenter =& MagicWord::get( 'img_center' ); $mwFramed =& MagicWord::get( 'img_framed' ); $mwPage =& MagicWord::get( 'img_page' ); $caption = ''; - $width = $height = $framed = $thumb = false; - $page = null; + $params = array(); + $framed = $thumb = false; $manual_thumb = '' ; + $align = $valign = ''; + $sk = $this->mOptions->getSkin(); foreach( $part as $val ) { - if ( $wgUseImageResize && ! is_null( $mwThumb->matchVariableStartToEnd($val) ) ) { + if ( !is_null( $mwThumb->matchVariableStartToEnd($val) ) ) { $thumb=true; } elseif ( ! is_null( $match = $mwManualThumb->matchVariableStartToEnd($val) ) ) { # use manually specified thumbnail $thumb=true; $manual_thumb = $match; - } elseif ( ! is_null( $mwRight->matchVariableStartToEnd($val) ) ) { - # remember to set an alignment, don't render immediately - $align = 'right'; - } elseif ( ! is_null( $mwLeft->matchVariableStartToEnd($val) ) ) { - # remember to set an alignment, don't render immediately - $align = 'left'; - } elseif ( ! is_null( $mwCenter->matchVariableStartToEnd($val) ) ) { - # remember to set an alignment, don't render immediately - $align = 'center'; - } elseif ( ! is_null( $mwNone->matchVariableStartToEnd($val) ) ) { - # remember to set an alignment, don't render immediately - $align = 'none'; - } elseif ( isset( $wgDjvuRenderer ) && $wgDjvuRenderer - && ! is_null( $match = $mwPage->matchVariableStartToEnd($val) ) ) { - # Select a page in a multipage document - $page = $match; - } elseif ( $wgUseImageResize && !$width && ! is_null( $match = $mwWidth->matchVariableStartToEnd($val) ) ) { - wfDebug( "img_width match: $match\n" ); - # $match is the image width in pixels - $m = array(); - if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $match, $m ) ) { - $width = intval( $m[1] ); - $height = intval( $m[2] ); + } else { + foreach( $alignments as $alignment ) { + if ( ! is_null( $mwAlign[$alignment]->matchVariableStartToEnd($val) ) ) { + switch ( $alignment ) { + case 'left': case 'right': case 'center': case 'none': + $align = $alignment; break; + default: + $valign = $alignment; + } + continue 2; + } + } + if ( ! is_null( $match = $mwPage->matchVariableStartToEnd($val) ) ) { + # Select a page in a multipage document + $params['page'] = $match; + } elseif ( !isset( $params['width'] ) && ! is_null( $match = $mwWidth->matchVariableStartToEnd($val) ) ) { + wfDebug( "img_width match: $match\n" ); + # $match is the image width in pixels + $m = array(); + if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $match, $m ) ) { + $params['width'] = intval( $m[1] ); + $params['height'] = intval( $m[2] ); + } else { + $params['width'] = intval($match); + } + } elseif ( ! is_null( $mwFramed->matchVariableStartToEnd($val) ) ) { + $framed=true; } else { - $width = intval($match); + $caption = $val; } - } elseif ( ! is_null( $mwFramed->matchVariableStartToEnd($val) ) ) { - $framed=true; - } else { - $caption = $val; } } # Strip bad stuff out of the alt text @@ -4439,8 +4478,7 @@ class Parser $alt = Sanitizer::stripAllTags( $alt ); # Linker does the rest - $sk =& $this->mOptions->getSkin(); - return $sk->makeImageLinkObj( $nt, $caption, $alt, $align, $width, $height, $framed, $thumb, $manual_thumb, $page ); + return $sk->makeImageLinkObj( $nt, $caption, $alt, $align, $params, $framed, $thumb, $manual_thumb, $valign ); } /** @@ -4518,24 +4556,6 @@ class Parser $uniq = preg_quote( $this->uniqPrefix(), '/' ); $comment = "(?:$uniq-!--.*?QINU)"; $secs = preg_split( - /* - "/ - ^( - (?:$comment|<\/?noinclude>)* # Initial comments will be stripped - (?: - (=+) # Should this be limited to 6? - .+? # Section title... - \\2 # Ending = count must match start - | - ^ - <h([1-6])\b.*?> - .*? - <\/h\\3\s*> - ) - (?:$comment|<\/?noinclude>|\s+)* # Trailing whitespace ok - )$ - /mix", - */ "/ ( ^ @@ -4559,7 +4579,8 @@ class Parser // "Section 0" returns the content before any other section. $rv = $secs[0]; } else { - $rv = ""; + //track missing section, will replace if found. + $rv = $newtext; } } elseif( $mode == "replace" ) { if( $section == 0 ) { @@ -4614,8 +4635,10 @@ class Parser } } } - # reinsert stripped tags - $rv = trim( $stripState->unstripBoth( $rv ) ); + if (is_string($rv)) + # reinsert stripped tags + $rv = trim( $stripState->unstripBoth( $rv ) ); + return $rv; } @@ -4628,34 +4651,35 @@ class Parser * * @param $text String: text to look in * @param $section Integer: section number + * @param $deftext: default to return if section is not found * @return string text of the requested section */ - function getSection( $text, $section ) { - return $this->extractSections( $text, $section, "get" ); + public function getSection( $text, $section, $deftext='' ) { + return $this->extractSections( $text, $section, "get", $deftext ); } - function replaceSection( $oldtext, $section, $text ) { + public function replaceSection( $oldtext, $section, $text ) { return $this->extractSections( $oldtext, $section, "replace", $text ); } /** - * Get the timestamp associated with the current revision, adjusted for + * Get the timestamp associated with the current revision, adjusted for * the default server-local timestamp */ function getRevisionTimestamp() { if ( is_null( $this->mRevisionTimestamp ) ) { wfProfileIn( __METHOD__ ); global $wgContLang; - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', array( 'rev_id' => $this->mRevisionId ), __METHOD__ ); - + // Normalize timestamp to internal MW format for timezone processing. // This has the added side-effect of replacing a null value with // the current time, which gives us more sensible behavior for // previews. $timestamp = wfTimestamp( TS_MW, $timestamp ); - + // The cryptic '' timezone parameter tells to use the site-default // timezone offset instead of the user settings. // @@ -4663,12 +4687,12 @@ class Parser // to other users, and potentially even used inside links and such, // it needs to be consistent for all visitors. $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' ); - + wfProfileOut( __METHOD__ ); } return $this->mRevisionTimestamp; } - + /** * Mutator for $mDefaultSort * @@ -4677,7 +4701,7 @@ class Parser public function setDefaultSort( $sort ) { $this->mDefaultSort = $sort; } - + /** * Accessor for $mDefaultSort * Will use the title/prefixed title if none is set @@ -4693,241 +4717,13 @@ class Parser : $this->mTitle->getPrefixedText(); } } - -} - -/** - * @todo document - * @package MediaWiki - */ -class ParserOutput -{ - var $mText, # The output text - $mLanguageLinks, # List of the full text of language links, in the order they appear - $mCategories, # Map of category names to sort keys - $mContainsOldMagic, # Boolean variable indicating if the input contained variables like {{CURRENTDAY}} - $mCacheTime, # Time when this object was generated, or -1 for uncacheable. Used in ParserCache. - $mVersion, # Compatibility check - $mTitleText, # title text of the chosen language variant - $mLinks, # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken. - $mTemplates, # 2-D map of NS/DBK to ID for the template references. ID=zero for broken. - $mImages, # DB keys of the images used, in the array key only - $mExternalLinks, # External link URLs, in the key only - $mHTMLtitle, # Display HTML title - $mSubtitle, # Additional subtitle - $mNewSection, # Show a new section link? - $mNoGallery; # No gallery on category page? (__NOGALLERY__) - - function ParserOutput( $text = '', $languageLinks = array(), $categoryLinks = array(), - $containsOldMagic = false, $titletext = '' ) - { - $this->mText = $text; - $this->mLanguageLinks = $languageLinks; - $this->mCategories = $categoryLinks; - $this->mContainsOldMagic = $containsOldMagic; - $this->mCacheTime = ''; - $this->mVersion = MW_PARSER_VERSION; - $this->mTitleText = $titletext; - $this->mLinks = array(); - $this->mTemplates = array(); - $this->mImages = array(); - $this->mExternalLinks = array(); - $this->mHTMLtitle = "" ; - $this->mSubtitle = "" ; - $this->mNewSection = false; - $this->mNoGallery = false; - } - function getText() { return $this->mText; } - function &getLanguageLinks() { return $this->mLanguageLinks; } - function getCategoryLinks() { return array_keys( $this->mCategories ); } - function &getCategories() { return $this->mCategories; } - function getCacheTime() { return $this->mCacheTime; } - function getTitleText() { return $this->mTitleText; } - function &getLinks() { return $this->mLinks; } - function &getTemplates() { return $this->mTemplates; } - function &getImages() { return $this->mImages; } - function &getExternalLinks() { return $this->mExternalLinks; } - function getNoGallery() { return $this->mNoGallery; } - function getSubtitle() { return $this->mSubtitle; } - - function containsOldMagic() { return $this->mContainsOldMagic; } - function setText( $text ) { return wfSetVar( $this->mText, $text ); } - function setLanguageLinks( $ll ) { return wfSetVar( $this->mLanguageLinks, $ll ); } - function setCategoryLinks( $cl ) { return wfSetVar( $this->mCategories, $cl ); } - function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); } - function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); } - function setTitleText( $t ) { return wfSetVar($this->mTitleText, $t); } - function setSubtitle( $st ) { return wfSetVar( $this->mSubtitle, $st ); } - - function addCategory( $c, $sort ) { $this->mCategories[$c] = $sort; } - function addImage( $name ) { $this->mImages[$name] = 1; } - function addLanguageLink( $t ) { $this->mLanguageLinks[] = $t; } - function addExternalLink( $url ) { $this->mExternalLinks[$url] = 1; } - - function setNewSection( $value ) { - $this->mNewSection = (bool)$value; - } - function getNewSection() { - return (bool)$this->mNewSection; - } - - function addLink( $title, $id = null ) { - $ns = $title->getNamespace(); - $dbk = $title->getDBkey(); - if ( !isset( $this->mLinks[$ns] ) ) { - $this->mLinks[$ns] = array(); - } - if ( is_null( $id ) ) { - $id = $title->getArticleID(); - } - $this->mLinks[$ns][$dbk] = $id; - } - - function addTemplate( $title, $id ) { - $ns = $title->getNamespace(); - $dbk = $title->getDBkey(); - if ( !isset( $this->mTemplates[$ns] ) ) { - $this->mTemplates[$ns] = array(); - } - $this->mTemplates[$ns][$dbk] = $id; - } - - /** - * Return true if this cached output object predates the global or - * per-article cache invalidation timestamps, or if it comes from - * an incompatible older version. - * - * @param string $touched the affected article's last touched timestamp - * @return bool - * @public - */ - function expired( $touched ) { - global $wgCacheEpoch; - return $this->getCacheTime() == -1 || // parser says it's uncacheable - $this->getCacheTime() < $touched || - $this->getCacheTime() <= $wgCacheEpoch || - !isset( $this->mVersion ) || - version_compare( $this->mVersion, MW_PARSER_VERSION, "lt" ); - } } /** - * Set options of the Parser - * @todo document - * @package MediaWiki + * @todo document, briefly. + * @addtogroup Parser */ -class ParserOptions -{ - # All variables are supposed to be private in theory, although in practise this is not the case. - var $mUseTeX; # Use texvc to expand <math> tags - var $mUseDynamicDates; # Use DateFormatter to format dates - var $mInterwikiMagic; # Interlanguage links are removed and returned in an array - var $mAllowExternalImages; # Allow external images inline - var $mAllowExternalImagesFrom; # If not, any exception? - var $mSkin; # Reference to the preferred skin - var $mDateFormat; # Date format index - var $mEditSection; # Create "edit section" links - var $mNumberHeadings; # Automatically number headings - var $mAllowSpecialInclusion; # Allow inclusion of special pages - var $mTidy; # Ask for tidy cleanup - var $mInterfaceMessage; # Which lang to call for PLURAL and GRAMMAR - var $mMaxIncludeSize; # Maximum size of template expansions, in bytes - var $mRemoveComments; # Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS - - var $mUser; # Stored user object, just used to initialise the skin - - function getUseTeX() { return $this->mUseTeX; } - function getUseDynamicDates() { return $this->mUseDynamicDates; } - function getInterwikiMagic() { return $this->mInterwikiMagic; } - function getAllowExternalImages() { return $this->mAllowExternalImages; } - function getAllowExternalImagesFrom() { return $this->mAllowExternalImagesFrom; } - function getEditSection() { return $this->mEditSection; } - function getNumberHeadings() { return $this->mNumberHeadings; } - function getAllowSpecialInclusion() { return $this->mAllowSpecialInclusion; } - function getTidy() { return $this->mTidy; } - function getInterfaceMessage() { return $this->mInterfaceMessage; } - function getMaxIncludeSize() { return $this->mMaxIncludeSize; } - function getRemoveComments() { return $this->mRemoveComments; } - - function &getSkin() { - if ( !isset( $this->mSkin ) ) { - $this->mSkin = $this->mUser->getSkin(); - } - return $this->mSkin; - } - - function getDateFormat() { - if ( !isset( $this->mDateFormat ) ) { - $this->mDateFormat = $this->mUser->getDatePreference(); - } - return $this->mDateFormat; - } - - function setUseTeX( $x ) { return wfSetVar( $this->mUseTeX, $x ); } - function setUseDynamicDates( $x ) { return wfSetVar( $this->mUseDynamicDates, $x ); } - function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); } - function setAllowExternalImages( $x ) { return wfSetVar( $this->mAllowExternalImages, $x ); } - function setAllowExternalImagesFrom( $x ) { return wfSetVar( $this->mAllowExternalImagesFrom, $x ); } - function setDateFormat( $x ) { return wfSetVar( $this->mDateFormat, $x ); } - function setEditSection( $x ) { return wfSetVar( $this->mEditSection, $x ); } - function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); } - function setAllowSpecialInclusion( $x ) { return wfSetVar( $this->mAllowSpecialInclusion, $x ); } - function setTidy( $x ) { return wfSetVar( $this->mTidy, $x); } - function setSkin( $x ) { $this->mSkin = $x; } - function setInterfaceMessage( $x ) { return wfSetVar( $this->mInterfaceMessage, $x); } - function setMaxIncludeSize( $x ) { return wfSetVar( $this->mMaxIncludeSize, $x ); } - function setRemoveComments( $x ) { return wfSetVar( $this->mRemoveComments, $x ); } - - function ParserOptions( $user = null ) { - $this->initialiseFromUser( $user ); - } - - /** - * Get parser options - * @static - */ - static function newFromUser( $user ) { - return new ParserOptions( $user ); - } - - /** Get user options */ - function initialiseFromUser( $userInput ) { - global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages; - global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion, $wgMaxArticleSize; - $fname = 'ParserOptions::initialiseFromUser'; - wfProfileIn( $fname ); - if ( !$userInput ) { - global $wgUser; - if ( isset( $wgUser ) ) { - $user = $wgUser; - } else { - $user = new User; - } - } else { - $user =& $userInput; - } - - $this->mUser = $user; - - $this->mUseTeX = $wgUseTeX; - $this->mUseDynamicDates = $wgUseDynamicDates; - $this->mInterwikiMagic = $wgInterwikiMagic; - $this->mAllowExternalImages = $wgAllowExternalImages; - $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom; - $this->mSkin = null; # Deferred - $this->mDateFormat = null; # Deferred - $this->mEditSection = true; - $this->mNumberHeadings = $user->getOption( 'numberheadings' ); - $this->mAllowSpecialInclusion = $wgAllowSpecialInclusion; - $this->mTidy = false; - $this->mInterfaceMessage = false; - $this->mMaxIncludeSize = $wgMaxArticleSize * 1024; - $this->mRemoveComments = true; - wfProfileOut( $fname ); - } -} - class OnlyIncludeReplacer { var $output = ''; @@ -4940,6 +4736,10 @@ class OnlyIncludeReplacer { } } +/** + * @todo document, briefly. + * @addtogroup Parser + */ class StripState { var $general, $nowiki; diff --git a/includes/ParserCache.php b/includes/ParserCache.php index 37a42b7f..1489fcf9 100644 --- a/includes/ParserCache.php +++ b/includes/ParserCache.php @@ -1,13 +1,8 @@ <?php /** * - * @package MediaWiki - * @subpackage Cache - */ - -/** - * - * @package MediaWiki + * @addtogroup Cache + * @todo document */ class ParserCache { /** @@ -28,14 +23,14 @@ class ParserCache { * * @param object $memCached */ - function ParserCache( &$memCached ) { + function __construct( &$memCached ) { $this->mMemc =& $memCached; } function getKey( &$article, &$user ) { global $action; $hash = $user->getPageRenderingHash(); - if( !$article->mTitle->userCanEdit() ) { + if( !$article->mTitle->quickUserCan( 'edit' ) ) { // section edit links are suppressed even if the user has them on $edit = '!edit=0'; } else { @@ -95,31 +90,30 @@ class ParserCache { function save( $parserOutput, &$article, &$user ){ global $wgParserCacheExpireTime; $key = $this->getKey( $article, $user ); - + if( $parserOutput->getCacheTime() != -1 ) { - + $now = wfTimestampNow(); $parserOutput->setCacheTime( $now ); - + // Save the timestamp so that we don't have to load the revision row on view $parserOutput->mTimestamp = $article->getTimestamp(); - + $parserOutput->mText .= "\n<!-- Saved in parser cache with key $key and timestamp $now -->\n"; wfDebug( "Saved in parser cache with key $key and timestamp $now\n" ); - + if( $parserOutput->containsOldMagic() ){ $expire = 3600; # 1 hour } else { $expire = $wgParserCacheExpireTime; } $this->mMemc->set( $key, $parserOutput, $expire ); - + } else { wfDebug( "Parser output was marked as uncacheable and has not been saved.\n" ); } - } - + } ?> diff --git a/includes/ParserOptions.php b/includes/ParserOptions.php new file mode 100644 index 00000000..e335720f --- /dev/null +++ b/includes/ParserOptions.php @@ -0,0 +1,119 @@ +<?php + +/** + * Set options of the Parser + * @todo document + * @addtogroup Parser + */ +class ParserOptions +{ + # All variables are supposed to be private in theory, although in practise this is not the case. + var $mUseTeX; # Use texvc to expand <math> tags + var $mUseDynamicDates; # Use DateFormatter to format dates + var $mInterwikiMagic; # Interlanguage links are removed and returned in an array + var $mAllowExternalImages; # Allow external images inline + var $mAllowExternalImagesFrom; # If not, any exception? + var $mSkin; # Reference to the preferred skin + var $mDateFormat; # Date format index + var $mEditSection; # Create "edit section" links + var $mNumberHeadings; # Automatically number headings + var $mAllowSpecialInclusion; # Allow inclusion of special pages + var $mTidy; # Ask for tidy cleanup + var $mInterfaceMessage; # Which lang to call for PLURAL and GRAMMAR + var $mMaxIncludeSize; # Maximum size of template expansions, in bytes + var $mRemoveComments; # Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS + + var $mUser; # Stored user object, just used to initialise the skin + + function getUseTeX() { return $this->mUseTeX; } + function getUseDynamicDates() { return $this->mUseDynamicDates; } + function getInterwikiMagic() { return $this->mInterwikiMagic; } + function getAllowExternalImages() { return $this->mAllowExternalImages; } + function getAllowExternalImagesFrom() { return $this->mAllowExternalImagesFrom; } + function getEditSection() { return $this->mEditSection; } + function getNumberHeadings() { return $this->mNumberHeadings; } + function getAllowSpecialInclusion() { return $this->mAllowSpecialInclusion; } + function getTidy() { return $this->mTidy; } + function getInterfaceMessage() { return $this->mInterfaceMessage; } + function getMaxIncludeSize() { return $this->mMaxIncludeSize; } + function getRemoveComments() { return $this->mRemoveComments; } + + function getSkin() { + if ( !isset( $this->mSkin ) ) { + $this->mSkin = $this->mUser->getSkin(); + } + return $this->mSkin; + } + + function getDateFormat() { + if ( !isset( $this->mDateFormat ) ) { + $this->mDateFormat = $this->mUser->getDatePreference(); + } + return $this->mDateFormat; + } + + function setUseTeX( $x ) { return wfSetVar( $this->mUseTeX, $x ); } + function setUseDynamicDates( $x ) { return wfSetVar( $this->mUseDynamicDates, $x ); } + function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); } + function setAllowExternalImages( $x ) { return wfSetVar( $this->mAllowExternalImages, $x ); } + function setAllowExternalImagesFrom( $x ) { return wfSetVar( $this->mAllowExternalImagesFrom, $x ); } + function setDateFormat( $x ) { return wfSetVar( $this->mDateFormat, $x ); } + function setEditSection( $x ) { return wfSetVar( $this->mEditSection, $x ); } + function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); } + function setAllowSpecialInclusion( $x ) { return wfSetVar( $this->mAllowSpecialInclusion, $x ); } + function setTidy( $x ) { return wfSetVar( $this->mTidy, $x); } + function setSkin( $x ) { $this->mSkin = $x; } + function setInterfaceMessage( $x ) { return wfSetVar( $this->mInterfaceMessage, $x); } + function setMaxIncludeSize( $x ) { return wfSetVar( $this->mMaxIncludeSize, $x ); } + function setRemoveComments( $x ) { return wfSetVar( $this->mRemoveComments, $x ); } + + function __construct( $user = null ) { + $this->initialiseFromUser( $user ); + } + + /** + * Get parser options + * @static + */ + static function newFromUser( $user ) { + return new ParserOptions( $user ); + } + + /** Get user options */ + function initialiseFromUser( $userInput ) { + global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages; + global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion, $wgMaxArticleSize; + $fname = 'ParserOptions::initialiseFromUser'; + wfProfileIn( $fname ); + if ( !$userInput ) { + global $wgUser; + if ( isset( $wgUser ) ) { + $user = $wgUser; + } else { + $user = new User; + } + } else { + $user =& $userInput; + } + + $this->mUser = $user; + + $this->mUseTeX = $wgUseTeX; + $this->mUseDynamicDates = $wgUseDynamicDates; + $this->mInterwikiMagic = $wgInterwikiMagic; + $this->mAllowExternalImages = $wgAllowExternalImages; + $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom; + $this->mSkin = null; # Deferred + $this->mDateFormat = null; # Deferred + $this->mEditSection = true; + $this->mNumberHeadings = $user->getOption( 'numberheadings' ); + $this->mAllowSpecialInclusion = $wgAllowSpecialInclusion; + $this->mTidy = false; + $this->mInterfaceMessage = false; + $this->mMaxIncludeSize = $wgMaxArticleSize * 1024; + $this->mRemoveComments = true; + wfProfileOut( $fname ); + } +} + +?> diff --git a/includes/ParserOutput.php b/includes/ParserOutput.php new file mode 100644 index 00000000..03f1819c --- /dev/null +++ b/includes/ParserOutput.php @@ -0,0 +1,133 @@ +<?php +/** + * @todo document + * @addtogroup Parser + */ +class ParserOutput +{ + var $mText, # The output text + $mLanguageLinks, # List of the full text of language links, in the order they appear + $mCategories, # Map of category names to sort keys + $mContainsOldMagic, # Boolean variable indicating if the input contained variables like {{CURRENTDAY}} + $mCacheTime, # Time when this object was generated, or -1 for uncacheable. Used in ParserCache. + $mVersion, # Compatibility check + $mTitleText, # title text of the chosen language variant + $mLinks, # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken. + $mTemplates, # 2-D map of NS/DBK to ID for the template references. ID=zero for broken. + $mImages, # DB keys of the images used, in the array key only + $mExternalLinks, # External link URLs, in the key only + $mHTMLtitle, # Display HTML title + $mSubtitle, # Additional subtitle + $mNewSection, # Show a new section link? + $mNoGallery, # No gallery on category page? (__NOGALLERY__) + $mHeadItems; # Items to put in the <head> section + + function ParserOutput( $text = '', $languageLinks = array(), $categoryLinks = array(), + $containsOldMagic = false, $titletext = '' ) + { + $this->mText = $text; + $this->mLanguageLinks = $languageLinks; + $this->mCategories = $categoryLinks; + $this->mContainsOldMagic = $containsOldMagic; + $this->mCacheTime = ''; + $this->mVersion = Parser::VERSION; + $this->mTitleText = $titletext; + $this->mLinks = array(); + $this->mTemplates = array(); + $this->mImages = array(); + $this->mExternalLinks = array(); + $this->mHTMLtitle = "" ; + $this->mSubtitle = "" ; + $this->mNewSection = false; + $this->mNoGallery = false; + $this->mHeadItems = array(); + } + + function getText() { return $this->mText; } + function &getLanguageLinks() { return $this->mLanguageLinks; } + function getCategoryLinks() { return array_keys( $this->mCategories ); } + function &getCategories() { return $this->mCategories; } + function getCacheTime() { return $this->mCacheTime; } + function getTitleText() { return $this->mTitleText; } + function &getLinks() { return $this->mLinks; } + function &getTemplates() { return $this->mTemplates; } + function &getImages() { return $this->mImages; } + function &getExternalLinks() { return $this->mExternalLinks; } + function getNoGallery() { return $this->mNoGallery; } + function getSubtitle() { return $this->mSubtitle; } + + function containsOldMagic() { return $this->mContainsOldMagic; } + function setText( $text ) { return wfSetVar( $this->mText, $text ); } + function setLanguageLinks( $ll ) { return wfSetVar( $this->mLanguageLinks, $ll ); } + function setCategoryLinks( $cl ) { return wfSetVar( $this->mCategories, $cl ); } + function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); } + function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); } + function setTitleText( $t ) { return wfSetVar($this->mTitleText, $t); } + function setSubtitle( $st ) { return wfSetVar( $this->mSubtitle, $st ); } + + function addCategory( $c, $sort ) { $this->mCategories[$c] = $sort; } + function addImage( $name ) { $this->mImages[$name] = 1; } + function addLanguageLink( $t ) { $this->mLanguageLinks[] = $t; } + function addExternalLink( $url ) { $this->mExternalLinks[$url] = 1; } + + function setNewSection( $value ) { + $this->mNewSection = (bool)$value; + } + function getNewSection() { + return (bool)$this->mNewSection; + } + + function addLink( $title, $id = null ) { + $ns = $title->getNamespace(); + $dbk = $title->getDBkey(); + if ( !isset( $this->mLinks[$ns] ) ) { + $this->mLinks[$ns] = array(); + } + if ( is_null( $id ) ) { + $id = $title->getArticleID(); + } + $this->mLinks[$ns][$dbk] = $id; + } + + function addTemplate( $title, $id ) { + $ns = $title->getNamespace(); + $dbk = $title->getDBkey(); + if ( !isset( $this->mTemplates[$ns] ) ) { + $this->mTemplates[$ns] = array(); + } + $this->mTemplates[$ns][$dbk] = $id; + } + + /** + * Return true if this cached output object predates the global or + * per-article cache invalidation timestamps, or if it comes from + * an incompatible older version. + * + * @param string $touched the affected article's last touched timestamp + * @return bool + * @public + */ + function expired( $touched ) { + global $wgCacheEpoch; + return $this->getCacheTime() == -1 || // parser says it's uncacheable + $this->getCacheTime() < $touched || + $this->getCacheTime() <= $wgCacheEpoch || + !isset( $this->mVersion ) || + version_compare( $this->mVersion, Parser::VERSION, "lt" ); + } + + /** + * Add some text to the <head>. + * If $tag is set, the section with that tag will only be included once + * in a given page. + */ + function addHeadItem( $section, $tag = false ) { + if ( $tag !== false ) { + $this->mHeadItems[$tag] = $section; + } else { + $this->mHeadItems[] = $section; + } + } +} + +?> diff --git a/includes/PatrolLog.php b/includes/PatrolLog.php new file mode 100644 index 00000000..a22839ff --- /dev/null +++ b/includes/PatrolLog.php @@ -0,0 +1,83 @@ +<?php + +/** + * Class containing static functions for working with + * logs of patrol events + * + * @author Rob Church <robchur@gmail.com> + */ +class PatrolLog { + + /** + * Record a log event for a change being patrolled + * + * @param mixed $change Change identifier or RecentChange object + * @param bool $auto Was this patrol event automatic? + */ + public static function record( $change, $auto = false ) { + if( !( is_object( $change ) && $change instanceof RecentChange ) ) { + $change = RecentChange::newFromId( $change ); + if( !is_object( $change ) ) + return false; + } + $title = Title::makeTitleSafe( $change->getAttribute( 'rc_namespace' ), + $change->getAttribute( 'rc_title' ) ); + if( is_object( $title ) ) { + $params = self::buildParams( $change, $auto ); + $log = new LogPage( 'patrol', false ); # False suppresses RC entries + $log->addEntry( 'patrol', $title, '', $params ); + return true; + } else { + return false; + } + } + + /** + * Generate the log action text corresponding to a patrol log item + * + * @param Title $title Title of the page that was patrolled + * @param array $params Log parameters (from logging.log_params) + * @param Skin $skin Skin to use for building links, etc. + * @return string + */ + public static function makeActionText( $title, $params, $skin ) { + # This is a bit of a hack, but...if $skin is not a Skin, then *do nothing* + # -- this is fine, because the action text we would be queried for under + # these conditions would have gone into recentchanges, which we aren't + # supposed to be updating + if( is_object( $skin ) ) { + list( $cur, $prev, $auto ) = $params; + # Standard link to the page in question + $link = $skin->makeLinkObj( $title ); + # Generate a diff link + $bits[] = 'oldid=' . urlencode( $cur ); + $bits[] = 'diff=prev'; + $bits = implode( '&', $bits ); + $diff = $skin->makeLinkObj( $title, htmlspecialchars( wfMsg( 'patrol-log-diff', $cur ) ), $bits ); + # Indicate whether or not the patrolling was automatic + $auto = $auto ? wfMsgHtml( 'patrol-log-auto' ) : ''; + # Put it all together + return wfMsgHtml( 'patrol-log-line', $diff, $link, $auto ); + } else { + return ''; + } + } + + /** + * Prepare log parameters for a patrolled change + * + * @param RecentChange $change RecentChange to represent + * @param bool $auto Whether the patrol event was automatic + * @return array + */ + private static function buildParams( $change, $auto ) { + return array( + $change->getAttribute( 'rc_this_oldid' ), + $change->getAttribute( 'rc_last_oldid' ), + (int)$auto + ); + } + +} + +?>
\ No newline at end of file diff --git a/includes/Profiler.php b/includes/Profiler.php index 30cda63f..da3a82ed 100644 --- a/includes/Profiler.php +++ b/includes/Profiler.php @@ -1,7 +1,6 @@ <?php /** * This file is only included if profiling is enabled - * @package MediaWiki */ $wgProfiling = true; @@ -41,14 +40,13 @@ if (!function_exists('memory_get_usage')) { /** * @todo document - * @package MediaWiki + * @addtogroup Profiler */ class Profiler { var $mStack = array (), $mWorkStack = array (), $mCollated = array (); var $mCalls = array (), $mTotals = array (); - function Profiler() - { + function __construct() { // Push an entry for the pre-profile setup time onto the stack global $wgRequestTime; if ( !empty( $wgRequestTime ) ) { @@ -57,7 +55,6 @@ class Profiler { } else { $this->profileIn( '-total' ); } - } function profileIn($functionname) { @@ -291,7 +288,7 @@ class Profiler { * @return Integer * @private */ - function calltreeCount(& $stack, $start) { + function calltreeCount($stack, $start) { $level = $stack[$start][1]; $count = 0; for ($i = $start -1; $i >= 0 && $stack[$i][1] > $level; $i --) { @@ -308,7 +305,7 @@ class Profiler { global $wguname, $wgProfilePerHost; $fname = 'Profiler::logToDB'; - $dbw = & wfGetDB(DB_MASTER); + $dbw = wfGetDB(DB_MASTER); if (!is_object($dbw)) return false; $errorState = $dbw->ignoreErrors( true ); diff --git a/includes/ProfilerSimple.php b/includes/ProfilerSimple.php index e69bfc47..f43c7dfc 100644 --- a/includes/ProfilerSimple.php +++ b/includes/ProfilerSimple.php @@ -1,20 +1,17 @@ <?php -/** - * Simple profiler base class - * @package MediaWiki - */ -/** - * @todo document - * @package MediaWiki - */ require_once(dirname(__FILE__).'/Profiler.php'); +/** + * Simple profiler base class. + * @todo document methods (?) + * @addtogroup Profiler + */ class ProfilerSimple extends Profiler { var $mMinimumTime = 0; var $mProfileID = false; - function ProfilerSimple() { + function __construct() { global $wgRequestTime,$wgRUstart; if (!empty($wgRequestTime) && !empty($wgRUstart)) { $this->mWorkStack[] = array( '-total', 0, $wgRequestTime,$this->getCpuTime($wgRUstart)); @@ -26,7 +23,6 @@ class ProfilerSimple extends Profiler { if (!is_array($entry)) { $entry = array('cpu'=> 0.0, 'cpu_sq' => 0.0, 'real' => 0.0, 'real_sq' => 0.0, 'count' => 0); $this->mCollated["-setup"] =& $entry; - } $entry['cpu'] += $elapsedcpu; $entry['cpu_sq'] += $elapsedcpu*$elapsedcpu; @@ -57,7 +53,7 @@ class ProfilerSimple extends Profiler { if ($wgDebugFunctionEntry) { $this->debug(str_repeat(' ', count($this->mWorkStack)).'Entering '.$functionname."\n"); } - $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), microtime(true), $this->getCpuTime()); + $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), microtime(true), $this->getCpuTime()); } function profileOut($functionname) { @@ -87,7 +83,6 @@ class ProfilerSimple extends Profiler { if (!is_array($entry)) { $entry = array('cpu'=> 0.0, 'cpu_sq' => 0.0, 'real' => 0.0, 'real_sq' => 0.0, 'count' => 0); $this->mCollated[$functionname] =& $entry; - } $entry['cpu'] += $elapsedcpu; $entry['cpu_sq'] += $elapsedcpu*$elapsedcpu; diff --git a/includes/ProfilerSimpleUDP.php b/includes/ProfilerSimpleUDP.php index a8527c38..500f1cbd 100644 --- a/includes/ProfilerSimpleUDP.php +++ b/includes/ProfilerSimpleUDP.php @@ -1,11 +1,13 @@ <?php -/* ProfilerSimpleUDP class, that sends out messages for 'udpprofile' daemon - (the one from wikipedia/udpprofile CVS ) -*/ require_once(dirname(__FILE__).'/Profiler.php'); require_once(dirname(__FILE__).'/ProfilerSimple.php'); +/** + * ProfilerSimpleUDP class, that sends out messages for 'udpprofile' daemon + * (the one from mediawiki/trunk/udpprofile SVN ) + * @addtogroup Profiler + */ class ProfilerSimpleUDP extends ProfilerSimple { function getFunctionReport() { global $wgUDPProfilerHost; @@ -15,8 +17,7 @@ class ProfilerSimpleUDP extends ProfilerSimple { # Less than minimum, ignore return; } - - + $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); $plength=0; $packet=""; diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index f96262fe..3cafbd55 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -17,27 +17,42 @@ * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html - * - * @package MediaWiki - * @subpackage SpecialPage */ +/** + * @todo document, briefly. + * @addtogroup SpecialPage + */ class ProtectionForm { var $mRestrictions = array(); var $mReason = ''; + var $mCascade = false; + var $mExpiry = null; - function ProtectionForm( &$article ) { + function __construct( &$article ) { global $wgRequest, $wgUser; global $wgRestrictionTypes, $wgRestrictionLevels; $this->mArticle =& $article; $this->mTitle =& $article->mTitle; if( $this->mTitle ) { + $this->mTitle->loadRestrictions(); + foreach( $wgRestrictionTypes as $action ) { // Fixme: this form currently requires individual selections, // but the db allows multiples separated by commas. $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) ); } + + $this->mCascade = $this->mTitle->areRestrictionsCascading(); + + if ( $this->mTitle->mRestrictionsExpiry == 'infinity' ) { + $this->mExpiry = 'infinite'; + } else if ( strlen($this->mTitle->mRestrictionsExpiry) == 0 ) { + $this->mExpiry = ''; + } else { + $this->mExpiry = wfTimestamp( TS_RFC2822, $this->mTitle->mRestrictionsExpiry ); + } } // The form will be available in read-only to show levels. @@ -48,6 +63,9 @@ class ProtectionForm { if( $wgRequest->wasPosted() ) { $this->mReason = $wgRequest->getText( 'mwProtect-reason' ); + $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' ); + $this->mExpiry = $wgRequest->getText( 'mwProtect-expiry' ); + foreach( $wgRestrictionTypes as $action ) { $val = $wgRequest->getVal( "mwProtect-level-$action" ); if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) { @@ -56,9 +74,21 @@ class ProtectionForm { } } } + + function execute() { + global $wgRequest; + if( $wgRequest->wasPosted() ) { + if( $this->save() ) { + global $wgOut; + $wgOut->redirect( $this->mTitle->getFullUrl() ); + } + } else { + $this->show(); + } + } - function show() { - global $wgOut; + function show( $err = null ) { + global $wgOut, $wgUser; $wgOut->setRobotpolicy( 'noindex,nofollow' ); @@ -69,17 +99,47 @@ class ProtectionForm { return; } - if( $this->save() ) { - $wgOut->redirect( $this->mTitle->getFullUrl() ); - return; + list( $cascadeSources, $restrictions ) = $this->mTitle->getCascadeProtectionSources(); + + if ( "" != $err ) { + $wgOut->setSubtitle( wfMsgHtml( 'formerror' ) ); + $wgOut->addHTML( "<p class='error'>{$err}</p>\n" ); + } + + if ( $cascadeSources && count($cascadeSources) > 0 ) { + $titles = ''; + + foreach ( $cascadeSources as $title ) { + $titles .= '* [[:' . $title->getPrefixedText() . "]]\n"; + } + + $notice = wfMsgExt( 'protect-cascadeon', array('parsemag'), count($cascadeSources) ) . "\r\n$titles"; + + $wgOut->addWikiText( $notice ); } $wgOut->setPageTitle( wfMsg( 'confirmprotect' ) ); $wgOut->setSubtitle( wfMsg( 'protectsub', $this->mTitle->getPrefixedText() ) ); - $wgOut->addWikiText( - wfMsg( $this->disabled ? "protect-viewtext" : "protect-text", - wfEscapeWikiText( $this->mTitle->getPrefixedText() ) ) ); + # Show an appropriate message if the user isn't allowed or able to change + # the protection settings at this time + if( $this->disabled ) { + if( $wgUser->isAllowed( 'protect' ) ) { + if( $wgUser->isBlocked() ) { + # Blocked + $message = 'protect-locked-blocked'; + } else { + # Database lock + $message = 'protect-locked-dblock'; + } + } else { + # Permission error + $message = 'protect-locked-access'; + } + } else { + $message = 'protect-text'; + } + $wgOut->addWikiText( wfMsg( $message, wfEscapeWikiText( $this->mTitle->getPrefixedText() ) ) ); $wgOut->addHTML( $this->buildForm() ); @@ -88,20 +148,43 @@ class ProtectionForm { function save() { global $wgRequest, $wgUser, $wgOut; - if( !$wgRequest->wasPosted() ) { - return false; - } - + if( $this->disabled ) { + $this->show(); return false; } $token = $wgRequest->getVal( 'wpEditToken' ); if( !$wgUser->matchEditToken( $token ) ) { - throw new FatalError( wfMsg( 'sessionfailure' ) ); + $this->show( wfMsg( 'sessionfailure' ) ); + return false; } - $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $this->mReason ); + if ( strlen( $this->mExpiry ) == 0 ) { + $this->mExpiry = 'infinite'; + } + + if ( $this->mExpiry == 'infinite' || $this->mExpiry == 'indefinite' ) { + $expiry = Block::infinity(); + } else { + # Convert GNU-style date, on error returns -1 for PHP <5.1 and false for PHP >=5.1 + $expiry = strtotime( $this->mExpiry ); + + if ( $expiry < 0 || $expiry === false ) { + $this->show( wfMsg( 'protect_expiry_invalid' ) ); + return false; + } + + $expiry = wfTimestamp( TS_MW, $expiry ); + + if ( $expiry < wfTimestampNow() ) { + $this->show( wfMsg( 'protect_expiry_old' ) ); + return false; + } + + } + + $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $this->mReason, $this->mCascade, $expiry ); if( !$ok ) { throw new FatalError( "Unknown error at restriction save time." ); } @@ -117,6 +200,7 @@ class ProtectionForm { // The submission needs to reenable the move permission selector // if it's in locked mode, or some browsers won't submit the data. $out .= wfOpenElement( 'form', array( + 'id' => 'mw-Protect-Form', 'action' => $this->mTitle->getLocalUrl( 'action=protect' ), 'method' => 'post', 'onsubmit' => 'protectEnable(true)' ) ); @@ -148,13 +232,25 @@ class ProtectionForm { $out .= "</tbody>\n"; $out .= "</table>\n"; + global $wgEnableCascadingProtection; + + if ($wgEnableCascadingProtection) + $out .= $this->buildCascadeInput(); + + $out .= "<table>\n"; + $out .= "<tbody>\n"; + + $out .= $this->buildExpiryInput(); + if( !$this->disabled ) { - $out .= "<table>\n"; - $out .= "<tbody>\n"; $out .= "<tr><td>" . $this->buildReasonInput() . "</td></tr>\n"; $out .= "<tr><td></td><td>" . $this->buildSubmit() . "</td></tr>\n"; - $out .= "</tbody>\n"; - $out .= "</table>\n"; + } + + $out .= "</tbody>\n"; + $out .= "</table>\n"; + + if ( !$this->disabled ) { $out .= "</form>\n"; $out .= $this->buildCleanupScript(); } @@ -202,11 +298,38 @@ class ProtectionForm { wfElement( 'input', array( 'size' => 60, 'name' => $id, - 'id' => $id ) ); + 'id' => $id, + 'value' => $this->mReason ) ); + } + + function buildCascadeInput() { + $id = 'mwProtect-cascade'; + $ci = wfCheckLabel( wfMsg( 'protect-cascade' ), $id, $id, $this->mCascade, $this->disabledAttrib); + return $ci; + } + + function buildExpiryInput() { + $id = 'mwProtect-expiry'; + + $ci = "<tr> <td align=\"right\">"; + $ci .= wfElement( 'label', array ( + 'id' => "$id-label", + 'for' => $id ), + wfMsg( 'protectexpiry' ) ); + $ci .= "</td> <td align=\"left\">"; + $ci .= wfElement( 'input', array( + 'size' => 60, + 'name' => $id, + 'id' => $id, + 'value' => $this->mExpiry ) + $this->disabledAttrib ); + $ci .= "</td></tr>"; + + return $ci; } function buildSubmit() { return wfElement( 'input', array( + 'id' => 'mw-Protect-submit', 'type' => 'submit', 'value' => wfMsg( 'confirm' ) ) ); } @@ -219,8 +342,17 @@ class ProtectionForm { } function buildCleanupScript() { - return '<script type="text/javascript">protectInitialize("mwProtectSet","' . - wfEscapeJsString( wfMsg( 'protect-unchain' ) ) . '")</script>'; + global $wgRestrictionLevels, $wgGroupPermissions; + $script = 'var wgCascadeableLevels='; + $CascadeableLevels = array(); + foreach( $wgRestrictionLevels as $key ) { + if ( isset($wgGroupPermissions[$key]['protect']) && $wgGroupPermissions[$key]['protect'] ) { + $CascadeableLevels[]="'" . wfEscapeJsString($key) . "'"; + } + } + $script .= "[" . implode(',',$CascadeableLevels) . "];\n"; + $script .= 'protectInitialize("mwProtectSet","' . wfEscapeJsString( wfMsg( 'protect-unchain' ) ) . '")'; + return '<script type="text/javascript">' . $script . '</script>'; } /** @@ -239,5 +371,4 @@ class ProtectionForm { } } - ?> diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php index 22ea4947..f72b640f 100644 --- a/includes/ProxyTools.php +++ b/includes/ProxyTools.php @@ -1,27 +1,63 @@ <?php /** * Functions for dealing with proxies - * @package MediaWiki */ +/** + * Extracts the XFF string from the request header + * Checks first for "X-Forwarded-For", then "Client-ip" + * Note: headers are spoofable + * @return string + */ function wfGetForwardedFor() { if( function_exists( 'apache_request_headers' ) ) { // More reliable than $_SERVER due to case and -/_ folding $set = apache_request_headers(); $index = 'X-Forwarded-For'; + $index2 = 'Client-ip'; } else { // Subject to spoofing with headers like X_Forwarded_For $set = $_SERVER; $index = 'HTTP_X_FORWARDED_FOR'; + $index2 = 'CLIENT-IP'; } + #Try a couple of headers if( isset( $set[$index] ) ) { return $set[$index]; + } else if( isset( $set[$index2] ) ) { + return $set[$index2]; } else { return null; } } -/** Work out the IP address based on various globals */ +/** + * Returns the browser/OS data from the request header + * Note: headers are spoofable + * @return string + */ +function wfGetAgent() { + if( function_exists( 'apache_request_headers' ) ) { + // More reliable than $_SERVER due to case and -/_ folding + $set = apache_request_headers(); + $index = 'User-Agent'; + } else { + // Subject to spoofing with headers like X_Forwarded_For + $set = $_SERVER; + $index = 'HTTP_USER_AGENT'; + } + if( isset( $set[$index] ) ) { + return $set[$index]; + } else { + return ''; + } +} + +/** + * Work out the IP address based on various globals + * For trusted proxies, use the XFF client IP (first of the chain) + * @return string + */ function wfGetIP() { global $wgIP; @@ -66,6 +102,13 @@ function wfGetIP() { return $ip; } +/** + * Checks if an IP is a trusted proxy providor + * Useful to tell if X-Fowarded-For data is possibly bogus + * Squid cache servers for the site and AOL are whitelisted + * @param string $ip + * @return bool + */ function wfIsTrustedProxy( $ip ) { global $wgSquidServers, $wgSquidServersNoPurge; @@ -130,6 +173,7 @@ function wfProxyCheck() { /** * Convert a network specification in CIDR notation to an integer network and a number of bits + * @return array(string, int) */ function wfParseCIDR( $range ) { return IP::parseCIDR( $range ); @@ -137,6 +181,7 @@ function wfParseCIDR( $range ) { /** * Check if an IP address is in the local proxy list + * @return bool */ function wfIsLocallyBlockedProxy( $ip ) { global $wgProxyList; @@ -169,6 +214,7 @@ function wfIsLocallyBlockedProxy( $ip ) { /** * TODO: move this list to the database in a global IP info table incorporating * trusted ISP proxies, blocked IP addresses and open proxies. + * @return bool */ function wfIsAOLProxy( $ip ) { $ranges = array( diff --git a/includes/QueryPage.php b/includes/QueryPage.php index ff6355e7..143c8be6 100644 --- a/includes/QueryPage.php +++ b/includes/QueryPage.php @@ -1,42 +1,45 @@ <?php /** * Contain a class for special pages - * @package MediaWiki */ /** - * List of query page classes and their associated special pages, for periodic update purposes + * List of query page classes and their associated special pages, + * for periodic updates. + * + * DO NOT CHANGE THIS LIST without testing that + * maintenance/updateSpecialPages.php still works. */ global $wgQueryPages; // not redundant $wgQueryPages = array( // QueryPage subclass Special page name Limit (false for none, none for the default) //---------------------------------------------------------------------------- - array( 'AncientPagesPage', 'Ancientpages' ), - array( 'BrokenRedirectsPage', 'BrokenRedirects' ), - array( 'CategoriesPage', 'Categories' ), - array( 'DeadendPagesPage', 'Deadendpages' ), - array( 'DisambiguationsPage', 'Disambiguations' ), - array( 'DoubleRedirectsPage', 'DoubleRedirects' ), - array( 'ListUsersPage', 'Listusers' ), - array( 'ListredirectsPage', 'Listredirects' ), - array( 'LonelyPagesPage', 'Lonelypages' ), - array( 'LongPagesPage', 'Longpages' ), - array( 'MostcategoriesPage', 'Mostcategories' ), - array( 'MostimagesPage', 'Mostimages' ), - array( 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ), - array( 'MostlinkedPage', 'Mostlinked' ), - array( 'MostrevisionsPage', 'Mostrevisions' ), - array( 'NewPagesPage', 'Newpages' ), - array( 'ShortPagesPage', 'Shortpages' ), - array( 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ), - array( 'UncategorizedPagesPage', 'Uncategorizedpages' ), - array( 'UncategorizedImagesPage', 'Uncategorizedimages' ), - array( 'UnusedCategoriesPage', 'Unusedcategories' ), - array( 'UnusedimagesPage', 'Unusedimages' ), - array( 'WantedCategoriesPage', 'Wantedcategories' ), - array( 'WantedPagesPage', 'Wantedpages' ), - array( 'UnwatchedPagesPage', 'Unwatchedpages' ), - array( 'UnusedtemplatesPage', 'Unusedtemplates' ), + array( 'AncientPagesPage', 'Ancientpages' ), + array( 'BrokenRedirectsPage', 'BrokenRedirects' ), + array( 'DeadendPagesPage', 'Deadendpages' ), + array( 'DisambiguationsPage', 'Disambiguations' ), + array( 'DoubleRedirectsPage', 'DoubleRedirects' ), + array( 'ListredirectsPage', 'Listredirects' ), + array( 'LonelyPagesPage', 'Lonelypages' ), + array( 'LongPagesPage', 'Longpages' ), + array( 'MostcategoriesPage', 'Mostcategories' ), + array( 'MostimagesPage', 'Mostimages' ), + array( 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ), + array( 'MostlinkedPage', 'Mostlinked' ), + array( 'MostrevisionsPage', 'Mostrevisions' ), + array( 'FewestrevisionsPage', 'Fewestrevisions' ), + array( 'NewPagesPage', 'Newpages' ), + array( 'ShortPagesPage', 'Shortpages' ), + array( 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ), + array( 'UncategorizedPagesPage', 'Uncategorizedpages' ), + array( 'UncategorizedImagesPage', 'Uncategorizedimages' ), + array( 'UnusedCategoriesPage', 'Unusedcategories' ), + array( 'UnusedimagesPage', 'Unusedimages' ), + array( 'WantedCategoriesPage', 'Wantedcategories' ), + array( 'WantedPagesPage', 'Wantedpages' ), + array( 'UnwatchedPagesPage', 'Unwatchedpages' ), + array( 'UnusedtemplatesPage', 'Unusedtemplates' ), + array( 'WithoutInterwikiPage', 'Withoutinterwiki' ), ); wfRunHooks( 'wgQueryPages', array( &$wgQueryPages ) ); @@ -49,8 +52,7 @@ if ( !$wgDisableCounters ) * This is a class for doing query pages; since they're almost all the same, * we factor out some of the functionality into a superclass, and let * subclasses derive from it. - * - * @package MediaWiki + * @addtogroup SpecialPage */ class QueryPage { /** @@ -59,7 +61,7 @@ class QueryPage { * @var bool */ var $listoutput = false; - + /** * The offset and limit in use, as passed to the query() function * @@ -197,8 +199,8 @@ class QueryPage { */ function recache( $limit, $ignoreErrors = true ) { $fname = get_class($this) . '::recache'; - $dbw =& wfGetDB( DB_MASTER ); - $dbr =& wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) ); + $dbw = wfGetDB( DB_MASTER ); + $dbr = wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) ); if ( !$dbw || !$dbr ) { return false; } @@ -282,7 +284,7 @@ class QueryPage { $sname = $this->getName(); $fname = get_class($this) . '::doQuery'; - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $wgOut->setSyndicated( $this->isSyndicated() ); @@ -306,7 +308,7 @@ class QueryPage { $updated = $wgLang->timeAndDate( $tRow->qci_timestamp, true, true ); $cacheNotice = wfMsg( 'perfcachedts', $updated ); $wgOut->addMeta( 'Data-Cache-Time', $tRow->qci_timestamp ); - $wgOut->addScript( '<script language="JavaScript">var dataCacheTime = \'' . $tRow->qci_timestamp . '\';</script>' ); + $wgOut->addInlineScript( "var dataCacheTime = '{$tRow->qci_timestamp}';" ); } else { $cacheNotice = wfMsg( 'perfcached' ); } @@ -330,58 +332,99 @@ class QueryPage { $num = $dbr->numRows($res); $this->preprocessResults( $dbr, $res ); - - $sk = $wgUser->getSkin( ); - - if($shownavigation) { - $wgOut->addHTML( $this->getPageHeader() ); - $top = wfShowingResults( $offset, $num); - $wgOut->addHTML( "<p>{$top}\n" ); - - # often disable 'next' link when we reach the end - $atend = $num < $limit; - - $sl = wfViewPrevNext( $offset, $limit , - $wgContLang->specialPage( $sname ), - wfArrayToCGI( $this->linkParameters() ), $atend ); - $wgOut->addHTML( "<br />{$sl}</p>\n" ); + $sk = $wgUser->getSkin(); + + # Top header and navigation + if( $shownavigation ) { + $wgOut->addHtml( $this->getPageHeader() ); + if( $num > 0 ) { + $wgOut->addHtml( '<p>' . wfShowingResults( $offset, $num ) . '</p>' ); + # Disable the "next" link when we reach the end + $paging = wfViewPrevNext( $offset, $limit, $wgContLang->specialPage( $sname ), + wfArrayToCGI( $this->linkParameters() ), ( $num < $limit ) ); + $wgOut->addHtml( '<p>' . $paging . '</p>' ); + } else { + # No results to show, so don't bother with "showing X of Y" etc. + # -- just let the user know and give up now + $wgOut->addHtml( '<p>' . wfMsgHtml( 'specialpage-empty' ) . '</p>' ); + return; + } + } + + # The actual results; specialist subclasses will want to handle this + # with more than a straight list, so we hand them the info, plus + # an OutputPage, and let them get on with it + $this->outputResults( $wgOut, + $wgUser->getSkin(), + $dbr, # Should use a ResultWrapper for this + $res, + $dbr->numRows( $res ), + $offset ); + + # Repeat the paging links at the bottom + if( $shownavigation ) { + $wgOut->addHtml( '<p>' . $paging . '</p>' ); } - if ( $num > 0 ) { - $s = array(); - if ( ! $this->listoutput ) - $s[] = $this->openList( $offset ); - - # Only read at most $num rows, because $res may contain the whole 1000 - for ( $i = 0; $i < $num && $obj = $dbr->fetchObject( $res ); $i++ ) { - $format = $this->formatResult( $sk, $obj ); - if ( $format ) { - $attr = ( isset ( $obj->usepatrol ) && $obj->usepatrol && - $obj->patrolled == 0 ) ? ' class="not-patrolled"' : ''; - $s[] = $this->listoutput ? $format : "<li{$attr}>{$format}</li>\n"; + + return $num; + } + + /** + * Format and output report results using the given information plus + * OutputPage + * + * @param OutputPage $out OutputPage to print to + * @param Skin $skin User skin to use + * @param Database $dbr Database (read) connection to use + * @param int $res Result pointer + * @param int $num Number of available result rows + * @param int $offset Paging offset + */ + protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) { + global $wgContLang; + + if( $num > 0 ) { + $html = array(); + if( !$this->listoutput ) + $html[] = $this->openList( $offset ); + + # $res might contain the whole 1,000 rows, so we read up to + # $num [should update this to use a Pager] + for( $i = 0; $i < $num && $row = $dbr->fetchObject( $res ); $i++ ) { + $line = $this->formatResult( $skin, $row ); + if( $line ) { + $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 ) + ? ' class="not-patrolled"' + : ''; + $html[] = $this->listoutput + ? $format + : "<li{$attr}>{$line}</li>\n"; } } - - if($this->tryLastResult()) { - // flush the very last result - $obj = null; - $format = $this->formatResult( $sk, $obj ); - if( $format ) { - $attr = ( isset ( $obj->usepatrol ) && $obj->usepatrol && - $obj->patrolled == 0 ) ? ' class="not-patrolled"' : ''; - $s[] = "<li{$attr}>{$format}</li>\n"; + + # Flush the final result + if( $this->tryLastResult() ) { + $row = null; + $line = $this->formatResult( $skin, $row ); + if( $line ) { + $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 ) + ? ' class="not-patrolled"' + : ''; + $html[] = $this->listoutput + ? $format + : "<li{$attr}>{$line}</li>\n"; } } - - $dbr->freeResult( $res ); - if ( ! $this->listoutput ) - $s[] = $this->closeList(); - $str = $this->listoutput ? $wgContLang->listToText( $s ) : implode( '', $s ); - $wgOut->addHTML( $str ); - } - if($shownavigation) { - $wgOut->addHTML( "<p>{$sl}</p>\n" ); + + if( !$this->listoutput ) + $html[] = $this->closeList(); + + $html = $this->listoutput + ? $wgContLang->listToText( $html ) + : implode( '', $html ); + + $out->addHtml( $html ); } - return $num; } function openList( $offset ) { @@ -411,7 +454,7 @@ class QueryPage { $this->feedUrl() ); $feed->outHeader(); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $sql = $this->getSQL() . $this->getOrder(); $sql = $dbr->limitResult( $sql, $limit, 0 ); $res = $dbr->query( $sql, 'QueryPage::doFeed' ); @@ -482,20 +525,4 @@ class QueryPage { } } -/** - * This is a subclass for very simple queries that are just looking for page - * titles that match some criteria. It formats each result item as a link to - * that page. - * - * @package MediaWiki - */ -class PageQueryPage extends QueryPage { - - function formatResult( $skin, $result ) { - global $wgContLang; - $nt = Title::makeTitle( $result->namespace, $result->title ); - return $skin->makeKnownLinkObj( $nt, htmlspecialchars( $wgContLang->convert( $nt->getPrefixedText() ) ) ); - } -} - ?> diff --git a/includes/RawPage.php b/includes/RawPage.php index a0b76886..93484829 100644 --- a/includes/RawPage.php +++ b/includes/RawPage.php @@ -7,12 +7,11 @@ * License: GPL (http://www.gnu.org/copyleft/gpl.html) * * @author Gabriel Wicke <wicke@wikidev.net> - * @package MediaWiki */ /** - * @todo document - * @package MediaWiki + * A simple method to retrieve the plain source of an article, + * using "action=raw" in the GET request string. */ class RawPage { var $mArticle, $mTitle, $mRequest; @@ -20,9 +19,8 @@ class RawPage { var $mSmaxage, $mMaxage; var $mContentType, $mExpandTemplates; - function RawPage( &$article, $request = false ) { + function __construct( &$article, $request = false ) { global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType; - global $wgUser; $allowedCTypes = array('text/x-wiki', $wgJsMimeType, 'text/css', 'application/x-zope-edit'); $this->mArticle =& $article; @@ -39,7 +37,7 @@ class RawPage { $maxage = $this->mRequest->getInt( 'maxage', $wgSquidMaxage ); $this->mExpandTemplates = $this->mRequest->getVal( 'templates' ) === 'expand'; $this->mUseMessageCache = $this->mRequest->getBool( 'usemsgcache' ); - + $oldid = $this->mRequest->getInt( 'oldid' ); switch ( $wgRequest->getText( 'direction' ) ) { case 'next': @@ -85,8 +83,7 @@ class RawPage { // Output may contain user-specific data; vary for open sessions $this->mPrivateCache = ( $this->mSmaxage == 0 ) || - ( isset( $_COOKIE[ini_get( 'session.name' )] ) || - $wgUser->isLoggedIn() ); + ( session_id() != '' ); if ( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) { $this->mContentType = 'text/x-wiki'; @@ -137,7 +134,13 @@ class RawPage { # allow the client to cache this for 24 hours $mode = $this->mPrivateCache ? 'private' : 'public'; header( 'Cache-Control: '.$mode.', s-maxage='.$this->mSmaxage.', max-age='.$this->mMaxage ); - echo $this->getRawText(); + $text = $this->getRawText(); + + if( !wfRunHooks( 'RawPageViewBeforeOutput', array( &$this, &$text ) ) ) { + wfDebug( __METHOD__ . ': RawPageViewBeforeOutput hook broke raw page output.' ); + } + + echo $text; $wgOut->disable(); } @@ -189,6 +192,20 @@ class RawPage { header( "HTTP/1.0 404 Not Found" ); } + // Special-case for empty CSS/JS + // + // Internet Explorer for Mac handles empty files badly; + // particularly so when keep-alive is active. It can lead + // to long timeouts as it seems to sit there waiting for + // more data that never comes. + // + // Give it a comment... + if( strlen( $text ) == 0 && + ($this->mContentType == 'text/css' || + $this->mContentType == 'text/javascript' ) ) { + return "/* Empty */"; + } + return $this->parseArticleText( $text ); } diff --git a/includes/RecentChange.php b/includes/RecentChange.php index 1c7791c2..fced4343 100644 --- a/includes/RecentChange.php +++ b/includes/RecentChange.php @@ -1,7 +1,6 @@ <?php /** * - * @package MediaWiki */ /** @@ -39,7 +38,6 @@ * numberofWatchingusers * * @todo document functions and variables - * @package MediaWiki */ class RecentChange { @@ -49,7 +47,7 @@ class RecentChange # Factory methods - /* static */ function newFromRow( $row ) + public static function newFromRow( $row ) { $rc = new RecentChange; $rc->loadFromRow( $row ); @@ -72,7 +70,7 @@ class RecentChange * @return RecentChange */ public static function newFromId( $rcid ) { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'recentchanges', '*', array( 'rc_id' => $rcid ), __METHOD__ ); if( $res && $dbr->numRows( $res ) > 0 ) { $row = $dbr->fetchObject( $res ); @@ -118,7 +116,7 @@ class RecentChange global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress, $wgRC2UDPPort, $wgRC2UDPPrefix; $fname = 'RecentChange::save'; - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); if ( !is_array($this->mExtra) ) { $this->mExtra = array(); } @@ -216,7 +214,7 @@ class RecentChange { $fname = 'RecentChange::markPatrolled'; - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'recentchanges', array( /* SET */ @@ -504,6 +502,8 @@ class RecentChange function getIRCLine() { global $wgUseRCPatrol; + // FIXME: Would be good to replace these 2 extract() calls with something more explicit + // e.g. list ($rc_type, $rc_id) = array_values ($this->mAttribs); [or something like that] extract($this->mAttribs); extract($this->mExtra); diff --git a/includes/Revision.php b/includes/Revision.php index c5235e22..71f214e3 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -1,19 +1,17 @@ <?php /** - * @package MediaWiki * @todo document */ /** - * @package MediaWiki * @todo document */ class Revision { - const DELETED_TEXT = 1; - const DELETED_COMMENT = 2; - const DELETED_USER = 4; + const DELETED_TEXT = 1; + const DELETED_COMMENT = 2; + const DELETED_USER = 4; const DELETED_RESTRICTED = 8; - + /** * Load a page revision from a given revision ID number. * Returns null if no such revision can be found. @@ -79,7 +77,7 @@ class Revision { * @access public * @static */ - public static function loadFromPageId( &$db, $pageid, $id = 0 ) { + public static function loadFromPageId( $db, $pageid, $id = 0 ) { $conds=array('page_id=rev_page','rev_page'=>intval( $pageid ), 'page_id'=>intval( $pageid )); if( $id ) { $conds['rev_id']=intval($id); @@ -145,10 +143,10 @@ class Revision { * @static */ private static function newFromConds( $conditions ) { - $db =& wfGetDB( DB_SLAVE ); + $db = wfGetDB( DB_SLAVE ); $row = Revision::loadFromConds( $db, $conditions ); if( is_null( $row ) ) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $row = Revision::loadFromConds( $dbw, $conditions ); } return $row; @@ -164,7 +162,7 @@ class Revision { * @access private * @static */ - private static function loadFromConds( &$db, $conditions ) { + private static function loadFromConds( $db, $conditions ) { $res = Revision::fetchFromConds( $db, $conditions ); if( $res ) { $row = $res->fetchObject(); @@ -226,7 +224,7 @@ class Revision { * @access private * @static */ - private static function fetchFromConds( &$db, $conditions ) { + private static function fetchFromConds( $db, $conditions ) { $res = $db->select( array( 'page', 'revision' ), array( 'page_namespace', @@ -240,7 +238,8 @@ class Revision { 'rev_user', 'rev_minor_edit', 'rev_timestamp', - 'rev_deleted' ), + 'rev_deleted', + 'rev_len' ), $conditions, 'Revision::fetchRow', array( 'LIMIT' => 1 ) ); @@ -249,6 +248,25 @@ class Revision { } /** + * Return the list of revision fields that should be selected to create + * a new revision. + */ + static function selectFields() { + return array( + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_timestamp', + 'rev_comment', + 'rev_minor_edit', + 'rev_user', + 'rev_user_text,'. + 'rev_deleted', + 'rev_len' + ); + } + + /** * @param object $row * @access private */ @@ -263,6 +281,11 @@ class Revision { $this->mMinorEdit = intval( $row->rev_minor_edit ); $this->mTimestamp = $row->rev_timestamp; $this->mDeleted = intval( $row->rev_deleted ); + + if( !isset( $row->rev_len ) || is_null( $row->rev_len ) ) + $this->mSize = null; + else + $this->mSize = intval( $row->rev_len ); if( isset( $row->page_latest ) ) { $this->mCurrent = ( $row->rev_id == $row->page_latest ); @@ -293,7 +316,8 @@ class Revision { $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0; $this->mTimestamp = isset( $row['timestamp'] ) ? strval( $row['timestamp'] ) : wfTimestamp( TS_MW ); $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0; - + $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null; + // Enforce spacing trimming on supplied text $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null; $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null; @@ -301,6 +325,9 @@ class Revision { $this->mTitle = null; # Load on demand if needed $this->mCurrent = false; + # If we still have no len_size, see it we have the text to figure it out + if ( !$this->mSize ) + $this->mSize = is_null($this->mText) ? null : strlen($this->mText); } else { throw new MWException( 'Revision constructor passed invalid row format.' ); } @@ -325,6 +352,13 @@ class Revision { } /** + * Returns the length of the text in this revision, or null if unknown. + */ + function getSize() { + return $this->mSize; + } + + /** * Returns the title of the page associated with this entry. * @return Title */ @@ -332,7 +366,7 @@ class Revision { if( isset( $this->mTitle ) ) { return $this->mTitle; } - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( array( 'page', 'revision' ), array( 'page_namespace', 'page_title' ), @@ -459,6 +493,18 @@ class Revision { } return $this->mText; } + + /** + * Fetch revision text if it's available to THIS user + * @return string + */ + function revText() { + if( !$this->userCan( self::DELETED_TEXT ) ) { + return ""; + } else { + return $this->getRawText(); + } + } /** * @return string @@ -565,7 +611,7 @@ class Revision { # Old revisions kept around in a legacy encoding? # Upconvert on demand. global $wgInputEncoding, $wgContLang; - $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding . '//IGNORE', $text ); + $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding, $text ); } } wfProfileOut( $fname ); @@ -666,6 +712,7 @@ class Revision { 'rev_user_text' => $this->mUserText, 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ), 'rev_deleted' => $this->mDeleted, + 'rev_len' => $this->mSize, ), $fname ); @@ -706,7 +753,7 @@ class Revision { if( !$row ) { // Text data is immutable; check slaves first. - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'text', array( 'old_text', 'old_flags' ), array( 'old_id' => $this->getTextId() ), @@ -715,7 +762,7 @@ class Revision { if( !$row ) { // Possible slave lag! - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $row = $dbw->selectRow( 'text', array( 'old_text', 'old_flags' ), array( 'old_id' => $this->getTextId() ), @@ -802,12 +849,12 @@ class Revision { * @param integer $id */ static function getTimestampFromID( $id ) { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', array( 'rev_id' => $id ), __METHOD__ ); if ( $timestamp === false ) { # Not in slave, try master - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', array( 'rev_id' => $id ), __METHOD__ ); } diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index 0c0f7244..fa5416dc 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -20,8 +20,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * - * @package MediaWiki - * @subpackage Parser + * @addtogroup Parser */ /** @@ -29,7 +28,7 @@ * Sanitizer::normalizeCharReferences and Sanitizer::decodeCharReferences */ define( 'MW_CHAR_REFS_REGEX', - '/&([A-Za-z0-9]+); + '/&([A-Za-z0-9\x80-\xff]+); |&\#([0-9]+); |&\#x([0-9A-Za-z]+); |&\#X([0-9A-Za-z]+); @@ -316,7 +315,20 @@ $wgHtmlEntities = array( 'zwj' => 8205, 'zwnj' => 8204 ); -/** @package MediaWiki */ +/** + * Character entity aliases accepted by MediaWiki + */ +global $wgHtmlEntityAliases; |