diff options
Diffstat (limited to 'includes')
154 files changed, 12483 insertions, 3937 deletions
diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index 2084c366..618c2736 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -1,36 +1,22 @@ <?php -//$wgRequestTime = microtime(); - -// unset( $IP ); -// @ini_set( 'allow_url_fopen', 0 ); # For security... - -# Valid web server entry point, enable includes. -# Please don't move this line to includes/Defines.php. This line essentially defines -# a valid entry point. If you put it in includes/Defines.php, then any script that includes -# it becomes an entry point, thereby defeating its purpose. -// define( 'MEDIAWIKI', true ); -// require_once( './includes/Defines.php' ); -// require_once( './LocalSettings.php' ); -// require_once( 'includes/Setup.php' ); -require_once( 'AjaxFunctions.php' ); +if( !defined( 'MEDIAWIKI' ) ) + die( 1 ); if ( ! $wgUseAjax ) { die( 1 ); } +require_once( 'AjaxFunctions.php' ); + class AjaxDispatcher { var $mode; var $func_name; var $args; function AjaxDispatcher() { - global $wgAjaxCachePolicy; - wfProfileIn( 'AjaxDispatcher::AjaxDispatcher' ); - $wgAjaxCachePolicy = new AjaxCachePolicy(); - $this->mode = ""; if (! empty($_GET["rs"])) { @@ -60,23 +46,45 @@ class AjaxDispatcher { } function performAction() { - global $wgAjaxCachePolicy, $wgAjaxExportList; + global $wgAjaxExportList, $wgOut; + if ( empty( $this->mode ) ) { return; } wfProfileIn( 'AjaxDispatcher::performAction' ); if (! in_array( $this->func_name, $wgAjaxExportList ) ) { - echo "-:{$this->func_name} not callable"; + header( 'Status: 400 Bad Request', true, 400 ); + echo "unknown function {$this->func_name}"; } else { - echo "+:"; - $result = call_user_func_array($this->func_name, $this->args); - header( 'Content-Type: text/html; charset=utf-8', true ); - $wgAjaxCachePolicy->writeHeader(); - echo $result; + try { + $result = call_user_func_array($this->func_name, $this->args); + + if ( $result === false || $result === NULL ) { + header( 'Status: 500 Internal Error', true, 500 ); + echo "{$this->func_name} returned no data"; + } + else { + if ( is_string( $result ) ) { + $result= new AjaxResponse( $result ); + } + + $result->sendHeaders(); + $result->printText(); + } + + } catch (Exception $e) { + if (!headers_sent()) { + header( 'Status: 500 Internal Error', true, 500 ); + print $e->getMessage(); + } else { + print $e->getMessage(); + } + } } + wfProfileOut( 'AjaxDispatcher::performAction' ); - exit; + $wgOut = null; } } diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php index 4387a607..9f7a332f 100644 --- a/includes/AjaxFunctions.php +++ b/includes/AjaxFunctions.php @@ -3,8 +3,6 @@ if( !defined( 'MEDIAWIKI' ) ) die( 1 ); -require_once('WebRequest.php'); - /** * Function converts an Javascript escaped string back into a string with * specified charset (default is UTF-8). @@ -70,35 +68,8 @@ function code2utf($num){ return ''; } -class AjaxCachePolicy { - var $policy; - - function AjaxCachePolicy( $policy = null ) { - $this->policy = $policy; - } - - function setPolicy( $policy ) { - $this->policy = $policy; - } - - function writeHeader() { - header ("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); - if ( is_null( $this->policy ) ) { - // Bust cache in the head - header ("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Date in the past - // always modified - header ("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 - header ("Pragma: no-cache"); // HTTP/1.0 - } else { - header ("Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->policy ) . " GMT"); - header ("Cache-Control: s-max-age={$this->policy},public,max-age={$this->policy}"); - } - } -} - - function wfSajaxSearch( $term ) { - global $wgContLang, $wgAjaxCachePolicy, $wgOut; + global $wgContLang, $wgOut; $limit = 16; $l = new Linker; @@ -110,8 +81,6 @@ function wfSajaxSearch( $term ) { if ( strlen( str_replace( '_', '', $term ) )<3 ) return; - $wgAjaxCachePolicy->setPolicy( 30*60 ); - $db =& wfGetDB( DB_SLAVE ); $res = $db->select( 'page', 'page_title', array( 'page_namespace' => 0, @@ -137,10 +106,10 @@ function wfSajaxSearch( $term ) { } $subtitlemsg = ( Title::newFromText($term) ? 'searchsubtitle' : 'searchsubtitleinvalid' ); - $subtitle = $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ); + $subtitle = $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ); #FIXME: parser is missing mTitle ! $term = htmlspecialchars( $term ); - return '<div style="float:right; border:solid 1px black;background:gainsboro;padding:2px;"><a onclick="Searching_Hide_Results();">' + $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>' @@ -152,6 +121,12 @@ 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; } ?> diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php new file mode 100644 index 00000000..40f50876 --- /dev/null +++ b/includes/AjaxResponse.php @@ -0,0 +1,203 @@ +<?php + +if( !defined( 'MEDIAWIKI' ) ) + die( 1 ); + +class AjaxResponse { + var $mCacheDuration; + var $mVary; + + var $mDisabled; + var $mText; + var $mResponseCode; + var $mLastModified; + var $mContentType; + + function AjaxResponse( $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 ); + } + } + + function setCacheDuration( $duration ) { + $this->mCacheDuration = $duration; + } + + function setVary( $vary ) { + $this->mVary = $vary; + } + + 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; + } + } + + function printText() { + if ( ! $this->mDisabled ) { + print $this->mText; + } + } + + function sendHeaders() { + global $wgUseSquid, $wgUseESI, $wgSquidMaxage; + + 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 + * any future call to AjaxResponse::printText() have no effect. The method + * returns true iff the response code was set to 304 Not Modified. + */ + function checkLastModified ( $timestamp ) { + global $wgCachePages, $wgCacheEpoch, $wgUser, $wgRequest; + $fname = 'AjaxResponse::checkLastModified'; + + if ( !$timestamp || $timestamp == '19700101000000' ) { + wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP\n" ); + return; + } + if( !$wgCachePages ) { + wfDebug( "$fname: CACHE DISABLED\n", false ); + return; + } + if( $wgUser->getOption( 'nocache' ) ) { + wfDebug( "$fname: USER DISABLED CACHE\n", false ); + return; + } + + $timestamp = wfTimestamp( TS_MW, $timestamp ); + $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->mTouched, $wgCacheEpoch ) ); + + if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { + # IE sends sizes after the date like this: + # Wed, 20 Aug 2003 06:51:19 GMT; length=5202 + # this breaks strtotime(). + $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] ); + $modsinceTime = strtotime( $modsince ); + $ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 ); + wfDebug( "$fname: -- client send If-Modified-Since: " . $modsince . "\n", false ); + wfDebug( "$fname: -- we might send Last-Modified : $lastmod\n", false ); + if( ($ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) { + $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 ); + $this->mLastModified = $lastmod; + } + } else { + wfDebug( "$fname: client did not send If-Modified-Since header\n", false ); + $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 + if ( $touched <= $mcvalue['timestamp'] ) { + wfDebug( "Got $mckey from cache\n" ); + $this->mText = $mcvalue['value']; + return true; + } else { + wfDebug( "$mckey has expired\n" ); + } + } + + return false; + } + + function storeInMemcached( $mckey, $expiry = 86400 ) { + global $wgMemc; + + $wgMemc->set( $mckey, + array( + 'timestamp' => wfTimestampNow(), + 'value' => $this->mText + ), $expiry + ); + + return true; + } +} +?> diff --git a/includes/Article.php b/includes/Article.php index b1e1f620..8c07b06c 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -5,11 +5,6 @@ */ /** - * Need the CacheManager to be loaded - */ -require_once( 'CacheManager.php' ); - -/** * Class representing a MediaWiki article and history. * * See design.txt for an overview. @@ -651,15 +646,16 @@ class Article { # diff page instead of the article. if ( !is_null( $diff ) ) { - require_once( 'DifferenceEngine.php' ); $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid ); // DifferenceEngine directly fetched the revision: $this->mRevIdFetched = $de->mNewid; $de->showDiffPage(); - - if( $diff == 0 ) { + + // Needed to get the page's current revision + $this->loadPageData(); + if( $diff == 0 || $diff == $this->mLatest ) { # Run view updates for current revision only $this->viewUpdates(); } @@ -719,6 +715,7 @@ class Article { $outputDone = false; if ( $pcache ) { if ( $wgOut->tryParserCache( $this, $wgUser ) ) { + wfRunHooks( 'ArticleViewHeader', array( &$this ) ); $outputDone = true; } } @@ -804,13 +801,13 @@ class Article { # Display content, don't attempt to save to parser cache # Don't show section-edit links on old revisions... this way lies madness. if( !$this->isCurrent() ) { - $oldEditSectionSetting = $wgOut->mParserOptions->setEditSection( false ); + $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false ); } # Display content and don't save to parser cache $wgOut->addPrimaryWikiText( $text, $this, false ); if( !$this->isCurrent() ) { - $wgOut->mParserOptions->setEditSection( $oldEditSectionSetting ); + $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); } } } @@ -886,7 +883,7 @@ class Article { } if ((!$wgUser->isAllowed('delete'))) { - $wgOut->sysopRequired(); + $wgOut->permissionRequired( 'delete' ); return; } @@ -1216,7 +1213,7 @@ class Article { # Silently ignore EDIT_MINOR if not allowed $isminor = ( $flags & EDIT_MINOR ) && $wgUser->isAllowed('minoredit'); - $bot = $wgUser->isBot() || ( $flags & EDIT_FORCE_BOT ); + $bot = $wgUser->isAllowed( 'bot' ) || ( $flags & EDIT_FORCE_BOT ); $text = $this->preSaveTransform( $text ); @@ -1447,7 +1444,7 @@ class Article { $wgOut->setPagetitle( wfMsg( 'addedwatch' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $link = $this->mTitle->getPrefixedText(); + $link = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); $text = wfMsg( 'addedwatchtext', $link ); $wgOut->addWikiText( $text ); } @@ -1467,7 +1464,6 @@ class Article { if (wfRunHooks('WatchArticle', array(&$wgUser, &$this))) { $wgUser->addWatch( $this->mTitle ); - $wgUser->saveSettings(); return wfRunHooks('WatchArticleComplete', array(&$wgUser, &$this)); } @@ -1495,7 +1491,7 @@ class Article { $wgOut->setPagetitle( wfMsg( 'removedwatch' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $link = $this->mTitle->getPrefixedText(); + $link = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); $text = wfMsg( 'removedwatchtext', $link ); $wgOut->addWikiText( $text ); } @@ -1515,7 +1511,6 @@ class Article { if (wfRunHooks('UnwatchArticle', array(&$wgUser, &$this))) { $wgUser->removeWatch( $this->mTitle ); - $wgUser->saveSettings(); return wfRunHooks('UnwatchArticleComplete', array(&$wgUser, &$this)); } @@ -1527,7 +1522,6 @@ class Article { * action=protect handler */ function protect() { - require_once 'ProtectionForm.php'; $form = new ProtectionForm( $this ); $form->show(); } @@ -1641,7 +1635,7 @@ class Article { # Check permissions if( $wgUser->isAllowed( 'delete' ) ) { - if( $wgUser->isBlocked() ) { + if( $wgUser->isBlocked( !$confirm ) ) { $wgOut->blockedPage(); return; } @@ -1842,7 +1836,7 @@ class Article { if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason))) { if ( $this->doDeleteArticle( $reason ) ) { - $deleted = $this->mTitle->getPrefixedText(); + $deleted = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); @@ -1910,29 +1904,35 @@ class Article { ); # Now that it's safely backed up, delete it - $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__); - if ($wgUseTrackbacks) - $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ ); + # If using cascading deletes, we can skip some explicit deletes + if ( !$dbw->cascadingDeletes() ) { + + $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); + + if ($wgUseTrackbacks) + $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ ); - # Clean up recentchanges entries... - $dbw->delete( 'recentchanges', array( 'rc_namespace' => $ns, 'rc_title' => $t ), __METHOD__ ); + # Delete outgoing links + $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) ); + $dbw->delete( 'imagelinks', array( 'il_from' => $id ) ); + $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) ); + $dbw->delete( 'templatelinks', array( 'tl_from' => $id ) ); + $dbw->delete( 'externallinks', array( 'el_from' => $id ) ); + $dbw->delete( 'langlinks', array( 'll_from' => $id ) ); + } + + # If using cleanup triggers, we can skip some manual deletes + if ( !$dbw->cleanupTriggers() ) { - # Finally, clean up the link tables - $t = $this->mTitle->getPrefixedDBkey(); + # Clean up recentchanges entries... + $dbw->delete( 'recentchanges', array( 'rc_namespace' => $ns, 'rc_title' => $t ), __METHOD__ ); + } # Clear caches Article::onArticleDelete( $this->mTitle ); - # Delete outgoing links - $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) ); - $dbw->delete( 'imagelinks', array( 'il_from' => $id ) ); - $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) ); - $dbw->delete( 'templatelinks', array( 'tl_from' => $id ) ); - $dbw->delete( 'externallinks', array( 'el_from' => $id ) ); - $dbw->delete( 'langlinks', array( 'll_from' => $id ) ); - # Log the deletion $log = new LogPage( 'delete' ); $log->addEntry( 'delete', $this->mTitle, $reason ); @@ -2141,7 +2141,7 @@ class Article { # If this is another user's talk page, update newtalk # Don't do this if $changed = false otherwise some idiot can null-edit a # load of user talk pages and piss people off - if( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getName() && $changed ) { + if( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed ) { if (wfRunHooks('ArticleEditUpdateNewTalk', array(&$this)) ) { $other = User::newFromName( $shortTitle ); if( is_null( $other ) && User::isIP( $shortTitle ) ) { @@ -2161,6 +2161,22 @@ 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 + * other shitty functions like editUpdates and such so it's not needed + * anymore. + */ + function createUpdates( $rev ) { + $this->mGoodAdjustment = $this->isCountable( $rev->getText() ); + $this->mTotalAdjustment = 1; + $this->editUpdates( $rev->getText(), $rev->getComment(), + $rev->isMinor(), wfTimestamp(), $rev->getId(), true ); + } /** * Generate the navigation links when browsing through an article revisions @@ -2174,6 +2190,10 @@ class Article { function setOldSubtitle( $oldid=0 ) { global $wgLang, $wgOut, $wgUser; + if ( !wfRunHooks( 'DisplayOldSubtitle', array(&$this, &$oldid) ) ) { + return; + } + $revision = Revision::newFromId( $oldid ); $current = ( $oldid == $this->mLatest ); diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 7d09d5b6..810a448e 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -10,6 +10,7 @@ function __autoload($className) { static $localClasses = array( 'AjaxDispatcher' => 'includes/AjaxDispatcher.php', 'AjaxCachePolicy' => 'includes/AjaxFunctions.php', + 'AjaxResponse' => 'includes/AjaxResponse.php', 'Article' => 'includes/Article.php', 'AuthPlugin' => 'includes/AuthPlugin.php', 'BagOStuff' => 'includes/BagOStuff.php', @@ -19,9 +20,11 @@ function __autoload($className) { 'TurckBagOStuff' => 'includes/BagOStuff.php', 'APCBagOStuff' => 'includes/BagOStuff.php', 'eAccelBagOStuff' => 'includes/BagOStuff.php', + 'DBABagOStuff' => 'includes/BagOStuff.php', 'Block' => 'includes/Block.php', 'CacheManager' => 'includes/CacheManager.php', 'CategoryPage' => 'includes/CategoryPage.php', + 'CategoryViewer' => 'includes/CategoryPage.php', 'Categoryfinder' => 'includes/Categoryfinder.php', 'RCCacheEntry' => 'includes/ChangesList.php', 'ChangesList' => 'includes/ChangesList.php', @@ -89,6 +92,7 @@ function __autoload($className) { 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php', 'Http' => 'includes/HttpFunctions.php', 'Image' => 'includes/Image.php', + 'IP' => 'includes/IP.php', 'ThumbnailImage' => 'includes/Image.php', 'ImageGallery' => 'includes/ImageGallery.php', 'ImagePage' => 'includes/ImagePage.php', @@ -113,16 +117,16 @@ function __autoload($className) { 'FakeMemCachedClient' => 'includes/ObjectCache.php', 'OutputPage' => 'includes/OutputPage.php', 'PageHistory' => 'includes/PageHistory.php', + 'IndexPager' => 'includes/Pager.php', + 'ReverseChronologicalPager' => 'includes/Pager.php', + 'TablePager' => 'includes/Pager.php', 'Parser' => 'includes/Parser.php', 'ParserOutput' => 'includes/Parser.php', 'ParserOptions' => 'includes/Parser.php', 'ParserCache' => 'includes/ParserCache.php', - 'element' => 'includes/ParserXML.php', - 'xml2php' => 'includes/ParserXML.php', - 'ParserXML' => 'includes/ParserXML.php', 'ProfilerSimple' => 'includes/ProfilerSimple.php', 'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php', - 'Profiler' => 'includes/Profiling.php', + 'Profiler' => 'includes/Profiler.php', 'ProxyTools' => 'includes/ProxyTools.php', 'ProtectionForm' => 'includes/ProtectionForm.php', 'QueryPage' => 'includes/QueryPage.php', @@ -213,6 +217,7 @@ function __autoload($className) { 'EmailNotification' => 'includes/UserMailer.php', 'WatchedItem' => 'includes/WatchedItem.php', 'WebRequest' => 'includes/WebRequest.php', + 'WebResponse' => 'includes/WebResponse.php', 'FauxRequest' => 'includes/WebRequest.php', 'MediaWiki' => 'includes/Wiki.php', 'WikiError' => 'includes/WikiError.php', @@ -221,7 +226,10 @@ function __autoload($className) { 'Xml' => 'includes/Xml.php', 'ZhClient' => 'includes/ZhClient.php', 'memcached' => 'includes/memcached-client.php', - 'UtfNormal' => 'includes/normal/UtfNormal.php' + 'UtfNormal' => 'includes/normal/UtfNormal.php', + 'UsercreateTemplate' => 'includes/templates/Userlogin.php', + 'UserloginTemplate' => 'includes/templates/Userlogin.php', + 'Language' => 'languages/Language.php', ); if ( isset( $localClasses[$className] ) ) { $filename = $localClasses[$className]; diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php index 182756ab..1dc93a2f 100644 --- a/includes/BagOStuff.php +++ b/includes/BagOStuff.php @@ -146,6 +146,17 @@ class BagOStuff { if($this->debugmode) wfDebug("BagOStuff debug: $text\n"); } + + /** + * Convert an optionally relative time to an absolute time + */ + static function convertExpiry( $exptime ) { + if(($exptime != 0) && ($exptime < 3600*24*30)) { + return time() + $exptime; + } else { + return $exptime; + } + } } @@ -183,9 +194,7 @@ class HashBagOStuff extends BagOStuff { } function set($key,$value,$exptime=0) { - if(($exptime != 0) && ($exptime < 3600*24*30)) - $exptime = time() + $exptime; - $this->bag[$key] = array( $value, $exptime ); + $this->bag[$key] = array( $value, BagOStuff::convertExpiry( $exptime ) ); } function delete($key,$time=0) { @@ -491,7 +500,7 @@ class APCBagOStuff extends BagOStuff { return true; } - function delete($key) { + function delete($key, $time=0) { apc_delete($key); return true; } @@ -535,4 +544,136 @@ class eAccelBagOStuff extends BagOStuff { return true; } } + +class DBABagOStuff extends BagOStuff { + var $mHandler, $mFile, $mReader, $mWriter, $mDisabled; + + function __construct( $handler = 'db3', $dir = false ) { + if ( $dir === false ) { + global $wgTmpDirectory; + $dir = $wgTmpDirectory; + } + $this->mFile = "$dir/mw-cache-" . wfWikiID(); + $this->mFile .= '.db'; + $this->mHandler = $handler; + } + + /** + * Encode value and expiry for storage + */ + function encode( $value, $expiry ) { + # Convert to absolute time + $expiry = BagOStuff::convertExpiry( $expiry ); + return sprintf( '%010u', intval( $expiry ) ) . ' ' . serialize( $value ); + } + + /** + * @return list containing value first and expiry second + */ + function decode( $blob ) { + if ( !is_string( $blob ) ) { + return array( null, 0 ); + } else { + return array( + unserialize( substr( $blob, 11 ) ), + intval( substr( $blob, 0, 10 ) ) + ); + } + } + + function getReader() { + if ( file_exists( $this->mFile ) ) { + $handle = dba_open( $this->mFile, 'rl', $this->mHandler ); + } else { + $handle = $this->getWriter(); + } + if ( !$handle ) { + wfDebug( "Unable to open DBA cache file {$this->mFile}\n" ); + } + return $handle; + } + + function getWriter() { + $handle = dba_open( $this->mFile, 'cl', $this->mHandler ); + if ( !$handle ) { + wfDebug( "Unable to open DBA cache file {$this->mFile}\n" ); + } + return $handle; + } + + function get( $key ) { + wfProfileIn( __METHOD__ ); + wfDebug( __METHOD__."($key)\n" ); + $handle = $this->getReader(); + if ( !$handle ) { + return null; + } + $val = dba_fetch( $key, $handle ); + list( $val, $expiry ) = $this->decode( $val ); + # Must close ASAP because locks are held + dba_close( $handle ); + + if ( !is_null( $val ) && $expiry && $expiry < time() ) { + # Key is expired, delete it + $handle = $this->getWriter(); + dba_delete( $key, $handle ); + dba_close( $handle ); + wfDebug( __METHOD__.": $key expired\n" ); + $val = null; + } + wfProfileOut( __METHOD__ ); + return $val; + } + + function set( $key, $value, $exptime=0 ) { + wfProfileIn( __METHOD__ ); + wfDebug( __METHOD__."($key)\n" ); + $blob = $this->encode( $value, $exptime ); + $handle = $this->getWriter(); + if ( !$handle ) { + return false; + } + $ret = dba_replace( $key, $blob, $handle ); + dba_close( $handle ); + wfProfileOut( __METHOD__ ); + return $ret; + } + + function delete( $key, $time = 0 ) { + wfProfileIn( __METHOD__ ); + $handle = $this->getWriter(); + if ( !$handle ) { + return false; + } + $ret = dba_delete( $key, $handle ); + dba_close( $handle ); + wfProfileOut( __METHOD__ ); + return $ret; + } + + function add( $key, $value, $exptime = 0 ) { + wfProfileIn( __METHOD__ ); + $blob = $this->encode( $value, $exptime ); + $handle = $this->getWriter(); + if ( !$handle ) { + return false; + } + $ret = dba_insert( $key, $blob, $handle ); + # Insert failed, check to see if it failed due to an expired key + if ( !$ret ) { + list( $value, $expiry ) = $this->decode( dba_fetch( $key, $handle ) ); + if ( $expiry < time() ) { + # Yes expired, delete and try again + dba_delete( $key, $handle ); + $ret = dba_insert( $key, $blob, $handle ); + # This time if it failed then it will be handled by the caller like any other race + } + } + + dba_close( $handle ); + wfProfileOut( __METHOD__ ); + return $ret; + } +} + ?> diff --git a/includes/Block.php b/includes/Block.php index 26fa444d..b11df22c 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -9,7 +9,6 @@ * All the functions in this class assume the object is either explicitly * loaded or filled. It is not load-on-demand. There are no accessors. * - * To use delete(), you only need to fill $mAddress * Globals used: $wgAutoblockExpiry, $wgAntiLockFlags * * @todo This could be used everywhere, but it isn't. @@ -18,27 +17,26 @@ class Block { /* public*/ var $mAddress, $mUser, $mBy, $mReason, $mTimestamp, $mAuto, $mId, $mExpiry, - $mRangeStart, $mRangeEnd; + $mRangeStart, $mRangeEnd, $mAnonOnly; /* 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 = '', $by = 0, $reason = '', - $timestamp = '' , $auto = 0, $expiry = '' ) + function Block( $address = '', $user = 0, $by = 0, $reason = '', + $timestamp = '' , $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0 ) { + $this->mId = 0; $this->mAddress = $address; $this->mUser = $user; $this->mBy = $by; $this->mReason = $reason; $this->mTimestamp = wfTimestamp(TS_MW,$timestamp); $this->mAuto = $auto; - if( empty( $expiry ) ) { - $this->mExpiry = $expiry; - } else { - $this->mExpiry = wfTimestamp( TS_MW, $expiry ); - } + $this->mAnonOnly = $anonOnly; + $this->mCreateAccount = $createAccount; + $this->mExpiry = self::decodeExpiry( $expiry ); $this->mForUpdate = false; $this->mFromMaster = false; @@ -46,19 +44,36 @@ class Block $this->initialiseRange(); } - /*static*/ function newFromDB( $address, $user = 0, $killExpired = true ) + static function newFromDB( $address, $user = 0, $killExpired = true ) { - $ban = new Block(); - $ban->load( $address, $user, $killExpired ); - return $ban; + $block = new Block(); + $block->load( $address, $user, $killExpired ); + if ( $block->isValid() ) { + return $block; + } else { + return null; + } + } + + static function newFromID( $id ) + { + $dbr =& wfGetDB( DB_SLAVE ); + $res = $dbr->resultObject( $dbr->select( 'ipblocks', '*', + array( 'ipb_id' => $id ), __METHOD__ ) ); + $block = new Block; + if ( $block->loadFromResult( $res ) ) { + return $block; + } else { + return null; + } } function clear() { $this->mAddress = $this->mReason = $this->mTimestamp = ''; - $this->mUser = $this->mBy = 0; + $this->mId = $this->mAnonOnly = $this->mCreateAccount = + $this->mAuto = $this->mUser = $this->mBy = 0; $this->mByName = false; - } /** @@ -70,56 +85,103 @@ class Block if ( $this->mForUpdate || $this->mFromMaster ) { $db =& wfGetDB( DB_MASTER ); if ( !$this->mForUpdate || ($wgAntiLockFlags & ALF_NO_BLOCK_LOCK) ) { - $options = ''; + $options = array(); } else { - $options = 'FOR UPDATE'; + $options = array( 'FOR UPDATE' ); } } else { $db =& wfGetDB( DB_SLAVE ); - $options = ''; + $options = array(); } return $db; } /** * Get a ban from the DB, with either the given address or the given username + * + * @param string $address The IP address of the user, or blank to skip IP blocks + * @param integer $user The user ID, or zero for anonymous users + * @param bool $killExpired Whether to delete expired rows while loading + * */ function load( $address = '', $user = 0, $killExpired = true ) { - $fname = 'Block::load'; wfDebug( "Block::load: '$address', '$user', $killExpired\n" ); - $options = ''; + $options = array(); $db =& $this->getDBOptions( $options ); $ret = false; $killed = false; - $ipblocks = $db->tableName( 'ipblocks' ); if ( 0 == $user && $address == '' ) { # Invalid user specification, not blocked $this->clear(); return false; - } elseif ( $address == '' ) { - $sql = "SELECT * FROM $ipblocks WHERE ipb_user={$user} $options"; - } elseif ( $user == '' ) { - $sql = "SELECT * FROM $ipblocks WHERE ipb_address=" . $db->addQuotes( $address ) . " $options"; - } elseif ( $options == '' ) { - # If there are no options (e.g. FOR UPDATE), use a UNION - # so that the query can make efficient use of indices - $sql = "SELECT * FROM $ipblocks WHERE ipb_address='" . $db->strencode( $address ) . - "' UNION SELECT * FROM $ipblocks WHERE ipb_user={$user}"; - } else { - # If there are options, a UNION can not be used, use one - # SELECT instead. Will do a full table scan. - $sql = "SELECT * FROM $ipblocks WHERE (ipb_address='" . $db->strencode( $address ) . - "' OR ipb_user={$user}) $options"; } - $res = $db->query( $sql, $fname ); - if ( 0 != $db->numRows( $res ) ) { + # Try user block + if ( $user ) { + $res = $db->resultObject( $db->select( 'ipblocks', '*', array( 'ipb_user' => $user ), + __METHOD__, $options ) ); + if ( $this->loadFromResult( $res, $killExpired ) ) { + return true; + } + } + + # Try IP block + # TODO: improve performance by merging this query with the autoblock one + # Slightly tricky while handling killExpired as well + if ( $address ) { + $conds = array( 'ipb_address' => $address, 'ipb_auto' => 0 ); + $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); + if ( $this->loadFromResult( $res, $killExpired ) ) { + if ( $user && $this->mAnonOnly ) { + # Block is marked anon-only + # Whitelist this IP address against autoblocks and range blocks + $this->clear(); + return false; + } else { + return true; + } + } + } + + # Try range block + if ( $this->loadRange( $address, $killExpired, $user == 0 ) ) { + if ( $user && $this->mAnonOnly ) { + $this->clear(); + return false; + } else { + return true; + } + } + + # Try autoblock + if ( $address ) { + $conds = array( 'ipb_address' => $address, 'ipb_auto' => 1 ); + if ( $user ) { + $conds['ipb_anon_only'] = 0; + } + $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); + if ( $this->loadFromResult( $res, $killExpired ) ) { + return true; + } + } + + # Give up + $this->clear(); + return false; + } + + /** + * Fill in member variables from a result wrapper + */ + function loadFromResult( ResultWrapper $res, $killExpired = true ) { + $ret = false; + if ( 0 != $res->numRows() ) { # Get first block - $row = $db->fetchObject( $res ); + $row = $res->fetchObject(); $this->initFromRow( $row ); if ( $killExpired ) { @@ -127,7 +189,7 @@ class Block do { $killed = $this->deleteIfExpired(); if ( $killed ) { - $row = $db->fetchObject( $res ); + $row = $res->fetchObject(); if ( $row ) { $this->initFromRow( $row ); } @@ -135,26 +197,14 @@ class Block } while ( $killed && $row ); # If there were any left after the killing finished, return true - if ( !$row ) { - $ret = false; - $this->clear(); - } else { + if ( $row ) { $ret = true; } } else { $ret = true; } } - $db->freeResult( $res ); - - # No blocks found yet? Try looking for range blocks - if ( !$ret && $address != '' ) { - $ret = $this->loadRange( $address, $killExpired ); - } - if ( !$ret ) { - $this->clear(); - } - + $res->free(); return $ret; } @@ -164,9 +214,7 @@ class Block */ function loadRange( $address, $killExpired = true ) { - $fname = 'Block::loadRange'; - - $iaddr = wfIP2Hex( $address ); + $iaddr = IP::toHex( $address ); if ( $iaddr === false ) { # Invalid address return false; @@ -176,27 +224,16 @@ class Block # Blocks should not cross a /16 boundary. $range = substr( $iaddr, 0, 4 ); - $options = ''; + $options = array(); $db =& $this->getDBOptions( $options ); - $ipblocks = $db->tableName( 'ipblocks' ); - $sql = "SELECT * FROM $ipblocks WHERE ipb_range_start LIKE '$range%' ". - "AND ipb_range_start <= '$iaddr' AND ipb_range_end >= '$iaddr' $options"; - $res = $db->query( $sql, $fname ); - $row = $db->fetchObject( $res ); - - $success = false; - if ( $row ) { - # Found a row, initialise this object - $this->initFromRow( $row ); - - # Is it expired? - if ( !$killExpired || !$this->deleteIfExpired() ) { - # No, return true - $success = true; - } - } + $conds = array( + "ipb_range_start LIKE '$range%'", + "ipb_range_start <= '$iaddr'", + "ipb_range_end >= '$iaddr'" + ); - $db->freeResult( $res ); + $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); + $success = $this->loadFromResult( $res, $killExpired ); return $success; } @@ -220,10 +257,10 @@ class Block $this->mUser = $row->ipb_user; $this->mBy = $row->ipb_by; $this->mAuto = $row->ipb_auto; + $this->mAnonOnly = $row->ipb_anon_only; + $this->mCreateAccount = $row->ipb_create_account; $this->mId = $row->ipb_id; - $this->mExpiry = $row->ipb_expiry ? - wfTimestamp(TS_MW,$row->ipb_expiry) : - $row->ipb_expiry; + $this->mExpiry = self::decodeExpiry( $row->ipb_expiry ); if ( isset( $row->user_name ) ) { $this->mByName = $row->user_name; } else { @@ -304,24 +341,32 @@ class Block function delete() { - $fname = 'Block::delete'; if (wfReadOnly()) { - return; + return false; } - $dbw =& wfGetDB( DB_MASTER ); - - if ( $this->mAddress == '' ) { - $condition = array( 'ipb_id' => $this->mId ); - } else { - $condition = array( 'ipb_address' => $this->mAddress ); + if ( !$this->mId ) { + throw new MWException( "Block::delete() now requires that the mId member be filled\n" ); } - return( $dbw->delete( 'ipblocks', $condition, $fname ) > 0 ? true : false ); + + $dbw =& wfGetDB( DB_MASTER ); + $dbw->delete( 'ipblocks', array( 'ipb_id' => $this->mId ), __METHOD__ ); + return $dbw->affectedRows() > 0; } function insert() { wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" ); $dbw =& wfGetDB( DB_MASTER ); + $dbw->begin(); + + # Unset ipb_anon_only for user blocks, makes no sense + if ( $this->mUser ) { + $this->mAnonOnly = 0; + } + + # Don't collide with expired blocks + Block::purgeExpired(); + $ipb_id = $dbw->nextSequenceValue('ipblocks_ipb_id_val'); $dbw->insert( 'ipblocks', array( @@ -332,13 +377,16 @@ class Block 'ipb_reason' => $this->mReason, 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), 'ipb_auto' => $this->mAuto, - 'ipb_expiry' => $this->mExpiry ? - $dbw->timestamp($this->mExpiry) : - $this->mExpiry, + 'ipb_anon_only' => $this->mAnonOnly, + 'ipb_create_account' => $this->mCreateAccount, + 'ipb_expiry' => self::encodeExpiry( $this->mExpiry, $dbw ), 'ipb_range_start' => $this->mRangeStart, 'ipb_range_end' => $this->mRangeEnd, - ), 'Block::insert' + ), 'Block::insert', array( 'IGNORE' ) ); + $affected = $dbw->affectedRows(); + $dbw->commit(); + return $affected; } function deleteIfExpired() @@ -417,18 +465,48 @@ class Block return wfSetVar( $this->mFromMaster, $x ); } - /* static */ function getAutoblockExpiry( $timestamp ) + function getRedactedName() { + if ( $this->mAuto ) { + return '#' . $this->mId; + } else { + return $this->mAddress; + } + } + + /** + * Encode expiry for DB + */ + static function encodeExpiry( $expiry, $db ) { + if ( $expiry == '' || $expiry == Block::infinity() ) { + return Block::infinity(); + } else { + return $db->timestamp( $expiry ); + } + } + + /** + * Decode expiry which has come from the DB + */ + static function decodeExpiry( $expiry ) { + if ( $expiry == '' || $expiry == Block::infinity() ) { + return Block::infinity(); + } else { + return wfTimestamp( TS_MW, $expiry ); + } + } + + static function getAutoblockExpiry( $timestamp ) { global $wgAutoblockExpiry; return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry ); } - /* static */ function normaliseRange( $range ) + static function normaliseRange( $range ) { $parts = explode( '/', $range ); if ( count( $parts ) == 2 ) { $shift = 32 - $parts[1]; - $ipint = wfIP2Unsigned( $parts[0] ); + $ipint = IP::toUnsigned( $parts[0] ); $ipint = $ipint >> $shift << $shift; $newip = long2ip( $ipint ); $range = "$newip/{$parts[1]}"; @@ -436,5 +514,28 @@ class Block return $range; } + /** + * Purge expired blocks from the ipblocks table + */ + static function purgeExpired() { + $dbw =& wfGetDB( DB_MASTER ); + $dbw->delete( 'ipblocks', array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), __METHOD__ ); + } + + static function infinity() { + # This is a special keyword for timestamps in PostgreSQL, and + # works with CHAR(14) as well because "i" sorts after all numbers. + return 'infinity'; + + /* + static $infinity; + if ( !isset( $infinity ) ) { + $dbr =& wfGetDB( DB_SLAVE ); + $infinity = $dbr->bigTimestamp(); + } + return $infinity; + */ + } + } ?> diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php index 53d69971..e55d2976 100644 --- a/includes/CategoryPage.php +++ b/includes/CategoryPage.php @@ -13,7 +13,6 @@ if( !defined( 'MEDIAWIKI' ) ) * @package MediaWiki */ class CategoryPage extends Article { - function view() { if(!wfRunHooks('CategoryPageView', array(&$this))) return; @@ -40,10 +39,27 @@ class CategoryPage extends Article { global $wgOut, $wgRequest; $from = $wgRequest->getVal( 'from' ); $until = $wgRequest->getVal( 'until' ); - - $wgOut->addHTML( $this->doCategoryMagic( $from, $until ) ); + + $viewer = new CategoryViewer( $this->mTitle, $from, $until ); + $wgOut->addHTML( $viewer->getHTML() ); } +} +class CategoryViewer { + var $title, $limit, $from, $until, + $articles, $articles_start_char, + $children, $children_start_char, + $showGallery, $gallery, + $skin; + + function __construct( $title, $from = '', $until = '' ) { + global $wgCategoryPagingLimit; + $this->title = $title; + $this->from = $from; + $this->until = $until; + $this->limit = $wgCategoryPagingLimit; + } + /** * Format the category data list. * @@ -52,130 +68,205 @@ class CategoryPage extends Article { * @return string HTML output * @private */ - function doCategoryMagic( $from = '', $until = '' ) { - global $wgOut; - global $wgContLang,$wgUser, $wgCategoryMagicGallery, $wgCategoryPagingLimit; - $fname = 'CategoryPage::doCategoryMagic'; - wfProfileIn( $fname ); - - $articles = array(); - $articles_start_char = array(); - $children = array(); - $children_start_char = array(); + function getHTML() { + global $wgOut, $wgCategoryMagicGallery, $wgCategoryPagingLimit; + wfProfileIn( __METHOD__ ); + + $this->showGallery = $wgCategoryMagicGallery && !$wgOut->mNoGallery; + + $this->clearCategoryState(); + $this->doCategoryQuery(); + $this->finaliseCategoryState(); + + $r = $this->getCategoryTop() . + $this->getSubcategorySection() . + $this->getPagesSection() . + $this->getImageSection() . + $this->getCategoryBottom(); + + wfProfileOut( __METHOD__ ); + return $r; + } + + function clearCategoryState() { + $this->articles = array(); + $this->articles_start_char = array(); + $this->children = array(); + $this->children_start_char = array(); + if( $this->showGallery ) { + $this->gallery = new ImageGallery(); + $this->gallery->setParsing(); + } + } + + function getSkin() { + if ( !$this->skin ) { + global $wgUser; + $this->skin = $wgUser->getSkin(); + } + return $this->skin; + } + + /** + * Add a subcategory to the internal lists + */ + function addSubcategory( $title, $sortkey, $pageLength ) { + global $wgContLang; + // Subcategory; strip the 'Category' namespace from the link text. + $this->children[] = $this->getSkin()->makeKnownLinkObj( + $title, $wgContLang->convertHtml( $title->getText() ) ); + + $this->children_start_char[] = $this->getSubcategorySortChar( $title, $sortkey ); + } + + /** + * Get the character to be used for sorting subcategories. + * If there's a link from Category:A to Category:B, the sortkey of the resulting + * entry in the categorylinks table is Category:A, not A, which it SHOULD be. + * Workaround: If sortkey == "Category:".$title, than use $title for sorting, + * else use sortkey... + */ + function getSubcategorySortChar( $title, $sortkey ) { + global $wgContLang; + + if( $title->getPrefixedText() == $sortkey ) { + $firstChar = $wgContLang->firstChar( $title->getDBkey() ); + } else { + $firstChar = $wgContLang->firstChar( $sortkey ); + } - $showGallery = $wgCategoryMagicGallery && !$wgOut->mNoGallery; - if( $showGallery ) { - $ig = new ImageGallery(); - $ig->setParsing(); + return $wgContLang->convert( $firstChar ); + } + + /** + * Add a page in the image namespace + */ + function addImage( $title, $sortkey, $pageLength ) { + if ( $this->showGallery ) { + $image = new Image( $title ); + if( $this->flip ) { + $this->gallery->insert( $image ); + } else { + $this->gallery->add( $image ); + } + } else { + $this->addPage( $title, $sortkey, $pageLength ); } + } + /** + * Add a miscellaneous page + */ + function addPage( $title, $sortkey, $pageLength ) { + global $wgContLang; + $this->articles[] = $this->getSkin()->makeSizeLinkObj( + $pageLength, $title, $wgContLang->convert( $title->getPrefixedText() ) + ); + $this->articles_start_char[] = $wgContLang->convert( $wgContLang->firstChar( $sortkey ) ); + } + + function finaliseCategoryState() { + if( $this->flip ) { + $this->children = array_reverse( $this->children ); + $this->children_start_char = array_reverse( $this->children_start_char ); + $this->articles = array_reverse( $this->articles ); + $this->articles_start_char = array_reverse( $this->articles_start_char ); + } + } + + function doCategoryQuery() { $dbr =& wfGetDB( DB_SLAVE ); - if( $from != '' ) { - $pageCondition = 'cl_sortkey >= ' . $dbr->addQuotes( $from ); - $flip = false; - } elseif( $until != '' ) { - $pageCondition = 'cl_sortkey < ' . $dbr->addQuotes( $until ); - $flip = true; + if( $this->from != '' ) { + $pageCondition = 'cl_sortkey >= ' . $dbr->addQuotes( $this->from ); + $this->flip = false; + } elseif( $this->until != '' ) { + $pageCondition = 'cl_sortkey < ' . $dbr->addQuotes( $this->until ); + $this->flip = true; } else { $pageCondition = '1 = 1'; - $flip = false; + $this->flip = false; } - $limit = $wgCategoryPagingLimit; $res = $dbr->select( array( 'page', 'categorylinks' ), array( 'page_title', 'page_namespace', 'page_len', 'cl_sortkey' ), array( $pageCondition, 'cl_from = page_id', - 'cl_to' => $this->mTitle->getDBKey()), + 'cl_to' => $this->title->getDBKey()), #'page_is_redirect' => 0), #+ $pageCondition, - $fname, - array( 'ORDER BY' => $flip ? 'cl_sortkey DESC' : 'cl_sortkey', - 'LIMIT' => $limit + 1 ) ); + __METHOD__, + array( 'ORDER BY' => $this->flip ? 'cl_sortkey DESC' : 'cl_sortkey', + 'LIMIT' => $this->limit + 1 ) ); - $sk =& $wgUser->getSkin(); - $r = "<br style=\"clear:both;\"/>\n"; $count = 0; - $nextPage = null; + $this->nextPage = null; while( $x = $dbr->fetchObject ( $res ) ) { - if( ++$count > $limit ) { + if( ++$count > $this->limit ) { // We've reached the one extra which shows that there are // additional pages to be had. Stop here... - $nextPage = $x->cl_sortkey; + $this->nextPage = $x->cl_sortkey; break; } $title = Title::makeTitle( $x->page_namespace, $x->page_title ); if( $title->getNamespace() == NS_CATEGORY ) { - // Subcategory; strip the 'Category' namespace from the link text. - array_push( $children, $sk->makeKnownLinkObj( $title, $wgContLang->convertHtml( $title->getText() ) ) ); - - // If there's a link from Category:A to Category:B, the sortkey of the resulting - // entry in the categorylinks table is Category:A, not A, which it SHOULD be. - // Workaround: If sortkey == "Category:".$title, than use $title for sorting, - // else use sortkey... - $sortkey=''; - if( $title->getPrefixedText() == $x->cl_sortkey ) { - $sortkey=$wgContLang->firstChar( $x->page_title ); - } else { - $sortkey=$wgContLang->firstChar( $x->cl_sortkey ); - } - array_push( $children_start_char, $wgContLang->convert( $sortkey ) ) ; - } elseif( $showGallery && $title->getNamespace() == NS_IMAGE ) { - // Show thumbnails of categorized images, in a separate chunk - if( $flip ) { - $ig->insert( Image::newFromTitle( $title ) ); - } else { - $ig->add( Image::newFromTitle( $title ) ); - } + $this->addSubcategory( $title, $x->cl_sortkey, $x->page_len ); + } elseif( $title->getNamespace() == NS_IMAGE ) { + $this->addImage( $title, $x->cl_sortkey, $x->page_len ); } else { - // Page in this category - array_push( $articles, $sk->makeSizeLinkObj( $x->page_len, $title, $wgContLang->convert( $title->getPrefixedText() ) ) ) ; - array_push( $articles_start_char, $wgContLang->convert( $wgContLang->firstChar( $x->cl_sortkey ) ) ); + $this->addPage( $title, $x->cl_sortkey, $x->page_len ); } } $dbr->freeResult( $res ); + } - if( $flip ) { - $children = array_reverse( $children ); - $children_start_char = array_reverse( $children_start_char ); - $articles = array_reverse( $articles ); - $articles_start_char = array_reverse( $articles_start_char ); - } - - if( $until != '' ) { - $r .= $this->pagingLinks( $this->mTitle, $nextPage, $until, $limit ); - } elseif( $nextPage != '' || $from != '' ) { - $r .= $this->pagingLinks( $this->mTitle, $from, $nextPage, $limit ); + function getCategoryTop() { + $r = "<br style=\"clear:both;\"/>\n"; + if( $this->until != '' ) { + $r .= $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit ); + } elseif( $this->nextPage != '' || $this->from != '' ) { + $r .= $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit ); } + return $r; + } + function getSubcategorySection() { # Don't show subcategories section if there are none. - if( count( $children ) > 0 ) { + $r = ''; + if( count( $this->children ) > 0 ) { # Showing subcategories $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n"; - $r .= wfMsgExt( 'subcategorycount', array( 'parse' ), count( $children) ); - $r .= $this->formatList( $children, $children_start_char ); + $r .= wfMsgExt( 'subcategorycount', array( 'parse' ), count( $this->children) ); + $r .= $this->formatList( $this->children, $this->children_start_char ); } + return $r; + } - # Showing articles in this category - $ti = htmlspecialchars( $this->mTitle->getText() ); - $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n"; - $r .= wfMsgExt( 'categoryarticlecount', array( 'parse' ), count( $articles) ); - $r .= $this->formatList( $articles, $articles_start_char ); + function getPagesSection() { + $ti = htmlspecialchars( $this->title->getText() ); + $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 ); + return $r; + } - if( $showGallery && ! $ig->isEmpty() ) { - $r.= $ig->toHTML(); + function getImageSection() { + if( $this->showGallery && ! $this->gallery->isEmpty() ) { + return $this->gallery->toHTML(); + } else { + return ''; } + } - if( $until != '' ) { - $r .= $this->pagingLinks( $this->mTitle, $nextPage, $until, $limit ); - } elseif( $nextPage != '' || $from != '' ) { - $r .= $this->pagingLinks( $this->mTitle, $from, $nextPage, $limit ); + function getCategoryBottom() { + if( $this->until != '' ) { + return $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit ); + } elseif( $this->nextPage != '' || $this->from != '' ) { + return $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit ); + } else { + return ''; } - - wfProfileOut( $fname ); - return $r; } /** @@ -293,7 +384,7 @@ class CategoryPage extends Article { */ function pagingLinks( $title, $first, $last, $limit, $query = array() ) { global $wgUser, $wgLang; - $sk =& $wgUser->getSkin(); + $sk =& $this->getSkin(); $limitText = $wgLang->formatNum( $limit ); $prevLink = htmlspecialchars( wfMsg( 'prevn', $limitText ) ); diff --git a/includes/ChangesList.php b/includes/ChangesList.php index b2c1abe2..6797bb41 100644 --- a/includes/ChangesList.php +++ b/includes/ChangesList.php @@ -394,6 +394,7 @@ class EnhancedChangesList extends ChangesList { * Enhanced RC group */ function recentChangesBlockGroup( $block ) { + global $wgContLang; $r = ''; # Collate list of users @@ -423,6 +424,7 @@ class EnhancedChangesList extends ChangesList { $users = array(); foreach( $userlinks as $userlink => $count) { $text = $userlink; + $text .= $wgContLang->getDirMark(); if( $count > 1 ) { $text .= ' ('.$count.'×)'; } @@ -450,6 +452,7 @@ class EnhancedChangesList extends ChangesList { # Article link $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched ); + $r .= $wgContLang->getDirMark(); $curIdEq = 'curid=' . $block[0]->mAttribs['rc_cur_id']; $currentRevision = $block[0]->mAttribs['rc_this_oldid']; diff --git a/includes/CoreParserFunctions.php b/includes/CoreParserFunctions.php index d6578abf..2081b3f2 100644 --- a/includes/CoreParserFunctions.php +++ b/includes/CoreParserFunctions.php @@ -5,6 +5,15 @@ */ class CoreParserFunctions { + static function intFunction( $parser, $part1 = '' /*, ... */ ) { + if ( strval( $part1 ) !== '' ) { + $args = array_slice( func_get_args(), 2 ); + return wfMsgReal( $part1, $args, true ); + } else { + return array( 'found' => false ); + } + } + static function ns( $parser, $part1 = '' ) { global $wgContLang; $found = false; @@ -106,7 +115,7 @@ class CoreParserFunctions { function isRaw( $param ) { static $mwRaw; if ( !$mwRaw ) { - $mwRaw =& MagicWord::get( MAG_RAWSUFFIX ); + $mwRaw =& MagicWord::get( 'rawsuffix' ); } if ( is_null( $param ) ) { return false; @@ -145,6 +154,27 @@ class CoreParserFunctions { $lang = $wgContLang->getLanguageName( strtolower( $arg ) ); return $lang != '' ? $lang : $arg; } + + function pad( $string = '', $length = 0, $char = 0, $direction = STR_PAD_RIGHT ) { + $length = min( max( $length, 0 ), 500 ); + $char = substr( $char, 0, 1 ); + return ( $string && (int)$length > 0 && strlen( trim( (string)$char ) ) > 0 ) + ? str_pad( $string, $length, (string)$char, $direction ) + : $string; + } + + function padleft( $parser, $string = '', $length = 0, $char = 0 ) { + return self::pad( $string, $length, $char, STR_PAD_LEFT ); + } + + function padright( $parser, $string = '', $length = 0, $char = 0 ) { + return self::pad( $string, $length, $char ); + } + + function anchorencode( $parser, $text ) { + return str_replace( '%', '.', str_replace('+', '_', urlencode( $text ) ) ); + } + } ?> diff --git a/includes/Credits.php b/includes/Credits.php index ff33de74..62f0b256 100644 --- a/includes/Credits.php +++ b/includes/Credits.php @@ -87,11 +87,13 @@ function getAuthorCredits($article) { $timestamp = $article->getTimestamp(); if ($timestamp) { - $d = $wgLang->timeanddate($article->getTimestamp(), true); + $d = $wgLang->date($article->getTimestamp(), true); + $t = $wgLang->time($article->getTimestamp(), true); } else { $d = ''; + $t = ''; } - return wfMsg('lastmodifiedby', $d, $author_credit); + return wfMsg('lastmodifiedatby', $d, $t, $author_credit); } /** diff --git a/includes/Database.php b/includes/Database.php index f8e579b4..53e59968 100644 --- a/includes/Database.php +++ b/includes/Database.php @@ -5,13 +5,6 @@ * @package MediaWiki */ -/** See Database::makeList() */ -define( 'LIST_COMMA', 0 ); -define( 'LIST_AND', 1 ); -define( 'LIST_SET', 2 ); -define( 'LIST_NAMES', 3); -define( 'LIST_OR', 4); - /** Number of times to re-try an operation in case of deadlock */ define( 'DEADLOCK_TRIES', 4 ); /** Minimum time to wait before retry, in microseconds */ @@ -86,6 +79,11 @@ class DBConnectionError extends DBError { return $this->getMessage() . "\n"; } + function getLogMessage() { + # Don't send to the exception log + return false; + } + function getPageTitle() { global $wgSitename; return "$wgSitename has a problem"; @@ -205,6 +203,11 @@ class DBQueryError extends DBError { } } + function getLogMessage() { + # Don't send to the exception log + return false; + } + function getPageTitle() { return $this->msg( 'databaseerror', 'Database error' ); } @@ -244,6 +247,9 @@ class Database { protected $mTrxLevel = 0; protected $mErrorCount = 0; protected $mLBInfo = array(); + protected $mCascadingDeletes = false; + protected $mCleanupTriggers = false; + protected $mStrictIPs = false; #------------------------------------------------------------------------------ # Accessors @@ -334,6 +340,28 @@ class Database { } } + /** + * Returns true if this database supports (and uses) cascading deletes + */ + function cascadingDeletes() { + return $this->mCascadingDeletes; + } + + /** + * Returns true if this database supports (and uses) triggers (e.g. on the page table) + */ + function cleanupTriggers() { + return $this->mCleanupTriggers; + } + + /** + * Returns true if this database is strict about what can be put into an IP field. + * Specifically, it uses a NULL value instead of an empty string. + */ + function strictIPs() { + return $this->mStrictIPs; + } + /**#@+ * Get function */ @@ -433,6 +461,7 @@ class Database { */ function open( $server, $user, $password, $dbName ) { global $wguname; + wfProfileIn( __METHOD__ ); # Test for missing mysql.so # First try to load it @@ -454,12 +483,28 @@ class Database { $success = false; - if ( $this->mFlags & DBO_PERSISTENT ) { - @/**/$this->mConn = mysql_pconnect( $server, $user, $password ); - } else { - # Create a new connection... - @/**/$this->mConn = mysql_connect( $server, $user, $password, true ); + wfProfileIn("dbconnect-$server"); + + # LIVE PATCH by Tim, ask Domas for why: retry loop + $this->mConn = false; + $max = 3; + for ( $i = 0; $i < $max && !$this->mConn; $i++ ) { + if ( $i > 1 ) { + usleep( 1000 ); + } + if ( $this->mFlags & DBO_PERSISTENT ) { + @/**/$this->mConn = mysql_pconnect( $server, $user, $password ); + } else { + # Create a new connection... + @/**/$this->mConn = mysql_connect( $server, $user, $password, true ); + } + if ($this->mConn === false) { + $iplus = $i + 1; + wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); + } } + + wfProfileOut("dbconnect-$server"); if ( $dbName != '' ) { if ( $this->mConn !== false ) { @@ -467,6 +512,7 @@ class Database { if ( !$success ) { $error = "Error selecting database $dbName on server {$this->mServer} " . "from client host {$wguname['nodename']}\n"; + wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n"); wfDebug( $error ); } } else { @@ -480,18 +526,19 @@ class Database { $success = (bool)$this->mConn; } - if ( !$success ) { + if ( $success ) { + global $wgDBmysql5; + if( $wgDBmysql5 ) { + // Tell the server we're communicating with it in UTF-8. + // This may engage various charset conversions. + $this->query( 'SET NAMES utf8' ); + } + } else { $this->reportConnectionError(); } - global $wgDBmysql5; - if( $wgDBmysql5 ) { - // Tell the server we're communicating with it in UTF-8. - // This may engage various charset conversions. - $this->query( 'SET NAMES utf8' ); - } - $this->mOpened = $success; + wfProfileOut( __METHOD__ ); return $success; } /**@}}*/ @@ -760,8 +807,8 @@ class Database { */ function fetchObject( $res ) { @/**/$row = mysql_fetch_object( $res ); - if( mysql_errno() ) { - throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( mysql_error() ) ); + if( $this->lastErrno() ) { + throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) ); } return $row; } @@ -772,8 +819,8 @@ class Database { */ function fetchRow( $res ) { @/**/$row = mysql_fetch_array( $res ); - if (mysql_errno() ) { - throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( mysql_error() ) ); + if ( $this->lastErrno() ) { + throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) ); } return $row; } @@ -783,8 +830,8 @@ class Database { */ function numRows( $res ) { @/**/$n = mysql_num_rows( $res ); - if( mysql_errno() ) { - throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( mysql_error() ) ); + if( $this->lastErrno() ) { + throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) ); } return $n; } @@ -1865,7 +1912,7 @@ class Database { function sourceFile( $filename ) { $fp = fopen( $filename, 'r' ); if ( false === $fp ) { - return "Could not open \"{$fname}\".\n"; + return "Could not open \"{$filename}\".\n"; } $cmd = ""; diff --git a/includes/DatabaseOracle.php b/includes/DatabaseOracle.php index d5d7379d..aa1e329e 100644 --- a/includes/DatabaseOracle.php +++ b/includes/DatabaseOracle.php @@ -6,11 +6,6 @@ * @package MediaWiki */ -/** - * Depends on database - */ -require_once( 'Database.php' ); - class OracleBlob extends DBObject { function isLOB() { return true; diff --git a/includes/DatabasePostgres.php b/includes/DatabasePostgres.php index 5897386f..a5e02e77 100644 --- a/includes/DatabasePostgres.php +++ b/includes/DatabasePostgres.php @@ -1,7 +1,7 @@ <?php /** - * This is PostgreSQL database abstraction layer. + * This is Postgres database abstraction layer. * * As it includes more generic version for DB functions, * than MySQL ones, some of them should be moved to parent @@ -10,11 +10,6 @@ * @package MediaWiki */ -/** - * Depends on database - */ -require_once( 'Database.php' ); - class DatabasePostgres extends Database { var $mInsertId = NULL; var $mLastResult = NULL; @@ -30,8 +25,10 @@ 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); } @@ -47,11 +44,12 @@ class DatabasePostgres extends Database { * If the failFunction is set to a non-zero integer, returns success */ function open( $server, $user, $password, $dbName ) { - # Test for PostgreSQL support, to avoid suppressed fatal error + # Test for Postgres support, to avoid suppressed fatal error if ( !function_exists( 'pg_connect' ) ) { - throw new DBConnectionError( $this, "PostgreSQL functions missing, have you compiled PHP with the --with-pgsql option?\n" ); + 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; $this->close(); @@ -62,7 +60,6 @@ class DatabasePostgres extends Database { $this->mDBname = $dbName; $success = false; - $hstring=""; if ($server!=false && $server!="") { $hstring="host=$server "; @@ -71,8 +68,11 @@ class DatabasePostgres extends Database { $hstring .= "port=$port "; } - error_reporting( E_ALL ); + 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"); if ( $this->mConn == false ) { @@ -83,12 +83,153 @@ class DatabasePostgres extends Database { } $this->mOpened = true; - ## If this is the initial connection, setup the schema stuff - if (defined('MEDIAWIKI_INSTALL') and !defined('POSTGRES_SEARCHPATH')) { - global $wgDBmwschema, $wgDBts2schema, $wgDBname; + ## If this is the initial connection, setup the schema stuff and possibly create the user + if (defined('MEDIAWIKI_INSTALL')) { + global $wgDBname, $wgDBuser, $wgDBpass, $wgDBsuperuser, $wgDBmwschema, + $wgDBts2schema, $wgDBts2locale; + print "OK</li>\n"; + + print "<li>Checking the version of Postgres..."; + $version = pg_fetch_result($this->doQuery("SELECT version()"),0,0); + if (!preg_match("/PostgreSQL (\d+\.\d+)(\S+)/", $version, $thisver)) { + print "<b>FAILED</b> (could not determine the version)</li>\n"; + dieout("</ul>"); + } + $PGMINVER = "8.1"; + if ($thisver[1] < $PGMINVER) { + print "<b>FAILED</b>. Required version is $PGMINVER. You have $thisver[1]$thisver[2]</li>\n"; + dieout("</ul>"); + } + print "version $thisver[1]$thisver[2] is OK.</li>\n"; + + $safeuser = $this->quote_ident($wgDBuser); + ## Are we connecting as a superuser for the first time? + if ($wgDBsuperuser) { + ## Are we really a superuser? Check out our rights + $SQL = "SELECT + CASE WHEN usesuper IS TRUE THEN + CASE WHEN usecreatedb IS TRUE THEN 3 ELSE 1 END + ELSE CASE WHEN usecreatedb IS TRUE THEN 2 ELSE 0 END + END AS rights + FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBsuperuser); + $rows = $this->numRows($res = $this->doQuery($SQL)); + if (!$rows) { + print "<li>ERROR: Could not read permissions for user \"$wgDBsuperuser\"</li>\n"; + dieout('</ul>'); + } + $perms = pg_fetch_result($res, 0, 0); + + $SQL = "SELECT 1 FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBuser); + $rows = $this->numRows($this->doQuery($SQL)); + if ($rows) { + print "<li>User \"$wgDBuser\" already exists, skipping account creation.</li>"; + } + else { + if ($perms != 1 and $perms != 3) { + print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create other users. "; + print 'Please use a different Postgres user.</li>'; + dieout('</ul>'); + } + print "<li>Creating user <b>$wgDBuser</b>..."; + $safepass = $this->addQuotes($wgDBpass); + $SQL = "CREATE USER $safeuser NOCREATEDB PASSWORD $safepass"; + $this->doQuery($SQL); + print "OK</li>\n"; + } + ## User now exists, check out the database + if ($dbName != $wgDBname) { + $SQL = "SELECT 1 FROM pg_catalog.pg_database WHERE datname = " . $this->addQuotes($wgDBname); + $rows = $this->numRows($this->doQuery($SQL)); + if ($rows) { + print "<li>Database \"$wgDBname\" already exists, skipping database creation.</li>"; + } + else { + if ($perms < 2) { + print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create databases. "; + print 'Please use a different Postgres user.</li>'; + dieout('</ul>'); + } + print "<li>Creating database <b>$wgDBname</b>..."; + $safename = $this->quote_ident($wgDBname); + $SQL = "CREATE DATABASE $safename OWNER $safeuser "; + $this->doQuery($SQL); + print "OK</li>\n"; + ## Hopefully tsearch2 and plpgsql are in template1... + } + + ## Reconnect to check out tsearch2 rights for this user + print "<li>Connecting to \"$wgDBname\" as superuser \"$wgDBsuperuser\" to check rights..."; + @$this->mConn = pg_connect("$hstring dbname=$wgDBname user=$user password=$password"); + if ( $this->mConn == false ) { + print "<b>FAILED TO CONNECT!</b></li>"; + dieout("</ul>"); + } + print "OK</li>\n"; + } + + ## Tsearch2 checks + print "<li>Checking that tsearch2 is installed in the database \"$wgDBname\"..."; + if (! $this->tableExists("pg_ts_cfg", $wgDBts2schema)) { + print "<b>FAILED</b>. tsearch2 must be installed in the database \"$wgDBname\"."; + print "Please see <a href='http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>"; + print " for instructions or ask on #postgresql on irc.freenode.net</li>\n"; + dieout("</ul>"); + } + print "OK</li>\n"; + print "<li>Ensuring that user \"$wgDBuser\" has select rights on the tsearch2 tables..."; + foreach (array('cfg','cfgmap','dict','parser') as $table) { + $SQL = "GRANT SELECT ON pg_ts_$table TO $safeuser"; + $this->doQuery($SQL); + } + print "OK</li>\n"; + + + ## Setup the schema for this user if needed + $result = $this->schemaExists($wgDBmwschema); + $safeschema = $this->quote_ident($wgDBmwschema); + if (!$result) { + print "<li>Creating schema <b>$wgDBmwschema</b> ..."; + $result = $this->doQuery("CREATE SCHEMA $safeschema AUTHORIZATION $safeuser"); + if (!$result) { + print "<b>FAILED</b>.</li>\n"; + dieout("</ul>"); + } + print "OK</li>\n"; + } + else { + print "<li>Schema already exists, explicitly granting rights...\n"; + $safeschema2 = $this->addQuotes($wgDBmwschema); + $SQL = "SELECT 'GRANT ALL ON '||pg_catalog.quote_ident(relname)||' TO $safeuser;'\n". + "FROM pg_catalog.pg_class p, pg_catalog.pg_namespace n\n". + "WHERE relnamespace = n.oid AND n.nspname = $safeschema2\n". + "AND p.relkind IN ('r','S','v')\n"; + $SQL .= "UNION\n"; + $SQL .= "SELECT 'GRANT ALL ON FUNCTION '||pg_catalog.quote_ident(proname)||'('||\n". + "pg_catalog.oidvectortypes(p.proargtypes)||') TO $safeuser;'\n". + "FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n\n". + "WHERE p.pronamespace = n.oid AND n.nspname = $safeschema2"; + $res = $this->doQuery($SQL); + if (!$res) { + print "<b>FAILED</b>. Could not set rights for the user.</li>\n"; + dieout("</ul>"); + } + $this->doQuery("SET search_path = $safeschema"); + $rows = $this->numRows($res); + while ($rows) { + $rows--; + $this->doQuery(pg_fetch_result($res, $rows, 0)); + } + print "OK</li>"; + } + + $wgDBsuperuser = ''; + return true; ## Reconnect as regular user + } + + if (!defined('POSTGRES_SEARCHPATH')) { ## Do we have the basic tsearch2 table? - print "<li>Checking for tsearch2 ..."; + print "<li>Checking for tsearch2 in the schema \"$wgDBts2schema\"..."; if (! $this->tableExists("pg_ts_dict", $wgDBts2schema)) { print "<b>FAILED</b>. Make sure tsearch2 is installed. See <a href="; print "'http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>"; @@ -97,15 +238,78 @@ class DatabasePostgres extends Database { } print "OK</li>\n"; + ## 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..."; + $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"; + dieout("</ul>"); + } + print "OK</li>"; + + ## Will the current locale work? Can we force it to? + print "<li>Verifying tsearch2 locale with $ctype..."; + $rows = $this->numRows($res); + $resetlocale = 0; + if (!$rows) { + print "<b>not found</b></li>\n"; + print "<li>Attempting to set default tsearch2 locale to \"$ctype\"..."; + $resetlocale = 1; + } + else { + $tsname = pg_fetch_result($res, 0, 0); + if ($tsname != 'default') { + print "<b>not set to default ($tsname)</b>"; + print "<li>Attempting to change tsearch2 default locale to \"$ctype\"..."; + $resetlocale = 1; + } + } + if ($resetlocale) { + $SQL = "UPDATE $wgDBts2schema.pg_ts_cfg SET locale = '$ctype' WHERE ts_name = 'default'"; + $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"; + dieout("</ul>"); + } + print "OK</li>"; + } + + ## Final test: try out a simple tsearch2 query + $SQL = "SELECT $wgDBts2schema.to_tsvector('default','MediaWiki tsearch2 testing')"; + $res = $this->doQuery($SQL); + if (!$res) { + print "<b>FAILED</b>. Specifically, \"$SQL\" did not work.</li>"; + dieout("</ul>"); + } + print "OK</li>"; + ## Do we have plpgsql installed? - print "<li>Checking for plpgsql ..."; + print "<li>Checking for Pl/Pgsql ..."; $SQL = "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'"; - $res = $this->doQuery($SQL); $rows = $this->numRows($this->doQuery($SQL)); if ($rows < 1) { - print "<b>FAILED</b>. Make sure the language plpgsql is installed for the database <tt>$wgDBname</tt>t</li>"; - ## XXX Better help - dieout("</ul>"); + // plpgsql is not installed, but if we have a pg_pltemplate table, we should be able to create it + print "not installed. Attempting to install Pl/Pgsql ..."; + $SQL = "SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) ". + "WHERE relname = 'pg_pltemplate' AND nspname='pg_catalog'"; + $rows = $this->numRows($this->doQuery($SQL)); + if ($rows >= 1) { + $result = $this->doQuery("CREATE LANGUAGE plpgsql"); + if (!$result) { + print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>"; + dieout("</ul>"); + } + } + else { + print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>"; + dieout("</ul>"); + } } print "OK</li>\n"; @@ -115,40 +319,46 @@ class DatabasePostgres extends Database { print "<li>Creating schema <b>$wgDBmwschema</b> ..."; $result = $this->doQuery("CREATE SCHEMA $wgDBmwschema"); if (!$result) { - print "FAILED.</li>\n"; - return false; + print "<b>FAILED</b>.</li>\n"; + dieout("</ul>"); } - print "ok</li>\n"; + print "OK</li>\n"; } else if ($result != $user) { - print "<li>Schema <b>$wgDBmwschema</b> exists but is not owned by <b>$user</b>. Not ideal.</li>\n"; + print "<li>Schema \"$wgDBmwschema\" exists but is not owned by \"$user\". Not ideal.</li>\n"; } else { - print "<li>Schema <b>$wgDBmwschema</b> exists and is owned by <b>$user ($result)</b>. Excellent.</li>\n"; + print "<li>Schema \"$wgDBmwschema\" exists and is owned by \"$user\". Excellent.</li>\n"; } ## Fix up the search paths if needed - print "<li>Setting the search path for user <b>$user</b> ..."; - $path = "$wgDBmwschema"; + print "<li>Setting the search path for user \"$user\" ..."; + $path = $this->quote_ident($wgDBmwschema); if ($wgDBts2schema !== $wgDBmwschema) - $path .= ", $wgDBts2schema"; + $path .= ", ". $this->quote_ident($wgDBts2schema); if ($wgDBmwschema !== 'public' and $wgDBts2schema !== 'public') $path .= ", public"; - $SQL = "ALTER USER $user SET search_path = $path"; + $SQL = "ALTER USER $safeuser SET search_path = $path"; $result = pg_query($this->mConn, $SQL); if (!$result) { - print "FAILED.</li>\n"; - return false; + print "<b>FAILED</b>.</li>\n"; + dieout("</ul>"); } - print "ok</li>\n"; + print "OK</li>\n"; ## Set for the rest of this session $SQL = "SET search_path = $path"; $result = pg_query($this->mConn, $SQL); if (!$result) { print "<li>Failed to set search_path</li>\n"; - return false; + dieout("</ul>"); } define( "POSTGRES_SEARCHPATH", $path ); + }} + + global $wgCommandLineMode; + ## If called from the command-line (e.g. importDump), only show errors + if ($wgCommandLineMode) { + $this->doQuery("SET client_min_messages = 'ERROR'"); } return $this->mConn; @@ -177,7 +387,7 @@ class DatabasePostgres extends Database { function freeResult( $res ) { if ( !@pg_free_result( $res ) ) { - throw new DBUnexpectedError($this, "Unable to free PostgreSQL result\n" ); + throw new DBUnexpectedError($this, "Unable to free Postgres result\n" ); } } @@ -228,7 +438,9 @@ class DatabasePostgres extends Database { return "No database connection"; } } - function lastErrno() { return 1; } + function lastErrno() { + return pg_last_error() ? 1 : 0; + } function affectedRows() { return pg_affected_rows( $this->mLastResult ); @@ -266,7 +478,7 @@ class DatabasePostgres extends Database { } function insert( $table, $a, $fname = 'Database::insert', $options = array() ) { - # PostgreSQL doesn't support options + # Postgres doesn't support options # We have a go at faking one of them # TODO: DELAYED, LOW_PRIORITY @@ -295,16 +507,12 @@ class DatabasePostgres extends Database { } function tableName( $name ) { - # Replace backticks into double quotes - $name = strtr($name,'`','"'); - - # Now quote PG reserved keywords + # Replace reserved words with better ones switch( $name ) { case 'user': - case 'old': - case 'group': - return '"' . $name . '"'; - + return 'mwuser'; + case 'text': + return 'pagecontent'; default: return $name; } @@ -323,15 +531,14 @@ class DatabasePostgres extends Database { } /** - * USE INDEX clause - * PostgreSQL doesn't have them and returns "" + * Postgres does not have a "USE INDEX" clause, so return an empty string */ function useIndexClause( $index ) { return ''; } # REPLACE query wrapper - # PostgreSQL simulates this with a DELETE followed by INSERT + # Postgres 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 @@ -433,7 +640,7 @@ class DatabasePostgres extends Database { /** * Returns an SQL expression for a simple conditional. - * Uses CASE on PostgreSQL. + * Uses CASE on Postgres * * @param string $cond SQL expression which will result in a boolean value * @param string $trueVal SQL expression to return if true @@ -449,9 +656,8 @@ class DatabasePostgres extends Database { return false; } - # Return DB-style timestamp used for MySQL schema function timestamp( $ts=0 ) { - return wfTimestamp(TS_DB,$ts); + return wfTimestamp(TS_POSTGRES,$ts); } /** @@ -499,7 +705,8 @@ class DatabasePostgres extends Database { $etable = preg_replace("/'/", "''", $table); $eschema = preg_replace("/'/", "''", $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'"; + . "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema' " + . "AND c.relkind IN ('r','v')"; $res = $this->query( $SQL ); $count = $res ? pg_num_rows($res) : 0; if ($res) @@ -563,9 +770,28 @@ class DatabasePostgres extends Database { return $sql; } - function update_interwiki() { + function setup_database() { + global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport; + + dbsource( "../maintenance/postgres/tables.sql", $this); + + ## Update version information + $mwv = $this->addQuotes($wgVersion); + $pgv = $this->addQuotes($this->getServerVersion()); + $pgu = $this->addQuotes($this->mUser); + $mws = $this->addQuotes($wgDBmwschema); + $tss = $this->addQuotes($wgDBts2schema); + $pgp = $this->addQuotes($wgDBport); + $dbn = $this->addQuotes($this->mDBname); + $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0); + + $SQL = "UPDATE mediawiki_version SET mw_version=$mwv, pg_version=$pgv, pg_user=$pgu, ". + "mw_schema = $mws, ts2_schema = $tss, pg_port=$pgp, pg_dbname=$dbn, ". + "ctype = '$ctype' ". + "WHERE type = 'Creation'"; + $this->query($SQL); + ## Avoid the non-standard "REPLACE INTO" syntax - ## Called by config/index.php $f = fopen( "../maintenance/interwiki.sql", 'r' ); if ($f == false ) { dieout( "<li>Could not find the interwiki.sql file"); @@ -604,6 +830,10 @@ class DatabasePostgres extends Database { return "E'" . pg_escape_string($s) . "'"; } + function quote_ident( $s ) { + return '"' . preg_replace( '/"/', '""', $s) . '"'; + } + } ?> diff --git a/includes/DateFormatter.php b/includes/DateFormatter.php index 02acac73..dc077fdc 100644 --- a/includes/DateFormatter.php +++ b/includes/DateFormatter.php @@ -1,25 +1,11 @@ <?php /** - * Contain things - * @todo document + * Date formatter, recognises dates in plain text and formats them accoding to user preferences. + * * @package MediaWiki * @subpackage Parser */ -/** */ -define('DF_ALL', -1); -define('DF_NONE', 0); -define('DF_MDY', 1); -define('DF_DMY', 2); -define('DF_YMD', 3); -define('DF_ISO1', 4); -define('DF_LASTPREF', 4); -define('DF_ISO2', 5); -define('DF_YDM', 6); -define('DF_DM', 7); -define('DF_MD', 8); -define('DF_LAST', 8); - /** * @todo preferences, OutputPage * @package MediaWiki @@ -31,7 +17,20 @@ class DateFormatter var $monthNames = '', $rxDM, $rxMD, $rxDMY, $rxYDM, $rxMDY, $rxYMD; var $regexes, $pDays, $pMonths, $pYears; - var $rules, $xMonths; + var $rules, $xMonths, $preferences; + + const ALL = -1; + const NONE = 0; + const MDY = 1; + const DMY = 2; + const YMD = 3; + const ISO1 = 4; + const LASTPREF = 4; + const ISO2 = 5; + const YDM = 6; + const DM = 7; + const MD = 8; + const LAST = 8; /** * @todo document @@ -55,75 +54,87 @@ class DateFormatter $this->prxISO2 = '\[\[(-?\d{4})-(\d{2})-(\d{2})]]'; # Real regular expressions - $this->regexes[DF_DMY] = "/{$this->prxDM} *,? *{$this->prxY}{$this->regexTrail}"; - $this->regexes[DF_YDM] = "/{$this->prxY} *,? *{$this->prxDM}{$this->regexTrail}"; - $this->regexes[DF_MDY] = "/{$this->prxMD} *,? *{$this->prxY}{$this->regexTrail}"; - $this->regexes[DF_YMD] = "/{$this->prxY} *,? *{$this->prxMD}{$this->regexTrail}"; - $this->regexes[DF_DM] = "/{$this->prxDM}{$this->regexTrail}"; - $this->regexes[DF_MD] = "/{$this->prxMD}{$this->regexTrail}"; - $this->regexes[DF_ISO1] = "/{$this->prxISO1}{$this->regexTrail}"; - $this->regexes[DF_ISO2] = "/{$this->prxISO2}{$this->regexTrail}"; + $this->regexes[self::DMY] = "/{$this->prxDM} *,? *{$this->prxY}{$this->regexTrail}"; + $this->regexes[self::YDM] = "/{$this->prxY} *,? *{$this->prxDM}{$this->regexTrail}"; + $this->regexes[self::MDY] = "/{$this->prxMD} *,? *{$this->prxY}{$this->regexTrail}"; + $this->regexes[self::YMD] = "/{$this->prxY} *,? *{$this->prxMD}{$this->regexTrail}"; + $this->regexes[self::DM] = "/{$this->prxDM}{$this->regexTrail}"; + $this->regexes[self::MD] = "/{$this->prxMD}{$this->regexTrail}"; + $this->regexes[self::ISO1] = "/{$this->prxISO1}{$this->regexTrail}"; + $this->regexes[self::ISO2] = "/{$this->prxISO2}{$this->regexTrail}"; # Extraction keys # See the comments in replace() for the meaning of the letters - $this->keys[DF_DMY] = 'jFY'; - $this->keys[DF_YDM] = 'Y jF'; - $this->keys[DF_MDY] = 'FjY'; - $this->keys[DF_YMD] = 'Y Fj'; - $this->keys[DF_DM] = 'jF'; - $this->keys[DF_MD] = 'Fj'; - $this->keys[DF_ISO1] = 'ymd'; # y means ISO year - $this->keys[DF_ISO2] = 'ymd'; + $this->keys[self::DMY] = 'jFY'; + $this->keys[self::YDM] = 'Y jF'; + $this->keys[self::MDY] = 'FjY'; + $this->keys[self::YMD] = 'Y Fj'; + $this->keys[self::DM] = 'jF'; + $this->keys[self::MD] = 'Fj'; + $this->keys[self::ISO1] = 'ymd'; # y means ISO year + $this->keys[self::ISO2] = 'ymd'; # Target date formats - $this->targets[DF_DMY] = '[[F j|j F]] [[Y]]'; - $this->targets[DF_YDM] = '[[Y]], [[F j|j F]]'; - $this->targets[DF_MDY] = '[[F j]], [[Y]]'; - $this->targets[DF_YMD] = '[[Y]] [[F j]]'; - $this->targets[DF_DM] = '[[F j|j F]]'; - $this->targets[DF_MD] = '[[F j]]'; - $this->targets[DF_ISO1] = '[[Y|y]]-[[F j|m-d]]'; - $this->targets[DF_ISO2] = '[[y-m-d]]'; + $this->targets[self::DMY] = '[[F j|j F]] [[Y]]'; + $this->targets[self::YDM] = '[[Y]], [[F j|j F]]'; + $this->targets[self::MDY] = '[[F j]], [[Y]]'; + $this->targets[self::YMD] = '[[Y]] [[F j]]'; + $this->targets[self::DM] = '[[F j|j F]]'; + $this->targets[self::MD] = '[[F j]]'; + $this->targets[self::ISO1] = '[[Y|y]]-[[F j|m-d]]'; + $this->targets[self::ISO2] = '[[y-m-d]]'; # Rules # pref source target - $this->rules[DF_DMY][DF_MD] = DF_DM; - $this->rules[DF_ALL][DF_MD] = DF_MD; - $this->rules[DF_MDY][DF_DM] = DF_MD; - $this->rules[DF_ALL][DF_DM] = DF_DM; - $this->rules[DF_NONE][DF_ISO2] = DF_ISO1; + $this->rules[self::DMY][self::MD] = self::DM; + $this->rules[self::ALL][self::MD] = self::MD; + $this->rules[self::MDY][self::DM] = self::MD; + $this->rules[self::ALL][self::DM] = self::DM; + $this->rules[self::NONE][self::ISO2] = self::ISO1; + + $this->preferences = array( + 'default' => self::NONE, + 'dmy' => self::DMY, + 'mdy' => self::MDY, + 'ymd' => self::YMD, + 'ISO 8601' => self::ISO1, + ); } /** * @static */ function &getInstance() { - global $wgDBname, $wgMemc; + global $wgMemc; static $dateFormatter = false; if ( !$dateFormatter ) { - $dateFormatter = $wgMemc->get( "$wgDBname:dateformatter" ); + $dateFormatter = $wgMemc->get( wfMemcKey( 'dateformatter' ) ); if ( !$dateFormatter ) { $dateFormatter = new DateFormatter; - $wgMemc->set( "$wgDBname:dateformatter", $dateFormatter, 3600 ); + $wgMemc->set( wfMemcKey( 'dateformatter' ), $dateFormatter, 3600 ); } } return $dateFormatter; } /** - * @param $preference - * @param $text + * @param string $preference User preference + * @param string $text Text to reformat */ function reformat( $preference, $text ) { - if ($preference == 'ISO 8601') $preference = 4; # The ISO 8601 option used to be 4 - for ( $i=1; $i<=DF_LAST; $i++ ) { + if ( isset( $this->preferences[$preference] ) ) { + $preference = $this->preferences[$preference]; + } else { + $preference = self::NONE; + } + for ( $i=1; $i<=self::LAST; $i++ ) { $this->mSource = $i; if ( @$this->rules[$preference][$i] ) { # Specific rules $this->mTarget = $this->rules[$preference][$i]; - } elseif ( @$this->rules[DF_ALL][$i] ) { + } elseif ( @$this->rules[self::ALL][$i] ) { # General rules - $this->mTarget = $this->rules[DF_ALL][$i]; + $this->mTarget = $this->rules[self::ALL][$i]; } elseif ( $preference ) { # User preference $this->mTarget = $preference; @@ -131,7 +142,7 @@ class DateFormatter # Default $this->mTarget = $i; } - $text = preg_replace_callback( $this->regexes[$i], 'wfMainDateReplace', $text ); + $text = preg_replace_callback( $this->regexes[$i], array( &$this, 'replace' ), $text ); } return $text; } @@ -277,12 +288,4 @@ class DateFormatter } } -/** - * @todo document - */ -function wfMainDateReplace( $matches ) { - $df =& DateFormatter::getInstance(); - return $df->replace( $matches ); -} - ?> diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 1964aaf2..e4ce8e5e 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -32,13 +32,22 @@ require_once( 'includes/SiteConfiguration.php' ); $wgConf = new SiteConfiguration; /** MediaWiki version number */ -$wgVersion = '1.7.1'; +$wgVersion = '1.8.1'; /** Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; -/** Will be same as you set @see $wgSitename */ -$wgMetaNamespace = FALSE; +/** + * Name of the project namespace. If left set to false, $wgSitename will be + * used instead. + */ +$wgMetaNamespace = false; + +/** + * Name of the project talk namespace. If left set to false, a name derived + * from the name of the project namespace will be used. + */ +$wgMetaNamespaceTalk = false; /** URL of the server. It will be automatically built including https mode */ @@ -115,8 +124,8 @@ $wgStylePath = "{$wgScriptPath}/skins"; $wgStyleDirectory = "{$IP}/skins"; $wgStyleSheetPath = &$wgStylePath; $wgArticlePath = "{$wgScript}?title=$1"; -$wgUploadPath = "{$wgScriptPath}/upload"; -$wgUploadDirectory = "{$IP}/upload"; +$wgUploadPath = "{$wgScriptPath}/images"; +$wgUploadDirectory = "{$IP}/images"; $wgHashedUploadDirectory = true; $wgLogo = "{$wgUploadPath}/wiki.png"; $wgFavicon = '/favicon.ico'; @@ -156,7 +165,7 @@ $wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split * Problematic punctuation: * []{}|# Are needed for link syntax, never enable these * % Enabled by default, minor problems with path to query rewrite rules, see below - * + Doesn't work with path to query rewrite rules, corrupted by apache + * + 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 * * All three of these punctuation problems can be avoided by using an alias, instead of a @@ -169,10 +178,12 @@ $wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split * passed in the query string rather than the path. This is a minor security issue * because articles can be created such that they are hard to view or edit. * + * In some rare cases you may wish to remove + for compatibility with old links. + * * Theoretically 0x80-0x9F of ISO 8859-1 should be disallowed, but * this breaks interlanguage links */ -$wgLegalTitleChars = " %!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF"; +$wgLegalTitleChars = " %!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+"; /** @@ -328,6 +339,10 @@ $wgSharedUploadDBname = false; $wgSharedUploadDBprefix = ''; /** Cache shared metadata in memcached. Don't do this if the commons wiki is in a different memcached domain */ $wgCacheSharedUploads = true; +/** Allow for upload to be copied from an URL. Requires Special:Upload?source=web */ +$wgAllowCopyUploads = false; +/** Max size for uploads, in bytes */ +$wgMaxUploadSize = 1024*1024*100; # 100MB /** * Point the upload navigation link to an external URL @@ -440,7 +455,6 @@ $wgDBconnection = ''; /** Database username */ $wgDBuser = 'wikiuser'; /** Database type - * "mysql" for working code and "PostgreSQL" for development/broken code */ $wgDBtype = "mysql"; /** Search type @@ -472,7 +486,7 @@ $wgSharedDB = null; # dbname: Default database name # user: DB user # password: DB password -# type: "mysql" or "pgsql" +# type: "mysql" or "postgres" # load: ratio of DB_SLAVE load, must be >=0, the sum of all loads must be >0 # groupLoads: array of load ratios, the key is the query group name. A query may belong # to several groups, the most specific group defined here is used. @@ -776,6 +790,14 @@ $wgShowSQLErrors = false; $wgColorErrors = true; /** + * If set to true, uncaught exceptions will print a complete stack trace + * to output. This should only be used for debugging, as it may reveal + * private information in function parameters due to PHP's backtrace + * formatting. + */ +$wgShowExceptionDetails = false; + +/** * disable experimental dmoz-like category browsing. Output things like: * Encyclopedia > Music > Style of Music > Jazz */ @@ -895,7 +917,7 @@ $wgGroupPermissions['bot' ]['autoconfirmed'] = true; $wgGroupPermissions['sysop']['block'] = true; $wgGroupPermissions['sysop']['createaccount'] = true; $wgGroupPermissions['sysop']['delete'] = true; -$wgGroupPermissions['sysop']['deletedhistory'] = true; // can view deleted history entries, but not see or restore the text +$wgGroupPermissions['sysop']['deletedhistory'] = true; // can view deleted history entries, but not see or restore the text $wgGroupPermissions['sysop']['editinterface'] = true; $wgGroupPermissions['sysop']['import'] = true; $wgGroupPermissions['sysop']['importupload'] = true; @@ -908,8 +930,9 @@ $wgGroupPermissions['sysop']['trackback'] = true; $wgGroupPermissions['sysop']['upload'] = true; $wgGroupPermissions['sysop']['reupload'] = true; $wgGroupPermissions['sysop']['reupload-shared'] = true; -$wgGroupPermissions['sysop']['unwatchedpages'] = true; +$wgGroupPermissions['sysop']['unwatchedpages'] = true; $wgGroupPermissions['sysop']['autoconfirmed'] = true; +$wgGroupPermissions['sysop']['upload_by_url'] = true; // Permission to change users' group assignments $wgGroupPermissions['bureaucrat']['userrights'] = true; @@ -1022,6 +1045,9 @@ $wgFileCacheDirectory = "{$wgUploadDirectory}/cache"; */ $wgUseGzip = false; +/** Whether MediaWiki should send an ETag header */ +$wgUseETag = false; + # Email notification settings # @@ -1153,21 +1179,17 @@ $wgTexvc = './math/texvc'; # # You have to create a 'profiling' table in your database before using # profiling see maintenance/archives/patch-profiling.sql . +# +# To enable profiling, edit StartProfiler.php -/** Enable for more detailed by-function times in debug log */ -$wgProfiling = false; /** Only record profiling info for pages that took longer than this */ $wgProfileLimit = 0.0; /** Don't put non-profiling info into log file */ $wgProfileOnly = false; /** Log sums from profiling into "profiling" table in db. */ $wgProfileToDatabase = false; -/** Only profile every n requests when profiling is turned on */ -$wgProfileSampleRate = 1; /** If true, print a raw call tree instead of per-function report */ $wgProfileCallTree = false; -/** If not empty, specifies profiler type to load */ -$wgProfilerType = ''; /** Should application server host be put into profiling table */ $wgProfilePerHost = false; @@ -1251,7 +1273,7 @@ $wgFileBlacklist = array( # HTML may contain cookie-stealing JavaScript and web bugs 'html', 'htm', 'js', 'jsb', # PHP scripts may execute arbitrary code on the server - 'php', 'phtml', 'php3', 'php4', 'phps', + 'php', 'phtml', 'php3', 'php4', 'php5', 'phps', # Other types that may be interpreted by some servers 'shtml', 'jhtml', 'pl', 'py', 'cgi', # May contain harmful executables for Windows victims @@ -1280,7 +1302,7 @@ $wgCheckFileExtensions = true; */ $wgStrictFileExtensions = true; -/** Warn if uploaded files are larger than this */ +/** Warn if uploaded files are larger than this (in bytes)*/ $wgUploadSizeWarning = 150 * 1024; /** For compatibility with old installations set to false */ @@ -1551,21 +1573,53 @@ $wgTidyInternal = function_exists( 'tidy_load_config' ); $wgDefaultSkin = 'monobook'; /** - * Settings added to this array will override the language globals for the user - * preferences used by anonymous visitors and newly created accounts. (See names - * and sample values in languages/Language.php) + * 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 = array(); + * Â $wgDefaultUserOptions ['editsection'] = 0; + * + */ +$wgDefaultUserOptions = array( + 'quickbar' => 1, + 'underline' => 2, + 'cols' => 80, + 'rows' => 25, + 'searchlimit' => 20, + 'contextlines' => 5, + 'contextchars' => 50, + 'skin' => false, + 'math' => 1, + 'rcdays' => 7, + 'rclimit' => 50, + 'wllimit' => 250, + 'highlightbroken' => 1, + 'stubthreshold' => 0, + 'previewontop' => 1, + 'editsection' => 1, + 'editsectiononrightclick'=> 0, + 'showtoc' => 1, + 'showtoolbar' => 1, + 'date' => 'default', + 'imagesize' => 2, + 'thumbsize' => 2, + 'rememberpassword' => 0, + 'enotifwatchlistpages' => 0, + 'enotifusertalkpages' => 1, + 'enotifminoredits' => 0, + 'enotifrevealaddr' => 0, + 'shownumberswatching' => 1, + 'fancysig' => 0, + 'externaleditor' => 0, + 'externaldiff' => 0, + 'showjumplinks' => 1, + 'numberheadings' => 0, + 'uselivepreview' => 0, + 'watchlistdays' => 3.0, +); /** Whether or not to allow and use real name fields. Defaults to true. */ $wgAllowRealName = true; -/** Use XML parser? */ -$wgUseXMLparser = false ; - /***************************************************************************** * Extensions */ @@ -2072,6 +2126,14 @@ $wgExternalServers = array(); $wgDefaultExternalStore = false; /** + * Revision text may be cached in $wgMemc to reduce load on external storage + * servers and object extraction overhead for frequently-loaded revisions. + * + * Set to 0 to disable, or number of seconds before cache expiry. + */ +$wgRevisionCacheExpiry = 0; + +/** * list of trusted media-types and mime types. * Use the MEDIATYPE_xxx constants to represent media types. * This list is used by Image::isSafeFile @@ -2144,14 +2206,22 @@ $wgUpdateRowsPerJob = 500; $wgUpdateRowsPerQuery = 10; /** - * Enable use of AJAX features, currently auto suggestion for the search bar + * Enable AJAX framework */ $wgUseAjax = false; /** - * List of Ajax-callable functions + * Enable auto suggestion for the search bar + * Requires $wgUseAjax to be true too. + * Causes wfSajaxSearch to be added to $wgAjaxExportList */ -$wgAjaxExportList = array( 'wfSajaxSearch' ); +$wgAjaxSearch = false; + +/** + * List of Ajax-callable functions. + * Extensions acting as Ajax callbacks must register here + */ +$wgAjaxExportList = array( ); /** * Allow DISPLAYTITLE to change title display @@ -2186,4 +2256,39 @@ $wgContentNamespaces = array( NS_MAIN ); */ $wgMaxShellMemory = 102400; +/** + * Maximum file size created by shell processes under linux, in KB + * ImageMagick convert for example can be fairly hungry for scratch space + */ +$wgMaxShellFileSize = 102400; + +/** + * DJVU settings + * Path of the djvutoxml executable + * Enable this and $wgDjvuRenderer to enable djvu rendering + */ +# $wgDjvuToXML = 'djvutoxml'; +$wgDjvuToXML = null; + +/** + * Path of the ddjvu DJVU renderer + * Enable this and $wgDjvuToXML 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 + */ +$wgDjvuPostProcessor = 'ppmtojpeg'; + +/** +* Enable direct access to the data API +* through api.php +*/ +$wgEnableAPI = false; +$wgEnableWriteAPI = false; + ?> diff --git a/includes/Defines.php b/includes/Defines.php index 9ff8303b..40727485 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -20,6 +20,17 @@ define( 'DBO_DEFAULT', 16 ); define( 'DBO_PERSISTENT', 32 ); /**#@-*/ +# Valid database indexes +# Operation-based indexes +define( 'DB_SLAVE', -1 ); # Read from the slave (or only server) +define( 'DB_MASTER', -2 ); # Write to master (or only server) +define( 'DB_LAST', -3 ); # Whatever database was used last + +# Obsolete aliases +define( 'DB_READ', -1 ); +define( 'DB_WRITE', -2 ); + + /**#@+ * Virtual namespaces; don't appear in the page database */ @@ -75,10 +86,8 @@ define( 'MW_MATH_MATHML', 5 ); /**#@-*/ /** - * User rights management - * a big array of string defining a right, that's how they are saved in the - * database. - * @todo Is this necessary? + * User rights list + * @deprecated */ $wgAvailableRights = array( 'block', @@ -108,6 +117,7 @@ define( 'CACHE_NONE', 0 ); // Do not cache define( 'CACHE_DB', 1 ); // Store cache objects in the DB define( 'CACHE_MEMCACHED', 2 ); // MemCached, must specify servers in $wgMemCacheServers define( 'CACHE_ACCEL', 3 ); // eAccelerator or Turck, whichever is available +define( 'CACHE_DBA', 4 ); // Use PHP's DBA extension to store in a DBM-style database /**#@-*/ @@ -151,10 +161,15 @@ define( 'ALF_NO_BLOCK_LOCK', 8 ); * Date format selectors; used in user preference storage and by * Language::date() and co. */ -define( 'MW_DATE_DEFAULT', '0' ); +/*define( 'MW_DATE_DEFAULT', '0' ); define( 'MW_DATE_MDY', '1' ); define( 'MW_DATE_DMY', '2' ); define( 'MW_DATE_YMD', '3' ); +define( 'MW_DATE_ISO', 'ISO 8601' );*/ +define( 'MW_DATE_DEFAULT', 'default' ); +define( 'MW_DATE_MDY', 'mdy' ); +define( 'MW_DATE_DMY', 'dmy' ); +define( 'MW_DATE_YMD', 'ymd' ); define( 'MW_DATE_ISO', 'ISO 8601' ); /**#@-*/ @@ -180,4 +195,15 @@ define( 'EDIT_FORCE_BOT', 16 ); define( 'EDIT_DEFER_UPDATES', 32 ); /**#@-*/ +/** + * Flags for Database::makeList() + * These are also available as Database class constants + */ +define( 'LIST_COMMA', 0 ); +define( 'LIST_AND', 1 ); +define( 'LIST_SET', 2 ); +define( 'LIST_NAMES', 3); +define( 'LIST_OR', 4); + + ?> diff --git a/includes/DifferenceEngine.php b/includes/DifferenceEngine.php index 741b7199..448bcb5d 100644 --- a/includes/DifferenceEngine.php +++ b/includes/DifferenceEngine.php @@ -5,10 +5,6 @@ * @subpackage DifferenceEngine */ -/** */ -define( 'MAX_DIFF_LINE', 10000 ); -define( 'MAX_DIFF_XREF_LENGTH', 10000 ); - /** * @todo document * @public @@ -188,7 +184,7 @@ CONTROL; $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" ); if( !$this->mNewRev->isCurrent() ) { - $oldEditSectionSetting = $wgOut->mParserOptions->setEditSection( false ); + $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false ); } $this->loadNewText(); @@ -198,7 +194,7 @@ CONTROL; $wgOut->addSecondaryWikiText( $this->mNewtext ); if( !$this->mNewRev->isCurrent() ) { - $wgOut->mParserOptions->setEditSection( $oldEditSectionSetting ); + $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); } wfProfileOut( $fname ); @@ -301,7 +297,7 @@ CONTROL; * Returns false on error */ function getDiffBody() { - global $wgMemc, $wgDBname; + global $wgMemc; $fname = 'DifferenceEngine::getDiffBody'; wfProfileIn( $fname ); @@ -309,7 +305,7 @@ CONTROL; $key = false; if ( $this->mOldid && $this->mNewid ) { // Try cache - $key = "$wgDBname:diff:oldid:{$this->mOldid}:newid:{$this->mNewid}"; + $key = wfMemcKey( 'diff', 'oldid', $this->mOldid, 'newid', $this->mNewid ); $difftext = $wgMemc->get( $key ); if ( $difftext ) { wfIncrStats( 'diff_cache_hit' ); @@ -411,8 +407,8 @@ CONTROL; # Native PHP diff $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); - $diffs =& new Diff( $ota, $nta ); - $formatter =& new TableDiffFormatter(); + $diffs = new Diff( $ota, $nta ); + $formatter = new TableDiffFormatter(); return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ); } @@ -724,6 +720,8 @@ class _DiffOp_Change extends _DiffOp { */ class _DiffEngine { + const MAX_XREF_LENGTH = 10000; + function diff ($from_lines, $to_lines) { $fname = '_DiffEngine::diff'; wfProfileIn( $fname ); @@ -821,7 +819,7 @@ class _DiffEngine * Returns the whole line if it's small enough, or the MD5 hash otherwise */ function _line_hash( $line ) { - if ( strlen( $line ) > MAX_DIFF_XREF_LENGTH ) { + if ( strlen( $line ) > self::MAX_XREF_LENGTH ) { return md5( $line ); } else { return $line; @@ -1576,6 +1574,8 @@ class _HWLDF_WordAccumulator { */ class WordLevelDiff extends MappedDiff { + const MAX_LINE_LENGTH = 10000; + function WordLevelDiff ($orig_lines, $closing_lines) { $fname = 'WordLevelDiff::WordLevelDiff'; wfProfileIn( $fname ); @@ -1604,7 +1604,7 @@ class WordLevelDiff extends MappedDiff $words[] = "\n"; $stripped[] = "\n"; } - if ( strlen( $line ) > MAX_DIFF_LINE ) { + if ( strlen( $line ) > self::MAX_LINE_LENGTH ) { $words[] = $line; $stripped[] = $line; } else { diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php index b857fa66..871c563b 100644 --- a/includes/DjVuImage.php +++ b/includes/DjVuImage.php @@ -208,7 +208,23 @@ class DjVuImage { 'resolution' => $resolution, 'gamma' => $gamma / 10.0 ); } + + /** + * Return an XML string describing the DjVu image + * @return string + */ + function retrieveMetaData() { + global $wgDjvuToXML; + if ( isset( $wgDjvuToXML ) ) { + $cmd = $wgDjvuToXML . ' --without-anno --without-text ' . $this->mFilename; + $xml = wfShellExec( $cmd, $retval ); + } else { + $xml = null; + } + return $xml; + } + } -?>
\ No newline at end of file +?> diff --git a/includes/EditPage.php b/includes/EditPage.php index d43a1202..a1207d10 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -845,7 +845,7 @@ class EditPage { $s = wfMsg('editingcomment', $this->mTitle->getPrefixedText() ); } else { $s = wfMsg('editingsection', $this->mTitle->getPrefixedText() ); - if( !$this->preview && !$this->diff ) { + if( !$this->summary && !$this->preview && !$this->diff ) { preg_match( "/^(=+)(.+)\\1/mi", $this->textbox1, $matches ); @@ -942,7 +942,7 @@ class EditPage { $cancel = $sk->makeKnownLink( $this->mTitle->getPrefixedText(), wfMsgExt('cancel', array('parseinline')) ); - $edithelpurl = $sk->makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' )); + $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' )); $edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'. htmlspecialchars( wfMsg( 'edithelp' ) ).'</a> '. htmlspecialchars( wfMsg( 'newwindow' ) ); @@ -1199,7 +1199,6 @@ END $wgOut->addHtml( wfHidden( 'wpAutoSummary', $autosumm ) ); if ( $this->isConflict ) { - require_once( "DifferenceEngine.php" ); $wgOut->addWikiText( '==' . wfMsg( "yourdiff" ) . '==' ); $de = new DifferenceEngine( $this->mTitle ); @@ -1380,11 +1379,6 @@ END wfProfileOut( $fname ); return $previewhead; } else { - # if user want to see preview when he edit an article - if( $wgUser->getOption('previewonfirst') and ($this->textbox1 == '')) { - $this->textbox1 = $this->getContent(); - } - $toparse = $this->textbox1; # If we're adding a comment, we need to show the @@ -1416,15 +1410,21 @@ END # If the user made changes, preserve them when showing the markup # (This happens when a user is blocked during edit, for instance) $first = $this->firsttime || ( !$this->save && $this->textbox1 == '' ); - $source = $first ? $this->getContent() : $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 - $rows = $wgUser->getOption( 'rows' ); - $cols = $wgUser->getOption( 'cols' ); - $attribs = array( 'id' => 'wpTextbox1', 'name' => 'wpTextbox1', 'cols' => $cols, 'rows' => $rows, 'readonly' => 'readonly' ); - $wgOut->addHtml( '<hr />' ); - $wgOut->addWikiText( wfMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() ) ); - $wgOut->addHtml( wfElement( 'textarea', $attribs, $source ) ); + if( $source !== false ) { + $rows = $wgUser->getOption( 'rows' ); + $cols = $wgUser->getOption( 'cols' ); + $attribs = array( 'id' => 'wpTextbox1', 'name' => 'wpTextbox1', 'cols' => $cols, 'rows' => $rows, 'readonly' => 'readonly' ); + $wgOut->addHtml( '<hr />' ); + $wgOut->addWikiText( wfMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() ) ); + $wgOut->addHtml( wfOpenElement( 'textarea', $attribs ) . htmlspecialchars( $source ) . wfCloseElement( 'textarea' ) ); + } } /** @@ -1720,7 +1720,6 @@ END * @return string HTML */ function getDiff() { - require_once( 'DifferenceEngine.php' ); $oldtext = $this->mArticle->fetchContent(); $newtext = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime ); diff --git a/includes/Exception.php b/includes/Exception.php index 1e24515b..56f18d5a 100644 --- a/includes/Exception.php +++ b/includes/Exception.php @@ -3,7 +3,8 @@ class MWException extends Exception { function useOutputPage() { - return !empty( $GLOBALS['wgFullyInitialised'] ); + return !empty( $GLOBALS['wgFullyInitialised'] ) && + !empty( $GLOBALS['wgArticle'] ) && !empty( $GLOBALS['wgTitle'] ); } function useMessageCache() { @@ -19,16 +20,28 @@ class MWException extends Exception return wfMsgReplaceArgs( $fallback, $args ); } } - + function getHTML() { - return '<p>' . htmlspecialchars( $this->getMessage() ) . - '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . - "</p>\n"; + global $wgShowExceptionDetails; + if( $wgShowExceptionDetails ) { + return '<p>' . htmlspecialchars( $this->getMessage() ) . + '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . + "</p>\n"; + } else { + return "<p>Set <b><tt>\$wgShowExceptionDetails = true;</tt></b> " . + "in LocalSettings.php to show detailed debugging information.</p>"; + } } function getText() { - return $this->getMessage() . - "\nBacktrace:\n" . $this->getTraceAsString() . "\n"; + global $wgShowExceptionDetails; + if( $wgShowExceptionDetails ) { + return $this->getMessage() . + "\nBacktrace:\n" . $this->getTraceAsString() . "\n"; + } else { + return "<p>Set <tt>\$wgShowExceptionDetails = true;</tt> " . + "in LocalSettings.php to show detailed debugging information.</p>"; + } } function getPageTitle() { @@ -40,6 +53,13 @@ class MWException extends Exception } } + function getLogMessage() { + $file = $this->getFile(); + $line = $this->getLine(); + $message = $this->getMessage(); + return "{$_SERVER['REQUEST_URI']} Exception from line $line of $file: $message"; + } + function reportHTML() { global $wgOut; if ( $this->useOutputPage() ) { @@ -67,6 +87,10 @@ class MWException extends Exception if ( $wgCommandLineMode ) { $this->reportText(); } else { + $log = $this->getLogMessage(); + if ( $log ) { + wfDebugLog( 'exception', $log ); + } $this->reportHTML(); } } @@ -93,11 +117,12 @@ class MWException extends Exception function htmlFooter() { echo "</body></html>"; - } + } + } /** - * Exception class which takes an HTML error message, and does not + * Exception class which takes an HTML error message, and does not * produce a backtrace. Replacement for OutputPage::fatalError(). */ class FatalError extends MWException { @@ -145,11 +170,11 @@ function wfReportException( Exception $e ) { $e->report(); } catch ( Exception $e2 ) { // Exception occurred from within exception handler - // Show a simpler error message for the original exception, + // Show a simpler error message for the original exception, // don't try to invoke report() $message = "MediaWiki internal error.\n\n" . - "Original exception: " . $e->__toString() . - "\n\nException caught inside exception handler: " . + "Original exception: " . $e->__toString() . + "\n\nException caught inside exception handler: " . $e2->__toString() . "\n"; if ( !empty( $GLOBALS['wgCommandLineMode'] ) ) { @@ -165,7 +190,7 @@ function wfReportException( Exception $e ) { /** * Exception handler which simulates the appropriate catch() handling: - * + * * try { * ... * } catch ( MWException $e ) { @@ -181,8 +206,7 @@ function wfExceptionHandler( $e ) { // Final cleanup, similar to wfErrorExit() if ( $wgFullyInitialised ) { try { - wfProfileClose(); - logProfilingData(); // uses $wgRequest, hence the $wgFullyInitialised condition + wfLogProfilingData(); // uses $wgRequest, hence the $wgFullyInitialised condition } catch ( Exception $e ) {} } diff --git a/includes/Exif.php b/includes/Exif.php index f9fb9a2c..2ab0feb1 100644 --- a/includes/Exif.php +++ b/includes/Exif.php @@ -293,7 +293,7 @@ class Exif { ); $this->file = $file; - $this->basename = basename( $this->file ); + $this->basename = wfBaseName( $this->file ); $this->makeFlatExifTags(); diff --git a/includes/Export.php b/includes/Export.php index da92694e..aa70e27b 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -16,46 +16,43 @@ # 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 */ -/** */ - -define( 'MW_EXPORT_FULL', 0 ); -define( 'MW_EXPORT_CURRENT', 1 ); - -define( 'MW_EXPORT_BUFFER', 0 ); -define( 'MW_EXPORT_STREAM', 1 ); - -define( 'MW_EXPORT_TEXT', 0 ); -define( 'MW_EXPORT_STUB', 1 ); - - -/** - * @package MediaWiki - * @subpackage 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; + + const BUFFER = 0; + const STREAM = 1; + + const TEXT = 0; + const STUB = 1; + /** - * If using MW_EXPORT_STREAM to stream a large amount of data, + * If using WikiExporter::STREAM to stream a large amount of data, * provide a database connection which is not managed by * LoadBalancer to read from: some history blob types will * make additional queries to pull source data while the * main query is still running. * * @param Database $db - * @param int $history one of MW_EXPORT_FULL or MW_EXPORT_CURRENT - * @param int $buffer one of MW_EXPORT_BUFFER or MW_EXPORT_STREAM + * @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 = MW_EXPORT_CURRENT, - $buffer = MW_EXPORT_BUFFER, $text = MW_EXPORT_TEXT ) { + function WikiExporter( &$db, $history = WikiExporter::CURRENT, + $buffer = WikiExporter::BUFFER, $text = WikiExporter::TEXT ) { $this->db =& $db; $this->history = $history; $this->buffer = $buffer; @@ -168,20 +165,42 @@ class WikiExporter { $revision = $this->db->tableName( 'revision' ); $text = $this->db->tableName( 'text' ); - if( $this->history == MW_EXPORT_FULL ) { + $order = 'ORDER BY page_id'; + $limit = ''; + + if( $this->history == WikiExporter::FULL ) { $join = 'page_id=rev_page'; - } elseif( $this->history == MW_EXPORT_CURRENT ) { + } elseif( $this->history == WikiExporter::CURRENT ) { if ( $this->list_authors && $cond != '' ) { // List authors, if so desired $this->do_list_authors ( $page , $revision , $cond ); } $join = 'page_id=rev_page AND page_latest=rev_id'; + } elseif ( is_array( $this->history ) ) { + $join = 'page_id=rev_page'; + if ( $this->history['dir'] == 'asc' ) { + $op = '>'; + $order .= ', rev_timestamp'; + } else { + $op = '<'; + $order .= ', rev_timestamp DESC'; + } + if ( !empty( $this->history['offset'] ) ) { + $join .= " AND rev_timestamp $op " . $this->db->addQuotes( + $this->db->timestamp( $this->history['offset'] ) ); + } + if ( !empty( $this->history['limit'] ) ) { + $limitNum = intval( $this->history['limit'] ); + if ( $limitNum > 0 ) { + $limit = "LIMIT $limitNum"; + } + } } else { wfProfileOut( $fname ); return new WikiError( "$fname given invalid history dump type." ); } $where = ( $cond == '' ) ? '' : "$cond AND"; - if( $this->buffer == MW_EXPORT_STREAM ) { + if( $this->buffer == WikiExporter::STREAM ) { $prev = $this->db->bufferResults( false ); } if( $cond == '' ) { @@ -193,19 +212,19 @@ class WikiExporter { $revindex = ''; $straight = ''; } - if( $this->text == MW_EXPORT_STUB ) { + if( $this->text == WikiExporter::STUB ) { $sql = "SELECT $straight * FROM $page $pageindex, $revision $revindex WHERE $where $join - ORDER BY page_id"; + $order $limit"; } else { $sql = "SELECT $straight * FROM $page $pageindex, $revision $revindex, $text WHERE $where $join AND rev_text_id=old_id - ORDER BY page_id"; + $order $limit"; } $result = $this->db->query( $sql, $fname ); $wrapper = $this->db->resultObject( $result ); @@ -215,7 +234,7 @@ class WikiExporter { $this->outputStream( $wrapper ); } - if( $this->buffer == MW_EXPORT_STREAM ) { + if( $this->buffer == WikiExporter::STREAM ) { $this->db->bufferResults( $prev ); } diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php index 21f632ec..14b55fdb 100644 --- a/includes/ExternalEdit.php +++ b/includes/ExternalEdit.php @@ -48,7 +48,7 @@ class ExternalEdit { $extension="wiki"; } elseif($this->mMode=="file") { $type="Edit file"; - $image = Image::newFromTitle( $this->mTitle ); + $image = new Image( $this->mTitle ); $img_url = $image->getURL(); if(strpos($img_url,"://")) { $url = $img_url; diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php index f610df80..861a9939 100644 --- a/includes/ExternalStoreDB.php +++ b/includes/ExternalStoreDB.php @@ -6,7 +6,6 @@ * DB accessable external objects * */ -require_once( 'LoadBalancer.php' ); /** @package MediaWiki */ diff --git a/includes/FileStore.php b/includes/FileStore.php index 85aaedfe..35ebd554 100644 --- a/includes/FileStore.php +++ b/includes/FileStore.php @@ -36,18 +36,16 @@ class FileStore { * @fixme Probably only works on MySQL. Abstract to the Database class? */ static function lock() { - $fname = __CLASS__ . '::' . __FUNCTION__; - $dbw = wfGetDB( DB_MASTER ); $lockname = $dbw->addQuotes( FileStore::lockName() ); - $result = $dbw->query( "SELECT GET_LOCK($lockname, 5) AS lockstatus", $fname ); + $result = $dbw->query( "SELECT GET_LOCK($lockname, 5) AS lockstatus", __METHOD__ ); $row = $dbw->fetchObject( $result ); $dbw->freeResult( $result ); if( $row->lockstatus == 1 ) { return true; } else { - wfDebug( "$fname failed to acquire lock\n" ); + wfDebug( __METHOD__." failed to acquire lock\n" ); return false; } } @@ -56,18 +54,15 @@ class FileStore { * Release the global file store lock. */ static function unlock() { - $fname = __CLASS__ . '::' . __FUNCTION__; - $dbw = wfGetDB( DB_MASTER ); $lockname = $dbw->addQuotes( FileStore::lockName() ); - $result = $dbw->query( "SELECT RELEASE_LOCK($lockname)", $fname ); + $result = $dbw->query( "SELECT RELEASE_LOCK($lockname)", __METHOD__ ); $row = $dbw->fetchObject( $result ); $dbw->freeResult( $result ); } private static function lockName() { - global $wgDBname, $wgDBprefix; - return "MediaWiki.{$wgDBname}.{$wgDBprefix}FileStore"; + return 'MediaWiki.' . wfWikiID() . '.FileStore'; } /** @@ -103,8 +98,6 @@ class FileStore { } private function copyFile( $sourcePath, $destPath, $flags=0 ) { - $fname = __CLASS__ . '::' . __FUNCTION__; - if( !file_exists( $sourcePath ) ) { // Abort! Abort! throw new FSException( "missing source file '$sourcePath'\n" ); @@ -135,11 +128,11 @@ class FileStore { wfRestoreWarnings(); if( $ok ) { - wfDebug( "$fname copied '$sourcePath' to '$destPath'\n" ); + wfDebug( __METHOD__." copied '$sourcePath' to '$destPath'\n" ); $transaction->addRollback( FSTransaction::DELETE_FILE, $destPath ); } else { throw new FSException( - "$fname failed to copy '$sourcePath' to '$destPath'\n" ); + __METHOD__." failed to copy '$sourcePath' to '$destPath'\n" ); } } @@ -239,13 +232,11 @@ class FileStore { * @return string or false if could not open file or bad extension */ static function calculateKey( $path, $extension ) { - $fname = __CLASS__ . '::' . __FUNCTION__; - wfSuppressWarnings(); $hash = sha1_file( $path ); wfRestoreWarnings(); if( $hash === false ) { - wfDebug( "$fname: couldn't hash file '$path'\n" ); + wfDebug( __METHOD__.": couldn't hash file '$path'\n" ); return false; } @@ -260,7 +251,7 @@ class FileStore { if( self::validKey( $key ) ) { return $key; } else { - wfDebug( "$fname: generated bad key '$key'\n" ); + wfDebug( __METHOD__.": generated bad key '$key'\n" ); return false; } } @@ -353,7 +344,6 @@ class FSTransaction { } private function apply( $actions ) { - $fname = __CLASS__ . '::' . __FUNCTION__; $result = true; foreach( $actions as $item ) { list( $action, $path ) = $item; @@ -362,9 +352,9 @@ class FSTransaction { $ok = unlink( $path ); wfRestoreWarnings(); if( $ok ) - wfDebug( "$fname: deleting file '$path'\n" ); + wfDebug( __METHOD__.": deleting file '$path'\n" ); else - wfDebug( "$fname: failed to delete file '$path'\n" ); + wfDebug( __METHOD__.": failed to delete file '$path'\n" ); $result = $result && $ok; } } @@ -374,4 +364,4 @@ class FSTransaction { class FSException extends MWException { } -?>
\ No newline at end of file +?> diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index e2033486..623f9d3b 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -33,9 +33,10 @@ require_once( 'XmlFunctions.php' ); /** * Compatibility functions - * PHP <4.3.x is not actively supported; 4.1.x and 4.2.x might or might not work. - * <4.1.x will not work, as we use a number of features introduced in 4.1.0 - * such as the new autoglobals. + * + * We more or less support PHP 5.0.x and up. + * Re-implementations of newer functions or functions in non-standard + * PHP extensions may be included here. */ if( !function_exists('iconv') ) { # iconv support is not in the default configuration and so may not be present. @@ -49,22 +50,6 @@ if( !function_exists('iconv') ) { } } -if( !function_exists('file_get_contents') ) { - # Exists in PHP 4.3.0+ - function file_get_contents( $filename ) { - return implode( '', file( $filename ) ); - } -} - -if( !function_exists('is_a') ) { - # Exists in PHP 4.2.0+ - function is_a( $object, $class_name ) { - return - (strcasecmp( get_class( $object ), $class_name ) == 0) || - is_subclass_of( $object, $class_name ); - } -} - # UTF-8 substr function based on a PHP manual comment if ( !function_exists( 'mb_substr' ) ) { function mb_substr( $str, $start ) { @@ -79,17 +64,6 @@ if ( !function_exists( 'mb_substr' ) ) { } } -if( !function_exists( 'floatval' ) ) { - /** - * First defined in PHP 4.2.0 - * @param mixed $var; - * @return float - */ - function floatval( $var ) { - return (float)$var; - } -} - if ( !function_exists( 'array_diff_key' ) ) { /** * Exists in PHP 5.1.0+ @@ -109,39 +83,25 @@ if ( !function_exists( 'array_diff_key' ) ) { /** - * Wrapper for clone() for PHP 4, for the moment. + * Wrapper for clone(), for compatibility with PHP4-friendly extensions. * PHP 5 won't let you declare a 'clone' function, even conditionally, * so it has to be a wrapper with a different name. */ function wfClone( $object ) { - // WARNING: clone() is not a function in PHP 5, so function_exists fails. - if( version_compare( PHP_VERSION, '5.0' ) < 0 ) { - return $object; - } else { - return clone( $object ); - } + return clone( $object ); } /** * Where as we got a random seed - * @var bool $wgTotalViews */ $wgRandomSeeded = false; /** * Seed Mersenne Twister - * Only necessary in PHP < 4.2.0 - * - * @return bool + * No-op for compatibility; only necessary in PHP < 4.2.0 */ function wfSeedRandom() { - global $wgRandomSeeded; - - if ( ! $wgRandomSeeded && version_compare( phpversion(), '4.2.0' ) < 0 ) { - $seed = hexdec(substr(md5(microtime()),-8)) & 0x7fffffff; - mt_srand( $seed ); - $wgRandomSeeded = true; - } + /* No-op */ } /** @@ -190,13 +150,25 @@ function wfUrlencode ( $s ) { */ function wfDebug( $text, $logonly = false ) { global $wgOut, $wgDebugLogFile, $wgDebugComments, $wgProfileOnly, $wgDebugRawPage; + static $recursion = 0; # Check for raw action using $_GET not $wgRequest, since the latter might not be initialised yet if ( isset( $_GET['action'] ) && $_GET['action'] == 'raw' && !$wgDebugRawPage ) { return; } - if ( isset( $wgOut ) && $wgDebugComments && !$logonly ) { + if ( $wgDebugComments && !$logonly ) { + if ( !isset( $wgOut ) ) { + return; + } + if ( !StubObject::isRealObject( $wgOut ) ) { + if ( $recursion ) { + return; + } + $recursion++; + $wgOut->_unstub(); + $recursion--; + } $wgOut->debug( $text ); } if ( '' != $wgDebugLogFile && !$wgProfileOnly ) { @@ -217,11 +189,12 @@ function wfDebug( $text, $logonly = false ) { * log file is specified, (default true) */ function wfDebugLog( $logGroup, $text, $public = true ) { - global $wgDebugLogGroups, $wgDBname; + global $wgDebugLogGroups; if( $text{strlen( $text ) - 1} != "\n" ) $text .= "\n"; if( isset( $wgDebugLogGroups[$logGroup] ) ) { $time = wfTimestamp( TS_DB ); - @error_log( "$time $wgDBname: $text", 3, $wgDebugLogGroups[$logGroup] ); + $wiki = wfWikiID(); + @error_log( "$time $wiki: $text", 3, $wgDebugLogGroups[$logGroup] ); } else if ( $public === true ) { wfDebug( $text, true ); } @@ -243,13 +216,12 @@ function wfLogDBError( $text ) { /** * @todo document */ -function logProfilingData() { +function wfLogProfilingData() { global $wgRequestTime, $wgDebugLogFile, $wgDebugRawPage, $wgRequest; global $wgProfiling, $wgUser; - $now = wfTime(); - - $elapsed = $now - $wgRequestTime; if ( $wgProfiling ) { + $now = wfTime(); + $elapsed = $now - $wgRequestTime; $prof = wfGetProfilingOutput( $wgRequestTime, $elapsed ); $forward = ''; if( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) @@ -260,7 +232,8 @@ function logProfilingData() { $forward .= ' from ' . $_SERVER['HTTP_FROM']; if( $forward ) $forward = "\t(proxied via {$_SERVER['REMOTE_ADDR']}{$forward})"; - if( is_object($wgUser) && $wgUser->isAnon() ) + // Don't unstub $wgUser at this late stage just for statistics purposes + if( StubObject::isRealObject($wgUser) && $wgUser->isAnon() ) $forward .= ' anon'; $log = sprintf( "%s\t%04.3f\t%s\n", gmdate( 'YmdHis' ), $elapsed, @@ -418,12 +391,12 @@ function wfMsgReal( $key, $args, $useDB = true, $forContent=false, $transform = function wfMsgWeirdKey ( $key ) { $subsource = str_replace ( ' ' , '_' , $key ) ; $source = wfMsgForContentNoTrans( $subsource ) ; - if ( $source == "<{$subsource}>" ) { + if ( wfEmptyMsg( $subsource, $source) ) { # Try again with first char lower case $subsource = strtolower ( substr ( $subsource , 0 , 1 ) ) . substr ( $subsource , 1 ) ; $source = wfMsgForContentNoTrans( $subsource ) ; } - if ( $source == "<{$subsource}>" ) { + if ( wfEmptyMsg( $subsource, $source ) ) { # Didn't work either, return blank text $source = "" ; } @@ -439,7 +412,7 @@ function wfMsgWeirdKey ( $key ) { * @private */ function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) { - global $wgParser, $wgMsgParserOptions, $wgContLang, $wgMessageCache, $wgLang; + global $wgParser, $wgContLang, $wgMessageCache, $wgLang; if ( is_object( $wgMessageCache ) ) $transstat = $wgMessageCache->getTransform(); @@ -466,7 +439,7 @@ function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) { if($message === false) $message = Language::getMessage($key); if ( $transform && strstr( $message, '{{' ) !== false ) { - $message = $wgParser->transformMsg($message, $wgMsgParserOptions); + $message = $wgParser->transformMsg($message, $wgMessageCache->getParserOptions() ); } } @@ -621,8 +594,7 @@ function wfAbruptExit( $error = false ){ wfDebug('WARNING: Abrupt exit\n'); } - wfProfileClose(); - logProfilingData(); + wfLogProfilingData(); if ( !$error ) { $wgLoadBalancer->closeAll(); @@ -867,8 +839,8 @@ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) { */ function wfEscapeWikiText( $text ) { $text = str_replace( - array( '[', '|', '\'', 'ISBN ' , '://' , "\n=", '{{' ), - array( '[', '|', ''', 'ISBN ', '://' , "\n=", '{{' ), + array( '[', '|', '\'', 'ISBN ', 'RFC ', '://', "\n=", '{{' ), + array( '[', '|', ''', 'ISBN ', 'RFC ', '://', "\n=", '{{' ), htmlspecialchars($text) ); return $text; } @@ -1296,6 +1268,11 @@ define('TS_EXIF', 5); define('TS_ORACLE', 6); /** + * Postgres format time. + */ +define('TS_POSTGRES', 7); + +/** * @param mixed $outputtype A timestamp in one of the supported formats, the * function will autodetect which format is supplied * and act accordingly. @@ -1329,6 +1306,10 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) { # TS_ISO_8601 $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], (int)$da[2],(int)$da[3],(int)$da[1]); + } elseif (preg_match("/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)[\+\- ](\d\d)$/",$ts,$da)) { + # TS_POSTGRES + $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], + (int)$da[2],(int)$da[3],(int)$da[1]); } else { # Bogus value; fall back to the epoch... wfDebug("wfTimestamp() fed bogus time value: $outputtype; $ts\n"); @@ -1352,6 +1333,8 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) { return gmdate( 'D, d M Y H:i:s', $uts ) . ' GMT'; case TS_ORACLE: return gmdate( 'd-M-y h.i.s A', $uts) . ' +00:00'; + case TS_POSTGRES: + return gmdate( 'Y-m-d H:i:s', $uts) . ' GMT'; default: throw new MWException( 'wfTimestamp() called with illegal output type.'); } @@ -1395,18 +1378,18 @@ function swap( &$x, &$y ) { } function wfGetCachedNotice( $name ) { - global $wgOut, $parserMemc, $wgDBname; + global $wgOut, $parserMemc; $fname = 'wfGetCachedNotice'; wfProfileIn( $fname ); $needParse = false; $notice = wfMsgForContent( $name ); - if( $notice == '<'. $name . ';>' || $notice == '-' ) { + if( wfEmptyMsg( $name, $notice ) || $notice == '-' ) { wfProfileOut( $fname ); return( false ); } - $cachedNotice = $parserMemc->get( $wgDBname . ':' . $name ); + $cachedNotice = $parserMemc->get( wfMemcKey( $name ) ); if( is_array( $cachedNotice ) ) { if( md5( $notice ) == $cachedNotice['hash'] ) { $notice = $cachedNotice['html']; @@ -1420,7 +1403,7 @@ function wfGetCachedNotice( $name ) { if( $needParse ) { if( is_object( $wgOut ) ) { $parsed = $wgOut->parse( $notice ); - $parserMemc->set( $wgDBname . ':' . $name, array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 ); + $parserMemc->set( wfMemcKey( $name ), array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 ); $notice = $parsed; } else { wfDebug( 'wfGetCachedNotice called for ' . $name . ' with no $wgOut available' ); @@ -1480,37 +1463,14 @@ function wfGetSiteNotice() { return $siteNotice; } -/** Global singleton instance of MimeMagic. This is initialized on demand, -* please always use the wfGetMimeMagic() function to get the instance. -* -* @private -*/ -$wgMimeMagic= NULL; - -/** Factory functions for the global MimeMagic object. -* This function always returns the same singleton instance of MimeMagic. -* That objects will be instantiated on the first call to this function. -* If needed, the MimeMagic.php file is automatically included by this function. -* @return MimeMagic the global MimeMagic objects. -*/ +/** + * BC wrapper for MimeMagic::singleton() + * @deprecated + */ function &wfGetMimeMagic() { - global $wgMimeMagic; - - if (!is_null($wgMimeMagic)) { - return $wgMimeMagic; - } - - if (!class_exists("MimeMagic")) { - #include on demand - require_once("MimeMagic.php"); - } - - $wgMimeMagic= new MimeMagic(); - - return $wgMimeMagic; + return MimeMagic::singleton(); } - /** * Tries to get the system directory for temporary files. * The TMPDIR, TMP, and TEMP environment variables are checked in sequence, @@ -1582,8 +1542,8 @@ function wfMkdirParents( $fullDir, $mode = 0777 ) { * Increment a statistics counter */ function wfIncrStats( $key ) { - global $wgDBname, $wgMemc; - $key = "$wgDBname:stats:$key"; + global $wgMemc; + $key = wfMemcKey( 'stats', $key ); if ( is_null( $wgMemc->incr( $key ) ) ) { $wgMemc->add( $key, 1 ); } @@ -1689,7 +1649,7 @@ function wfUrlProtocols() { * @return collected stdout as a string (trailing newlines stripped) */ function wfShellExec( $cmd, &$retval=null ) { - global $IP, $wgMaxShellMemory; + global $IP, $wgMaxShellMemory, $wgMaxShellFileSize; if( ini_get( 'safe_mode' ) ) { wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" ); @@ -1700,11 +1660,12 @@ function wfShellExec( $cmd, &$retval=null ) { if ( php_uname( 's' ) == 'Linux' ) { $time = ini_get( 'max_execution_time' ); $mem = intval( $wgMaxShellMemory ); + $filesize = intval( $wgMaxShellFileSize ); if ( $time > 0 && $mem > 0 ) { - $script = "$IP/bin/ulimit.sh"; + $script = "$IP/bin/ulimit-tvf.sh"; if ( is_executable( $script ) ) { - $cmd = escapeshellarg( $script ) . " $time $mem $cmd"; + $cmd = escapeshellarg( $script ) . " $time $mem $filesize $cmd"; } } } elseif ( php_uname( 's' ) == 'Windows NT' ) { @@ -2002,4 +1963,104 @@ function wfIsLocalURL( $url ) { return Http::isLocalURL( $url ); } +/** + * Initialise php session + */ +function wfSetupSession() { + global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain; + if( $wgSessionsInMemcached ) { + require_once( 'MemcachedSessions.php' ); + } elseif( 'files' != ini_get( 'session.save_handler' ) ) { + # If it's left on 'user' or another setting from another + # application, it will end up failing. Try to recover. + ini_set ( 'session.save_handler', 'files' ); + } + session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain ); + session_cache_limiter( 'private, must-revalidate' ); + @session_start(); +} + +/** + * Get an object from the precompiled serialized directory + * + * @return mixed The variable on success, false on failure + */ +function wfGetPrecompiledData( $name ) { + global $IP; + + $file = "$IP/serialized/$name"; + if ( file_exists( $file ) ) { + $blob = file_get_contents( $file ); + if ( $blob ) { + return unserialize( $blob ); + } + } + return false; +} + +function wfGetCaller( $level = 2 ) { + $backtrace = debug_backtrace(); + if ( isset( $backtrace[$level] ) ) { + if ( isset( $backtrace[$level]['class'] ) ) { + $caller = $backtrace[$level]['class'] . '::' . $backtrace[$level]['function']; + } else { + $caller = $backtrace[$level]['function']; + } + } else { + $caller = 'unknown'; + } + return $caller; +} + +/** Return a string consisting all callers in stack, somewhat useful sometimes for profiling specific points */ +function wfGetAllCallers() { + return implode('/', array_map( + create_function('$frame',' + return isset( $frame["class"] )? + $frame["class"]."::".$frame["function"]: + $frame["function"]; + '), + array_reverse(debug_backtrace()))); +} + +/** + * Get a cache key + */ +function wfMemcKey( /*... */ ) { + global $wgDBprefix, $wgDBname; + $args = func_get_args(); + if ( $wgDBprefix ) { + $key = "$wgDBname-$wgDBprefix:" . implode( ':', $args ); + } else { + $key = $wgDBname . ':' . implode( ':', $args ); + } + return $key; +} + +/** + * Get a cache key for a foreign DB + */ +function wfForeignMemcKey( $db, $prefix /*, ... */ ) { + $args = array_slice( func_get_args(), 2 ); + if ( $prefix ) { + $key = "$db-$prefix:" . implode( ':', $args ); + } else { + $key = $db . ':' . implode( ':', $args ); + } + return $key; +} + +/** + * Get an ASCII string identifying this wiki + * This is used as a prefix in memcached keys + */ +function wfWikiID() { + global $wgDBprefix, $wgDBname; + if ( $wgDBprefix ) { + return "$wgDBname-$wgDBprefix"; + } else { + return $wgDBname; + } +} + ?> diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php index c3d74b20..3ee85859 100644 --- a/includes/HTMLForm.php +++ b/includes/HTMLForm.php @@ -71,7 +71,7 @@ class HTMLForm { ( $checked ? ' checked="checked"' : '' ) . " />" . wfMsg( $this->mName.'-'.$varname.'-'.$value ) . "</label></div>\n"; } - return $this->fieldset( $this->mName.'-'.$varname, $s ); + return $this->fieldset( $varname, $s ); } /** @@ -109,10 +109,11 @@ class HTMLForm { } } // end class - -// functions used by SpecialUserrights.php - /** Build a select with all defined groups + * + * used by SpecialUserrights.php + * @todo move it to there, and don't forget to copy it for SpecialMakesysop.php + * * @param $selectname String: name of this element. Name of form is automaticly prefixed. * @param $selectmsg String: FIXME * @param $selected Array: array of element selected when posted. Only multiples will show them. @@ -154,24 +155,4 @@ function HTMLSelectGroups($selectname, $selectmsg, $selected=array(), $multiple= return $out; } -/** Build a select with all existent rights - * @param $selected Array: Names(?) of user rights that should be selected. - * @return string HTML select. - */ -function HTMLSelectRights($selected='') { - global $wgAvailableRights; - $out = '<select name="editgroup-getrights[]" multiple="multiple">'; - $groupRights = explode(',',$selected); - - foreach($wgAvailableRights as $right) { - - // check box when right exist - if(in_array($right, $groupRights)) { $selected = 'selected="selected" '; } - else { $selected = ''; } - - $out .= '<option value="'.$right.'" '.$selected.'>'.$right."</option>\n"; - } - $out .= "</select>\n"; - return $out; -} ?> diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php index 8f5d3624..357c1d48 100644 --- a/includes/HistoryBlob.php +++ b/includes/HistoryBlob.php @@ -231,7 +231,6 @@ class HistoryBlobStub { wfProfileOut( $fname ); return false; } - require_once('ExternalStore.php'); $row->old_text=ExternalStore::fetchFromUrl($url); } diff --git a/includes/Hooks.php b/includes/Hooks.php index 4daffaf3..575a28c5 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -64,7 +64,7 @@ function wfRunHooks($event, $args = null) { if (count($hook) < 1) { throw new MWException("Empty array in hooks for " . $event . "\n"); } else if (is_object($hook[0])) { - $object =& $wgHooks[$event][$index][0]; + $object = $wgHooks[$event][$index][0]; if (count($hook) < 2) { $method = "on" . $event; } else { @@ -87,7 +87,7 @@ function wfRunHooks($event, $args = null) { } else if (is_string($hook)) { # functions look like strings, too $func = $hook; } else if (is_object($hook)) { - $object =& $wgHooks[$event][$index]; + $object = $wgHooks[$event][$index]; $method = "on" . $event; } else { throw new MWException("Unknown datatype in hooks for " . $event . "\n"); @@ -101,18 +101,18 @@ function wfRunHooks($event, $args = null) { $hook_args = $args; } - if ( isset( $object ) ) { $func = get_class( $object ) . '::' . $method; + $callback = array( $object, $method ); + } elseif ( false !== ( $pos = strpos( '::', $func ) ) ) { + $callback = array( substr( $func, 0, $pos ), substr( $func, $pos + 2 ) ); + } else { + $callback = $func; } /* Call the hook. */ wfProfileIn( $func ); - if( isset( $object ) ) { - $retval = call_user_func_array(array(&$object, $method), $hook_args); - } else { - $retval = call_user_func_array($func, $hook_args); - } + $retval = call_user_func_array( $callback, $hook_args ); wfProfileOut( $func ); /* String return is an error; false return means stop processing. */ diff --git a/includes/IP.php b/includes/IP.php new file mode 100644 index 00000000..f3ff3427 --- /dev/null +++ b/includes/IP.php @@ -0,0 +1,211 @@ +<?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 + */ + +// Some regex definition to "play" with IP address and IP address blocks + +// 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]\d|1?\d{1,2})'); +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) +define( 'RE_IP_PREFIX' , '(3[0-2]|[12]?\d)'); +define( 'RE_IP_BLOCK', RE_IP_ADD . '\/' . RE_IP_PREFIX); + +class IP { + + /** + * Validate an IP address. + * @return boolean True if it is valid. + */ + public static function isValid( $ip ) { + return preg_match( '/^' . RE_IP_ADD . '$/', $ip, $matches) ; + } + + /** + * Validate an IP Block. + * @return boolean True if it is valid. + */ + public static function isValidBlock( $ipblock ) { + return ( count(self::toArray($ipblock)) == 1 + 5 ); + } + + /** + * Determine if an IP address really is an IP address, and if it is public, + * i.e. not RFC 1918 or similar + * Comes from ProxyTools.php + */ + public static function isPublic( $ip ) { + $n = IP::toUnsigned( $ip ); + if ( !$n ) { + return false; + } + + // ip2long accepts incomplete addresses, as well as some addresses + // followed by garbage characters. Check that it's really valid. + if( $ip != long2ip( $n ) ) { + return false; + } + + static $privateRanges = false; + if ( !$privateRanges ) { + $privateRanges = array( + array( '10.0.0.0', '10.255.255.255' ), # RFC 1918 (private) + array( '172.16.0.0', '172.31.255.255' ), # " + array( '192.168.0.0', '192.168.255.255' ), # " + array( '0.0.0.0', '0.255.255.255' ), # this network + array( '127.0.0.0', '127.255.255.255' ), # loopback + ); + } + + foreach ( $privateRanges as $r ) { + $start = IP::toUnsigned( $r[0] ); + $end = IP::toUnsigned( $r[1] ); + if ( $n >= $start && $n <= $end ) { + return false; + } + } + return true; + } + + /** + * Split out an IP block as an array of 4 bytes and a mask, + * return false if it cant be determined + * + * @parameter $ip string A quad dotted IP address + * @return array + */ + public static function toArray( $ipblock ) { + if(! preg_match( '/^' . RE_IP_ADD . '(?:\/(?:'.RE_IP_PREFIX.'))?' . '$/', $ipblock, $matches ) ) { + return false; + } else { + return $matches; + } + } + + /** + * Return a zero-padded hexadecimal representation of an IP address. + * + * Hexadecimal addresses are used because they can easily be extended to + * IPv6 support. To separate the ranges, the return value from this + * 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. + */ + public static function toHex( $ip ) { + $n = self::toUnsigned( $ip ); + if ( $n !== false ) { + $n = sprintf( '%08X', $n ); + } + return $n; + } + + /** + * Given an IP address in dotted-quad 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. + */ + public static function toUnsigned( $ip ) { + if ( $ip == '255.255.255.255' ) { + $n = -1; + } else { + $n = ip2long( $ip ); + if ( $n == -1 || $n === false ) { # Return value on error depends on PHP version + $n = false; + } + } + if ( $n < 0 ) { + $n += pow( 2, 32 ); + } + return $n; + } + + /** + * Convert a dotted-quad IP to a signed integer + * Returns false on failure + */ + public static function toSigned( $ip ) { + if ( $ip == '255.255.255.255' ) { + $n = -1; + } else { + $n = ip2long( $ip ); + if ( $n == -1 ) { + $n = false; + } + } + return $n; + } + + /** + * Convert a network specification in CIDR notation to an integer network and a number of bits + */ + public static function parseCIDR( $range ) { + $parts = explode( '/', $range, 2 ); + if ( count( $parts ) != 2 ) { + return array( false, false ); + } + $network = IP::toSigned( $parts[0] ); + if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 32 ) { + $bits = $parts[1]; + if ( $bits == 0 ) { + $network = 0; + } else { + $network &= ~((1 << (32 - $bits)) - 1); + } + # Convert to unsigned + if ( $network < 0 ) { + $network += pow( 2, 32 ); + } + } 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. + * + * Formats are: + * 1.2.3.4/24 CIDR + * 1.2.3.4 - 1.2.3.5 Explicit range + * 1.2.3.4 Single IP + */ + public static function parseRange( $range ) { + if ( strpos( $range, '/' ) !== false ) { + # CIDR + list( $network, $bits ) = IP::parseCIDR( $range ); + if ( $network === false ) { + $start = $end = false; + } else { + $start = sprintf( '%08X', $network ); + $end = sprintf( '%08X', $network + pow( 2, (32 - $bits) ) - 1 ); + } + } elseif ( strpos( $range, '-' ) !== false ) { + # Explicit range + list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) ); + if ( $start > $end ) { + $start = $end = false; + } else { + $start = IP::toHex( $start ); + $end = IP::toHex( $end ); + } + } else { + # Single IP + $start = $end = IP::toHex( $range ); + } + if ( $start === false || $end === false ) { + return array( false, false ); + } else { + return array( $start, $end ); + } + } +} +?> diff --git a/includes/Image.php b/includes/Image.php index 185d732a..55e53e26 100644 --- a/includes/Image.php +++ b/includes/Image.php @@ -46,6 +46,7 @@ class Image $size, # Size in bytes (loadFromXxx) $metadata, # Metadata $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx) + $page, # Page to render when creating thumbnails $lastError; # Error string associated with a thumbnail display error @@ -86,6 +87,7 @@ class Image $this->extension = Image::normalizeExtension( $n ? substr( $this->name, $n + 1 ) : '' ); $this->historyLine = 0; + $this->page = 1; $this->dataLoaded = false; } @@ -119,12 +121,12 @@ class Image * Returns an array, first element is the local cache key, second is the shared cache key, if there is one */ function getCacheKeys( ) { - global $wgDBname, $wgUseSharedUploads, $wgSharedUploadDBname, $wgCacheSharedUploads; + global $wgUseSharedUploads, $wgSharedUploadDBname, $wgCacheSharedUploads; $hashedName = md5($this->name); - $keys = array( "$wgDBname:Image:$hashedName" ); + $keys = array( wfMemcKey( 'Image', $hashedName ) ); if ( $wgUseSharedUploads && $wgSharedUploadDBname && $wgCacheSharedUploads ) { - $keys[] = "$wgSharedUploadDBname:Image:$hashedName"; + $keys[] = wfForeignMemcKey( $wgSharedUploadDBname, false, 'Image', $hashedName ); } return $keys; } @@ -142,7 +144,7 @@ class Image // Check if the key existed and belongs to this version of MediaWiki if (!empty($cachedValues) && is_array($cachedValues) && isset($cachedValues['version']) && ( $cachedValues['version'] == MW_IMAGE_VERSION ) - && $cachedValues['fileExists'] && isset( $cachedValues['mime'] ) && isset( $cachedValues['metadata'] ) ) + && isset( $cachedValues['mime'] ) && isset( $cachedValues['metadata'] ) ) { if ( $wgUseSharedUploads && $cachedValues['fromShared']) { # if this is shared file, we need to check if image @@ -200,13 +202,13 @@ class Image * Save the image metadata to memcached */ function saveToCache() { - global $wgMemc; + global $wgMemc, $wgUseSharedUploads; $this->load(); $keys = $this->getCacheKeys(); - if ( $this->fileExists ) { - // We can't cache negative metadata for non-existent files, - // because if the file later appears in commons, the local - // keys won't be purged. + // We can't cache negative metadata for non-existent files, + // because if the file later appears in commons, the local + // keys won't be purged. + if ( $this->fileExists || !$wgUseSharedUploads ) { $cachedValues = array( 'version' => MW_IMAGE_VERSION, 'name' => $this->name, @@ -258,7 +260,7 @@ class Image if ( $this->fileExists ) { - $magic=& wfGetMimeMagic(); + $magic=& MimeMagic::singleton(); $this->mime = $magic->guessMimeType($this->imagePath,true); $this->type = $magic->getMediaType($this->imagePath,$this->mime); @@ -266,7 +268,7 @@ class Image # Get size in bytes $this->size = filesize( $this->imagePath ); - $magic=& wfGetMimeMagic(); + $magic=& MimeMagic::singleton(); # Height and width wfSuppressWarnings(); @@ -307,7 +309,11 @@ class Image $this->dataLoaded = true; - $this->metadata = serialize( $this->retrieveExifData( $this->imagePath ) ); + 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; @@ -323,7 +329,6 @@ class Image wfProfileIn( __METHOD__ ); $dbr =& wfGetDB( DB_SLAVE ); - $this->checkDBSchema($dbr); $row = $dbr->selectRow( 'image', @@ -374,6 +379,7 @@ class Image $this->fileExists = false; $this->fromSharedDirectory = false; $this->metadata = serialize ( array() ) ; + $this->mime = false; } # Unconditionally set loaded=true, we don't want the accessors constantly rechecking @@ -416,9 +422,12 @@ class Image $this->loadFromDB(); if ( !$wgSharedUploadDBname && $wgUseSharedUploads ) { $this->loadFromFile(); - } elseif ( $this->fileExists ) { + } elseif ( $this->fileExists || !$wgUseSharedUploads ) { + // We can do negative caching for local images, because the cache + // will be purged on upload. But we can't do it when shared images + // are enabled, since updates to that won't purge foreign caches. $this->saveToCache(); - } + } } $this->dataLoaded = true; } @@ -601,7 +610,7 @@ class Image * @todo remember the result of this check. */ function canRender() { - global $wgUseImageMagick; + global $wgUseImageMagick, $wgDjvuRenderer; if( $this->getWidth()<=0 || $this->getHeight()<=0 ) return false; @@ -647,6 +656,7 @@ class Image if ( $mime === 'image/vnd.wap.wbmp' || $mime === 'image/x-xbitmap' ) return true; } + if ( $mime === 'image/vnd.djvu' && isset( $wgDjvuRenderer ) && $wgDjvuRenderer ) return true; return false; } @@ -734,9 +744,16 @@ class Image * Return the escapeLocalURL of this image * @public */ - function getEscapeLocalURL() { + function getEscapeLocalURL( $query=false) { $this->getTitle(); - return $this->title->escapeLocalURL(); + if ( $query === false ) { + if ( $this->page != 1 ) { + $query = 'page=' . $this->page; + } else { + $query = ''; + } + } + return $this->title->escapeLocalURL( $query ); } /** @@ -836,6 +853,9 @@ class Image */ function thumbName( $width ) { $thumb = $width."px-".$this->name; + if ( $this->page != 1 ) { + $thumb = "page{$this->page}-$thumb"; + } if( $this->mustRender() ) { if( $this->canRender() ) { @@ -1123,6 +1143,7 @@ class Image global $wgSVGConverters, $wgSVGConverter; global $wgUseImageMagick, $wgImageMagickConvertCommand; global $wgCustomConvertCommand; + global $wgDjvuRenderer, $wgDjvuPostProcessor; $this->load(); @@ -1149,96 +1170,112 @@ class Image $err = wfShellExec( $cmd, $retval ); wfProfileOut( 'rsvg' ); } - } 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. - " -resize " . 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 ( $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' ); - 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 ); - } + } 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 + } - $src_image = call_user_func( $loader, $this->imagePath ); - if ( $truecolor ) { - $dst_image = imagecreatetruecolor( $width, $height ); + # 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 { - $dst_image = imagecreate( $width, $height ); + # 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 ); + } + + $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 ); } - 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 ); } # @@ -1367,14 +1404,16 @@ class Image } function checkDBSchema(&$db) { + static $checkDone = false; global $wgCheckDBSchema; - if (!$wgCheckDBSchema) { + if (!$wgCheckDBSchema || $checkDone) { return; } # img_name must be unique if ( !$db->indexUnique( 'image', 'img_name' ) && !$db->indexExists('image','PRIMARY') ) { throw new MWException( 'Database schema not up to date, please run maintenance/archives/patch-image_name_unique.sql' ); } + $checkDone = true; # new fields must exist # @@ -1489,7 +1528,7 @@ class Image * @return bool * @static */ - function isHashed( $shared ) { + public static function isHashed( $shared ) { global $wgHashedUploadDirectory, $wgHashedSharedUploadDirectory; return $shared ? $wgHashedSharedUploadDirectory : $wgHashedUploadDirectory; } @@ -1706,7 +1745,7 @@ class Image function getExifData() { global $wgRequest; - if ( $this->metadata === '0' ) + if ( $this->metadata === '0' || $this->mime == 'image/vnd.djvu' ) return array(); $purge = $wgRequest->getVal( 'action' ) == 'purge'; @@ -2095,7 +2134,7 @@ class Image $tempFile = $store->filePath( $row->fa_storage_key ); $metadata = serialize( $this->retrieveExifData( $tempFile ) ); - $magic = wfGetMimeMagic(); + $magic = MimeMagic::singleton(); $mime = $magic->guessMimeType( $tempFile, true ); $media_type = $magic->getMediaType( $tempFile, $mime ); list( $major_mime, $minor_mime ) = self::splitMime( $mime ); @@ -2204,6 +2243,73 @@ class Image return $revisions; } + + /** + * Select a page from a multipage document. Determines the page used for + * rendering thumbnails. + * + * @param $page Integer: page number, starting with 1 + */ + function selectPage( $page ) { + wfDebug( __METHOD__." selecting page $page \n" ); + $this->page = $page; + if ( ! $this->dataLoaded ) { + $this->load(); + } + if ( ! isset( $this->multiPageXML ) ) { + $this->initializeMultiPageXML(); + } + $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' ) { + $deja = new DjVuImage( $this->imagePath ); + $this->metadata = $deja->retrieveMetaData(); + $this->purgeMetadataCache(); + + # Update metadata in the database + $dbw =& wfGetDB( DB_MASTER ); + $dbw->update( 'image', + array( 'img_metadata' => $this->metadata ), + array( 'img_name' => $this->name ), + __METHOD__ + ); + } + wfSuppressWarnings(); + $this->multiPageXML = new SimpleXMLElement( $this->metadata ); + wfRestoreWarnings(); + } + + /** + * Returns 'true' if this image is a multipage document, e.g. a DJVU + * document. + * + * @return Bool + */ + function isMultipage() { + return ( $this->mime == 'image/vnd.djvu' ); + } + + /** + * Returns the number of pages of a multipage document, or NULL for + * documents which aren't multipage documents + */ + function pageCount() { + if ( ! $this->isMultipage() ) { + return null; + } + if ( ! isset( $this->multiPageXML ) ) { + $this->initializeMultiPageXML(); + } + return count( $this->multiPageXML->xpath( '//OBJECT' ) ); + } } //class diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php index a66b4d79..d182d527 100644 --- a/includes/ImageFunctions.php +++ b/includes/ImageFunctions.php @@ -1,223 +1,256 @@ -<?php
-
-/**
- * Returns the image directory of an image
- * The result is an absolute path.
- *
- * This function is called from thumb.php before Setup.php is included
- *
- * @param $fname String: file name of the image file.
- * @public
- */
-function wfImageDir( $fname ) {
- global $wgUploadDirectory, $wgHashedUploadDirectory;
-
- if (!$wgHashedUploadDirectory) { return $wgUploadDirectory; }
-
- $hash = md5( $fname );
- $dest = $wgUploadDirectory . '/' . $hash{0} . '/' . substr( $hash, 0, 2 );
-
- return $dest;
-}
-
-/**
- * Returns the image directory of an image's thubnail
- * The result is an absolute path.
- *
- * This function is called from thumb.php before Setup.php is included
- *
- * @param $fname String: file name of the original image file
- * @param $shared Boolean: (optional) use the shared upload directory (default: 'false').
- * @public
- */
-function wfImageThumbDir( $fname, $shared = false ) {
- $base = wfImageArchiveDir( $fname, 'thumb', $shared );
- if ( Image::isHashed( $shared ) ) {
- $dir = "$base/$fname";
- } else {
- $dir = $base;
- }
-
- return $dir;
-}
-
-/**
- * Old thumbnail directory, kept for conversion
- */
-function wfDeprecatedThumbDir( $thumbName , $subdir='thumb', $shared=false) {
- return wfImageArchiveDir( $thumbName, $subdir, $shared );
-}
-
-/**
- * Returns the image directory of an image's old version
- * The result is an absolute path.
- *
- * This function is called from thumb.php before Setup.php is included
- *
- * @param $fname String: file name of the thumbnail file, including file size prefix.
- * @param $subdir String: subdirectory of the image upload directory that should be used for storing the old version. Default is 'archive'.
- * @param $shared Boolean use the shared upload directory (only relevant for other functions which call this one). Default is 'false'.
- * @public
- */
-function wfImageArchiveDir( $fname , $subdir='archive', $shared=false ) {
- global $wgUploadDirectory, $wgHashedUploadDirectory;
- global $wgSharedUploadDirectory, $wgHashedSharedUploadDirectory;
- $dir = $shared ? $wgSharedUploadDirectory : $wgUploadDirectory;
- $hashdir = $shared ? $wgHashedSharedUploadDirectory : $wgHashedUploadDirectory;
- if (!$hashdir) { return $dir.'/'.$subdir; }
- $hash = md5( $fname );
-
- return $dir.'/'.$subdir.'/'.$hash[0].'/'.substr( $hash, 0, 2 );
-}
-
-
-/*
- * Return the hash path component of an image path (URL or filesystem),
- * e.g. "/3/3c/", or just "/" if hashing is not used.
- *
- * @param $dbkey The filesystem / database name of the file
- * @param $fromSharedDirectory Use the shared file repository? It may
- * use different hash settings from the local one.
- */
-function wfGetHashPath ( $dbkey, $fromSharedDirectory = false ) {
- if( Image::isHashed( $fromSharedDirectory ) ) {
- $hash = md5($dbkey);
- return '/' . $hash{0} . '/' . substr( $hash, 0, 2 ) . '/';
- } else {
- return '/';
- }
-}
-
-/**
- * Returns the image URL of an image's old version
- *
- * @param $name String: file name of the image file
- * @param $subdir String: (optional) subdirectory of the image upload directory that is used by the old version. Default is 'archive'
- * @public
- */
-function wfImageArchiveUrl( $name, $subdir='archive' ) {
- global $wgUploadPath, $wgHashedUploadDirectory;
-
- if ($wgHashedUploadDirectory) {
- $hash = md5( substr( $name, 15) );
- $url = $wgUploadPath.'/'.$subdir.'/' . $hash{0} . '/' .
- substr( $hash, 0, 2 ) . '/'.$name;
- } else {
- $url = $wgUploadPath.'/'.$subdir.'/'.$name;
- }
- return wfUrlencode($url);
-}
-
-/**
- * Return a rounded pixel equivalent for a labeled CSS/SVG length.
- * http://www.w3.org/TR/SVG11/coords.html#UnitIdentifiers
- *
- * @param $length String: CSS/SVG length.
- * @return Integer: length in pixels
- */
-function wfScaleSVGUnit( $length ) {
- static $unitLength = array(
- 'px' => 1.0,
- 'pt' => 1.25,
- 'pc' => 15.0,
- 'mm' => 3.543307,
- 'cm' => 35.43307,
- 'in' => 90.0,
- '' => 1.0, // "User units" pixels by default
- '%' => 2.0, // Fake it!
- );
- if( preg_match( '/^(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)$/', $length, $matches ) ) {
- $length = floatval( $matches[1] );
- $unit = $matches[2];
- return round( $length * $unitLength[$unit] );
- } else {
- // Assume pixels
- return round( floatval( $length ) );
- }
-}
-
-/**
- * Compatible with PHP getimagesize()
- * @todo support gzipped SVGZ
- * @todo check XML more carefully
- * @todo sensible defaults
- *
- * @param $filename String: full name of the file (passed to php fopen()).
- * @return array
- */
-function wfGetSVGsize( $filename ) {
- $width = 256;
- $height = 256;
-
- // Read a chunk of the file
- $f = fopen( $filename, "rt" );
- if( !$f ) return false;
- $chunk = fread( $f, 4096 );
- fclose( $f );
-
- // Uber-crappy hack! Run through a real XML parser.
- if( !preg_match( '/<svg\s*([^>]*)\s*>/s', $chunk, $matches ) ) {
- return false;
- }
- $tag = $matches[1];
- if( preg_match( '/\bwidth\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) {
- $width = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) );
- }
- if( preg_match( '/\bheight\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) {
- $height = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) );
- }
-
- return array( $width, $height, 'SVG',
- "width=\"$width\" height=\"$height\"" );
-}
-
-/**
- * Determine if an image exists on the 'bad image list'.
- *
- * @param $name String: the image name to check
- * @return bool
- */
-function wfIsBadImage( $name ) {
- static $titleList = false;
- wfProfileIn( __METHOD__ );
- $bad = false;
- if( wfRunHooks( 'BadImage', array( $name, &$bad ) ) ) {
- if( !$titleList ) {
- # Build the list now
- $titleList = array();
- $lines = explode( "\n", wfMsgForContent( 'bad_image_list' ) );
- foreach( $lines as $line ) {
- if( preg_match( '/^\*\s*\[\[:?(.*?)\]\]/i', $line, $matches ) ) {
- $title = Title::newFromText( $matches[1] );
- if( is_object( $title ) && $title->getNamespace() == NS_IMAGE )
- $titleList[ $title->getDBkey() ] = true;
- }
- }
- }
- wfProfileOut( __METHOD__ );
- return array_key_exists( $name, $titleList );
- } else {
- wfProfileOut( __METHOD__ );
- return $bad;
- }
-}
-
-/**
- * Calculate the largest thumbnail width for a given original file size
- * such that the thumbnail's height is at most $maxHeight.
- * @param $boxWidth Integer Width of the thumbnail box.
- * @param $boxHeight Integer Height of the thumbnail box.
- * @param $maxHeight Integer Maximum height expected for the thumbnail.
- * @return Integer.
- */
-function wfFitBoxWidth( $boxWidth, $boxHeight, $maxHeight ) {
- $idealWidth = $boxWidth * $maxHeight / $boxHeight;
- $roundedUp = ceil( $idealWidth );
- if( round( $roundedUp * $boxHeight / $boxWidth ) > $maxHeight )
- return floor( $idealWidth );
- else
- return $roundedUp;
-}
-
-
-?>
+<?php + +/** + * Returns the image directory of an image + * The result is an absolute path. + * + * This function is called from thumb.php before Setup.php is included + * + * @param $fname String: file name of the image file. + * @public + */ +function wfImageDir( $fname ) { + global $wgUploadDirectory, $wgHashedUploadDirectory; + + if (!$wgHashedUploadDirectory) { return $wgUploadDirectory; } + + $hash = md5( $fname ); + $dest = $wgUploadDirectory . '/' . $hash{0} . '/' . substr( $hash, 0, 2 ); + + return $dest; +} + +/** + * Returns the image directory of an image's thubnail + * The result is an absolute path. + * + * This function is called from thumb.php before Setup.php is included + * + * @param $fname String: file name of the original image file + * @param $shared Boolean: (optional) use the shared upload directory (default: 'false'). + * @public + */ +function wfImageThumbDir( $fname, $shared = false ) { + $base = wfImageArchiveDir( $fname, 'thumb', $shared ); + if ( Image::isHashed( $shared ) ) { + $dir = "$base/$fname"; + } else { + $dir = $base; + } + + return $dir; +} + +/** + * Old thumbnail directory, kept for conversion + */ +function wfDeprecatedThumbDir( $thumbName , $subdir='thumb', $shared=false) { + return wfImageArchiveDir( $thumbName, $subdir, $shared ); +} + +/** + * Returns the image directory of an image's old version + * The result is an absolute path. + * + * This function is called from thumb.php before Setup.php is included + * + * @param $fname String: file name of the thumbnail file, including file size prefix. + * @param $subdir String: subdirectory of the image upload directory that should be used for storing the old version. Default is 'archive'. + * @param $shared Boolean use the shared upload directory (only relevant for other functions which call this one). Default is 'false'. + * @public + */ +function wfImageArchiveDir( $fname , $subdir='archive', $shared=false ) { + global $wgUploadDirectory, $wgHashedUploadDirectory; + global $wgSharedUploadDirectory, $wgHashedSharedUploadDirectory; + $dir = $shared ? $wgSharedUploadDirectory : $wgUploadDirectory; + $hashdir = $shared ? $wgHashedSharedUploadDirectory : $wgHashedUploadDirectory; + if (!$hashdir) { return $dir.'/'.$subdir; } + $hash = md5( $fname ); + + return $dir.'/'.$subdir.'/'.$hash[0].'/'.substr( $hash, 0, 2 ); +} + + +/* + * Return the hash path component of an image path (URL or filesystem), + * e.g. "/3/3c/", or just "/" if hashing is not used. + * + * @param $dbkey The filesystem / database name of the file + * @param $fromSharedDirectory Use the shared file repository? It may + * use different hash settings from the local one. + */ +function wfGetHashPath ( $dbkey, $fromSharedDirectory = false ) { + if( Image::isHashed( $fromSharedDirectory ) ) { + $hash = md5($dbkey); + return '/' . $hash{0} . '/' . substr( $hash, 0, 2 ) . '/'; + } else { + return '/'; + } +} + +/** + * Returns the image URL of an image's old version + * + * @param $name String: file name of the image file + * @param $subdir String: (optional) subdirectory of the image upload directory that is used by the old version. Default is 'archive' + * @public + */ +function wfImageArchiveUrl( $name, $subdir='archive' ) { + global $wgUploadPath, $wgHashedUploadDirectory; + + if ($wgHashedUploadDirectory) { + $hash = md5( substr( $name, 15) ); + $url = $wgUploadPath.'/'.$subdir.'/' . $hash{0} . '/' . + substr( $hash, 0, 2 ) . '/'.$name; + } else { + $url = $wgUploadPath.'/'.$subdir.'/'.$name; + } + return wfUrlencode($url); +} + +/** + * Return a rounded pixel equivalent for a labeled CSS/SVG length. + * http://www.w3.org/TR/SVG11/coords.html#UnitIdentifiers + * + * @param $length String: CSS/SVG length. + * @return Integer: length in pixels + */ +function wfScaleSVGUnit( $length ) { + static $unitLength = array( + 'px' => 1.0, + 'pt' => 1.25, + 'pc' => 15.0, + 'mm' => 3.543307, + 'cm' => 35.43307, + 'in' => 90.0, + '' => 1.0, // "User units" pixels by default + '%' => 2.0, // Fake it! + ); + if( preg_match( '/^(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)$/', $length, $matches ) ) { + $length = floatval( $matches[1] ); + $unit = $matches[2]; + return round( $length * $unitLength[$unit] ); + } else { + // Assume pixels + return round( floatval( $length ) ); + } +} + +/** + * Compatible with PHP getimagesize() + * @todo support gzipped SVGZ + * @todo check XML more carefully + * @todo sensible defaults + * + * @param $filename String: full name of the file (passed to php fopen()). + * @return array + */ +function wfGetSVGsize( $filename ) { + $width = 256; + $height = 256; + + // Read a chunk of the file + $f = fopen( $filename, "rt" ); + if( !$f ) return false; + $chunk = fread( $f, 4096 ); + fclose( $f ); + + // Uber-crappy hack! Run through a real XML parser. + if( !preg_match( '/<svg\s*([^>]*)\s*>/s', $chunk, $matches ) ) { + return false; + } + $tag = $matches[1]; + if( preg_match( '/\bwidth\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) { + $width = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) ); + } + if( preg_match( '/\bheight\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) { + $height = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) ); + } + + return array( $width, $height, 'SVG', + "width=\"$width\" height=\"$height\"" ); +} + +/** + * Determine if an image exists on the 'bad image list'. + * + * The format of MediaWiki:Bad_image_list is as follows: + * * Only list items (lines starting with "*") are considered + * * The first link on a line must be a link to a bad image + * * Any subsequent links on the same line are considered to be exceptions, + * i.e. articles where the image may occur inline. + * + * @param string $name the image name to check + * @param Title $contextTitle The page on which the image occurs, if known + * @return bool + */ +function wfIsBadImage( $name, $contextTitle = false ) { + static $badImages = false; + wfProfileIn( __METHOD__ ); + + # Run the extension hook + $bad = false; + if( !wfRunHooks( 'BadImage', array( $name, &$bad ) ) ) { + wfProfileOut( __METHOD__ ); + return $bad; + } + + if( !$badImages ) { + # Build the list now + $badImages = array(); + $lines = explode( "\n", wfMsgForContent( 'bad_image_list' ) ); + foreach( $lines as $line ) { + # List items only + if ( substr( $line, 0, 1 ) !== '*' ) { + continue; + } + + # Find all links + if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) { + continue; + } + + $exceptions = array(); + $imageDBkey = false; + foreach ( $m[1] as $i => $titleText ) { + $title = Title::newFromText( $titleText ); + if ( !is_null( $title ) ) { + if ( $i == 0 ) { + $imageDBkey = $title->getDBkey(); + } else { + $exceptions[$title->getPrefixedDBkey()] = true; + } + } + } + + if ( $imageDBkey !== false ) { + $badImages[$imageDBkey] = $exceptions; + } + } + } + + $contextKey = $contextTitle ? $contextTitle->getPrefixedDBkey() : false; + $bad = isset( $badImages[$name] ) && !isset( $badImages[$name][$contextKey] ); + wfProfileOut( __METHOD__ ); + return $bad; +} + +/** + * Calculate the largest thumbnail width for a given original file size + * such that the thumbnail's height is at most $maxHeight. + * @param $boxWidth Integer Width of the thumbnail box. + * @param $boxHeight Integer Height of the thumbnail box. + * @param $maxHeight Integer Maximum height expected for the thumbnail. + * @return Integer. + */ +function wfFitBoxWidth( $boxWidth, $boxHeight, $maxHeight ) { + $idealWidth = $boxWidth * $maxHeight / $boxHeight; + $roundedUp = ceil( $idealWidth ); + if( round( $roundedUp * $boxHeight / $boxWidth ) > $maxHeight ) + return floor( $idealWidth ); + else + return $roundedUp; +} + + +?> diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php index 0935ac30..7ff456b6 100644 --- a/includes/ImageGallery.php +++ b/includes/ImageGallery.php @@ -55,7 +55,7 @@ class ImageGallery * * @param $skin Skin object */ - function useSkin( &$skin ) { + function useSkin( $skin ) { $this->mSkin =& $skin; } @@ -82,6 +82,7 @@ class ImageGallery */ function add( $image, $html='' ) { $this->mImages[] = array( &$image, $html ); + wfDebug( "ImageGallery::add " . $image->getName() . "\n" ); } /** @@ -135,7 +136,7 @@ class ImageGallery function toHTML() { global $wgLang, $wgIgnoreImageErrors, $wgGenerateThumbnailOnParse; - $sk =& $this->getSkin(); + $sk = $this->getSkin(); $s = '<table class="gallery" cellspacing="0" cellpadding="0">'; if( $this->mCaption ) @@ -157,8 +158,7 @@ class ImageGallery # The image is blacklisted, just show it as a text link. $thumbhtml = '<div style="height: 152px;">' . $sk->makeKnownLinkObj( $nt, htmlspecialchars( $nt->getText() ) ) . '</div>'; - } - else if( !( $thumb = $img->getThumbnail( 120, 120, $wgGenerateThumbnailOnParse ) ) ) { + } else if( !( $thumb = $img->getThumbnail( 120, 120, $wgGenerateThumbnailOnParse ) ) ) { # Error generating thumbnail. $thumbhtml = '<div style="height: 152px;">' . htmlspecialchars( $img->getLastError() ) . '</div>'; diff --git a/includes/ImagePage.php b/includes/ImagePage.php index dac9602d..908dd5cc 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -9,8 +9,6 @@ if( !defined( 'MEDIAWIKI' ) ) die( 1 ); -require_once( 'Image.php' ); - /** * Special handling for image description pages * @package MediaWiki @@ -165,7 +163,7 @@ class ImagePage extends Article { } function openShowImage() { - global $wgOut, $wgUser, $wgImageLimits, $wgRequest; + global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang; global $wgUseImageResize, $wgGenerateThumbnailOnParse; $full_url = $this->img->getURL(); @@ -187,6 +185,12 @@ class ImagePage extends Article { if ( $this->img->exists() ) { # image + $page = $wgRequest->getIntOrNull( 'page' ); + if ( ! is_null( $page ) ) { + $this->img->selectPage( $page ); + } else { + $page = 1; + } $width = $this->img->getWidth(); $height = $this->img->getHeight(); $showLink = false; @@ -236,9 +240,50 @@ class ImagePage extends Article { $url = $this->img->getViewURL(); $showLink = true; } + + 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( $wgRequest->getVal( 'image' ) ).'" />' . $anchorclose . '</div>' ); + + if ( $this->img->isMultipage() ) { + $count = $this->img->pageCount(); + + 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' ); + } 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' ); + } else { + $thumb2 = ''; + } + + $select = '<form name="pageselector" action="' . $this->img->getEscapeLocalUrl( '' ) . '" method="GET" onchange="document.pageselector.submit();">' ; + $select .= $wgOut->parse( wfMsg( 'imgmultigotopre' ), false ) . + ' <select id="pageselector" name="page">'; + for ( $i=1; $i <= $count; $i++ ) { + $select .= Xml::option( $wgLang->formatNum( $i ), $i, + $i == $page ); + } + $select .= '</select>' . $wgOut->parse( wfMsg( 'imgmultigotopost' ), false ) . + '<input type="submit" value="' . + htmlspecialchars( wfMsg( 'imgmultigo' ) ) . '"></form>'; + + $wgOut->addHTML( '</td><td><div class="multipageimagenavbox">' . + "$select<hr />$thumb1\n$thumb2<br clear=\"all\" /></div></td></tr></table>" ); + } } else { #if direct link is allowed but it's not a renderable image, show an icon. if ($this->img->isSafeFile()) { @@ -312,10 +357,7 @@ END $wgOut->addHTML($sharedtext); if ($wgRepositoryBaseUrl && $wgFetchCommonsDescriptions) { - require_once("HttpFunctions.php"); - $ur = ini_set('allow_url_fopen', true); - $text = wfGetHTTP($url . '?action=render'); - ini_set('allow_url_fopen', $ur); + $text = Http::get($url . '?action=render'); if ($text) $this->mExtraDescription = $text; } @@ -373,7 +415,7 @@ END $line = $this->img->nextHistoryLine(); if ( $line ) { - $list =& new ImageHistoryList( $sk ); + $list = new ImageHistoryList( $sk ); $s = $list->beginImageHistoryList() . $list->imageHistoryLine( true, wfTimestamp(TS_MW, $line->img_timestamp), $this->mTitle->getDBkey(), $line->img_user, @@ -435,13 +477,14 @@ END global $wgUser, $wgOut, $wgRequest; $confirm = $wgRequest->wasPosted(); + $reason = $wgRequest->getVal( 'wpReason' ); $image = $wgRequest->getVal( 'image' ); $oldimage = $wgRequest->getVal( 'oldimage' ); # Only sysops can delete images. Previously ordinary users could delete # old revisions, but this is no longer the case. if ( !$wgUser->isAllowed('delete') ) { - $wgOut->sysopRequired(); + $wgOut->permissionRequired( 'delete' ); return; } if ( $wgUser->isBlocked() ) { @@ -465,7 +508,7 @@ END # Deleting old images doesn't require confirmation if ( !is_null( $oldimage ) || $confirm ) { if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $oldimage ) ) { - $this->doDelete(); + $this->doDelete( $reason ); } else { $wgOut->showFatalError( wfMsg( 'sessionfailure' ) ); } @@ -482,13 +525,16 @@ END return $this->confirmDelete( $q, $wgRequest->getText( 'wpReason' ) ); } - function doDelete() { + /* + * Delete an image. + * @param $reason User provided reason for deletion. + */ + function doDelete( $reason ) { global $wgOut, $wgRequest, $wgUseSquid; global $wgPostCommitUpdateList; $fname = 'ImagePage::doDelete'; - $reason = $wgRequest->getVal( 'wpReason' ); $oldimage = $wgRequest->getVal( 'oldimage' ); $dbw =& wfGetDB( DB_MASTER ); @@ -576,7 +622,7 @@ END return; } if ( ! $this->mTitle->userCanEdit() ) { - $wgOut->sysopRequired(); + $wgOut->readOnlyPage( $this->getContent(), true ); return; } if ( $wgUser->isBlocked() ) { diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php index e0f0f6fd..061f1b19 100644 --- a/includes/LinkBatch.php +++ b/includes/LinkBatch.php @@ -66,7 +66,7 @@ class LinkBatch { */ function execute() { $linkCache =& LinkCache::singleton(); - $this->executeInto( $linkCache ); + return $this->executeInto( $linkCache ); } /** diff --git a/includes/LinkCache.php b/includes/LinkCache.php index 451b3f0c..8e56225b 100644 --- a/includes/LinkCache.php +++ b/includes/LinkCache.php @@ -21,7 +21,7 @@ class LinkCache { /** * Get an instance of this class */ - function &singleton() { + static function &singleton() { static $instance; if ( !isset( $instance ) ) { $instance = new LinkCache; @@ -37,8 +37,7 @@ class LinkCache { } /* private */ function getKey( $title ) { - global $wgDBname; - return $wgDBname.':lc:title:'.$title; + return wfMemcKey( 'lc', 'title', $title ); } /** diff --git a/includes/Linker.php b/includes/Linker.php index 4a0eafbd..d34971ff 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -456,11 +456,16 @@ class Linker { /** @todo document */ function makeImageLinkObj( $nt, $label, $alt, $align = '', $width = false, $height = false, $framed = false, - $thumb = false, $manual_thumb = '' ) + $thumb = false, $manual_thumb = '', $page = null ) { global $wgContLang, $wgUser, $wgThumbLimits, $wgGenerateThumbnailOnParse; $img = new Image( $nt ); + + if ( ! is_null( $page ) ) { + $img->selectPage( $page ); + } + if ( !$img->allowInlineDisplay() && $img->exists() ) { return $this->makeKnownLinkObj( $nt ); } @@ -468,7 +473,7 @@ class Linker { $url = $img->getViewURL(); $error = $prefix = $postfix = ''; - wfDebug( "makeImageLinkObj: '$width'x'$height'\n" ); + wfDebug( "makeImageLinkObj: '$width'x'$height', \"$label\"\n" ); if ( 'center' == $align ) { @@ -564,7 +569,6 @@ class Linker { */ function makeThumbLinkObj( $img, $label = '', $alt, $align = 'right', $boxwidth = 180, $boxheight=false, $framed=false , $manual_thumb = "" ) { global $wgStylePath, $wgContLang, $wgGenerateThumbnailOnParse; - $url = $img->getViewURL(); $thumbUrl = ''; $error = ''; @@ -583,7 +587,7 @@ class Linker { // Use image dimensions, don't scale $boxwidth = $width; $boxheight = $height; - $thumbUrl = $url; + $thumbUrl = $img->getViewURL(); } else { if ( $boxheight === false ) $boxheight = -1; @@ -626,7 +630,7 @@ class Linker { $s = "<div class=\"thumb t{$align}\"><div style=\"width:{$oboxwidth}px;\">"; if( $thumbUrl == '' ) { // Couldn't generate thumbnail? Scale the image client-side. - $thumbUrl = $url; + $thumbUrl = $img->getViewURL(); } if ( $error ) { $s .= htmlspecialchars( $error ); @@ -1081,7 +1085,7 @@ class Linker { * * @static */ - function splitTrail( $trail ) { + static function splitTrail( $trail ) { static $regex = false; if ( $regex === false ) { global $wgContLang; diff --git a/includes/LoadBalancer.php b/includes/LoadBalancer.php index f985a7b4..3e81aea9 100644 --- a/includes/LoadBalancer.php +++ b/includes/LoadBalancer.php @@ -4,26 +4,6 @@ * @package MediaWiki */ -/** - * Depends on the database object - */ -require_once( 'Database.php' ); - -# Valid database indexes -# Operation-based indexes -define( 'DB_SLAVE', -1 ); # Read from the slave (or only server) -define( 'DB_MASTER', -2 ); # Write to master (or only server) -define( 'DB_LAST', -3 ); # Whatever database was used last - -# Obsolete aliases -define( 'DB_READ', -1 ); -define( 'DB_WRITE', -2 ); - - -# Scale polling time so that under overload conditions, the database server -# receives a SHOW STATUS query at an average interval of this many microseconds -define( 'AVG_STATUS_POLL', 2000 ); - /** * Database load balancing object @@ -38,26 +18,13 @@ class LoadBalancer { /* private */ var $mWaitForFile, $mWaitForPos, $mWaitTimeout; /* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error'; - function LoadBalancer() - { - $this->mServers = array(); - $this->mConnections = array(); - $this->mFailFunction = false; - $this->mReadIndex = -1; - $this->mForce = -1; - $this->mLastIndex = -1; - $this->mErrorConnection = false; - $this->mAllowLag = false; - } - - function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 ) - { - $lb = new LoadBalancer; - $lb->initialise( $servers, $failFunction, $waitTimeout ); - return $lb; - } + /** + * Scale polling time so that under overload conditions, the database server + * receives a SHOW STATUS query at an average interval of this many microseconds + */ + const AVG_STATUS_POLL = 2000; - function initialise( $servers, $failFunction = false, $waitTimeout = 10 ) + function LoadBalancer( $servers, $failFunction = false, $waitTimeout = 10, $waitForMasterNow = false ) { $this->mServers = $servers; $this->mFailFunction = $failFunction; @@ -71,6 +38,8 @@ class LoadBalancer { $this->mWaitForPos = false; $this->mWaitTimeout = $waitTimeout; $this->mLaggedSlaveMode = false; + $this->mErrorConnection = false; + $this->mAllowLag = false; foreach( $servers as $i => $server ) { $this->mLoads[$i] = $server['load']; @@ -83,6 +52,14 @@ class LoadBalancer { } } } + if ( $waitForMasterNow ) { + $this->loadMasterPos(); + } + } + + static function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 ) + { + return new LoadBalancer( $servers, $failFunction, $waitTimeout ); } /** @@ -180,7 +157,7 @@ class LoadBalancer { $i = $this->getRandomNonLagged( $loads ); if ( $i === false && count( $loads ) != 0 ) { # All slaves lagged. Switch to read-only mode - $wgReadOnly = wfMsgNoDB( 'readonly_lag' ); + $wgReadOnly = wfMsgNoDBForContent( 'readonly_lag' ); $i = $this->pickRandom( $loads ); } } @@ -201,7 +178,7 @@ class LoadBalancer { # Too much load, back off and wait for a while. # The sleep time is scaled by the number of threads connected, # to produce a roughly constant global poll rate. - $sleepTime = AVG_STATUS_POLL * $status['Threads_connected']; + $sleepTime = self::AVG_STATUS_POLL * $status['Threads_connected']; # If we reach the timeout and exit the loop, don't use it $i = false; @@ -442,9 +419,6 @@ class LoadBalancer { extract( $server ); # Get class for this database type $class = 'Database' . ucfirst( $type ); - if ( !class_exists( $class ) ) { - require_once( "$class.php" ); - } # Create object $db = new $class( $host, $user, $password, $dbname, 1, $flags ); @@ -625,21 +599,24 @@ class LoadBalancer { * Results are cached for a short time in memcached */ function getLagTimes() { - global $wgDBname; - + wfProfileIn( __METHOD__ ); $expiry = 5; $requestRate = 10; global $wgMemc; - $times = $wgMemc->get( "$wgDBname:lag_times" ); + $times = $wgMemc->get( wfMemcKey( 'lag_times' ) ); if ( $times ) { # Randomly recache with probability rising over $expiry $elapsed = time() - $times['timestamp']; $chance = max( 0, ( $expiry - $elapsed ) * $requestRate ); if ( mt_rand( 0, $chance ) != 0 ) { unset( $times['timestamp'] ); + wfProfileOut( __METHOD__ ); return $times; } + wfIncrStats( 'lag_cache_miss_expired' ); + } else { + wfIncrStats( 'lag_cache_miss_absent' ); } # Cache key missing or expired @@ -655,10 +632,11 @@ class LoadBalancer { # Add a timestamp key so we know when it was cached $times['timestamp'] = time(); - $wgMemc->set( "$wgDBname:lag_times", $times, $expiry ); + $wgMemc->set( wfMemcKey( 'lag_times' ), $times, $expiry ); # But don't give the timestamp to the caller unset($times['timestamp']); + wfProfileOut( __METHOD__ ); return $times; } } diff --git a/includes/LogPage.php b/includes/LogPage.php index f588105f..954b178f 100644 --- a/includes/LogPage.php +++ b/includes/LogPage.php @@ -207,13 +207,13 @@ class LogPage { * @param string $comment Description associated * @param array $params Parameters passed later to wfMsg.* functions */ - function addEntry( $action, &$target, $comment, $params = array() ) { + function addEntry( $action, $target, $comment, $params = array() ) { if ( !is_array( $params ) ) { $params = array( $params ); } $this->action = $action; - $this->target =& $target; + $this->target = $target; $this->comment = $comment; $this->params = LogPage::makeParamBlob( $params ); diff --git a/includes/MagicWord.php b/includes/MagicWord.php index c80d2583..68cbe345 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -6,170 +6,21 @@ */ /** - * private - */ -$wgMagicFound = false; - -/** Actual keyword to be used is set in Language.php */ - -$magicWords = array( - 'MAG_REDIRECT', - 'MAG_NOTOC', - 'MAG_START', - 'MAG_CURRENTMONTH', - 'MAG_CURRENTMONTHNAME', - 'MAG_CURRENTMONTHNAMEGEN', - 'MAG_CURRENTMONTHABBREV', - 'MAG_CURRENTDAY', - 'MAG_CURRENTDAY2', - 'MAG_CURRENTDAYNAME', - 'MAG_CURRENTYEAR', - 'MAG_CURRENTTIME', - 'MAG_NUMBEROFARTICLES', - 'MAG_SUBST', - 'MAG_MSG', - 'MAG_MSGNW', - 'MAG_NOEDITSECTION', - 'MAG_END', - 'MAG_IMG_THUMBNAIL', - 'MAG_IMG_RIGHT', - 'MAG_IMG_LEFT', - 'MAG_IMG_NONE', - 'MAG_IMG_WIDTH', - 'MAG_IMG_CENTER', - 'MAG_INT', - 'MAG_FORCETOC', - 'MAG_SITENAME', - 'MAG_NS', - 'MAG_LOCALURL', - 'MAG_LOCALURLE', - 'MAG_SERVER', - 'MAG_IMG_FRAMED', - 'MAG_PAGENAME', - 'MAG_PAGENAMEE', - 'MAG_NAMESPACE', - 'MAG_NAMESPACEE', - 'MAG_TOC', - 'MAG_GRAMMAR', - 'MAG_NOTITLECONVERT', - 'MAG_NOCONTENTCONVERT', - 'MAG_CURRENTWEEK', - 'MAG_CURRENTDOW', - 'MAG_REVISIONID', - 'MAG_SCRIPTPATH', - 'MAG_SERVERNAME', - 'MAG_NUMBEROFFILES', - 'MAG_IMG_MANUALTHUMB', - 'MAG_PLURAL', - 'MAG_FULLURL', - 'MAG_FULLURLE', - 'MAG_LCFIRST', - 'MAG_UCFIRST', - 'MAG_LC', - 'MAG_UC', - 'MAG_FULLPAGENAME', - 'MAG_FULLPAGENAMEE', - 'MAG_RAW', - 'MAG_SUBPAGENAME', - 'MAG_SUBPAGENAMEE', - 'MAG_DISPLAYTITLE', - 'MAG_TALKSPACE', - 'MAG_TALKSPACEE', - 'MAG_SUBJECTSPACE', - 'MAG_SUBJECTSPACEE', - 'MAG_TALKPAGENAME', - 'MAG_TALKPAGENAMEE', - 'MAG_SUBJECTPAGENAME', - 'MAG_SUBJECTPAGENAMEE', - 'MAG_NUMBEROFUSERS', - 'MAG_RAWSUFFIX', - 'MAG_NEWSECTIONLINK', - 'MAG_NUMBEROFPAGES', - 'MAG_CURRENTVERSION', - 'MAG_BASEPAGENAME', - 'MAG_BASEPAGENAMEE', - 'MAG_URLENCODE', - 'MAG_CURRENTTIMESTAMP', - 'MAG_DIRECTIONMARK', - 'MAG_LANGUAGE', - 'MAG_CONTENTLANGUAGE', - 'MAG_PAGESINNAMESPACE', - 'MAG_NOGALLERY', - 'MAG_NUMBEROFADMINS', - 'MAG_FORMATNUM', -); -if ( ! defined( 'MEDIAWIKI_INSTALL' ) ) - wfRunHooks( 'MagicWordMagicWords', array( &$magicWords ) ); - -for ( $i = 0; $i < count( $magicWords ); ++$i ) - define( $magicWords[$i], $i ); - -$wgVariableIDs = array( - MAG_CURRENTMONTH, - MAG_CURRENTMONTHNAME, - MAG_CURRENTMONTHNAMEGEN, - MAG_CURRENTMONTHABBREV, - MAG_CURRENTDAY, - MAG_CURRENTDAY2, - MAG_CURRENTDAYNAME, - MAG_CURRENTYEAR, - MAG_CURRENTTIME, - MAG_NUMBEROFARTICLES, - MAG_NUMBEROFFILES, - MAG_SITENAME, - MAG_SERVER, - MAG_SERVERNAME, - MAG_SCRIPTPATH, - MAG_PAGENAME, - MAG_PAGENAMEE, - MAG_FULLPAGENAME, - MAG_FULLPAGENAMEE, - MAG_NAMESPACE, - MAG_NAMESPACEE, - MAG_CURRENTWEEK, - MAG_CURRENTDOW, - MAG_REVISIONID, - MAG_SUBPAGENAME, - MAG_SUBPAGENAMEE, - MAG_DISPLAYTITLE, - MAG_TALKSPACE, - MAG_TALKSPACEE, - MAG_SUBJECTSPACE, - MAG_SUBJECTSPACEE, - MAG_TALKPAGENAME, - MAG_TALKPAGENAMEE, - MAG_SUBJECTPAGENAME, - MAG_SUBJECTPAGENAMEE, - MAG_NUMBEROFUSERS, - MAG_RAWSUFFIX, - MAG_NEWSECTIONLINK, - MAG_NUMBEROFPAGES, - MAG_CURRENTVERSION, - MAG_BASEPAGENAME, - MAG_BASEPAGENAMEE, - MAG_URLENCODE, - MAG_CURRENTTIMESTAMP, - MAG_DIRECTIONMARK, - MAG_LANGUAGE, - MAG_CONTENTLANGUAGE, - MAG_PAGESINNAMESPACE, - MAG_NUMBEROFADMINS, -); -if ( ! defined( 'MEDIAWIKI_INSTALL' ) ) - wfRunHooks( 'MagicWordwgVariableIDs', array( &$wgVariableIDs ) ); - -/** * This class encapsulates "magic words" such as #redirect, __NOTOC__, etc. * Usage: - * if (MagicWord::get( MAG_REDIRECT )->match( $text ) ) + * if (MagicWord::get( 'redirect' )->match( $text ) ) * * Possible future improvements: * * Simultaneous searching for a number of magic words - * * $wgMagicWords in shared memory + * * MagicWord::$mObjects in shared memory * * Please avoid reading the data out of one of these objects and then writing * special case code. If possible, add another match()-like function here. * + * To add magic words in an extension, use the LanguageGetMagic hook. For + * magic words which are also Parser variables, add a MagicWordwgVariableIDs + * hook. Use string keys. + * * @package MediaWiki */ class MagicWord { @@ -178,7 +29,82 @@ class MagicWord { */ var $mId, $mSynonyms, $mCaseSensitive, $mRegex; var $mRegexStart, $mBaseRegex, $mVariableRegex; - var $mModified; + var $mModified, $mFound; + + static public $mVariableIDsInitialised = false; + static public $mVariableIDs = array( + 'currentmonth', + 'currentmonthname', + 'currentmonthnamegen', + 'currentmonthabbrev', + 'currentday', + 'currentday2', + 'currentdayname', + 'currentyear', + 'currenttime', + 'currenthour', + 'localmonth', + 'localmonthname', + 'localmonthnamegen', + 'localmonthabbrev', + 'localday', + 'localday2', + 'localdayname', + 'localyear', + 'localtime', + 'localhour', + 'numberofarticles', + 'numberoffiles', + 'sitename', + 'server', + 'servername', + 'scriptpath', + 'pagename', + 'pagenamee', + 'fullpagename', + 'fullpagenamee', + 'namespace', + 'namespacee', + 'currentweek', + 'currentdow', + 'localweek', + 'localdow', + 'revisionid', + 'revisionday', + 'revisionday2', + 'revisionmonth', + 'revisionyear', + 'revisiontimestamp', + 'subpagename', + 'subpagenamee', + 'displaytitle', + 'talkspace', + 'talkspacee', + 'subjectspace', + 'subjectspacee', + 'talkpagename', + 'talkpagenamee', + 'subjectpagename', + 'subjectpagenamee', + 'numberofusers', + 'rawsuffix', + 'newsectionlink', + 'numberofpages', + 'currentversion', + 'basepagename', + 'basepagenamee', + 'urlencode', + 'currenttimestamp', + 'localtimestamp', + 'directionmark', + 'language', + 'contentlanguage', + 'pagesinnamespace', + 'numberofadmins', + ); + + static public $mObjects = array(); + /**#@-*/ function MagicWord($id = 0, $syn = '', $cs = false) { @@ -196,18 +122,32 @@ class MagicWord { * Factory: creates an object representing an ID * @static */ - function &get( $id ) { - global $wgMagicWords; - - if ( !is_array( $wgMagicWords ) ) { - throw new MWException( "Incorrect initialisation order, \$wgMagicWords does not exist\n" ); - } - if (!array_key_exists( $id, $wgMagicWords ) ) { + static function &get( $id ) { + if (!array_key_exists( $id, self::$mObjects ) ) { $mw = new MagicWord(); $mw->load( $id ); - $wgMagicWords[$id] = $mw; + self::$mObjects[$id] = $mw; + } + return self::$mObjects[$id]; + } + + /** + * Get an array of parser variable IDs + */ + static function getVariableIDs() { + if ( !self::$mVariableIDsInitialised ) { + # Deprecated constant definition hook, available for extensions that need it + $magicWords = array(); + wfRunHooks( 'MagicWordMagicWords', array( &$magicWords ) ); + foreach ( $magicWords as $word ) { + define( $word, $word ); + } + + # Get variable IDs + wfRunHooks( 'MagicWordwgVariableIDs', array( &self::$mVariableIDs ) ); + self::$mVariableIDsInitialised = true; } - return $wgMagicWords[$id]; + return self::$mVariableIDs; } # Initialises this object with an ID @@ -215,6 +155,11 @@ class MagicWord { global $wgContLang; $this->mId = $id; $wgContLang->getMagic( $this ); + if ( !$this->mSynonyms ) { + $this->mSynonyms = array( 'dkjsagfjsgashfajsh' ); + #throw new MWException( "Error: invalid magic word '$id'" ); + wfDebugLog( 'exception', "Error: invalid magic word '$id'\n" ); + } } /** @@ -233,7 +178,7 @@ class MagicWord { $escSyn[] = preg_quote( $synonym, '/' ); $this->mBaseRegex = implode( '|', $escSyn ); - $case = $this->mCaseSensitive ? '' : 'i'; + $case = $this->mCaseSensitive ? '' : 'iu'; $this->mRegex = "/{$this->mBaseRegex}/{$case}"; $this->mRegexStart = "/^(?:{$this->mBaseRegex})/{$case}"; $this->mVariableRegex = str_replace( "\\$1", "(.*?)", $this->mRegex ); @@ -260,7 +205,7 @@ class MagicWord { if ( $this->mRegex === '' ) $this->initRegex(); - return $this->mCaseSensitive ? '' : 'i'; + return $this->mCaseSensitive ? '' : 'iu'; } /** @@ -310,14 +255,16 @@ class MagicWord { $matchcount = preg_match( $this->getVariableStartToEndRegex(), $text, $matches ); if ( $matchcount == 0 ) { return NULL; - } elseif ( count($matches) == 1 ) { - return $matches[0]; } else { # multiple matched parts (variable match); some will be empty because of # synonyms. The variable will be the second non-empty one so remove any # blank elements and re-sort the indices. + # See also bug 6526 + $matches = array_values(array_filter($matches)); - return $matches[1]; + + if ( count($matches) == 1 ) { return $matches[0]; } + else { return $matches[1]; } } } @@ -327,19 +274,25 @@ class MagicWord { * input string, removing all instances of the word */ function matchAndRemove( &$text ) { - global $wgMagicFound; - $wgMagicFound = false; - $text = preg_replace_callback( $this->getRegex(), 'pregRemoveAndRecord', $text ); - return $wgMagicFound; + $this->mFound = false; + $text = preg_replace_callback( $this->getRegex(), array( &$this, 'pregRemoveAndRecord' ), $text ); + return $this->mFound; } function matchStartAndRemove( &$text ) { - global $wgMagicFound; - $wgMagicFound = false; - $text = preg_replace_callback( $this->getRegexStart(), 'pregRemoveAndRecord', $text ); - return $wgMagicFound; + $this->mFound = false; + $text = preg_replace_callback( $this->getRegexStart(), array( &$this, 'pregRemoveAndRecord' ), $text ); + return $this->mFound; } + /** + * Used in matchAndRemove() + * @private + **/ + function pregRemoveAndRecord( $match ) { + $this->mFound = true; + return ''; + } /** * Replaces the word with something else @@ -425,8 +378,9 @@ class MagicWord { * lookup in a list of magic words */ function addToArray( &$array, $value ) { + global $wgContLang; foreach ( $this->mSynonyms as $syn ) { - $array[$syn] = $value; + $array[$wgContLang->lc($syn)] = $value; } } @@ -435,14 +389,4 @@ class MagicWord { } } -/** - * Used in matchAndRemove() - * @private - **/ -function pregRemoveAndRecord( $match ) { - global $wgMagicFound; - $wgMagicFound = true; - return ''; -} - ?> diff --git a/includes/Math.php b/includes/Math.php index f9d6a605..a8b33984 100644 --- a/includes/Math.php +++ b/includes/Math.php @@ -259,7 +259,7 @@ class MathRenderer { return $path; } - function renderMath( $tex ) { + public static function renderMath( $tex ) { global $wgUser; $math = new MathRenderer( $tex ); $math->setOutputMode( $wgUser->getOption('math')); diff --git a/includes/MemcachedSessions.php b/includes/MemcachedSessions.php index af49109c..e2dc52ca 100644 --- a/includes/MemcachedSessions.php +++ b/includes/MemcachedSessions.php @@ -13,8 +13,7 @@ * @todo document */ function memsess_key( $id ) { - global $wgDBname; - return "$wgDBname:session:$id"; + return wfMemcKey( 'session', $id ); } /** diff --git a/includes/MessageCache.php b/includes/MessageCache.php index c8b7124c..9cab222b 100644 --- a/includes/MessageCache.php +++ b/includes/MessageCache.php @@ -36,10 +36,6 @@ class MessageCache { $this->mMemcKey = $memcPrefix.':messages'; $this->mKeys = false; # initialised on demand $this->mInitialised = true; - - wfProfileIn( __METHOD__.'-parseropt' ); - $this->mParserOptions = new ParserOptions( $u=NULL ); - wfProfileOut( __METHOD__.'-parseropt' ); $this->mParser = null; # When we first get asked for a message, @@ -51,18 +47,25 @@ class MessageCache { wfProfileOut( __METHOD__ ); } + function getParserOptions() { + if ( !$this->mParserOptions ) { + $this->mParserOptions = new ParserOptions; + } + return $this->mParserOptions; + } + /** * Try to load the cache from a local file */ function loadFromLocal( $hash ) { - global $wgLocalMessageCache, $wgDBname; + global $wgLocalMessageCache; $this->mCache = false; if ( $wgLocalMessageCache === false ) { return; } - $filename = "$wgLocalMessageCache/messages-$wgDBname"; + $filename = "$wgLocalMessageCache/messages-" . wfWikiID(); wfSuppressWarnings(); $file = fopen( $filename, 'r' ); @@ -75,7 +78,7 @@ class MessageCache { $localHash = fread( $file, 32 ); if ( $hash == $localHash ) { // All good, get the rest of it - $serialized = fread( $file, 1000000 ); + $serialized = fread( $file, 10000000 ); $this->mCache = unserialize( $serialized ); } fclose( $file ); @@ -85,13 +88,13 @@ class MessageCache { * Save the cache to a local file */ function saveToLocal( $serialized, $hash ) { - global $wgLocalMessageCache, $wgDBname; + global $wgLocalMessageCache; if ( $wgLocalMessageCache === false ) { return; } - $filename = "$wgLocalMessageCache/messages-$wgDBname"; + $filename = "$wgLocalMessageCache/messages-" . wfWikiID(); $oldUmask = umask( 0 ); wfMkdirParents( $wgLocalMessageCache, 0777 ); umask( $oldUmask ); @@ -108,12 +111,12 @@ class MessageCache { } function loadFromScript( $hash ) { - global $wgLocalMessageCache, $wgDBname; + global $wgLocalMessageCache; if ( $wgLocalMessageCache === false ) { return; } - $filename = "$wgLocalMessageCache/messages-$wgDBname"; + $filename = "$wgLocalMessageCache/messages-" . wfWikiID(); wfSuppressWarnings(); $file = fopen( $filename, 'r' ); @@ -126,16 +129,16 @@ class MessageCache { if ($hash!=$localHash) { return; } - require("$wgLocalMessageCache/messages-$wgDBname"); + require("$wgLocalMessageCache/messages-" . wfWikiID()); } function saveToScript($array, $hash) { - global $wgLocalMessageCache, $wgDBname; + global $wgLocalMessageCache; if ( $wgLocalMessageCache === false ) { return; } - $filename = "$wgLocalMessageCache/messages-$wgDBname"; + $filename = "$wgLocalMessageCache/messages-" . wfWikiID(); $oldUmask = umask( 0 ); wfMkdirParents( $wgLocalMessageCache, 0777 ); umask( $oldUmask ); @@ -190,6 +193,9 @@ class MessageCache { } else { $this->loadFromScript( $hash ); } + if ( $this->mCache ) { + wfDebug( "MessageCache::load(): got from local cache\n" ); + } } wfProfileOut( $fname.'-fromlocal' ); @@ -197,18 +203,20 @@ class MessageCache { if ( !$this->mCache ) { wfProfileIn( $fname.'-fromcache' ); $this->mCache = $this->mMemc->get( $this->mMemcKey ); - - # Save to local cache - if ( $wgLocalMessageCache !== false ) { - $serialized = serialize( $this->mCache ); - if ( !$hash ) { - $hash = md5( $serialized ); - $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry ); - } - if ($wgLocalMessageCacheSerialized) { - $this->saveToLocal( $serialized,$hash ); - } else { - $this->saveToScript( $this->mCache, $hash ); + if ( $this->mCache ) { + wfDebug( "MessageCache::load(): got from global cache\n" ); + # Save to local cache + if ( $wgLocalMessageCache !== false ) { + $serialized = serialize( $this->mCache ); + if ( !$hash ) { + $hash = md5( $serialized ); + $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry ); + } + if ($wgLocalMessageCacheSerialized) { + $this->saveToLocal( $serialized,$hash ); + } else { + $this->saveToScript( $this->mCache, $hash ); + } } } wfProfileOut( $fname.'-fromcache' ); @@ -283,7 +291,7 @@ class MessageCache { * Loads all or main part of cacheable messages from the database */ function loadFromDB() { - global $wgAllMessagesEn, $wgLang; + global $wgLang; $fname = 'MessageCache::loadFromDB'; $dbr =& wfGetDB( DB_SLAVE ); @@ -306,7 +314,8 @@ class MessageCache { # Negative caching # Go through the language array and the extension array and make a note of # any keys missing from the cache - foreach ( $wgAllMessagesEn as $key => $value ) { + $allMessages = Language::getMessagesFor( 'en' ); + foreach ( $allMessages as $key => $value ) { $uckey = $wgLang->ucfirst( $key ); if ( !array_key_exists( $uckey, $this->mCache ) ) { $this->mCache[$uckey] = false; @@ -314,7 +323,7 @@ class MessageCache { } # Make sure all extension messages are available - wfLoadAllExtensions(); + MessageCache::loadAllMessages(); # Add them to the cache foreach ( $this->mExtensionMessages as $key => $value ) { @@ -332,10 +341,11 @@ class MessageCache { * Not really needed anymore */ function getKeys() { - global $wgAllMessagesEn, $wgContLang; + global $wgContLang; if ( !$this->mKeys ) { $this->mKeys = array(); - foreach ( $wgAllMessagesEn as $key => $value ) { + $allMessages = Language::getMessagesFor( 'en' ); + foreach ( $allMessages as $key => $value ) { $title = $wgContLang->ucfirst( $key ); array_push( $this->mKeys, $title ); } @@ -351,11 +361,11 @@ class MessageCache { } function replace( $title, $text ) { - global $wgLocalMessageCache, $wgLocalMessageCacheSerialized, $parserMemc, $wgDBname; + global $wgLocalMessageCache, $wgLocalMessageCacheSerialized, $parserMemc; $this->lock(); $this->load(); - $parserMemc->delete("$wgDBname:sidebar"); + $parserMemc->delete(wfMemcKey('sidebar')); if ( is_array( $this->mCache ) ) { $this->mCache[$title] = $text; $this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry ); @@ -403,17 +413,14 @@ class MessageCache { $this->mMemc->delete( $lockKey ); } - function get( $key, $useDB, $forcontent=true, $isfullkey = false ) { - global $wgContLanguageCode; + function get( $key, $useDB = true, $forcontent = true, $isfullkey = false ) { + global $wgContLanguageCode, $wgContLang, $wgLang; if( $forcontent ) { - global $wgContLang; $lang =& $wgContLang; - $langcode = $wgContLanguageCode; } else { - global $wgLang, $wgLanguageCode; $lang =& $wgLang; - $langcode = $wgLanguageCode; } + $langcode = $lang->getCode(); # If uninitialised, someone is trying to call this halfway through Setup.php if( !$this->mInitialised ) { return '<' . htmlspecialchars($key) . '>'; @@ -425,7 +432,7 @@ class MessageCache { $message = false; if( !$this->mDisable && $useDB ) { - $title = $lang->ucfirst( $key ); + $title = $wgContLang->ucfirst( $key ); if(!$isfullkey && ($langcode != $wgContLanguageCode) ) { $title .= '/' . $langcode; } @@ -442,6 +449,7 @@ class MessageCache { # Try the array in the language object if( $message === false ) { + #wfDebug( "Trying language object for message $key\n" ); wfSuppressWarnings(); $message = $lang->getMessage( $key ); wfRestoreWarnings(); @@ -460,11 +468,26 @@ class MessageCache { } } + # Try the array of another language + if( $message === false && strpos( $key, '/' ) ) { + $message = explode( '/', $key ); + if ( $message[1] ) { + wfSuppressWarnings(); + $message = Language::getMessageFor( $message[0], $message[1] ); + wfRestoreWarnings(); + if ( is_null( $message ) ) { + $message = false; + } + } else { + $message = false; + } + } + # Is this a custom message? Try the default language in the db... if( ($message === false || $message === '-' ) && !$this->mDisable && $useDB && !$isfullkey && ($langcode != $wgContLanguageCode) ) { - $message = $this->getFromCache( $lang->ucfirst( $key ) ); + $message = $this->getFromCache( $wgContLang->ucfirst( $key ) ); } # Final fallback @@ -489,6 +512,7 @@ class MessageCache { if ( $this->mUseCache ) { $message = $this->mMemc->get( $this->mMemcKey . ':' . $title ); if ( $message == '###NONEXISTENT###' ) { + $this->mCache[$title] = false; return false; } elseif( !is_null( $message ) ) { $this->mCache[$title] = $message; @@ -516,6 +540,7 @@ class MessageCache { # Negative caching # Use some special text instead of false, because false gets converted to '' somewhere $this->mMemc->set( $this->mMemcKey . ':' . $title, '###NONEXISTENT###', $this->mExpiry ); + $this->mCache[$title] = false; } return $message; @@ -531,7 +556,7 @@ class MessageCache { } if ( !$this->mDisableTransform && $this->mParser ) { if( strpos( $message, '{{' ) !== false ) { - $message = $this->mParser->transformMsg( $message, $this->mParserOptions ); + $message = $this->mParser->transformMsg( $message, $this->getParserOptions() ); } } return $message; @@ -570,12 +595,43 @@ class MessageCache { } /** + * Get the extension messages for a specific language + * + * @param string $lang The messages language, English by default + */ + function getExtensionMessagesFor( $lang = 'en' ) { + wfProfileIn( __METHOD__ ); + $messages = array(); + foreach( $this->mExtensionMessages as $key => $message ) { + if ( isset( $message[$lang] ) ) { + $messages[$key] = $message[$lang]; + } elseif ( isset( $message['en'] ) ) { + $messages[$key] = $message['en']; + } + } + wfProfileOut( __METHOD__ ); + return $messages; + } + + /** * Clear all stored messages. Mainly used after a mass rebuild. */ function clear() { + global $wgLocalMessageCache; if( $this->mUseCache ) { + # Global cache $this->mMemc->delete( $this->mMemcKey ); + # Invalidate all local caches + $this->mMemc->delete( "{$this->mMemcKey}-hash" ); } } + + static function loadAllMessages() { + # Some extensions will load their messages when you load their class file + wfLoadAllExtensions(); + # Others will respond to this hook + wfRunHooks( 'LoadAllMessages' ); + # Still others will respond to neither, they are EVIL. We sometimes need to know! + } } ?> diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index 30861ba3..dd197c31 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -74,7 +74,7 @@ if ($wgLoadFileinfoExtension) { * file extension, * * Instances of this class are stateles, there only needs to be one global instance -* of MimeMagic. Please use wfGetMimeMagic to get that instance. +* of MimeMagic. Please use MimeMagic::singleton() to get that instance. * @package MediaWiki */ class MimeMagic { @@ -97,8 +97,11 @@ class MimeMagic { */ var $mExtToMime= NULL; - /** Initializes the MimeMagic object. This is called by wfGetMimeMagic when instantiation - * the global MimeMagic singleton object. + /** The singleton instance + */ + private static $instance; + + /** Initializes the MimeMagic object. This is called by MimeMagic::singleton(). * * This constructor parses the mime.types and mime.info files and build internal mappings. */ @@ -227,6 +230,16 @@ class MimeMagic { } + /** + * Get an instance of this class + */ + static function &singleton() { + if ( !isset( self::$instance ) ) { + self::$instance = new MimeMagic; + } + return self::$instance; + } + /** returns a list of file extensions for a given mime type * as a space separated string. */ @@ -497,13 +510,22 @@ class MimeMagic { # NOTE: this function is available since PHP 4.3.0, but only if # PHP was compiled with --with-mime-magic or, before 4.3.2, with --enable-mime-magic. # - # On Winodws, you must set mime_magic.magicfile in php.ini to point to the mime.magic file bundeled with PHP; + # On Windows, you must set mime_magic.magicfile in php.ini to point to the mime.magic file bundeled with PHP; # sometimes, this may even be needed under linus/unix. # # Also note that this has been DEPRECATED in favor of the fileinfo extension by PECL, see above. # see http://www.php.net/manual/en/ref.mime-magic.php for details. $m= mime_content_type($file); + + if ( $m == 'text/plain' ) { + // mime_content_type sometimes considers DJVU files to be text/plain. + $deja = new DjVuImage( $file ); + if( $deja->isValid() ) { + wfDebug("$fname: (re)detected $file as image/vnd.djvu\n"); + $m = 'image/vnd.djvu'; + } + } } else wfDebug("$fname: no magic mime detector found!\n"); diff --git a/includes/Namespace.php b/includes/Namespace.php index ab7511d0..73dc2969 100644 --- a/includes/Namespace.php +++ b/includes/Namespace.php @@ -49,7 +49,7 @@ class Namespace { * Check if the given namespace might be moved * @return bool */ - function isMovable( $index ) { + static function isMovable( $index ) { return !( $index < NS_MAIN || $index == NS_IMAGE || $index == NS_CATEGORY ); } @@ -57,7 +57,7 @@ class Namespace { * Check if the given namespace is not a talk page * @return bool */ - function isMain( $index ) { + static function isMain( $index ) { return ! Namespace::isTalk( $index ); } @@ -65,7 +65,7 @@ class Namespace { * Check if the give namespace is a talk page * @return bool */ - function isTalk( $index ) { + static function isTalk( $index ) { return ($index > NS_MAIN) // Special namespaces are negative && ($index % 2); // Talk namespaces are odd-numbered } @@ -73,7 +73,7 @@ class Namespace { /** * Get the talk namespace corresponding to the given index */ - function getTalk( $index ) { + static function getTalk( $index ) { if ( Namespace::isTalk( $index ) ) { return $index; } else { @@ -82,7 +82,7 @@ class Namespace { } } - function getSubject( $index ) { + static function getSubject( $index ) { if ( Namespace::isTalk( $index ) ) { return $index - 1; } else { @@ -93,7 +93,7 @@ class Namespace { /** * Returns the canonical (English Wikipedia) name for a given index */ - function getCanonicalName( $index ) { + static function getCanonicalName( $index ) { global $wgCanonicalNamespaceNames; return $wgCanonicalNamespaceNames[$index]; } @@ -102,7 +102,7 @@ class Namespace { * Returns the index for a given canonical name, or NULL * The input *must* be converted to lower case first */ - function getCanonicalIndex( $name ) { + static function getCanonicalIndex( $name ) { global $wgCanonicalNamespaceNames; static $xNamespaces = false; if ( $xNamespaces === false ) { @@ -122,7 +122,7 @@ class Namespace { * Can this namespace ever have a talk namespace? * @param $index Namespace index */ - function canTalk( $index ) { + static function canTalk( $index ) { return( $index >= NS_MAIN ); } } diff --git a/includes/ObjectCache.php b/includes/ObjectCache.php index fe7417d2..2b26cf4e 100644 --- a/includes/ObjectCache.php +++ b/includes/ObjectCache.php @@ -69,13 +69,10 @@ function &wfGetCache( $inputType ) { } elseif ( $type == CACHE_ACCEL ) { if ( !array_key_exists( CACHE_ACCEL, $wgCaches ) ) { if ( function_exists( 'eaccelerator_get' ) ) { - require_once( 'BagOStuff.php' ); $wgCaches[CACHE_ACCEL] = new eAccelBagOStuff; } elseif ( function_exists( 'apc_fetch') ) { - require_once( 'BagOStuff.php' ); $wgCaches[CACHE_ACCEL] = new APCBagOStuff; } elseif ( function_exists( 'mmcache_get' ) ) { - require_once( 'BagOStuff.php' ); $wgCaches[CACHE_ACCEL] = new TurckBagOStuff; } else { $wgCaches[CACHE_ACCEL] = false; @@ -84,11 +81,15 @@ function &wfGetCache( $inputType ) { if ( $wgCaches[CACHE_ACCEL] !== false ) { $cache =& $wgCaches[CACHE_ACCEL]; } + } elseif ( $type == CACHE_DBA ) { + if ( !array_key_exists( CACHE_DBA, $wgCaches ) ) { + $wgCaches[CACHE_DBA] = new DBABagOStuff; + } + $cache =& $wgCaches[CACHE_DBA]; } - + if ( $type == CACHE_DB || ( $inputType == CACHE_ANYTHING && $cache === false ) ) { if ( !array_key_exists( CACHE_DB, $wgCaches ) ) { - require_once( 'BagOStuff.php' ); $wgCaches[CACHE_DB] = new MediaWikiBagOStuff('objectcache'); } $cache =& $wgCaches[CACHE_DB]; diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 31a0781a..0d55c2e0 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -22,7 +22,7 @@ class OutputPage { var $mDoNothing; var $mContainsOldMagic, $mContainsNewMagic; var $mIsArticleRelated; - var $mParserOptions; + protected $mParserOptions; // lazy initialised, use parserOptions() var $mShowFeedLinks = false; var $mEnableClientCache = true; var $mArticleBodyOnly = false; @@ -46,7 +46,7 @@ class OutputPage { $this->mCategoryLinks = array(); $this->mDoNothing = false; $this->mContainsOldMagic = $this->mContainsNewMagic = 0; - $this->mParserOptions = ParserOptions::newFromUser( $temp = NULL ); + $this->mParserOptions = null; $this->mSquidMaxage = 0; $this->mScripts = ''; $this->mETag = false; @@ -92,7 +92,7 @@ class OutputPage { * returns true iff cache-ok headers was sent. */ function checkLastModified ( $timestamp ) { - global $wgCachePages, $wgCacheEpoch, $wgUser; + global $wgCachePages, $wgCacheEpoch, $wgUser, $wgRequest; $fname = 'OutputPage::checkLastModified'; if ( !$timestamp || $timestamp == '19700101000000' ) { @@ -122,7 +122,7 @@ class OutputPage { wfDebug( "$fname: -- we might send Last-Modified : $lastmod\n", false ); if( ($ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) { # Make sure you're in a place you can leave when you call us! - header( "HTTP/1.0 304 Not Modified" ); + $wgRequest->response()->header( "HTTP/1.0 304 Not Modified" ); $this->mLastModified = $lastmod; $this->sendCacheControl(); wfDebug( "$fname: CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false ); @@ -255,10 +255,13 @@ class OutputPage { /* @deprecated */ function setParserOptions( $options ) { - return $this->ParserOptions( $options ); + return $this->parserOptions( $options ); } - function ParserOptions( $options = null ) { + function parserOptions( $options = null ) { + if ( !$this->mParserOptions ) { + $this->mParserOptions = new ParserOptions; + } return wfSetVar( $this->mParserOptions, $options ); } @@ -289,9 +292,13 @@ class OutputPage { function addWikiTextTitle($text, &$title, $linestart) { global $wgParser; - $parserOutput = $wgParser->parse( $text, $title, $this->mParserOptions, + $fname = 'OutputPage:addWikiTextTitle'; + wfProfileIn($fname); + wfIncrStats('pcache_not_possible'); + $parserOutput = $wgParser->parse( $text, $title, $this->parserOptions(), $linestart, true, $this->mRevisionId ); $this->addParserOutput( $parserOutput ); + wfProfileOut($fname); } function addParserOutputNoText( &$parserOutput ) { @@ -304,13 +311,19 @@ class OutputPage { } if ( $parserOutput->mHTMLtitle != "" ) { $this->mPagetitle = $parserOutput->mHTMLtitle ; + } + if ( $parserOutput->mSubtitle != '' ) { $this->mSubtitle .= $parserOutput->mSubtitle ; } + $this->mNoGallery = $parserOutput->getNoGallery(); + wfRunHooks( 'OutputPageParserOutput', array( &$this, $parserOutput ) ); } function addParserOutput( &$parserOutput ) { $this->addParserOutputNoText( $parserOutput ); - $this->addHTML( $parserOutput->getText() ); + $text = $parserOutput->getText(); + wfRunHooks( 'OutputPageBeforeHTML',array( &$this, &$text ) ); + $this->addHTML( $text ); } /** @@ -320,21 +333,17 @@ class OutputPage { function addPrimaryWikiText( $text, $article, $cache = true ) { global $wgParser, $wgUser; - $this->mParserOptions->setTidy(true); + $popts = $this->parserOptions(); + $popts->setTidy(true); $parserOutput = $wgParser->parse( $text, $article->mTitle, - $this->mParserOptions, true, true, $this->mRevisionId ); - $this->mParserOptions->setTidy(false); + $popts, true, true, $this->mRevisionId ); + $popts->setTidy(false); if ( $cache && $article && $parserOutput->getCacheTime() != -1 ) { $parserCache =& ParserCache::singleton(); $parserCache->save( $parserOutput, $article, $wgUser ); } - $this->addParserOutputNoText( $parserOutput ); - $text = $parserOutput->getText(); - $this->mNoGallery = $parserOutput->getNoGallery(); - wfRunHooks( 'OutputPageBeforeHTML',array( &$this, &$text ) ); - $parserOutput->setText( $text ); - $this->addHTML( $parserOutput->getText() ); + $this->addParserOutput( $parserOutput ); } /** @@ -342,9 +351,10 @@ class OutputPage { */ function addSecondaryWikiText( $text, $linestart = true ) { global $wgTitle; - $this->mParserOptions->setTidy(true); + $popts = $this->parserOptions(); + $popts->setTidy(true); $this->addWikiTextTitle($text, $wgTitle, $linestart); - $this->mParserOptions->setTidy(false); + $popts->setTidy(false); } @@ -364,10 +374,11 @@ class OutputPage { */ function parse( $text, $linestart = true, $interface = false ) { global $wgParser, $wgTitle; - if ( $interface) { $this->mParserOptions->setInterfaceMessage(true); } - $parserOutput = $wgParser->parse( $text, $wgTitle, $this->mParserOptions, + $popts = $this->parserOptions(); + if ( $interface) { $popts->setInterfaceMessage(true); } + $parserOutput = $wgParser->parse( $text, $wgTitle, $popts, $linestart, true, $this->mRevisionId ); - if ( $interface) { $this->mParserOptions->setInterfaceMessage(false); } + if ( $interface) { $popts->setInterfaceMessage(false); } return $parserOutput->getText(); } @@ -381,18 +392,7 @@ class OutputPage { $parserCache =& ParserCache::singleton(); $parserOutput = $parserCache->get( $article, $user ); if ( $parserOutput !== false ) { - $this->mLanguageLinks += $parserOutput->getLanguageLinks(); - $this->addCategoryLinks( $parserOutput->getCategories() ); - $this->addKeywords( $parserOutput ); - $this->mNewSectionLink = $parserOutput->getNewSection(); - $this->mNoGallery = $parserOutput->getNoGallery(); - $text = $parserOutput->getText(); - wfRunHooks( 'OutputPageBeforeHTML', array( &$this, &$text ) ); - $this->addHTML( $text ); - $t = $parserOutput->getTitleText(); - if( !empty( $t ) ) { - $this->setPageTitle( $t ); - } + $this->addParserOutput( $parserOutput ); return true; } else { return false; @@ -422,15 +422,15 @@ class OutputPage { } function sendCacheControl() { - global $wgUseSquid, $wgUseESI, $wgSquidMaxage; + global $wgUseSquid, $wgUseESI, $wgUseETag, $wgSquidMaxage, $wgRequest; $fname = 'OutputPage::sendCacheControl'; - if ($this->mETag) - header("ETag: $this->mETag"); + if ($wgUseETag && $this->mETag) + $wgRequest->response()->header("ETag: $this->mETag"); # don't serve compressed data to clients who can't handle it # maintain different caches for logged-in users and non-logged in ones - header( 'Vary: Accept-Encoding, Cookie' ); + $wgRequest->response()->header( 'Vary: Accept-Encoding, Cookie' ); if( !$this->uncacheableBecauseRequestvars() && $this->mEnableClientCache ) { if( $wgUseSquid && ! isset( $_COOKIE[ini_get( 'session.name') ] ) && ! $this->isPrintable() && $this->mSquidMaxage != 0 ) @@ -442,8 +442,8 @@ class OutputPage { wfDebug( "$fname: proxy caching with ESI; {$this->mLastModified} **\n", false ); # start with a shorter timeout for initial testing # header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"'); - header( 'Surrogate-Control: max-age='.$wgSquidMaxage.'+'.$this->mSquidMaxage.', content="ESI/1.0"'); - header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); + $wgRequest->response()->header( 'Surrogate-Control: max-age='.$wgSquidMaxage.'+'.$this->mSquidMaxage.', content="ESI/1.0"'); + $wgRequest->response()->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); } else { # We'll purge the proxy cache for anons explicitly, but require end user agents # to revalidate against the proxy on each visit. @@ -452,24 +452,24 @@ class OutputPage { wfDebug( "$fname: local proxy caching; {$this->mLastModified} **\n", false ); # start with a shorter timeout for initial testing # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" ); - header( 'Cache-Control: s-maxage='.$this->mSquidMaxage.', must-revalidate, max-age=0' ); + $wgRequest->response()->header( 'Cache-Control: s-maxage='.$this->mSquidMaxage.', must-revalidate, max-age=0' ); } } else { # We do want clients to cache if they can, but they *must* check for updates # on revisiting the page. wfDebug( "$fname: private caching; {$this->mLastModified} **\n", false ); - header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); - header( "Cache-Control: private, must-revalidate, max-age=0" ); + $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); + $wgRequest->response()->header( "Cache-Control: private, must-revalidate, max-age=0" ); } - if($this->mLastModified) header( "Last-modified: {$this->mLastModified}" ); + if($this->mLastModified) $wgRequest->response()->header( "Last-modified: {$this->mLastModified}" ); } else { wfDebug( "$fname: no caching **\n", false ); # In general, the absence of a last modified header should be enough to prevent # the client from using its cache. We send a few other things just to make sure. - header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); - header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); - header( 'Pragma: no-cache' ); + $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); + $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); + $wgRequest->response()->header( 'Pragma: no-cache' ); } } @@ -478,9 +478,9 @@ class OutputPage { * the object, let's actually output it: */ function output() { - global $wgUser, $wgOutputEncoding; + global $wgUser, $wgOutputEncoding, $wgRequest; global $wgContLanguageCode, $wgDebugRedirects, $wgMimeType; - global $wgJsMimeType, $wgStylePath, $wgUseAjax, $wgScriptPath, $wgServer; + global $wgJsMimeType, $wgStylePath, $wgUseAjax, $wgAjaxSearch, $wgScriptPath, $wgServer; if( $this->mDoNothing ){ return; @@ -490,13 +490,14 @@ class OutputPage { $sk = $wgUser->getSkin(); if ( $wgUseAjax ) { - $this->addScript( "<script type=\"{$wgJsMimeType}\"> - var wgScriptPath=\"{$wgScriptPath}\"; - var wgServer=\"{$wgServer}\"; - </script>" ); $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajax.js\"></script>\n" ); } + if ( $wgUseAjax && $wgAjaxSearch ) { + $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxsearch.js\"></script>\n" ); + $this->addScript( "<script type=\"{$wgJsMimeType}\">hookEvent(\"load\", sajax_onload);</script>\n" ); + } + if ( '' != $this->mRedirect ) { if( substr( $this->mRedirect, 0, 4 ) != 'http' ) { # Standards require redirect URLs to be absolute @@ -505,7 +506,7 @@ class OutputPage { } if( $this->mRedirectCode == '301') { if( !$wgDebugRedirects ) { - header("HTTP/1.1 {$this->mRedirectCode} Moved Permanently"); + $wgRequest->response()->header("HTTP/1.1 {$this->mRedirectCode} Moved Permanently"); } $this->mLastModified = wfTimestamp( TS_RFC2822 ); } @@ -518,7 +519,7 @@ class OutputPage { print "<p>Location: <a href=\"$url\">$url</a></p>\n"; print "</body>\n</html>\n"; } else { - header( 'Location: '.$this->mRedirect ); + $wgRequest->response()->header( 'Location: '.$this->mRedirect ); } wfProfileOut( $fname ); return; @@ -575,7 +576,7 @@ class OutputPage { ); if ( $statusMessage[$this->mStatusCode] ) - header( 'HTTP/1.1 ' . $this->mStatusCode . ' ' . $statusMessage[$this->mStatusCode] ); + $wgRequest->response()->header( 'HTTP/1.1 ' . $this->mStatusCode . ' ' . $statusMessage[$this->mStatusCode] ); } # Buffer output; final headers may depend on later processing @@ -584,8 +585,8 @@ class OutputPage { # Disable temporary placeholders, so that the skin produces HTML $sk->postParseLinkColour( false ); - header( "Content-type: $wgMimeType; charset={$wgOutputEncoding}" ); - header( 'Content-language: '.$wgContLanguageCode ); + $wgRequest->response()->header( "Content-type: $wgMimeType; charset={$wgOutputEncoding}" ); + $wgRequest->response()->header( 'Content-language: '.$wgContLanguageCode ); if ($this->mArticleBodyOnly) { $this->out($this->mBodytext); @@ -617,11 +618,6 @@ class OutputPage { $wgInputEncoding = strtolower( $wgInputEncoding ); - if( $wgUser->getOption( 'altencoding' ) ) { - $wgContLang->setAltEncoding(); - return; - } - if ( empty( $_SERVER['HTTP_ACCEPT_CHARSET'] ) ) { $wgOutputEncoding = strtolower( $wgOutputEncoding ); return; @@ -715,11 +711,10 @@ class OutputPage { /** * Display an error page noting that a given permission bit is required. - * This should generally replace the sysopRequired, developerRequired etc. * @param string $permission key required */ function permissionRequired( $permission ) { - global $wgUser; + global $wgGroupPermissions, $wgUser; $this->setPageTitle( wfMsg( 'badaccess' ) ); $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); @@ -727,46 +722,46 @@ class OutputPage { $this->setArticleRelated( false ); $this->mBodytext = ''; - $sk = $wgUser->getSkin(); - $ap = $sk->makeKnownLink( wfMsgForContent( 'administrators' ) ); - $this->addHTML( wfMsgHtml( 'badaccesstext', $ap, $permission ) ); - $this->returnToMain(); + $groups = array(); + foreach( $wgGroupPermissions as $key => $value ) { + if( isset( $value[$permission] ) && $value[$permission] == true ) { + $groupName = User::getGroupName( $key ); + $groupPage = User::getGroupPage( $key ); + if( $groupPage ) { + $skin =& $wgUser->getSkin(); + $groups[] = '"'.$skin->makeLinkObj( $groupPage, $groupName ).'"'; + } else { + $groups[] = '"'.$groupName.'"'; + } + } + } + $n = count( $groups ); + $groups = implode( ', ', $groups ); + switch( $n ) { + case 0: + case 1: + case 2: + $message = wfMsgHtml( "badaccess-group$n", $groups ); + break; + default: + $message = wfMsgHtml( 'badaccess-groups', $groups ); + } + $this->addHtml( $message ); + $this->returnToMain( false ); } /** * @deprecated */ function sysopRequired() { - global $wgUser; - - $this->setPageTitle( wfMsg( 'sysoptitle' ) ); - $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); - $this->setRobotpolicy( 'noindex,nofollow' ); - $this->setArticleRelated( false ); - $this->mBodytext = ''; - - $sk = $wgUser->getSkin(); - $ap = $sk->makeKnownLink( wfMsgForContent( 'administrators' ), '' ); - $this->addHTML( wfMsgHtml( 'sysoptext', $ap ) ); - $this->returnToMain(); + throw new MWException( "Call to deprecated OutputPage::sysopRequired() method\n" ); } /** * @deprecated */ function developerRequired() { - global $wgUser; - - $this->setPageTitle( wfMsg( 'developertitle' ) ); - $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); - $this->setRobotpolicy( 'noindex,nofollow' ); - $this->setArticleRelated( false ); - $this->mBodytext = ''; - - $sk = $wgUser->getSkin(); - $ap = $sk->makeKnownLink( wfMsgForContent( 'administrators' ), '' ); - $this->addHTML( wfMsgHtml( 'developertext', $ap ) ); - $this->returnToMain(); + throw new MWException( "Call to deprecated OutputPage::developerRequired() method\n" ); } /** @@ -774,6 +769,12 @@ class OutputPage { */ function loginToUse() { global $wgUser, $wgTitle, $wgContLang; + + if( $wgUser->isLoggedIn() ) { + $this->permissionRequired( 'read' ); + return; + } + $skin = $wgUser->getSkin(); $this->setPageTitle( wfMsg( 'loginreqtitle' ) ); @@ -786,7 +787,11 @@ class OutputPage { $this->addHtml( wfMsgWikiHtml( 'loginreqpagetext', $loginLink ) ); $this->addHtml( "\n<!--" . $wgTitle->getPrefixedUrl() . "-->" ); - $this->returnToMain(); + # Don't return to the main page if the user can't read it + # otherwise we'll end up in a pointless loop + $mainPage = Title::newFromText( wfMsgForContent( 'mainpage' ) ); + if( $mainPage->userCanRead() ) + $this->returnToMain( true, $mainPage ); } /** @obsolete */ @@ -828,7 +833,7 @@ class OutputPage { if ( $wgTitle->getNamespace() == NS_MEDIAWIKI ) { $source = wfMsgWeirdKey ( $wgTitle->getText() ); } else { - $source = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' ); + $source = ''; } } $rows = $wgUser->getIntOption( 'rows' ); @@ -1045,7 +1050,7 @@ class OutputPage { $link = $wgRequest->escapeAppendQuery( 'feed=rss' ); $ret .= "<link rel='alternate' type='application/rss+xml' title='RSS 2.0' href='$link' />\n"; $link = $wgRequest->escapeAppendQuery( 'feed=atom' ); - $ret .= "<link rel='alternate' type='application/atom+xml' title='Atom 0.3' href='$link' />\n"; + $ret .= "<link rel='alternate' type='application/atom+xml' title='Atom 1.0' href='$link' />\n"; } return $ret; diff --git a/includes/PageHistory.php b/includes/PageHistory.php index de006285..d7f426fc 100644 --- a/includes/PageHistory.php +++ b/includes/PageHistory.php @@ -40,8 +40,6 @@ class PageHistory { $this->mTitle =& $article->mTitle; $this->mNotificationTimestamp = NULL; $this->mSkin = $wgUser->getSkin(); - - $this->defaultLimit = 50; } /** @@ -93,99 +91,30 @@ class PageHistory { return; } - $dbr =& wfGetDB(DB_SLAVE); - - /* - * Extract limit, the number of revisions to show, and - * offset, the timestamp to begin at, from the URL. - */ - $limit = $wgRequest->getInt('limit', $this->defaultLimit); - if ( $limit <= 0 ) { - $limit = $this->defaultLimit; - } elseif ( $limit > 50000 ) { - # Arbitrary maximum - # Any more than this and we'll probably get an out of memory error - $limit = 50000; - } - - $offset = $wgRequest->getText('offset'); - - /* Offset must be an integral. */ - if (!strlen($offset) || !preg_match("/^[0-9]+$/", $offset)) - $offset = 0; -# $offset = $dbr->timestamp($offset); - $dboffset = $offset === 0 ? 0 : $dbr->timestamp($offset); + /* - * "go=last" means to jump to the last history page. + * "go=first" means to jump to the last (earliest) history page. + * This is deprecated, it no longer appears in the user interface */ - if (($gowhere = $wgRequest->getText("go")) !== NULL) { - $gourl = null; - switch ($gowhere) { - case "first": - if (($lastid = $this->getLastOffsetForPaging($this->mTitle->getArticleID(), $limit)) === NULL) - break; - $gourl = $wgTitle->getLocalURL("action=history&limit={$limit}&offset=". - wfTimestamp(TS_MW, $lastid)); - break; - } - - if (!is_null($gourl)) { - $wgOut->redirect($gourl); - return; - } + if ( $wgRequest->getText("go") == 'first' ) { + $limit = $wgRequest->getInt( 'limit', 50 ); + $wgOut->redirect( $wgTitle->getLocalURL( "action=history&limit={$limit}&dir=prev" ) ); + return; } - /* - * Fetch revisions. - * - * If the user clicked "previous", we retrieve the revisions backwards, - * then reverse them. This is to avoid needing to know the timestamp of - * previous revisions when generating the URL. + /** + * Do the list */ - $direction = $this->getDirection(); - $revisions = $this->fetchRevisions($limit, $dboffset, $direction); - $navbar = $this->makeNavbar($revisions, $offset, $limit, $direction); - - /* - * We fetch one more revision than needed to get the timestamp of the - * one after this page (and to know if it exists). - * - * linesonpage stores the actual number of lines. - */ - if (count($revisions) < $limit + 1) - $this->linesonpage = count($revisions); - else - $this->linesonpage = count($revisions) - 1; - - /* Un-reverse revisions */ - if ($direction == PageHistory::DIR_PREV) - $revisions = array_reverse($revisions); - - /* - * Print the top navbar. - */ - $s = $navbar; - $s .= $this->beginHistoryList(); - $counter = 1; - - /* - * Print each revision, excluding the one-past-the-end, if any. - */ - foreach (array_slice($revisions, 0, $limit) as $i => $line) { - $latest = !$i && $offset == 0; - $firstInList = !$i; - $next = isset( $revisions[$i + 1] ) ? $revisions[$i + 1 ] : null; - $s .= $this->historyLine($line, $next, $counter, $this->getNotificationTimestamp(), $latest, $firstInList); - $counter++; - } - - /* - * End navbar. - */ - $s .= $this->endHistoryList(); - $s .= $navbar; - - $wgOut->addHTML( $s ); + $pager = new PageHistoryPager( $this ); + $navbar = $pager->getNavigationBar(); + $this->linesonpage = $pager->getNumRows(); + $wgOut->addHTML( + $pager->getNavigationBar() . + $this->beginHistoryList() . + $pager->getBody() . + $this->endHistoryList() . + $pager->getNavigationBar() + ); wfProfileOut( $fname ); } @@ -318,16 +247,15 @@ class PageHistory { /** @todo document */ function lastLink( $rev, $next, $counter ) { $last = wfMsgExt( 'last', array( 'escape' ) ); - if( is_null( $next ) ) { - if( $rev->getTimestamp() == $this->getEarliestOffset() ) { - return $last; - } else { - // Cut off by paging; there are more behind us... - return $this->mSkin->makeKnownLinkObj( - $this->mTitle, - $last, - "diff=" . $rev->getId() . "&oldid=prev" ); - } + if ( is_null( $next ) ) { + # Probably no next row + return $last; + } elseif ( $next === 'unknown' ) { + # Next row probably exists but is unknown, use an oldid=prev link + return $this->mSkin->makeKnownLinkObj( + $this->mTitle, + $last, + "diff=" . $rev->getId() . "&oldid=prev" ); } elseif( !$rev->userCan( Revision::DELETED_TEXT ) ) { return $last; } else { @@ -387,72 +315,23 @@ class PageHistory { } /** @todo document */ - function getLatestOffset( $id = null ) { - if ( $id === null) $id = $this->mTitle->getArticleID(); - return $this->getExtremeOffset( $id, 'max' ); - } - - /** @todo document */ - function getEarliestOffset( $id = null ) { - if ( $id === null) $id = $this->mTitle->getArticleID(); - return $this->getExtremeOffset( $id, 'min' ); - } - - /** @todo document */ - function getExtremeOffset( $id, $func ) { - $db =& wfGetDB(DB_SLAVE); - return $db->selectField( 'revision', - "$func(rev_timestamp)", - array( 'rev_page' => $id ), - 'PageHistory::getExtremeOffset' ); - } - - /** @todo document */ function getLatestId() { if( is_null( $this->mLatestId ) ) { $id = $this->mTitle->getArticleID(); $db =& wfGetDB(DB_SLAVE); - $this->mLatestId = $db->selectField( 'revision', - "max(rev_id)", - array( 'rev_page' => $id ), + $this->mLatestId = $db->selectField( 'page', + "page_latest", + array( 'page_id' => $id ), 'PageHistory::getLatestID' ); } return $this->mLatestId; } - /** @todo document */ - function getLastOffsetForPaging( $id, $step ) { - $fname = 'PageHistory::getLastOffsetForPaging'; - - $dbr =& wfGetDB(DB_SLAVE); - $res = $dbr->select( - 'revision', - 'rev_timestamp', - "rev_page=$id", - $fname, - array('ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $step)); - - $n = $dbr->numRows( $res ); - $last = null; - while( $obj = $dbr->fetchObject( $res ) ) { - $last = $obj->rev_timestamp; - } - $dbr->freeResult( $res ); - return $last; - } - /** - * @return returns the direction of browsing watchlist + * Fetch an array of revisions, specified by a given limit, offset and + * direction. This is now only used by the feeds. It was previously + * used by the main UI but that's now handled by the pager. */ - function getDirection() { - global $wgRequest; - if ($wgRequest->getText("dir") == "prev") - return PageHistory::DIR_PREV; - else - return PageHistory::DIR_NEXT; - } - - /** @todo document */ function fetchRevisions($limit, $offset, $direction) { $fname = 'PageHistory::fetchRevisions'; @@ -516,83 +395,6 @@ class PageHistory { return $this->mNotificationTimestamp; } - - /** @todo document */ - function makeNavbar($revisions, $offset, $limit, $direction) { - global $wgLang; - - $revisions = array_slice($revisions, 0, $limit); - - $latestTimestamp = wfTimestamp(TS_MW, $this->getLatestOffset()); - $earliestTimestamp = wfTimestamp(TS_MW, $this->getEarliestOffset()); - - /* - * When we're displaying previous revisions, we need to reverse - * the array, because it's queried in reverse order. - */ - if ($direction == PageHistory::DIR_PREV) - $revisions = array_reverse($revisions); - - /* - * lowts is the timestamp of the first revision on this page. - * hights is the timestamp of the last revision. - */ - - $lowts = $hights = 0; - - if( count( $revisions ) ) { - $latestShown = wfTimestamp(TS_MW, $revisions[0]->rev_timestamp); - $earliestShown = wfTimestamp(TS_MW, $revisions[count($revisions) - 1]->rev_timestamp); - } else { - $latestShown = null; - $earliestShown = null; - } - - /* Don't announce the limit everywhere if it's the default */ - $usefulLimit = $limit == $this->defaultLimit ? '' : $limit; - - $urls = array(); - foreach (array(20, 50, 100, 250, 500) as $num) { - $urls[] = $this->MakeLink( $wgLang->formatNum($num), - array('offset' => $offset == 0 ? '' : wfTimestamp(TS_MW, $offset), 'limit' => $num, ) ); - } - - $bits = implode($urls, ' | '); - - wfDebug("latestShown=$latestShown latestTimestamp=$latestTimestamp\n"); - if( $latestShown < $latestTimestamp ) { - $prevtext = $this->MakeLink( wfMsgHtml("prevn", $limit), - array( 'dir' => 'prev', 'offset' => $latestShown, 'limit' => $usefulLimit ) ); - $lasttext = $this->MakeLink( wfMsgHtml('histlast'), - array( 'limit' => $usefulLimit ) ); - } else { - $prevtext = wfMsgHtml("prevn", $limit); - $lasttext = wfMsgHtml('histlast'); - } - - wfDebug("earliestShown=$earliestShown earliestTimestamp=$earliestTimestamp\n"); - if( $earliestShown > $earliestTimestamp ) { - $nexttext = $this->MakeLink( wfMsgHtml("nextn", $limit), - array( 'offset' => $earliestShown, 'limit' => $usefulLimit ) ); - $firsttext = $this->MakeLink( wfMsgHtml('histfirst'), - array( 'go' => 'first', 'limit' => $usefulLimit ) ); - } else { - $nexttext = wfMsgHtml("nextn", $limit); - $firsttext = wfMsgHtml('histfirst'); - } - - $firstlast = "($lasttext | $firsttext)"; - - return "$firstlast " . wfMsgHtml("viewprevnext", $prevtext, $nexttext, $bits); - } - - function MakeLink($text, $query = NULL) { - if ( $query === null ) return $text; - return $this->mSkin->makeKnownLinkObj( - $this->mTitle, $text, - wfArrayToCGI( $query, array( 'action' => 'history' ))); - } - /** * Output a subscription feed listing recent edits to this page. @@ -678,8 +480,72 @@ class PageHistory { function stripComment( $text ) { return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text ); } +} + + +class PageHistoryPager extends ReverseChronologicalPager { + public $mLastRow = false, $mPageHistory; + + function __construct( $pageHistory ) { + parent::__construct(); + $this->mPageHistory = $pageHistory; + } + + 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'), + 'conds' => array('rev_page' => $this->mPageHistory->mTitle->getArticleID() ), + 'options' => array( 'USE INDEX' => 'page_timestamp' ) + ); + } + + function getIndexField() { + return 'rev_timestamp'; + } + function formatRow( $row ) { + if ( $this->mLastRow ) { + $latest = $this->mCounter == 1 && $this->mOffset == ''; + $firstInList = $this->mCounter == 1; + $s = $this->mPageHistory->historyLine( $this->mLastRow, $row, $this->mCounter++, + $this->mPageHistory->getNotificationTimestamp(), $latest, $firstInList ); + } else { + $s = ''; + } + $this->mLastRow = $row; + return $s; + } + + function getStartBody() { + $this->mLastRow = false; + $this->mCounter = 1; + return ''; + } + function getEndBody() { + if ( $this->mLastRow ) { + $latest = $this->mCounter == 1 && $this->mOffset == 0; + $firstInList = $this->mCounter == 1; + if ( $this->mIsBackwards ) { + # Next row is unknown, but for UI reasons, probably exists if an offset has been specified + if ( $this->mOffset == '' ) { + $next = null; + } else { + $next = 'unknown'; + } + } else { + # The next row is the past-the-end row + $next = $this->mPastTheEndRow; + } + $s = $this->mPageHistory->historyLine( $this->mLastRow, $next, $this->mCounter++, + $this->mPageHistory->getNotificationTimestamp(), $latest, $firstInList ); + } else { + $s = ''; + } + return $s; + } } ?> diff --git a/includes/Pager.php b/includes/Pager.php new file mode 100644 index 00000000..b14aa8ca --- /dev/null +++ b/includes/Pager.php @@ -0,0 +1,656 @@ +<?php + +/** + * Basic pager interface. + */ +interface Pager { + function getNavigationBar(); + function getBody(); +} + +/** + * IndexPager is an efficient pager which uses a (roughly unique) index in the + * data set to implement paging, rather than a "LIMIT offset,limit" clause. + * In MySQL, such a limit/offset clause requires counting through the specified number + * of offset rows to find the desired data, which can be expensive for large offsets. + * + * ReverseChronologicalPager is a child class of the abstract IndexPager, and contains + * some formatting and display code which is specific to the use of timestamps as + * indexes. Here is a synopsis of its operation: + * + * * The query is specified by the offset, limit and direction (dir) parameters, in + * addition to any subclass-specific parameters. + * + * * The offset is the non-inclusive start of the DB query. A row with an index value + * equal to the offset will never be shown. + * + * * The query may either be done backwards, where the rows are returned by the database + * in the opposite order to which they are displayed to the user, or forwards. This is + * specified by the "dir" parameter, dir=prev means backwards, anything else means + * forwards. The offset value specifies the start of the database result set, which + * may be either the start or end of the displayed data set. This allows "previous" + * links to be implemented without knowledge of the index value at the start of the + * previous page. + * + * * An additional row beyond the user-specified limit is always requested. This allows + * us to tell whether we should display a "next" link in the case of forwards mode, + * or a "previous" link in the case of backwards mode. Determining whether to + * display the other link (the one for the page before the start of the database + * result set) can be done heuristically by examining the offset. + * + * * An empty offset indicates that the offset condition should be omitted from the query. + * This naturally produces either the first page or the last page depending on the + * dir parameter. + * + * Subclassing the pager to implement concrete functionality should be fairly simple, + * 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. + */ +abstract class IndexPager implements Pager { + public $mRequest; + public $mLimitsShown = array( 20, 50, 100, 250, 500 ); + public $mDefaultLimit = 50; + public $mOffset, $mLimit; + public $mQueryDone = false; + public $mDb; + public $mPastTheEndRow; + + protected $mIndexField; + + /** + * Default query direction. false for ascending, true for descending + */ + public $mDefaultDirection = false; + + /** + * Result object for the query. Warning: seek before use. + */ + public $mResult; + + function __construct() { + global $wgRequest; + $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; + } + $this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' ); + $this->mIndexField = $this->getIndexField(); + $this->mDb = wfGetDB( DB_SLAVE ); + } + + /** + * Do the query, using information from the object context. This function + * has been kept minimal to make it overridable if necessary, to allow for + * result sets formed from multiple DB queries. + */ + function doQuery() { + # Use the child class name for profiling + $fname = __METHOD__ . ' (' . get_class( $this ) . ')'; + wfProfileIn( $fname ); + + $descending = ( $this->mIsBackwards == $this->mDefaultDirection ); + # Plus an extra row so that we can tell the "next" link should be shown + $queryLimit = $this->mLimit + 1; + + $this->mResult = $this->reallyDoQuery( $this->mOffset, $queryLimit, $descending ); + $this->extractResultInfo( $this->mOffset, $queryLimit, $this->mResult ); + $this->mQueryDone = true; + + wfProfileOut( $fname ); + } + + /** + * Extract some useful data from the result object for use by + * the navigation bar, put it into $this + */ + function extractResultInfo( $offset, $limit, ResultWrapper $res ) { + $numRows = $res->numRows(); + if ( $numRows ) { + $row = $res->fetchRow(); + $firstIndex = $row[$this->mIndexField]; + + # Discard the extra result row if there is one + if ( $numRows > $this->mLimit && $numRows > 1 ) { + $res->seek( $numRows - 1 ); + $this->mPastTheEndRow = $res->fetchObject(); + $indexField = $this->mIndexField; + $this->mPastTheEndIndex = $this->mPastTheEndRow->$indexField; + $res->seek( $numRows - 2 ); + $row = $res->fetchRow(); + $lastIndex = $row[$this->mIndexField]; + } else { + $this->mPastTheEndRow = null; + # Setting indexes to an empty string means that they will be omitted + # if they would otherwise appear in URLs. It just so happens that this + # is the right thing to do in the standard UI, in all the relevant cases. + $this->mPastTheEndIndex = ''; + $res->seek( $numRows - 1 ); + $row = $res->fetchRow(); + $lastIndex = $row[$this->mIndexField]; + } + } else { + $firstIndex = ''; + $lastIndex = ''; + $this->mPastTheEndRow = null; + $this->mPastTheEndIndex = ''; + } + + if ( $this->mIsBackwards ) { + $this->mIsFirst = ( $numRows < $limit ); + $this->mIsLast = ( $offset == '' ); + $this->mLastShown = $firstIndex; + $this->mFirstShown = $lastIndex; + } else { + $this->mIsFirst = ( $offset == '' ); + $this->mIsLast = ( $numRows < $limit ); + $this->mLastShown = $lastIndex; + $this->mFirstShown = $firstIndex; + } + } + + /** + * Do a query with specified parameters, rather than using the object context + * + * @param string $offset Index offset, inclusive + * @param integer $limit Exact query limit + * @param boolean $descending Query direction, false for ascending, true for descending + * @return ResultWrapper + */ + function reallyDoQuery( $offset, $limit, $ascending ) { + $fname = __METHOD__ . ' (' . get_class( $this ) . ')'; + $info = $this->getQueryInfo(); + $tables = $info['tables']; + $fields = $info['fields']; + $conds = isset( $info['conds'] ) ? $info['conds'] : array(); + $options = isset( $info['options'] ) ? $info['options'] : array(); + if ( $ascending ) { + $options['ORDER BY'] = $this->mIndexField; + $operator = '>'; + } else { + $options['ORDER BY'] = $this->mIndexField . ' DESC'; + $operator = '<'; + } + if ( $offset != '' ) { + $conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset ); + } + $options['LIMIT'] = intval( $limit ); + $res = $this->mDb->select( $tables, $fields, $conds, $fname, $options ); + return new ResultWrapper( $this->mDb, $res ); + } + + /** + * Get the formatted result list. Calls getStartBody(), formatRow() and + * getEndBody(), concatenates the results and returns them. + */ + function getBody() { + if ( !$this->mQueryDone ) { + $this->doQuery(); + } + # Don't use any extra rows returned by the query + $numRows = min( $this->mResult->numRows(), $this->mLimit ); + + $s = $this->getStartBody(); + if ( $numRows ) { + if ( $this->mIsBackwards ) { + for ( $i = $numRows - 1; $i >= 0; $i-- ) { + $this->mResult->seek( $i ); + $row = $this->mResult->fetchObject(); + $s .= $this->formatRow( $row ); + } + } else { + $this->mResult->seek( 0 ); + for ( $i = 0; $i < $numRows; $i++ ) { + $row = $this->mResult->fetchObject(); + $s .= $this->formatRow( $row ); + } + } + } else { + $s .= $this->getEmptyBody(); + } + $s .= $this->getEndBody(); + return $s; + } + + /** + * Make a self-link + */ + function makeLink($text, $query = NULL) { + if ( $query === null ) { + return $text; + } else { + return $this->getSkin()->makeKnownLinkObj( $this->getTitle(), $text, + wfArrayToCGI( $query, $this->getDefaultQuery() ) ); + } + } + + /** + * Hook into getBody(), allows text to be inserted at the start. This + * will be called even if there are no rows in the result set. + */ + function getStartBody() { + return ''; + } + + /** + * Hook into getBody() for the end of the list + */ + function getEndBody() { + return ''; + } + + /** + * Hook into getBody(), for the bit between the start and the + * end when there are no rows + */ + function getEmptyBody() { + return ''; + } + + /** + * Title used for self-links. Override this if you want to be able to + * use a title other than $wgTitle + */ + function getTitle() { + return $GLOBALS['wgTitle']; + } + + /** + * Get the current skin. This can be overridden if necessary. + */ + function getSkin() { + if ( !isset( $this->mSkin ) ) { + global $wgUser; + $this->mSkin = $wgUser->getSkin(); + } + return $this->mSkin; + } + + /** + * Get an array of query parameters that should be put into self-links. + * By default, all parameters passed in the URL are used, except for a + * short blacklist. + */ + function getDefaultQuery() { + if ( !isset( $this->mDefaultQuery ) ) { + $this->mDefaultQuery = $_GET; + unset( $this->mDefaultQuery['title'] ); + unset( $this->mDefaultQuery['dir'] ); + unset( $this->mDefaultQuery['offset'] ); + unset( $this->mDefaultQuery['limit'] ); + } + return $this->mDefaultQuery; + } + + /** + * Get the number of rows in the result set + */ + function getNumRows() { + if ( !$this->mQueryDone ) { + $this->doQuery(); + } + return $this->mResult->numRows(); + } + + /** + * Get a query array for the prev, next, first and last links. + */ + function getPagingQueries() { + if ( !$this->mQueryDone ) { + $this->doQuery(); + } + + # Don't announce the limit everywhere if it's the default + $urlLimit = $this->mLimit == $this->mDefaultLimit ? '' : $this->mLimit; + + if ( $this->mIsFirst ) { + $prev = false; + $first = false; + } else { + $prev = array( 'dir' => 'prev', 'offset' => $this->mFirstShown, 'limit' => $urlLimit ); + $first = array( 'limit' => $urlLimit ); + } + if ( $this->mIsLast ) { + $next = false; + $last = false; + } else { + $next = array( 'offset' => $this->mLastShown, 'limit' => $urlLimit ); + $last = array( 'dir' => 'prev', 'limit' => $urlLimit ); + } + return compact( 'prev', 'next', 'first', 'last' ); + } + + /** + * Get paging links. If a link is disabled, the item from $disabledTexts will + * be used. If there is no such item, the unlinked text from $linkTexts will + * be used. Both $linkTexts and $disabledTexts are arrays of HTML. + */ + function getPagingLinks( $linkTexts, $disabledTexts = array() ) { + $queries = $this->getPagingQueries(); + $links = array(); + foreach ( $queries as $type => $query ) { + if ( $query !== false ) { + $links[$type] = $this->makeLink( $linkTexts[$type], $queries[$type] ); + } elseif ( isset( $disabledTexts[$type] ) ) { + $links[$type] = $disabledTexts[$type]; + } else { + $links[$type] = $linkTexts[$type]; + } + } + return $links; + } + + function getLimitLinks() { + global $wgLang; + $links = array(); + if ( $this->mIsBackwards ) { + $offset = $this->mPastTheEndIndex; + } else { + $offset = $this->mOffset; + } + foreach ( $this->mLimitsShown as $limit ) { + $links[] = $this->makeLink( $wgLang->formatNum( $limit ), + array( 'offset' => $offset, 'limit' => $limit ) ); + } + return $links; + } + + /** + * Abstract formatting function. This should return an HTML string + * representing the result row $row. Rows will be concatenated and + * returned by getBody() + */ + abstract function formatRow( $row ); + + /** + * This function should be overridden to provide all parameters + * needed for the main paged query. It returns an associative + * array with the following elements: + * tables => Table(s) for passing to Database::select() + * fields => Field(s) for passing to Database::select(), may be * + * conds => WHERE conditions + * options => option array + */ + abstract function getQueryInfo(); + + /** + * This function should be overridden to return the name of the + * index field. + */ + abstract function getIndexField(); +} + +/** + * IndexPager with a formatted navigation bar + */ +abstract class ReverseChronologicalPager extends IndexPager { + public $mDefaultDirection = true; + + function __construct() { + parent::__construct(); + } + + function getNavigationBar() { + global $wgLang; + + if ( isset( $this->mNavigationBar ) ) { + return $this->mNavigationBar; + } + $linkTexts = array( + 'prev' => wfMsgHtml( "prevn", $this->mLimit ), + 'next' => wfMsgHtml( 'nextn', $this->mLimit ), + 'first' => wfMsgHtml('histlast'), + 'last' => wfMsgHtml( 'histfirst' ) + ); + + $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; + } +} + +/** + * Table-based display with a user-selectable sort order + */ +abstract class TablePager extends IndexPager { + var $mSort; + var $mCurrentRow; + + function __construct() { + global $wgRequest; + $this->mSort = $wgRequest->getText( 'sort' ); + if ( !array_key_exists( $this->mSort, $this->getFieldNames() ) ) { + $this->mSort = $this->getDefaultSort(); + } + if ( $wgRequest->getBool( 'asc' ) ) { + $this->mDefaultDirection = false; + } elseif ( $wgRequest->getBool( 'desc' ) ) { + $this->mDefaultDirection = true; + } /* Else leave it at whatever the class default is */ + + parent::__construct(); + } + + function getStartBody() { + global $wgStylePath; + $tableClass = htmlspecialchars( $this->getTableClass() ); + $sortClass = htmlspecialchars( $this->getSortHeaderClass() ); + + $s = "<table border='1' class=\"$tableClass\"><thead><tr>\n"; + $fields = $this->getFieldNames(); + + # Make table header + foreach ( $fields as $field => $name ) { + if ( strval( $name ) == '' ) { + $s .= "<th> </th>\n"; + } elseif ( $this->isFieldSortable( $field ) ) { + $query = array( 'sort' => $field, 'limit' => $this->mLimit ); + if ( $field == $this->mSort ) { + # This is the sorted column + # Prepare a link that goes in the other sort order + if ( $this->mDefaultDirection ) { + # Descending + $image = 'Arr_u.png'; + $query['asc'] = '1'; + $query['desc'] = ''; + $alt = htmlspecialchars( wfMsg( 'descending_abbrev' ) ); + } else { + # Ascending + $image = 'Arr_d.png'; + $query['asc'] = ''; + $query['desc'] = '1'; + $alt = htmlspecialchars( wfMsg( 'ascending_abbrev' ) ); + } + $image = htmlspecialchars( "$wgStylePath/common/images/$image" ); + $link = $this->makeLink( + "<img width=\"12\" height=\"12\" alt=\"$alt\" src=\"$image\" />" . + htmlspecialchars( $name ), $query ); + $s .= "<th class=\"$sortClass\">$link</th>\n"; + } else { + $s .= '<th>' . $this->makeLink( htmlspecialchars( $name ), $query ) . "</th>\n"; + } + } else { + $s .= '<th>' . htmlspecialchars( $name ) . "</th>\n"; + } + } + $s .= "</tr></thead><tbody>\n"; + return $s; + } + + function getEndBody() { + return '</tbody></table>'; + } + + function getEmptyBody() { + $colspan = count( $this->getFieldNames() ); + $msgEmpty = wfMsgHtml( 'table_pager_empty' ); + return "<tr><td colspan=\"$colspan\">$msgEmpty</td></tr>\n"; + } + + function formatRow( $row ) { + $s = "<tr>\n"; + $fieldNames = $this->getFieldNames(); + $this->mCurrentRow = $row; # In case formatValue needs to know + foreach ( $fieldNames as $field => $name ) { + $value = isset( $row->$field ) ? $row->$field : null; + $formatted = strval( $this->formatValue( $field, $value ) ); + if ( $formatted == '' ) { + $formatted = ' '; + } + $class = 'TablePager_col_' . htmlspecialchars( $field ); + $s .= "<td class=\"$class\">$formatted</td>\n"; + } + $s .= "</tr>\n"; + return $s; + } + + function getIndexField() { + return $this->mSort; + } + + function getTableClass() { + return 'TablePager'; + } + + function getNavClass() { + return 'TablePager_nav'; + } + + function getSortHeaderClass() { + return 'TablePager_sort'; + } + + /** + * A navigation bar with images + */ + function getNavigationBar() { + global $wgStylePath, $wgContLang; + $path = "$wgStylePath/common/images"; + $labels = array( + 'first' => 'table_pager_first', + 'prev' => 'table_pager_prev', + 'next' => 'table_pager_next', + 'last' => 'table_pager_last', + ); + $images = array( + 'first' => $wgContLang->isRTL() ? 'arrow_last_25.png' : 'arrow_first_25.png', + 'prev' => $wgContLang->isRTL() ? 'arrow_right_25.png' : 'arrow_left_25.png', + 'next' => $wgContLang->isRTL() ? 'arrow_left_25.png' : 'arrow_right_25.png', + 'last' => $wgContLang->isRTL() ? 'arrow_first_25.png' : 'arrow_last_25.png', + ); + $disabledImages = array( + 'first' => $wgContLang->isRTL() ? 'arrow_disabled_last_25.png' : 'arrow_disabled_first_25.png', + 'prev' => $wgContLang->isRTL() ? 'arrow_disabled_right_25.png' : 'arrow_disabled_left_25.png', + 'next' => $wgContLang->isRTL() ? 'arrow_disabled_left_25.png' : 'arrow_disabled_right_25.png', + 'last' => $wgContLang->isRTL() ? 'arrow_disabled_first_25.png' : 'arrow_disabled_last_25.png', + ); + + $linkTexts = array(); + $disabledTexts = array(); + foreach ( $labels as $type => $label ) { + $msgLabel = wfMsgHtml( $label ); + $linkTexts[$type] = "<img src=\"$path/{$images[$type]}\" alt=\"$msgLabel\"/><br/>$msgLabel"; + $disabledTexts[$type] = "<img src=\"$path/{$disabledImages[$type]}\" alt=\"$msgLabel\"/><br/>$msgLabel"; + } + $links = $this->getPagingLinks( $linkTexts, $disabledTexts ); + + $navClass = htmlspecialchars( $this->getNavClass() ); + $s = "<table class=\"$navClass\" align=\"center\" cellpadding=\"3\"><tr>"; + $cellAttrs = 'valign="top" align="center" width="' . 100 / count( $links ) . '%"'; + foreach ( $labels as $type => $label ) { + $s .= "<td $cellAttrs>{$links[$type]}</td>\n"; + } + $s .= '</tr></table>'; + return $s; + } + + /** + * Get a <select> element which has options for each of the allowed limits + */ + function getLimitSelect() { + global $wgLang; + $s = "<select name=\"limit\">"; + foreach ( $this->mLimitsShown as $limit ) { + $selected = $limit == $this->mLimit ? 'selected="selected"' : ''; + $formattedLimit = $wgLang->formatNum( $limit ); + $s .= "<option value=\"$limit\" $selected>$formattedLimit</option>\n"; + } + $s .= "</select>"; + return $s; + } + + /** + * Get <input type="hidden"> elements for use in a method="get" form. + * Resubmits all defined elements of the $_GET array, except for a + * blacklist, passed in the $blacklist parameter. + */ + function getHiddenFields( $blacklist = array() ) { + $blacklist = (array)$blacklist; + $query = $_GET; + foreach ( $blacklist as $name ) { + unset( $query[$name] ); + } + $s = ''; + foreach ( $query as $name => $value ) { + $encName = htmlspecialchars( $name ); + $encValue = htmlspecialchars( $value ); + $s .= "<input type=\"hidden\" name=\"$encName\" value=\"$encValue\"/>\n"; + } + return $s; + } + + /** + * Get a form containing a limit selection dropdown + */ + function getLimitForm() { + # Make the select with some explanatory text + $url = $this->getTitle()->escapeLocalURL(); + $msgSubmit = wfMsgHtml( 'table_pager_limit_submit' ); + return + "<form method=\"get\" action=\"$url\">" . + wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) . + "\n<input type=\"submit\" value=\"$msgSubmit\"/>\n" . + $this->getHiddenFields( 'limit' ) . + "</form>\n"; + } + + /** + * Return true if the named field should be sortable by the UI, false otherwise + * @param string $field + */ + abstract function isFieldSortable( $field ); + + /** + * Format a table cell. The return value should be HTML, but use an empty string + * not for empty cells. Do not include the <td> and </td>. + * + * @param string $name The database field name + * @param string $value The value retrieved from the database + * + * The current result row is available as $this->mCurrentRow, in case you need + * more context. + */ + abstract function formatValue( $name, $value ); + + /** + * The database field name used as a default sort order + */ + abstract function getDefaultSort(); + + /** + * An array mapping database field names to a textual description of the field + * name, for use in the table header. The description should be plain text, it + * will be HTML-escaped later. + */ + abstract function getFieldNames(); +} +?> diff --git a/includes/Parser.php b/includes/Parser.php index 31976baf..76783448 100644 --- a/includes/Parser.php +++ b/includes/Parser.php @@ -13,25 +13,13 @@ */ define( 'MW_PARSER_VERSION', '1.6.1' ); -/** - * Variable substitution O(N^2) attack - * - * Without countermeasures, it would be possible to attack the parser by saving - * a page filled with a large number of inclusions of large pages. The size of - * the generated page would be proportional to the square of the input size. - * Hence, we limit the number of inclusions of any given page, thus bringing any - * attack back to O(N). - */ - -define( 'MAX_INCLUDE_REPEAT', 100 ); -define( 'MAX_INCLUDE_SIZE', 1000000 ); // 1 Million - define( 'RLH_FOR_UPDATE', 1 ); # Allowed values for $mOutputType define( 'OT_HTML', 1 ); define( 'OT_WIKI', 2 ); define( 'OT_MSG' , 3 ); +define( 'OT_PREPROCESS', 4 ); # Flags for setFunctionHook define( 'SFH_NO_HASH', 1 ); @@ -90,7 +78,8 @@ define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 ); * settings: * $wgUseTex*, $wgUseDynamicDates*, $wgInterwikiMagic*, * $wgNamespacesWithSubpages, $wgAllowExternalImages*, - * $wgLocaltimezone, $wgAllowSpecialInclusion* + * $wgLocaltimezone, $wgAllowSpecialInclusion*, + * $wgMaxArticleSize* * * * only within ParserOptions * </pre> @@ -109,6 +98,7 @@ class Parser var $mOutput, $mAutonumber, $mDTopen, $mStripState = array(); var $mIncludeCount, $mArgStack, $mLastSection, $mInPre; var $mInterwikiLinkHolders, $mLinkHolders, $mUniqPrefix; + var $mIncludeSizes; var $mTemplates, // cache of already loaded templates, avoids // multiple SQL queries for the same string $mTemplatePath; // stores an unsorted hash of all the templates already loaded @@ -119,6 +109,7 @@ class Parser var $mOptions, // ParserOptions object $mTitle, // Title context, used for self-link rendering and similar things $mOutputType, // Output type, one of the OT_xxx constants + $ot, // Shortcut alias, see setOutputType() $mRevisionId; // ID to display in {{REVISIONID}} tags /**#@-*/ @@ -148,31 +139,35 @@ class Parser $this->setHook( 'pre', array( $this, 'renderPreTag' ) ); - $this->setFunctionHook( MAG_NS, array( 'CoreParserFunctions', 'ns' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_URLENCODE, array( 'CoreParserFunctions', 'urlencode' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_LCFIRST, array( 'CoreParserFunctions', 'lcfirst' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_UCFIRST, array( 'CoreParserFunctions', 'ucfirst' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_LC, array( 'CoreParserFunctions', 'lc' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_UC, array( 'CoreParserFunctions', 'uc' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_LOCALURL, array( 'CoreParserFunctions', 'localurl' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_LOCALURLE, array( 'CoreParserFunctions', 'localurle' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_FULLURL, array( 'CoreParserFunctions', 'fullurl' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_FULLURLE, array( 'CoreParserFunctions', 'fullurle' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_FORMATNUM, array( 'CoreParserFunctions', 'formatnum' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_GRAMMAR, array( 'CoreParserFunctions', 'grammar' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_PLURAL, array( 'CoreParserFunctions', 'plural' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_NUMBEROFPAGES, array( 'CoreParserFunctions', 'numberofpages' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_NUMBEROFUSERS, array( 'CoreParserFunctions', 'numberofusers' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_NUMBEROFARTICLES, array( 'CoreParserFunctions', 'numberofarticles' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_NUMBEROFFILES, array( 'CoreParserFunctions', 'numberoffiles' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_NUMBEROFADMINS, array( 'CoreParserFunctions', 'numberofadmins' ), SFH_NO_HASH ); - $this->setFunctionHook( MAG_LANGUAGE, array( 'CoreParserFunctions', 'language' ), SFH_NO_HASH ); + $this->setFunctionHook( 'int', array( 'CoreParserFunctions', 'intFunction' ), SFH_NO_HASH ); + $this->setFunctionHook( 'ns', array( 'CoreParserFunctions', 'ns' ), SFH_NO_HASH ); + $this->setFunctionHook( 'urlencode', array( 'CoreParserFunctions', 'urlencode' ), SFH_NO_HASH ); + $this->setFunctionHook( 'lcfirst', array( 'CoreParserFunctions', 'lcfirst' ), SFH_NO_HASH ); + $this->setFunctionHook( 'ucfirst', array( 'CoreParserFunctions', 'ucfirst' ), SFH_NO_HASH ); + $this->setFunctionHook( 'lc', array( 'CoreParserFunctions', 'lc' ), SFH_NO_HASH ); + $this->setFunctionHook( 'uc', array( 'CoreParserFunctions', 'uc' ), SFH_NO_HASH ); + $this->setFunctionHook( 'localurl', array( 'CoreParserFunctions', 'localurl' ), SFH_NO_HASH ); + $this->setFunctionHook( 'localurle', array( 'CoreParserFunctions', 'localurle' ), SFH_NO_HASH ); + $this->setFunctionHook( 'fullurl', array( 'CoreParserFunctions', 'fullurl' ), SFH_NO_HASH ); + $this->setFunctionHook( 'fullurle', array( 'CoreParserFunctions', 'fullurle' ), SFH_NO_HASH ); + $this->setFunctionHook( 'formatnum', array( 'CoreParserFunctions', 'formatnum' ), SFH_NO_HASH ); + $this->setFunctionHook( 'grammar', array( 'CoreParserFunctions', 'grammar' ), SFH_NO_HASH ); + $this->setFunctionHook( 'plural', array( 'CoreParserFunctions', 'plural' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofpages', array( 'CoreParserFunctions', 'numberofpages' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofusers', array( 'CoreParserFunctions', 'numberofusers' ), SFH_NO_HASH ); + $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( '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 ); + $this->setFunctionHook( 'anchorencode', array( 'CoreParserFunctions', 'anchorencode' ), SFH_NO_HASH ); if ( $wgAllowDisplayTitle ) { - $this->setFunctionHook( MAG_DISPLAYTITLE, array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH ); + $this->setFunctionHook( 'displaytitle', array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH ); } if ( $wgAllowSlowParserFunctions ) { - $this->setFunctionHook( MAG_PAGESINNAMESPACE, array( 'CoreParserFunctions', 'pagesinnamespace' ), SFH_NO_HASH ); + $this->setFunctionHook( 'pagesinnamespace', array( 'CoreParserFunctions', 'pagesinnamespace' ), SFH_NO_HASH ); } $this->initialiseVariables(); @@ -187,6 +182,7 @@ class Parser * @private */ function clearState() { + wfProfileIn( __METHOD__ ); if ( $this->mFirstCall ) { $this->firstCallInit(); } @@ -226,8 +222,25 @@ class Parser $this->mShowToc = true; $this->mForceTocPosition = false; + $this->mIncludeSizes = array( + 'pre-expand' => 0, + 'post-expand' => 0, + 'arg' => 0 + ); wfRunHooks( 'ParserClearState', array( &$this ) ); + wfProfileOut( __METHOD__ ); + } + + function setOutputType( $ot ) { + $this->mOutputType = $ot; + // Shortcut alias + $this->ot = array( + 'html' => $ot == OT_HTML, + 'wiki' => $ot == OT_WIKI, + 'msg' => $ot == OT_MSG, + 'pre' => $ot == OT_PREPROCESS, + ); } /** @@ -235,7 +248,7 @@ class Parser * * @public */ - function UniqPrefix() { + function uniqPrefix() { return $this->mUniqPrefix; } @@ -259,7 +272,7 @@ class Parser */ global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang; - $fname = 'Parser::parse'; + $fname = 'Parser::parse-' . wfGetCaller(); wfProfileIn( $fname ); if ( $clearState ) { @@ -268,8 +281,11 @@ class Parser $this->mOptions = $options; $this->mTitle =& $title; - $this->mRevisionId = $revid; - $this->mOutputType = OT_HTML; + $oldRevisionId = $this->mRevisionId; + if( $revid !== null ) { + $this->mRevisionId = $revid; + } + $this->setOutputType( OT_HTML ); //$text = $this->strip( $text, $this->mStripState ); // VOODOO MAGIC FIX! Sometimes the above segfaults in PHP5. @@ -279,12 +295,6 @@ class Parser $text = $this->strip( $text, $x ); wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$x ) ); - # Hook to suspend the parser in this state - if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$x ) ) ) { - wfProfileOut( $fname ); - return $text ; - } - $text = $this->internalParse( $text ); $text = $this->unstrip( $text, $this->mStripState ); @@ -321,8 +331,8 @@ class Parser } else { # attempt to sanitize at least some nesting problems # (bug #2702 and quite a few others) - $tidyregs = array( - # ''Something [http://www.cool.com cool''] --> + $tidyregs = array( + # ''Something [http://www.cool.com cool''] --> # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a> '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' => '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9', @@ -337,10 +347,10 @@ class Parser '\\1\\3<div\\5>\\6</div>\\8\\9', # remove empty italic or bold tag pairs, some # introduced by rules above - '/<([bi])><\/\\1>/' => '' + '/<([bi])><\/\\1>/' => '', ); - $text = preg_replace( + $text = preg_replace( array_keys( $tidyregs ), array_values( $tidyregs ), $text ); @@ -348,13 +358,63 @@ class Parser wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) ); + # Information on include size limits, for the benefit of users who try to skirt them + if ( max( $this->mIncludeSizes ) > 1000 ) { + $max = $this->mOptions->getMaxIncludeSize(); + $text .= "<!-- \n" . + "Pre-expand include size: {$this->mIncludeSizes['pre-expand']} bytes\n" . + "Post-expand include size: {$this->mIncludeSizes['post-expand']} bytes\n" . + "Template argument size: {$this->mIncludeSizes['arg']} bytes\n" . + "Maximum: $max bytes\n" . + "-->\n"; + } $this->mOutput->setText( $text ); + $this->mRevisionId = $oldRevisionId; wfProfileOut( $fname ); return $this->mOutput; } /** + * Recursive parser entry point that can be called from an extension tag + * hook. + */ + function recursiveTagParse( $text ) { + wfProfileIn( __METHOD__ ); + $x =& $this->mStripState; + wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$x ) ); + $text = $this->strip( $text, $x ); + wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$x ) ); + $text = $this->internalParse( $text ); + wfProfileOut( __METHOD__ ); + return $text; + } + + /** + * Expand templates and variables in the text, producing valid, static wikitext. + * Also removes comments. + */ + function preprocess( $text, $title, $options ) { + wfProfileIn( __METHOD__ ); + $this->clearState(); + $this->setOutputType( OT_PREPROCESS ); + $this->mOptions = $options; + $this->mTitle = $title; + $x =& $this->mStripState; + wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$x ) ); + $text = $this->strip( $text, $x ); + wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$x ) ); + if ( $this->mOptions->getRemoveComments() ) { + $text = Sanitizer::removeHTMLcomments( $text ); + } + $text = $this->replaceVariables( $text ); + $text = $this->unstrip( $text, $x ); + $text = $this->unstripNowiki( $text, $x ); + wfProfileOut( __METHOD__ ); + return $text; + } + + /** * Get a random string * * @private @@ -391,8 +451,7 @@ class Parser * @static */ function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){ - $rand = Parser::getRandomString(); - $n = 1; + static $n = 1; $stripped = ''; $matches = array(); @@ -419,7 +478,7 @@ class Parser $inside = $p[4]; } - $marker = "$uniq_prefix-$element-$rand" . sprintf('%08X', $n++) . '-QINU'; + $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . '-QINU'; $stripped .= $marker; if ( $close === '/>' ) { @@ -470,11 +529,10 @@ class Parser * @private */ function strip( $text, &$state, $stripcomments = false , $dontstrip = array () ) { + wfProfileIn( __METHOD__ ); $render = ($this->mOutputType == OT_HTML); - # Replace any instances of the placeholders $uniq_prefix = $this->mUniqPrefix; - #$text = str_replace( $uniq_prefix, wfHtmlEscapeFirst( $uniq_prefix ), $text ); $commentState = array(); $elements = array_merge( @@ -501,6 +559,7 @@ class Parser list( $element, $content, $params, $tag ) = $data; if( $render ) { $tagName = strtolower( $element ); + wfProfileIn( __METHOD__."-render-$tagName" ); switch( $tagName ) { case '!--': // Comment @@ -535,17 +594,25 @@ class Parser throw new MWException( "Invalid call hook $element" ); } } + wfProfileOut( __METHOD__."-render-$tagName" ); } else { // Just stripping tags; keep the source $output = $tag; } + + // Unstrip the output, because unstrip() is no longer recursive so + // it won't do it itself + $output = $this->unstrip( $output, $state ); + if( !$stripcomments && $element == '!--' ) { $commentState[$marker] = $output; + } elseif ( $element == 'html' || $element == 'nowiki' ) { + $state['nowiki'][$marker] = $output; } else { - $state[$element][$marker] = $output; + $state['general'][$marker] = $output; } } - + # Unstrip comments unless explicitly told otherwise. # (The comments are always stripped prior to this point, so as to # not invoke any extension tags / parser hooks contained within @@ -555,6 +622,7 @@ class Parser $text = strtr( $text, $commentState ); } + wfProfileOut( __METHOD__ ); return $text; } @@ -565,20 +633,14 @@ class Parser * @private */ function unstrip( $text, &$state ) { - if ( !is_array( $state ) ) { + if ( !isset( $state['general'] ) ) { return $text; } - $replacements = array(); - foreach( $state as $tag => $contentDict ) { - if( $tag != 'nowiki' && $tag != 'html' ) { - foreach( $contentDict as $uniq => $content ) { - $replacements[$uniq] = $content; - } - } - } - $text = strtr( $text, $replacements ); - + wfProfileIn( __METHOD__ ); + # TODO: good candidate for FSS + $text = strtr( $text, $state['general'] ); + wfProfileOut( __METHOD__ ); return $text; } @@ -588,20 +650,15 @@ class Parser * @private */ function unstripNoWiki( $text, &$state ) { - if ( !is_array( $state ) ) { + if ( !isset( $state['nowiki'] ) ) { return $text; } - $replacements = array(); - foreach( $state as $tag => $contentDict ) { - if( $tag == 'nowiki' || $tag == 'html' ) { - foreach( $contentDict as $uniq => $content ) { - $replacements[$uniq] = $content; - } - } - } - $text = strtr( $text, $replacements ); - + wfProfileIn( __METHOD__ ); + # TODO: good candidate for FSS + $text = strtr( $text, $state['nowiki'] ); + wfProfileOut( __METHOD__ ); + return $text; } @@ -617,7 +674,7 @@ class Parser if ( !$state ) { $state = array(); } - $state['item'][$rnd] = $text; + $state['general'][$rnd] = $text; return $rnd; } @@ -797,13 +854,13 @@ class Parser } $after = substr ( $x , 1 ) ; if ( $fc == '!' ) $after = str_replace ( '!!' , '||' , $after ) ; - + // Split up multiple cells on the same line. // FIXME: This can result in improper nesting of tags processed // by earlier parser steps, but should avoid splitting up eg // attribute values containing literal "||". $after = wfExplodeMarkup( '||', $after ); - + $t[$k] = '' ; # Loop through each table cell @@ -877,11 +934,17 @@ class Parser $fname = 'Parser::internalParse'; wfProfileIn( $fname ); + # Hook to suspend the parser in this state + if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$x ) ) ) { + wfProfileOut( $fname ); + return $text ; + } + # Remove <noinclude> tags and <includeonly> sections $text = strtr( $text, array( '<onlyinclude>' => '' , '</onlyinclude>' => '' ) ); $text = strtr( $text, array( '<noinclude>' => '', '</noinclude>' => '') ); $text = preg_replace( '/<includeonly>.*?<\/includeonly>/s', '', $text ); - + $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ) ); $text = $this->replaceVariables( $text, $args ); @@ -923,9 +986,52 @@ class Parser * @private */ function &doMagicLinks( &$text ) { - $text = $this->magicISBN( $text ); - $text = $this->magicRFC( $text, 'RFC ', 'rfcurl' ); - $text = $this->magicRFC( $text, 'PMID ', 'pubmedurl' ); + wfProfileIn( __METHOD__ ); + $text = preg_replace_callback( + '!(?: # Start cases + <a.*?</a> | # Skip link text + <.*?> | # Skip stuff inside HTML elements + (?:RFC|PMID)\s+([0-9]+) | # RFC or PMID, capture number as m[1] + ISBN\s+([0-9Xx-]+) # ISBN, capture number as m[2] + )!x', array( &$this, 'magicLinkCallback' ), $text ); + wfProfileOut( __METHOD__ ); + return $text; + } + + function magicLinkCallback( $m ) { + if ( substr( $m[0], 0, 1 ) == '<' ) { + # Skip HTML element + return $m[0]; + } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) { + $isbn = $m[2]; + $num = strtr( $isbn, array( + '-' => '', + ' ' => '', + 'x' => 'X', + )); + $titleObj = Title::makeTitle( NS_SPECIAL, 'Booksources' ); + $text = '<a href="' . + $titleObj->escapeLocalUrl( "isbn=$num" ) . + "\" class=\"internal\">ISBN $isbn</a>"; + } else { + if ( substr( $m[0], 0, 3 ) == 'RFC' ) { + $keyword = 'RFC'; + $urlmsg = 'rfcurl'; + $id = $m[1]; + } elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) { + $keyword = 'PMID'; + $urlmsg = 'pubmedurl'; + $id = $m[1]; + } else { + throw new MWException( __METHOD__.': unrecognised match type "' . + substr($m[0], 0, 20 ) . '"' ); + } + + $url = wfMsg( $urlmsg, $id); + $sk =& $this->mOptions->getSkin(); + $la = $sk->getExternalLinkAttributes( $url, $keyword.$id ); + $text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>"; + } return $text; } @@ -1193,13 +1299,8 @@ class Parser } $text = $wgContLang->markNoConversion($text); - - # Normalize any HTML entities in input. They will be - # re-escaped by makeExternalLink(). - $url = Sanitizer::decodeCharReferences( $url ); - # Escape any control characters introduced by the above step - $url = preg_replace( '/[\][<>"\\x00-\\x20\\x7F]/e', "urlencode('\\0')", $url ); + $url = Sanitizer::cleanUrl( $url ); # Process the trail (i.e. everything after this link up until start of the next link), # replacing any non-bracketed links @@ -1280,12 +1381,7 @@ class Parser $url = substr( $url, 0, -$numSepChars ); } - # Normalize any HTML entities in input. They will be - # re-escaped by makeExternalLink() or maybeMakeExternalImage() - $url = Sanitizer::decodeCharReferences( $url ); - - # Escape any control characters introduced by the above step - $url = preg_replace( '/[\][<>"\\x00-\\x20\\x7F]/e', "urlencode('\\0')", $url ); + $url = Sanitizer::cleanUrl( $url ); # Is this an external image? $text = $this->maybeMakeExternalImage( $url ); @@ -1316,7 +1412,7 @@ class Parser * the URL differently; as a workaround, just use the output for * statistical records, not for actual linking/output. */ - function replaceUnusualEscapes( $url ) { + static function replaceUnusualEscapes( $url ) { return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', array( 'Parser', 'replaceUnusualEscapesCallback' ), $url ); } @@ -1327,7 +1423,7 @@ class Parser * @static * @private */ - function replaceUnusualEscapesCallback( $matches ) { + private static function replaceUnusualEscapesCallback( $matches ) { $char = urldecode( $matches[0] ); $ord = ord( $char ); // Is it an unsafe or HTTP reserved character according to RFC 1738? @@ -1397,7 +1493,7 @@ class Parser $useLinkPrefixExtension = $wgContLang->linkPrefixExtension(); if( is_null( $this->mTitle ) ) { - throw new MWException( 'nooo' ); + throw new MWException( __METHOD__.": \$this->mTitle is null\n" ); } $nottalk = !$this->mTitle->isTalkPage(); @@ -1412,10 +1508,8 @@ class Parser } $selflink = $this->mTitle->getPrefixedText(); - wfProfileOut( $fname.'-setup' ); - - $checkVariantLink = sizeof($wgContLang->getVariants())>1; $useSubpages = $this->areSubpagesAllowed(); + wfProfileOut( $fname.'-setup' ); # Loop for each link for ($k = 0; isset( $a[$k] ); $k++) { @@ -1438,6 +1532,7 @@ class Parser $might_be_img = false; + wfProfileIn( "$fname-e1" ); if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt $text = $m[2]; # If we get a ] at the beginning of $m[3] that means we have a link that's something like: @@ -1449,27 +1544,33 @@ class Parser # and no image is in sight. See bug 2095. # if( $text !== '' && - preg_match( "/^\](.*)/s", $m[3], $n ) && + substr( $m[3], 0, 1 ) === ']' && strpos($text, '[') !== false ) { $text .= ']'; # so that replaceExternalLinks($text) works later - $m[3] = $n[1]; + $m[3] = substr( $m[3], 1 ); } # fix up urlencoded title texts - if(preg_match('/%/', $m[1] )) + if( strpos( $m[1], '%' ) !== false ) { # Should anchors '#' also be rejected? $m[1] = str_replace( array('<', '>'), array('<', '>'), urldecode($m[1]) ); + } $trail = $m[3]; } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption $might_be_img = true; $text = $m[2]; - if(preg_match('/%/', $m[1] )) $m[1] = urldecode($m[1]); + if ( strpos( $m[1], '%' ) !== false ) { + $m[1] = urldecode($m[1]); + } $trail = ""; } else { # Invalid form; output directly $s .= $prefix . '[[' . $line ; + wfProfileOut( "$fname-e1" ); continue; } + wfProfileOut( "$fname-e1" ); + wfProfileIn( "$fname-misc" ); # Don't allow internal links to pages containing # PROTO: where PROTO is a valid URL protocol; these @@ -1492,38 +1593,37 @@ class Parser $link = substr($link, 1); } + wfProfileOut( "$fname-misc" ); + wfProfileIn( "$fname-title" ); $nt = Title::newFromText( $this->unstripNoWiki($link, $this->mStripState) ); if( !$nt ) { $s .= $prefix . '[[' . $line; + wfProfileOut( "$fname-title" ); continue; } - #check other language variants of the link - #if the article does not exist - if( $checkVariantLink - && $nt->getArticleID() == 0 ) { - $wgContLang->findVariantLink($link, $nt); - } - $ns = $nt->getNamespace(); $iw = $nt->getInterWiki(); - + wfProfileOut( "$fname-title" ); + if ($might_be_img) { # if this is actually an invalid link + wfProfileIn( "$fname-might_be_img" ); if ($ns == NS_IMAGE && $noforce) { #but might be an image $found = false; while (isset ($a[$k+1]) ) { #look at the next 'line' to see if we can close it there $spliced = array_splice( $a, $k + 1, 1 ); $next_line = array_shift( $spliced ); - if( preg_match("/^(.*?]].*?)]](.*)$/sD", $next_line, $m) ) { - # the first ]] closes the inner link, the second the image + $m = explode( ']]', $next_line, 3 ); + if ( count( $m ) == 3 ) { + # the first ]] closes the inner link, the second the image $found = true; - $text .= '[[' . $m[1]; + $text .= "[[{$m[0]}]]{$m[1]}"; $trail = $m[2]; break; - } elseif( preg_match("/^.*?]].*$/sD", $next_line, $m) ) { + } elseif ( count( $m ) == 2 ) { #if there's exactly one ]] that's fine, we'll keep looking - $text .= '[[' . $m[0]; + $text .= "[[{$m[0]}]]{$m[1]}"; } else { #if $next_line is invalid too, we need look no further $text .= '[[' . $next_line; @@ -1534,35 +1634,40 @@ class Parser # we couldn't find the end of this imageLink, so output it raw #but don't ignore what might be perfectly normal links in the text we've examined $text = $this->replaceInternalLinks($text); - $s .= $prefix . '[[' . $link . '|' . $text; + $s .= "{$prefix}[[$link|$text"; # note: no $trail, because without an end, there *is* no trail + wfProfileOut( "$fname-might_be_img" ); continue; } } else { #it's not an image, so output it raw - $s .= $prefix . '[[' . $link . '|' . $text; + $s .= "{$prefix}[[$link|$text"; # note: no $trail, because without an end, there *is* no trail + wfProfileOut( "$fname-might_be_img" ); continue; } + wfProfileOut( "$fname-might_be_img" ); } $wasblank = ( '' == $text ); if( $wasblank ) $text = $link; - # Link not escaped by : , create the various objects if( $noforce ) { # Interwikis + 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; + wfProfileOut( "$fname-interwiki" ); continue; } + wfProfileOut( "$fname-interwiki" ); if ( $ns == NS_IMAGE ) { wfProfileIn( "$fname-image" ); - if ( !wfIsBadImage( $nt->getDBkey() ) ) { + if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) { # recursively parse links inside the image caption # actually, this will parse them in any other parameters, too, # but it might be hard to fix that, and it doesn't matter ATM @@ -1630,12 +1735,13 @@ class Parser $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); continue; } elseif( $ns == NS_IMAGE ) { - $img = Image::newFromTitle( $nt ); + $img = new Image( $nt ); if( $img->exists() ) { // Force a blue link if the file exists; may be a remote // upload on the shared repository, and we want to see its // auto-generated page. $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); + $this->mOutput->addLink( $nt ); continue; } } @@ -1648,11 +1754,12 @@ class Parser /** * Make a link placeholder. The text returned can be later resolved to a real link with * replaceLinkHolders(). This is done for two reasons: firstly to avoid further - * parsing of interwiki links, and secondly to allow all extistence checks and + * parsing of interwiki links, and secondly to allow all existence checks and * article length checks (for stub links) to be bundled into a single query. * */ function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + wfProfileIn( __METHOD__ ); if ( ! is_object($nt) ) { # Fail gracefully $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}"; @@ -1674,6 +1781,7 @@ class Parser $retVal = '<!--LINK '. ($nr-1) ."-->{$trail}"; } } + wfProfileOut( __METHOD__ ); return $retVal; } @@ -1745,6 +1853,9 @@ class Parser wfProfileIn( $fname ); $ret = $target; # default return value is no change + # bug 7425 + $target = trim( $target ); + # Some namespaces don't allow subpages, # so only perform processing if subpages are allowed if( $this->areSubpagesAllowed() ) { @@ -2049,14 +2160,14 @@ class Parser function findColonNoLinks($str, &$before, &$after) { $fname = 'Parser::findColonNoLinks'; wfProfileIn( $fname ); - + $pos = strpos( $str, ':' ); if( $pos === false ) { // Nothing to find! wfProfileOut( $fname ); return false; } - + $lt = strpos( $str, '<' ); if( $lt === false || $lt > $pos ) { // Easy; no tag nesting to worry about @@ -2065,14 +2176,14 @@ class Parser wfProfileOut( $fname ); return $pos; } - + // Ugly state machine to walk through avoiding tags. $state = MW_COLON_STATE_TEXT; $stack = 0; $len = strlen( $str ); for( $i = 0; $i < $len; $i++ ) { $c = $str{$i}; - + switch( $state ) { // (Using the number is a performance hack for common cases) case 0: // MW_COLON_STATE_TEXT: @@ -2222,107 +2333,165 @@ class Parser $ts = time(); wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) ); + # Use the time zone + global $wgLocaltimezone; + if ( isset( $wgLocaltimezone ) ) { + $oldtz = getenv( 'TZ' ); + putenv( 'TZ='.$wgLocaltimezone ); + } + $localTimestamp = date( 'YmdHis', $ts ); + $localMonth = date( 'm', $ts ); + $localMonthName = date( 'n', $ts ); + $localDay = date( 'j', $ts ); + $localDay2 = date( 'd', $ts ); + $localDayOfWeek = date( 'w', $ts ); + $localWeek = date( 'W', $ts ); + $localYear = date( 'Y', $ts ); + $localHour = date( 'H', $ts ); + if ( isset( $wgLocaltimezone ) ) { + putenv( 'TZ='.$oldtz ); + } + switch ( $index ) { - case MAG_CURRENTMONTH: + case 'currentmonth': return $varCache[$index] = $wgContLang->formatNum( date( 'm', $ts ) ); - case MAG_CURRENTMONTHNAME: + case 'currentmonthname': return $varCache[$index] = $wgContLang->getMonthName( date( 'n', $ts ) ); - case MAG_CURRENTMONTHNAMEGEN: + case 'currentmonthnamegen': return $varCache[$index] = $wgContLang->getMonthNameGen( date( 'n', $ts ) ); - case MAG_CURRENTMONTHABBREV: + case 'currentmonthabbrev': return $varCache[$index] = $wgContLang->getMonthAbbreviation( date( 'n', $ts ) ); - case MAG_CURRENTDAY: + case 'currentday': return $varCache[$index] = $wgContLang->formatNum( date( 'j', $ts ) ); - case MAG_CURRENTDAY2: + case 'currentday2': return $varCache[$index] = $wgContLang->formatNum( date( 'd', $ts ) ); - case MAG_PAGENAME: + case 'localmonth': + return $varCache[$index] = $wgContLang->formatNum( $localMonth ); + case 'localmonthname': + return $varCache[$index] = $wgContLang->getMonthName( $localMonthName ); + case 'localmonthnamegen': + return $varCache[$index] = $wgContLang->getMonthNameGen( $localMonthName ); + case 'localmonthabbrev': + return $varCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName ); + case 'localday': + return $varCache[$index] = $wgContLang->formatNum( $localDay ); + case 'localday2': + return $varCache[$index] = $wgContLang->formatNum( $localDay2 ); + case 'pagename': return $this->mTitle->getText(); - case MAG_PAGENAMEE: + case 'pagenamee': return $this->mTitle->getPartialURL(); - case MAG_FULLPAGENAME: + case 'fullpagename': return $this->mTitle->getPrefixedText(); - case MAG_FULLPAGENAMEE: + case 'fullpagenamee': return $this->mTitle->getPrefixedURL(); - case MAG_SUBPAGENAME: + case 'subpagename': return $this->mTitle->getSubpageText(); - case MAG_SUBPAGENAMEE: + case 'subpagenamee': return $this->mTitle->getSubpageUrlForm(); - case MAG_BASEPAGENAME: + case 'basepagename': return $this->mTitle->getBaseText(); - case MAG_BASEPAGENAMEE: + case 'basepagenamee': return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) ); - case MAG_TALKPAGENAME: + case 'talkpagename': if( $this->mTitle->canTalk() ) { $talkPage = $this->mTitle->getTalkPage(); return $talkPage->getPrefixedText(); } else { return ''; } - case MAG_TALKPAGENAMEE: + case 'talkpagenamee': if( $this->mTitle->canTalk() ) { $talkPage = $this->mTitle->getTalkPage(); return $talkPage->getPrefixedUrl(); } else { return ''; } - case MAG_SUBJECTPAGENAME: + case 'subjectpagename': $subjPage = $this->mTitle->getSubjectPage(); return $subjPage->getPrefixedText(); - case MAG_SUBJECTPAGENAMEE: + case 'subjectpagenamee': $subjPage = $this->mTitle->getSubjectPage(); return $subjPage->getPrefixedUrl(); - case MAG_REVISIONID: + case 'revisionid': return $this->mRevisionId; - case MAG_NAMESPACE: + case 'revisionday': + return intval( substr( wfRevisionTimestamp( $this->mRevisionId ), 6, 2 ) ); + case 'revisionday2': + return substr( wfRevisionTimestamp( $this->mRevisionId ), 6, 2 ); + case 'revisionmonth': + return intval( substr( wfRevisionTimestamp( $this->mRevisionId ), 4, 2 ) ); + case 'revisionyear': + return substr( wfRevisionTimestamp( $this->mRevisionId ), 0, 4 ); + case 'revisiontimestamp': + return wfRevisionTimestamp( $this->mRevisionId ); + case 'namespace': return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) ); - case MAG_NAMESPACEE: + case 'namespacee': return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) ); - case MAG_TALKSPACE: + case 'talkspace': return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : ''; - case MAG_TALKSPACEE: + case 'talkspacee': return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : ''; - case MAG_SUBJECTSPACE: + case 'subjectspace': return $this->mTitle->getSubjectNsText(); - case MAG_SUBJECTSPACEE: + case 'subjectspacee': return( wfUrlencode( $this->mTitle->getSubjectNsText() ) ); - case MAG_CURRENTDAYNAME: + case 'currentdayname': return $varCache[$index] = $wgContLang->getWeekdayName( date( 'w', $ts ) + 1 ); - case MAG_CURRENTYEAR: + case 'currentyear': return $varCache[$index] = $wgContLang->formatNum( date( 'Y', $ts ), true ); - case MAG_CURRENTTIME: + case 'currenttime': return $varCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false ); - case MAG_CURRENTWEEK: + case 'currenthour': + return $varCache[$index] = $wgContLang->formatNum( date( 'H', $ts ), true ); + case 'currentweek': // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to // int to remove the padding return $varCache[$index] = $wgContLang->formatNum( (int)date( 'W', $ts ) ); - case MAG_CURRENTDOW: + case 'currentdow': return $varCache[$index] = $wgContLang->formatNum( date( 'w', $ts ) ); - case MAG_NUMBEROFARTICLES: + case 'localdayname': + return $varCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 ); + case 'localyear': + return $varCache[$index] = $wgContLang->formatNum( $localYear, true ); + case 'localtime': + return $varCache[$index] = $wgContLang->time( $localTimestamp, false, false ); + case 'localhour': + return $varCache[$index] = $wgContLang->formatNum( $localHour, true ); + case 'localweek': + // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to + // int to remove the padding + return $varCache[$index] = $wgContLang->formatNum( (int)$localWeek ); + case 'localdow': + return $varCache[$index] = $wgContLang->formatNum( $localDayOfWeek ); + case 'numberofarticles': return $varCache[$index] = $wgContLang->formatNum( wfNumberOfArticles() ); - case MAG_NUMBEROFFILES: + case 'numberoffiles': return $varCache[$index] = $wgContLang->formatNum( wfNumberOfFiles() ); - case MAG_NUMBEROFUSERS: + case 'numberofusers': return $varCache[$index] = $wgContLang->formatNum( wfNumberOfUsers() ); - case MAG_NUMBEROFPAGES: + case 'numberofpages': return $varCache[$index] = $wgContLang->formatNum( wfNumberOfPages() ); - case MAG_NUMBEROFADMINS: + case 'numberofadmins': return $varCache[$index] = $wgContLang->formatNum( wfNumberOfAdmins() ); - case MAG_CURRENTTIMESTAMP: + case 'currenttimestamp': return $varCache[$index] = wfTimestampNow(); - case MAG_CURRENTVERSION: - global $wgVersion; - return $wgVersion; - case MAG_SITENAME: + case 'localtimestamp': + return $varCache[$index] = $localTimestamp; + case 'currentversion': + return $varCache[$index] = SpecialVersion::getVersion(); + case 'sitename': return $wgSitename; - case MAG_SERVER: + case 'server': return $wgServer; - case MAG_SERVERNAME: + case 'servername': return $wgServerName; - case MAG_SCRIPTPATH: + case 'scriptpath': return $wgScriptPath; - case MAG_DIRECTIONMARK: + case 'directionmark': return $wgContLang->getDirMark(); - case MAG_CONTENTLANGUAGE: + case 'contentlanguage': global $wgContLanguageCode; return $wgContLanguageCode; default: @@ -2342,10 +2511,10 @@ class Parser function initialiseVariables() { $fname = 'Parser::initialiseVariables'; wfProfileIn( $fname ); - global $wgVariableIDs; + $variableIDs = MagicWord::getVariableIDs(); $this->mVariables = array(); - foreach ( $wgVariableIDs as $id ) { + foreach ( $variableIDs as $id ) { $mw =& MagicWord::get( $id ); $mw->addToArray( $this->mVariables, $id ); } @@ -2361,172 +2530,169 @@ class Parser * '{' => array( # opening parentheses * 'end' => '}', # closing parentheses * 'cb' => array(2 => callback, # replacement callback to call if {{..}} is found - * 4 => callback # replacement callback to call if {{{{..}}}} is found + * 3 => callback # replacement callback to call if {{{..}}} is found * ) * ) + * 'min' => 2, # Minimum parenthesis count in cb + * 'max' => 3, # Maximum parenthesis count in cb * @private */ function replace_callback ($text, $callbacks) { - wfProfileIn( __METHOD__ . '-self' ); + wfProfileIn( __METHOD__ ); $openingBraceStack = array(); # this array will hold a stack of parentheses which are not closed yet $lastOpeningBrace = -1; # last not closed parentheses - for ($i = 0; $i < strlen($text); $i++) { - # check for any opening brace - $rule = null; - $nextPos = -1; - foreach ($callbacks as $key => $value) { - $pos = strpos ($text, $key, $i); - if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)) { - $rule = $value; - $nextPos = $pos; - } + $validOpeningBraces = implode( '', array_keys( $callbacks ) ); + + $i = 0; + while ( $i < strlen( $text ) ) { + # Find next opening brace, closing brace or pipe + if ( $lastOpeningBrace == -1 ) { + $currentClosing = ''; + $search = $validOpeningBraces; + } else { + $currentClosing = $openingBraceStack[$lastOpeningBrace]['braceEnd']; + $search = $validOpeningBraces . '|' . $currentClosing; } - - if ($lastOpeningBrace >= 0) { - $pos = strpos ($text, $openingBraceStack[$lastOpeningBrace]['braceEnd'], $i); - - if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)){ - $rule = null; - $nextPos = $pos; - } - - $pos = strpos ($text, '|', $i); - - if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)){ - $rule = null; - $nextPos = $pos; + $rule = null; + $i += strcspn( $text, $search, $i ); + if ( $i < strlen( $text ) ) { + if ( $text[$i] == '|' ) { + $found = 'pipe'; + } elseif ( $text[$i] == $currentClosing ) { + $found = 'close'; + } elseif ( isset( $callbacks[$text[$i]] ) ) { + $found = 'open'; + $rule = $callbacks[$text[$i]]; + } else { + # Some versions of PHP have a strcspn which stops on null characters + # Ignore and continue + ++$i; + continue; } - } - - if ($nextPos == -1) + } else { + # All done break; + } - $i = $nextPos; - - # found openning brace, lets add it to parentheses stack - if (null != $rule) { + if ( $found == 'open' ) { + # found opening brace, let's add it to parentheses stack $piece = array('brace' => $text[$i], 'braceEnd' => $rule['end'], - 'count' => 1, 'title' => '', 'parts' => null); - # count openning brace characters - while ($i+1 < strlen($text) && $text[$i+1] == $piece['brace']) { - $piece['count']++; - $i++; - } - - $piece['startAt'] = $i+1; - $piece['partStart'] = $i+1; + # count opening brace characters + $piece['count'] = strspn( $text, $piece['brace'], $i ); + $piece['startAt'] = $piece['partStart'] = $i + $piece['count']; + $i += $piece['count']; - # we need to add to stack only if openning brace count is enough for any given rule - foreach ($rule['cb'] as $cnt => $fn) { - if ($piece['count'] >= $cnt) { - $lastOpeningBrace ++; - $openingBraceStack[$lastOpeningBrace] = $piece; - break; + # we need to add to stack only if opening brace count is enough for one of the rules + if ( $piece['count'] >= $rule['min'] ) { + $lastOpeningBrace ++; + $openingBraceStack[$lastOpeningBrace] = $piece; + } + } elseif ( $found == 'close' ) { + # lets check if it is enough characters for closing brace + $maxCount = $openingBraceStack[$lastOpeningBrace]['count']; + $count = strspn( $text, $text[$i], $i, $maxCount ); + + # check for maximum matching characters (if there are 5 closing + # characters, we will probably need only 3 - depending on the rules) + $matchingCount = 0; + $matchingCallback = null; + $cbType = $callbacks[$openingBraceStack[$lastOpeningBrace]['brace']]; + if ( $count > $cbType['max'] ) { + # The specified maximum exists in the callback array, unless the caller + # has made an error + $matchingCount = $cbType['max']; + } else { + # Count is less than the maximum + # Skip any gaps in the callback array to find the true largest match + # Need to use array_key_exists not isset because the callback can be null + $matchingCount = $count; + while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $cbType['cb'] ) ) { + --$matchingCount; } } - continue; - } - else if ($lastOpeningBrace >= 0) { - # first check if it is a closing brace - if ($openingBraceStack[$lastOpeningBrace]['braceEnd'] == $text[$i]) { - # lets check if it is enough characters for closing brace - $count = 1; - while ($i+$count < strlen($text) && $text[$i+$count] == $text[$i]) - $count++; - - # if there are more closing parentheses than opening ones, we parse less - if ($openingBraceStack[$lastOpeningBrace]['count'] < $count) - $count = $openingBraceStack[$lastOpeningBrace]['count']; - - # check for maximum matching characters (if there are 5 closing characters, we will probably need only 3 - depending on the rules) - $matchingCount = 0; - $matchingCallback = null; - foreach ($callbacks[$openingBraceStack[$lastOpeningBrace]['brace']]['cb'] as $cnt => $fn) { - if ($count >= $cnt && $matchingCount < $cnt) { - $matchingCount = $cnt; - $matchingCallback = $fn; - } - } + if ($matchingCount <= 0) { + $i += $count; + continue; + } + $matchingCallback = $cbType['cb'][$matchingCount]; - if ($matchingCount == 0) { - $i += $count - 1; - continue; - } + # let's set a title or last part (if '|' was found) + if (null === $openingBraceStack[$lastOpeningBrace]['parts']) { + $openingBraceStack[$lastOpeningBrace]['title'] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $i - $openingBraceStack[$lastOpeningBrace]['partStart']); + } else { + $openingBraceStack[$lastOpeningBrace]['parts'][] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $i - $openingBraceStack[$lastOpeningBrace]['partStart']); + } - # lets set a title or last part (if '|' was found) - if (null === $openingBraceStack[$lastOpeningBrace]['parts']) - $openingBraceStack[$lastOpeningBrace]['title'] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']); - else - $openingBraceStack[$lastOpeningBrace]['parts'][] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']); - - $pieceStart = $openingBraceStack[$lastOpeningBrace]['startAt'] - $matchingCount; - $pieceEnd = $i + $matchingCount; - - if( is_callable( $matchingCallback ) ) { - $cbArgs = array ( - 'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart), - 'title' => trim($openingBraceStack[$lastOpeningBrace]['title']), - 'parts' => $openingBraceStack[$lastOpeningBrace]['parts'], - 'lineStart' => (($pieceStart > 0) && ($text[$pieceStart-1] == "\n")), - ); - # finally we can call a user callback and replace piece of text - wfProfileOut( __METHOD__ . '-self' ); - $replaceWith = call_user_func( $matchingCallback, $cbArgs ); - wfProfileIn( __METHOD__ . '-self' ); - $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd); - $i = $pieceStart + strlen($replaceWith) - 1; - } - else { - # null value for callback means that parentheses should be parsed, but not replaced - $i += $matchingCount - 1; - } + $pieceStart = $openingBraceStack[$lastOpeningBrace]['startAt'] - $matchingCount; + $pieceEnd = $i + $matchingCount; + + if( is_callable( $matchingCallback ) ) { + $cbArgs = array ( + 'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart), + 'title' => trim($openingBraceStack[$lastOpeningBrace]['title']), + 'parts' => $openingBraceStack[$lastOpeningBrace]['parts'], + 'lineStart' => (($pieceStart > 0) && ($text[$pieceStart-1] == "\n")), + ); + # finally we can call a user callback and replace piece of text + $replaceWith = call_user_func( $matchingCallback, $cbArgs ); + $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd); + $i = $pieceStart + strlen($replaceWith); + } else { + # null value for callback means that parentheses should be parsed, but not replaced + $i += $matchingCount; + } - # reset last openning parentheses, but keep it in case there are unused characters - $piece = array('brace' => $openingBraceStack[$lastOpeningBrace]['brace'], - 'braceEnd' => $openingBraceStack[$lastOpeningBrace]['braceEnd'], - 'count' => $openingBraceStack[$lastOpeningBrace]['count'], - 'title' => '', - 'parts' => null, - 'startAt' => $openingBraceStack[$lastOpeningBrace]['startAt']); - $openingBraceStack[$lastOpeningBrace--] = null; - - if ($matchingCount < $piece['count']) { - $piece['count'] -= $matchingCount; - $piece['startAt'] -= $matchingCount; - $piece['partStart'] = $piece['startAt']; - # do we still qualify for any callback with remaining count? - foreach ($callbacks[$piece['brace']]['cb'] as $cnt => $fn) { - if ($piece['count'] >= $cnt) { - $lastOpeningBrace ++; - $openingBraceStack[$lastOpeningBrace] = $piece; - break; - } + # reset last opening parentheses, but keep it in case there are unused characters + $piece = array('brace' => $openingBraceStack[$lastOpeningBrace]['brace'], + 'braceEnd' => $openingBraceStack[$lastOpeningBrace]['braceEnd'], + 'count' => $openingBraceStack[$lastOpeningBrace]['count'], + 'title' => '', + 'parts' => null, + 'startAt' => $openingBraceStack[$lastOpeningBrace]['startAt']); + $openingBraceStack[$lastOpeningBrace--] = null; + + if ($matchingCount < $piece['count']) { + $piece['count'] -= $matchingCount; + $piece['startAt'] -= $matchingCount; + $piece['partStart'] = $piece['startAt']; + # do we still qualify for any callback with remaining count? + $currentCbList = $callbacks[$piece['brace']]['cb']; + while ( $piece['count'] ) { + if ( array_key_exists( $piece['count'], $currentCbList ) ) { + $lastOpeningBrace++; + $openingBraceStack[$lastOpeningBrace] = $piece; + break; } + --$piece['count']; } - continue; } - + } elseif ( $found == 'pipe' ) { # lets set a title if it is a first separator, or next part otherwise - if ($text[$i] == '|') { - if (null === $openingBraceStack[$lastOpeningBrace]['parts']) { - $openingBraceStack[$lastOpeningBrace]['title'] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']); - $openingBraceStack[$lastOpeningBrace]['parts'] = array(); - } - else - $openingBraceStack[$lastOpeningBrace]['parts'][] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']); - - $openingBraceStack[$lastOpeningBrace]['partStart'] = $i + 1; + if (null === $openingBraceStack[$lastOpeningBrace]['parts']) { + $openingBraceStack[$lastOpeningBrace]['title'] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $i - $openingBraceStack[$lastOpeningBrace]['partStart']); + $openingBraceStack[$lastOpeningBrace]['parts'] = array(); + } else { + $openingBraceStack[$lastOpeningBrace]['parts'][] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $i - $openingBraceStack[$lastOpeningBrace]['partStart']); } + $openingBraceStack[$lastOpeningBrace]['partStart'] = ++$i; } } - wfProfileOut( __METHOD__ . '-self' ); + wfProfileOut( __METHOD__ ); return $text; } @@ -2547,11 +2713,11 @@ class Parser */ function replaceVariables( $text, $args = array(), $argsOnly = false ) { # Prevent too big inclusions - if( strlen( $text ) > MAX_INCLUDE_SIZE ) { + if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) { return $text; } - $fname = 'Parser::replaceVariables'; + $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/; wfProfileIn( $fname ); # This function is called recursively. To keep track of arguments we need a stack: @@ -2561,32 +2727,45 @@ class Parser if ( !$argsOnly ) { $braceCallbacks[2] = array( &$this, 'braceSubstitution' ); } - if ( $this->mOutputType == OT_HTML || $this->mOutputType == OT_WIKI ) { + if ( $this->mOutputType != OT_MSG ) { $braceCallbacks[3] = array( &$this, 'argSubstitution' ); } - $callbacks = array(); - $callbacks['{'] = array('end' => '}', 'cb' => $braceCallbacks); - $callbacks['['] = array('end' => ']', 'cb' => array(2=>null)); - $text = $this->replace_callback ($text, $callbacks); - - array_pop( $this->mArgStack ); + if ( $braceCallbacks ) { + $callbacks = array( + '{' => array( + 'end' => '}', + 'cb' => $braceCallbacks, + 'min' => $argsOnly ? 3 : 2, + 'max' => isset( $braceCallbacks[3] ) ? 3 : 2, + ), + '[' => array( + 'end' => ']', + 'cb' => array(2=>null), + 'min' => 2, + 'max' => 2, + ) + ); + $text = $this->replace_callback ($text, $callbacks); + array_pop( $this->mArgStack ); + } wfProfileOut( $fname ); return $text; } - + /** * Replace magic variables * @private */ function variableSubstitution( $matches ) { + global $wgContLang; $fname = 'Parser::variableSubstitution'; - $varname = $matches[1]; + $varname = $wgContLang->lc($matches[1]); wfProfileIn( $fname ); $skip = false; if ( $this->mOutputType == OT_WIKI ) { # Do only magic variables prefixed by SUBST - $mwSubst =& MagicWord::get( MAG_SUBST ); + $mwSubst =& MagicWord::get( 'subst' ); if (!$mwSubst->matchStartAndRemove( $varname )) $skip = true; # Note that if we don't substitute the variable below, @@ -2595,8 +2774,14 @@ class Parser } if ( !$skip && array_key_exists( $varname, $this->mVariables ) ) { $id = $this->mVariables[$varname]; - $text = $this->getVariableValue( $id ); - $this->mOutput->mContainsOldMagic = true; + # Now check if we did really match, case sensitive or not + $mw =& MagicWord::get( $id ); + if ($mw->match($matches[1])) { + $text = $this->getVariableValue( $id ); + $this->mOutput->mContainsOldMagic = true; + } else { + $text = $matches[0]; + } } else { $text = $matches[0]; } @@ -2642,8 +2827,9 @@ class Parser */ function braceSubstitution( $piece ) { global $wgContLang, $wgLang, $wgAllowDisplayTitle, $action; - $fname = 'Parser::braceSubstitution'; + $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/; wfProfileIn( $fname ); + wfProfileIn( __METHOD__.'-setup' ); # Flags $found = false; # $text has been filled @@ -2659,10 +2845,11 @@ class Parser $linestart = ''; + # $part1 is the bit before the first |, and must contain only title characters # $args is a list of arguments, starting from index 0, not including $part1 - $part1 = $piece['title']; + $titleText = $part1 = $piece['title']; # If the third subpattern matched anything, it will start with | if (null == $piece['parts']) { @@ -2677,11 +2864,13 @@ class Parser $args = (null == $piece['parts']) ? array() : $piece['parts']; $argc = count( $args ); + wfProfileOut( __METHOD__.'-setup' ); # SUBST + wfProfileIn( __METHOD__.'-modifiers' ); if ( !$found ) { - $mwSubst =& MagicWord::get( MAG_SUBST ); - if ( $mwSubst->matchStartAndRemove( $part1 ) xor ($this->mOutputType == OT_WIKI) ) { + $mwSubst =& MagicWord::get( 'subst' ); + if ( $mwSubst->matchStartAndRemove( $part1 ) xor $this->ot['wiki'] ) { # One of two possibilities is true: # 1) Found SUBST but not in the PST phase # 2) Didn't find SUBST and in the PST phase @@ -2693,33 +2882,25 @@ class Parser } } - # MSG, MSGNW, INT and RAW + # MSG, MSGNW and RAW if ( !$found ) { # Check for MSGNW: - $mwMsgnw =& MagicWord::get( MAG_MSGNW ); + $mwMsgnw =& MagicWord::get( 'msgnw' ); if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) { $nowiki = true; } else { # Remove obsolete MSG: - $mwMsg =& MagicWord::get( MAG_MSG ); + $mwMsg =& MagicWord::get( 'msg' ); $mwMsg->matchStartAndRemove( $part1 ); } # Check for RAW: - $mwRaw =& MagicWord::get( MAG_RAW ); + $mwRaw =& MagicWord::get( 'raw' ); if ( $mwRaw->matchStartAndRemove( $part1 ) ) { $forceRawInterwiki = true; } - - # Check if it is an internal message - $mwInt =& MagicWord::get( MAG_INT ); - if ( $mwInt->matchStartAndRemove( $part1 ) ) { - if ( $this->incrementIncludeCount( 'int:'.$part1 ) ) { - $text = $linestart . wfMsgReal( $part1, $args, true ); - $found = true; - } - } } + wfProfileOut( __METHOD__.'-modifiers' ); # Parser functions if ( !$found ) { @@ -2764,7 +2945,7 @@ class Parser } } } - wfProfileOut( __METHOD__ . '-pfunc' ); + wfProfileOut( __METHOD__ . '-pfunc' ); } # Template table test @@ -2780,9 +2961,8 @@ class Parser $noargs = true; $found = true; $text = $linestart . - '{{' . $part1 . '}}' . - '<!-- WARNING: template loop detected -->'; - wfDebug( "$fname: template loop broken at '$part1'\n" ); + "[[$part1]]<!-- WARNING: template loop detected -->"; + wfDebug( __METHOD__.": template loop broken at '$part1'\n" ); } else { # set $text to cached message. $text = $linestart . $this->mTemplates[$piece['title']]; @@ -2806,6 +2986,7 @@ class Parser if ( !is_null( $title ) ) { + $titleText = $title->getPrefixedText(); $checkVariantLink = sizeof($wgContLang->getVariants())>1; # Check for language variants if the template is not found if($checkVariantLink && $title->getArticleID() == 0){ @@ -2813,36 +2994,32 @@ class Parser } if ( !$title->isExternal() ) { - # Check for excessive inclusion - $dbk = $title->getPrefixedDBkey(); - if ( $this->incrementIncludeCount( $dbk ) ) { - if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->mOutputType != OT_WIKI ) { - $text = SpecialPage::capturePath( $title ); - if ( is_string( $text ) ) { - $found = true; - $noparse = true; - $noargs = true; - $isHTML = true; - $this->disableCache(); - } - } else { - $articleContent = $this->fetchTemplate( $title ); - if ( $articleContent !== false ) { - $found = true; - $text = $articleContent; - $replaceHeadings = true; - } + if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) { + $text = SpecialPage::capturePath( $title ); + if ( is_string( $text ) ) { + $found = true; + $noparse = true; + $noargs = true; + $isHTML = true; + $this->disableCache(); + } + } else { + $articleContent = $this->fetchTemplate( $title ); + if ( $articleContent !== false ) { + $found = true; + $text = $articleContent; + $replaceHeadings = true; } } # If the title is valid but undisplayable, make a link to it - if ( $this->mOutputType == OT_HTML && !$found ) { - $text = '[['.$title->getPrefixedText().']]'; + if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) { + $text = "[[:$titleText]]"; $found = true; } } elseif ( $title->isTrans() ) { // Interwiki transclusion - if ( $this->mOutputType == OT_HTML && !$forceRawInterwiki ) { + if ( $this->ot['html'] && !$forceRawInterwiki ) { $text = $this->interwikiTransclude( $title, 'render' ); $isHTML = true; $noparse = true; @@ -2852,7 +3029,7 @@ class Parser } $found = true; } - + # Template cache array insertion # Use the original $piece['title'] not the mangled $part1, so that # modifiers such as RAW: produce separate cache entries @@ -2865,14 +3042,22 @@ class Parser $text = $linestart . $text; } } - wfProfileOut( __METHOD__ . '-loadtpl' ); + wfProfileOut( __METHOD__ . '-loadtpl' ); + } + + if ( $found && !$this->incrementIncludeSize( 'pre-expand', strlen( $text ) ) ) { + # Error, oversize inclusion + $text = $linestart . + "[[$titleText]]<!-- WARNING: template omitted, pre-expand include size too large -->"; + $noparse = true; + $noargs = true; } # Recursive parsing, escaping and link table handling # Only for HTML output - if ( $nowiki && $found && $this->mOutputType == OT_HTML ) { + if ( $nowiki && $found && ( $this->ot['html'] || $this->ot['pre'] ) ) { $text = wfEscapeWikiText( $text ); - } elseif ( ($this->mOutputType == OT_HTML || $this->mOutputType == OT_WIKI) && $found ) { + } elseif ( !$this->ot['msg'] && $found ) { if ( $noargs ) { $assocArgs = array(); } else { @@ -2911,16 +3096,20 @@ class Parser $text = preg_replace( '/<noinclude>.*?<\/noinclude>/s', '', $text ); $text = strtr( $text, array( '<includeonly>' => '' , '</includeonly>' => '' ) ); - if( $this->mOutputType == OT_HTML ) { + if( $this->ot['html'] || $this->ot['pre'] ) { # Strip <nowiki>, <pre>, etc. $text = $this->strip( $text, $this->mStripState ); - $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs ); + if ( $this->ot['html'] ) { + $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs ); + } elseif ( $this->ot['pre'] && $this->mOptions->getRemoveComments() ) { + $text = Sanitizer::removeHTMLcomments( $text ); + } } $text = $this->replaceVariables( $text, $assocArgs ); # 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 ) { @@ -2933,6 +3122,14 @@ class Parser # Prune lower levels off the recursion check path $this->mTemplatePath = $lastPathLevel; + if ( $found && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) { + # Error, oversize inclusion + $text = $linestart . + "[[$titleText]]<!-- WARNING: template omitted, post-expand include size too large -->"; + $noparse = true; + $noargs = true; + } + if ( !$found ) { wfProfileOut( $fname ); return $piece['text']; @@ -2946,7 +3143,7 @@ class Parser } else { # replace ==section headers== # XXX this needs to go away once we have a better parser. - if ( $this->mOutputType != OT_WIKI && $replaceHeadings ) { + if ( !$this->ot['wiki'] && !$this->ot['pre'] && $replaceHeadings ) { if( !is_null( $title ) ) $encodedname = base64_encode($title->getPrefixedDBkey()); else @@ -3070,25 +3267,31 @@ class Parser if ( array_key_exists( $arg, $inputArgs ) ) { $text = $inputArgs[$arg]; - } else if ($this->mOutputType == OT_HTML && null != $matches['parts'] && count($matches['parts']) > 0) { + } else if (($this->mOutputType == OT_HTML || $this->mOutputType == OT_PREPROCESS ) && + null != $matches['parts'] && count($matches['parts']) > 0) { $text = $matches['parts'][0]; } + if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) { + $text = $matches['text'] . + '<!-- WARNING: argument omitted, expansion size too large -->'; + } return $text; } /** - * Returns true if the function is allowed to include this entity - * @private + * Increment an include size counter + * + * @param string $type The type of expansion + * @param integer $size The size of the text + * @return boolean False if this inclusion would take it over the maximum, true otherwise */ - function incrementIncludeCount( $dbk ) { - if ( !array_key_exists( $dbk, $this->mIncludeCount ) ) { - $this->mIncludeCount[$dbk] = 0; - } - if ( ++$this->mIncludeCount[$dbk] <= MAX_INCLUDE_REPEAT ) { - return true; - } else { + function incrementIncludeSize( $type, $size ) { + if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) { return false; + } else { + $this->mIncludeSizes[$type] += $size; + return true; } } @@ -3098,7 +3301,7 @@ class Parser function stripNoGallery( &$text ) { # if the string __NOGALLERY__ (not case-sensitive) occurs in the HTML, # do not add TOC - $mw = MagicWord::get( MAG_NOGALLERY ); + $mw = MagicWord::get( 'nogallery' ); $this->mOutput->mNoGallery = $mw->matchAndRemove( $text ) ; } @@ -3108,19 +3311,19 @@ class Parser function stripToc( $text ) { # if the string __NOTOC__ (not case-sensitive) occurs in the HTML, # do not add TOC - $mw = MagicWord::get( MAG_NOTOC ); + $mw = MagicWord::get( 'notoc' ); if( $mw->matchAndRemove( $text ) ) { $this->mShowToc = false; } - - $mw = MagicWord::get( MAG_TOC ); + + $mw = MagicWord::get( 'toc' ); if( $mw->match( $text ) ) { $this->mShowToc = true; $this->mForceTocPosition = true; - + // Set a placeholder. At the end we'll fill it in with the TOC. $text = $mw->replace( '<!--MWTOC-->', $text, 1 ); - + // Only keep the first one. $text = $mw->replace( '', $text ); } @@ -3152,7 +3355,7 @@ class Parser } # Inhibit editsection links if requested in the page - $esw =& MagicWord::get( MAG_NOEDITSECTION ); + $esw =& MagicWord::get( 'noeditsection' ); if( $esw->matchAndRemove( $text ) ) { $showEditLink = 0; } @@ -3168,13 +3371,13 @@ class Parser # Allow user to stipulate that a page should have a "new section" # link added via __NEWSECTIONLINK__ - $mw =& MagicWord::get( MAG_NEWSECTIONLINK ); + $mw =& MagicWord::get( 'newsectionlink' ); if( $mw->matchAndRemove( $text ) ) $this->mOutput->setNewSection( true ); # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML, # override above conditions and always show TOC above first header - $mw =& MagicWord::get( MAG_FORCETOC ); + $mw =& MagicWord::get( 'forcetoc' ); if ($mw->matchAndRemove( $text ) ) { $this->mShowToc = true; $enoughToc = true; @@ -3381,137 +3584,6 @@ class Parser } /** - * Return an HTML link for the "ISBN 123456" text - * @private - */ - function magicISBN( $text ) { - $fname = 'Parser::magicISBN'; - wfProfileIn( $fname ); - - $a = split( 'ISBN ', ' '.$text ); - if ( count ( $a ) < 2 ) { - wfProfileOut( $fname ); - return $text; - } - $text = substr( array_shift( $a ), 1); - $valid = '0123456789-Xx'; - - foreach ( $a as $x ) { - # hack: don't replace inside thumbnail title/alt - # attributes - if(preg_match('/<[^>]+(alt|title)="[^">]*$/', $text)) { - $text .= "ISBN $x"; - continue; - } - - $isbn = $blank = '' ; - while ( $x !== '' && ' ' == $x{0} ) { - $blank .= ' '; - $x = substr( $x, 1 ); - } - if ( $x == '' ) { # blank isbn - $text .= "ISBN $blank"; - continue; - } - while ( strstr( $valid, $x{0} ) != false ) { - $isbn .= $x{0}; - $x = substr( $x, 1 ); - } - $num = str_replace( '-', '', $isbn ); - $num = str_replace( ' ', '', $num ); - $num = str_replace( 'x', 'X', $num ); - - if ( '' == $num ) { - $text .= "ISBN $blank$x"; - } else { - $titleObj = Title::makeTitle( NS_SPECIAL, 'Booksources' ); - $text .= '<a href="' . - $titleObj->escapeLocalUrl( 'isbn='.$num ) . - "\" class=\"internal\">ISBN $isbn</a>"; - $text .= $x; - } - } - wfProfileOut( $fname ); - return $text; - } - - /** - * Return an HTML link for the "RFC 1234" text - * - * @private - * @param string $text Text to be processed - * @param string $keyword Magic keyword to use (default RFC) - * @param string $urlmsg Interface message to use (default rfcurl) - * @return string - */ - function magicRFC( $text, $keyword='RFC ', $urlmsg='rfcurl' ) { - - $valid = '0123456789'; - $internal = false; - - $a = split( $keyword, ' '.$text ); - if ( count ( $a ) < 2 ) { - return $text; - } - $text = substr( array_shift( $a ), 1); - - /* Check if keyword is preceed by [[. - * This test is made here cause of the array_shift above - * that prevent the test to be done in the foreach. - */ - if ( substr( $text, -2 ) == '[[' ) { - $internal = true; - } - - foreach ( $a as $x ) { - /* token might be empty if we have RFC RFC 1234 */ - if ( $x=='' ) { - $text.=$keyword; - continue; - } - - # hack: don't replace inside thumbnail title/alt - # attributes - if(preg_match('/<[^>]+(alt|title)="[^">]*$/', $text)) { - $text .= $keyword . $x; - continue; - } - - $id = $blank = '' ; - - /** remove and save whitespaces in $blank */ - while ( $x{0} == ' ' ) { - $blank .= ' '; - $x = substr( $x, 1 ); - } - - /** remove and save the rfc number in $id */ - while ( strstr( $valid, $x{0} ) != false ) { - $id .= $x{0}; - $x = substr( $x, 1 ); - } - - if ( $id == '' ) { - /* call back stripped spaces*/ - $text .= $keyword.$blank.$x; - } elseif( $internal ) { - /* normal link */ - $text .= $keyword.$id.$x; - } else { - /* build the external link*/ - $url = wfMsg( $urlmsg, $id); - $sk =& $this->mOptions->getSkin(); - $la = $sk->getExternalLinkAttributes( $url, $keyword.$id ); - $text .= "<a href=\"{$url}\"{$la}>{$keyword}{$id}</a>{$x}"; - } - - /* Check if the next RFC keyword is preceed by [[ */ - $internal = ( substr($x,-2) == '[[' ); - } - return $text; - } - - /** * Transform wiki markup when saving a page by doing \r\n -> \n * conversion, substitting signatures, {{subst:}} templates, etc. * @@ -3526,7 +3598,7 @@ class Parser function preSaveTransform( $text, &$title, &$user, $options, $clearState = true ) { $this->mOptions = $options; $this->mTitle =& $title; - $this->mOutputType = OT_WIKI; + $this->setOutputType( OT_WIKI ); if ( $clearState ) { $this->clearState(); @@ -3569,10 +3641,10 @@ class Parser # Variable replacement # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags $text = $this->replaceVariables( $text ); - + # Strip out <nowiki> etc. added via replaceVariables $text = $this->strip( $text, $stripState, false, array( 'gallery' ) ); - + # Signatures $sigText = $this->getUserSig( $user ); $text = strtr( $text, array( @@ -3585,35 +3657,31 @@ class Parser # global $wgLegalTitleChars; $tc = "[$wgLegalTitleChars]"; - $np = str_replace( array( '(', ')' ), array( '', '' ), $tc ); # No parens + $nc = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii! - $namespacechar = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii! - $conpat = "/^({$np}+) \\(({$tc}+)\\)$/"; + $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/"; # [[ns:page (context)|]] + $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/"; # [[ns:page (context), context|]] + $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]] - $p1 = "/\[\[({$np}+) \\(({$np}+)\\)\\|]]/"; # [[page (context)|]] - $p2 = "/\[\[\\|({$tc}+)]]/"; # [[|page]] - $p3 = "/\[\[(:*$namespacechar+):({$np}+)\\|]]/"; # [[namespace:page|]] and [[:namespace:page|]] - $p4 = "/\[\[(:*$namespacechar+):({$np}+) \\(({$np}+)\\)\\|]]/"; # [[ns:page (cont)|]] and [[:ns:page (cont)|]] - $context = ''; - $t = $this->mTitle->getText(); - if ( preg_match( $conpat, $t, $m ) ) { - $context = $m[2]; - } - $text = preg_replace( $p4, '[[\\1:\\2 (\\3)|\\2]]', $text ); - $text = preg_replace( $p1, '[[\\1 (\\2)|\\1]]', $text ); - $text = preg_replace( $p3, '[[\\1:\\2|\\2]]', $text ); + # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]" + $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text ); + $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text ); - if ( '' == $context ) { - $text = preg_replace( $p2, '[[\\1]]', $text ); + $t = $this->mTitle->getText(); + if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) { + $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text ); + } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && '' != "$m[1]$m[2]" ) { + $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text ); } else { - $text = preg_replace( $p2, "[[\\1 ({$context})|\\1]]", $text ); + # if there's no context, don't bother duplicating the title + $text = preg_replace( $p2, '[[\\1]]', $text ); } # Trim trailing whitespace - # MAG_END (__END__) tag allows for trailing + # __END__ tag allows for trailing # whitespace to be deliberately included $text = rtrim( $text ); - $mw =& MagicWord::get( MAG_END ); + $mw =& MagicWord::get( 'end' ); $mw->matchAndRemove( $text ); return $text; @@ -3631,7 +3699,7 @@ class Parser $username = $user->getName(); $nickname = $user->getOption( 'nickname' ); $nickname = $nickname === '' ? $username : $nickname; - + if( $user->getBoolOption( 'fancysig' ) !== false ) { # Sig. might contain markup; validate this if( $this->validateSig( $nickname ) !== false ) { @@ -3661,7 +3729,7 @@ class Parser function validateSig( $text ) { return( wfIsWellFormedXmlFragment( $text ) ? $text : false ); } - + /** * Clean up signature text * @@ -3675,16 +3743,16 @@ class Parser function cleanSig( $text, $parsing = false ) { global $wgTitle; $this->startExternalParse( $wgTitle, new ParserOptions(), $parsing ? OT_WIKI : OT_MSG ); - - $substWord = MagicWord::get( MAG_SUBST ); + + $substWord = MagicWord::get( 'subst' ); $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase(); $substText = '{{' . $substWord->getSynonym( 0 ); $text = preg_replace( $substRegex, $substText, $text ); $text = $this->cleanSigInSig( $text ); $text = $this->replaceVariables( $text ); - - $this->clearState(); + + $this->clearState(); return $text; } @@ -3697,7 +3765,7 @@ class Parser $text = preg_replace( '/~{3,5}/', '', $text ); return $text; } - + /** * Set up some variables which are usually set up in parse() * so that an external function can call some class members with confidence @@ -3706,7 +3774,7 @@ class Parser function startExternalParse( &$title, $options, $outputType, $clearState = true ) { $this->mTitle =& $title; $this->mOptions = $options; - $this->mOutputType = $outputType; + $this->setOutputType( $outputType ); if ( $clearState ) { $this->clearState(); } @@ -3734,9 +3802,13 @@ class Parser wfProfileIn($fname); - $this->mTitle = $wgTitle; + if ( $wgTitle ) { + $this->mTitle = $wgTitle; + } else { + $this->mTitle = Title::newFromText('msg'); + } $this->mOptions = $options; - $this->mOutputType = OT_MSG; + $this->setOutputType( OT_MSG ); $this->clearState(); $text = $this->replaceVariables( $text ); @@ -3785,7 +3857,7 @@ class Parser * * @public * - * @param mixed $id The magic word ID, or (deprecated) the function name. Function names are case-insensitive. + * @param string $id The magic word ID * @param mixed $callback The callback function (and object) to use * @param integer $flags a combination of the following flags: * SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}} @@ -3793,21 +3865,16 @@ class Parser * @return The old callback function for this name, if any */ function setFunctionHook( $id, $callback, $flags = 0 ) { - if( is_string( $id ) ) { - $id = strtolower( $id ); - } $oldVal = @$this->mFunctionHooks[$id]; $this->mFunctionHooks[$id] = $callback; # Add to function cache - if ( is_int( $id ) ) { - $mw = MagicWord::get( $id ); - $synonyms = $mw->getSynonyms(); - $sensitive = intval( $mw->isCaseSensitive() ); - } else { - $synonyms = array( $id ); - $sensitive = 0; - } + $mw = MagicWord::get( $id ); + if( !$mw ) + throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' ); + + $synonyms = $mw->getSynonyms(); + $sensitive = intval( $mw->isCaseSensitive() ); foreach ( $synonyms as $syn ) { # Case @@ -3828,6 +3895,15 @@ class Parser } /** + * Get all registered function hook identifiers + * + * @return array + */ + function getFunctionHooks() { + return array_keys( $this->mFunctionHooks ); + } + + /** * Replace <!--LINK--> link placeholders with actual links, in the buffer * Placeholders created in Skin::makeLinkObj() * Returns an array of links found, indexed by PDBK: @@ -3839,6 +3915,7 @@ class Parser function replaceLinkHolders( &$text, $options = 0 ) { global $wgUser; global $wgOutputReplace; + global $wgContLang, $wgLanguageCode; $fname = 'Parser::replaceLinkHolders'; wfProfileIn( $fname ); @@ -3929,6 +4006,91 @@ class Parser } wfProfileOut( $fname.'-check' ); + # Do a second query for different language variants of links (if needed) + if($wgContLang->hasVariants()){ + $linkBatch = new LinkBatch(); + $variantMap = array(); // maps $pdbkey_Variant => $pdbkey_original + + // Add variants of links to link batch + foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { + $title = $this->mLinkHolders['titles'][$key]; + if ( is_null( $title ) ) + continue; + + $pdbk = $title->getPrefixedDBkey(); + + // generate all variants of the link title text + $allTextVariants = $wgContLang->convertLinkToAllVariants($title->getText()); + + // if link was not found (in first query), add all variants to query + if ( !isset($colours[$pdbk]) ){ + foreach($allTextVariants as $textVariant){ + $variantTitle = Title::makeTitle( $ns, $textVariant ); + if(is_null($variantTitle)) continue; + $linkBatch->addObj( $variantTitle ); + $variantMap[$variantTitle->getPrefixedDBkey()][] = $key; + } + } + } + + + if(!$linkBatch->isEmpty()){ + // construct query + $titleClause = $linkBatch->constructSet('page', $dbr); + + $variantQuery = "SELECT page_id, page_namespace, page_title"; + if ( $threshold > 0 ) { + $variantQuery .= ', page_len, page_is_redirect'; + } + + $variantQuery .= " FROM $page WHERE $titleClause"; + if ( $options & RLH_FOR_UPDATE ) { + $variantQuery .= ' FOR UPDATE'; + } + + $varRes = $dbr->query( $variantQuery, $fname ); + + // for each found variants, figure out link holders and replace + while ( $s = $dbr->fetchObject($varRes) ) { + + $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title ); + $varPdbk = $variantTitle->getPrefixedDBkey(); + $linkCache->addGoodLinkObj( $s->page_id, $variantTitle ); + $this->mOutput->addLink( $variantTitle, $s->page_id ); + + $holderKeys = $variantMap[$varPdbk]; + + // loop over link holders + foreach($holderKeys as $key){ + $title = $this->mLinkHolders['titles'][$key]; + if ( is_null( $title ) ) continue; + + $pdbk = $title->getPrefixedDBkey(); + + if(!isset($colours[$pdbk])){ + // found link in some of the variants, replace the link holder data + $this->mLinkHolders['titles'][$key] = $variantTitle; + $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey(); + + // set pdbk and colour + $pdbks[$key] = $varPdbk; + if ( $threshold > 0 ) { + $size = $s->page_len; + if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) { + $colours[$varPdbk] = 1; + } else { + $colours[$varPdbk] = 2; + } + } + else { + $colours[$varPdbk] = 1; + } + } + } + } + } + } + # Construct search and replace arrays wfProfileIn( $fname.'-construct' ); $wgOutputReplace = array(); @@ -4033,13 +4195,13 @@ class Parser function renderPreTag( $text, $attribs, $parser ) { // Backwards-compatibility hack $content = preg_replace( '!<nowiki>(.*?)</nowiki>!is', '\\1', $text ); - + $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' ); return wfOpenElement( 'pre', $attribs ) . wfEscapeHTMLTagsOnly( $content ) . '</pre>'; } - + /** * Renders an image gallery from a text with one line per image. * text labels may be given by using |-style alternative text. E.g. @@ -4058,7 +4220,7 @@ class Parser if( isset( $params['caption'] ) ) $ig->setCaption( $params['caption'] ); - + $lines = explode( "\n", $text ); foreach ( $lines as $line ) { # match lines like these: @@ -4068,7 +4230,8 @@ class Parser if ( count( $matches ) == 0 ) { continue; } - $nt =& Title::newFromText( $matches[1] ); + $tp = Title::newFromText( $matches[1] ); + $nt =& $tp; if( is_null( $nt ) ) { # Bogus title. Ignore these so we don't bomb out later. continue; @@ -4101,7 +4264,7 @@ class Parser * Parse image options text and use it to make an image */ function makeImage( &$nt, $options ) { - global $wgUseImageResize; + global $wgUseImageResize, $wgDjvuRenderer; $align = ''; @@ -4117,17 +4280,19 @@ class Parser $part = explode( '|', $options); - $mwThumb =& MagicWord::get( MAG_IMG_THUMBNAIL ); - $mwManualThumb =& MagicWord::get( MAG_IMG_MANUALTHUMB ); - $mwLeft =& MagicWord::get( MAG_IMG_LEFT ); - $mwRight =& MagicWord::get( MAG_IMG_RIGHT ); - $mwNone =& MagicWord::get( MAG_IMG_NONE ); - $mwWidth =& MagicWord::get( MAG_IMG_WIDTH ); - $mwCenter =& MagicWord::get( MAG_IMG_CENTER ); - $mwFramed =& MagicWord::get( MAG_IMG_FRAMED ); + $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; $manual_thumb = '' ; foreach( $part as $key => $val ) { @@ -4149,8 +4314,12 @@ class Parser } 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 && ! is_null( $match = $mwWidth->matchVariableStartToEnd($val) ) ) { - wfDebug( "MAG_IMG_WIDTH match: $match\n" ); + wfDebug( "img_width match: $match\n" ); # $match is the image width in pixels if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $match, $m ) ) { $width = intval( $m[1] ); @@ -4175,7 +4344,7 @@ class Parser # Linker does the rest $sk =& $this->mOptions->getSkin(); - return $sk->makeImageLinkObj( $nt, $caption, $alt, $align, $width, $height, $framed, $thumb, $manual_thumb ); + return $sk->makeImageLinkObj( $nt, $caption, $alt, $align, $width, $height, $framed, $thumb, $manual_thumb, $page ); } /** @@ -4242,15 +4411,15 @@ class Parser # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML # comments to be stripped as well) $striparray = array(); - + $oldOutputType = $this->mOutputType; $oldOptions = $this->mOptions; $this->mOptions = new ParserOptions(); - $this->mOutputType = OT_WIKI; - + $this->setOutputType( OT_WIKI ); + $striptext = $this->strip( $text, $striparray, true ); - - $this->mOutputType = $oldOutputType; + + $this->setOutputType( $oldOutputType ); $this->mOptions = $oldOptions; # now that we can be sure that no pseudo-sections are in the source, @@ -4293,7 +4462,7 @@ class Parser /mix", $striptext, -1, PREG_SPLIT_DELIM_CAPTURE); - + if( $mode == "get" ) { if( $section == 0 ) { // "Section 0" returns the content before any other section. @@ -4360,7 +4529,7 @@ class Parser $rv = trim( $rv ); return $rv; } - + /** * This function returns the text of a section, specified by a number ($section). * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or @@ -4375,7 +4544,7 @@ class Parser function getSection( $text, $section ) { return $this->extractSections( $text, $section, "get" ); } - + function replaceSection( $oldtext, $section, $text ) { return $this->extractSections( $oldtext, $section, "replace", $text ); } @@ -4435,6 +4604,7 @@ class ParserOutput 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 ); } @@ -4442,7 +4612,8 @@ class ParserOutput 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 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; } @@ -4456,12 +4627,15 @@ class ParserOutput return (bool)$this->mNewSection; } - function addLink( $title, $id ) { + 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; } @@ -4513,6 +4687,8 @@ class ParserOptions 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 @@ -4521,12 +4697,13 @@ class ParserOptions function getInterwikiMagic() { return $this->mInterwikiMagic; } function getAllowExternalImages() { return $this->mAllowExternalImages; } function getAllowExternalImagesFrom() { return $this->mAllowExternalImagesFrom; } - function getDateFormat() { return $this->mDateFormat; } 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 ) ) { @@ -4535,6 +4712,13 @@ class ParserOptions 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 ); } @@ -4547,6 +4731,8 @@ class ParserOptions 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 ); @@ -4556,14 +4742,14 @@ class ParserOptions * Get parser options * @static */ - function newFromUser( &$user ) { + static function newFromUser( $user ) { return new ParserOptions( $user ); } /** Get user options */ - function initialiseFromUser( &$userInput ) { + function initialiseFromUser( $userInput ) { global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages; - global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion; + global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion, $wgMaxArticleSize; $fname = 'ParserOptions::initialiseFromUser'; wfProfileIn( $fname ); if ( !$userInput ) { @@ -4586,12 +4772,14 @@ class ParserOptions $this->mAllowExternalImages = $wgAllowExternalImages; $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom; $this->mSkin = null; # Deferred - $this->mDateFormat = $user->getOption( 'date' ); + $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 ); } } @@ -4711,6 +4899,26 @@ function wfLoadSiteStats() { } /** + * Get revision timestamp from the database considering timecorrection + * + * @param $id Int: page revision id + * @return integer + */ +function wfRevisionTimestamp( $id ) { + global $wgContLang; + $fname = 'wfRevisionTimestamp'; + + wfProfileIn( $fname ); + $dbr =& wfGetDB( DB_SLAVE ); + $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', + array( 'rev_id' => $id ), __METHOD__ ); + $timestamp = $wgContLang->userAdjust( $timestamp ); + wfProfileOut( $fname ); + + return $timestamp; +} + +/** * Escape html tags * Basically replacing " > and < with HTML entities ( ", >, <) * diff --git a/includes/ParserCache.php b/includes/ParserCache.php index 3ec7512f..1f2e2aaf 100644 --- a/includes/ParserCache.php +++ b/includes/ParserCache.php @@ -13,7 +13,7 @@ class ParserCache { /** * Get an instance of this object */ - function &singleton() { + public static function &singleton() { static $instance; if ( !isset( $instance ) ) { global $parserMemc; @@ -33,7 +33,7 @@ class ParserCache { } function getKey( &$article, &$user ) { - global $wgDBname, $action; + global $action; $hash = $user->getPageRenderingHash(); if( !$article->mTitle->userCanEdit() ) { // section edit links are suppressed even if the user has them on @@ -43,7 +43,7 @@ class ParserCache { } $pageid = intval( $article->getID() ); $renderkey = (int)($action == 'render'); - $key = "$wgDBname:pcache:idhash:$pageid-$renderkey!$hash$edit"; + $key = wfMemcKey( 'pcache', 'idhash', "$pageid-$renderkey!$hash$edit" ); return $key; } diff --git a/includes/Profiler.php b/includes/Profiler.php new file mode 100644 index 00000000..78003e02 --- /dev/null +++ b/includes/Profiler.php @@ -0,0 +1,369 @@ +<?php +/** + * This file is only included if profiling is enabled + * @package MediaWiki + */ + +$wgProfiling = true; + +/** + * @param $functioname name of the function we will profile + */ +function wfProfileIn($functionname) { + global $wgProfiler; + $wgProfiler->profileIn($functionname); +} + +/** + * @param $functioname name of the function we have profiled + */ +function wfProfileOut($functionname = 'missing') { + global $wgProfiler; + $wgProfiler->profileOut($functionname); +} + +function wfGetProfilingOutput($start, $elapsed) { + global $wgProfiler; + return $wgProfiler->getOutput($start, $elapsed); +} + +function wfProfileClose() { + global $wgProfiler; + $wgProfiler->close(); +} + +if (!function_exists('memory_get_usage')) { + # Old PHP or --enable-memory-limit not compiled in + function memory_get_usage() { + return 0; + } +} + +/** + * @todo document + * @package MediaWiki + */ +class Profiler { + var $mStack = array (), $mWorkStack = array (), $mCollated = array (); + var $mCalls = array (), $mTotals = array (); + + function Profiler() + { + // Push an entry for the pre-profile setup time onto the stack + global $wgRequestTime; + if ( !empty( $wgRequestTime ) ) { + $this->mWorkStack[] = array( '-total', 0, $wgRequestTime, 0 ); + $this->mStack[] = array( '-setup', 1, $wgRequestTime, 0, microtime(true), 0 ); + } else { + $this->profileIn( '-total' ); + } + + } + + function profileIn($functionname) { + global $wgDebugFunctionEntry; + if ($wgDebugFunctionEntry && function_exists('wfDebug')) { + wfDebug(str_repeat(' ', count($this->mWorkStack)).'Entering '.$functionname."\n"); + } + $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), $this->getTime(), memory_get_usage()); + } + + function profileOut($functionname) { + $memory = memory_get_usage(); + $time = $this->getTime(); + + global $wgDebugFunctionEntry; + + if ($wgDebugFunctionEntry && function_exists('wfDebug')) { + wfDebug(str_repeat(' ', count($this->mWorkStack) - 1).'Exiting '.$functionname."\n"); + } + + $bit = array_pop($this->mWorkStack); + + if (!$bit) { + wfDebug("Profiling error, !\$bit: $functionname\n"); + } else { + //if ($wgDebugProfiling) { + if ($functionname == 'close') { + $message = "Profile section ended by close(): {$bit[0]}"; + wfDebug( "$message\n" ); + $this->mStack[] = array( $message, 0, '0 0', 0, '0 0', 0 ); + } + elseif ($bit[0] != $functionname) { + $message = "Profiling error: in({$bit[0]}), out($functionname)"; + wfDebug( "$message\n" ); + $this->mStack[] = array( $message, 0, '0 0', 0, '0 0', 0 ); + } + //} + $bit[] = $time; + $bit[] = $memory; + $this->mStack[] = $bit; + } + } + + function close() { + while (count($this->mWorkStack)) { + $this->profileOut('close'); + } + } + + function getOutput() { + global $wgDebugFunctionEntry; + $wgDebugFunctionEntry = false; + + if (!count($this->mStack) && !count($this->mCollated)) { + return "No profiling output\n"; + } + $this->close(); + + global $wgProfileCallTree; + if ($wgProfileCallTree) { + return $this->getCallTree(); + } else { + return $this->getFunctionReport(); + } + } + + function getCallTree($start = 0) { + return implode('', array_map(array (& $this, 'getCallTreeLine'), $this->remapCallTree($this->mStack))); + } + + function remapCallTree($stack) { + if (count($stack) < 2) { + return $stack; + } + $outputs = array (); + for ($max = count($stack) - 1; $max > 0;) { + /* Find all items under this entry */ + $level = $stack[$max][1]; + $working = array (); + for ($i = $max -1; $i >= 0; $i --) { + if ($stack[$i][1] > $level) { + $working[] = $stack[$i]; + } else { + break; + } + } + $working = $this->remapCallTree(array_reverse($working)); + $output = array (); + foreach ($working as $item) { + array_push($output, $item); + } + array_unshift($output, $stack[$max]); + $max = $i; + + array_unshift($outputs, $output); + } + $final = array (); + foreach ($outputs as $output) { + foreach ($output as $item) { + $final[] = $item; + } + } + return $final; + } + + function getCallTreeLine($entry) { + list ($fname, $level, $start, $x, $end) = $entry; + $delta = $end - $start; + $space = str_repeat(' ', $level); + + # The ugly double sprintf is to work around a PHP bug, + # which has been fixed in recent releases. + return sprintf( "%10s %s %s\n", + trim( sprintf( "%7.3f", $delta * 1000.0 ) ), + $space, $fname ); + } + + function getTime() { + return microtime(true); + #return $this->getUserTime(); + } + + function getUserTime() { + $ru = getrusage(); + return $ru['ru_utime.tv_sec'].' '.$ru['ru_utime.tv_usec'] / 1e6; + } + + function getFunctionReport() { + $width = 140; + $nameWidth = $width - 65; + $format = "%-{$nameWidth}s %6d %13.3f %13.3f %13.3f%% %9d (%13.3f -%13.3f) [%d]\n"; + $titleFormat = "%-{$nameWidth}s %6s %13s %13s %13s %9s\n"; + $prof = "\nProfiling data\n"; + $prof .= sprintf($titleFormat, 'Name', 'Calls', 'Total', 'Each', '%', 'Mem'); + $this->mCollated = array (); + $this->mCalls = array (); + $this->mMemory = array (); + + # Estimate profiling overhead + $profileCount = count($this->mStack); + wfProfileIn('-overhead-total'); + for ($i = 0; $i < $profileCount; $i ++) { + wfProfileIn('-overhead-internal'); + wfProfileOut('-overhead-internal'); + } + wfProfileOut('-overhead-total'); + + # First, subtract the overhead! + foreach ($this->mStack as $entry) { + $fname = $entry[0]; + $thislevel = $entry[1]; + $start = $entry[2]; + $end = $entry[4]; + $elapsed = $end - $start; + $memory = $entry[5] - $entry[3]; + + if ($fname == '-overhead-total') { + $overheadTotal[] = $elapsed; + $overheadMemory[] = $memory; + } + elseif ($fname == '-overhead-internal') { + $overheadInternal[] = $elapsed; + } + } + $overheadTotal = array_sum($overheadTotal) / count($overheadInternal); + $overheadMemory = array_sum($overheadMemory) / count($overheadInternal); + $overheadInternal = array_sum($overheadInternal) / count($overheadInternal); + + # Collate + foreach ($this->mStack as $index => $entry) { + $fname = $entry[0]; + $thislevel = $entry[1]; + $start = $entry[2]; + $end = $entry[4]; + $elapsed = $end - $start; + + $memory = $entry[5] - $entry[3]; + $subcalls = $this->calltreeCount($this->mStack, $index); + + if (!preg_match('/^-overhead/', $fname)) { + # Adjust for profiling overhead (except special values with elapsed=0 + if ( $elapsed ) { + $elapsed -= $overheadInternal; + $elapsed -= ($subcalls * $overheadTotal); + $memory -= ($subcalls * $overheadMemory); + } + } + + if (!array_key_exists($fname, $this->mCollated)) { + $this->mCollated[$fname] = 0; + $this->mCalls[$fname] = 0; + $this->mMemory[$fname] = 0; + $this->mMin[$fname] = 1 << 24; + $this->mMax[$fname] = 0; + $this->mOverhead[$fname] = 0; + } + + $this->mCollated[$fname] += $elapsed; + $this->mCalls[$fname]++; + $this->mMemory[$fname] += $memory; + $this->mMin[$fname] = min($this->mMin[$fname], $elapsed); + $this->mMax[$fname] = max($this->mMax[$fname], $elapsed); + $this->mOverhead[$fname] += $subcalls; + } + + $total = @ $this->mCollated['-total']; + $this->mCalls['-overhead-total'] = $profileCount; + + # Output + arsort($this->mCollated, SORT_NUMERIC); + foreach ($this->mCollated as $fname => $elapsed) { + $calls = $this->mCalls[$fname]; + $percent = $total ? 100. * $elapsed / $total : 0; + $memory = $this->mMemory[$fname]; + $prof .= sprintf($format, substr($fname, 0, $nameWidth), $calls, (float) ($elapsed * 1000), (float) ($elapsed * 1000) / $calls, $percent, $memory, ($this->mMin[$fname] * 1000.0), ($this->mMax[$fname] * 1000.0), $this->mOverhead[$fname]); + + global $wgProfileToDatabase; + if ($wgProfileToDatabase) { + Profiler :: logToDB($fname, (float) ($elapsed * 1000), $calls); + } + } + $prof .= "\nTotal: $total\n\n"; + + return $prof; + } + + /** + * Counts the number of profiled function calls sitting under + * the given point in the call graph. Not the most efficient algo. + * + * @param $stack Array: + * @param $start Integer: + * @return Integer + * @private + */ + function calltreeCount(& $stack, $start) { + $level = $stack[$start][1]; + $count = 0; + for ($i = $start -1; $i >= 0 && $stack[$i][1] > $level; $i --) { + $count ++; + } + return $count; + } + + /** + * @static + */ + function logToDB($name, $timeSum, $eventCount) { + # Warning: $wguname is a live patch, it should be moved to Setup.php + global $wguname, $wgProfilePerHost; + + $fname = 'Profiler::logToDB'; + $dbw = & wfGetDB(DB_MASTER); + if (!is_object($dbw)) + return false; + $errorState = $dbw->ignoreErrors( true ); + $profiling = $dbw->tableName('profiling'); + + $name = substr($name, 0, 255); + $encname = $dbw->strencode($name); + + if ($wgProfilePerHost) { + $pfhost = $wguname['nodename']; + } else { + $pfhost = ''; + } + + $sql = "UPDATE $profiling "."SET pf_count=pf_count+{$eventCount}, "."pf_time=pf_time + {$timeSum} ". + "WHERE pf_name='{$encname}' AND pf_server='{$pfhost}'"; + $dbw->query($sql); + + $rc = $dbw->affectedRows(); + if ($rc == 0) { + $dbw->insert('profiling', array ('pf_name' => $name, 'pf_count' => $eventCount, + 'pf_time' => $timeSum, 'pf_server' => $pfhost ), $fname, array ('IGNORE')); + } + // When we upgrade to mysql 4.1, the insert+update + // can be merged into just a insert with this construct added: + // "ON DUPLICATE KEY UPDATE ". + // "pf_count=pf_count + VALUES(pf_count), ". + // "pf_time=pf_time + VALUES(pf_time)"; + $dbw->ignoreErrors( $errorState ); + } + + /** + * Get the function name of the current profiling section + */ + function getCurrentSection() { + $elt = end($this->mWorkStack); + return $elt[0]; + } + + static function getCaller( $level ) { + $backtrace = debug_backtrace(); + if ( isset( $backtrace[$level] ) ) { + if ( isset( $backtrace[$level]['class'] ) ) { + $caller = $backtrace[$level]['class'] . '::' . $backtrace[$level]['function']; + } else { + $caller = $backtrace[$level]['function']; + } + } else { + $caller = 'unknown'; + } + return $caller; + } + +} + +?> diff --git a/includes/ProfilerSimple.php b/includes/ProfilerSimple.php index ed058c65..d5bdaf94 100644 --- a/includes/ProfilerSimple.php +++ b/includes/ProfilerSimple.php @@ -8,9 +8,11 @@ * @todo document * @package MediaWiki */ -require_once(dirname(__FILE__).'/Profiling.php'); +require_once(dirname(__FILE__).'/Profiler.php'); class ProfilerSimple extends Profiler { + var $mMinimumTime = 0; + function ProfilerSimple() { global $wgRequestTime,$wgRUstart; if (!empty($wgRequestTime) && !empty($wgRUstart)) { @@ -33,6 +35,10 @@ class ProfilerSimple extends Profiler { } } + function setMinimum( $min ) { + $this->mMinimumTime = $min; + } + function profileIn($functionname) { global $wgDebugFunctionEntry; if ($wgDebugFunctionEntry) { @@ -86,9 +92,14 @@ class ProfilerSimple extends Profiler { } function getCpuTime($ru=null) { - if ($ru==null) - $ru=getrusage(); - return ($ru['ru_utime.tv_sec']+$ru['ru_stime.tv_sec']+($ru['ru_utime.tv_usec']+$ru['ru_stime.tv_usec'])*1e-6); + if ( function_exists( 'getrusage' ) ) { + if ( $ru == null ) + $ru = getrusage(); + return ($ru['ru_utime.tv_sec'] + $ru['ru_stime.tv_sec'] + ($ru['ru_utime.tv_usec'] + + $ru['ru_stime.tv_usec']) * 1e-6); + } else { + return 0; + } } /* If argument is passed, it assumes that it is dual-format time string, returns proper float time value */ diff --git a/includes/ProfilerSimpleUDP.php b/includes/ProfilerSimpleUDP.php index c395228b..e0490512 100644 --- a/includes/ProfilerSimpleUDP.php +++ b/includes/ProfilerSimpleUDP.php @@ -3,20 +3,25 @@ (the one from wikipedia/udpprofile CVS ) */ -require_once(dirname(__FILE__).'/Profiling.php'); +require_once(dirname(__FILE__).'/Profiler.php'); require_once(dirname(__FILE__).'/ProfilerSimple.php'); class ProfilerSimpleUDP extends ProfilerSimple { function getFunctionReport() { global $wgUDPProfilerHost; global $wgUDPProfilerPort; - global $wgDBname; + if ( $this->mCollated['-total']['real'] < $this->mMinimumTime ) { + # Less than minimum, ignore + return; + } + + $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); $plength=0; $packet=""; foreach ($this->mCollated as $entry=>$pfdata) { - $pfline=sprintf ("%s %s %d %f %f %f %f %s\n", $wgDBname,"-",$pfdata['count'], + $pfline=sprintf ("%s %s %d %f %f %f %f %s\n", wfWikiID(),"-",$pfdata['count'], $pfdata['cpu'],$pfdata['cpu_sq'],$pfdata['real'],$pfdata['real_sq'],$entry); $length=strlen($pfline); /* printf("<!-- $pfline -->"); */ diff --git a/includes/ProfilerStub.php b/includes/ProfilerStub.php index 3bcdaab2..4cf0aa44 100644 --- a/includes/ProfilerStub.php +++ b/includes/ProfilerStub.php @@ -21,6 +21,6 @@ function wfProfileOut( $fn = '' ) { } function wfGetProfilingOutput( $s, $e ) {} function wfProfileClose() {} -function wfLogProfilingData() {} +$wgProfiling = false; ?> diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index 2a40a376..fd1bc81e 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -79,7 +79,7 @@ class ProtectionForm { $wgOut->addWikiText( wfMsg( $this->disabled ? "protect-viewtext" : "protect-text", - $this->mTitle->getPrefixedText() ) ); + wfEscapeWikiText( $this->mTitle->getPrefixedText() ) ) ); $wgOut->addHTML( $this->buildForm() ); @@ -230,7 +230,6 @@ class ProtectionForm { function showLogExtract( &$out ) { # Show relevant lines from the deletion log: $out->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'protect' ) ) . "</h2>\n" ); - require_once( 'SpecialLog.php' ); $logViewer = new LogViewer( new LogReader( new FauxRequest( diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php index bed79c10..7974c882 100644 --- a/includes/ProxyTools.php +++ b/includes/ProxyTools.php @@ -55,7 +55,7 @@ function wfGetIP() { # Set $ip to the IP address given by that trusted server, unless the address is not sensible (e.g. private) foreach ( $ipchain as $i => $curIP ) { if ( array_key_exists( $curIP, $trustedProxies ) ) { - if ( isset( $ipchain[$i + 1] ) && wfIsIPPublic( $ipchain[$i + 1] ) ) { + if ( isset( $ipchain[$i + 1] ) && IP::isPublic( $ipchain[$i + 1] ) ) { $ip = $ipchain[$i + 1]; } } else { @@ -70,74 +70,12 @@ function wfGetIP() { } /** - * Given an IP address in dotted-quad notation, returns an unsigned integer. - * Like ip2long() except that it actually works and has a consistent error return value. - */ -function wfIP2Unsigned( $ip ) { - $n = ip2long( $ip ); - if ( $n == -1 || $n === false ) { # Return value on error depends on PHP version - $n = false; - } elseif ( $n < 0 ) { - $n += pow( 2, 32 ); - } - return $n; -} - -/** - * Return a zero-padded hexadecimal representation of an IP address - */ -function wfIP2Hex( $ip ) { - $n = wfIP2Unsigned( $ip ); - if ( $n !== false ) { - $n = sprintf( '%08X', $n ); - } - return $n; -} - -/** - * Determine if an IP address really is an IP address, and if it is public, - * i.e. not RFC 1918 or similar - */ -function wfIsIPPublic( $ip ) { - $n = wfIP2Unsigned( $ip ); - if ( !$n ) { - return false; - } - - // ip2long accepts incomplete addresses, as well as some addresses - // followed by garbage characters. Check that it's really valid. - if( $ip != long2ip( $n ) ) { - return false; - } - - static $privateRanges = false; - if ( !$privateRanges ) { - $privateRanges = array( - array( '10.0.0.0', '10.255.255.255' ), # RFC 1918 (private) - array( '172.16.0.0', '172.31.255.255' ), # " - array( '192.168.0.0', '192.168.255.255' ), # " - array( '0.0.0.0', '0.255.255.255' ), # this network - array( '127.0.0.0', '127.255.255.255' ), # loopback - ); - } - - foreach ( $privateRanges as $r ) { - $start = wfIP2Unsigned( $r[0] ); - $end = wfIP2Unsigned( $r[1] ); - if ( $n >= $start && $n <= $end ) { - return false; - } - } - return true; -} - -/** * Forks processes to scan the originating IP for an open proxy server * MemCached can be used to skip IPs that have already been scanned */ function wfProxyCheck() { global $wgBlockOpenProxies, $wgProxyPorts, $wgProxyScriptPath; - global $wgUseMemCached, $wgMemc, $wgDBname, $wgProxyMemcExpiry; + global $wgUseMemCached, $wgMemc, $wgProxyMemcExpiry; global $wgProxyKey; if ( !$wgBlockOpenProxies ) { @@ -149,7 +87,7 @@ function wfProxyCheck() { # Get MemCached key $skip = false; if ( $wgUseMemCached ) { - $mcKey = "$wgDBname:proxy:ip:$ip"; + $mcKey = wfMemcKey( 'proxy', 'ip', $ip ); $mcValue = $wgMemc->get( $mcKey ); if ( $mcValue ) { $skip = true; @@ -182,18 +120,7 @@ function wfProxyCheck() { * Convert a network specification in CIDR notation to an integer network and a number of bits */ function wfParseCIDR( $range ) { - $parts = explode( '/', $range, 2 ); - if ( count( $parts ) != 2 ) { - return array( false, false ); - } - $network = wfIP2Unsigned( $parts[0] ); - if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 32 ) { - $bits = $parts[1]; - } else { - $network = false; - $bits = false; - } - return array( $network, $bits ); + return IP::parseCIDR( $range ); } /** diff --git a/includes/QueryPage.php b/includes/QueryPage.php index 53e17616..7d6dc900 100644 --- a/includes/QueryPage.php +++ b/includes/QueryPage.php @@ -448,10 +448,10 @@ class QueryPage { } function feedTitle() { - global $wgLanguageCode, $wgSitename; + global $wgContLanguageCode, $wgSitename; $page = SpecialPage::getPage( $this->getName() ); $desc = $page->getDescription(); - return "$wgSitename - $desc [$wgLanguageCode]"; + return "$wgSitename - $desc [$wgContLanguageCode]"; } function feedDesc() { diff --git a/includes/RawPage.php b/includes/RawPage.php index 3cdabfd9..a0b76886 100644 --- a/includes/RawPage.php +++ b/includes/RawPage.php @@ -22,6 +22,7 @@ class RawPage { function RawPage( &$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; @@ -37,6 +38,7 @@ class RawPage { $smaxage = $this->mRequest->getIntOrNull( 'smaxage', $wgSquidMaxage ); $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' ) ) { @@ -80,6 +82,12 @@ class RawPage { $this->mCharset = $wgInputEncoding; $this->mSmaxage = intval( $smaxage ); $this->mMaxage = $maxage; + + // Output may contain user-specific data; vary for open sessions + $this->mPrivateCache = ( $this->mSmaxage == 0 ) || + ( isset( $_COOKIE[ini_get( 'session.name' )] ) || + $wgUser->isLoggedIn() ); + if ( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) { $this->mContentType = 'text/x-wiki'; } else { @@ -127,13 +135,14 @@ class RawPage { header( "Content-type: ".$this->mContentType.'; charset='.$this->mCharset ); # allow the client to cache this for 24 hours - header( 'Cache-Control: s-maxage='.$this->mSmaxage.', max-age='.$this->mMaxage ); + $mode = $this->mPrivateCache ? 'private' : 'public'; + header( 'Cache-Control: '.$mode.', s-maxage='.$this->mSmaxage.', max-age='.$this->mMaxage ); echo $this->getRawText(); $wgOut->disable(); } function getRawText() { - global $wgUser, $wgOut; + global $wgUser, $wgOut, $wgRequest; if($this->mGen) { $sk = $wgUser->getSkin(); $sk->initPage($wgOut); @@ -152,11 +161,11 @@ class RawPage { $text = ''; if( $this->mTitle ) { // If it's a MediaWiki message we can just hit the message cache - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + if ( $this->mUseMessageCache && $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { $key = $this->mTitle->getDBkey(); $text = wfMsgForContentNoTrans( $key ); # If the message doesn't exist, return a blank - if( $text == '<' . $key . '>' ) + if( wfEmptyMsg( $key, $text ) ) $text = ''; $found = true; } else { @@ -188,14 +197,8 @@ class RawPage { return ''; else if ( $this->mExpandTemplates ) { - global $wgTitle; - - $parser = new Parser(); - $parser->Options( new ParserOptions() ); // We don't want this to be user-specific - $parser->Title( $wgTitle ); - $parser->OutputType( OT_HTML ); - - return $parser->replaceVariables( $text ); + global $wgParser; + return $wgParser->preprocess( $text, $this->mTitle, new ParserOptions() ); } else return $text; } diff --git a/includes/RecentChange.php b/includes/RecentChange.php index f320a47a..ebd4b335 100644 --- a/includes/RecentChange.php +++ b/includes/RecentChange.php @@ -108,11 +108,21 @@ class RecentChange $this->mAttribs['rc_ip'] = ''; } + ## If our database is strict about IP addresses, use NULL instead of an empty string + if ( $dbw->strictIPs() and $this->mAttribs['rc_ip'] == '' ) { + unset( $this->mAttribs['rc_ip'] ); + } + # Fixup database timestamps $this->mAttribs['rc_timestamp'] = $dbw->timestamp($this->mAttribs['rc_timestamp']); $this->mAttribs['rc_cur_time'] = $dbw->timestamp($this->mAttribs['rc_cur_time']); $this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'rc_rc_id_seq' ); + ## If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL + if ( $dbw->cascadingDeletes() and $this->mAttribs['rc_cur_id']==0 ) { + unset ( $this->mAttribs['rc_cur_id'] ); + } + # Insert new row $dbw->insert( 'recentchanges', $this->mAttribs, $fname ); @@ -163,7 +173,7 @@ class RecentChange } } - // E-mail notifications + # E-mail notifications global $wgUseEnotif; if( $wgUseEnotif ) { # this would be better as an extension hook @@ -177,6 +187,8 @@ class RecentChange $this->mAttribs['rc_last_oldid'] ); } + # Notify extensions + wfRunHooks( 'RecentChange_save', array( &$this ) ); } # Marks a certain row as patrolled @@ -200,8 +212,8 @@ class RecentChange $oldId, $lastTimestamp, $bot = "default", $ip = '', $oldSize = 0, $newSize = 0, $newId = 0) { - if ( $bot == 'default' ) { - $bot = $user->isBot(); + if ( $bot === 'default' ) { + $bot = $user->isAllowed( 'bot' ); } if ( !$ip ) { @@ -243,9 +255,14 @@ class RecentChange return( $rc->mAttribs['rc_id'] ); } - # Makes an entry in the database corresponding to page creation - # Note: the title object must be loaded with the new id using resetArticleID() - /*static*/ function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot = "default", + /** + * Makes an entry in the database corresponding to page creation + * Note: the title object must be loaded with the new id using resetArticleID() + * @todo Document parameters and return + * @public + * @static + */ + public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot = "default", $ip='', $size = 0, $newId = 0 ) { if ( !$ip ) { @@ -255,7 +272,7 @@ class RecentChange } } if ( $bot == 'default' ) { - $bot = $user->isBot(); + $bot = $user->isAllowed( 'bot' ); } $rc = new RecentChange; @@ -314,7 +331,7 @@ class RecentChange 'rc_comment' => $comment, 'rc_this_oldid' => 0, 'rc_last_oldid' => 0, - 'rc_bot' => $user->isBot() ? 1 : 0, + 'rc_bot' => $user->isAllowed( 'bot' ) ? 1 : 0, 'rc_moved_to_ns' => $newTitle->getNamespace(), 'rc_moved_to_title' => $newTitle->getDBkey(), 'rc_ip' => $ip, @@ -364,7 +381,7 @@ class RecentChange 'rc_comment' => $comment, 'rc_this_oldid' => 0, 'rc_last_oldid' => 0, - 'rc_bot' => $user->isBot() ? 1 : 0, + 'rc_bot' => $user->isAllowed( 'bot' ) ? 1 : 0, 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', 'rc_ip' => $ip, diff --git a/includes/Revision.php b/includes/Revision.php index 653bacb8..bd68e05a 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -4,9 +4,6 @@ * @todo document */ -/** */ -require_once( 'Database.php' ); - /** * @package MediaWiki * @todo document @@ -22,10 +19,10 @@ class Revision { * Returns null if no such revision can be found. * * @param int $id - * @static * @access public + * @static */ - function newFromId( $id ) { + public static function newFromId( $id ) { return Revision::newFromConds( array( 'page_id=rev_page', 'rev_id' => intval( $id ) ) ); @@ -42,7 +39,7 @@ class Revision { * @access public * @static */ - function newFromTitle( &$title, $id = 0 ) { + public static function newFromTitle( &$title, $id = 0 ) { if( $id ) { $matchId = intval( $id ); } else { @@ -56,6 +53,21 @@ class Revision { } /** + * Load a page revision from a given revision ID number. + * Returns null if no such revision can be found. + * + * @param Database $db + * @param int $id + * @access public + * @static + */ + public static function loadFromId( &$db, $id ) { + return Revision::loadFromConds( $db, + array( 'page_id=rev_page', + 'rev_id' => intval( $id ) ) ); + } + + /** * Load either the current, or a specified, revision * that's attached to a given page. If not attached * to that page, will return null. @@ -65,8 +77,9 @@ class Revision { * @param int $id * @return Revision * @access 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); @@ -86,8 +99,9 @@ class Revision { * @param int $id * @return Revision * @access public + * @static */ - function loadFromTitle( &$db, $title, $id = 0 ) { + public static function loadFromTitle( &$db, $title, $id = 0 ) { if( $id ) { $matchId = intval( $id ); } else { @@ -113,7 +127,7 @@ class Revision { * @access public * @static */ - function loadFromTimestamp( &$db, &$title, $timestamp ) { + public static function loadFromTimestamp( &$db, &$title, $timestamp ) { return Revision::loadFromConds( $db, array( 'rev_timestamp' => $db->timestamp( $timestamp ), @@ -127,10 +141,10 @@ class Revision { * * @param array $conditions * @return Revision - * @static * @access private + * @static */ - function newFromConds( $conditions ) { + private static function newFromConds( $conditions ) { $db =& wfGetDB( DB_SLAVE ); $row = Revision::loadFromConds( $db, $conditions ); if( is_null( $row ) ) { @@ -147,10 +161,10 @@ class Revision { * @param Database $db * @param array $conditions * @return Revision - * @static * @access private + * @static */ - function loadFromConds( &$db, $conditions ) { + private static function loadFromConds( &$db, $conditions ) { $res = Revision::fetchFromConds( $db, $conditions ); if( $res ) { $row = $res->fetchObject(); @@ -171,10 +185,10 @@ class Revision { * * @param Title $title * @return ResultWrapper - * @static * @access public + * @static */ - function fetchAllRevisions( &$title ) { + public static function fetchAllRevisions( &$title ) { return Revision::fetchFromConds( wfGetDB( DB_SLAVE ), array( 'page_namespace' => $title->getNamespace(), @@ -189,10 +203,10 @@ class Revision { * * @param Title $title * @return ResultWrapper - * @static * @access public + * @static */ - function fetchRevision( &$title ) { + public static function fetchRevision( &$title ) { return Revision::fetchFromConds( wfGetDB( DB_SLAVE ), array( 'rev_id=page_latest', @@ -209,10 +223,10 @@ class Revision { * @param Database $db * @param array $conditions * @return ResultWrapper - * @static * @access private + * @static */ - function fetchFromConds( &$db, $conditions ) { + private static function fetchFromConds( &$db, $conditions ) { $res = $db->select( array( 'page', 'revision' ), array( 'page_namespace', @@ -259,10 +273,13 @@ class Revision { $this->mTitle = null; } + // Lazy extraction... + $this->mText = null; if( isset( $row->old_text ) ) { - $this->mText = $this->getRevisionText( $row ); + $this->mTextRow = $row; } else { - $this->mText = null; + // 'text' table row entry will be lazy-loaded + $this->mTextRow = null; } } elseif( is_array( $row ) ) { // Build a new revision to be saved... @@ -519,7 +536,6 @@ class Revision { wfProfileOut( $fname ); return false; } - require_once('ExternalStore.php'); $text=ExternalStore::fetchFromURL($url); } @@ -609,7 +625,6 @@ class Revision { } else { $store = $wgDefaultExternalStore; } - require_once('ExternalStore.php'); // Store and get the URL $data = ExternalStore::insert( $store, $data ); if ( !$data ) { @@ -668,14 +683,37 @@ class Revision { function loadText() { $fname = 'Revision::loadText'; wfProfileIn( $fname ); - - $dbr =& wfGetDB( DB_SLAVE ); - $row = $dbr->selectRow( 'text', - array( 'old_text', 'old_flags' ), - array( 'old_id' => $this->getTextId() ), - $fname); + + // Caching may be beneficial for massive use of external storage + global $wgRevisionCacheExpiry, $wgMemc; + $key = wfMemcKey( 'revisiontext', 'textid', $this->getTextId() ); + if( $wgRevisionCacheExpiry ) { + $text = $wgMemc->get( $key ); + if( is_string( $text ) ) { + wfProfileOut( $fname ); + return $text; + } + } + + // If we kept data for lazy extraction, use it now... + if ( isset( $this->mTextRow ) ) { + $row = $this->mTextRow; + $this->mTextRow = null; + } else { + $row = null; + } + + if( !$row ) { + // Text data is immutable; check slaves first. + $dbr =& wfGetDB( DB_SLAVE ); + $row = $dbr->selectRow( 'text', + array( 'old_text', 'old_flags' ), + array( 'old_id' => $this->getTextId() ), + $fname); + } if( !$row ) { + // Possible slave lag! $dbw =& wfGetDB( DB_MASTER ); $row = $dbw->selectRow( 'text', array( 'old_text', 'old_flags' ), @@ -684,6 +722,11 @@ class Revision { } $text = Revision::getRevisionText( $row ); + + if( $wgRevisionCacheExpiry ) { + $wgMemc->set( $key, $text, $wgRevisionCacheExpiry ); + } + wfProfileOut( $fname ); return $text; diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index f5a24dfa..185679f6 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -327,77 +327,90 @@ class Sanitizer { * @param array $args for the processing callback * @return string */ - function removeHTMLtags( $text, $processCallback = null, $args = array() ) { + static function removeHTMLtags( $text, $processCallback = null, $args = array() ) { global $wgUseTidy, $wgUserHtml; - $fname = 'Parser::removeHTMLtags'; - wfProfileIn( $fname ); - - if( $wgUserHtml ) { - $htmlpairs = array( # Tags that must be closed - 'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1', - 'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's', - 'strike', 'strong', 'tt', 'var', 'div', 'center', - 'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre', - 'ruby', 'rt' , 'rb' , 'rp', 'p', 'span', 'u' - ); - $htmlsingle = array( - 'br', 'hr', 'li', 'dt', 'dd' - ); - $htmlsingleonly = array( # Elements that cannot have close tags - 'br', 'hr' - ); - $htmlnest = array( # Tags that can be nested--?? - 'table', 'tr', 'td', 'th', 'div', 'blockquote', 'ol', 'ul', - 'dl', 'font', 'big', 'small', 'sub', 'sup', 'span' - ); - $tabletags = array( # Can only appear inside table - 'td', 'th', 'tr', - ); - $htmllist = array( # Tags used by list - 'ul','ol', - ); - $listtags = array( # Tags that can appear in a list - 'li', - ); - } else { - $htmlpairs = array(); - $htmlsingle = array(); - $htmlnest = array(); - $tabletags = array(); - } + static $htmlpairs, $htmlsingle, $htmlsingleonly, $htmlnest, $tabletags, + $htmllist, $listtags, $htmlsingleallowed, $htmlelements, $staticInitialised; + + wfProfileIn( __METHOD__ ); + + if ( !$staticInitialised ) { + if( $wgUserHtml ) { + $htmlpairs = array( # Tags that must be closed + 'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1', + 'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's', + 'strike', 'strong', 'tt', 'var', 'div', 'center', + 'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre', + 'ruby', 'rt' , 'rb' , 'rp', 'p', 'span', 'u' + ); + $htmlsingle = array( + 'br', 'hr', 'li', 'dt', 'dd' + ); + $htmlsingleonly = array( # Elements that cannot have close tags + 'br', 'hr' + ); + $htmlnest = array( # Tags that can be nested--?? + 'table', 'tr', 'td', 'th', 'div', 'blockquote', 'ol', 'ul', + 'dl', 'font', 'big', 'small', 'sub', 'sup', 'span' + ); + $tabletags = array( # Can only appear inside table + 'td', 'th', 'tr', + ); + $htmllist = array( # Tags used by list + 'ul','ol', + ); + $listtags = array( # Tags that can appear in a list + 'li', + ); + + } else { + $htmlpairs = array(); + $htmlsingle = array(); + $htmlnest = array(); + $tabletags = array(); + } - $htmlsingleallowed = array_merge( $htmlsingle, $tabletags ); - $htmlelements = array_merge( $htmlsingle, $htmlpairs, $htmlnest ); + $htmlsingleallowed = array_merge( $htmlsingle, $tabletags ); + $htmlelements = array_merge( $htmlsingle, $htmlpairs, $htmlnest ); + + # Convert them all to hashtables for faster lookup + $vars = array( 'htmlpairs', 'htmlsingle', 'htmlsingleonly', 'htmlnest', 'tabletags', + 'htmllist', 'listtags', 'htmlsingleallowed', 'htmlelements' ); + foreach ( $vars as $var ) { + $$var = array_flip( $$var ); + } + $staticInitialised = true; + } # Remove HTML comments $text = Sanitizer::removeHTMLcomments( $text ); $bits = explode( '<', $text ); $text = array_shift( $bits ); if(!$wgUseTidy) { - $tagstack = array(); $tablestack = array(); + $tagstack = $tablestack = array(); foreach ( $bits as $x ) { $prev = error_reporting( E_ALL & ~( E_NOTICE | E_WARNING ) ); - preg_match( '/^(\\/?)(\\w+)([^>]*?)(\\/{0,1}>)([^<]*)$/', - $x, $regs ); + preg_match( '!^(/?)(\\w+)([^>]*?)(/{0,1}>)([^<]*)$!', $x, $regs ); list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs; error_reporting( $prev ); $badtag = 0 ; - if ( in_array( $t = strtolower( $t ), $htmlelements ) ) { + if ( isset( $htmlelements[$t = strtolower( $t )] ) ) { # Check our stack if ( $slash ) { # Closing a tag... - if( in_array( $t, $htmlsingleonly ) ) { + if( isset( $htmlsingleonly[$t] ) ) { $badtag = 1; } elseif ( ( $ot = @array_pop( $tagstack ) ) != $t ) { - if ( in_array($ot, $htmlsingleallowed) ) { + if ( isset( $htmlsingleallowed[$ot] ) ) { # Pop all elements with an optional close tag # and see if we find a match below them $optstack = array(); array_push ($optstack, $ot); while ( ( ( $ot = @array_pop( $tagstack ) ) != $t ) && - in_array($ot, $htmlsingleallowed) ) { + isset( $htmlsingleallowed[$ot] ) ) + { array_push ($optstack, $ot); } if ( $t != $ot ) { @@ -410,7 +423,7 @@ class Sanitizer { } else { @array_push( $tagstack, $ot ); # <li> can be nested in <ul> or <ol>, skip those cases: - if(!(in_array($ot, $htmllist) && in_array($t, $listtags) )) { + if(!(isset( $htmllist[$ot] ) && isset( $listtags[$t] ) )) { $badtag = 1; } } @@ -422,20 +435,20 @@ class Sanitizer { $newparams = ''; } else { # Keep track for later - if ( in_array( $t, $tabletags ) && + if ( isset( $tabletags[$t] ) && ! in_array( 'table', $tagstack ) ) { $badtag = 1; } else if ( in_array( $t, $tagstack ) && - ! in_array ( $t , $htmlnest ) ) { + ! isset( $htmlnest [$t ] ) ) { $badtag = 1 ; # Is it a self closed htmlpair ? (bug 5487) } else if( $brace == '/>' && - in_array($t, $htmlpairs) ) { + isset( $htmlpairs[$t] ) ) { $badtag = 1; - } elseif( in_array( $t, $htmlsingleonly ) ) { + } elseif( isset( $htmlsingleonly[$t] ) ) { # Hack to force empty tag for uncloseable elements $brace = '/>'; - } else if( in_array( $t, $htmlsingle ) ) { + } else if( isset( $htmlsingle[$t] ) ) { # Hack to not close $htmlsingle tags $brace = NULL; } else { @@ -475,7 +488,7 @@ class Sanitizer { preg_match( '/^(\\/?)(\\w+)([^>]*?)(\\/{0,1}>)([^<]*)$/', $x, $regs ); @list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs; - if ( in_array( $t = strtolower( $t ), $htmlelements ) ) { + if ( isset( $htmlelements[$t = strtolower( $t )] ) ) { if( is_callable( $processCallback ) ) { call_user_func_array( $processCallback, array( &$params, $args ) ); } @@ -487,7 +500,7 @@ class Sanitizer { } } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $text; } @@ -501,9 +514,8 @@ class Sanitizer { * @param string $text * @return string */ - function removeHTMLcomments( $text ) { - $fname='Parser::removeHTMLcomments'; - wfProfileIn( $fname ); + static function removeHTMLcomments( $text ) { + wfProfileIn( __METHOD__ ); while (($start = strpos($text, '<!--')) !== false) { $end = strpos($text, '-->', $start + 4); if ($end === false) { @@ -533,7 +545,7 @@ class Sanitizer { $text = substr_replace($text, '', $start, $end - $start); } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $text; } @@ -551,7 +563,7 @@ class Sanitizer { * @todo Check for legal values where the DTD limits things. * @todo Check for unique id attribute :P */ - function validateTagAttributes( $attribs, $element ) { + static function validateTagAttributes( $attribs, $element ) { $whitelist = array_flip( Sanitizer::attributeWhitelist( $element ) ); $out = array(); foreach( $attribs as $attribute => $value ) { @@ -626,7 +638,7 @@ class Sanitizer { * @param string $element * @return string */ - function fixTagAttributes( $text, $element ) { + static function fixTagAttributes( $text, $element ) { if( trim( $text ) == '' ) { return ''; } @@ -649,7 +661,7 @@ class Sanitizer { * @param $text * @return HTML-encoded text fragment */ - function encodeAttribute( $text ) { + static function encodeAttribute( $text ) { $encValue = htmlspecialchars( $text ); // Whitespace is normalized during attribute decoding, @@ -670,7 +682,7 @@ class Sanitizer { * @param $text * @return HTML-encoded text fragment */ - function safeEncodeAttribute( $text ) { + static function safeEncodeAttribute( $text ) { $encValue = Sanitizer::encodeAttribute( $text ); # Templates and links may be expanded in later parsing, @@ -713,7 +725,7 @@ class Sanitizer { * @param string $id * @return string */ - function escapeId( $id ) { + static function escapeId( $id ) { static $replace = array( '%3A' => ':', '%' => '.' @@ -730,7 +742,7 @@ class Sanitizer { * @return string * @private */ - function armorLinksCallback( $matches ) { + private static function armorLinksCallback( $matches ) { return str_replace( ':', ':', $matches[1] ); } @@ -742,7 +754,7 @@ class Sanitizer { * @param string * @return array */ - function decodeTagAttributes( $text ) { + static function decodeTagAttributes( $text ) { $attribs = array(); if( trim( $text ) == '' ) { @@ -780,7 +792,7 @@ class Sanitizer { * @return string * @private */ - function getTagAttributeCallback( $set ) { + private static function getTagAttributeCallback( $set ) { if( isset( $set[6] ) ) { # Illegal #XXXXXX color with no quotes. return $set[6]; @@ -814,7 +826,7 @@ class Sanitizer { * @return string * @private */ - function normalizeAttributeValue( $text ) { + private static function normalizeAttributeValue( $text ) { return str_replace( '"', '"', preg_replace( '/\r\n|[\x20\x0d\x0a\x09]/', @@ -836,7 +848,7 @@ class Sanitizer { * @return string * @private */ - function normalizeCharReferences( $text ) { + static function normalizeCharReferences( $text ) { return preg_replace_callback( MW_CHAR_REFS_REGEX, array( 'Sanitizer', 'normalizeCharReferencesCallback' ), @@ -846,7 +858,7 @@ class Sanitizer { * @param string $matches * @return string */ - function normalizeCharReferencesCallback( $matches ) { + static function normalizeCharReferencesCallback( $matches ) { $ret = null; if( $matches[1] != '' ) { $ret = Sanitizer::normalizeEntity( $matches[1] ); @@ -871,8 +883,9 @@ class Sanitizer { * * @param string $name * @return string + * @static */ - function normalizeEntity( $name ) { + static function normalizeEntity( $name ) { global $wgHtmlEntities; if( isset( $wgHtmlEntities[$name] ) ) { return "&$name;"; @@ -881,7 +894,7 @@ class Sanitizer { } } - function decCharReference( $codepoint ) { + static function decCharReference( $codepoint ) { $point = intval( $codepoint ); if( Sanitizer::validateCodepoint( $point ) ) { return sprintf( '&#%d;', $point ); @@ -890,7 +903,7 @@ class Sanitizer { } } - function hexCharReference( $codepoint ) { + static function hexCharReference( $codepoint ) { $point = hexdec( $codepoint ); if( Sanitizer::validateCodepoint( $point ) ) { return sprintf( '&#x%x;', $point ); @@ -904,7 +917,7 @@ class Sanitizer { * @param int $codepoint * @return bool */ - function validateCodepoint( $codepoint ) { + private static function validateCodepoint( $codepoint ) { return ($codepoint == 0x09) || ($codepoint == 0x0a) || ($codepoint == 0x0d) @@ -920,8 +933,9 @@ class Sanitizer { * @param string $text * @return string * @public + * @static */ - function decodeCharReferences( $text ) { + public static function decodeCharReferences( $text ) { return preg_replace_callback( MW_CHAR_REFS_REGEX, array( 'Sanitizer', 'decodeCharReferencesCallback' ), @@ -932,7 +946,7 @@ class Sanitizer { * @param string $matches * @return string */ - function decodeCharReferencesCallback( $matches ) { + static function decodeCharReferencesCallback( $matches ) { if( $matches[1] != '' ) { return Sanitizer::decodeEntity( $matches[1] ); } elseif( $matches[2] != '' ) { @@ -953,7 +967,7 @@ class Sanitizer { * @return string * @private */ - function decodeChar( $codepoint ) { + static function decodeChar( $codepoint ) { if( Sanitizer::validateCodepoint( $codepoint ) ) { return codepointToUtf8( $codepoint ); } else { @@ -969,7 +983,7 @@ class Sanitizer { * @param string $name * @return string */ - function decodeEntity( $name ) { + static function decodeEntity( $name ) { global $wgHtmlEntities; if( isset( $wgHtmlEntities[$name] ) ) { return codepointToUtf8( $wgHtmlEntities[$name] ); @@ -985,7 +999,7 @@ class Sanitizer { * @param string $element * @return array */ - function attributeWhitelist( $element ) { + static function attributeWhitelist( $element ) { static $list; if( !isset( $list ) ) { $list = Sanitizer::setupAttributeWhitelist(); @@ -996,9 +1010,10 @@ class Sanitizer { } /** + * @todo Document it a bit * @return array */ - function setupAttributeWhitelist() { + static function setupAttributeWhitelist() { $common = array( 'id', 'class', 'lang', 'dir', 'title', 'style' ); $block = array_merge( $common, array( 'align' ) ); $tablealign = array( 'align', 'char', 'charoff', 'valign' ); @@ -1082,9 +1097,9 @@ class Sanitizer { # 11.2.1 'table' => array_merge( $common, array( 'summary', 'width', 'border', 'frame', - 'rules', 'cellspacing', 'cellpadding', - 'align', 'bgcolor', 'frame', 'rules', - 'border' ) ), + 'rules', 'cellspacing', 'cellpadding', + 'align', 'bgcolor', + ) ), # 11.2.2 'caption' => array_merge( $common, array( 'align' ) ), @@ -1142,7 +1157,7 @@ class Sanitizer { * @param string $text HTML fragment * @return string */ - function stripAllTags( $text ) { + static function stripAllTags( $text ) { # Actual <tags> $text = preg_replace( '/ < .*? > /x', '', $text ); @@ -1169,7 +1184,7 @@ class Sanitizer { * @return string * @static */ - function hackDocType() { + static function hackDocType() { global $wgHtmlEntities; $out = "<!DOCTYPE html [\n"; foreach( $wgHtmlEntities as $entity => $codepoint ) { @@ -1178,6 +1193,47 @@ class Sanitizer { $out .= "]>\n"; return $out; } + + static function cleanUrl( $url, $hostname=true ) { + # Normalize any HTML entities in input. They will be + # re-escaped by makeExternalLink(). + $url = Sanitizer::decodeCharReferences( $url ); + + # Escape any control characters introduced by the above step + $url = preg_replace( '/[\][<>"\\x00-\\x20\\x7F]/e', "urlencode('\\0')", $url ); + + # Validate hostname portion + if( preg_match( '!^([^:]+:)(//[^/]+)?(.*)$!iD', $url, $matches ) ) { + list( $whole, $protocol, $host, $rest ) = $matches; + + // Characters that will be ignored in IDNs. + // http://tools.ietf.org/html/3454#section-3.1 + // Strip them before further processing so blacklists and such work. + $strip = "/ + \\s| # general whitespace + \xc2\xad| # 00ad SOFT HYPHEN + \xe1\xa0\x86| # 1806 MONGOLIAN TODO SOFT HYPHEN + \xe2\x80\x8b| # 200b ZERO WIDTH SPACE + \xe2\x81\xa0| # 2060 WORD JOINER + \xef\xbb\xbf| # feff ZERO WIDTH NO-BREAK SPACE + \xcd\x8f| # 034f COMBINING GRAPHEME JOINER + \xe1\xa0\x8b| # 180b MONGOLIAN FREE VARIATION SELECTOR ONE + \xe1\xa0\x8c| # 180c MONGOLIAN FREE VARIATION SELECTOR TWO + \xe1\xa0\x8d| # 180d MONGOLIAN FREE VARIATION SELECTOR THREE + \xe2\x80\x8c| # 200c ZERO WIDTH NON-JOINER + \xe2\x80\x8d| # 200d ZERO WIDTH JOINER + [\xef\xb8\x80-\xef\xb8\x8f] # fe00-fe00f VARIATION SELECTOR-1-16 + /xuD"; + + $host = preg_replace( $strip, '', $host ); + + // @fixme: validate hostnames here + + return $protocol . $host . $rest; + } else { + return $url; + } + } } diff --git a/includes/SearchEngine.php b/includes/SearchEngine.php index c3b38519..5e598883 100644 --- a/includes/SearchEngine.php +++ b/includes/SearchEngine.php @@ -50,67 +50,72 @@ class SearchEngine { * @return Title * @private */ - function getNearMatch( $term ) { - # Exact match? No need to look further. - $title = Title::newFromText( $term ); - if (is_null($title)) - return NULL; + function getNearMatch( $searchterm ) { + global $wgContLang; - if ( $title->getNamespace() == NS_SPECIAL || $title->exists() ) { - return $title; - } + $allSearchTerms = array($searchterm); - # Now try all lower case (i.e. first letter capitalized) - # - $title = Title::newFromText( strtolower( $term ) ); - if ( $title->exists() ) { - return $title; + if($wgContLang->hasVariants()){ + $allSearchTerms = array_merge($allSearchTerms,$wgContLang->convertLinkToAllVariants($searchterm)); } - # Now try capitalized string - # - $title = Title::newFromText( ucwords( strtolower( $term ) ) ); - if ( $title->exists() ) { - return $title; - } + foreach($allSearchTerms as $term){ - # Now try all upper case - # - $title = Title::newFromText( strtoupper( $term ) ); - if ( $title->exists() ) { - return $title; - } + # Exact match? No need to look further. + $title = Title::newFromText( $term ); + if (is_null($title)) + return NULL; - # Now try Word-Caps-Breaking-At-Word-Breaks, for hyphenated names etc - $title = Title::newFromText( preg_replace_callback( - '/\b([\w\x80-\xff]+)\b/', - create_function( '$matches', ' - global $wgContLang; - return $wgContLang->ucfirst($matches[1]); - ' ), - $term ) ); - if ( $title->exists() ) { - return $title; - } + if ( $title->getNamespace() == NS_SPECIAL || $title->exists() ) { + return $title; + } - global $wgCapitalLinks, $wgContLang; - if( !$wgCapitalLinks ) { - // Catch differs-by-first-letter-case-only - $title = Title::newFromText( $wgContLang->ucfirst( $term ) ); + # Now try all lower case (i.e. first letter capitalized) + # + $title = Title::newFromText( $wgContLang->lc( $term ) ); if ( $title->exists() ) { return $title; } - $title = Title::newFromText( $wgContLang->lcfirst( $term ) ); + + # Now try capitalized string + # + $title = Title::newFromText( $wgContLang->ucwords( $term ) ); + if ( $title->exists() ) { + return $title; + } + + # Now try all upper case + # + $title = Title::newFromText( $wgContLang->uc( $term ) ); if ( $title->exists() ) { return $title; } + + # Now try Word-Caps-Breaking-At-Word-Breaks, for hyphenated names etc + $title = Title::newFromText( $wgContLang->ucwordbreaks($term) ); + if ( $title->exists() ) { + return $title; + } + + global $wgCapitalLinks, $wgContLang; + if( !$wgCapitalLinks ) { + // Catch differs-by-first-letter-case-only + $title = Title::newFromText( $wgContLang->ucfirst( $term ) ); + if ( $title->exists() ) { + return $title; + } + $title = Title::newFromText( $wgContLang->lcfirst( $term ) ); + if ( $title->exists() ) { + return $title; + } + } } - $title = Title::newFromText( $term ); + $title = Title::newFromText( $searchterm ); # Entering an IP address goes to the contributions page if ( ( $title->getNamespace() == NS_USER && User::isIP($title->getText() ) ) - || User::isIP( trim( $term ) ) ) { + || User::isIP( trim( $searchterm ) ) ) { return Title::makeTitle( NS_SPECIAL, "Contributions/" . $title->getDbkey() ); } @@ -121,7 +126,7 @@ class SearchEngine { } # Quoted term? Try without the quotes... - if( preg_match( '/^"([^"]+)"$/', $term, $matches ) ) { + if( preg_match( '/^"([^"]+)"$/', $searchterm, $matches ) ) { return SearchEngine::getNearMatch( $matches[1] ); } diff --git a/includes/SearchPostgres.php b/includes/SearchPostgres.php index 8e36b0b5..faf53f02 100644 --- a/includes/SearchPostgres.php +++ b/includes/SearchPostgres.php @@ -98,8 +98,8 @@ class SearchPostgres extends SearchEngine { $match = $this->parseQuery( $filteredTerm, $fulltext ); $query = "SELECT page_id, page_namespace, page_title, old_text AS page_text ". - "FROM page p, revision r, text t WHERE p.page_latest = r.rev_id " . - "AND r.rev_text_id = t.old_id AND $fulltext @@ to_tsquery('$match')"; + "FROM page p, revision r, pagecontent c WHERE p.page_latest = r.rev_id " . + "AND r.rev_text_id = c.old_id AND $fulltext @@ to_tsquery('default','$match')"; ## Redirects if (! $this->showRedirects) @@ -113,7 +113,7 @@ class SearchPostgres extends SearchEngine { $query .= " AND page_namespace IN ($namespaces)"; } - $query .= " ORDER BY rank($fulltext, to_tsquery('$fulltext')) DESC"; + $query .= " ORDER BY rank($fulltext, to_tsquery('default','$fulltext')) DESC"; $query .= $this->db->limitResult( '', $this->limit, $this->offset ); diff --git a/includes/Setup.php b/includes/Setup.php index 1ef83cc7..8fe9ef71 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -8,7 +8,10 @@ * This file is not a valid entry point, perform no further processing unless * MEDIAWIKI is defined */ -if( defined( 'MEDIAWIKI' ) ) { +if( !defined( 'MEDIAWIKI' ) ) { + echo "This file is part of MediaWiki, it is not a valid entry point.\n"; + exit( 1 ); +} # The main wiki script and things like database # conversion and maintenance scripts all share a @@ -16,64 +19,37 @@ if( defined( 'MEDIAWIKI' ) ) { # setting up a few globals. # +$fname = 'Setup.php'; +wfProfileIn( $fname ); + // Check to see if we are at the file scope if ( !isset( $wgVersion ) ) { echo "Error, Setup.php must be included from the file scope, after DefaultSettings.php\n"; die( 1 ); } -if( !isset( $wgProfiling ) ) - $wgProfiling = false; - require_once( "$IP/includes/AutoLoader.php" ); -if ( function_exists( 'wfProfileIn' ) ) { - /* nada, everything should be done already */ -} elseif ( $wgProfiling and (0 == rand() % $wgProfileSampleRate ) ) { - $wgProfiling = true; - if ($wgProfilerType == "") { - $wgProfiler = new Profiler(); - } else { - $prclass="Profiler{$wgProfilerType}"; - require_once( $prclass.".php" ); - $wgProfiler = new $prclass(); - } -} else { - require_once( "$IP/includes/ProfilerStub.php" ); -} - -$fname = 'Setup.php'; -wfProfileIn( $fname ); - wfProfileIn( $fname.'-exception' ); require_once( "$IP/includes/Exception.php" ); wfInstallExceptionHandler(); wfProfileOut( $fname.'-exception' ); wfProfileIn( $fname.'-includes' ); - require_once( "$IP/includes/GlobalFunctions.php" ); require_once( "$IP/includes/Hooks.php" ); require_once( "$IP/includes/Namespace.php" ); -require_once( "$IP/includes/User.php" ); -require_once( "$IP/includes/OutputPage.php" ); -require_once( "$IP/includes/MagicWord.php" ); -require_once( "$IP/includes/MessageCache.php" ); -require_once( "$IP/includes/Parser.php" ); -require_once( "$IP/includes/LoadBalancer.php" ); require_once( "$IP/includes/ProxyTools.php" ); require_once( "$IP/includes/ObjectCache.php" ); require_once( "$IP/includes/ImageFunctions.php" ); - -if ( $wgUseDynamicDates ) { - require_once( "$IP/includes/DateFormatter.php" ); -} - +require_once( "$IP/includes/StubObject.php" ); wfProfileOut( $fname.'-includes' ); wfProfileIn( $fname.'-misc1' ); + $wgIP = false; # Load on demand -$wgRequest = new WebRequest(); +# Can't stub this one, it sets up $_GET and $_REQUEST in its constructor +$wgRequest = new WebRequest; if ( function_exists( 'posix_uname' ) ) { $wguname = posix_uname(); $wgNodeName = $wguname['nodename']; @@ -83,7 +59,7 @@ if ( function_exists( 'posix_uname' ) ) { # Useful debug output if ( $wgCommandLineMode ) { - # wfDebug( '"' . implode( '" "', $argv ) . '"' ); + wfDebug( "\n\nStart command line script $self\n" ); } elseif ( function_exists( 'getallheaders' ) ) { wfDebug( "\n\nStart request\n" ); wfDebug( $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . "\n" ); @@ -102,6 +78,14 @@ if ( $wgSkipSkin ) { $wgUseEnotif = $wgEnotifUserTalk || $wgEnotifWatchlist; +if($wgMetaNamespace === FALSE) { + $wgMetaNamespace = str_replace( ' ', '_', $wgSitename ); +} + +# These are now the same, always +# To determine the user language, use $wgLang->getCode() +$wgContLanguageCode = $wgLanguageCode; + wfProfileOut( $fname.'-misc1' ); wfProfileIn( $fname.'-memcached' ); @@ -131,7 +115,7 @@ if( !ini_get( 'session.auto_start' ) ) if( !$wgCommandLineMode && ( isset( $_COOKIE[session_name()] ) || isset( $_COOKIE[$wgCookiePrefix.'Token'] ) ) ) { wfIncrStats( 'request_with_session' ); - User::SetupSession(); + wfSetupSession(); $wgSessionStarted = true; } else { wfIncrStats( 'request_without_session' ); @@ -139,7 +123,7 @@ if( !$wgCommandLineMode && ( isset( $_COOKIE[session_name()] ) || isset( $_COOKI } wfProfileOut( $fname.'-SetupSession' ); -wfProfileIn( $fname.'-database' ); +wfProfileIn( $fname.'-globals' ); if ( !$wgDBservers ) { $wgDBservers = array(array( @@ -152,47 +136,18 @@ if ( !$wgDBservers ) { 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT )); } -$wgLoadBalancer = LoadBalancer::newFromParams( $wgDBservers, false, $wgMasterWaitTimeout ); -$wgLoadBalancer->loadMasterPos(); - -wfProfileOut( $fname.'-database' ); -wfProfileIn( $fname.'-language1' ); - -require_once( "$IP/languages/Language.php" ); - -function setupLangObj($langclass) { - global $IP; - - if( ! class_exists( $langclass ) ) { - # Default to English/UTF-8 - $baseclass = 'LanguageUtf8'; - require_once( "$IP/languages/$baseclass.php" ); - $lc = strtolower(substr($langclass, 8)); - $snip = " - class $langclass extends $baseclass { - function getVariants() { - return array(\"$lc\"); - } - - }"; - eval($snip); - } - $lang = new $langclass(); +$wgLoadBalancer = new StubObject( 'wgLoadBalancer', 'LoadBalancer', + array( $wgDBservers, false, $wgMasterWaitTimeout, true ) ); +$wgContLang = new StubContLang; +$wgUser = new StubUser; +$wgLang = new StubUserLang; +$wgOut = new StubObject( 'wgOut', 'OutputPage' ); +$wgParser = new StubObject( 'wgParser', 'Parser' ); +$wgMessageCache = new StubObject( 'wgMessageCache', 'MessageCache', + array( $parserMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry, wfWikiID() ) ); - return $lang; -} - -# $wgLanguageCode may be changed later to fit with user preference. -# The content language will remain fixed as per the configuration, -# so let's keep it. -$wgContLanguageCode = $wgLanguageCode; -$wgContLangClass = 'Language' . str_replace( '-', '_', ucfirst( $wgContLanguageCode ) ); - -$wgContLang = setupLangObj( $wgContLangClass ); -$wgContLang->initEncoding(); - -wfProfileOut( $fname.'-language1' ); +wfProfileOut( $fname.'-globals' ); wfProfileIn( $fname.'-User' ); # Skin setup functions @@ -204,104 +159,22 @@ foreach ( $wgSkinExtensionFunctions as $func ) { } if( !is_object( $wgAuth ) ) { - require_once( 'AuthPlugin.php' ); - $wgAuth = new AuthPlugin(); -} - -if( $wgCommandLineMode ) { - # Used for some maintenance scripts; user session cookies can screw things up - # when the database is in an in-between state. - $wgUser = new User(); - # Prevent loading User settings from the DB. - $wgUser->setLoaded( true ); -} else { - $wgUser = null; - wfRunHooks('AutoAuthenticate',array(&$wgUser)); - if ($wgUser === null) { - $wgUser = User::loadFromSession(); - } + $wgAuth = new StubObject( 'wgAuth', 'AuthPlugin' ); } - wfProfileOut( $fname.'-User' ); -wfProfileIn( $fname.'-language2' ); - -// wgLanguageCode now specifically means the UI language -$wgLanguageCode = $wgRequest->getText('uselang', ''); -if ($wgLanguageCode == '') - $wgLanguageCode = $wgUser->getOption('language'); -# Validate $wgLanguageCode, which will soon be sent to an eval() -if( empty( $wgLanguageCode ) || !preg_match( '/^[a-z]+(-[a-z]+)?$/', $wgLanguageCode ) ) { - $wgLanguageCode = $wgContLanguageCode; -} - -$wgLangClass = 'Language'. str_replace( '-', '_', ucfirst( $wgLanguageCode ) ); - -if( $wgLangClass == $wgContLangClass ) { - $wgLang = &$wgContLang; -} else { - wfSuppressWarnings(); - // Preload base classes to work around APC/PHP5 bug - include_once("$IP/languages/$wgLangClass.deps.php"); - include_once("$IP/languages/$wgLangClass.php"); - wfRestoreWarnings(); - - $wgLang = setupLangObj( $wgLangClass ); -} - -wfProfileOut( $fname.'-language2' ); -wfProfileIn( $fname.'-MessageCache' ); -$wgMessageCache = new MessageCache( $parserMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry, $wgDBname); - -wfProfileOut( $fname.'-MessageCache' ); - -# -# I guess the warning about UI switching might still apply... -# -# FIXME: THE ABOVE MIGHT BREAK NAMESPACES, VARIABLES, -# SEARCH INDEX UPDATES, AND MANY MANY THINGS. -# DO NOT USE THIS MODE EXCEPT FOR TESTING RIGHT NOW. -# -# To disable it, the easiest thing could be to uncomment the -# following; they should effectively disable the UI switch functionality -# -# $wgLangClass = $wgContLangClass; -# $wgLanguageCode = $wgContLanguageCode; -# $wgLang = $wgContLang; -# -# TODO: Need to change reference to $wgLang to $wgContLang at proper -# places, including namespaces, dates in signatures, magic words, -# and links -# -# TODO: Need to look at the issue of input/output encoding -# - - -wfProfileIn( $fname.'-OutputPage' ); - -$wgOut = new OutputPage(); - -wfProfileOut( $fname.'-OutputPage' ); wfProfileIn( $fname.'-misc2' ); $wgDeferredUpdateList = array(); $wgPostCommitUpdateList = array(); -$wgMagicWords = array(); +if ( $wgAjaxSearch ) $wgAjaxExportList[] = 'wfSajaxSearch'; -if ( $wgUseXMLparser ) { - require_once( 'ParserXML.php' ); - $wgParser = new ParserXML(); -} else { - $wgParser = new Parser(); -} -$wgOut->setParserOptions( ParserOptions::newFromUser( $wgUser ) ); -$wgMsgParserOptions = ParserOptions::newFromUser($wgUser); wfSeedRandom(); # Placeholders in case of DB error -$wgTitle = Title::makeTitle( NS_SPECIAL, 'Error' ); -$wgArticle = new Article($wgTitle); +$wgTitle = null; +$wgArticle = null; wfProfileOut( $fname.'-misc2' ); wfProfileIn( $fname.'-extensions' ); @@ -311,7 +184,10 @@ wfProfileIn( $fname.'-extensions' ); # of the extension file. This allows the extension to perform # any necessary initialisation in the fully initialised environment foreach ( $wgExtensionFunctions as $func ) { + $profName = $fname.'-extensions-'.strval( $func ); + wfProfileIn( $profName ); call_user_func( $func ); + wfProfileOut( $profName ); } // For compatibility @@ -321,10 +197,9 @@ wfRunHooks( 'LogPageLogHeader', array( &$wgLogHeaders ) ); wfRunHooks( 'LogPageActionText', array( &$wgLogActions ) ); -wfDebug( "\n" ); +wfDebug( "Fully initialised\n" ); $wgFullyInitialised = true; wfProfileOut( $fname.'-extensions' ); wfProfileOut( $fname ); -} ?> diff --git a/includes/SiteStatsUpdate.php b/includes/SiteStatsUpdate.php index 1b6d3804..b91dcfeb 100644 --- a/includes/SiteStatsUpdate.php +++ b/includes/SiteStatsUpdate.php @@ -75,8 +75,18 @@ class SiteStatsUpdate { if ( $updates ) { $site_stats = $dbw->tableName( 'site_stats' ); $sql = $dbw->limitResultForUpdate("UPDATE $site_stats SET $updates", 1); + $dbw->begin(); $dbw->query( $sql, $fname ); + $dbw->commit(); } + + /* + global $wgDBname, $wgTitle; + if ( $this->mGood && $wgDBname == 'enwiki' ) { + $good = $dbw->selectField( 'site_stats', 'ss_good_articles', '', $fname ); + error_log( $good . ' ' . $wgTitle->getPrefixedDBkey() . "\n", 3, '/home/wikipedia/logs/million.log' ); + } + */ } } ?> diff --git a/includes/Skin.php b/includes/Skin.php index 8a03f461..ffbe27c7 100644 --- a/includes/Skin.php +++ b/includes/Skin.php @@ -33,7 +33,7 @@ class Skin extends Linker { * @return array of strings * @static */ - function &getSkinNames() { + static function &getSkinNames() { global $wgValidSkinNames; static $skinsInitialised = false; if ( !$skinsInitialised ) { @@ -68,7 +68,7 @@ class Skin extends Linker { * @return string * @static */ - function normalizeKey( $key ) { + static function normalizeKey( $key ) { global $wgDefaultSkin; $skinNames = Skin::getSkinNames(); @@ -107,7 +107,7 @@ class Skin extends Linker { * @return Skin * @static */ - function &newFromKey( $key ) { + static function &newFromKey( $key ) { global $wgStyleDirectory; $key = Skin::normalizeKey( $key ); @@ -133,7 +133,7 @@ class Skin extends Linker { $className = 'SkinStandard'; require_once( "{$wgStyleDirectory}/Standard.php" ); } - $skin =& new $className; + $skin = new $className; return $skin; } @@ -157,7 +157,7 @@ class Skin extends Linker { } function initPage( &$out ) { - global $wgFavicon; + global $wgFavicon, $wgScriptPath, $wgSitename, $wgLanguageCode, $wgLanguageNames; $fname = 'Skin::initPage'; wfProfileIn( $fname ); @@ -166,6 +166,14 @@ class Skin extends Linker { $out->addLink( array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) ); } + # OpenSearch description link + $out->addLink( array( + 'rel' => 'search', + 'type' => 'application/opensearchdescription+xml', + 'href' => "$wgScriptPath/opensearch_desc.php", + 'title' => "$wgSitename ({$wgLanguageNames[$wgLanguageCode]})", + )); + $this->addMetadataLinks($out); $this->mRevisionId = $out->mRevisionId; @@ -255,17 +263,70 @@ class Skin extends Linker { $out->out( $this->afterContent() ); + $out->out( $this->bottomScripts() ); + $out->out( $out->reportTime() ); $out->out( "\n</body></html>" ); } + static function makeGlobalVariablesScript( $data ) { + $r = '<script type= "' . $data['jsmimetype'] . '"> + var skin = "' . Xml::escapeJsString( $data['skinname'] ) . '"; + var stylepath = "' . Xml::escapeJsString( $data['stylepath'] ) . '"; + + var wgArticlePath = "' . Xml::escapeJsString( $data['articlepath'] ) . '"; + var wgScriptPath = "' . Xml::escapeJsString( $data['scriptpath'] ) . '"; + var wgServer = "' . Xml::escapeJsString( $data['serverurl'] ) . '"; + + var wgCanonicalNamespace = "' . Xml::escapeJsString( $data['nscanonical'] ) . '"; + var wgNamespaceNumber = ' . (int)$data['nsnumber'] . '; + var wgPageName = "' . Xml::escapeJsString( $data['titleprefixeddbkey'] ) . '"; + var wgTitle = "' . Xml::escapeJsString( $data['titletext'] ) . '"; + var wgArticleId = ' . (int)$data['articleid'] . '; + var wgIsArticle = ' . ( $data['isarticle'] ? 'true' : 'false' ) . '; + + var wgUserName = ' . ( $data['username'] == NULL ? 'null' : ( '"' . Xml::escapeJsString( $data['username'] ) . '"' ) ) . '; + var wgUserLanguage = "' . Xml::escapeJsString( $data['userlang'] ) . '"; + var wgContentLanguage = "' . Xml::escapeJsString( $data['lang'] ) . '"; + </script> + '; + + return $r; + } + function getHeadScripts() { global $wgStylePath, $wgUser, $wgAllowUserJs, $wgJsMimeType; - $r = "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/wikibits.js\"></script>\n"; + global $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgLang; + global $wgTitle, $wgCanonicalNamespaceNames, $wgOut; + + $nsname = @$wgCanonicalNamespaceNames[ $wgTitle->getNamespace() ]; + if ( $nsname === NULL ) $nsname = $wgTitle->getNsText(); + + $vars = array( + 'jsmimetype' => $wgJsMimeType, + 'skinname' => $this->getSkinName(), + 'stylepath' => $wgStylePath, + 'articlepath' => $wgArticlePath, + 'scriptpath' => $wgScriptPath, + 'serverurl' => $wgServer, + 'nscanonical' => $nsname, + 'nsnumber' => $wgTitle->getNamespace(), + 'titleprefixeddbkey' => $wgTitle->getPrefixedDBKey(), + 'titletext' => $wgTitle->getText(), + 'articleid' => $wgTitle->getArticleId(), + 'isarticle' => $wgOut->isArticle(), + 'username' => $wgUser->isAnon() ? NULL : $wgUser->getName(), + 'userlang' => $wgLang->getCode(), + 'lang' => $wgContLang->getCode(), + ); + + $r = self::makeGlobalVariablesScript( $vars ); + + $r .= "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/wikibits.js\"></script>\n"; if( $wgAllowUserJs && $wgUser->isLoggedIn() ) { $userpage = $wgUser->getUserPage(); - $userjs = htmlspecialchars( $this->makeUrl( + $userjs = htmlspecialchars( self::makeUrl( $userpage->getPrefixedText().'/'.$this->getSkinName().'.js', 'action=raw&ctype='.$wgJsMimeType)); $r .= '<script type="'.$wgJsMimeType.'" src="'.$userjs."\"></script>\n"; @@ -305,9 +366,9 @@ class Skin extends Linker { $s = "@import \"$wgStylePath/$sheet\";\n"; if($wgContLang->isRTL()) $s .= "@import \"$wgStylePath/common/common_rtl.css\";\n"; - $query = "action=raw&ctype=text/css&smaxage=$wgSquidMaxage"; - $s .= '@import "' . $this->makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI ) . "\";\n" . - '@import "'.$this->makeNSUrl( ucfirst( $this->getSkinName() . '.css' ), $query, NS_MEDIAWIKI ) . "\";\n"; + $query = "usemsgcache=yes&action=raw&ctype=text/css&smaxage=$wgSquidMaxage"; + $s .= '@import "' . self::makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI ) . "\";\n" . + '@import "' . self::makeNSUrl( ucfirst( $this->getSkinName() . '.css' ), $query, NS_MEDIAWIKI ) . "\";\n"; $s .= $this->doGetUserStyles(); return $s."\n"; @@ -343,7 +404,7 @@ class Skin extends Linker { $s .= $wgRequest->getText('wpTextbox1'); } else { $userpage = $wgUser->getUserPage(); - $s.= '@import "'.$this->makeUrl( + $s.= '@import "'.self::makeUrl( $userpage->getPrefixedText().'/'.$this->getSkinName().'.css', 'action=raw&ctype=text/css').'";'."\n"; } @@ -393,7 +454,7 @@ END; } function getBodyOptions() { - global $wgUser, $wgTitle, $wgOut, $wgRequest; + global $wgUser, $wgTitle, $wgOut, $wgRequest, $wgContLang; extract( $wgRequest->getValues( 'oldid', 'redirect', 'diff' ) ); @@ -416,6 +477,7 @@ END; } $a['onload'] .= 'setupRightClickEdit()'; } + $a['class'] = 'ns-'.$wgTitle->getNamespace().' '.($wgContLang->isRTL() ? "rtl" : "ltr"); return $a; } @@ -573,14 +635,23 @@ END; } /** - * This gets called immediately before the \</body\> tag. - * @return String HTML to be put after \</body\> ??? + * This gets called shortly before the \</body\> tag. + * @return String HTML to be put before \</body\> */ function afterContent() { $printfooter = "<div class=\"printfooter\">\n" . $this->printFooter() . "</div>\n"; return $printfooter . $this->doAfterContent(); } + /** + * This gets called shortly before the \</body\> tag. + * @return String HTML-wrapped JS code to be put before \</body\> + */ + function bottomScripts() { + global $wgJsMimeType; + return "\n\t\t<script type=\"$wgJsMimeType\">if (window.runOnloadHook) runOnloadHook();</script>\n"; + } + /** @return string Retrievied from HTML text */ function printSource() { global $wgTitle; @@ -802,8 +873,8 @@ END; . $this->escapeSearchLink() . "\">\n" . '<input type="text" name="search" size="19" value="' . htmlspecialchars(substr($search,0,256)) . "\" />\n" - . '<input type="submit" name="go" value="' . wfMsg ('go') . '" /> ' - . '<input type="submit" name="fulltext" value="' . wfMsg ('search') . "\" />\n</form>"; + . '<input type="submit" name="go" value="' . wfMsg ('searcharticle') . '" /> ' + . '<input type="submit" name="fulltext" value="' . wfMsg ('searchbutton') . "\" />\n</form>"; return $s; } @@ -983,8 +1054,9 @@ END; $timestamp = $wgArticle->getTimestamp(); if ( $timestamp ) { - $d = $wgLang->timeanddate( $timestamp, true ); - $s = ' ' . wfMsg( 'lastmodified', $d ); + $d = $wgLang->date( $timestamp, true ); + $t = $wgLang->time( $timestamp, true ); + $s = ' ' . wfMsg( 'lastmodifiedat', $d, $t ); } else { $s = ''; } @@ -1013,30 +1085,13 @@ END; /** * show a drop-down box of special pages - * @TODO crash bug913. Need to be rewrote completly. */ function specialPagesList() { - global $wgUser, $wgContLang, $wgServer, $wgRedirectScript, $wgAvailableRights; - require_once('SpecialPage.php'); + global $wgUser, $wgContLang, $wgServer, $wgRedirectScript; $a = array(); - $pages = SpecialPage::getPages(); - - // special pages without access restriction - foreach ( $pages[''] as $name => $page ) { - $a[$name] = $page->getDescription(); - } - - // Other special pages that are restricted. - // Copied from SpecialSpecialpages.php - foreach($wgAvailableRights as $right) { - if( $wgUser->isAllowed($right) ) { - /** Add all pages for this right */ - if(isset($pages[$right])) { - foreach($pages[$right] as $name => $page) { - $a[$name] = $page->getDescription(); - } - } - } + $pages = array_merge( SpecialPage::getRegularPages(), SpecialPage::getRestrictedPages() ); + foreach ( $pages as $name => $page ) { + $pages[$name] = $page->getDescription(); } $go = wfMsg( 'go' ); @@ -1049,7 +1104,7 @@ END; $s .= "<option value=\"{$spp}\">{$sp}</option>\n"; - foreach ( $a as $name => $desc ) { + foreach ( $pages as $name => $desc ) { $p = $wgContLang->specialPage( $name ); $s .= "<option value=\"{$p}\">{$desc}</option>\n"; } @@ -1323,20 +1378,32 @@ END; if( $wgTitle->isTalkPage() ) { $link = $wgTitle->getSubjectPage(); switch( $link->getNamespace() ) { - case NS_MAIN: - $text = wfMsg('articlepage'); - break; - case NS_USER: - $text = wfMsg('userpage'); - break; - case NS_PROJECT: - $text = wfMsg('projectpage'); - break; - case NS_IMAGE: - $text = wfMsg('imagepage'); - break; - default: - $text= wfMsg('articlepage'); + case NS_MAIN: + $text = wfMsg( 'articlepage' ); + break; + case NS_USER: + $text = wfMsg( 'userpage' ); + break; + case NS_PROJECT: + $text = wfMsg( 'projectpage' ); + break; + case NS_IMAGE: + $text = wfMsg( 'imagepage' ); + break; + case NS_MEDIAWIKI: + $text = wfMsg( 'mediawikipage' ); + break; + case NS_TEMPLATE: + $text = wfMsg( 'templatepage' ); + break; + case NS_HELP: + $text = wfMsg( 'viewhelppage' ); + break; + case NS_CATEGORY: + $text = wfMsg( 'categorypage' ); + break; + default: + $text = wfMsg( 'articlepage' ); } } else { $link = $wgTitle->getTalkPage(); @@ -1370,56 +1437,56 @@ END; } /* these are used extensively in SkinTemplate, but also some other places */ - /*static*/ function makeSpecialUrl( $name, $urlaction='' ) { + static function makeSpecialUrl( $name, $urlaction = '' ) { $title = Title::makeTitle( NS_SPECIAL, $name ); return $title->getLocalURL( $urlaction ); } - /*static*/ function makeI18nUrl ( $name, $urlaction='' ) { - $title = Title::newFromText( wfMsgForContent($name) ); - $this->checkTitle($title, $name); + static function makeI18nUrl( $name, $urlaction = '' ) { + $title = Title::newFromText( wfMsgForContent( $name ) ); + self::checkTitle( $title, $name ); return $title->getLocalURL( $urlaction ); } - /*static*/ function makeUrl ( $name, $urlaction='' ) { + static function makeUrl( $name, $urlaction = '' ) { $title = Title::newFromText( $name ); - $this->checkTitle($title, $name); + self::checkTitle( $title, $name ); return $title->getLocalURL( $urlaction ); } # If url string starts with http, consider as external URL, else # internal - /*static*/ function makeInternalOrExternalUrl( $name ) { + static function makeInternalOrExternalUrl( $name ) { if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $name ) ) { return $name; } else { - return $this->makeUrl( $name ); + return self::makeUrl( $name ); } } # this can be passed the NS number as defined in Language.php - /*static*/ function makeNSUrl( $name, $urlaction='', $namespace=NS_MAIN ) { + static function makeNSUrl( $name, $urlaction = '', $namespace = NS_MAIN ) { $title = Title::makeTitleSafe( $namespace, $name ); - $this->checkTitle($title, $name); + self::checkTitle( $title, $name ); return $title->getLocalURL( $urlaction ); } /* these return an array with the 'href' and boolean 'exists' */ - /*static*/ function makeUrlDetails ( $name, $urlaction='' ) { + static function makeUrlDetails( $name, $urlaction = '' ) { $title = Title::newFromText( $name ); - $this->checkTitle($title, $name); + self::checkTitle( $title, $name ); return array( 'href' => $title->getLocalURL( $urlaction ), - 'exists' => $title->getArticleID() != 0?true:false + 'exists' => $title->getArticleID() != 0 ? true : false ); } /** * Make URL details where the article exists (or at least it's convenient to think so) */ - function makeKnownUrlDetails( $name, $urlaction='' ) { + static function makeKnownUrlDetails( $name, $urlaction = '' ) { $title = Title::newFromText( $name ); - $this->checkTitle($title, $name); + self::checkTitle( $title, $name ); return array( 'href' => $title->getLocalURL( $urlaction ), 'exists' => true @@ -1427,10 +1494,10 @@ END; } # make sure we have some title to operate on - /*static*/ function checkTitle ( &$title, &$name ) { - if(!is_object($title)) { + static function checkTitle( &$title, &$name ) { + if( !is_object( $title ) ) { $title = Title::newFromText( $name ); - if(!is_object($title)) { + if( !is_object( $title ) ) { $title = Title::newFromText( '--error: link target missing--' ); } } @@ -1443,16 +1510,16 @@ END; * @private */ function buildSidebar() { - global $wgDBname, $parserMemc, $wgEnableSidebarCache; - global $wgLanguageCode, $wgContLanguageCode; + global $parserMemc, $wgEnableSidebarCache; + global $wgLang, $wgContLang; $fname = 'SkinTemplate::buildSidebar'; wfProfileIn( $fname ); - $key = "{$wgDBname}:sidebar"; + $key = wfMemcKey( 'sidebar' ); $cacheSidebar = $wgEnableSidebarCache && - ($wgLanguageCode == $wgContLanguageCode); + ($wgLang->getCode() == $wgContLang->getCode()); if ($cacheSidebar) { $cachedsidebar = $parserMemc->get( $key ); @@ -1480,7 +1547,7 @@ END; $text = $line[1]; if (wfEmptyMsg($line[0], $link)) $link = $line[0]; - $href = $this->makeInternalOrExternalUrl( $link ); + $href = self::makeInternalOrExternalUrl( $link ); $bar[$heading][] = array( 'text' => $text, 'href' => $href, diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php index 6657d381..482680e6 100644 --- a/includes/SkinTemplate.php +++ b/includes/SkinTemplate.php @@ -31,8 +31,6 @@ if ( ! defined( 'MEDIAWIKI' ) ) * @subpackage Skins */ -require_once 'GlobalFunctions.php'; - /** * Wrapper object for MediaWiki's localization functions, * to be passed to the template engine. @@ -140,7 +138,7 @@ class SkinTemplate extends Skin { global $wgMaxCredits, $wgShowCreditsIfMax; global $wgPageShowWatchingUsers; global $wgUseTrackbacks; - global $wgDBname; + global $wgArticlePath, $wgScriptPath, $wgServer, $wgLang, $wgCanonicalNamespaceNames; $fname = 'SkinTemplate::outputPage'; wfProfileIn( $fname ); @@ -175,11 +173,11 @@ class SkinTemplate extends Skin { $this->userpage = $userPage->getPrefixedText(); if ( $wgUser->isLoggedIn() || $this->showIPinHeader() ) { - $this->userpageUrlDetails = $this->makeUrlDetails($this->userpage); + $this->userpageUrlDetails = self::makeUrlDetails( $this->userpage ); } else { # This won't be used in the standard skins, but we define it to preserve the interface # To save time, we check for existence - $this->userpageUrlDetails = $this->makeKnownUrlDetails($this->userpage); + $this->userpageUrlDetails = self::makeKnownUrlDetails( $this->userpage ); } $this->usercss = $this->userjs = $this->userjsprev = false; @@ -193,6 +191,16 @@ class SkinTemplate extends Skin { $tpl->set( 'pagetitle', $wgOut->getHTMLTitle() ); $tpl->set( 'displaytitle', $wgOut->mPageLinkTitle ); + $nsname = @$wgCanonicalNamespaceNames[ $this->mTitle->getNamespace() ]; + if ( $nsname === NULL ) $nsname = $this->mTitle->getNsText(); + + $tpl->set( 'nscanonical', $nsname ); + $tpl->set( 'nsnumber', $this->mTitle->getNamespace() ); + $tpl->set( 'titleprefixeddbkey', $this->mTitle->getPrefixedDBKey() ); + $tpl->set( 'titletext', $this->mTitle->getText() ); + $tpl->set( 'articleid', $this->mTitle->getArticleId() ); + $tpl->set( 'isarticle', $wgOut->isArticle() ); + $tpl->setRef( "thispage", $this->thispage ); $subpagestr = $this->subPageSubtitle(); $tpl->set( @@ -230,6 +238,7 @@ class SkinTemplate extends Skin { $tpl->set('headscripts', $out->getScript() ); $tpl->setRef( 'wgScript', $wgScript ); $tpl->setRef( 'skinname', $this->skinname ); + $tpl->set( 'skinclass', get_class( $this ) ); $tpl->setRef( 'stylename', $this->stylename ); $tpl->set( 'printable', $wgRequest->getBool( 'printable' ) ); $tpl->setRef( 'loggedin', $this->loggedin ); @@ -245,15 +254,19 @@ class SkinTemplate extends Skin { $tpl->set( 'searchaction', $this->escapeSearchLink() ); $tpl->set( 'search', trim( $wgRequest->getVal( 'search' ) ) ); $tpl->setRef( 'stylepath', $wgStylePath ); + $tpl->setRef( 'articlepath', $wgArticlePath ); + $tpl->setRef( 'scriptpath', $wgScriptPath ); + $tpl->setRef( 'serverurl', $wgServer ); $tpl->setRef( 'logopath', $wgLogo ); $tpl->setRef( "lang", $wgContLanguageCode ); $tpl->set( 'dir', $wgContLang->isRTL() ? "rtl" : "ltr" ); $tpl->set( 'rtl', $wgContLang->isRTL() ); $tpl->set( 'langname', $wgContLang->getLanguageName( $wgContLanguageCode ) ); $tpl->set( 'showjumplinks', $wgUser->getOption( 'showjumplinks' ) ); - $tpl->setRef( 'username', $this->username ); + $tpl->set( 'username', $wgUser->isAnon() ? NULL : $this->username ); $tpl->setRef( 'userpage', $this->userpage); $tpl->setRef( 'userpageurl', $this->userpageUrlDetails['href']); + $tpl->set( 'userlang', $wgLang->getCode() ); $tpl->set( 'pagecss', $this->setupPageCss() ); $tpl->setRef( 'usercss', $this->usercss); $tpl->setRef( 'userjs', $this->userjs); @@ -261,16 +274,16 @@ class SkinTemplate extends Skin { global $wgUseSiteJs; if ($wgUseSiteJs) { if($this->loggedin) { - $tpl->set( 'jsvarurl', $this->makeUrl('-','action=raw&smaxage=0&gen=js') ); + $tpl->set( 'jsvarurl', self::makeUrl('-','action=raw&smaxage=0&gen=js') ); } else { - $tpl->set( 'jsvarurl', $this->makeUrl('-','action=raw&gen=js') ); + $tpl->set( 'jsvarurl', self::makeUrl('-','action=raw&gen=js') ); } } else { $tpl->set('jsvarurl', false); } $newtalks = $wgUser->getNewMessageLinks(); - if (count($newtalks) == 1 && $newtalks[0]["wiki"] === $wgDBname) { + if (count($newtalks) == 1 && $newtalks[0]["wiki"] === wfWikiID() ) { $usertitle = $this->mUser->getUserPage(); $usertalktitle = $usertitle->getTalkPage(); if( !$usertalktitle->equals( $this->mTitle ) ) { @@ -308,7 +321,9 @@ class SkinTemplate extends Skin { $tpl->setRef( 'newtalk', $ntl ); $tpl->setRef( 'skin', $this); $tpl->set( 'logo', $this->logoText() ); - if ( $wgOut->isArticle() and (!isset( $oldid ) or isset( $diff )) and 0 != $wgArticle->getID() ) { + if ( $wgOut->isArticle() and (!isset( $oldid ) or isset( $diff )) and + $wgArticle and 0 != $wgArticle->getID() ) + { if ( !$wgDisableCounters ) { $viewcount = $wgLang->formatNum( $wgArticle->getCount() ); if ( $viewcount ) { @@ -376,6 +391,7 @@ class SkinTemplate extends Skin { $tpl->setRef( 'debug', $out->mDebugtext ); $tpl->set( 'reporttime', $out->reportTime() ); $tpl->set( 'sitenotice', wfGetSiteNotice() ); + $tpl->set( 'bottomscripts', $this->bottomScripts() ); $printfooter = "<div class=\"printfooter\">\n" . $this->printSource() . "</div>\n"; $out->mBodytext .= $printfooter ; @@ -474,27 +490,27 @@ class SkinTemplate extends Skin { 'class' => $usertalkUrlDetails['exists']?false:'new', 'active' => ( $usertalkUrlDetails['href'] == $pageurl ) ); - $href = $this->makeSpecialUrl('Preferences'); + $href = self::makeSpecialUrl( 'Preferences' ); $personal_urls['preferences'] = array( - 'text' => wfMsg('preferences'), - 'href' => $this->makeSpecialUrl('Preferences'), + 'text' => wfMsg( 'mypreferences' ), + 'href' => self::makeSpecialUrl( 'Preferences' ), 'active' => ( $href == $pageurl ) ); - $href = $this->makeSpecialUrl('Watchlist'); + $href = self::makeSpecialUrl( 'Watchlist' ); $personal_urls['watchlist'] = array( - 'text' => wfMsg('watchlist'), + 'text' => wfMsg( 'watchlist' ), 'href' => $href, 'active' => ( $href == $pageurl ) ); - $href = $this->makeSpecialUrl("Contributions/$this->username"); + $href = self::makeSpecialUrl( "Contributions/$this->username" ); $personal_urls['mycontris'] = array( - 'text' => wfMsg('mycontris'), + 'text' => wfMsg( 'mycontris' ), 'href' => $href # FIXME # 'active' => ( $href == $pageurl . '/' . $this->username ) ); $personal_urls['logout'] = array( - 'text' => wfMsg('userlogout'), - 'href' => $this->makeSpecialUrl( 'Userlogout', + 'text' => wfMsg( 'userlogout' ), + 'href' => self::makeSpecialUrl( 'Userlogout', $wgTitle->getNamespace() === NS_SPECIAL && $wgTitle->getText() === 'Preferences' ? '' : "returnto={$this->thisurl}" ) ); @@ -517,14 +533,14 @@ class SkinTemplate extends Skin { ); $personal_urls['anonlogin'] = array( 'text' => wfMsg('userlogin'), - 'href' => $this->makeSpecialUrl('Userlogin', 'returnto=' . $this->thisurl ), + 'href' => self::makeSpecialUrl( 'Userlogin', 'returnto=' . $this->thisurl ), 'active' => ( NS_SPECIAL == $wgTitle->getNamespace() && 'Userlogin' == $wgTitle->getDBkey() ) ); } else { $personal_urls['login'] = array( 'text' => wfMsg('userlogin'), - 'href' => $this->makeSpecialUrl('Userlogin', 'returnto=' . $this->thisurl ), + 'href' => self::makeSpecialUrl( 'Userlogin', 'returnto=' . $this->thisurl ), 'active' => ( NS_SPECIAL == $wgTitle->getNamespace() && 'Userlogin' == $wgTitle->getDBkey() ) ); } @@ -554,9 +570,9 @@ class SkinTemplate extends Skin { } $text = wfMsg( $message ); - if ( $text == "<$message>" ) { + if ( wfEmptyMsg( $message, $text ) ) { global $wgContLang; - $text = $wgContLang->getNsText( Namespace::getSubject( $title->getNamespace() ) ); + $text = $wgContLang->getFormattedNsText( Namespace::getSubject( $title->getNamespace() ) ); } return array( @@ -565,23 +581,23 @@ class SkinTemplate extends Skin { 'href' => $title->getLocalUrl( $query ) ); } - function makeTalkUrlDetails( $name, $urlaction='' ) { + function makeTalkUrlDetails( $name, $urlaction = '' ) { $title = Title::newFromText( $name ); $title = $title->getTalkPage(); - $this->checkTitle($title, $name); + self::checkTitle( $title, $name ); return array( 'href' => $title->getLocalURL( $urlaction ), - 'exists' => $title->getArticleID() != 0?true:false + 'exists' => $title->getArticleID() != 0 ? true : false ); } - function makeArticleUrlDetails( $name, $urlaction='' ) { + function makeArticleUrlDetails( $name, $urlaction = '' ) { $title = Title::newFromText( $name ); $title= $title->getSubjectPage(); - $this->checkTitle($title, $name); + self::checkTitle( $title, $name ); return array( 'href' => $title->getLocalURL( $urlaction ), - 'exists' => $title->getArticleID() != 0?true:false + 'exists' => $title->getArticleID() != 0 ? true : false ); } @@ -696,7 +712,7 @@ class SkinTemplate extends Skin { 'class' => false, 'text' => wfMsgExt( 'undelete_short', array( 'parsemag' ), $n ), 'href' => $undelTitle->getLocalUrl( 'target=' . urlencode( $this->thispage ) ) - #'href' => $this->makeSpecialUrl("Undelete/$this->thispage") + #'href' => self::makeSpecialUrl( "Undelete/$this->thispage" ) ); } } @@ -782,26 +798,26 @@ class SkinTemplate extends Skin { $diff = $wgRequest->getVal( 'diff' ); $nav_urls = array(); - $nav_urls['mainpage'] = array('href' => $this->makeI18nUrl('mainpage')); + $nav_urls['mainpage'] = array( 'href' => self::makeI18nUrl( 'mainpage') ); if( $wgEnableUploads ) { if ($wgUploadNavigationUrl) { - $nav_urls['upload'] = array('href' => $wgUploadNavigationUrl ); + $nav_urls['upload'] = array( 'href' => $wgUploadNavigationUrl ); } else { - $nav_urls['upload'] = array('href' => $this->makeSpecialUrl('Upload')); + $nav_urls['upload'] = array( 'href' => self::makeSpecialUrl( 'Upload' ) ); } } else { if ($wgUploadNavigationUrl) - $nav_urls['upload'] = array('href' => $wgUploadNavigationUrl ); + $nav_urls['upload'] = array( 'href' => $wgUploadNavigationUrl ); else $nav_urls['upload'] = false; } - $nav_urls['specialpages'] = array('href' => $this->makeSpecialUrl('Specialpages')); + $nav_urls['specialpages'] = array( 'href' => self::makeSpecialUrl( 'Specialpages' ) ); // A print stylesheet is attached to all pages, but nobody ever // figures that out. :) Add a link... if( $this->iscontent && ($action == '' || $action == 'view' || $action == 'purge' ) ) { - $revid = $wgArticle->getLatest(); + $revid = $wgArticle ? $wgArticle->getLatest() : 0; if ( !( $revid == 0 ) ) $nav_urls['print'] = array( 'text' => wfMsg( 'printableversion' ), @@ -852,11 +868,11 @@ class SkinTemplate extends Skin { if($id || $ip) { # both anons and non-anons have contri list $nav_urls['contributions'] = array( - 'href' => $this->makeSpecialUrl('Contributions/' . $this->mTitle->getText() ) + 'href' => self::makeSpecialUrl( 'Contributions/' . $this->mTitle->getText() ) ); if ( $wgUser->isAllowed( 'block' ) ) $nav_urls['blockip'] = array( - 'href' => $this->makeSpecialUrl( 'Blockip/' . $this->mTitle->getText() ) + 'href' => self::makeSpecialUrl( 'Blockip/' . $this->mTitle->getText() ) ); } else { $nav_urls['contributions'] = false; @@ -864,7 +880,7 @@ class SkinTemplate extends Skin { $nav_urls['emailuser'] = false; if( $this->showEmailUser( $id ) ) { $nav_urls['emailuser'] = array( - 'href' => $this->makeSpecialUrl('Emailuser/' . $this->mTitle->getText() ) + 'href' => self::makeSpecialUrl( 'Emailuser/' . $this->mTitle->getText() ) ); } wfProfileOut( $fname ); @@ -892,6 +908,11 @@ class SkinTemplate extends Skin { $sitecss = ''; $usercss = ''; $siteargs = '&maxage=' . $wgSquidMaxage; + if( $this->loggedin ) { + // Ensure that logged-in users' generated CSS isn't clobbered + // by anons' publicly cacheable generated CSS. + $siteargs .= '&smaxage=0'; + } # Add user-specific code if this is a user and we allow that kind of thing @@ -904,7 +925,7 @@ class SkinTemplate extends Skin { $usercss = $wgRequest->getText('wpTextbox1'); } else { $usercss = '@import "' . - $this->makeUrl($this->userpage . '/'.$this->skinname.'.css', + self::makeUrl($this->userpage . '/'.$this->skinname.'.css', 'action=raw&ctype=text/css') . '";' ."\n"; } @@ -915,10 +936,10 @@ class SkinTemplate extends Skin { # If we use the site's dynamic CSS, throw that in, too if ( $wgUseSiteCss ) { - $query = "action=raw&ctype=text/css&smaxage=$wgSquidMaxage"; - $sitecss .= '@import "' . $this->makeNSUrl('Common.css', $query, NS_MEDIAWIKI) . '";' . "\n"; - $sitecss .= '@import "' . $this->makeNSUrl(ucfirst($this->skinname) . '.css', $query, NS_MEDIAWIKI) . '";' . "\n"; - $sitecss .= '@import "' . $this->makeUrl('-','action=raw&gen=css' . $siteargs) . '";' . "\n"; + $query = "usemsgcache=yes&action=raw&ctype=text/css&smaxage=$wgSquidMaxage"; + $sitecss .= '@import "' . self::makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI) . '";' . "\n"; + $sitecss .= '@import "' . self::makeNSUrl( ucfirst( $this->skinname ) . '.css', $query, NS_MEDIAWIKI ) . '";' . "\n"; + $sitecss .= '@import "' . self::makeUrl( '-', 'action=raw&gen=css' . $siteargs ) . '";' . "\n"; } # If we use any dynamic CSS, make a little CDATA block out of it. @@ -944,7 +965,7 @@ class SkinTemplate extends Skin { # XXX: additional security check/prompt? $this->userjsprev = '/*<![CDATA[*/ ' . $wgRequest->getText('wpTextbox1') . ' /*]]>*/'; } else { - $this->userjs = $this->makeUrl($this->userpage.'/'.$this->skinname.'.js', 'action=raw&ctype='.$wgJsMimeType.'&dontcountme=s'); + $this->userjs = self::makeUrl($this->userpage.'/'.$this->skinname.'.js', 'action=raw&ctype='.$wgJsMimeType.'&dontcountme=s'); } } wfProfileOut( $fname ); @@ -996,8 +1017,8 @@ class SkinTemplate extends Skin { // avoid inclusion of non defined user JavaScript (with custom skins only) // by checking for default message content $msgKey = ucfirst($this->skinname).'.js'; - $userJS = wfMsg($msgKey); - if ('<'.$msgKey.'>' != $userJS) { + $userJS = wfMsgForContent($msgKey); + if ( !wfEmptyMsg( $msgKey, $userJS ) ) { $s .= $userJS; } @@ -1060,6 +1081,13 @@ class QuickTemplate { /** * @private */ + function jstext( $str ) { + echo Xml::escapeJsString( $this->data[$str] ); + } + + /** + * @private + */ function html( $str ) { echo $this->data[$str]; } @@ -1087,7 +1115,7 @@ class QuickTemplate { $text = $this->translator->translate( $str ); $parserOutput = $wgParser->parse( $text, $wgTitle, - $wgOut->mParserOptions, true ); + $wgOut->parserOptions(), true ); echo $parserOutput->getText(); } diff --git a/includes/SpecialAllmessages.php b/includes/SpecialAllmessages.php index 60258f9e..6e3f6588 100644 --- a/includes/SpecialAllmessages.php +++ b/includes/SpecialAllmessages.php @@ -9,7 +9,7 @@ * */ function wfSpecialAllmessages() { - global $wgOut, $wgAllMessagesEn, $wgRequest, $wgMessageCache, $wgTitle; + global $wgOut, $wgRequest, $wgMessageCache, $wgTitle; global $wgUseDatabaseMessages; # The page isn't much use if the MediaWiki namespace is not being used @@ -27,16 +27,16 @@ function wfSpecialAllmessages() { $navText = wfMsg( 'allmessagestext' ); # Make sure all extension messages are available - wfLoadAllExtensions(); + MessageCache::loadAllMessages(); $first = true; - $sortedArray = array_merge( $wgAllMessagesEn, $wgMessageCache->mExtensionMessages ); + $sortedArray = array_merge( Language::getMessagesFor( 'en' ), $wgMessageCache->getExtensionMessagesFor( 'en' ) ); ksort( $sortedArray ); $messages = array(); $wgMessageCache->disableTransform(); foreach ( $sortedArray as $key => $value ) { - $messages[$key]['enmsg'] = is_array( $value ) ? $value['en'] : $value; + $messages[$key]['enmsg'] = $value; $messages[$key]['statmsg'] = wfMsgNoDb( $key ); $messages[$key]['msg'] = wfMsg ( $key ); } @@ -62,10 +62,10 @@ function wfSpecialAllmessages() { * */ function makePhp($messages) { - global $wgLanguageCode; - $txt = "\n\n".'$wgAllMessages'.ucfirst($wgLanguageCode).' = array('."\n"; + global $wgLang; + $txt = "\n\n\$messages = array(\n"; foreach( $messages as $key => $m ) { - if(strtolower($wgLanguageCode) != 'en' and $m['msg'] == $m['enmsg'] ) { + if($wgLang->getCode() != 'en' and $m['msg'] == $m['enmsg'] ) { //if (strstr($m['msg'],"\n")) { // $txt.='/* '; // $comment=' */'; @@ -74,7 +74,7 @@ function makePhp($messages) { // $comment = ''; //} continue; - } elseif ($m['msg'] == '<'.$key.'>'){ + } elseif ( wfEmptyMsg( $key, $m['msg'] ) ) { $m['msg'] = ''; $comment = ' #empty'; } else { @@ -90,7 +90,7 @@ function makePhp($messages) { * */ function makeHTMLText( $messages ) { - global $wgLang, $wgUser, $wgLanguageCode, $wgContLanguageCode; + global $wgLang, $wgContLang, $wgUser; $fname = "makeHTMLText"; wfProfileIn( $fname ); @@ -148,8 +148,8 @@ function makeHTMLText( $messages ) { foreach( $messages as $key => $m ) { $title = $wgLang->ucfirst( $key ); - if($wgLanguageCode != $wgContLanguageCode) - $title.="/$wgLanguageCode"; + if($wgLang->getCode() != $wgContLang->getCode()) + $title.= '/' . $wgLang->getCode(); $titleObj =& Title::makeTitle( NS_MEDIAWIKI, $title ); $talkPage =& Title::makeTitle( NS_MEDIAWIKI_TALK, $title ); diff --git a/includes/SpecialAllpages.php b/includes/SpecialAllpages.php index 53a5b348..345c48e6 100644 --- a/includes/SpecialAllpages.php +++ b/includes/SpecialAllpages.php @@ -91,15 +91,11 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) { # in the querycache table. $dbr =& wfGetDB( DB_SLAVE ); - $page = $dbr->tableName( 'page' ); - $fromwhere = "FROM $page WHERE page_namespace=$namespace"; - $order_arr = array ( 'ORDER BY' => 'page_title' ); - $order_str = 'ORDER BY page_title'; $out = ""; $where = array( 'page_namespace' => $namespace ); - global $wgMemc, $wgDBname; - $key = "$wgDBname:allpages:ns:$namespace"; + global $wgMemc; + $key = wfMemcKey( 'allpages', 'ns', $namespace ); $lines = $wgMemc->get( $key ); if( !is_array( $lines ) ) { @@ -280,11 +276,9 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) { $sk->makeKnownLink( $wgContLang->specialPage( "Allpages" ), wfMsgHtml ( 'allpages' ) ); if ( isset($dbr) && $dbr && ($n == $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) { - $namespaceparam = $namespace ? "&namespace=$namespace" : ""; - $out2 .= " | " . $sk->makeKnownLink( - $wgContLang->specialPage( "Allpages" ), - wfMsgHtml ( 'nextpage', $s->page_title ), - "from=" . wfUrlEncode ( $s->page_title ) . $namespaceparam ); + $self = Title::makeTitle( NS_SPECIAL, 'Allpages' ); + $q = 'from=' . $t->getPartialUrl() . ( $namespace ? '&namespace=' . $namespace : '' ); + $out2 .= ' | ' . $sk->makeKnownLinkObj( $self, wfMsgHtml( 'nextpage', $t->getText() ), $q ); } $out2 .= "</td></tr></table><hr />"; } diff --git a/includes/SpecialBlockip.php b/includes/SpecialBlockip.php index b3f67ab1..4eb4957a 100644 --- a/includes/SpecialBlockip.php +++ b/includes/SpecialBlockip.php @@ -46,6 +46,15 @@ class IPBlockForm { $this->BlockReason = $wgRequest->getText( 'wpBlockReason' ); $this->BlockExpiry = $wgRequest->getVal( 'wpBlockExpiry', wfMsg('ipbotheroption') ); $this->BlockOther = $wgRequest->getVal( 'wpBlockOther', '' ); + $this->BlockAnonOnly = $wgRequest->getBool( 'wpAnonOnly' ); + + # Unchecked checkboxes are not included in the form data at all, so having one + # that is true by default is a bit tricky + if ( $wgRequest->wasPosted() ) { + $this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', false ); + } else { + $this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', true ); + } } function showForm( $err ) { @@ -102,7 +111,7 @@ class IPBlockForm { <tr> <td align=\"right\">{$mIpaddress}:</td> <td align=\"left\"> - <input tabindex='1' type='text' size='20' name=\"wpBlockAddress\" value=\"{$scBlockAddress}\" /> + <input tabindex='1' type='text' size='40' name=\"wpBlockAddress\" value=\"{$scBlockAddress}\" /> </td> </tr> <tr>"); @@ -133,13 +142,36 @@ class IPBlockForm { <tr> <td> </td> <td align=\"left\"> - <input tabindex='4' type='submit' name=\"wpBlock\" value=\"{$mIpbsubmit}\" /> + " . wfCheckLabel( wfMsg( 'ipbanononly' ), + 'wpAnonOnly', 'wpAnonOnly', $this->BlockAnonOnly, + array( 'tabindex' => 4 ) ) . " + </td> + </tr> + <tr> + <td> </td> + <td align=\"left\"> + " . wfCheckLabel( wfMsg( 'ipbcreateaccount' ), + 'wpCreateAccount', 'wpCreateAccount', $this->BlockCreateAccount, + array( 'tabindex' => 5 ) ) . " + </td> + </tr> + <tr> + <td style='padding-top: 1em'> </td> + <td style='padding-top: 1em' align=\"left\"> + <input tabindex='5' type='submit' name=\"wpBlock\" value=\"{$mIpbsubmit}\" /> </td> </tr> </table> <input type='hidden' name='wpEditToken' value=\"{$token}\" /> </form>\n" ); + $user = User::newFromName( $this->BlockAddress ); + if( is_object( $user ) ) { + $this->showLogFragment( $wgOut, $user->getUserPage() ); + } elseif( preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', $this->BlockAddress ) ) { + $this->showLogFragment( $wgOut, Title::makeTitle( NS_USER, $this->BlockAddress ) ); + } + } function doSubmit() { @@ -166,8 +198,12 @@ class IPBlockForm { } else { # Username block if ( $wgSysopUserBans ) { - $userId = User::idFromName( $this->BlockAddress ); - if ( $userId == 0 ) { + $user = User::newFromName( $this->BlockAddress ); + if( !is_null( $user ) && $user->getID() ) { + # Use canonical name + $this->BlockAddress = $user->getName(); + $userId = $user->getID(); + } else { $this->showForm( wfMsg( 'nosuchusershort', htmlspecialchars( $this->BlockAddress ) ) ); return; } @@ -188,7 +224,7 @@ class IPBlockForm { } if ( $expirestr == 'infinite' || $expirestr == 'indefinite' ) { - $expiry = ''; + $expiry = Block::infinity(); } else { # Convert GNU-style date, on error returns -1 for PHP <5.1 and false for PHP >=5.1 $expiry = strtotime( $expirestr ); @@ -199,20 +235,24 @@ class IPBlockForm { } $expiry = wfTimestamp( TS_MW, $expiry ); - } # Create block # Note: for a user block, ipb_address is only for display purposes - $ban = new Block( $this->BlockAddress, $userId, $wgUser->getID(), - $this->BlockReason, wfTimestampNow(), 0, $expiry ); + $block = new Block( $this->BlockAddress, $userId, $wgUser->getID(), + $this->BlockReason, wfTimestampNow(), 0, $expiry, $this->BlockAnonOnly, + $this->BlockCreateAccount ); - if (wfRunHooks('BlockIp', array(&$ban, &$wgUser))) { + if (wfRunHooks('BlockIp', array(&$block, &$wgUser))) { - $ban->insert(); + if ( !$block->insert() ) { + $this->showForm( wfMsg( 'ipb_already_blocked', + htmlspecialchars( $this->BlockAddress ) ) ); + return; + } - wfRunHooks('BlockIpComplete', array($ban, $wgUser)); + wfRunHooks('BlockIpComplete', array($block, $wgUser)); # Make log entry $log = new LogPage( 'block' ); @@ -234,6 +274,14 @@ class IPBlockForm { $text = wfMsg( 'blockipsuccesstext', $this->BlockAddress ); $wgOut->addWikiText( $text ); } + + function showLogFragment( &$out, &$title ) { + $out->addHtml( wfElement( 'h2', NULL, LogPage::logName( 'block' ) ) ); + $request = new FauxRequest( array( 'page' => $title->getPrefixedText(), 'type' => 'block' ) ); + $viewer = new LogViewer( new LogReader( $request ) ); + $viewer->showList( $out ); + } + } ?> diff --git a/includes/SpecialBrokenRedirects.php b/includes/SpecialBrokenRedirects.php index e5c2dd8e..653e13e2 100644 --- a/includes/SpecialBrokenRedirects.php +++ b/includes/SpecialBrokenRedirects.php @@ -68,7 +68,7 @@ class BrokenRedirectsPage extends PageQueryPage { $from = $skin->makeKnownLinkObj( $fromObj ,'', 'redirect=no' ); $edit = $skin->makeBrokenLinkObj( $fromObj , "(".wfMsg("qbedit").")" , 'redirect=no'); $to = $skin->makeBrokenLinkObj( $toObj ); - $arr = $wgContLang->isRTL() ? '←' : '→'; + $arr = $wgContLang->getArrow(); return "$from $edit $arr $to"; } diff --git a/includes/SpecialCategories.php b/includes/SpecialCategories.php index 8a6dd5ff..89cff20a 100644 --- a/includes/SpecialCategories.php +++ b/includes/SpecialCategories.php @@ -36,7 +36,7 @@ class CategoriesPage extends QueryPage { 1 as value, COUNT(*) as count FROM $categorylinks - GROUP BY cl_to"; + GROUP BY 1,2,3,4"; return $s; } diff --git a/includes/SpecialConfirmemail.php b/includes/SpecialConfirmemail.php index fd0425a8..72567609 100644 --- a/includes/SpecialConfirmemail.php +++ b/includes/SpecialConfirmemail.php @@ -30,7 +30,11 @@ class EmailConfirmation extends SpecialPage { global $wgUser, $wgOut; if( empty( $code ) ) { if( $wgUser->isLoggedIn() ) { - $this->showRequestForm(); + if( User::isValidEmailAddr( $wgUser->getEmail() ) ) { + $this->showRequestForm(); + } else { + $wgOut->addWikiText( wfMsg( 'confirmemail_noemail' ) ); + } } else { $title = Title::makeTitle( NS_SPECIAL, 'Userlogin' ); $self = Title::makeTitle( NS_SPECIAL, 'Confirmemail' ); diff --git a/includes/SpecialDeadendpages.php b/includes/SpecialDeadendpages.php index 3f4a0519..b319a170 100644 --- a/includes/SpecialDeadendpages.php +++ b/includes/SpecialDeadendpages.php @@ -16,6 +16,10 @@ class DeadendPagesPage extends PageQueryPage { return "Deadendpages"; } + function getPageHeader() { + return '<p>' . wfMsg('deadendpagestext') . '</p>'; + } + /** * LEFT JOIN is expensive * diff --git a/includes/SpecialDisambiguations.php b/includes/SpecialDisambiguations.php index 1a0297af..0355c85b 100644 --- a/includes/SpecialDisambiguations.php +++ b/includes/SpecialDisambiguations.php @@ -19,37 +19,69 @@ class DisambiguationsPage extends PageQueryPage { function isExpensive( ) { return true; } function isSyndicated() { return false; } + function getDisambiguationPageObj() { + return Title::makeTitleSafe( NS_MEDIAWIKI, 'disambiguationspage'); + } + function getPageHeader( ) { global $wgUser; $sk = $wgUser->getSkin(); - #FIXME : probably need to add a backlink to the maintenance page. - return '<p>'.wfMsg('disambiguationstext', $sk->makeKnownLink(wfMsgForContent('disambiguationspage')) )."</p><br />\n"; + return '<p>'.wfMsg('disambiguationstext', $sk->makeKnownLinkObj($this->getDisambiguationPageObj()))."</p><br />\n"; } function getSQL() { $dbr =& wfGetDB( DB_SLAVE ); extract( $dbr->tableNames( 'page', 'pagelinks', 'templatelinks' ) ); - $dp = Title::newFromText(wfMsgForContent('disambiguationspage')); - $id = $dp->getArticleId(); - $dns = $dp->getNamespace(); - $dtitle = $dbr->addQuotes( $dp->getDBkey() ); - - if($dns != NS_TEMPLATE) { - # FIXME we assume the disambiguation message is a template but - # the page can potentially be from another namespace :/ - wfDebug("Mediawiki:disambiguationspage message does not refer to a template!\n"); - } - - $sql = "SELECT 'Disambiguations' AS \"type\", pa.page_namespace AS namespace," - ." pa.page_title AS title, la.pl_from AS value" - ." FROM {$templatelinks} AS lb, {$page} AS pa, {$pagelinks} AS la" - ." WHERE lb.tl_namespace = $dns AND lb.tl_title = $dtitle" # disambiguation template - .' AND pa.page_id = lb.tl_from' - .' AND pa.page_namespace = la.pl_namespace' - .' AND pa.page_title = la.pl_title'; - return $sql; + $dMsgText = wfMsgForContent('disambiguationspage'); + + $linkBatch = new LinkBatch; + + # If the text can be treated as a title, use it verbatim. + # Otherwise, pull the titles from the links table + $dp = Title::newFromText($dMsgText); + if( $dp ) { + if($dp->getNamespace() != NS_TEMPLATE) { + # FIXME we assume the disambiguation message is a template but + # the page can potentially be from another namespace :/ + wfDebug("Mediawiki:disambiguationspage message does not refer to a template!\n"); + } + $linkBatch->addObj( $dp ); + } else { + # Get all the templates linked from the Mediawiki:Disambiguationspage + $disPageObj = $this->getDisambiguationPageObj(); + $res = $dbr->select( + array('pagelinks', 'page'), + 'pl_title', + array('page_id = pl_from', 'pl_namespace' => NS_TEMPLATE, + 'page_namespace' => $disPageObj->getNamespace(), 'page_title' => $disPageObj->getDBkey()), + 'DisambiguationsPage::getSQL' ); + + while ( $row = $dbr->fetchObject( $res ) ) { + $linkBatch->addObj( Title::makeTitle( NS_TEMPLATE, $row->pl_title )); + } + $dbr->freeResult( $res ); + } + + $set = $linkBatch->constructSet( 'lb.tl', $dbr ); + if( $set === false ) { + $set = 'FALSE'; # We must always return a valid sql query, but this way DB will always quicly return an empty result + wfDebug("Mediawiki:disambiguationspage message does not link to any templates!\n"); + } + + $sql = "SELECT 'Disambiguations' AS \"type\", pb.page_namespace AS namespace," + ." pb.page_title AS title, la.pl_from AS value" + ." FROM {$templatelinks} AS lb, {$page} AS pb, {$pagelinks} AS la, {$page} AS pa" + ." WHERE $set" # disambiguation template(s) + .' AND pa.page_id = la.pl_from' + .' AND pa.page_namespace = ' . NS_MAIN # Limit to just articles in the main namespace + .' AND pb.page_id = lb.tl_from' + .' AND pb.page_namespace = la.pl_namespace' + .' AND pb.page_title = la.pl_title' + .' ORDER BY lb.tl_namespace, lb.tl_title'; + + return $sql; } function getOrder() { @@ -57,14 +89,16 @@ class DisambiguationsPage extends PageQueryPage { } function formatResult( $skin, $result ) { + global $wgContLang; $title = Title::newFromId( $result->value ); $dp = Title::makeTitle( $result->namespace, $result->title ); $from = $skin->makeKnownLinkObj( $title,''); $edit = $skin->makeBrokenLinkObj( $title, "(".wfMsg("qbedit").")" , 'redirect=no'); + $arr = $wgContLang->getArrow(); $to = $skin->makeKnownLinkObj( $dp,''); - return "$from $edit => $to"; + return "$from $edit $arr $to"; } } diff --git a/includes/SpecialDoubleRedirects.php b/includes/SpecialDoubleRedirects.php index fe480f60..fe42b00a 100644 --- a/includes/SpecialDoubleRedirects.php +++ b/includes/SpecialDoubleRedirects.php @@ -87,7 +87,7 @@ class DoubleRedirectsPage extends PageQueryPage { $edit = $skin->makeBrokenLinkObj( $titleA, "(".wfMsg("qbedit").")" , 'redirect=no'); $linkB = $skin->makeKnownLinkObj( $titleB, '', 'redirect=no' ); $linkC = $skin->makeKnownLinkObj( $titleC ); - $arr = $wgContLang->isRTL() ? '←' : '→'; + $arr = $wgContLang->getArrow() . $wgContLang->getDirMark(); return( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" ); } diff --git a/includes/SpecialEmailuser.php b/includes/SpecialEmailuser.php index c66389e1..d711947f 100644 --- a/includes/SpecialEmailuser.php +++ b/includes/SpecialEmailuser.php @@ -49,7 +49,7 @@ function wfSpecialEmailuser( $par ) { $f = new EmailUserForm( $nu ); if ( "success" == $action ) { - $f->showSuccess(); + $f->showSuccess( $nu ); } else if ( "submit" == $action && $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) { $f->doSubmit(); @@ -148,13 +148,13 @@ class EmailUserForm { } } - function showSuccess() { + function showSuccess( &$user ) { global $wgOut; $wgOut->setPagetitle( wfMsg( "emailsent" ) ); $wgOut->addHTML( wfMsg( "emailsenttext" ) ); - $wgOut->returnToMain( false ); + $wgOut->returnToMain( false, $user->getUserPage() ); } } ?> diff --git a/includes/SpecialExport.php b/includes/SpecialExport.php index 73dcbcd5..dc52e00b 100644 --- a/includes/SpecialExport.php +++ b/includes/SpecialExport.php @@ -22,9 +22,6 @@ * @subpackage SpecialPage */ -/** */ -require_once( 'Export.php' ); - /** * */ @@ -33,16 +30,54 @@ function wfSpecialExport( $page = '' ) { global $wgExportAllowHistory, $wgExportMaxHistory; $curonly = true; - if( $wgRequest->getVal( 'action' ) == 'submit') { + $fullHistory = array( + 'dir' => 'asc', + 'offset' => false, + 'limit' => $wgExportMaxHistory, + ); + if( $wgRequest->wasPosted() ) { $page = $wgRequest->getText( 'pages' ); $curonly = $wgRequest->getCheck( 'curonly' ); - } - if( $wgRequest->getCheck( 'history' ) ) { - $curonly = false; + $rawOffset = $wgRequest->getVal( 'offset' ); + if( $rawOffset ) { + $offset = wfTimestamp( TS_MW, $rawOffset ); + } else { + $offset = null; + } + $limit = $wgRequest->getInt( 'limit' ); + $dir = $wgRequest->getVal( 'dir' ); + $history = array( + 'dir' => 'asc', + 'offset' => false, + 'limit' => $wgExportMaxHistory, + ); + $historyCheck = $wgRequest->getCheck( 'history' ); + if ( $curonly ) { + $history = WikiExporter::CURRENT; + } elseif ( !$historyCheck ) { + if ( $limit > 0 && $limit < $wgExportMaxHistory ) { + $history['limit'] = $limit; + } + if ( !is_null( $offset ) ) { + $history['offset'] = $offset; + } + if ( strtolower( $dir ) == 'desc' ) { + $history['dir'] = 'desc'; + } + } + } else { + // Default to current-only for GET requests + $page = $wgRequest->getText( 'pages', $page ); + $historyCheck = $wgRequest->getCheck( 'history' ); + if( $historyCheck ) { + $history = WikiExporter::FULL; + } else { + $history = WikiExporter::CURRENT; + } } if( !$wgExportAllowHistory ) { // Override - $curonly = true; + $history = WikiExporter::CURRENT; } $list_authors = $wgRequest->getCheck( 'listauthors' ); @@ -63,12 +98,12 @@ function wfSpecialExport( $page = '' ) { $pages = explode( "\n", $page ); $db =& wfGetDB( DB_SLAVE ); - $history = $curonly ? MW_EXPORT_CURRENT : MW_EXPORT_FULL; $exporter = new WikiExporter( $db, $history ); $exporter->list_authors = $list_authors ; $exporter->openStream(); foreach( $pages as $page ) { + /* if( $wgExportMaxHistory && !$curonly ) { $title = Title::newFromText( $page ); if( $title ) { @@ -79,7 +114,7 @@ function wfSpecialExport( $page = '' ) { continue; } } - } + }*/ $exporter->pageByName( $page ); } diff --git a/includes/SpecialImagelist.php b/includes/SpecialImagelist.php index e456abf5..54ee83e5 100644 --- a/includes/SpecialImagelist.php +++ b/includes/SpecialImagelist.php @@ -11,111 +11,159 @@ function wfSpecialImagelist() { global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest, $wgMiserMode; - $sort = $wgRequest->getVal( 'sort' ); - $wpIlMatch = $wgRequest->getText( 'wpIlMatch' ); - $dbr =& wfGetDB( DB_SLAVE ); - $image = $dbr->tableName( 'image' ); - $sql = "SELECT img_size,img_name,img_user,img_user_text," . - "img_description,img_timestamp FROM $image"; - - if ( !$wgMiserMode && !empty( $wpIlMatch ) ) { - $nt = Title::newFromUrl( $wpIlMatch ); - if($nt ) { - $m = $dbr->strencode( strtolower( $nt->getDBkey() ) ); - $m = str_replace( "%", "\\%", $m ); - $m = str_replace( "_", "\\_", $m ); - $sql .= " WHERE LCASE(img_name) LIKE '%{$m}%'"; + $pager = new ImageListPager; + + $limit = $pager->getForm(); + $body = $pager->getBody(); + $nav = $pager->getNavigationBar(); + $wgOut->addHTML( " + $limit + <br/> + $body + $nav" ); +} + +class ImageListPager extends TablePager { + var $mFieldNames = null; + var $mMessages = array(); + var $mQueryConds = array(); + + function __construct() { + global $wgRequest, $wgMiserMode; + if ( $wgRequest->getText( 'sort', 'img_date' ) == 'img_date' ) { + $this->mDefaultDirection = true; + } else { + $this->mDefaultDirection = false; + } + $search = $wgRequest->getText( 'ilsearch' ); + if ( $search != '' && !$wgMiserMode ) { + $nt = Title::newFromUrl( $search ); + if( $nt ) { + $dbr =& wfGetDB( DB_SLAVE ); + $m = $dbr->strencode( strtolower( $nt->getDBkey() ) ); + $m = str_replace( "%", "\\%", $m ); + $m = str_replace( "_", "\\_", $m ); + $this->mQueryConds = array( "LCASE(img_name) LIKE '%{$m}%'" ); + } } - } - if ( "bysize" == $sort ) { - $sql .= " ORDER BY img_size DESC"; - } else if ( "byname" == $sort ) { - $sql .= " ORDER BY img_name"; - } else { - $sort = "bydate"; - $sql .= " ORDER BY img_timestamp DESC"; + parent::__construct(); } - list( $limit, $offset ) = wfCheckLimits( 50 ); - $lt = $wgLang->formatNum( "${limit}" ); - $sql .= " LIMIT {$limit}"; - - $wgOut->addWikiText( wfMsg( 'imglegend' ) ); - $wgOut->addHTML( wfMsgExt( 'imagelisttext', array('parse'), $lt, wfMsg( $sort ) ) ); - - $sk = $wgUser->getSkin(); - $titleObj = Title::makeTitle( NS_SPECIAL, "Imagelist" ); - $action = $titleObj->escapeLocalURL( "sort={$sort}&limit={$limit}" ); - - if ( !$wgMiserMode ) { - $wgOut->addHTML( "<form id=\"imagesearch\" method=\"post\" action=\"" . - "{$action}\">" . - wfElement( 'input', - array( - 'type' => 'text', - 'size' => '20', - 'name' => 'wpIlMatch', - 'value' => $wpIlMatch, )) . - wfElement( 'input', - array( - 'type' => 'submit', - 'name' => 'wpIlSubmit', - 'value' => wfMsg( 'ilsubmit'), )) . - '</form>' ); + function getFieldNames() { + if ( !$this->mFieldNames ) { + $this->mFieldNames = array( + 'links' => '', + 'img_timestamp' => wfMsg( 'imagelist_date' ), + 'img_name' => wfMsg( 'imagelist_name' ), + 'img_user_text' => wfMsg( 'imagelist_user' ), + 'img_size' => wfMsg( 'imagelist_size' ), + 'img_description' => wfMsg( 'imagelist_description' ), + ); + } + return $this->mFieldNames; } - $here = Title::makeTitle( NS_SPECIAL, 'Imagelist' ); + function isFieldSortable( $field ) { + static $sortable = array( 'img_timestamp', 'img_name', 'img_size' ); + return in_array( $field, $sortable ); + } - foreach ( array( 'byname', 'bysize', 'bydate') as $sorttype ) { - $urls = null; - foreach ( array( 50, 100, 250, 500 ) as $num ) { - $urls[] = $sk->makeKnownLinkObj( $here, $wgLang->formatNum( $num ), - "sort={$sorttype}&limit={$num}&wpIlMatch=" . urlencode( $wpIlMatch ) ); - } - $sortlinks[] = wfMsgExt( - 'showlast', - array( 'parseinline', 'replaceafter' ), - implode($urls, ' | '), - wfMsgExt( $sorttype, array('escape') ) + function getQueryInfo() { + $fields = $this->getFieldNames(); + unset( $fields['links'] ); + $fields = array_keys( $fields ); + $fields[] = 'img_user'; + return array( + 'tables' => 'image', + 'fields' => $fields, + 'conds' => $this->mQueryConds ); } - $wgOut->addHTML( implode( $sortlinks, "<br />\n") . "\n\n<hr />" ); - // lines - $wgOut->addHTML( '<p>' ); - $res = $dbr->query( $sql, "wfSpecialImagelist" ); + function getDefaultSort() { + return 'img_timestamp'; + } - while ( $s = $dbr->fetchObject( $res ) ) { - $name = $s->img_name; - $ut = $s->img_user_text; - if ( 0 == $s->img_user ) { - $ul = $ut; - } else { - $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut ); + function getStartBody() { + # Do a link batch query for user pages + if ( $this->mResult->numRows() ) { + $lb = new LinkBatch; + $this->mResult->seek( 0 ); + while ( $row = $this->mResult->fetchObject() ) { + if ( $row->img_user ) { + $lb->add( NS_USER, str_replace( ' ', '_', $row->img_user_text ) ); + } + } + $lb->execute(); } - $dirmark = $wgContLang->getDirMark(); // to keep text in correct direction - - $ilink = "<a href=\"" . htmlspecialchars( Image::imageUrl( $name ) ) . - "\">" . strtr(htmlspecialchars( $name ), '_', ' ') . "</a>"; + # Cache messages used in each row + $this->mMessages['imgdesc'] = wfMsgHtml( 'imgdesc' ); + $this->mMessages['imgfile'] = wfMsgHtml( 'imgfile' ); + + return parent::getStartBody(); + } - $nb = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'), - $wgLang->formatNum( $s->img_size ) ); + function formatValue( $field, $value ) { + global $wgLang; + switch ( $field ) { + case 'links': + $name = $this->mCurrentRow->img_name; + $ilink = "<a href=\"" . htmlspecialchars( Image::imageUrl( $name ) ) . + "\">" . $this->mMessages['imgfile'] . "</a>"; + $desc = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ), + $this->mMessages['imgdesc'] ); + return "$desc | $ilink"; + case 'img_timestamp': + return $wgLang->timeanddate( $value, true ); + case 'img_name': + return htmlspecialchars( $value ); + case 'img_user_text': + if ( $this->mCurrentRow->img_user ) { + $link = $this->getSkin()->makeLinkObj( Title::makeTitle( NS_USER, $value ), + htmlspecialchars( $value ) ); + } else { + $link = htmlspecialchars( $value ); + } + return $link; + case 'img_size': + return $wgLang->formatNum( $value ); + case 'img_description': + return $this->getSkin()->commentBlock( $value ); + } + } - $desc = $sk->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ), - wfMsg( 'imgdesc' ) ); + function getForm() { + global $wgRequest, $wgMiserMode; + $url = $this->getTitle()->escapeLocalURL(); + $msgSubmit = wfMsgHtml( 'table_pager_limit_submit' ); + $msgSearch = wfMsgHtml( 'imagelist_search_for' ); + $search = $wgRequest->getText( 'ilsearch' ); + $encSearch = htmlspecialchars( $search ); + $s = "<form method=\"get\" action=\"$url\">\n" . + wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ); + if ( !$wgMiserMode ) { + $s .= "<br/>\n" . $msgSearch . + " <input type=\"text\" size=\"20\" name=\"ilsearch\" value=\"$encSearch\"/><br/>\n"; + } + $s .= " <input type=\"submit\" value=\"$msgSubmit\"/>\n" . + $this->getHiddenFields( array( 'limit', 'ilsearch' ) ) . + "</form>\n"; + return $s; + } - $date = $wgLang->timeanddate( $s->img_timestamp, true ); - $comment = $sk->commentBlock( $s->img_description ); + function getTableClass() { + return 'imagelist ' . parent::getTableClass(); + } - $l = "({$desc}) {$dirmark}{$ilink} . . {$dirmark}{$nb} . . {$dirmark}{$ul}". - " . . {$dirmark}{$date} . . {$dirmark}{$comment}<br />\n"; - $wgOut->addHTML( $l ); + function getNavClass() { + return 'imagelist_nav ' . parent::getNavClass(); } - $dbr->freeResult( $res ); - $wgOut->addHTML( '</p>' ); + function getSortHeaderClass() { + return 'imagelist_sort ' . parent::getSortHeaderClass(); + } } ?> diff --git a/includes/SpecialImport.php b/includes/SpecialImport.php index 7976d6c8..aaadb662 100644 --- a/includes/SpecialImport.php +++ b/includes/SpecialImport.php @@ -175,39 +175,41 @@ class ImportReporter { $wgOut->addHtml( "<ul>\n" ); } - function reportPage( $title, $origTitle, $revisionCount ) { + function reportPage( $title, $origTitle, $revisionCount, $successCount ) { global $wgOut, $wgUser, $wgLang, $wgContLang; $skin = $wgUser->getSkin(); $this->mPageCount++; - $localCount = $wgLang->formatNum( $revisionCount ); - $contentCount = $wgContLang->formatNum( $revisionCount ); + $localCount = $wgLang->formatNum( $successCount ); + $contentCount = $wgContLang->formatNum( $successCount ); $wgOut->addHtml( "<li>" . $skin->makeKnownLinkObj( $title ) . " " . - wfMsgHtml( 'import-revision-count', $localCount ) . + wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) . "</li>\n" ); - $log = new LogPage( 'import' ); - if( $this->mIsUpload ) { - $detail = wfMsgForContent( 'import-logentry-upload-detail', - $contentCount ); - $log->addEntry( 'upload', $title, $detail ); - } else { - $interwiki = '[[:' . $this->mInterwiki . ':' . - $origTitle->getPrefixedText() . ']]'; - $detail = wfMsgForContent( 'import-logentry-interwiki-detail', - $contentCount, $interwiki ); - $log->addEntry( 'interwiki', $title, $detail ); + if( $successCount > 0 ) { + $log = new LogPage( 'import' ); + if( $this->mIsUpload ) { + $detail = wfMsgForContent( 'import-logentry-upload-detail', + $contentCount ); + $log->addEntry( 'upload', $title, $detail ); + } else { + $interwiki = '[[:' . $this->mInterwiki . ':' . + $origTitle->getPrefixedText() . ']]'; + $detail = wfMsgForContent( 'import-logentry-interwiki-detail', + $contentCount, $interwiki ); + $log->addEntry( 'interwiki', $title, $detail ); + } + + $comment = $detail; // quick + $dbw = wfGetDB( DB_MASTER ); + $nullRevision = Revision::newNullRevision( + $dbw, $title->getArticleId(), $comment, true ); + $nullRevId = $nullRevision->insertOn( $dbw ); } - - $comment = $detail; // quick - $dbw = wfGetDB( DB_MASTER ); - $nullRevision = Revision::newNullRevision( - $dbw, $title->getArticleId(), $comment, true ); - $nullRevId = $nullRevision->insertOn( $dbw ); } function close() { @@ -238,7 +240,7 @@ class WikiRevision { if( is_object( $title ) ) { $this->title = $title; } elseif( is_null( $title ) ) { - throw new MWException( "WikiRevision given a null title in import." ); + throw new MWException( "WikiRevision given a null title in import. You may need to adjust \$wgLegalTitleChars." ); } else { throw new MWException( "WikiRevision given non-object title in import." ); } @@ -327,9 +329,17 @@ class WikiRevision { $created = true; } else { $created = false; - } - # FIXME: Check for exact conflicts + $prior = Revision::loadFromTimestamp( $dbw, $this->title, $this->timestamp ); + if( !is_null( $prior ) ) { + // FIXME: this could fail slightly for multiple matches :P + wfDebug( __METHOD__ . ": skipping existing revision for [[" . + $this->title->getPrefixedText() . "]], timestamp " . + $this->timestamp . "\n" ); + return false; + } + } + # FIXME: Use original rev_id optionally # FIXME: blah blah blah @@ -353,13 +363,14 @@ class WikiRevision { if( $created ) { wfDebug( __METHOD__ . ": running onArticleCreate\n" ); Article::onArticleCreate( $this->title ); - } else { - if( $changed ) { - wfDebug( __METHOD__ . ": running onArticleEdit\n" ); - Article::onArticleEdit( $this->title ); - } - } - if( $created || $changed ) { + + wfDebug( __METHOD__ . ": running create updates\n" ); + $article->createUpdates( $revision ); + + } elseif( $changed ) { + wfDebug( __METHOD__ . ": running onArticleEdit\n" ); + Article::onArticleEdit( $this->title ); + wfDebug( __METHOD__ . ": running edit updates\n" ); $article->editUpdates( $this->getText(), @@ -499,7 +510,7 @@ class WikiImporter { */ function importRevision( &$revision ) { $dbw =& wfGetDB( DB_MASTER ); - $dbw->deadlockLoop( array( &$revision, 'importOldRevision' ) ); + return $dbw->deadlockLoop( array( &$revision, 'importOldRevision' ) ); } /** @@ -536,12 +547,13 @@ class WikiImporter { * @param Title $title * @param Title $origTitle * @param int $revisionCount + * @param int $successCount number of revisions for which callback returned true * @private */ - function pageOutCallback( $title, $origTitle, $revisionCount ) { + function pageOutCallback( $title, $origTitle, $revisionCount, $successCount ) { if( is_callable( $this->mPageOutCallback ) ) { call_user_func( $this->mPageOutCallback, $title, $origTitle, - $revisionCount ); + $revisionCount, $successCount ); } } @@ -565,6 +577,7 @@ class WikiImporter { xml_set_element_handler( $parser, "in_siteinfo", "out_siteinfo" ); } elseif( $name == 'page' ) { $this->workRevisionCount = 0; + $this->workSuccessCount = 0; xml_set_element_handler( $parser, "in_page", "out_page" ); } else { return $this->throwXMLerror( "Expected <page>, got <$name>" ); @@ -633,11 +646,12 @@ class WikiImporter { xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" ); $this->pageOutCallback( $this->pageTitle, $this->origTitle, - $this->workRevisionCount ); + $this->workRevisionCount, $this->workSuccessCount ); $this->workTitle = null; $this->workRevision = null; $this->workRevisionCount = 0; + $this->workSuccessCount = 0; $this->pageTitle = null; $this->origTitle = null; } @@ -728,11 +742,10 @@ class WikiImporter { } xml_set_element_handler( $parser, "in_page", "out_page" ); - $out = call_user_func_array( $this->mRevisionCallback, + $ok = call_user_func_array( $this->mRevisionCallback, array( &$this->workRevision, &$this ) ); - if( !empty( $out ) ) { - global $wgOut; - $wgOut->addHTML( "<li>" . $out . "</li>\n" ); + if( $ok ) { + $this->workSuccessCount++; } } diff --git a/includes/SpecialIpblocklist.php b/includes/SpecialIpblocklist.php index cc5c805c..437fac7f 100644 --- a/includes/SpecialIpblocklist.php +++ b/includes/SpecialIpblocklist.php @@ -12,17 +12,19 @@ function wfSpecialIpblocklist() { global $wgUser, $wgOut, $wgRequest; $ip = $wgRequest->getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) ); + $id = $wgRequest->getVal( 'id' ); $reason = $wgRequest->getText( 'wpUnblockReason' ); $action = $wgRequest->getText( 'action' ); + $successip = $wgRequest->getVal( 'successip' ); - $ipu = new IPUnblockForm( $ip, $reason ); + $ipu = new IPUnblockForm( $ip, $id, $reason ); if ( "success" == $action ) { - $ipu->showList( wfMsgWikiHtml( 'unblocked', htmlspecialchars( $ip ) ) ); + $ipu->showList( $wgOut->parse( wfMsg( 'unblocked', $successip ) ) ); } else if ( "submit" == $action && $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) { if ( ! $wgUser->isAllowed('block') ) { - $wgOut->sysopRequired(); + $wgOut->permissionRequired( 'block' ); return; } $ipu->doSubmit(); @@ -39,10 +41,11 @@ function wfSpecialIpblocklist() { * @subpackage SpecialPage */ class IPUnblockForm { - var $ip, $reason; + var $ip, $reason, $id; - function IPUnblockForm( $ip, $reason ) { + function IPUnblockForm( $ip, $id, $reason ) { $this->ip = $ip; + $this->id = $id; $this->reason = $reason; } @@ -64,13 +67,27 @@ class IPUnblockForm { } $token = htmlspecialchars( $wgUser->editToken() ); + $addressPart = false; + if ( $this->id ) { + $block = Block::newFromID( $this->id ); + if ( $block ) { + $encName = htmlspecialchars( $block->getRedactedName() ); + $encId = htmlspecialchars( $this->id ); + $addressPart = $encName . "<input type='hidden' name=\"id\" value=\"$encId\" />"; + } + } + if ( !$addressPart ) { + $addressPart = "<input tabindex='1' type='text' size='20' " . + "name=\"wpUnblockAddress\" value=\"" . htmlspecialchars( $this->ip ) . "\" />"; + } + $wgOut->addHTML( " <form id=\"unblockip\" method=\"post\" action=\"{$action}\"> <table border='0'> <tr> <td align='right'>{$ipa}:</td> <td align='left'> - <input tabindex='1' type='text' size='20' name=\"wpUnblockAddress\" value=\"" . htmlspecialchars( $this->ip ) . "\" /> + {$addressPart} </td> </tr> <tr> @@ -94,27 +111,46 @@ class IPUnblockForm { function doSubmit() { global $wgOut; - $block = new Block(); - $this->ip = trim( $this->ip ); - - if ( $this->ip{0} == "#" ) { - $block->mId = substr( $this->ip, 1 ); + if ( $this->id ) { + $block = Block::newFromID( $this->id ); + if ( $block ) { + $this->ip = $block->getRedactedName(); + } } else { - $block->mAddress = $this->ip; + $block = new Block(); + $this->ip = trim( $this->ip ); + if ( substr( $this->ip, 0, 1 ) == "#" ) { + $id = substr( $this->ip, 1 ); + $block = Block::newFromID( $id ); + } else { + $block = Block::newFromDB( $this->ip ); + if ( !$block ) { + $block = null; + } + } + } + $success = false; + if ( $block ) { + # Delete block + if ( $block->delete() ) { + # Make log entry + $log = new LogPage( 'block' ); + $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $this->ip ), $this->reason ); + $success = true; + } } - # Delete block (if it exists) - # We should probably check for errors rather than just declaring success - $block->delete(); - - # Make log entry - $log = new LogPage( 'block' ); - $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $this->ip ), $this->reason ); - - # Report to the user - $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" ); - $success = $titleObj->getFullURL( "action=success&ip=" . urlencode( $this->ip ) ); - $wgOut->redirect( $success ); + if ( $success ) { + # Report to the user + $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" ); + $success = $titleObj->getFullURL( "action=success&successip=" . urlencode( $this->ip ) ); + $wgOut->redirect( $success ); + } else { + if ( !$this->ip && $this->id ) { + $this->ip = '#' . $this->id; + } + $this->showForm( wfMsg( 'ipb_cant_unblock', htmlspecialchars( $this->id ) ) ); + } } function showList( $msg ) { @@ -124,33 +160,55 @@ class IPUnblockForm { if ( "" != $msg ) { $wgOut->setSubtitle( $msg ); } - global $wgRequest; - list( $this->limit, $this->offset ) = $wgRequest->getLimitOffset(); - $this->counter = 0; - $paging = '<p>' . wfViewPrevNext( $this->offset, $this->limit, - Title::makeTitle( NS_SPECIAL, 'Ipblocklist' ), - 'ip=' . urlencode( $this->ip ) ) . "</p>\n"; - $wgOut->addHTML( $paging ); + // Purge expired entries on one in every 10 queries + if ( !mt_rand( 0, 10 ) ) { + Block::purgeExpired(); + } - $search = $this->searchForm(); - $wgOut->addHTML( $search ); - - $wgOut->addHTML( "<ul>" ); - if( !Block::enumBlocks( array( &$this, "addRow" ), 0 ) ) { - // FIXME hack to solve #bug 1487 - $wgOut->addHTML( '<li>'.wfMsgHtml( 'ipblocklistempty' ).'</li>' ); + $conds = array(); + if ( $this->ip == '' ) { + // No extra conditions + } elseif ( substr( $this->ip, 0, 1 ) == '#' ) { + $conds['ipb_id'] = substr( $this->ip, 1 ); + } elseif ( IP::toUnsigned( $this->ip ) !== false ) { + $conds['ipb_address'] = $this->ip; + $conds['ipb_auto'] = 0; + } elseif( preg_match( "/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\\/(\\d{1,2})$/", $this->ip, $matches ) ) { + $conds['ipb_address'] = Block::normaliseRange( $this->ip ); + $conds['ipb_auto'] = 0; + } else { + $user = User::newFromName( $this->ip ); + if ( $user && ( $id = $user->getID() ) != 0 ) { + $conds['ipb_user'] = $id; + } else { + // Uh...? + $conds['ipb_address'] = $this->ip; + $conds['ipb_auto'] = 0; + } + } + + $pager = new IPBlocklistPager( $this, $conds ); + $s = $pager->getNavigationBar() . + $this->searchForm(); + if ( $pager->getNumRows() ) { + $s .= "<ul>" . + $pager->getBody() . + "</ul>"; + } else { + $s .= '<p>' . wfMsgHTML( 'ipblocklistempty' ) . '</p>'; } - $wgOut->addHTML( "</ul>\n" ); - $wgOut->addHTML( $paging ); + $s .= $pager->getNavigationBar(); + $wgOut->addHTML( $s ); } function searchForm() { - global $wgTitle; + global $wgTitle, $wgScript, $wgRequest; return wfElement( 'form', array( - 'action' => $wgTitle->getLocalUrl() ), + 'action' => $wgScript ), null ) . + wfHidden( 'title', $wgTitle->getPrefixedDbKey() ) . wfElement( 'input', array( 'type' => 'hidden', 'name' => 'action', @@ -158,7 +216,7 @@ class IPUnblockForm { wfElement( 'input', array( 'type' => 'hidden', 'name' => 'limit', - 'value' => $this->limit ) ). + 'value' => $wgRequest->getText( 'limit' ) ) ) . wfElement( 'input', array( 'name' => 'ip', 'value' => $this->ip ) ) . @@ -171,33 +229,10 @@ class IPUnblockForm { /** * Callback function to output a block */ - function addRow( $block, $tag ) { - global $wgOut, $wgUser, $wgLang; + function formatRow( $block ) { + global $wgUser, $wgLang; - if( $this->ip != '' ) { - if( $block->mAuto ) { - if( stristr( $block->mId, $this->ip ) == false ) { - return; - } - } else { - if( stristr( $block->mAddress, $this->ip ) == false ) { - return; - } - } - } - - // Loading blocks is fast; displaying them is slow. - // Quick hack for paging. - $this->counter++; - if( $this->counter <= $this->offset ) { - return; - } - if( $this->counter - $this->offset > $this->limit ) { - return; - } - - $fname = 'IPUnblockForm-addRow'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); static $sk=null, $msg=null; @@ -205,14 +240,15 @@ class IPUnblockForm { $sk = $wgUser->getSkin(); if( is_null( $msg ) ) { $msg = array(); - foreach( array( 'infiniteblock', 'expiringblock', 'contribslink', 'unblocklink' ) as $key ) { + $keys = array( 'infiniteblock', 'expiringblock', 'contribslink', 'unblocklink', + 'anononlyblock', 'createaccountblock' ); + foreach( $keys as $key ) { $msg[$key] = wfMsgHtml( $key ); } $msg['blocklistline'] = wfMsg( 'blocklistline' ); $msg['contribslink'] = wfMsg( 'contribslink' ); } - # Prepare links to the blocker's user and talk pages $blocker_name = $block->getByName(); $blocker = $sk->MakeLinkObj( Title::makeTitle( NS_USER, $blocker_name ), $blocker_name ); @@ -220,35 +256,101 @@ class IPUnblockForm { # Prepare links to the block target's user and contribs. pages (as applicable, don't do it for autoblocks) if( $block->mAuto ) { - $target = '#' . $block->mId; # Hide the IP addresses of auto-blocks; privacy + $target = $block->getRedactedName(); # Hide the IP addresses of auto-blocks; privacy } else { $target = $sk->makeLinkObj( Title::makeTitle( NS_USER, $block->mAddress ), $block->mAddress ); $target .= ' (' . $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), $msg['contribslink'], 'target=' . urlencode( $block->mAddress ) ) . ')'; } - # Prep the address for the unblock link, masking autoblocks as before - $addr = $block->mAuto ? '#' . $block->mId : $block->mAddress; - $formattedTime = $wgLang->timeanddate( $block->mTimestamp, true ); - if ( $block->mExpiry === "" ) { - $formattedExpiry = $msg['infiniteblock']; + $properties = array(); + if ( $block->mExpiry === "" || $block->mExpiry === Block::infinity() ) { + $properties[] = $msg['infiniteblock']; } else { - $formattedExpiry = wfMsgReplaceArgs( $msg['expiringblock'], + $properties[] = wfMsgReplaceArgs( $msg['expiringblock'], array( $wgLang->timeanddate( $block->mExpiry, true ) ) ); } + if ( $block->mAnonOnly ) { + $properties[] = $msg['anononlyblock']; + } + if ( $block->mCreateAccount ) { + $properties[] = $msg['createaccountblock']; + } + $properties = implode( ', ', $properties ); - $line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $formattedExpiry ) ); + $line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $properties ) ); - $wgOut->addHTML( "<li>{$line}" ); + $s = "<li>{$line}"; if ( $wgUser->isAllowed('block') ) { $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" ); - $wgOut->addHTML( ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&ip=' . urlencode( $addr ) ) . ')' ); + $s .= ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&id=' . urlencode( $block->mId ) ) . ')'; } - $wgOut->addHTML( $sk->commentBlock( $block->mReason ) ); - $wgOut->addHTML( "</li>\n" ); - wfProfileOut( $fname ); + $s .= $sk->commentBlock( $block->mReason ); + $s .= "</li>\n"; + wfProfileOut( __METHOD__ ); + return $s; + } +} + +class IPBlocklistPager extends ReverseChronologicalPager { + public $mForm, $mConds; + + function __construct( $form, $conds = array() ) { + $this->mForm = $form; + $this->mConds = $conds; + parent::__construct(); + } + + function getStartBody() { + wfProfileIn( __METHOD__ ); + # Do a link batch query + $this->mResult->seek( 0 ); + $lb = new LinkBatch; + + /* + while ( $row = $this->mResult->fetchObject() ) { + $lb->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) ); + $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) ); + $lb->addObj( Title::makeTitleSafe( NS_USER, $row->ipb_address ) ); + $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ipb_address ) ); + }*/ + # Faster way + # Usernames and titles are in fact related by a simple substitution of space -> underscore + # The last few lines of Title::secureAndSplit() tell the story. + while ( $row = $this->mResult->fetchObject() ) { + $name = str_replace( ' ', '_', $row->user_name ); + $lb->add( NS_USER, $name ); + $lb->add( NS_USER_TALK, $name ); + $name = str_replace( ' ', '_', $row->ipb_address ); + $lb->add( NS_USER, $name ); + $lb->add( NS_USER_TALK, $name ); + } + $lb->execute(); + wfProfileOut( __METHOD__ ); + return ''; + } + + function formatRow( $row ) { + $block = new Block; + $block->initFromRow( $row ); + return $this->mForm->formatRow( $block ); + } + + function getQueryInfo() { + $conds = $this->mConds; + $conds[] = 'ipb_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() ); + $conds[] = 'ipb_by=user_id'; + return array( + 'tables' => array( 'ipblocks', 'user' ), + 'fields' => $this->mDb->tableName( 'ipblocks' ) . '.*,user_name', + 'conds' => $conds, + ); + } + + function getIndexField() { + return 'ipb_timestamp'; } } diff --git a/includes/SpecialListredirects.php b/includes/SpecialListredirects.php index 3cbdedab..f717ef72 100644 --- a/includes/SpecialListredirects.php +++ b/includes/SpecialListredirects.php @@ -32,6 +32,7 @@ class ListredirectsPage extends QueryPage { # Make a link to the redirect itself $rd_title = Title::makeTitle( $result->namespace, $result->title ); + $arr = $wgContLang->getArrow() . $wgContLang->getDirMark(); $rd_link = $skin->makeKnownLinkObj( $rd_title, '', 'redirect=no' ); # Find out where the redirect leads @@ -50,11 +51,8 @@ class ListredirectsPage extends QueryPage { $targetLink = '*'; } - # Check the language; RTL wikis need a ← - $arr = $wgContLang->isRTL() ? ' ← ' : ' → '; - # Format the whole thing and return it - return( $rd_link . $arr . $targetLink ); + return "$rd_link $arr $targetLink"; } diff --git a/includes/SpecialListusers.php b/includes/SpecialListusers.php index 20b26b63..4668d0c7 100644 --- a/includes/SpecialListusers.php +++ b/includes/SpecialListusers.php @@ -93,7 +93,7 @@ class ListUsersPage extends QueryPage { $out .= wfCloseElement( 'select' ) . ' ';;# . wfElement( 'br' ); # Username field - $out .= wfElement( 'label', array( 'for' => 'username' ), wfMsg( 'specialloguserlabel' ) ) . ' '; + $out .= wfElement( 'label', array( 'for' => 'username' ), wfMsg( 'listusersfrom' ) ) . ' '; $out .= wfElement( 'input', array( 'type' => 'text', 'id' => 'username', 'name' => 'username', 'value' => $this->requestedUser ) ) . ' '; @@ -111,6 +111,7 @@ class ListUsersPage extends QueryPage { } function getSQL() { + global $wgDBtype; $dbr =& wfGetDB( DB_SLAVE ); $user = $dbr->tableName( 'user' ); $user_groups = $dbr->tableName( 'user_groups' ); @@ -133,24 +134,26 @@ class ListUsersPage extends QueryPage { "LEFT JOIN $user_groups ON user_id=ug_user " . $this->userQueryWhere( $dbr ) . " GROUP BY user_name"; - + if ( $wgDBtype != 'mysql' ) { + $sql .= ",user_id"; + } return $sql; } function userQueryWhere( &$dbr ) { - $conds = $this->userQueryConditions(); + $conds = $this->userQueryConditions( $dbr ); return empty( $conds ) ? "" : "WHERE " . $dbr->makeList( $conds, LIST_AND ); } - function userQueryConditions() { + function userQueryConditions( $dbr ) { $conds = array(); if( $this->requestedGroup != '' ) { $conds['ug_group'] = $this->requestedGroup; } if( $this->requestedUser != '' ) { - $conds['user_name'] = $this->requestedUser; + $conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser ); } return $conds; } @@ -189,16 +192,12 @@ class ListUsersPage extends QueryPage { if( count( $groups ) > 0 ) { foreach( $groups as $group => $desc ) { - if( $page = User::getGroupPage( $group ) ) { - $list[] = $skin->makeLinkObj( $page, htmlspecialchars( $desc ) ); - } else { - $list[] = htmlspecialchars( $desc ); - } + $list[] = User::makeGroupLinkHTML( $group, $desc ); } $groups = implode( ', ', $list ); } else { $groups = ''; - } + } } diff --git a/includes/SpecialLockdb.php b/includes/SpecialLockdb.php index 38d715be..72172e2c 100644 --- a/includes/SpecialLockdb.php +++ b/includes/SpecialLockdb.php @@ -11,10 +11,18 @@ function wfSpecialLockdb() { global $wgUser, $wgOut, $wgRequest; - if ( ! $wgUser->isAllowed('siteadmin') ) { - $wgOut->developerRequired(); + if( !$wgUser->isAllowed( 'siteadmin' ) ) { + $wgOut->permissionRequired( 'siteadmin' ); return; } + + # If the lock file isn't writable, we can do sweet bugger all + global $wgReadOnlyFile; + if( !is_writable( dirname( $wgReadOnlyFile ) ) ) { + DBLockForm::notWritable(); + return; + } + $action = $wgRequest->getVal( 'action' ); $f = new DBLockForm(); @@ -56,12 +64,13 @@ class DBLockForm { $elr = htmlspecialchars( wfMsg( 'enterlockreason' ) ); $titleObj = Title::makeTitle( NS_SPECIAL, 'Lockdb' ); $action = $titleObj->escapeLocalURL( 'action=submit' ); + $reason = htmlspecialchars( $this->reason ); $token = htmlspecialchars( $wgUser->editToken() ); $wgOut->addHTML( <<<END <form id="lockdb" method="post" action="{$action}"> {$elr}: -<textarea name="wpLockReason" rows="10" cols="60" wrap="virtual"></textarea> +<textarea name="wpLockReason" rows="10" cols="60" wrap="virtual">{$reason}</textarea> <table border="0"> <tr> <td align="right"> @@ -91,10 +100,13 @@ END $this->showForm( wfMsg( 'locknoconfirm' ) ); return; } - $fp = fopen( $wgReadOnlyFile, 'w' ); + $fp = @fopen( $wgReadOnlyFile, 'w' ); if ( false === $fp ) { - $wgOut->showFileNotFoundError( $wgReadOnlyFile ); + # This used to show a file not found error, but the likeliest reason for fopen() + # to fail at this point is insufficient permission to write to the file...good old + # is_writable() is plain wrong in some cases, it seems... + $this->notWritable(); return; } fwrite( $fp, $this->reason ); @@ -113,6 +125,12 @@ END $wgOut->setSubtitle( wfMsg( 'lockdbsuccesssub' ) ); $wgOut->addWikiText( wfMsg( 'lockdbsuccesstext' ) ); } + + function notWritable() { + global $wgOut; + $wgOut->errorPage( 'lockdb', 'lockfilenotwritable' ); + } + } ?> diff --git a/includes/SpecialLog.php b/includes/SpecialLog.php index a9e8573a..e32d2240 100644 --- a/includes/SpecialLog.php +++ b/includes/SpecialLog.php @@ -28,11 +28,11 @@ */ function wfSpecialLog( $par = '' ) { global $wgRequest; - $logReader =& new LogReader( $wgRequest ); + $logReader = new LogReader( $wgRequest ); if( $wgRequest->getVal( 'type' ) == '' && $par != '' ) { $logReader->limitType( $par ); } - $logViewer =& new LogViewer( $logReader ); + $logViewer = new LogViewer( $logReader ); $logViewer->show(); } diff --git a/includes/SpecialLonelypages.php b/includes/SpecialLonelypages.php index 326ae54d..15022924 100644 --- a/includes/SpecialLonelypages.php +++ b/includes/SpecialLonelypages.php @@ -15,6 +15,9 @@ class LonelyPagesPage extends PageQueryPage { function getName() { return "Lonelypages"; } + function getPageHeader() { + return '<p>' . wfMsg('lonelypagestext') . '</p>'; + } function sortDescending() { return false; diff --git a/includes/SpecialLongpages.php b/includes/SpecialLongpages.php index af56c17c..3736d6fc 100644 --- a/includes/SpecialLongpages.php +++ b/includes/SpecialLongpages.php @@ -7,11 +7,6 @@ /** * - */ -require_once( 'SpecialShortpages.php' ); - -/** - * * @package MediaWiki * @subpackage SpecialPage */ diff --git a/includes/SpecialMostcategories.php b/includes/SpecialMostcategories.php index 5591bbc4..c0d662cc 100644 --- a/includes/SpecialMostcategories.php +++ b/includes/SpecialMostcategories.php @@ -31,7 +31,7 @@ class MostcategoriesPage extends QueryPage { FROM $categorylinks LEFT JOIN $page ON cl_from = page_id WHERE page_namespace = " . NS_MAIN . " - GROUP BY cl_from + GROUP BY 1,2,3 HAVING COUNT(*) > 1 "; } diff --git a/includes/SpecialMostimages.php b/includes/SpecialMostimages.php index 30fbdddf..09f71088 100644 --- a/includes/SpecialMostimages.php +++ b/includes/SpecialMostimages.php @@ -29,7 +29,7 @@ class MostimagesPage extends QueryPage { il_to as title, COUNT(*) as value FROM $imagelinks - GROUP BY il_to + GROUP BY 1,2,3 HAVING COUNT(*) > 1 "; } diff --git a/includes/SpecialMostlinked.php b/includes/SpecialMostlinked.php index ccccc1a4..1791228d 100644 --- a/includes/SpecialMostlinked.php +++ b/includes/SpecialMostlinked.php @@ -37,7 +37,7 @@ class MostlinkedPage extends QueryPage { page_namespace FROM $pagelinks LEFT JOIN $page ON pl_namespace=page_namespace AND pl_title=page_title - GROUP BY pl_namespace,pl_title + GROUP BY 1,2,3,5 HAVING COUNT(*) > 1"; } diff --git a/includes/SpecialMostlinkedcategories.php b/includes/SpecialMostlinkedcategories.php index 0944d2f8..5942b3f4 100644 --- a/includes/SpecialMostlinkedcategories.php +++ b/includes/SpecialMostlinkedcategories.php @@ -32,7 +32,7 @@ class MostlinkedCategoriesPage extends QueryPage { cl_to as title, COUNT(*) as value FROM $categorylinks - GROUP BY cl_to + GROUP BY 1,2,3 "; } diff --git a/includes/SpecialMostrevisions.php b/includes/SpecialMostrevisions.php index 81a49c99..676923ae 100644 --- a/includes/SpecialMostrevisions.php +++ b/includes/SpecialMostrevisions.php @@ -31,9 +31,9 @@ class MostrevisionsPage extends QueryPage { page_title as title, COUNT(*) as value FROM $revision - LEFT JOIN $page ON page_id = rev_page + JOIN $page ON page_id = rev_page WHERE page_namespace = " . NS_MAIN . " - GROUP BY rev_page + GROUP BY 1,2,3 HAVING COUNT(*) > 1 "; } diff --git a/includes/SpecialMovepage.php b/includes/SpecialMovepage.php index 39397129..e33c1530 100644 --- a/includes/SpecialMovepage.php +++ b/includes/SpecialMovepage.php @@ -11,12 +11,19 @@ function wfSpecialMovepage( $par = null ) { global $wgUser, $wgOut, $wgRequest, $action, $wgOnlySysopMayMove; - # check rights. We don't want newbies to move pages to prevents possible attack - if ( !$wgUser->isAllowed( 'move' ) or $wgUser->isBlocked() or ($wgOnlySysopMayMove and $wgUser->isNewbie())) { - $wgOut->showErrorPage( "movenologin", "movenologintext" ); + # Check rights + if ( !$wgUser->isAllowed( 'move' ) ) { + $wgOut->showErrorPage( 'movenologin', 'movenologintext' ); return; } - # We don't move protected pages + + # Don't allow blocked users to move pages + if ( $wgUser->isBlocked() ) { + $wgOut->blockedPage(); + return; + } + + # Check for database lock if ( wfReadOnly() ) { $wgOut->readOnlyPage(); return; @@ -249,8 +256,8 @@ class MovePageForm { $wgOut->setPagetitle( wfMsg( 'movepage' ) ); $wgOut->setSubtitle( wfMsg( 'pagemovedsub' ) ); - $oldText = $wgRequest->getVal('oldtitle'); - $newText = $wgRequest->getVal('newtitle'); + $oldText = wfEscapeWikiText( $wgRequest->getVal('oldtitle') ); + $newText = wfEscapeWikiText( $wgRequest->getVal('newtitle') ); $talkmoved = $wgRequest->getVal('talkmoved'); $text = wfMsg( 'pagemovedtext', $oldText, $newText ); @@ -266,7 +273,7 @@ class MovePageForm { $wgOut->addWikiText( wfMsg( 'talkexists' ) ); } else { $oldTitle = Title::newFromText( $oldText ); - if ( !$oldTitle->isTalkPage() && $talkmoved != 'notalkpage' ) { + if ( isset( $oldTitle ) && !$oldTitle->isTalkPage() && $talkmoved != 'notalkpage' ) { $wgOut->addWikiText( wfMsg( 'talkpagenotmoved', wfMsg( $talkmoved ) ) ); } } diff --git a/includes/SpecialNewimages.php b/includes/SpecialNewimages.php index 976611a3..95c90e42 100644 --- a/includes/SpecialNewimages.php +++ b/includes/SpecialNewimages.php @@ -17,7 +17,8 @@ function wfSpecialNewimages( $par, $specialPage ) { $shownav = !$specialPage->including(); $hidebots = $wgRequest->getBool('hidebots',1); - if($hidebots) { + $hidebotsql = ''; + if ($hidebots) { /** Make a list of group names which have the 'bot' flag set. @@ -28,23 +29,26 @@ function wfSpecialNewimages( $par, $specialPage ) { $botconds[]="ug_group='$groupname'"; } } - $isbotmember=$dbr->makeList($botconds, LIST_OR); - /** This join, in conjunction with WHERE ug_group - IS NULL, returns only those rows from IMAGE - where the uploading user is not a member of - a group which has the 'bot' permission set. - */ - $ug = $dbr->tableName('user_groups'); - $joinsql=" LEFT OUTER JOIN $ug ON img_user=ug_user AND (" - . $isbotmember.')'; + /* If not bot groups, do not set $hidebotsql */ + if ($botconds) { + $isbotmember=$dbr->makeList($botconds, LIST_OR); + + /** This join, in conjunction with WHERE ug_group + IS NULL, returns only those rows from IMAGE + where the uploading user is not a member of + a group which has the 'bot' permission set. + */ + $ug = $dbr->tableName('user_groups'); + $hidebotsql = " LEFT OUTER JOIN $ug ON img_user=ug_user AND ($isbotmember)"; + } } $image = $dbr->tableName('image'); $sql="SELECT img_timestamp from $image"; - if($hidebots) { - $sql.=$joinsql.' WHERE ug_group IS NULL'; + if ($hidebotsql) { + $sql .= "$hidebotsql WHERE ug_group IS NULL"; } $sql.=' ORDER BY img_timestamp DESC LIMIT 1'; $res = $dbr->query($sql, 'wfSpecialNewImages'); @@ -91,8 +95,8 @@ function wfSpecialNewimages( $par, $specialPage ) { $sql='SELECT img_size, img_name, img_user, img_user_text,'. "img_description,img_timestamp FROM $image"; - if($hidebots) { - $sql.=$joinsql; + if($hidebotsql) { + $sql .= $hidebotsql; $where[]='ug_group IS NULL'; } if(count($where)) { @@ -130,7 +134,7 @@ function wfSpecialNewimages( $par, $specialPage ) { $ut = $s->img_user_text; $nt = Title::newFromText( $name, NS_IMAGE ); - $img = Image::newFromTitle( $nt ); + $img = new Image( $nt ); $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut ); $gallery->add( $img, "$ul<br />\n<i>".$wgLang->timeanddate( $s->img_timestamp, true )."</i><br />\n" ); diff --git a/includes/SpecialNewpages.php b/includes/SpecialNewpages.php index c0c6ba96..3fd0eba2 100644 --- a/includes/SpecialNewpages.php +++ b/includes/SpecialNewpages.php @@ -11,10 +11,13 @@ * @subpackage SpecialPage */ class NewPagesPage extends QueryPage { + var $namespace; + var $username = ''; - function NewPagesPage( $namespace = NS_MAIN ) { + function NewPagesPage( $namespace = NS_MAIN, $username = '' ) { $this->namespace = $namespace; + $this->username = $username; } function getName() { @@ -26,12 +29,23 @@ class NewPagesPage extends QueryPage { return false; } + function makeUserWhere( &$dbo ) { + $title = Title::makeTitleSafe( NS_USER, $this->username ); + if( $title ) { + return ' AND rc_user_text = ' . $dbo->addQuotes( $title->getText() ); + } else { + return ''; + } + } + function getSQL() { global $wgUser, $wgUseRCPatrol; $usepatrol = ( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) ? 1 : 0; $dbr =& wfGetDB( DB_SLAVE ); extract( $dbr->tableNames( 'recentchanges', 'page', 'text' ) ); + $uwhere = $this->makeUserWhere( $dbr ); + # FIXME: text will break with compression return "SELECT 'Newpages' as type, @@ -50,7 +64,8 @@ class NewPagesPage extends QueryPage { page_latest as rev_id FROM $recentchanges,$page WHERE rc_cur_id=page_id AND rc_new=1 - AND rc_namespace=" . $this->namespace . " AND page_is_redirect=0"; + AND rc_namespace=" . $this->namespace . " AND page_is_redirect=0 + {$uwhere}"; } function preprocessResults( &$dbo, &$res ) { @@ -112,34 +127,20 @@ class NewPagesPage extends QueryPage { } /** - * Show a namespace selection form for filtering + * Show a form for filtering namespace and username * * @return string */ function getPageHeader() { - $thisTitle = Title::makeTitle( NS_SPECIAL, $this->getName() ); - $form = wfOpenElement( 'form', array( - 'method' => 'post', - 'action' => $thisTitle->getLocalUrl() ) ); - $form .= wfElement( 'label', array( 'for' => 'namespace' ), - wfMsg( 'namespace' ) ) . ' '; - $form .= HtmlNamespaceSelector( $this->namespace ); - # Preserve the offset and limit - $form .= wfElement( 'input', array( - 'type' => 'hidden', - 'name' => 'offset', - 'value' => $this->offset ) ); - $form .= wfElement( 'input', array( - 'type' => 'hidden', - 'name' => 'limit', - 'value' => $this->limit ) ); - $form .= wfElement( 'input', array( - 'type' => 'submit', - 'name' => 'submit', - 'id' => 'submit', - 'value' => wfMsg( 'allpagessubmit' ) ) ); - $form .= wfCloseElement( 'form' ); - return( $form ); + $self = Title::makeTitle( NS_SPECIAL, $this->getName() ); + $form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) ); + $form .= '<table><tr><td align="right">' . wfMsgHtml( 'namespace' ) . '</td>'; + $form .= '<td>' . HtmlNamespaceSelector( $this->namespace ) . '</td><tr>'; + $form .= '<tr><td align="right">' . wfMsgHtml( 'newpages-username' ) . '</td>'; + $form .= '<td>' . wfInput( 'username', 30, $this->username ) . '</td></tr>'; + $form .= '<tr><td></td><td>' . wfSubmitButton( wfMsg( 'allpagessubmit' ) ) . '</td></tr></table>'; + $form .= wfHidden( 'offset', $this->offset ) . wfHidden( 'limit', $this->limit ) . '</form>'; + return $form; } /** @@ -148,7 +149,7 @@ class NewPagesPage extends QueryPage { * @return array */ function linkParameters() { - return( array( 'namespace' => $this->namespace ) ); + return( array( 'namespace' => $this->namespace, 'username' => $this->username ) ); } } @@ -161,6 +162,7 @@ function wfSpecialNewpages($par, $specialPage) { list( $limit, $offset ) = wfCheckLimits(); $namespace = NS_MAIN; + $username = ''; if ( $par ) { $bits = preg_split( '/\s*,\s*/', trim( $par ) ); @@ -184,12 +186,14 @@ function wfSpecialNewpages($par, $specialPage) { } else { if( $ns = $wgRequest->getInt( 'namespace', 0 ) ) $namespace = $ns; + if( $un = $wgRequest->getText( 'username' ) ) + $username = $un; } if ( ! isset( $shownavigation ) ) $shownavigation = ! $specialPage->including(); - $npp = new NewPagesPage( $namespace ); + $npp = new NewPagesPage( $namespace, $username ); if ( ! $npp->doFeed( $wgRequest->getVal( 'feed' ), $limit ) ) $npp->doQuery( $offset, $limit, $shownavigation ); diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index ffcd51fa..294c05ef 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -279,24 +279,43 @@ class SpecialPage } /** - * Return categorised listable special pages - * Returns a 2d array where the first index is the restriction name + * Return categorised listable special pages for all users * @static */ - static function getPages() { + static function getRegularPages() { if ( !self::$mListInitialised ) { self::initList(); } - $pages = array( - '' => array(), - 'sysop' => array(), - 'developer' => array() - ); + $pages = array(); + + foreach ( self::$mList as $name => $rec ) { + $page = self::getPage( $name ); + if ( $page->isListed() && $page->getRestriction() == '' ) { + $pages[$name] = $page; + } + } + return $pages; + } + + /** + * Return categorised listable special pages which are available + * for the current user, but not for everyone + * @static + */ + static function getRestrictedPages() { + global $wgUser; + if ( !self::$mListInitialised ) { + self::initList(); + } + $pages = array(); foreach ( self::$mList as $name => $rec ) { $page = self::getPage( $name ); if ( $page->isListed() ) { - $pages[$page->getRestriction()][$page->getName()] = $page; + $restriction = $page->getRestriction(); + if ( $restriction != '' && $wgUser->isAllowed( $restriction ) ) { + $pages[$name] = $page; + } } } return $pages; @@ -313,7 +332,7 @@ class SpecialPage * @param $title a title object * @param $including output is being captured for use in {{special:whatever}} */ - function executePath( &$title, $including = false ) { + static function executePath( &$title, $including = false ) { global $wgOut, $wgTitle; $fname = 'SpecialPage::executePath'; wfProfileIn( $fname ); @@ -410,7 +429,7 @@ class SpecialPage * and displayRestrictionError() * * @param string $name Name of the special page, as seen in links and URLs - * @param string $restriction Minimum user level required, e.g. "sysop" or "developer". + * @param string $restriction User right required, e.g. "block" or "delete" * @param boolean $listed Whether the page is listed in Special:Specialpages * @param string $function Function called by execute(). By default it is constructed from $name * @param string $file File which is included by execute(). It is also constructed from $name by default @@ -460,15 +479,7 @@ class SpecialPage * special page (as defined by $mRestriction) */ function userCanExecute( &$user ) { - if ( $this->mRestriction == "" ) { - return true; - } else { - if ( in_array( $this->mRestriction, $user->getRights() ) ) { - return true; - } else { - return false; - } - } + return $user->isAllowed( $this->mRestriction ); } /** diff --git a/includes/SpecialPreferences.php b/includes/SpecialPreferences.php index c6003b7c..5eadf3d6 100644 --- a/includes/SpecialPreferences.php +++ b/includes/SpecialPreferences.php @@ -74,7 +74,7 @@ class PreferencesForm { # User toggles (the big ugly unsorted list of checkboxes) $this->mToggles = array(); if ( $this->mPosted ) { - $togs = $wgLang->getUserToggles(); + $togs = User::getToggles(); foreach ( $togs as $tname ) { $this->mToggles[$tname] = $request->getCheck( "wpOp$tname" ) ? 1 : 0; } @@ -156,11 +156,16 @@ class PreferencesForm { /** * @access private */ - function validateDate( &$val, $min = 0, $max=0x7fffffff ) { - if ( ( sprintf('%d', $val) === $val && $val >= $min && $val <= $max ) || $val == 'ISO 8601' ) + function validateDate( $val ) { + global $wgLang, $wgContLang; + if ( $val !== false && ( + in_array( $val, (array)$wgLang->getDatePreferences() ) || + in_array( $val, (array)$wgContLang->getDatePreferences() ) ) ) + { return $val; - else - return 0; + } else { + return $wgLang->getDefaultDateFormat(); + } } /** @@ -257,7 +262,7 @@ class PreferencesForm { if( $wgUseTeX ) { $wgUser->setOption( 'math', $this->mMath ); } - $wgUser->setOption( 'date', $this->validateDate( $this->mDate, 0, 20 ) ); + $wgUser->setOption( 'date', $this->validateDate( $this->mDate ) ); $wgUser->setOption( 'searchlimit', $this->validateIntOrNull( $this->mSearch ) ); $wgUser->setOption( 'contextlines', $this->validateIntOrNull( $this->mSearchLines ) ); $wgUser->setOption( 'contextchars', $this->validateIntOrNull( $this->mSearchChars ) ); @@ -356,7 +361,7 @@ class PreferencesForm { $this->mQuickbar = $wgUser->getOption( 'quickbar' ); $this->mSkin = Skin::normalizeKey( $wgUser->getOption( 'skin' ) ); $this->mMath = $wgUser->getOption( 'math' ); - $this->mDate = $wgUser->getOption( 'date' ); + $this->mDate = $wgUser->getDatePreference(); $this->mRows = $wgUser->getOption( 'rows' ); $this->mCols = $wgUser->getOption( 'cols' ); $this->mStubs = $wgUser->getOption( 'stubthreshold' ); @@ -371,7 +376,7 @@ class PreferencesForm { $this->mUnderline = $wgUser->getOption( 'underline' ); $this->mWatchlistDays = $wgUser->getOption( 'watchlistdays' ); - $togs = $wgLang->getUserToggles(); + $togs = User::getToggles(); foreach ( $togs as $tname ) { $ttext = wfMsg('tog-'.$tname); $this->mToggles[$tname] = $wgUser->getOption( $tname ); @@ -470,8 +475,8 @@ class PreferencesForm { $qbs = $wgLang->getQuickbarSettings(); $skinNames = $wgLang->getSkinNames(); $mathopts = $wgLang->getMathNames(); - $dateopts = $wgLang->getDateFormats(); - $togs = $wgLang->getUserToggles(); + $dateopts = $wgLang->getDatePreferences(); + $togs = User::getToggles(); $titleObj = Title::makeTitle( NS_SPECIAL, 'Preferences' ); $action = $titleObj->escapeLocalURL(); @@ -595,7 +600,7 @@ class PreferencesForm { * Make sure the site language is in the list; a custom language code * might not have a defined name... */ - $languages = $wgLang->getLanguageNames(); + $languages = $wgLang->getLanguageNames( true ); if( !array_key_exists( $wgContLanguageCode, $languages ) ) { $languages[$wgContLanguageCode] = $wgContLanguageCode; } @@ -610,12 +615,8 @@ class PreferencesForm { $selbox = null; foreach($languages as $code => $name) { global $IP; - /* only add languages that have a file */ - $langfile="$IP/languages/Language".str_replace('-', '_', ucfirst($code)).".php"; - if(file_exists($langfile) || $code == $wgContLanguageCode) { - $sel = ($code == $selectedLang)? ' selected="selected"' : ''; - $selbox .= "<option value=\"$code\"$sel>$code - $name</option>\n"; - } + $sel = ($code == $selectedLang)? ' selected="selected"' : ''; + $selbox .= "<option value=\"$code\"$sel>$code - $name</option>\n"; } $wgOut->addHTML( $this->addRow( @@ -764,20 +765,20 @@ class PreferencesForm { <legend>" . wfMsg( 'files' ) . "</legend> <div><label for='wpImageSize'>" . wfMsg('imagemaxsize') . "</label> <select id='wpImageSize' name='wpImageSize'>"); - $imageLimitOptions = null; - foreach ( $wgImageLimits as $index => $limits ) { - $selected = ($index == $this->mImageSize) ? 'selected="selected"' : ''; - $imageLimitOptions .= "<option value=\"{$index}\" {$selected}>{$limits[0]}×{$limits[1]}". wfMsgHtml('unit-pixel') ."</option>\n"; - } + $imageLimitOptions = null; + foreach ( $wgImageLimits as $index => $limits ) { + $selected = ($index == $this->mImageSize) ? 'selected="selected"' : ''; + $imageLimitOptions .= "<option value=\"{$index}\" {$selected}>{$limits[0]}×{$limits[1]}". wfMsgHtml('unit-pixel') ."</option>\n"; + } - $imageThumbOptions = null; - $wgOut->addHTML( "{$imageLimitOptions}</select></div> - <div><label for='wpThumbSize'>" . wfMsg('thumbsize') . "</label> <select name='wpThumbSize' id='wpThumbSize'>"); - foreach ( $wgThumbLimits as $index => $size ) { - $selected = ($index == $this->mThumbSize) ? 'selected="selected"' : ''; - $imageThumbOptions .= "<option value=\"{$index}\" {$selected}>{$size}". wfMsgHtml('unit-pixel') ."</option>\n"; - } - $wgOut->addHTML( "{$imageThumbOptions}</select></div></fieldset>\n\n"); + $imageThumbOptions = null; + $wgOut->addHTML( "{$imageLimitOptions}</select></div> + <div><label for='wpThumbSize'>" . wfMsg('thumbsize') . "</label> <select name='wpThumbSize' id='wpThumbSize'>"); + foreach ( $wgThumbLimits as $index => $size ) { + $selected = ($index == $this->mThumbSize) ? 'selected="selected"' : ''; + $imageThumbOptions .= "<option value=\"{$index}\" {$selected}>{$size}". wfMsgHtml('unit-pixel') ."</option>\n"; + } + $wgOut->addHTML( "{$imageThumbOptions}</select></div></fieldset>\n\n"); # Date format # @@ -789,9 +790,9 @@ class PreferencesForm { if ($dateopts) { $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg( 'dateformat' ) . "</legend>\n" ); $idCnt = 0; - $epoch = '20010408091234'; - foreach($dateopts as $key => $option) { - if( $key == MW_DATE_DEFAULT ) { + $epoch = '20010115161234'; # Wikipedia day + foreach( $dateopts as $key ) { + if( $key == 'default' ) { $formatted = wfMsgHtml( 'datedefault' ); } else { $formatted = htmlspecialchars( $wgLang->timeanddate( $epoch, false, $key ) ); @@ -816,8 +817,7 @@ class PreferencesForm { ) . "<tr><td colspan='2'> <input type='button' value=\"" . wfMsg( 'guesstimezone' ) ."\" onclick='javascript:guessTimezone()' id='guesstimezonebutton' style='display:none;' /> - </td></tr></table></fieldset> - <div class='prefsectiontip'>¹" . wfMsg( 'timezonetext' ) . "</div> + </td></tr></table><div class='prefsectiontip'>¹" . wfMsg( 'timezonetext' ) . "</div></fieldset> </fieldset>\n\n" ); # Editing diff --git a/includes/SpecialRecentchanges.php b/includes/SpecialRecentchanges.php index 97f810d9..8dfb68a5 100644 --- a/includes/SpecialRecentchanges.php +++ b/includes/SpecialRecentchanges.php @@ -319,7 +319,7 @@ function rcFilterByCategories ( &$rows , $categories , $any ) { } function rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod ) { - global $messageMemc, $wgDBname, $wgFeedCacheTimeout; + global $messageMemc, $wgFeedCacheTimeout; global $wgFeedClasses, $wgTitle, $wgSitename, $wgContLanguageCode; if( !isset( $wgFeedClasses[$feedFormat] ) ) { @@ -327,8 +327,8 @@ function rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod ) { return false; } - $timekey = "$wgDBname:rcfeed:$feedFormat:timestamp"; - $key = "$wgDBname:rcfeed:$feedFormat:limit:$limit:minor:$hideminor"; + $timekey = wfMemcKey( 'rcfeed', $feedFormat, 'timestamp' ); + $key = wfMemcKey( 'rcfeed', $feedFormat, 'limit', $limit, 'minor', $hideminor ); $feedTitle = $wgSitename . ' - ' . wfMsgForContent( 'recentchanges' ) . ' [' . $wgContLanguageCode . ']'; @@ -481,7 +481,7 @@ function makeOptionsLink( $title, $override, $options ) { global $wgUser, $wgContLang; $sk = $wgUser->getSkin(); return $sk->makeKnownLink( $wgContLang->specialPage( 'Recentchanges' ), - $title, wfArrayToCGI( $override, $options ) ); + htmlspecialchars( $title ), wfArrayToCGI( $override, $options ) ); } /** @@ -624,7 +624,6 @@ function rcFormatDiffRow( $title, $oldid, $newid, $timestamp, $comment ) { $fname = 'rcFormatDiff'; wfProfileIn( $fname ); - require_once( 'DifferenceEngine.php' ); $skin = $wgUser->getSkin(); $completeText = '<p>' . $skin->formatComment( $comment ) . "</p>\n"; diff --git a/includes/SpecialRecentchangeslinked.php b/includes/SpecialRecentchangeslinked.php index 2a611c4d..59a3beb5 100644 --- a/includes/SpecialRecentchangeslinked.php +++ b/includes/SpecialRecentchangeslinked.php @@ -71,6 +71,14 @@ function wfSpecialRecentchangeslinked( $par = NULL ) { $uid = $wgUser->getID(); + $GROUPBY = " + GROUP BY rc_cur_id,rc_namespace,rc_title, + rc_user,rc_comment,rc_user_text,rc_timestamp,rc_minor, + rc_new, rc_id, rc_this_oldid, rc_last_oldid, rc_bot, rc_patrolled, rc_type +" . ($uid ? ",wl_user" : "") . " + ORDER BY rc_timestamp DESC + LIMIT {$limit}"; + // If target is a Category, use categorylinks and invert from and to if( $nt->getNamespace() == NS_CATEGORY ) { $catkey = $dbr->addQuotes( $nt->getDBKey() ); @@ -97,11 +105,7 @@ function wfSpecialRecentchangeslinked( $par = NULL ) { {$cmq} AND cl_from=rc_cur_id AND cl_to=$catkey - GROUP BY rc_cur_id,rc_namespace,rc_title, - rc_user,rc_comment,rc_user_text,rc_timestamp,rc_minor, - rc_new - ORDER BY rc_timestamp DESC - LIMIT {$limit}; +$GROUPBY "; } else { $sql = @@ -129,11 +133,8 @@ function wfSpecialRecentchangeslinked( $par = NULL ) { AND pl_namespace=rc_namespace AND pl_title=rc_title AND pl_from=$id -GROUP BY rc_cur_id,rc_namespace,rc_title, - rc_user,rc_comment,rc_user_text,rc_timestamp,rc_minor, - rc_new -ORDER BY rc_timestamp DESC - LIMIT {$limit}"; +$GROUPBY +"; } $res = $dbr->query( $sql, $fname ); @@ -156,9 +157,6 @@ ORDER BY rc_timestamp DESC if ( 0 == $count ) { break; } $obj = $dbr->fetchObject( $res ); --$count; -# print_r ( $obj ) ; -# print "<br/>\n" ; - $rc = RecentChange::newFromRow( $obj ); $rc->counter = $counter++; $s .= $list->recentChangesLine( $rc , !empty( $obj->wl_user) ); diff --git a/includes/SpecialRevisiondelete.php b/includes/SpecialRevisiondelete.php index 7fa8bbb4..afbb589c 100644 --- a/includes/SpecialRevisiondelete.php +++ b/includes/SpecialRevisiondelete.php @@ -13,7 +13,7 @@ function wfSpecialRevisiondelete( $par = null ) { global $wgOut, $wgRequest, $wgUser; $target = $wgRequest->getVal( 'target' ); - $oldid = $wgRequest->getInt( 'oldid' ); + $oldid = $wgRequest->getIntArray( 'oldid' ); $sk = $wgUser->getSkin(); $page = Title::newFromUrl( $target ); @@ -23,6 +23,11 @@ function wfSpecialRevisiondelete( $par = null ) { return; } + if( is_null( $oldid ) ) { + $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); + return; + } + $form = new RevisionDeleteForm( $wgRequest ); if( $wgRequest->wasPosted() ) { $form->submit( $wgRequest ); @@ -58,13 +63,15 @@ class RevisionDeleteForm { function show( $request ) { global $wgOut, $wgUser; - $first = $this->revisions[0]; - $wgOut->addWikiText( wfMsg( 'revdelete-selected', $this->page->getPrefixedText() ) ); $wgOut->addHtml( "<ul>" ); foreach( $this->revisions as $revid ) { $rev = Revision::newFromTitle( $this->page, $revid ); + if( !isset( $rev ) ) { + $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); + return; + } $wgOut->addHtml( $this->historyLine( $rev ) ); $bitfields[] = $rev->mDeleted; // FIXME } @@ -85,7 +92,8 @@ class RevisionDeleteForm { $special = Title::makeTitle( NS_SPECIAL, 'Revisiondelete' ); $wgOut->addHtml( wfElement( 'form', array( 'method' => 'post', - 'action' => $special->getLocalUrl( 'action=submit' ) ) ) ); + 'action' => $special->getLocalUrl( 'action=submit' ) ), + null ) ); $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'revdelete-legend' ) . '</legend>' ); foreach( $this->checks as $item ) { @@ -180,6 +188,9 @@ class RevisionDeleter { // To work! foreach( $items as $revid ) { $rev = Revision::newFromId( $revid ); + if( !isset( $rev ) ) { + return false; + } $this->updateRevision( $rev, $bitfield ); $this->updateRecentChanges( $rev, $bitfield ); diff --git a/includes/SpecialSearch.php b/includes/SpecialSearch.php index 4db27e87..057b487c 100644 --- a/includes/SpecialSearch.php +++ b/includes/SpecialSearch.php @@ -77,6 +77,7 @@ class SpecialSearch { function goResult( $term ) { global $wgOut; global $wgGoToEdit; + global $wgContLang; $this->setupPage( $term ); @@ -110,7 +111,7 @@ class SpecialSearch { $editurl = $t->escapeLocalURL( 'action=edit' ); } } - $wgOut->addWikiText( wfMsg( 'noexactmatch', $term ) ); + $wgOut->addWikiText( wfMsg( 'noexactmatch', wfEscapeWikiText( $term ) ) ); return $this->showResults( $term ); } @@ -151,7 +152,7 @@ class SpecialSearch { wfMsg( 'googlesearch', htmlspecialchars( $term ), htmlspecialchars( $wgInputEncoding ), - htmlspecialchars( wfMsg( 'search' ) ) + htmlspecialchars( wfMsg( 'searchbutton' ) ) ) ); wfProfileOut( $fname ); diff --git a/includes/SpecialShortpages.php b/includes/SpecialShortpages.php index d8e13c7b..34b3505b 100644 --- a/includes/SpecialShortpages.php +++ b/includes/SpecialShortpages.php @@ -65,6 +65,9 @@ class ShortPagesPage extends QueryPage { $dm = $wgContLang->getDirMark(); $title = Title::makeTitleSafe( $result->namespace, $result->title ); + if ( !$title ) { + return '<!-- Invalid title ' . htmlspecialchars( "{$result->namespace}:{$result->title}" ). '-->'; + } $hlink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' ); $plink = $this->isCached() ? $skin->makeLinkObj( $title ) diff --git a/includes/SpecialSpecialpages.php b/includes/SpecialSpecialpages.php index 0b53db73..6a01cd08 100644 --- a/includes/SpecialSpecialpages.php +++ b/includes/SpecialSpecialpages.php @@ -9,29 +9,16 @@ * */ function wfSpecialSpecialpages() { - global $wgOut, $wgUser, $wgAvailableRights; + global $wgOut, $wgUser; $wgOut->setRobotpolicy( 'index,nofollow' ); $sk = $wgUser->getSkin(); - # Get listable pages, in a 2-d array with the first dimension being user right - $pages = SpecialPage::getPages(); - /** Pages available to all */ - wfSpecialSpecialpages_gen($pages[''],'spheading',$sk); + wfSpecialSpecialpages_gen( SpecialPage::getRegularPages(), 'spheading', $sk ); /** Restricted special pages */ - $rpages = array(); - foreach($wgAvailableRights as $right) { - /** only show pages a user can access */ - if( $wgUser->isAllowed($right) ) { - /** some rights might not have any special page associated */ - if(isset($pages[$right])) { - $rpages = array_merge( $rpages, $pages[$right] ); - } - } - } - wfSpecialSpecialpages_gen( $rpages, 'restrictedpheading', $sk ); + wfSpecialSpecialpages_gen( SpecialPage::getRestrictedPages(), 'restrictedpheading', $sk ); } /** diff --git a/includes/SpecialStatistics.php b/includes/SpecialStatistics.php index 5903546a..4a51efd9 100644 --- a/includes/SpecialStatistics.php +++ b/includes/SpecialStatistics.php @@ -75,12 +75,32 @@ function wfSpecialStatistics() { $text .= wfMsg( 'userstatstext', $wgLang->formatNum( $users ), $wgLang->formatNum( $admins ), - '[[' . wfMsgForContent( 'administrators' ) . ']]', - // should logically be after #admins, damn backwards compatability! - $wgLang->formatNum( sprintf( '%.2f', $admins / $users * 100 ) ) + '[[' . wfMsgForContent( 'grouppage-sysop' ) . ']]', # TODO somehow remove, kept for backwards compatibility + $wgLang->formatNum( sprintf( '%.2f', $admins / $users * 100 ) ), + User::makeGroupLinkWiki( 'sysop' ) ); $wgOut->addWikiText( $text ); + + global $wgDisableCounters, $wgMiserMode, $wgUser, $wgLang, $wgContLang; + if( !$wgDisableCounters && !$wgMiserMode ) { + $sql = "SELECT page_namespace, page_title, page_counter FROM {$page} WHERE page_is_redirect = 0 AND page_counter > 0 ORDER BY page_counter DESC"; + $sql = $dbr->limitResult($sql, 10, 0); + $res = $dbr->query( $sql, $fname ); + if( $res ) { + $wgOut->addHtml( '<h2>' . wfMsgHtml( 'statistics-mostpopular' ) . '</h2>' ); + $skin =& $wgUser->getSkin(); + $wgOut->addHtml( '<ol>' ); + while( $row = $dbr->fetchObject( $res ) ) { + $link = $skin->makeKnownLinkObj( Title::makeTitleSafe( $row->page_namespace, $row->page_title ) ); + $dirmark = $wgContLang->getDirMark(); + $wgOut->addHtml( '<li>' . $link . $dirmark . ' [' . $wgLang->formatNum( $row->page_counter ) . ']</li>' ); + } + $wgOut->addHtml( '</ol>' ); + $dbr->freeResult( $res ); + } + } + } } ?> diff --git a/includes/SpecialUndelete.php b/includes/SpecialUndelete.php index 695c8c29..8e0291ec 100644 --- a/includes/SpecialUndelete.php +++ b/includes/SpecialUndelete.php @@ -77,7 +77,6 @@ class PageArchive { * @fixme Does this belong in Image for fuller encapsulation? */ function listFiles() { - $fname = __CLASS__ . '::' . __FUNCTION__; if( $this->title->getNamespace() == NS_IMAGE ) { $dbr =& wfGetDB( DB_SLAVE ); $res = $dbr->select( 'filearchive', @@ -93,7 +92,7 @@ class PageArchive { 'fa_user_text', 'fa_timestamp' ), array( 'fa_name' => $this->title->getDbKey() ), - $fname, + __METHOD__, array( 'ORDER BY' => 'fa_timestamp DESC' ) ); $ret = $dbr->resultObject( $res ); return $ret; @@ -108,14 +107,13 @@ class PageArchive { * @return string */ function getRevisionText( $timestamp ) { - $fname = 'PageArchive::getRevisionText'; $dbr =& wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'archive', array( 'ar_text', 'ar_flags', 'ar_text_id' ), array( 'ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDbkey(), 'ar_timestamp' => $dbr->timestamp( $timestamp ) ), - $fname ); + __METHOD__ ); if( $row ) { return $this->getTextFromRow( $row ); } else { @@ -127,8 +125,6 @@ class PageArchive { * Get the text from an archive row containing ar_text, ar_flags and ar_text_id */ function getTextFromRow( $row ) { - $fname = 'PageArchive::getTextFromRow'; - if( is_null( $row->ar_text_id ) ) { // An old row from MediaWiki 1.4 or previous. // Text is embedded in this row in classic compression format. @@ -139,7 +135,7 @@ class PageArchive { $text = $dbr->selectRow( 'text', array( 'old_text', 'old_flags' ), array( 'old_id' => $row->ar_text_id ), - $fname ); + __METHOD__ ); return Revision::getRevisionText( $text ); } } @@ -252,7 +248,6 @@ class PageArchive { private function undeleteRevisions( $timestamps ) { global $wgParser, $wgDBtype; - $fname = __CLASS__ . '::' . __FUNCTION__; $restoreAll = empty( $timestamps ); $dbw =& wfGetDB( DB_MASTER ); @@ -267,7 +262,7 @@ class PageArchive { array( 'page_id', 'page_latest' ), array( 'page_namespace' => $this->title->getNamespace(), 'page_title' => $this->title->getDBkey() ), - $fname, + __METHOD__, $options ); if( $page ) { # Page already exists. Import the history, and if necessary @@ -311,12 +306,12 @@ class PageArchive { 'ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDBkey(), $oldones ), - $fname, + __METHOD__, /* options */ array( 'ORDER BY' => 'ar_timestamp' ) ); if( $dbw->numRows( $result ) < count( $timestamps ) ) { - wfDebug( "$fname: couldn't find all requested rows\n" ); + wfDebug( __METHOD__.": couldn't find all requested rows\n" ); return false; } @@ -355,17 +350,11 @@ class PageArchive { if( $revision ) { # FIXME: Update latest if newer as well... if( $newid ) { - # FIXME: update article count if changed... + // Attach the latest revision to the page... $article->updateRevisionOn( $dbw, $revision, $previousRevId ); - - # Finally, clean up the link tables - $options = new ParserOptions; - $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, - true, true, $newRevId ); - $u = new LinksUpdate( $this->title, $parserOutput ); - $u->doUpdate(); - - #TODO: SearchUpdate, etc. + + // Update site stats, link tables, etc + $article->createUpdates( $revision ); } if( $newid ) { @@ -383,7 +372,7 @@ class PageArchive { 'ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDBkey(), $oldones ), - $fname ); + __METHOD__ ); return $restored; } @@ -401,9 +390,10 @@ class UndeleteForm { function UndeleteForm( &$request, $par = "" ) { global $wgUser; - $this->mAction = $request->getText( 'action' ); - $this->mTarget = $request->getText( 'target' ); - $this->mTimestamp = $request->getText( 'timestamp' ); + $this->mAction = $request->getVal( 'action' ); + $this->mTarget = $request->getVal( 'target' ); + $time = $request->getVal( 'timestamp' ); + $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : ''; $this->mFile = $request->getVal( 'file' ); $posted = $request->wasPosted() && @@ -463,7 +453,6 @@ class UndeleteForm { /* private */ function showList() { global $wgLang, $wgContLang, $wgUser, $wgOut; - $fname = "UndeleteForm::showList"; # List undeletable articles $result = PageArchive::listAllPages(); @@ -479,14 +468,10 @@ class UndeleteForm { $undelete =& Title::makeTitle( NS_SPECIAL, 'Undelete' ); $wgOut->addHTML( "<ul>\n" ); while( $row = $result->fetchObject() ) { - $n = ($row->ar_namespace ? - ($wgContLang->getNsText( $row->ar_namespace ) . ":") : ""). - $row->ar_title; - $link = $sk->makeKnownLinkObj( $undelete, - htmlspecialchars( $n ), "target=" . urlencode( $n ) ); - $revisions = htmlspecialchars( wfMsg( "undeleterevisions", - $wgLang->formatNum( $row->count ) ) ); - $wgOut->addHTML( "<li>$link ($revisions)</li>\n" ); + $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); + $link = $sk->makeKnownLinkObj( $undelete, htmlspecialchars( $title->getPrefixedText() ), 'target=' . $title->getPrefixedUrl() ); + $revs = wfMsgHtml( 'undeleterevisions', $wgLang->formatNum( $row->count ) ); + $wgOut->addHtml( "<li>{$link} ({$revs})</li>\n" ); } $result->free(); $wgOut->addHTML( "</ul>\n" ); @@ -496,11 +481,10 @@ class UndeleteForm { /* private */ function showRevision( $timestamp ) { global $wgLang, $wgUser, $wgOut; - $fname = "UndeleteForm::showRevision"; if(!preg_match("/[0-9]{14}/",$timestamp)) return 0; - $archive =& new PageArchive( $this->mTargetObj ); + $archive = new PageArchive( $this->mTargetObj ); $text = $archive->getRevisionText( $timestamp ); $wgOut->setPagetitle( wfMsg( "undeletepage" ) ); @@ -619,8 +603,7 @@ class UndeleteForm { # Show relevant lines from the deletion log: $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" ); - require_once( 'SpecialLog.php' ); - $logViewer =& new LogViewer( + $logViewer = new LogViewer( new LogReader( new FauxRequest( array( 'page' => $this->mTargetObj->getPrefixedText(), diff --git a/includes/SpecialUnlockdb.php b/includes/SpecialUnlockdb.php index a10d1ee0..6627f75f 100644 --- a/includes/SpecialUnlockdb.php +++ b/includes/SpecialUnlockdb.php @@ -11,10 +11,11 @@ function wfSpecialUnlockdb() { global $wgUser, $wgOut, $wgRequest; - if ( ! $wgUser->isAllowed('siteadmin') ) { - $wgOut->developerRequired(); + if( !$wgUser->isAllowed( 'siteadmin' ) ) { + $wgOut->permissionRequired( 'siteadmin' ); return; } + $action = $wgRequest->getVal( 'action' ); $f = new DBUnlockForm(); @@ -38,6 +39,12 @@ class DBUnlockForm { { global $wgOut, $wgUser; + global $wgReadOnlyFile; + if( !file_exists( $wgReadOnlyFile ) ) { + $wgOut->addWikiText( wfMsg( 'databasenotlocked' ) ); + return; + } + $wgOut->setPagetitle( wfMsg( "unlockdb" ) ); $wgOut->addWikiText( wfMsg( "unlockdbtext" ) ); diff --git a/includes/SpecialUpload.php b/includes/SpecialUpload.php index 06336df9..ade58056 100644 --- a/includes/SpecialUpload.php +++ b/includes/SpecialUpload.php @@ -5,10 +5,7 @@ * @subpackage SpecialPage */ -/** - * - */ -require_once 'Image.php'; + /** * Entry point */ @@ -30,7 +27,7 @@ class UploadForm { var $mUploadFile, $mUploadDescription, $mLicense ,$mIgnoreWarning, $mUploadError; var $mUploadSaveName, $mUploadTempName, $mUploadSize, $mUploadOldVersion; var $mUploadCopyStatus, $mUploadSource, $mReUpload, $mAction, $mUpload; - var $mOname, $mSessionKey, $mStashed, $mDestFile, $mRemoveTempFile; + var $mOname, $mSessionKey, $mStashed, $mDestFile, $mRemoveTempFile, $mSourceType; /**#@-*/ /** @@ -39,6 +36,7 @@ class UploadForm { * @param $request Data posted. */ function UploadForm( &$request ) { + global $wgAllowCopyUploads; $this->mDestFile = $request->getText( 'wpDestFile' ); if( !$request->wasPosted() ) { @@ -55,6 +53,7 @@ class UploadForm { $this->mUploadCopyStatus = $request->getText( 'wpUploadCopyStatus' ); $this->mUploadSource = $request->getText( 'wpUploadSource' ); $this->mWatchthis = $request->getBool( 'wpWatchthis' ); + $this->mSourceType = $request->getText( 'wpSourceType' ); wfDebug( "UploadForm: watchthis is: '$this->mWatchthis'\n" ); $this->mAction = $request->getVal( 'action' ); @@ -79,17 +78,113 @@ class UploadForm { /** *Check for a newly uploaded file. */ - $this->mUploadTempName = $request->getFileTempName( 'wpUploadFile' ); - $this->mUploadSize = $request->getFileSize( 'wpUploadFile' ); - $this->mOname = $request->getFileName( 'wpUploadFile' ); - $this->mUploadError = $request->getUploadError( 'wpUploadFile' ); - $this->mSessionKey = false; - $this->mStashed = false; - $this->mRemoveTempFile = false; // PHP will handle this + if( $wgAllowCopyUploads && $this->mSourceType == 'web' ) { + $this->initializeFromUrl( $request ); + } else { + $this->initializeFromUpload( $request ); + } } } /** + * Initialize the uploaded file from PHP data + * @access private + */ + function initializeFromUpload( $request ) { + $this->mUploadTempName = $request->getFileTempName( 'wpUploadFile' ); + $this->mUploadSize = $request->getFileSize( 'wpUploadFile' ); + $this->mOname = $request->getFileName( 'wpUploadFile' ); + $this->mUploadError = $request->getUploadError( 'wpUploadFile' ); + $this->mSessionKey = false; + $this->mStashed = false; + $this->mRemoveTempFile = false; // PHP will handle this + } + + /** + * Copy a web file to a temporary file + * @access private + */ + function initializeFromUrl( $request ) { + global $wgTmpDirectory, $wgMaxUploadSize; + $url = $request->getText( 'wpUploadFileURL' ); + $local_file = tempnam( $wgTmpDirectory, 'WEBUPLOAD' ); + + $this->mUploadTempName = $local_file; + $this->mUploadError = $this->curlCopy( $url, $local_file ); + $this->mUploadSize = $this->mUploadTempFileSize; + $this->mOname = array_pop( explode( '/', $url ) ); + $this->mSessionKey = false; + $this->mStashed = false; + + // PHP won't auto-cleanup the file + $this->mRemoveTempFile = file_exists( $local_file ); + } + + /** + * Safe copy from URL + * Returns true if there was an error, false otherwise + */ + private function curlCopy( $url, $dest ) { + global $wgMaxUploadSize, $wgUser; + + if( !$wgUser->isAllowed( 'upload_by_url' ) ) { + $wgOut->permissionRequired( 'upload_by_url' ); + return true; + } + + # Maybe remove some pasting blanks :-) + $url = strtolower( trim( $url ) ); + if( substr( $url, 0, 7 ) != 'http://' && substr( $url, 0, 6 ) != 'ftp://' ) { + # Only HTTP or FTP URLs + return true; + } + + # Open temporary file + $this->mUploadTempFileSize = 0; + $this->mUploadTempFile = @fopen( $this->mUploadTempName, "wb" ); + if( $this->mUploadTempFile === false ) { + # Could not open temporary file to write in + return true; + } + + $ch = curl_init(); + curl_setopt( $ch, CURLOPT_HTTP_VERSION, 1.0); # Probably not needed, but apparently can work around some bug + curl_setopt( $ch, CURLOPT_TIMEOUT, 10); # 10 seconds timeout + curl_setopt( $ch, CURLOPT_LOW_SPEED_LIMIT, 512); # 0.5KB per second minimum transfer speed + curl_setopt( $ch, CURLOPT_URL, $url); + curl_setopt( $ch, CURLOPT_WRITEFUNCTION, array( $this, 'uploadCurlCallback' ) ); + curl_exec( $ch ); + $error = curl_errno( $ch ) ? true : false; +# if ( $error ) print curl_error ( $ch ) ; # Debugging output + curl_close( $ch ); + + fclose( $this->mUploadTempFile ); + unset( $this->mUploadTempFile ); + if( $error ) { + unlink( $dest ); + } + + return $error; + } + + /** + * Callback function for CURL-based web transfer + * Write data to file unless we've passed the length limit; + * if so, abort immediately. + * @access private + */ + function uploadCurlCallback( $ch, $data ) { + global $wgMaxUploadSize; + $length = strlen( $data ); + $this->mUploadTempFileSize += $length; + if( $this->mUploadTempFileSize > $wgMaxUploadSize ) { + return 0; + } + fwrite( $this->mUploadTempFile, $data ); + return $length; + } + + /** * Start doing stuff * @access public */ @@ -104,13 +199,12 @@ class UploadForm { } # Check permissions - if( $wgUser->isLoggedIn() ) { - if( !$wgUser->isAllowed( 'upload' ) ) { + if( !$wgUser->isAllowed( 'upload' ) ) { + if( !$wgUser->isLoggedIn() ) { + $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); + } else { $wgOut->permissionRequired( 'upload' ); - return; } - } else { - $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); return; } @@ -126,17 +220,17 @@ class UploadForm { } /** Check if the image directory is writeable, this is a common mistake */ - if ( !is_writeable( $wgUploadDirectory ) ) { + if( !is_writeable( $wgUploadDirectory ) ) { $wgOut->addWikiText( wfMsg( 'upload_directory_read_only', $wgUploadDirectory ) ); return; } if( $this->mReUpload ) { - if ( !$this->unsaveUploadedFile() ) { + if( !$this->unsaveUploadedFile() ) { return; } $this->mainUploadForm(); - } else if ( 'submit' == $this->mAction || $this->mUpload ) { + } else if( 'submit' == $this->mAction || $this->mUpload ) { $this->processUpload(); } else { $this->mainUploadForm(); @@ -156,7 +250,7 @@ class UploadForm { global $wgUser, $wgOut; /* Check for PHP error if any, requires php 4.2 or newer */ - if ( $this->mUploadError == 1/*UPLOAD_ERR_INI_SIZE*/ ) { + if( $this->mUploadError == 1/*UPLOAD_ERR_INI_SIZE*/ ) { $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) ); return; } @@ -170,7 +264,7 @@ class UploadForm { } # Chop off any directories in the given filename - if ( $this->mDestFile ) { + if( $this->mDestFile ) { $basename = wfBaseName( $this->mDestFile ); } else { $basename = wfBaseName( $this->mOname ); @@ -196,7 +290,7 @@ class UploadForm { $partname .= '.' . $ext[$i]; } - if ( strlen( $partname ) < 3 ) { + if( strlen( $partname ) < 3 ) { $this->mainUploadForm( wfMsgHtml( 'minlength' ) ); return; } @@ -363,7 +457,9 @@ class UploadForm { * is a PHP-managed upload temporary */ function saveUploadedFile( $saveName, $tempName, $useRename = false ) { - global $wgOut; + global $wgOut, $wgAllowCopyUploads; + + if ( !$useRename AND $wgAllowCopyUploads AND $this->mSourceType == 'web' ) $useRename = true; $fname= "SpecialUpload::saveUploadedFile"; @@ -586,6 +682,7 @@ class UploadForm { function mainUploadForm( $msg='' ) { global $wgOut, $wgUser; global $wgUseCopyrightUpload; + global $wgRequest, $wgAllowCopyUploads; $cols = intval($wgUser->getOption( 'cols' )); $ew = $wgUser->getOption( 'editwidth' ); @@ -624,13 +721,33 @@ class UploadForm { ? 'checked="checked"' : ''; + // Prepare form for upload or upload/copy + if( $wgAllowCopyUploads && $wgUser->isAllowed( 'upload_by_url' ) ) { + $source_comment = wfMsgHtml( 'upload_source_url' ); + $filename_form = + "<input type='radio' id='wpSourceTypeFile' name='wpSourceType' value='file' onchange='toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\")' checked />" . + "<input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' onfocus='toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\");toggle_element_check(\"wpSourceTypeFile\",\"wpSourceTypeURL\")'" . + ($this->mDestFile?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") . "size='40' />" . + wfMsgHTML( 'upload_source_file' ) . "<br/>" . + "<input type='radio' id='wpSourceTypeURL' name='wpSourceType' value='web' onchange='toggle_element_activation(\"wpUploadFile\",\"wpUploadFileURL\")' />" . + "<input tabindex='1' type='text' name='wpUploadFileURL' id='wpUploadFileURL' onfocus='toggle_element_activation(\"wpUploadFile\",\"wpUploadFileURL\");toggle_element_check(\"wpSourceTypeURL\",\"wpSourceTypeFile\")'" . + ($this->mDestFile?"":"onchange='fillDestFilename(\"wpUploadFileURL\")' ") . "size='40' DISABLED />" . + wfMsgHtml( 'upload_source_url' ) ; + } else { + $filename_form = + "<input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " . + ($this->mDestFile?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") . + "size='40' />" . + "<input type='hidden' name='wpSourceType' value='file' />" ; + } + $wgOut->addHTML( " <form id='upload' method='post' enctype='multipart/form-data' action=\"$action\"> <table border='0'> <tr> - <td align='right'><label for='wpUploadFile'>{$sourcefilename}:</label></td> + <td align='right' valign='top'><label for='wpUploadFile'>{$sourcefilename}:</label></td> <td align='left'> - <input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " . ($this->mDestFile?"":"onchange='fillDestFilename()' ") . "size='40' /> + {$filename_form} </td> </tr> <tr> @@ -687,7 +804,7 @@ class UploadForm { <td></td> <td> <input tabindex='7' type='checkbox' name='wpWatchthis' id='wpWatchthis' $watchChecked value='true' /> - <label for='wpWatchthis'>" . wfMsgHtml( 'watchthis' ) . "</label> + <label for='wpWatchthis'>" . wfMsgHtml( 'watchthisupload' ) . "</label> <input tabindex='8' type='checkbox' name='wpIgnoreWarning' id='wpIgnoreWarning' value='true' /> <label for='wpIgnoreWarning'>" . wfMsgHtml( 'ignorewarnings' ) . "</label> </td> @@ -767,7 +884,7 @@ class UploadForm { */ function verify( $tmpfile, $extension ) { #magically determine mime type - $magic=& wfGetMimeMagic(); + $magic=& MimeMagic::singleton(); $mime= $magic->guessMimeType($tmpfile,false); $fname= "SpecialUpload::verify"; @@ -816,7 +933,7 @@ class UploadForm { function verifyExtension( $mime, $extension ) { $fname = 'SpecialUpload::verifyExtension'; - $magic =& wfGetMimeMagic(); + $magic =& MimeMagic::singleton(); if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) if ( ! $magic->isRecognizableExtension( $extension ) ) { @@ -1106,4 +1223,6 @@ class UploadForm { } } + + ?> diff --git a/includes/SpecialUploadMogile.php b/includes/SpecialUploadMogile.php index 51a6dd28..05bfca08 100644 --- a/includes/SpecialUploadMogile.php +++ b/includes/SpecialUploadMogile.php @@ -8,7 +8,6 @@ /** * */ -require_once( 'SpecialUpload.php' ); require_once( 'MogileFS.php' ); /** diff --git a/includes/SpecialUserlogin.php b/includes/SpecialUserlogin.php index 4ee35b1b..574579cc 100644 --- a/includes/SpecialUserlogin.php +++ b/includes/SpecialUserlogin.php @@ -24,7 +24,17 @@ function wfSpecialUserlogin() { * @package MediaWiki * @subpackage SpecialPage */ + class LoginForm { + + const SUCCESS = 0; + const NO_NAME = 1; + const ILLEGAL = 2; + const WRONG_PLUGIN_PASS = 3; + const NOT_EXISTS = 4; + const WRONG_PASS = 5; + const EMPTY_PASS = 6; + var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted; var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword; var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage; @@ -185,8 +195,8 @@ class LoginForm { function addNewAccountInternal() { global $wgUser, $wgOut; global $wgEnableSorbs, $wgProxyWhitelist; - global $wgMemc, $wgAccountCreationThrottle, $wgDBname; - global $wgAuth, $wgMinimalPasswordLength, $wgReservedUsernames; + global $wgMemc, $wgAccountCreationThrottle; + global $wgAuth, $wgMinimalPasswordLength; // If the user passes an invalid domain, something is fishy if( !$wgAuth->validDomain( $this->mDomain ) ) { @@ -227,7 +237,7 @@ class LoginForm { $name = trim( $this->mName ); $u = User::newFromName( $name ); - if ( is_null( $u ) || in_array( $u->getName(), $wgReservedUsernames ) ) { + if ( is_null( $u ) || !User::isCreatableName( $u->getName() ) ) { $this->mainLoginForm( wfMsg( 'noname' ) ); return false; } @@ -248,7 +258,7 @@ class LoginForm { } if ( $wgAccountCreationThrottle ) { - $key = $wgDBname.':acctcreate:ip:'.$ip; + $key = wfMemcKey( 'acctcreate', 'ip', $ip ); $value = $wgMemc->incr( $key ); if ( !$value ) { $wgMemc->set( $key, 1, 86400 ); @@ -305,17 +315,16 @@ class LoginForm { /** * @private */ - function processLogin() { - global $wgUser, $wgAuth, $wgReservedUsernames; - + + function authenticateUserData() + { + global $wgUser, $wgAuth; if ( '' == $this->mName ) { - $this->mainLoginForm( wfMsg( 'noname' ) ); - return; + return self::NO_NAME; } $u = User::newFromName( $this->mName ); - if( is_null( $u ) || in_array( $u->getName(), $wgReservedUsernames ) ) { - $this->mainLoginForm( wfMsg( 'noname' ) ); - return; + if( is_null( $u ) || !User::isUsableName( $u->getName() ) ) { + return self::ILLEGAL; } if ( 0 == $u->getID() ) { global $wgAuth; @@ -328,42 +337,67 @@ class LoginForm { if ( $wgAuth->authenticate( $u->getName(), $this->mPassword ) ) { $u =& $this->initUser( $u ); } else { - $this->mainLoginForm( wfMsg( 'wrongpassword' ) ); - return; + return self::WRONG_PLUGIN_PASS; } } else { - $this->mainLoginForm( wfMsg( 'nosuchuser', $u->getName() ) ); - return; + return self::NOT_EXISTS; } } else { $u->loadFromDatabase(); } if (!$u->checkPassword( $this->mPassword )) { - $this->mainLoginForm( wfMsg( $this->mPassword == '' ? 'wrongpasswordempty' : 'wrongpassword' ) ); - return; + return '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS; } + else + { + $wgAuth->updateUser( $u ); + $wgUser = $u; - # We've verified now, update the real record - # - if ( $this->mRemember ) { - $r = 1; - } else { - $r = 0; + return self::SUCCESS; } - $u->setOption( 'rememberpassword', $r ); - - $wgAuth->updateUser( $u ); + } + + function processLogin() { + global $wgUser, $wgAuth; - $wgUser = $u; - $wgUser->setCookies(); + switch ($this->authenticateUserData()) + { + case self::SUCCESS: + # We've verified now, update the real record + if( (bool)$this->mRemember != (bool)$wgUser->getOption( 'rememberpassword' ) ) { + $wgUser->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 ); + $wgUser->saveSettings(); + } else { + $wgUser->invalidateCache(); + } + $wgUser->setCookies(); - $wgUser->saveSettings(); + if( $this->hasSessionCookie() ) { + return $this->successfulLogin( wfMsg( 'loginsuccess', $wgUser->getName() ) ); + } else { + return $this->cookieRedirectCheck( 'login' ); + } + break; - if( $this->hasSessionCookie() ) { - return $this->successfulLogin( wfMsg( 'loginsuccess', $wgUser->getName() ) ); - } else { - return $this->cookieRedirectCheck( 'login' ); + case self::NO_NAME: + case self::ILLEGAL: + $this->mainLoginForm( wfMsg( 'noname' ) ); + break; + case self::WRONG_PLUGIN_PASS: + $this->mainLoginForm( wfMsg( 'wrongpassword' ) ); + break; + case self::NOT_EXISTS: + $this->mainLoginForm( wfMsg( 'nosuchuser', htmlspecialchars( $this->mName ) ) ); + break; + case self::WRONG_PASS: + $this->mainLoginForm( wfMsg( 'wrongpassword' ) ); + break; + case self::EMPTY_PASS: + $this->mainLoginForm( wfMsg( 'wrongpasswordempty' ) ); + break; + default: + wfDebugDieBacktrace( "Unhandled case value" ); } } @@ -413,7 +447,7 @@ class LoginForm { global $wgServer, $wgScript; if ( '' == $u->getEmail() ) { - return wfMsg( 'noemail', $u->getName() ); + return new WikiError( wfMsg( 'noemail', $u->getName() ) ); } $np = $u->randomPassword(); @@ -470,6 +504,27 @@ class LoginForm { $wgOut->returnToMain( false ); } + /** */ + function userBlockedMessage() { + global $wgOut; + + # Let's be nice about this, it's likely that this feature will be used + # for blocking large numbers of innocent people, e.g. range blocks on + # schools. Don't blame it on the user. There's a small chance that it + # really is the user's fault, i.e. the username is blocked and they + # haven't bothered to log out before trying to create an account to + # evade it, but we'll leave that to their guilty conscience to figure + # out. + + $wgOut->setPageTitle( wfMsg( 'cantcreateaccounttitle' ) ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setArticleRelated( false ); + + $ip = wfGetIP(); + $wgOut->addWikiText( wfMsg( 'cantcreateaccounttext', $ip ) ); + $wgOut->returnToMain( false ); + } + /** * @private */ @@ -477,9 +532,14 @@ class LoginForm { global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail; global $wgCookiePrefix, $wgAuth, $wgLoginLanguageSelector; - if ( $this->mType == 'signup' && !$wgUser->isAllowedToCreateAccount() ) { - $this->userNotPrivilegedMessage(); - return; + if ( $this->mType == 'signup' ) { + if ( !$wgUser->isAllowed( 'createaccount' ) ) { + $this->userNotPrivilegedMessage(); + return; + } elseif ( $wgUser->isBlockedFromCreateAccount() ) { + $this->userBlockedMessage(); + return; + } } if ( '' == $this->mName ) { @@ -492,16 +552,13 @@ class LoginForm { $titleObj = Title::makeTitle( NS_SPECIAL, 'Userlogin' ); - require_once( 'SkinTemplate.php' ); - require_once( 'templates/Userlogin.php' ); - if ( $this->mType == 'signup' ) { - $template =& new UsercreateTemplate(); + $template = new UsercreateTemplate(); $q = 'action=submitlogin&type=signup'; $linkq = 'type=login'; $linkmsg = 'gotaccount'; } else { - $template =& new UserloginTemplate(); + $template = new UserloginTemplate(); $q = 'action=submitlogin&type=login'; $linkq = 'type=signup'; $linkmsg = 'nologin'; @@ -570,7 +627,7 @@ class LoginForm { function showCreateOrLoginLink( &$user ) { if( $this->mType == 'signup' ) { return( true ); - } elseif( $user->isAllowedToCreateAccount() ) { + } elseif( $user->isAllowed( 'createaccount' ) ) { return( true ); } else { return( false ); @@ -634,7 +691,7 @@ class LoginForm { */ function makeLanguageSelector() { $msg = wfMsgForContent( 'loginlanguagelinks' ); - if( $msg != '' && $msg != '<loginlanguagelinks>' ) { + if( $msg != '' && !wfEmptyMsg( 'loginlanguagelinks', $msg ) ) { $langs = explode( "\n", $msg ); $links = array(); foreach( $langs as $lang ) { diff --git a/includes/SpecialUserrights.php b/includes/SpecialUserrights.php index 8f43092c..b17cc4aa 100644 --- a/includes/SpecialUserrights.php +++ b/includes/SpecialUserrights.php @@ -165,7 +165,7 @@ class UserrightsForm extends HTMLForm { 'name' => 'wpEditToken', 'value' => $wgUser->editToken( $username ) ) ) . $this->fieldset( 'editusergroup', - $wgOut->parse( wfMsg('editing', $username ) ) . + $wgOut->parse( wfMsg('editinguser', $username ) ) . '<table border="0" align="center"><tr><td>'. HTMLSelectGroups('member', $this->mName.'-groupsmember', $groups,true,6). '</td><td>'. diff --git a/includes/SpecialVersion.php b/includes/SpecialVersion.php index 5f7e857f..8744597a 100644 --- a/includes/SpecialVersion.php +++ b/includes/SpecialVersion.php @@ -45,7 +45,7 @@ class SpecialVersion { * @static */ function MediaWikiCredits() { - $version = $this->getVersion(); + $version = self::getVersion(); $dbr =& wfGetDB( DB_SLAVE ); $ret = @@ -77,9 +77,9 @@ class SpecialVersion { return str_replace( "\t\t", '', $ret ); } - function getVersion() { + public static function getVersion() { global $wgVersion, $IP; - $svn = $this->getSvnRevision( $IP ); + $svn = self::getSvnRevision( $IP ); return $svn ? "$wgVersion (r$svn)" : $wgVersion; } @@ -129,6 +129,11 @@ class SpecialVersion { $out .= "** Parser extension tags:\n"; $out .= '***' . $this->listToText( $tags ). "\n"; } + + if( $cnt = count( $fhooks = $wgParser->getFunctionHooks() ) ) { + $out .= "** Parser function hooks:\n"; + $out .= '***' . $this->listToText( $fhooks ) . "\n"; + } if ( count( $wgSkinExtensionFunction ) ) { $out .= "** Skin extension functions:\n"; @@ -142,7 +147,7 @@ class SpecialVersion { if ( $a['name'] === $b['name'] ) return 0; else - return LanguageUtf8::lc( $a['name'] ) > LanguageUtf8::lc( $b['name'] ) ? 1 : -1; + return Language::lc( $a['name'] ) > Language::lc( $b['name'] ) ? 1 : -1; } function formatCredits( $name, $version = null, $author = null, $url = null, $description = null) { @@ -232,34 +237,51 @@ class SpecialVersion { /** * Retrieve the revision number of a Subversion working directory. + * + * @bug 7335 * * @param string $dir * @return mixed revision number as int, or false if not a SVN checkout */ - function getSvnRevision( $dir ) { - if( !function_exists( 'simplexml_load_file' ) ) { - // We could fall back to expat... YUCK - return false; - } - + public static function getSvnRevision( $dir ) { // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html $entries = $dir . '/.svn/entries'; - - // SimpleXml whines about the xmlns... - wfSuppressWarnings(); - $xml = simplexml_load_file( $entries ); - wfRestoreWarnings(); - - if( $xml ) { - foreach( $xml->entry as $entry ) { - if( $xml->entry[0]['name'] == '' ) { - // The directory entry should always have a revision marker. - if( $entry['revision'] ) { - return intval( $entry['revision'] ); + + if( !file_exists( $entries ) ) { + return false; + } + + $content = file( $entries ); + + // check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4) + if( preg_match( '/^<\?xml/', $content[0] ) ) { + // subversion is release <= 1.3 + if( !function_exists( 'simplexml_load_file' ) ) { + // We could fall back to expat... YUCK + return false; + } + + // SimpleXml whines about the xmlns... + wfSuppressWarnings(); + $xml = simplexml_load_file( $entries ); + wfRestoreWarnings(); + + if( $xml ) { + foreach( $xml->entry as $entry ) { + if( $xml->entry[0]['name'] == '' ) { + // The directory entry should always have a revision marker. + if( $entry['revision'] ) { + return intval( $entry['revision'] ); + } } } } + return false; + } else { + // subversion is release 1.4 + return intval( $content[3] ); } + return false; } diff --git a/includes/SpecialWantedcategories.php b/includes/SpecialWantedcategories.php index 8e75953a..97bb0a26 100644 --- a/includes/SpecialWantedcategories.php +++ b/includes/SpecialWantedcategories.php @@ -34,7 +34,7 @@ class WantedCategoriesPage extends QueryPage { FROM $categorylinks LEFT JOIN $page ON cl_to = page_title AND page_namespace = ". NS_CATEGORY ." WHERE page_title IS NULL - GROUP BY cl_to + GROUP BY 1,2,3 "; } diff --git a/includes/SpecialWantedpages.php b/includes/SpecialWantedpages.php index 8bbe49cb..7b070604 100644 --- a/includes/SpecialWantedpages.php +++ b/includes/SpecialWantedpages.php @@ -46,7 +46,7 @@ class WantedPagesPage extends QueryPage { WHERE pg1.page_namespace IS NULL AND pl_namespace NOT IN ( 2, 3 ) AND pg2.page_namespace != 8 - GROUP BY pl_namespace, pl_title + GROUP BY 1,2,3 HAVING COUNT(*) > $count"; } diff --git a/includes/SpecialWatchlist.php b/includes/SpecialWatchlist.php index 5b1e2890..87c925ac 100644 --- a/includes/SpecialWatchlist.php +++ b/includes/SpecialWatchlist.php @@ -17,7 +17,7 @@ require_once( 'SpecialRecentchanges.php' ); */ function wfSpecialWatchlist( $par ) { global $wgUser, $wgOut, $wgLang, $wgMemc, $wgRequest, $wgContLang; - global $wgUseWatchlistCache, $wgWLCacheTimeout, $wgDBname; + global $wgUseWatchlistCache, $wgWLCacheTimeout; global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker; global $wgEnotifWatchlist; $fname = 'wfSpecialWatchlist'; @@ -90,20 +90,20 @@ function wfSpecialWatchlist( $par ) { if( !is_null( $t ) ) { $wl = WatchedItem::fromUserTitle( $wgUser, $t ); if( $wl->removeWatch() === false ) { - $wgOut->addHTML( "<br />\n" . wfMsg( 'couldntremove', htmlspecialchars($one) ) ); + $wgOut->addHTML( wfMsg( 'couldntremove', htmlspecialchars($one) ) . "<br />\n" ); } else { wfRunHooks('UnwatchArticle', array(&$wgUser, new Article($t))); - $wgOut->addHTML( ' (' . htmlspecialchars($one) . ')' ); + $wgOut->addHTML( '(' . htmlspecialchars($one) . ')<br />' ); } } else { - $wgOut->addHTML( "<br />\n" . wfMsg( 'iteminvalidname', htmlspecialchars($one) ) ); + $wgOut->addHTML( wfMsg( 'iteminvalidname', htmlspecialchars($one) ) . "<br />\n" ); } } - $wgOut->addHTML( "<br />\n" . wfMsg( 'wldone' ) . "</p>\n" ); + $wgOut->addHTML( "</p>\n<p>" . wfMsg( 'wldone' ) . "</p>\n" ); } if ( $wgUseWatchlistCache ) { - $memckey = "$wgDBname:watchlist:id:" . $wgUser->getId(); + $memckey = wfMemcKey( 'watchlist', 'id', $wgUser->getId() ); $cache_s = @$wgMemc->get( $memckey ); if( $cache_s ){ $wgOut->addWikiText( wfMsg('wlsaved') ); @@ -235,9 +235,8 @@ function wfSpecialWatchlist( $par ) { $wgOut->addHTML( '</ul>' ); } $wgOut->addHTML( - "<input type='submit' name='remove' value=\"" . - htmlspecialchars( wfMsg( "removechecked" ) ) . "\" />\n" . - "</form>\n" + wfSubmitButton( wfMsg('removechecked'), array('name' => 'remove') ) . + "\n</form>\n" ); return; @@ -274,7 +273,7 @@ function wfSpecialWatchlist( $par ) { } # TODO: Consider removing the third parameter - $header .= wfMsg( 'watchdetails', $wgLang->formatNum( $nitems ), + $header .= wfMsgExt( 'watchdetails', array( 'parsemag' ), $wgLang->formatNum( $nitems ), $wgLang->formatNum( $npages ), '', $specialTitle->getFullUrl( 'edit=yes' ) ); $wgOut->addWikiText( $header ); @@ -313,14 +312,15 @@ function wfSpecialWatchlist( $par ) { $numRows = $dbr->numRows( $res ); /* Start bottom header */ - $wgOut->addHTML( "<hr />\n<p>" ); + $wgOut->addHTML( "<hr />\n" ); - if($days >= 1) + if($days >= 1) { $wgOut->addWikiText( wfMsg( 'rcnote', $wgLang->formatNum( $numRows ), $wgLang->formatNum( $days ), $wgLang->timeAndDate( wfTimestampNow(), true ) ) . '<br />' , false ); - elseif($days > 0) + } elseif($days > 0) { $wgOut->addWikiText( wfMsg( 'wlnote', $wgLang->formatNum( $numRows ), $wgLang->formatNum( round($days*24) ) ) . '<br />' , false ); + } $wgOut->addHTML( "\n" . wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n" ); @@ -343,24 +343,25 @@ function wfSpecialWatchlist( $par ) { $wgOut->addHTML( implode( ' | ', $links ) ); # Form for namespace filtering - $thisAction = $thisTitle->escapeLocalUrl(); - $nsForm = "<form method=\"post\" action=\"{$thisAction}\">\n"; - $nsForm .= "<label for=\"namespace\">" . wfMsgExt( 'namespace', array( 'parseinline') ) . "</label> "; - $nsForm .= HTMLnamespaceselector( $nameSpace, '' ) . "\n"; - $nsForm .= ( $hideOwn ? "<input type=\"hidden\" name=\"hideown\" value=\"1\" />\n" : "" ); - $nsForm .= ( $hideBots ? "<input type=\"hidden\" name=\"hidebots\" value=\"1\" />\n" : "" ); - $nsForm .= "<input type=\"hidden\" name=\"days\" value=\"" . $days . "\" />\n"; - $nsForm .= "<input type=\"submit\" name=\"submit\" value=\"" . wfMsgExt( 'allpagessubmit', array( 'escape') ) . "\" />\n"; - $nsForm .= "</form>\n"; - $wgOut->addHTML( $nsForm ); + $wgOut->addHTML( "\n" . + wfOpenElement( 'form', array( + 'method' => 'post', + 'action' => $thisTitle->getLocalURL(), + ) ) . + wfMsgExt( 'namespace', array( 'parseinline') ) . + HTMLnamespaceselector( $nameSpace, '' ) . "\n" . + ( $hideOwn ? wfHidden('hideown', 1)."\n" : '' ) . + ( $hideBots ? wfHidden('hidebots', 1)."\n" : '' ) . + wfHidden( 'days', $days ) . "\n" . + wfSubmitButton( wfMsgExt( 'allpagessubmit', array( 'escape') ) ) . "\n" . + wfCloseElement( 'form' ) . "\n" + ); if ( $numRows == 0 ) { $wgOut->addWikitext( "<br />" . wfMsg( 'watchnochange' ), false ); - $wgOut->addHTML( "</p>\n" ); return; } - $wgOut->addHTML( "</p>\n" ); /* End bottom header */ $list = ChangesList::newFromUser( $wgUser ); @@ -475,6 +476,8 @@ function wlCountItems( &$user, $talk = true ) { * code needs to do something further */ function wlHandleClear( &$out, &$request, $par ) { + global $wgLang; + # Check this function has something to do if( $request->getText( 'action' ) == 'clear' || $par == 'clear' ) { global $wgUser; @@ -486,17 +489,19 @@ function wlHandleClear( &$out, &$request, $par ) { # Clearing, so do it and report the result $dbw =& wfGetDB( DB_MASTER ); $dbw->delete( 'watchlist', array( 'wl_user' => $wgUser->mId ), 'wlHandleClear' ); - $out->addWikiText( wfMsg( 'watchlistcleardone', $count ) ); + $out->addWikiText( wfMsgExt( 'watchlistcleardone', array( 'parsemag', 'escape'), $wgLang->formatNum( $count ) ) ); $out->returnToMain(); } else { # Confirming, so show a form $wlTitle = Title::makeTitle( NS_SPECIAL, 'Watchlist' ); $out->addHTML( wfElement( 'form', array( 'method' => 'post', 'action' => $wlTitle->getLocalUrl( 'action=clear' ) ), NULL ) ); - $out->addWikiText( wfMsg( 'watchlistcount', $count ) ); + $out->addWikiText( wfMsgExt( 'watchlistcount', array( 'parsemag', 'escape'), $wgLang->formatNum( $count ) ) ); $out->addWikiText( wfMsg( 'watchlistcleartext' ) ); - $out->addHTML( wfElement( 'input', array( 'type' => 'hidden', 'name' => 'token', 'value' => $wgUser->editToken( 'clearwatchlist' ) ), '' ) ); - $out->addHTML( wfElement( 'input', array( 'type' => 'submit', 'name' => 'submit', 'value' => wfMsgHtml( 'watchlistclearbutton' ) ), '' ) ); - $out->addHTML( wfCloseElement( 'form' ) ); + $out->addHTML( + wfHidden( 'token', $wgUser->editToken( 'clearwatchlist' ) ) . + wfElement( 'input', array( 'type' => 'submit', 'name' => 'submit', 'value' => wfMsgHtml( 'watchlistclearbutton' ) ), '' ) . + wfCloseElement( 'form' ) + ); } return( true ); } else { @@ -509,5 +514,4 @@ function wlHandleClear( &$out, &$request, $par ) { return( false ); } } - ?> diff --git a/includes/SpecialWhatlinkshere.php b/includes/SpecialWhatlinkshere.php index cedf6049..a95530fe 100644 --- a/includes/SpecialWhatlinkshere.php +++ b/includes/SpecialWhatlinkshere.php @@ -59,7 +59,7 @@ class WhatLinksHerePage { $isredir = ' (' . wfMsg( 'isredirect' ) . ")\n"; - $wgOut->addHTML('< '.$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n"); + $wgOut->addHTML( wfMsg( 'whatlinkshere-barrow' ) . ' ' .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n"); $this->showIndirectLinks( 0, $this->target, $this->limit, $this->from, $this->dir ); } @@ -128,7 +128,7 @@ class WhatLinksHerePage { if ( !$dbr->numRows( $plRes ) && !$dbr->numRows( $tlRes ) ) { if ( 0 == $level ) { - $wgOut->addWikiText( wfMsg( 'nolinkshere' ) ); + $wgOut->addWikiText( wfMsg( 'nolinkshere', $this->target->getPrefixedText() ) ); } return; } @@ -187,7 +187,7 @@ class WhatLinksHerePage { } if ( 0 == $level ) { - $wgOut->addWikiText( wfMsg( 'linkshere' ) ); + $wgOut->addWikiText( wfMsg( 'linkshere', $this->target->getPrefixedText() ) ); } $isredir = wfMsg( 'isredirect' ); $istemplate = wfMsg( 'istemplate' ); diff --git a/includes/StreamFile.php b/includes/StreamFile.php index 83417185..81538a84 100644 --- a/includes/StreamFile.php +++ b/includes/StreamFile.php @@ -6,10 +6,12 @@ function wfStreamFile( $fname ) { $stat = @stat( $fname ); if ( !$stat ) { header( 'HTTP/1.0 404 Not Found' ); + $encFile = htmlspecialchars( $fname ); + $encScript = htmlspecialchars( $_SERVER['SCRIPT_NAME'] ); echo "<html><body> <h1>File not found</h1> -<p>Although this PHP script ({$_SERVER['SCRIPT_NAME']}) exists, the file requested for output -does not.</p> +<p>Although this PHP script ($encScript) exists, the file requested for output +($encFile) does not.</p> </body></html>"; return; } @@ -64,7 +66,7 @@ function wfGetType( $filename ) { return 'unknown/unknown'; } else { - $magic=& wfGetMimeMagic(); + $magic=& MimeMagic::singleton(); return $magic->guessMimeType($filename); //full fancy mime detection } } diff --git a/includes/StubObject.php b/includes/StubObject.php new file mode 100644 index 00000000..ed3b117a --- /dev/null +++ b/includes/StubObject.php @@ -0,0 +1,130 @@ +<?php + +/** + * Class to implement stub globals, which are globals that delay loading the + * their associated module code by deferring initialisation until the first + * method call. + * + * Note on unstub loops: + * + * Unstub loops (infinite recursion) sometimes occur when a constructor calls + * another function, and the other function calls some method of the stub. The + * best way to avoid this is to make constructors as lightweight as possible, + * deferring any initialisation which depends on other modules. As a last + * resort, you can use StubObject::isRealObject() to break the loop, but as a + * general rule, the stub object mechanism should be transparent, and code + * which refers to it should be kept to a minimum. + */ +class StubObject { + var $mGlobal, $mClass, $mParams; + function __construct( $global = null, $class = null, $params = array() ) { + $this->mGlobal = $global; + $this->mClass = $class; + $this->mParams = $params; + } + + static function isRealObject( $obj ) { + return is_object( $obj ) && !is_a( $obj, 'StubObject' ); + } + + function _call( $name, $args ) { + $this->_unstub( $name, 5 ); + return call_user_func_array( array( $GLOBALS[$this->mGlobal], $name ), $args ); + } + + function _newObject() { + return wfCreateObject( $this->mClass, $this->mParams ); + } + + function __call( $name, $args ) { + return $this->_call( $name, $args ); + } + + /** + * This is public, for the convenience of external callers wishing to access + * properties, e.g. eval.php + */ + function _unstub( $name = '_unstub', $level = 2 ) { + static $recursionLevel = 0; + if ( get_class( $GLOBALS[$this->mGlobal] ) != $this->mClass ) { + $fname = __METHOD__.'-'.$this->mGlobal; + wfProfileIn( $fname ); + $caller = wfGetCaller( $level ); + if ( ++$recursionLevel > 2 ) { + throw new MWException( "Unstub loop detected on call of \${$this->mGlobal}->$name from $caller\n" ); + } + wfDebug( "Unstubbing \${$this->mGlobal} on call of \${$this->mGlobal}->$name from $caller\n" ); + $GLOBALS[$this->mGlobal] = $this->_newObject(); + --$recursionLevel; + wfProfileOut( $fname ); + } + } +} + +class StubContLang extends StubObject { + function __construct() { + parent::__construct( 'wgContLang' ); + } + + function __call( $name, $args ) { + return StubObject::_call( $name, $args ); + } + + function _newObject() { + global $wgContLanguageCode; + $obj = Language::factory( $wgContLanguageCode ); + $obj->initEncoding(); + $obj->initContLang(); + return $obj; + } +} +class StubUserLang extends StubObject { + function __construct() { + parent::__construct( 'wgLang' ); + } + + function __call( $name, $args ) { + return $this->_call( $name, $args ); + } + + function _newObject() { + global $wgContLanguageCode, $wgRequest, $wgUser, $wgContLang; + $code = $wgRequest->getVal('uselang', ''); + if ($code == '') + $code = $wgUser->getOption('language'); + # Validate $code + if( empty( $code ) || !preg_match( '/^[a-z]+(-[a-z]+)?$/', $code ) ) { + $code = $wgContLanguageCode; + } + + if( $code == $wgContLanguageCode ) { + return $wgContLang; + } else { + $obj = Language::factory( $code ); + return $obj; + } + } +} +class StubUser extends StubObject { + function __construct() { + parent::__construct( 'wgUser' ); + } + + function __call( $name, $args ) { + return $this->_call( $name, $args ); + } + + function _newObject() { + global $wgCommandLineMode; + if( $wgCommandLineMode ) { + $user = new User; + $user->setLoaded( true ); + } else { + $user = User::loadFromSession(); + wfRunHooks('AutoAuthenticate',array($user)); + } + return $user; + } +} + +?> diff --git a/includes/Title.php b/includes/Title.php index bc8f69a2..0e86063e 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -108,7 +108,7 @@ class Title { * @static * @access public */ - function newFromText( $text, $defaultNamespace = NS_MAIN ) { + public static function newFromText( $text, $defaultNamespace = NS_MAIN ) { $fname = 'Title::newFromText'; if( is_object( $text ) ) { @@ -132,7 +132,7 @@ class Title { */ $filteredText = Sanitizer::decodeCharReferences( $text ); - $t =& new Title(); + $t = new Title(); $t->mDbkeyform = str_replace( ' ', '_', $filteredText ); $t->mDefaultNamespace = $defaultNamespace; @@ -233,8 +233,8 @@ class Title { * @static * @access public */ - function &makeTitle( $ns, $title ) { - $t =& new Title(); + public static function &makeTitle( $ns, $title ) { + $t = new Title(); $t->mInterwiki = ''; $t->mFragment = ''; $t->mNamespace = intval( $ns ); @@ -246,7 +246,7 @@ class Title { } /** - * Create a new Title frrom a namespace index and a DB key. + * Create a new Title from a namespace index and a DB key. * The parameters will be checked for validity, which is a bit slower * than makeTitle() but safer for user-provided data. * @@ -256,7 +256,7 @@ class Title { * @static * @access public */ - function makeTitleSafe( $ns, $title ) { + public static function makeTitleSafe( $ns, $title ) { $t = new Title(); $t->mDbkeyform = Title::makeName( $ns, $title ); if( $t->secureAndSplit() ) { @@ -273,7 +273,7 @@ class Title { * @return Title the new object * @access public */ - function newMainPage() { + public static function newMainPage() { return Title::newFromText( wfMsgForContent( 'mainpage' ) ); } @@ -285,8 +285,8 @@ class Title { * @static * @access public */ - function newFromRedirect( $text ) { - $mwRedir = MagicWord::get( MAG_REDIRECT ); + public static function newFromRedirect( $text ) { + $mwRedir = MagicWord::get( 'redirect' ); $rt = NULL; if ( $mwRedir->matchStart( $text ) ) { if ( preg_match( '/\[{2}(.*?)(?:\||\]{2})/', $text, $m ) ) { @@ -336,7 +336,7 @@ class Title { * @static * @access public */ - function legalChars() { + public static function legalChars() { global $wgLegalTitleChars; return $wgLegalTitleChars; } @@ -376,7 +376,7 @@ class Title { * @param string $title the DB key form the title * @return string the prefixed form of the title */ - /* static */ function makeName( $ns, $title ) { + public static function makeName( $ns, $title ) { global $wgContLang; $n = $wgContLang->getNsText( $ns ); @@ -392,13 +392,13 @@ class Title { * @access public */ function getInterwikiLink( $key ) { - global $wgMemc, $wgDBname, $wgInterwikiExpiry; + global $wgMemc, $wgInterwikiExpiry; global $wgInterwikiCache; $fname = 'Title::getInterwikiLink'; $key = strtolower( $key ); - $k = $wgDBname.':interwiki:'.$key; + $k = wfMemcKey( 'interwiki', $key ); if( array_key_exists( $k, Title::$interwikiCache ) ) { return Title::$interwikiCache[$k]->iw_url; } @@ -445,18 +445,18 @@ class Title { * @access public */ function getInterwikiCached( $key ) { - global $wgDBname, $wgInterwikiCache, $wgInterwikiScopes, $wgInterwikiFallbackSite; + global $wgInterwikiCache, $wgInterwikiScopes, $wgInterwikiFallbackSite; static $db, $site; if (!$db) $db=dba_open($wgInterwikiCache,'r','cdb'); /* Resolve site name */ if ($wgInterwikiScopes>=3 and !$site) { - $site = dba_fetch("__sites:{$wgDBname}", $db); + $site = dba_fetch('__sites:' . wfWikiID(), $db); if ($site=="") $site = $wgInterwikiFallbackSite; } - $value = dba_fetch("{$wgDBname}:{$key}", $db); + $value = dba_fetch( wfMemcKey( $key ), $db); if ($value=='' and $wgInterwikiScopes>=3) { /* try site-level */ $value = dba_fetch("_{$site}:{$key}", $db); @@ -476,7 +476,7 @@ class Title { $s->iw_url=$url; $s->iw_local=(int)$local; } - Title::$interwikiCache[$wgDBname.':interwiki:'.$key] = $s; + Title::$interwikiCache[wfMemcKey( 'interwiki', $key )] = $s; return $s->iw_url; } /** @@ -488,12 +488,10 @@ class Title { * @access public */ function isLocal() { - global $wgDBname; - if ( $this->mInterwiki != '' ) { # Make sure key is loaded into cache $this->getInterwikiLink( $this->mInterwiki ); - $k = $wgDBname.':interwiki:' . $this->mInterwiki; + $k = wfMemcKey( 'interwiki', $this->mInterwiki ); return (bool)(Title::$interwikiCache[$k]->iw_local); } else { return true; @@ -508,13 +506,11 @@ class Title { * @access public */ function isTrans() { - global $wgDBname; - if ($this->mInterwiki == '') return false; # Make sure key is loaded into cache $this->getInterwikiLink( $this->mInterwiki ); - $k = $wgDBname.':interwiki:' . $this->mInterwiki; + $k = wfMemcKey( 'interwiki', $this->mInterwiki ); return (bool)(Title::$interwikiCache[$k]->iw_trans); } @@ -1075,6 +1071,7 @@ class Title { if( $action == 'create' ) { if( ( $this->isTalkPage() && !$wgUser->isAllowed( 'createtalk' ) ) || ( !$this->isTalkPage() && !$wgUser->isAllowed( 'createpage' ) ) ) { + wfProfileOut( $fname ); return false; } } @@ -1897,7 +1894,7 @@ class Title { $linkCache->clearLink( $nt->getPrefixedDBkey() ); # Recreate the redirect, this time in the other direction. - $mwRedir = MagicWord::get( MAG_REDIRECT ); + $mwRedir = MagicWord::get( 'redirect' ); $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; $redirectArticle = new Article( $this ); $newid = $redirectArticle->insertOn( $dbw ); @@ -1970,7 +1967,7 @@ class Title { $linkCache->clearLink( $nt->getPrefixedDBkey() ); # Insert redirect - $mwRedir = MagicWord::get( MAG_REDIRECT ); + $mwRedir = MagicWord::get( 'redirect' ); $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; $redirectArticle = new Article( $this ); $newid = $redirectArticle->insertOn( $dbw ); @@ -2147,7 +2144,9 @@ class Title { $stack[$parent] = array(); } else { $nt = Title::newFromText($parent); - $stack[$parent] = $nt->getParentCategoryTree( $children + array($parent => 1) ); + if ( $nt ) { + $stack[$parent] = $nt->getParentCategoryTree( $children + array($parent => 1) ); + } } } return $stack; @@ -2241,6 +2240,46 @@ class Title { } } + /** + * Get the last touched timestamp + */ + function getTouched() { + $dbr =& wfGetDB( DB_SLAVE ); + $touched = $dbr->selectField( 'page', 'page_touched', + array( + 'page_namespace' => $this->getNamespace(), + 'page_title' => $this->getDBkey() + ), __METHOD__ + ); + return $touched; + } + + /** + * Get a cached value from a global cache that is invalidated when this page changes + * @param string $key the key + * @param callback $callback A callback function which generates the value on cache miss + */ + function getRelatedCache( $memc, $key, $expiry, $callback, $params = array() ) { + $touched = $this->getTouched(); + $cacheEntry = $memc->get( $key ); + if ( $cacheEntry ) { + if ( $cacheEntry['touched'] >= $touched ) { + return $cacheEntry['value']; + } else { + wfDebug( __METHOD__.": $key expired\n" ); + } + } else { + wfDebug( __METHOD__.": $key not found\n" ); + } + $value = call_user_func_array( $callback, $params ); + $cacheEntry = array( + 'value' => $value, + 'touched' => $touched + ); + $memc->set( $key, $cacheEntry, $expiry ); + return $value; + } + function trackbackURL() { global $wgTitle, $wgScriptPath, $wgServer; diff --git a/includes/User.php b/includes/User.php index f2426284..aa964d22 100644 --- a/includes/User.php +++ b/includes/User.php @@ -24,6 +24,7 @@ class User { */ var $mBlockedby; //!< var $mBlockreason; //!< + var $mBlock; //!< var $mDataLoaded; //!< var $mEmail; //!< var $mEmailAuthenticated; //!< @@ -41,9 +42,46 @@ class User { var $mSkin; //!< var $mToken; //!< var $mTouched; //!< + var $mDatePreference; // !< var $mVersion; //!< serialized version /**@}} */ + static public $mToggles = array( + 'highlightbroken', + 'justify', + 'hideminor', + 'extendwatchlist', + 'usenewrc', + 'numberheadings', + 'showtoolbar', + 'editondblclick', + 'editsection', + 'editsectiononrightclick', + 'showtoc', + 'rememberpassword', + 'editwidth', + 'watchcreations', + 'watchdefault', + 'minordefault', + 'previewontop', + 'previewonfirst', + 'nocache', + 'enotifwatchlistpages', + 'enotifusertalkpages', + 'enotifminoredits', + 'enotifrevealaddr', + 'shownumberswatching', + 'fancysig', + 'externaleditor', + 'externaldiff', + 'showjumplinks', + 'uselivepreview', + 'autopatrol', + 'forceeditsummary', + 'watchlisthideown', + 'watchlisthidebots', + ); + /** Constructor using User:loadDefaults() */ function User() { $this->loadDefaults(); @@ -114,8 +152,6 @@ class User { */ function __sleep() { return array( -'mBlockedby', -'mBlockreason', 'mDataLoaded', 'mEmail', 'mEmailAuthenticated', @@ -257,6 +293,48 @@ class User { return true; } + + /** + * Usernames which fail to pass this function will be blocked + * from user login and new account registrations, but may be used + * internally by batch processes. + * + * If an account already exists in this form, login will be blocked + * by a failure to pass this function. + * + * @param string $name + * @return bool + */ + static function isUsableName( $name ) { + global $wgReservedUsernames; + return + // Must be a usable username, obviously ;) + self::isValidUserName( $name ) && + + // Certain names may be reserved for batch processes. + !in_array( $name, $wgReservedUsernames ); + } + + /** + * Usernames which fail to pass this function will be blocked + * from new account registrations, but may be used internally + * either by batch processes or by user accounts which have + * already been created. + * + * Additional character blacklisting may be added here + * rather than in isValidUserName() to avoid disrupting + * existing accounts. + * + * @param string $name + * @return bool + */ + static function isCreatableName( $name ) { + return + self::isUsableName( $name ) && + + // Registration-time character blacklisting... + strpos( $name, '@' ) === false; + } /** * Is the input a valid password? @@ -348,11 +426,9 @@ class User { $this->mPassword = $this->mNewpassword = ''; $this->mRights = array(); $this->mGroups = array(); - $this->mOptions = User::getDefaultOptions(); + $this->mOptions = null; + $this->mDatePreference = null; - foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) { - $this->mOptions['searchNs'.$nsnum] = $val; - } unset( $this->mSkin ); $this->mDataLoaded = false; $this->mBlockedby = -1; # Unset @@ -380,19 +456,23 @@ class User { * @private */ function getDefaultOptions() { + global $wgNamespacesToBeSearchedDefault; /** * Site defaults will override the global/language defaults */ - global $wgContLang, $wgDefaultUserOptions; - $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptions(); + global $wgDefaultUserOptions, $wgContLang; + $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides(); /** * default language setting */ - $variant = $wgContLang->getPreferredVariant(); + $variant = $wgContLang->getPreferredVariant( false ); $defOpt['variant'] = $variant; $defOpt['language'] = $variant; + foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) { + $defOpt['searchNs'.$nsnum] = $val; + } return $defOpt; } @@ -414,6 +494,18 @@ class User { } /** + * Get a list of user toggle names + * @return array + */ + static function getToggles() { + global $wgContLang; + $extraToggles = array(); + wfRunHooks( 'UserToggles', array( &$extraToggles ) ); + return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() ); + } + + + /** * Get blocking information * @private * @param bool $bFromSlave Specify whether to check slave or master. To improve performance, @@ -436,21 +528,21 @@ class User { $ip = wfGetIP(); # User/IP blocking - $block = new Block(); - $block->fromMaster( !$bFromSlave ); - if ( $block->load( $ip , $this->mId ) ) { + $this->mBlock = new Block(); + $this->mBlock->fromMaster( !$bFromSlave ); + if ( $this->mBlock->load( $ip , $this->mId ) ) { wfDebug( "$fname: Found block.\n" ); - $this->mBlockedby = $block->mBy; - $this->mBlockreason = $block->mReason; + $this->mBlockedby = $this->mBlock->mBy; + $this->mBlockreason = $this->mBlock->mReason; if ( $this->isLoggedIn() ) { $this->spreadBlock(); } } else { + $this->mBlock = null; wfDebug( "$fname: No block.\n" ); } # Proxy blocking - # FIXME ? proxyunbannable is to deprecate the old isSysop() if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) { # Local list @@ -531,7 +623,7 @@ class User { return false; } - global $wgMemc, $wgDBname, $wgRateLimitLog; + global $wgMemc, $wgRateLimitLog; $fname = 'User::pingLimiter'; wfProfileIn( $fname ); @@ -541,15 +633,15 @@ class User { $ip = wfGetIP(); if( isset( $limits['anon'] ) && $id == 0 ) { - $keys["$wgDBname:limiter:$action:anon"] = $limits['anon']; + $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon']; } if( isset( $limits['user'] ) && $id != 0 ) { - $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user']; + $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['user']; } if( $this->isNewbie() ) { if( isset( $limits['newbie'] ) && $id != 0 ) { - $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie']; + $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie']; } if( isset( $limits['ip'] ) ) { $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip']; @@ -569,7 +661,7 @@ class User { if( $count > $max ) { wfDebug( "$fname: tripped! $key at $count $summary\n" ); if( $wgRateLimitLog ) { - @error_log( wfTimestamp( TS_MW ) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog ); + @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog ); } $triggered = true; } else { @@ -638,19 +730,10 @@ class User { /** * Initialise php session + * @deprecated use wfSetupSession() */ function SetupSession() { - global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain; - if( $wgSessionsInMemcached ) { - require_once( 'MemcachedSessions.php' ); - } elseif( 'files' != ini_get( 'session.save_handler' ) ) { - # If it's left on 'user' or another setting from another - # application, it will end up failing. Try to recover. - ini_set ( 'session.save_handler', 'files' ); - } - session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain ); - session_cache_limiter( 'private, must-revalidate' ); - @session_start(); + wfSetupSession(); } /** @@ -658,7 +741,7 @@ class User { * @static */ function loadFromSession() { - global $wgMemc, $wgDBname, $wgCookiePrefix; + global $wgMemc, $wgCookiePrefix; if ( isset( $_SESSION['wsUserID'] ) ) { if ( 0 != $_SESSION['wsUserID'] ) { @@ -682,7 +765,7 @@ class User { } $passwordCorrect = FALSE; - $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" ); + $user = $wgMemc->get( $key = wfMemcKey( 'user', 'id', $sId ) ); if( !is_object( $user ) || $user->mVersion < MW_USER_VERSION ) { # Expire old serialized objects; they may be corrupt. $user = false; @@ -694,6 +777,8 @@ class User { $user->loadFromDatabase(); } else { wfDebug( "User::loadFromSession() got from cache!\n" ); + # Set block status to unloaded, that should be loaded every time + $user->mBlockedby = -1; } if ( isset( $_SESSION['wsToken'] ) ) { @@ -831,8 +916,8 @@ class User { # Check memcached separately for anons, who have no # entire User object stored in there. if( !$this->mId ) { - global $wgDBname, $wgMemc; - $key = "$wgDBname:newtalk:ip:" . $this->getName(); + global $wgMemc; + $key = wfMemcKey( 'newtalk', 'ip', $this->getName() ); $newtalk = $wgMemc->get( $key ); if( is_integer( $newtalk ) ) { $this->mNewtalk = (bool)$newtalk; @@ -852,7 +937,6 @@ class User { * Return the talk page(s) this user has new messages on. */ function getNewMessageLinks() { - global $wgDBname; $talks = array(); if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks))) return $talks; @@ -861,7 +945,7 @@ class User { return array(); $up = $this->getUserPage(); $utp = $up->getTalkPage(); - return array(array("wiki" => $wgDBname, "link" => $utp->getLocalURL())); + return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL())); } @@ -956,8 +1040,8 @@ class User { if( $this->isAnon() ) { // Anons have a separate memcached space, since // user records aren't kept for them. - global $wgDBname, $wgMemc; - $key = "$wgDBname:newtalk:ip:$val"; + global $wgMemc; + $key = wfMemcKey( 'newtalk', 'ip', $val ); $wgMemc->set( $key, $val ? 1 : 0 ); } else { if( $val ) { @@ -967,16 +1051,49 @@ class User { } } $this->invalidateCache(); - $this->saveSettings(); + } + } + + /** + * Generate a current or new-future timestamp to be stored in the + * user_touched field when we update things. + */ + private static function newTouchedTimestamp() { + global $wgClockSkewFudge; + return wfTimestamp( TS_MW, time() + $wgClockSkewFudge ); + } + + /** + * Clear user data from memcached. + * Use after applying fun updates to the database; caller's + * responsibility to update user_touched if appropriate. + * + * Called implicitly from invalidateCache() and saveSettings(). + */ + private function clearUserCache() { + if( $this->mId ) { + global $wgMemc; + $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) ); } } + /** + * Immediately touch the user data cache for this account. + * Updates user_touched field, and removes account data from memcached + * for reload on the next hit. + */ function invalidateCache() { - global $wgClockSkewFudge; - $this->loadFromDatabase(); - $this->mTouched = wfTimestamp(TS_MW, time() + $wgClockSkewFudge ); - # Don't forget to save the options after this or - # it won't take effect! + if( $this->mId ) { + $this->mTouched = self::newTouchedTimestamp(); + + $dbw =& wfGetDB( DB_MASTER ); + $dbw->update( 'user', + array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ), + array( 'user_id' => $this->mId ), + __METHOD__ ); + + $this->clearUserCache(); + } } function validateCache( $timestamp ) { @@ -1004,7 +1121,7 @@ class User { # Set the random token (used for persistent authentication) function setToken( $token = false ) { - global $wgSecretKey, $wgProxyKey, $wgDBname; + global $wgSecretKey, $wgProxyKey; if ( !$token ) { if ( $wgSecretKey ) { $key = $wgSecretKey; @@ -1013,7 +1130,7 @@ class User { } else { $key = microtime(); } - $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId ); + $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId ); } else { $this->mToken = $token; } @@ -1061,6 +1178,9 @@ class User { */ function getOption( $oname ) { $this->loadFromDatabase(); + if ( is_null( $this->mOptions ) ) { + $this->mOptions = User::getDefaultOptions(); + } if ( array_key_exists( $oname, $this->mOptions ) ) { return trim( $this->mOptions[$oname] ); } else { @@ -1069,6 +1189,23 @@ class User { } /** + * Get the user's date preference, including some important migration for + * old user rows. + */ + function getDatePreference() { + if ( is_null( $this->mDatePreference ) ) { + global $wgLang; + $value = $this->getOption( 'date' ); + $map = $wgLang->getDatePreferenceMigrationMap(); + if ( isset( $map[$value] ) ) { + $value = $map[$value]; + } + $this->mDatePreference = $value; + } + return $this->mDatePreference; + } + + /** * @param string $oname The option to check * @return bool False if the option is not selected, true if it is */ @@ -1092,6 +1229,9 @@ class User { function setOption( $oname, $val ) { $this->loadFromDatabase(); + if ( is_null( $this->mOptions ) ) { + $this->mOptions = User::getDefaultOptions(); + } if ( $oname == 'skin' ) { # Clear cached skin, so the new one displays immediately in Special:Preferences unset( $this->mSkin ); @@ -1102,7 +1242,6 @@ class User { $val = str_replace( "\r", "\n", $val ); $val = str_replace( "\n", " ", $val ); $this->mOptions[$oname] = $val; - $this->invalidateCache(); } function getRights() { @@ -1153,7 +1292,6 @@ class User { $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() ); $this->invalidateCache(); - $this->saveSettings(); } /** @@ -1174,7 +1312,6 @@ class User { $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() ); $this->invalidateCache(); - $this->saveSettings(); } @@ -1199,44 +1336,16 @@ class User { } /** - * Deprecated in 1.6, die in 1.7, to be removed in 1.8 - * @deprecated - */ - function isSysop() { - throw new MWException( "Call to deprecated (v1.7) User::isSysop() method\n" ); - #return $this->isAllowed( 'protect' ); - } - - /** - * Deprecated in 1.6, die in 1.7, to be removed in 1.8 - * @deprecated - */ - function isDeveloper() { - throw new MWException( "Call to deprecated (v1.7) User::isDeveloper() method\n" ); - #return $this->isAllowed( 'siteadmin' ); - } - - /** - * Deprecated in 1.6, die in 1.7, to be removed in 1.8 - * @deprecated - */ - function isBureaucrat() { - throw new MWException( "Call to deprecated (v1.7) User::isBureaucrat() method\n" ); - #return $this->isAllowed( 'makesysop' ); - } - - /** * Whether the user is a bot - * @todo need to be migrated to the new user level management sytem + * @deprecated */ function isBot() { - $this->loadFromDatabase(); - return in_array( 'bot', $this->mRights ); + return $this->isAllowed( 'bot' ); } /** * Check if user is allowed to access a feature / make an action - * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions). + * @param string $action Action to be checked * @return boolean True: action is allowed, False: action should not be allowed */ function isAllowed($action='') { @@ -1375,7 +1484,7 @@ class User { $dbw =& wfGetDB( DB_MASTER ); $success = $dbw->update( 'watchlist', array( /* SET */ - 'wl_notificationtimestamp' => 0 + 'wl_notificationtimestamp' => NULL ), array( /* WHERE */ 'wl_user' => $currentUser ), 'UserMailer::clearAll' @@ -1391,6 +1500,9 @@ class User { * @return string Encoding options */ function encodeOptions() { + if ( is_null( $this->mOptions ) ) { + $this->mOptions = User::getDefaultOptions(); + } $a = array(); foreach ( $this->mOptions as $oname => $oval ) { array_push( $a, $oname.'='.$oval ); @@ -1403,6 +1515,9 @@ class User { * @private */ function decodeOptions( $str ) { + global $wgLang; + + $this->mOptions = array(); $a = explode( "\n", $str ); foreach ( $a as $s ) { if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) { @@ -1451,13 +1566,15 @@ class User { /** * Save object settings into database + * @fixme Only rarely do all these fields need to be set! */ function saveSettings() { - global $wgMemc, $wgDBname; $fname = 'User::saveSettings'; if ( wfReadOnly() ) { return; } if ( 0 == $this->mId ) { return; } + + $this->mTouched = self::newTouchedTimestamp(); $dbw =& wfGetDB( DB_MASTER ); $dbw->update( 'user', @@ -1475,7 +1592,7 @@ class User { 'user_id' => $this->mId ), $fname ); - $wgMemc->delete( "$wgDBname:user:id:$this->mId" ); + $this->clearUserCache(); } @@ -1532,13 +1649,13 @@ class User { } $userblock = Block::newFromDB( '', $this->mId ); - if ( !$userblock->isValid() ) { + if ( !$userblock ) { return; } # Check if this IP address is already blocked $ipblock = Block::newFromDB( wfGetIP() ); - if ( $ipblock->isValid() ) { + if ( $ipblock ) { # If the user is already blocked. Then check if the autoblock would # excede the user block. If it would excede, then do nothing, else # prolong block time @@ -1549,6 +1666,8 @@ class User { # Just update the timestamp $ipblock->updateTimestamp(); return; + } else { + $ipblock = new Block; } # Make a new block object with the desired properties @@ -1586,7 +1705,7 @@ class User { * @return string */ function getPageRenderingHash() { - global $wgContLang; + global $wgContLang, $wgUseDynamicDates; if( $this->mHash ){ return $this->mHash; } @@ -1596,7 +1715,9 @@ class User { $confstr = $this->getOption( 'math' ); $confstr .= '!' . $this->getOption( 'stubthreshold' ); - $confstr .= '!' . $this->getOption( 'date' ); + if ( $wgUseDynamicDates ) { + $confstr .= '!' . $this->getDatePreference(); + } $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : ''); $confstr .= '!' . $this->getOption( 'language' ); $confstr .= '!' . $this->getOption( 'thumbsize' ); @@ -1612,8 +1733,13 @@ class User { return $confstr; } + function isBlockedFromCreateAccount() { + $this->getBlockedStatus(); + return $this->mBlock && $this->mBlock->mCreateAccount; + } + function isAllowedToCreateAccount() { - return $this->isAllowed( 'createaccount' ) && !$this->isBlocked(); + return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount(); } /** @@ -1700,7 +1826,7 @@ class User { } elseif ( function_exists( 'iconv' ) ) { # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted # Check for this with iconv - $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) ); + $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password ) ); if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) { return true; } @@ -1906,7 +2032,7 @@ class User { * @return array list of permission key names for given groups combined * @static */ - function getGroupPermissions( $groups ) { + static function getGroupPermissions( $groups ) { global $wgGroupPermissions; $rights = array(); foreach( $groups as $group ) { @@ -1923,10 +2049,10 @@ class User { * @return string localized descriptive name for group, if provided * @static */ - function getGroupName( $group ) { + static function getGroupName( $group ) { $key = "group-$group"; $name = wfMsg( $key ); - if( $name == '' || $name == "<$key>" ) { + if( $name == '' || wfEmptyMsg( $key, $name ) ) { return $group; } else { return $name; @@ -1938,17 +2064,16 @@ class User { * @return string localized descriptive name for member of a group, if provided * @static */ - function getGroupMember( $group ) { + static function getGroupMember( $group ) { $key = "group-$group-member"; $name = wfMsg( $key ); - if( $name == '' || $name == "<$key>" ) { + if( $name == '' || wfEmptyMsg( $key, $name ) ) { return $group; } else { return $name; } } - /** * Return the set of defined explicit groups. * The *, 'user', 'autoconfirmed' and 'emailconfirmed' @@ -1957,20 +2082,20 @@ class User { * @return array * @static */ - function getAllGroups() { + static function getAllGroups() { global $wgGroupPermissions; return array_diff( array_keys( $wgGroupPermissions ), array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) ); } - + /** * Get the title of a page describing a particular group * * @param $group Name of the group * @return mixed */ - function getGroupPage( $group ) { + static function getGroupPage( $group ) { $page = wfMsgForContent( 'grouppage-' . $group ); if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) { $title = Title::newFromText( $page ); @@ -1979,8 +2104,47 @@ class User { } return false; } - - + + /** + * Create a link to the group in HTML, if available + * + * @param $group Name of the group + * @param $text The text of the link + * @return mixed + */ + static function makeGroupLinkHTML( $group, $text = '' ) { + if( $text == '' ) { + $text = self::getGroupName( $group ); + } + $title = self::getGroupPage( $group ); + if( $title ) { + global $wgUser; + $sk = $wgUser->getSkin(); + return $sk->makeLinkObj( $title, $text ); + } else { + return $text; + } + } + + /** + * Create a link to the group in Wikitext, if available + * + * @param $group Name of the group + * @param $text The text of the link (by default, the name of the group) + * @return mixed + */ + static function makeGroupLinkWiki( $group, $text = '' ) { + if( $text == '' ) { + $text = self::getGroupName( $group ); + } + $title = self::getGroupPage( $group ); + if( $title ) { + $page = $title->getPrefixedText(); + return "[[$page|$text]]"; + } else { + return $text; + } + } } ?> diff --git a/includes/UserMailer.php b/includes/UserMailer.php index 8de39a64..78a8be91 100644 --- a/includes/UserMailer.php +++ b/includes/UserMailer.php @@ -53,7 +53,10 @@ class MailAddress { * @return string */ function toString() { - if( $this->name != '' ) { + # PHP's mail() implementation under Windows is somewhat shite, and + # can't handle "Joe Bloggs <joe@bloggs.com>" format email addresses, + # so don't bother generating them + if( $this->name != '' && !wfIsWindows() ) { $quoted = wfQuotedPrintable( $this->name ); if( strpos( $quoted, '.' ) !== false ) { $quoted = '"' . $quoted . '"'; @@ -101,6 +104,11 @@ function userMailer( $to, $from, $subject, $body, $replyto=false ) { // Create the mail object using the Mail::factory method $mail_object =& Mail::factory('smtp', $wgSMTP); + if( PEAR::isError( $mail_object ) ) { + wfDebug( "PEAR::Mail factory failed: " . $mail_object->getMessage() . "\n" ); + return $mail_object->getMessage(); + } + wfDebug( "Sending mail via PEAR::Mail to $dest\n" ); $mailResult =& $mail_object->send($dest, $headers, $body); @@ -258,8 +266,11 @@ class EmailNotification { $wuser = $dbr->fetchObject( $res ); $watchingUser->setID($wuser->wl_user); + if ( ( $enotifwatchlistpage && $watchingUser->getOption('enotifwatchlistpages') ) || - ( $enotifusertalkpage && $watchingUser->getOption('enotifusertalkpages') ) + ( $enotifusertalkpage + && $watchingUser->getOption('enotifusertalkpages') + && $title->equals( $watchingUser->getTalkPage() ) ) && (!$minorEdit || ($wgEnotifMinorEdits && $watchingUser->getOption('enotifminoredits') ) ) && ($watchingUser->isEmailConfirmed() ) ) { # ... adjust remaining text and page edit time placeholders @@ -286,7 +297,7 @@ class EmailNotification { ); # FIXME what do we do on failure ? } - + wfProfileOut( $fname ); } # function NotifyOnChange /** diff --git a/includes/Utf8Case.php b/includes/Utf8Case.php index 9a2c7302..8c7fdd0b 100644 --- a/includes/Utf8Case.php +++ b/includes/Utf8Case.php @@ -6,7 +6,7 @@ * Hack for bugs in ucfirst() and company * * These are pulled from memcached if possible, as this is faster than filling - * up a big array manually. See also languages/LanguageUtf8.php + * up a big array manually. * @package MediaWiki * @subpackage Language */ diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php index 3885bb98..788774fb 100644 --- a/includes/WatchedItem.php +++ b/includes/WatchedItem.php @@ -35,8 +35,7 @@ class WatchedItem { * Returns the memcached key for this item */ function watchKey() { - global $wgDBname; - return "$wgDBname:watchlist:user:$this->id:page:$this->ns:$this->ti"; + return wfMemcKey( 'watchlist', 'user', $this->id, 'page', $this->ns, $this->ti ); } /** diff --git a/includes/WebRequest.php b/includes/WebRequest.php index 4031e369..32307ed2 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -34,6 +34,15 @@ * * @package MediaWiki */ + +/** + * Some entry points may use this file without first enabling the + * autoloader. + */ +if ( !function_exists( '__autoload' ) ) { + require_once( dirname(__FILE__) . '/normal/UtfNormal.php' ); +} + class WebRequest { function WebRequest() { $this->checkMagicQuotes(); @@ -44,6 +53,8 @@ class WebRequest { substr( $_SERVER['PATH_INFO'], 1 ); } } + + private $_response; /** * Recursively strips slashes from the given array; @@ -117,7 +128,6 @@ class WebRequest { $data = $wgContLang->checkTitleEncoding( $data ); } } - require_once( 'normal/UtfNormal.php' ); $data = $this->normalizeUnicode( $data ); return $data; } else { @@ -437,6 +447,19 @@ class WebRequest { wfDebug( "WebRequest::getFileName() '" . $_FILES[$key]['name'] . "' normalized to '$name'\n" ); return $name; } + + /** + * Return a handle to WebResponse style object, for setting cookies, + * headers and other stuff, for Request being worked on. + */ + function response() { + /* Lazy initialization of response object for this request */ + if (!is_object($this->_response)) { + $this->_response = new WebResponse; + } + return $this->_response; + } + } /** diff --git a/includes/WebResponse.php b/includes/WebResponse.php new file mode 100644 index 00000000..e159152e --- /dev/null +++ b/includes/WebResponse.php @@ -0,0 +1,18 @@ +<?php + +/* + * Allow programs to request this object from WebRequest::response() and handle all outputting (or lack of outputting) via it. + */ + +class WebResponse { + function header($string, $replace=true) { + header($string,$replace); + } + + function setcookie($name, $value, $expire) { + global $wgCookiePath, $wgCookieDomain, $wgCookieSecure; + setcookie($name,$value,$expire, $wgCookiePath, $wgCookieDomain, $wgCookieSecure); + } +} + +?>
\ No newline at end of file diff --git a/includes/WebStart.php b/includes/WebStart.php new file mode 100644 index 00000000..0c71ce53 --- /dev/null +++ b/includes/WebStart.php @@ -0,0 +1,82 @@ +<?php + +# This does the initial setup for a web request. It does some security checks, +# starts the profiler and loads the configuration, and optionally loads +# Setup.php depending on whether MW_NO_SETUP is defined. + +# Protect against register_globals +# This must be done before any globals are set by the code +if ( ini_get( 'register_globals' ) ) { + if ( isset( $_REQUEST['GLOBALS'] ) ) { + die( '<a href="http://www.hardened-php.net/index.76.html">$GLOBALS overwrite vulnerability</a>'); + } + $verboten = array( + 'GLOBALS', + '_SERVER', + 'HTTP_SERVER_VARS', + '_GET', + 'HTTP_GET_VARS', + '_POST', + 'HTTP_POST_VARS', + '_COOKIE', + 'HTTP_COOKIE_VARS', + '_FILES', + 'HTTP_POST_FILES', + '_ENV', + 'HTTP_ENV_VARS', + '_REQUEST', + '_SESSION', + 'HTTP_SESSION_VARS' + ); + foreach ( $_REQUEST as $name => $value ) { + if( in_array( $name, $verboten ) ) { + header( "HTTP/1.x 500 Internal Server Error" ); + echo "register_globals security paranoia: trying to overwrite superglobals, aborting."; + die( -1 ); + } + unset( $GLOBALS[$name] ); + } +} + +$wgRequestTime = microtime(true); +# getrusage() does not exist on the Microsoft Windows platforms, catching this +if ( function_exists ( 'getrusage' ) ) { + $wgRUstart = getrusage(); +} else { + $wgRUstart = array(); +} +unset( $IP ); +@ini_set( 'allow_url_fopen', 0 ); # For security + +# Valid web server entry point, enable includes. +# Please don't move this line to includes/Defines.php. This line essentially +# defines a valid entry point. If you put it in includes/Defines.php, then +# any script that includes it becomes an entry point, thereby defeating +# its purpose. +define( 'MEDIAWIKI', true ); + +# Start profiler +require_once( './StartProfiler.php' ); +wfProfileIn( 'WebStart.php-conf' ); + +# Load up some global defines. +require_once( './includes/Defines.php' ); + +# LocalSettings.php is the per site customization file. If it does not exit +# the wiki installer need to be launched or the generated file moved from +# ./config/ to ./ +if( !file_exists( './LocalSettings.php' ) ) { + $IP = '.'; + require_once( './includes/DefaultSettings.php' ); # used for printing the version + require_once( './includes/templates/NoLocalSettings.php' ); + die(); +} + +# Include this site setttings +require_once( './LocalSettings.php' ); +wfProfileOut( 'WebStart.php-conf' ); + +if ( !defined( 'MW_NO_SETUP' ) ) { + require_once( './includes/Setup.php' ); +} +?> diff --git a/includes/Wiki.php b/includes/Wiki.php index 6f010003..401756be 100644 --- a/includes/Wiki.php +++ b/includes/Wiki.php @@ -170,6 +170,12 @@ class MediaWiki { * @return Article */ function articleFromTitle( $title ) { + $article = null; + wfRunHooks('ArticleFromTitle', array( &$title, &$article ) ); + if ( $article ) { + return $article; + } + if( NS_MEDIA == $title->getNamespace() ) { // FIXME: where should this go? $title = Title::makeTitle( NS_IMAGE, $title->getDBkey() ); @@ -258,8 +264,14 @@ class MediaWiki { */ function doUpdates ( &$updates ) { wfProfileIn( 'MediaWiki::doUpdates' ); + $dbw =& wfGetDB( DB_MASTER ); foreach( $updates as $up ) { $up->doUpdate(); + + # Commit after every update to prevent lock contention + if ( $dbw->trxLevel() ) { + $dbw->commit(); + } } wfProfileOut( 'MediaWiki::doUpdates' ); } @@ -270,7 +282,7 @@ class MediaWiki { function doJobs() { global $wgJobRunRate; - if ( $wgJobRunRate <= 0 ) { + if ( $wgJobRunRate <= 0 || wfReadOnly() ) { return; } if ( $wgJobRunRate < 1 ) { @@ -302,8 +314,7 @@ class MediaWiki { * Ends this task peacefully */ function restInPeace ( &$loadBalancer ) { - wfProfileClose(); - logProfilingData(); + wfLogProfilingData(); $loadBalancer->closeAll(); wfDebug( "Request ended normally\n" ); } diff --git a/includes/Xml.php b/includes/Xml.php index 52993367..34574458 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -1,279 +1,301 @@ -<?php
-
-/**
- * Module of static functions for generating XML
- */
-
-class Xml {
- /**
- * Format an XML element with given attributes and, optionally, text content.
- * Element and attribute names are assumed to be ready for literal inclusion.
- * Strings are assumed to not contain XML-illegal characters; special
- * characters (<, >, &) are escaped but illegals are not touched.
- *
- * @param $element String:
- * @param $attribs Array: Name=>value pairs. Values will be escaped.
- * @param $contents String: NULL to make an open tag only; '' for a contentless closed tag (default)
- * @return string
- */
- function element( $element, $attribs = null, $contents = '') {
- $out = '<' . $element;
- if( !is_null( $attribs ) ) {
- foreach( $attribs as $name => $val ) {
- $out .= ' ' . $name . '="' . Sanitizer::encodeAttribute( $val ) . '"';
- }
- }
- if( is_null( $contents ) ) {
- $out .= '>';
- } else {
- if( $contents === '' ) {
- $out .= ' />';
- } else {
- $out .= '>' . htmlspecialchars( $contents ) . "</$element>";
- }
- }
- return $out;
- }
-
- /**
- * Format an XML element as with self::element(), but run text through the
- * UtfNormal::cleanUp() validator first to ensure that no invalid UTF-8
- * is passed.
- *
- * @param $element String:
- * @param $attribs Array: Name=>value pairs. Values will be escaped.
- * @param $contents String: NULL to make an open tag only; '' for a contentless closed tag (default)
- * @return string
- */
- function elementClean( $element, $attribs = array(), $contents = '') {
- if( $attribs ) {
- $attribs = array_map( array( 'UtfNormal', 'cleanUp' ), $attribs );
- }
- if( $contents ) {
- $contents = UtfNormal::cleanUp( $contents );
- }
- return self::element( $element, $attribs, $contents );
- }
-
- // Shortcuts
- function openElement( $element, $attribs = null ) { return self::element( $element, $attribs, null ); }
- function closeElement( $element ) { return "</$element>"; }
-
- /**
- * Create a namespace selector
- *
- * @param $selected Mixed: the namespace which should be selected, default ''
- * @param $allnamespaces String: value of a special item denoting all namespaces. Null to not include (default)
- * @param $includehidden Bool: include hidden namespaces?
- * @return String: Html string containing the namespace selector
- */
- function &namespaceSelector($selected = '', $allnamespaces = null, $includehidden=false) {
- global $wgContLang;
- if( $selected !== '' ) {
- if( is_null( $selected ) ) {
- // No namespace selected; let exact match work without hitting Main
- $selected = '';
- } else {
- // Let input be numeric strings without breaking the empty match.
- $selected = intval( $selected );
- }
- }
- $s = "<select id='namespace' name='namespace' class='namespaceselector'>\n\t";
- $arr = $wgContLang->getFormattedNamespaces();
- if( !is_null($allnamespaces) ) {
- $arr = array($allnamespaces => wfMsg('namespacesall')) + $arr;
- }
- foreach ($arr as $index => $name) {
- if ($index < NS_MAIN) continue;
-
- $name = $index !== 0 ? $name : wfMsg('blanknamespace');
-
- if ($index === $selected) {
- $s .= self::element("option",
- array("value" => $index, "selected" => "selected"),
- $name);
- } else {
- $s .= self::element("option", array("value" => $index), $name);
- }
- }
- $s .= "\n</select>\n";
- return $s;
- }
-
- function span( $text, $class, $attribs=array() ) {
- return self::element( 'span', array( 'class' => $class ) + $attribs, $text );
- }
-
- /**
- * Convenience function to build an HTML text input field
- * @return string HTML
- */
- function input( $name, $size=false, $value=false, $attribs=array() ) {
- return self::element( 'input', array(
- 'name' => $name,
- 'size' => $size,
- 'value' => $value ) + $attribs );
- }
-
- /**
- * Internal function for use in checkboxes and radio buttons and such.
- * @return array
- */
- function attrib( $name, $present = true ) {
- return $present ? array( $name => $name ) : array();
- }
-
- /**
- * Convenience function to build an HTML checkbox
- * @return string HTML
- */
- function check( $name, $checked=false, $attribs=array() ) {
- return self::element( 'input', array(
- 'name' => $name,
- 'type' => 'checkbox',
- 'value' => 1 ) + self::attrib( 'checked', $checked ) + $attribs );
- }
-
- /**
- * Convenience function to build an HTML radio button
- * @return string HTML
- */
- function radio( $name, $value, $checked=false, $attribs=array() ) {
- return self::element( 'input', array(
- 'name' => $name,
- 'type' => 'radio',
- 'value' => $value ) + self::attrib( 'checked', $checked ) + $attribs );
- }
-
- /**
- * Convenience function to build an HTML form label
- * @return string HTML
- */
- function label( $label, $id ) {
- return self::element( 'label', array( 'for' => $id ), $label );
- }
-
- /**
- * Convenience function to build an HTML text input field with a label
- * @return string HTML
- */
- function inputLabel( $label, $name, $id, $size=false, $value=false, $attribs=array() ) {
- return Xml::label( $label, $id ) .
- ' ' .
- self::input( $name, $size, $value, array( 'id' => $id ) + $attribs );
- }
-
- /**
- * Convenience function to build an HTML checkbox with a label
- * @return string HTML
- */
- function checkLabel( $label, $name, $id, $checked=false, $attribs=array() ) {
- return self::check( $name, $checked, array( 'id' => $id ) + $attribs ) .
- ' ' .
- self::label( $label, $id );
- }
-
- /**
- * Convenience function to build an HTML radio button with a label
- * @return string HTML
- */
- function radioLabel( $label, $name, $value, $id, $checked=false, $attribs=array() ) {
- return self::radio( $name, $value, $checked, array( 'id' => $id ) + $attribs ) .
- ' ' .
- self::label( $label, $id );
- }
-
- /**
- * Convenience function to build an HTML submit button
- * @param $value String: label text for the button
- * @param $attribs Array: optional custom attributes
- * @return string HTML
- */
- function submitButton( $value, $attribs=array() ) {
- return self::element( 'input', array( 'type' => 'submit', 'value' => $value ) + $attribs );
- }
-
- /**
- * Convenience function to build an HTML hidden form field.
- * @todo Document $name parameter.
- * @param $name FIXME
- * @param $value String: label text for the button
- * @param $attribs Array: optional custom attributes
- * @return string HTML
- */
- function hidden( $name, $value, $attribs=array() ) {
- return self::element( 'input', array(
- 'name' => $name,
- 'type' => 'hidden',
- 'value' => $value ) + $attribs );
- }
-
- /**
- * Returns an escaped string suitable for inclusion in a string literal
- * for JavaScript source code.
- * Illegal control characters are assumed not to be present.
- *
- * @param string $string
- * @return string
- */
- function escapeJsString( $string ) {
- // See ECMA 262 section 7.8.4 for string literal format
- $pairs = array(
- "\\" => "\\\\",
- "\"" => "\\\"",
- '\'' => '\\\'',
- "\n" => "\\n",
- "\r" => "\\r",
-
- # To avoid closing the element or CDATA section
- "<" => "\\x3c",
- ">" => "\\x3e",
- );
- return strtr( $string, $pairs );
- }
-
- /**
- * Check if a string is well-formed XML.
- * Must include the surrounding tag.
- *
- * @param $text String: string to test.
- * @return bool
- *
- * @todo Error position reporting return
- */
- function isWellFormed( $text ) {
- $parser = xml_parser_create( "UTF-8" );
-
- # case folding violates XML standard, turn it off
- xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
-
- if( !xml_parse( $parser, $text, true ) ) {
- $err = xml_error_string( xml_get_error_code( $parser ) );
- $position = xml_get_current_byte_index( $parser );
- //$fragment = $this->extractFragment( $html, $position );
- //$this->mXmlError = "$err at byte $position:\n$fragment";
- xml_parser_free( $parser );
- return false;
- }
- xml_parser_free( $parser );
- return true;
- }
-
- /**
- * Check if a string is a well-formed XML fragment.
- * Wraps fragment in an \<html\> bit and doctype, so it can be a fragment
- * and can use HTML named entities.
- *
- * @param $text String:
- * @return bool
- */
- function isWellFormedXmlFragment( $text ) {
- $html =
- Sanitizer::hackDocType() .
- '<html>' .
- $text .
- '</html>';
- return Xml::isWellFormed( $html );
- }
-}
-?>
+<?php + +/** + * Module of static functions for generating XML + */ + +class Xml { + /** + * Format an XML element with given attributes and, optionally, text content. + * Element and attribute names are assumed to be ready for literal inclusion. + * Strings are assumed to not contain XML-illegal characters; special + * characters (<, >, &) are escaped but illegals are not touched. + * + * @param $element String: + * @param $attribs Array: Name=>value pairs. Values will be escaped. + * @param $contents String: NULL to make an open tag only; '' for a contentless closed tag (default) + * @return string + */ + public static function element( $element, $attribs = null, $contents = '') { + $out = '<' . $element; + if( !is_null( $attribs ) ) { + foreach( $attribs as $name => $val ) { + $out .= ' ' . $name . '="' . Sanitizer::encodeAttribute( $val ) . '"'; + } + } + if( is_null( $contents ) ) { + $out .= '>'; + } else { + if( $contents === '' ) { + $out .= ' />'; + } else { + $out .= '>' . htmlspecialchars( $contents ) . "</$element>"; + } + } + return $out; + } + + /** + * Format an XML element as with self::element(), but run text through the + * UtfNormal::cleanUp() validator first to ensure that no invalid UTF-8 + * is passed. + * + * @param $element String: + * @param $attribs Array: Name=>value pairs. Values will be escaped. + * @param $contents String: NULL to make an open tag only; '' for a contentless closed tag (default) + * @return string + */ + public static function elementClean( $element, $attribs = array(), $contents = '') { + if( $attribs ) { + $attribs = array_map( array( 'UtfNormal', 'cleanUp' ), $attribs ); + } + if( $contents ) { + $contents = UtfNormal::cleanUp( $contents ); + } + return self::element( $element, $attribs, $contents ); + } + + // Shortcuts + public static function openElement( $element, $attribs = null ) { return self::element( $element, $attribs, null ); } + public static function closeElement( $element ) { return "</$element>"; } + + /** + * Create a namespace selector + * + * @param $selected Mixed: the namespace which should be selected, default '' + * @param $allnamespaces String: value of a special item denoting all namespaces. Null to not include (default) + * @param $includehidden Bool: include hidden namespaces? + * @return String: Html string containing the namespace selector + */ + public static function &namespaceSelector($selected = '', $allnamespaces = null, $includehidden=false) { + global $wgContLang; + if( $selected !== '' ) { + if( is_null( $selected ) ) { + // No namespace selected; let exact match work without hitting Main + $selected = ''; + } else { + // Let input be numeric strings without breaking the empty match. + $selected = intval( $selected ); + } + } + $s = "\n<select id='namespace' name='namespace' class='namespaceselector'>\n"; + $arr = $wgContLang->getFormattedNamespaces(); + if( !is_null($allnamespaces) ) { + $arr = array($allnamespaces => wfMsg('namespacesall')) + $arr; + } + foreach ($arr as $index => $name) { + if ($index < NS_MAIN) continue; + + $name = $index !== 0 ? $name : wfMsg('blanknamespace'); + + if ($index === $selected) { + $s .= "\t" . self::element("option", + array("value" => $index, "selected" => "selected"), + $name) . "\n"; + } else { + $s .= "\t" . self::element("option", array("value" => $index), $name) . "\n"; + } + } + $s .= "</select>\n"; + return $s; + } + + public static function span( $text, $class, $attribs=array() ) { + return self::element( 'span', array( 'class' => $class ) + $attribs, $text ); + } + + /** + * Convenience function to build an HTML text input field + * @return string HTML + */ + public static function input( $name, $size=false, $value=false, $attribs=array() ) { + return self::element( 'input', array( + 'name' => $name, + 'size' => $size, + 'value' => $value ) + $attribs ); + } + + /** + * Internal function for use in checkboxes and radio buttons and such. + * @return array + */ + public static function attrib( $name, $present = true ) { + return $present ? array( $name => $name ) : array(); + } + + /** + * Convenience function to build an HTML checkbox + * @return string HTML + */ + public static function check( $name, $checked=false, $attribs=array() ) { + return self::element( 'input', array( + 'name' => $name, + 'type' => 'checkbox', + 'value' => 1 ) + self::attrib( 'checked', $checked ) + $attribs ); + } + + /** + * Convenience function to build an HTML radio button + * @return string HTML + */ + public static function radio( $name, $value, $checked=false, $attribs=array() ) { + return self::element( 'input', array( + 'name' => $name, + 'type' => 'radio', + 'value' => $value ) + self::attrib( 'checked', $checked ) + $attribs ); + } + + /** + * Convenience function to build an HTML form label + * @return string HTML + */ + public static function label( $label, $id ) { + return self::element( 'label', array( 'for' => $id ), $label ); + } + + /** + * Convenience function to build an HTML text input field with a label + * @return string HTML + */ + public static function inputLabel( $label, $name, $id, $size=false, $value=false, $attribs=array() ) { + return Xml::label( $label, $id ) . + ' ' . + self::input( $name, $size, $value, array( 'id' => $id ) + $attribs ); + } + + /** + * Convenience function to build an HTML checkbox with a label + * @return string HTML + */ + public static function checkLabel( $label, $name, $id, $checked=false, $attribs=array() ) { + return self::check( $name, $checked, array( 'id' => $id ) + $attribs ) . + ' ' . + self::label( $label, $id ); + } + + /** + * Convenience function to build an HTML radio button with a label + * @return string HTML + */ + public static function radioLabel( $label, $name, $value, $id, $checked=false, $attribs=array() ) { + return self::radio( $name, $value, $checked, array( 'id' => $id ) + $attribs ) . + ' ' . + self::label( $label, $id ); + } + + /** + * Convenience function to build an HTML submit button + * @param $value String: label text for the button + * @param $attribs Array: optional custom attributes + * @return string HTML + */ + public static function submitButton( $value, $attribs=array() ) { + return self::element( 'input', array( 'type' => 'submit', 'value' => $value ) + $attribs ); + } + + /** + * Convenience function to build an HTML hidden form field. + * @todo Document $name parameter. + * @param $name FIXME + * @param $value String: label text for the button + * @param $attribs Array: optional custom attributes + * @return string HTML + */ + public static function hidden( $name, $value, $attribs=array() ) { + return self::element( 'input', array( + 'name' => $name, + 'type' => 'hidden', + 'value' => $value ) + $attribs ); + } + + /** + * Convenience function to build an HTML drop-down list item. + * @param $text String: text for this item + * @param $value String: form submission value; if empty, use text + * @param $selected boolean: if true, will be the default selected item + * @param $attribs array: optional additional HTML attributes + * @return string HTML + */ + public static function option( $text, $value=null, $selected=false, + $attribs=array() ) { + if( !is_null( $value ) ) { + $attribs['value'] = $value; + } + if( $selected ) { + $attribs['selected'] = 'selected'; + } + return self::element( 'option', $attribs, $text ); + } + + /** + * Returns an escaped string suitable for inclusion in a string literal + * for JavaScript source code. + * Illegal control characters are assumed not to be present. + * + * @param string $string + * @return string + */ + public static function escapeJsString( $string ) { + // See ECMA 262 section 7.8.4 for string literal format + $pairs = array( + "\\" => "\\\\", + "\"" => "\\\"", + '\'' => '\\\'', + "\n" => "\\n", + "\r" => "\\r", + + # To avoid closing the element or CDATA section + "<" => "\\x3c", + ">" => "\\x3e", + + # To avoid any complaints about bad entity refs + "&" => "\\x26", + ); + return strtr( $string, $pairs ); + } + + /** + * Check if a string is well-formed XML. + * Must include the surrounding tag. + * + * @param $text String: string to test. + * @return bool + * + * @todo Error position reporting return + */ + public static function isWellFormed( $text ) { + $parser = xml_parser_create( "UTF-8" ); + + # case folding violates XML standard, turn it off + xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); + + if( !xml_parse( $parser, $text, true ) ) { + $err = xml_error_string( xml_get_error_code( $parser ) ); + $position = xml_get_current_byte_index( $parser ); + //$fragment = $this->extractFragment( $html, $position ); + //$this->mXmlError = "$err at byte $position:\n$fragment"; + xml_parser_free( $parser ); + return false; + } + xml_parser_free( $parser ); + return true; + } + + /** + * Check if a string is a well-formed XML fragment. + * Wraps fragment in an \<html\> bit and doctype, so it can be a fragment + * and can use HTML named entities. + * + * @param $text String: + * @return bool + */ + public static function isWellFormedXmlFragment( $text ) { + $html = + Sanitizer::hackDocType() . + '<html>' . + $text . + '</html>'; + return Xml::isWellFormed( $html ); + } +} +?> diff --git a/includes/XmlFunctions.php b/includes/XmlFunctions.php index 64e349f2..cbdcf5c4 100644 --- a/includes/XmlFunctions.php +++ b/includes/XmlFunctions.php @@ -1,19 +1,19 @@ <?php - /** * Aliases for functions in the Xml module + * Look at the Xml class (Xml.php) for the implementations. */ -function wfElement( $element, $attribs = null, $contents = '') { - return Xml::element( $element, $attribs, $contents ); +function wfElement( $element, $attribs = null, $contents = '') { + return Xml::element( $element, $attribs, $contents ); } function wfElementClean( $element, $attribs = array(), $contents = '') { return Xml::elementClean( $element, $attribs, $contents ); } -function wfOpenElement( $element, $attribs = null ) { - return Xml::openElement( $element, $attribs ); +function wfOpenElement( $element, $attribs = null ) { + return Xml::openElement( $element, $attribs ); } -function wfCloseElement( $element ) { - return "</$element>"; +function wfCloseElement( $element ) { + return "</$element>"; } function &HTMLnamespaceselector($selected = '', $allnamespaces = null, $includehidden=false) { return Xml::namespaceSelector( $selected, $allnamespaces, $includehidden ); diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php new file mode 100644 index 00000000..f578f41b --- /dev/null +++ b/includes/api/ApiBase.php @@ -0,0 +1,441 @@ +<?php + + +/* + * Created on Sep 5, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +abstract class ApiBase { + + // These constants allow modules to specify exactly how to treat incomming parameters. + + const PARAM_DFLT = 0; + const PARAM_ISMULTI = 1; + const PARAM_TYPE = 2; + const PARAM_MAX1 = 3; + const PARAM_MAX2 = 4; + const PARAM_MIN = 5; + + private $mMainModule, $mModuleName, $mParamPrefix; + + /** + * Constructor + */ + public function __construct($mainModule, $moduleName, $paramPrefix = '') { + $this->mMainModule = $mainModule; + $this->mModuleName = $moduleName; + $this->mParamPrefix = $paramPrefix; + } + + /** + * Executes this module + */ + public abstract function execute(); + + /** + * Get the name of the query being executed by this instance + */ + public function getModuleName() { + return $this->mModuleName; + } + + /** + * Get main module + */ + public function getMain() { + return $this->mMainModule; + } + + /** + * If this module's $this is the same as $this->mMainModule, its the root, otherwise no + */ + public function isMain() { + return $this === $this->mMainModule; + } + + /** + * Get result object + */ + public function getResult() { + // Main module has getResult() method overriden + // Safety - avoid infinite loop: + if ($this->isMain()) + ApiBase :: dieDebug(__METHOD__, 'base method was called on main module. '); + return $this->getMain()->getResult(); + } + + /** + * Get the result data array + */ + public function & getResultData() { + return $this->getResult()->getData(); + } + + /** + * Generates help message for this module, or false if there is no description + */ + public function makeHelpMsg() { + + static $lnPrfx = "\n "; + + $msg = $this->getDescription(); + + if ($msg !== false) { + + if (!is_array($msg)) + $msg = array ( + $msg + ); + $msg = $lnPrfx . implode($lnPrfx, $msg) . "\n"; + + // Parameters + $paramsMsg = $this->makeHelpMsgParameters(); + if ($paramsMsg !== false) { + $msg .= "Parameters:\n$paramsMsg"; + } + + // Examples + $examples = $this->getExamples(); + if ($examples !== false) { + if (!is_array($examples)) + $examples = array ( + $examples + ); + $msg .= 'Example' . (count($examples) > 1 ? 's' : '') . ":\n "; + $msg .= implode($lnPrfx, $examples) . "\n"; + } + + if ($this->getMain()->getShowVersions()) { + $versions = $this->getVersion(); + if (is_array($versions)) + $versions = implode("\n ", $versions); + $msg .= "Version:\n $versions\n"; + } + } + + return $msg; + } + + public function makeHelpMsgParameters() { + $params = $this->getAllowedParams(); + if ($params !== false) { + + $paramsDescription = $this->getParamDescription(); + $msg = ''; + foreach (array_keys($params) as $paramName) { + $desc = isset ($paramsDescription[$paramName]) ? $paramsDescription[$paramName] : ''; + if (is_array($desc)) + $desc = implode("\n" . str_repeat(' ', 19), $desc); + $msg .= sprintf(" %-14s - %s\n", $this->encodeParamName($paramName), $desc); + } + return $msg; + + } else + return false; + } + + /** + * Returns the description string for this module + */ + protected function getDescription() { + return false; + } + + /** + * Returns usage examples for this module. Return null if no examples are available. + */ + protected function getExamples() { + return false; + } + + /** + * Returns an array of allowed parameters (keys) => default value for that parameter + */ + protected function getAllowedParams() { + return false; + } + + /** + * Returns the description string for the given parameter. + */ + protected function getParamDescription() { + return false; + } + + /** + * This method mangles parameter name based on the prefix supplied to the constructor. + * Override this method to change parameter name during runtime + */ + public function encodeParamName($paramName) { + return $this->mParamPrefix . $paramName; + } + + /** + * Using getAllowedParams(), makes an array of the values provided by the user, + * with key being the name of the variable, and value - validated value from user or default. + * This method can be used to generate local variables using extract(). + */ + public function extractRequestParams() { + $params = $this->getAllowedParams(); + $results = array (); + + foreach ($params as $paramName => $paramSettings) + $results[$paramName] = $this->getParameterFromSettings($paramName, $paramSettings); + + return $results; + } + + /** + * Get a value for the given parameter + */ + protected function getParameter($paramName) { + $params = $this->getAllowedParams(); + $paramSettings = $params[$paramName]; + return $this->getParameterFromSettings($paramName, $paramSettings); + } + + /** + * Using the settings determine the value for the given parameter + * @param $paramName String: parameter name + * @param $paramSettings Mixed: default value or an array of settings using PARAM_* constants. + */ + protected function getParameterFromSettings($paramName, $paramSettings) { + global $wgRequest; + + // Some classes may decide to change parameter names + $paramName = $this->encodeParamName($paramName); + + if (!is_array($paramSettings)) { + $default = $paramSettings; + $multi = false; + $type = gettype($paramSettings); + } else { + $default = isset ($paramSettings[self :: PARAM_DFLT]) ? $paramSettings[self :: PARAM_DFLT] : null; + $multi = isset ($paramSettings[self :: PARAM_ISMULTI]) ? $paramSettings[self :: PARAM_ISMULTI] : false; + $type = isset ($paramSettings[self :: PARAM_TYPE]) ? $paramSettings[self :: PARAM_TYPE] : null; + + // When type is not given, and no choices, the type is the same as $default + if (!isset ($type)) { + if (isset ($default)) + $type = gettype($default); + else + $type = 'NULL'; // allow everything + } + } + + if ($type == 'boolean') { + if (isset ($default) && $default !== false) { + // Having a default value of anything other than 'false' is pointless + ApiBase :: dieDebug(__METHOD__, "Boolean param $paramName's default is set to '$default'"); + } + + $value = $wgRequest->getCheck($paramName); + } else + $value = $wgRequest->getVal($paramName, $default); + + if (isset ($value) && ($multi || is_array($type))) + $value = $this->parseMultiValue($paramName, $value, $multi, is_array($type) ? $type : null); + + // More validation only when choices were not given + // choices were validated in parseMultiValue() + if (!is_array($type) && isset ($value)) { + + switch ($type) { + case 'NULL' : // nothing to do + break; + case 'string' : // nothing to do + break; + case 'integer' : // Force everything using intval() + $value = is_array($value) ? array_map('intval', $value) : intval($value); + break; + case 'limit' : + if (!isset ($paramSettings[self :: PARAM_MAX1]) || !isset ($paramSettings[self :: PARAM_MAX2])) + ApiBase :: dieDebug(__METHOD__, "MAX1 or MAX2 are not defined for the limit $paramName"); + if ($multi) + ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); + $min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : 0; + $value = intval($value); + $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX1], $paramSettings[self :: PARAM_MAX2]); + break; + case 'boolean' : + if ($multi) + ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); + break; + case 'timestamp' : + if ($multi) + ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); + if (!preg_match('/^[0-9]{14}$/', $value)) + $this->dieUsage("Invalid value '$value' for timestamp parameter $paramName", "badtimestamp_{$valueName}"); + break; + default : + ApiBase :: dieDebug(__METHOD__, "Param $paramName's type is unknown - $type"); + + } + } + + return $value; + } + + /** + * Return an array of values that were given in a 'a|b|c' notation, + * after it optionally validates them against the list allowed values. + * + * @param valueName - The name of the parameter (for error reporting) + * @param value - The value being parsed + * @param allowMultiple - Can $value contain more than one value separated by '|'? + * @param allowedValues - An array of values to check against. If null, all values are accepted. + * @return (allowMultiple ? an_array_of_values : a_single_value) + */ + protected function parseMultiValue($valueName, $value, $allowMultiple, $allowedValues) { + $valuesList = explode('|', $value); + if (!$allowMultiple && count($valuesList) != 1) { + $possibleValues = is_array($allowedValues) ? "of '" . implode("', '", $allowedValues) . "'" : ''; + $this->dieUsage("Only one $possibleValues is allowed for parameter '$valueName'", "multival_$valueName"); + } + if (is_array($allowedValues)) { + $unknownValues = array_diff($valuesList, $allowedValues); + if ($unknownValues) { + $this->dieUsage('Unrecognised value' . (count($unknownValues) > 1 ? "s '" : " '") . implode("', '", $unknownValues) . "' for parameter '$valueName'", "unknown_$valueName"); + } + } + + return $allowMultiple ? $valuesList : $valuesList[0]; + } + + /** + * Validate the value against the minimum and user/bot maximum limits. Prints usage info on failure. + */ + function validateLimit($varname, $value, $min, $max, $botMax) { + global $wgUser; + + if ($value < $min) { + $this->dieUsage("$varname may not be less than $min (set to $value)", $varname); + } + + if ($this->getMain()->isBot()) { + if ($value > $botMax) { + $this->dieUsage("$varname may not be over $botMax (set to $value) for bots", $varname); + } + } + elseif ($value > $max) { + $this->dieUsage("$varname may not be over $max (set to $value) for users", $varname); + } + } + + /** + * Call main module's error handler + */ + public function dieUsage($description, $errorCode, $httpRespCode = 0) { + $this->getMain()->mainDieUsage($description, $this->encodeParamName($errorCode), $httpRespCode); + } + + /** + * Internal code errors should be reported with this method + */ + protected static function dieDebug($method, $message) { + wfDebugDieBacktrace("Internal error in $method: $message"); + } + + /** + * Profiling: total module execution time + */ + private $mTimeIn = 0, $mModuleTime = 0; + + /** + * Start module profiling + */ + public function profileIn() { + if ($this->mTimeIn !== 0) + ApiBase :: dieDebug(__METHOD__, 'called twice without calling profileOut()'); + $this->mTimeIn = microtime(true); + } + + /** + * End module profiling + */ + public function profileOut() { + if ($this->mTimeIn === 0) + ApiBase :: dieDebug(__METHOD__, 'called without calling profileIn() first'); + if ($this->mDBTimeIn !== 0) + ApiBase :: dieDebug(__METHOD__, 'must be called after database profiling is done with profileDBOut()'); + + $this->mModuleTime += microtime(true) - $this->mTimeIn; + $this->mTimeIn = 0; + } + + /** + * Total time the module was executed + */ + public function getProfileTime() { + if ($this->mTimeIn !== 0) + ApiBase :: dieDebug(__METHOD__, 'called without calling profileOut() first'); + return $this->mModuleTime; + } + + /** + * Profiling: database execution time + */ + private $mDBTimeIn = 0, $mDBTime = 0; + + /** + * Start module profiling + */ + public function profileDBIn() { + if ($this->mTimeIn === 0) + ApiBase :: dieDebug(__METHOD__, 'must be called while profiling the entire module with profileIn()'); + if ($this->mDBTimeIn !== 0) + ApiBase :: dieDebug(__METHOD__, 'called twice without calling profileDBOut()'); + $this->mDBTimeIn = microtime(true); + } + + /** + * End database profiling + */ + public function profileDBOut() { + if ($this->mTimeIn === 0) + ApiBase :: dieDebug(__METHOD__, 'must be called while profiling the entire module with profileIn()'); + if ($this->mDBTimeIn === 0) + ApiBase :: dieDebug(__METHOD__, 'called without calling profileDBIn() first'); + + $time = microtime(true) - $this->mDBTimeIn; + $this->mDBTimeIn = 0; + + $this->mDBTime += $time; + $this->getMain()->mDBTime += $time; + } + + /** + * Total time the module used the database + */ + public function getProfileDBTime() { + if ($this->mDBTimeIn !== 0) + ApiBase :: dieDebug(__METHOD__, 'called without calling profileDBOut() first'); + return $this->mDBTime; + } + + public abstract function getVersion(); + + public static function getBaseVersion() { + return __CLASS__ . ': $Id: ApiBase.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php new file mode 100644 index 00000000..6f5b4aca --- /dev/null +++ b/includes/api/ApiFormatBase.php @@ -0,0 +1,161 @@ +<?php + + +/* + * Created on Sep 19, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiBase.php'); +} + +abstract class ApiFormatBase extends ApiBase { + + private $mIsHtml, $mFormat; + + /** + * Constructor + */ + public function __construct($main, $format) { + parent :: __construct($main, $format); + + $this->mIsHtml = (substr($format, -2, 2) === 'fm'); // ends with 'fm' + if ($this->mIsHtml) + $this->mFormat = substr($format, 0, -2); // remove ending 'fm' + else + $this->mFormat = $format; + $this->mFormat = strtoupper($this->mFormat); + } + + /** + * Overriding class returns the mime type that should be sent to the client. + * This method is not called if getIsHtml() returns true. + * @return string + */ + public abstract function getMimeType(); + + public function getNeedsRawData() { + return false; + } + + /** + * Returns true when an HTML filtering printer should be used. + * The default implementation assumes that formats ending with 'fm' + * should be formatted in HTML. + */ + public function getIsHtml() { + return $this->mIsHtml; + } + + /** + * Initialize the printer function and prepares the output headers, etc. + * This method must be the first outputing method during execution. + * A help screen's header is printed for the HTML-based output + */ + function initPrinter($isError) { + $isHtml = $this->getIsHtml(); + $mime = $isHtml ? 'text/html' : $this->getMimeType(); + header("Content-Type: $mime; charset=utf-8;"); + + if ($isHtml) { +?> + <html> + <head> + <title>MediaWiki API</title> + </head> + <body> +<?php + + + if (!$isError) { +?> + <br/> + <small> + This result is being shown in <?=$this->mFormat?> format, + which might not be suitable for your application.<br/> + See <a href='api.php'>API help</a> for more information.<br/> + </small> +<?php + + + } +?> + <pre> +<?php + + + } + } + + /** + * Finish printing. Closes HTML tags. + */ + public function closePrinter() { + if ($this->getIsHtml()) { +?> + </pre> + </body> +<?php + + + } + } + + public function printText($text) { + if ($this->getIsHtml()) + echo $this->formatHTML($text); + else + echo $text; + } + + /** + * Prety-print various elements in HTML format, such as xml tags and URLs. + * This method also replaces any '<' with < + */ + protected function formatHTML($text) { + // encode all tags as safe blue strings + $text = ereg_replace('\<([^>]+)\>', '<font color=blue><\1></font>', $text); + // identify URLs + $text = ereg_replace("[a-zA-Z]+://[^ '()<\n]+", '<a href="\\0">\\0</a>', $text); + // identify requests to api.php + $text = ereg_replace("api\\.php\\?[^ ()<\n\t]+", '<a href="\\0">\\0</a>', $text); + // make strings inside * bold + $text = ereg_replace("\\*[^<>\n]+\\*", '<b>\\0</b>', $text); + // make strings inside $ italic + $text = ereg_replace("\\$[^<>\n]+\\$", '<b><i>\\0</i></b>', $text); + + return $text; + } + + /** + * Returns usage examples for this format. + */ + protected function getExamples() { + return 'api.php?action=query&meta=siteinfo&si=namespaces&format=' . $this->getModuleName(); + } + + public static function getBaseVersion() { + return __CLASS__ . ': $Id: ApiFormatBase.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php new file mode 100644 index 00000000..fdc29cf2 --- /dev/null +++ b/includes/api/ApiFormatJson.php @@ -0,0 +1,56 @@ +<?php + + +/* + * Created on Sep 19, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiFormatBase.php'); +} + +class ApiFormatJson extends ApiFormatBase { + + public function __construct($main, $format) { + parent :: __construct($main, $format); + } + + public function getMimeType() { + return 'application/json'; + } + + public function execute() { + require ('ApiFormatJson_json.php'); + $json = new Services_JSON(); + $this->printText($json->encode($this->getResultData(), true)); + } + + protected function getDescription() { + return 'Output data in JSON format'; + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiFormatJson.php 16725 2006-10-01 21:20:55Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiFormatJson_json.php b/includes/api/ApiFormatJson_json.php new file mode 100644 index 00000000..375de7eb --- /dev/null +++ b/includes/api/ApiFormatJson_json.php @@ -0,0 +1,841 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ + +/** +* Converts to and from JSON format. +* +* JSON (JavaScript Object Notation) is a lightweight data-interchange +* format. It is easy for humans to read and write. It is easy for machines +* to parse and generate. It is based on a subset of the JavaScript +* Programming Language, Standard ECMA-262 3rd Edition - December 1999. +* This feature can also be found in Python. JSON is a text format that is +* completely language independent but uses conventions that are familiar +* to programmers of the C-family of languages, including C, C++, C#, Java, +* JavaScript, Perl, TCL, and many others. These properties make JSON an +* ideal data-interchange language. +* +* This package provides a simple encoder and decoder for JSON notation. It +* is intended for use with client-side Javascript applications that make +* use of HTTPRequest to perform server communication functions - data can +* be encoded into JSON notation for use in a client-side javascript, or +* decoded from incoming Javascript requests. JSON format is native to +* Javascript, and can be directly eval()'ed with no further parsing +* overhead +* +* All strings should be in ASCII or UTF-8 format! +* +* LICENSE: Redistribution and use in source and binary forms, with or +* without modification, are permitted provided that the following +* conditions are met: Redistributions of source code must retain the +* above copyright notice, this list of conditions and the following +* disclaimer. Redistributions in binary form must reproduce the above +* copyright notice, this list of conditions and the following disclaimer +* in the documentation and/or other materials provided with the +* distribution. +* +* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED +* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +* NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +* DAMAGE. +* +* @category +* @package Services_JSON +* @author Michal Migurski <mike-json@teczno.com> +* @author Matt Knapp <mdknapp[at]gmail[dot]com> +* @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com> +* @copyright 2005 Michal Migurski +* @version CVS: $Id: JSON.php,v 1.30 2006/03/08 16:10:20 migurski Exp $ +* @license http://www.opensource.org/licenses/bsd-license.php +* @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 +*/ + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_SLICE', 1); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_STR', 2); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_ARR', 3); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_OBJ', 4); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_CMT', 5); + +/** +* Behavior switch for Services_JSON::decode() +*/ +define('SERVICES_JSON_LOOSE_TYPE', 16); + +/** +* Behavior switch for Services_JSON::decode() +*/ +define('SERVICES_JSON_SUPPRESS_ERRORS', 32); + +/** +* Converts to and from JSON format. +* +* Brief example of use: +* +* <code> +* // create a new instance of Services_JSON +* $json = new Services_JSON(); +* +* // convert a complexe value to JSON notation, and send it to the browser +* $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); +* $output = $json->encode($value); +* +* print($output); +* // prints: ["foo","bar",[1,2,"baz"],[3,[4]]] +* +* // accept incoming POST data, assumed to be in JSON notation +* $input = file_get_contents('php://input', 1000000); +* $value = $json->decode($input); +* </code> +*/ +class Services_JSON +{ + /** + * constructs a new JSON instance + * + * @param int $use object behavior flags; combine with boolean-OR + * + * possible values: + * - SERVICES_JSON_LOOSE_TYPE: loose typing. + * "{...}" syntax creates associative arrays + * instead of objects in decode(). + * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression. + * Values which can't be encoded (e.g. resources) + * appear as NULL instead of throwing errors. + * By default, a deeply-nested resource will + * bubble up with an error, so all return values + * from encode() should be checked with isError() + */ + function Services_JSON($use = 0) + { + $this->use = $use; + } + + /** + * convert a string from one UTF-16 char to one UTF-8 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf16 UTF-16 character + * @return string UTF-8 character + * @access private + */ + function utf162utf8($utf16) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); + } + + $bytes = (ord($utf16{0}) << 8) | ord($utf16{1}); + + switch(true) { + case ((0x7F & $bytes) == $bytes): + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x7F & $bytes); + + case (0x07FF & $bytes) == $bytes: + // return a 2-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xC0 | (($bytes >> 6) & 0x1F)) + . chr(0x80 | ($bytes & 0x3F)); + + case (0xFFFF & $bytes) == $bytes: + // return a 3-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xE0 | (($bytes >> 12) & 0x0F)) + . chr(0x80 | (($bytes >> 6) & 0x3F)) + . chr(0x80 | ($bytes & 0x3F)); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * convert a string from one UTF-8 char to one UTF-16 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf8 UTF-8 character + * @return string UTF-16 character + * @access private + */ + function utf82utf16($utf8) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); + } + + switch(strlen($utf8)) { + case 1: + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return $utf8; + + case 2: + // return a UTF-16 character from a 2-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x07 & (ord($utf8{0}) >> 2)) + . chr((0xC0 & (ord($utf8{0}) << 6)) + | (0x3F & ord($utf8{1}))); + + case 3: + // return a UTF-16 character from a 3-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr((0xF0 & (ord($utf8{0}) << 4)) + | (0x0F & (ord($utf8{1}) >> 2))) + . chr((0xC0 & (ord($utf8{1}) << 6)) + | (0x7F & ord($utf8{2}))); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * @param bool $pretty pretty-print output with indents and newlines + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access public + */ + function encode($var, $pretty=false) + { + $this->indent = 0; + $this->pretty = $pretty; + $this->nameValSeparator = $pretty ? ': ' : ':'; + return $this->encode2($var); + } + + /** + * encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access private + */ + function encode2($var) + { + if ($this->pretty) { + $close = "\n" . str_repeat("\t", $this->indent); + $open = $close . "\t"; + $mid = ',' . $open; + } + else { + $open = $close = ''; + $mid = ','; + } + + switch (gettype($var)) { + case 'boolean': + return $var ? 'true' : 'false'; + + case 'NULL': + return 'null'; + + case 'integer': + return (int) $var; + + case 'double': + case 'float': + return (float) $var; + + case 'string': + // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT + $ascii = ''; + $strlen_var = strlen($var); + + /* + * Iterate over every character in the string, + * escaping with a slash or encoding to UTF-8 where necessary + */ + for ($c = 0; $c < $strlen_var; ++$c) { + + $ord_var_c = ord($var{$c}); + + switch (true) { + case $ord_var_c == 0x08: + $ascii .= '\b'; + break; + case $ord_var_c == 0x09: + $ascii .= '\t'; + break; + case $ord_var_c == 0x0A: + $ascii .= '\n'; + break; + case $ord_var_c == 0x0C: + $ascii .= '\f'; + break; + case $ord_var_c == 0x0D: + $ascii .= '\r'; + break; + + case $ord_var_c == 0x22: + case $ord_var_c == 0x2F: + case $ord_var_c == 0x5C: + // double quote, slash, slosh + $ascii .= '\\'.$var{$c}; + break; + + case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): + // characters U-00000000 - U-0000007F (same as ASCII) + $ascii .= $var{$c}; + break; + + case (($ord_var_c & 0xE0) == 0xC0): + // characters U-00000080 - U-000007FF, mask 110XXXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, ord($var{$c + 1})); + $c += 1; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF0) == 0xE0): + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2})); + $c += 2; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF8) == 0xF0): + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3})); + $c += 3; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFC) == 0xF8): + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4})); + $c += 4; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFE) == 0xFC): + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4}), + ord($var{$c + 5})); + $c += 5; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + } + } + + return '"'.$ascii.'"'; + + case 'array': + /* + * As per JSON spec if any array key is not an integer + * we must treat the the whole array as an object. We + * also try to catch a sparsely populated associative + * array with numeric keys here because some JS engines + * will create an array with empty indexes up to + * max_index which can cause memory issues and because + * the keys, which may be relevant, will be remapped + * otherwise. + * + * As per the ECMA and JSON specification an object may + * have any string as a property. Unfortunately due to + * a hole in the ECMA specification if the key is a + * ECMA reserved word or starts with a digit the + * parameter is only accessible using ECMAScript's + * bracket notation. + */ + + // treat as a JSON object + if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) { + $this->indent++; + $properties = array_map(array($this, 'name_value'), + array_keys($var), + array_values($var)); + $this->indent--; + + foreach($properties as $property) { + if(Services_JSON::isError($property)) { + return $property; + } + } + + return '{' . $open . join($mid, $properties) . $close . '}'; + } + + // treat it like a regular array + $this->indent++; + $elements = array_map(array($this, 'encode2'), $var); + $this->indent--; + + foreach($elements as $element) { + if(Services_JSON::isError($element)) { + return $element; + } + } + + return '[' . $open . join($mid, $elements) . $close . ']'; + + case 'object': + $vars = get_object_vars($var); + + $this->indent++; + $properties = array_map(array($this, 'name_value'), + array_keys($vars), + array_values($vars)); + $this->indent--; + + foreach($properties as $property) { + if(Services_JSON::isError($property)) { + return $property; + } + } + + return '{' . $open . join($mid, $properties) . $close . '}'; + + default: + return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) + ? 'null' + : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string"); + } + } + + /** + * array-walking function for use in generating JSON-formatted name-value pairs + * + * @param string $name name of key to use + * @param mixed $value reference to an array element to be encoded + * + * @return string JSON-formatted name-value pair, like '"name":value' + * @access private + */ + function name_value($name, $value) + { + $encoded_value = $this->encode2($value); + + if(Services_JSON::isError($encoded_value)) { + return $encoded_value; + } + + return $this->encode2(strval($name)) . $this->nameValSeparator . $encoded_value; + } + + /** + * reduce a string by removing leading and trailing comments and whitespace + * + * @param $str string string value to strip of comments and whitespace + * + * @return string string value stripped of comments and whitespace + * @access private + */ + function reduce_string($str) + { + $str = preg_replace(array( + + // eliminate single line comments in '// ...' form + '#^\s*//(.+)$#m', + + // eliminate multi-line comments in '/* ... */' form, at start of string + '#^\s*/\*(.+)\*/#Us', + + // eliminate multi-line comments in '/* ... */' form, at end of string + '#/\*(.+)\*/\s*$#Us' + + ), '', $str); + + // eliminate extraneous space + return trim($str); + } + + /** + * decodes a JSON string into appropriate variable + * + * @param string $str JSON-formatted string + * + * @return mixed number, boolean, string, array, or object + * corresponding to given JSON input string. + * See argument 1 to Services_JSON() above for object-output behavior. + * Note that decode() always returns strings + * in ASCII or UTF-8 format! + * @access public + */ + function decode($str) + { + $str = $this->reduce_string($str); + + switch (strtolower($str)) { + case 'true': + return true; + + case 'false': + return false; + + case 'null': + return null; + + default: + $m = array(); + + if (is_numeric($str)) { + // Lookie-loo, it's a number + + // This would work on its own, but I'm trying to be + // good about returning integers where appropriate: + // return (float)$str; + + // Return float or int, as appropriate + return ((float)$str == (integer)$str) + ? (integer)$str + : (float)$str; + + } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) { + // STRINGS RETURNED IN UTF-8 FORMAT + $delim = substr($str, 0, 1); + $chrs = substr($str, 1, -1); + $utf8 = ''; + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c < $strlen_chrs; ++$c) { + + $substr_chrs_c_2 = substr($chrs, $c, 2); + $ord_chrs_c = ord($chrs{$c}); + + switch (true) { + case $substr_chrs_c_2 == '\b': + $utf8 .= chr(0x08); + ++$c; + break; + case $substr_chrs_c_2 == '\t': + $utf8 .= chr(0x09); + ++$c; + break; + case $substr_chrs_c_2 == '\n': + $utf8 .= chr(0x0A); + ++$c; + break; + case $substr_chrs_c_2 == '\f': + $utf8 .= chr(0x0C); + ++$c; + break; + case $substr_chrs_c_2 == '\r': + $utf8 .= chr(0x0D); + ++$c; + break; + + case $substr_chrs_c_2 == '\\"': + case $substr_chrs_c_2 == '\\\'': + case $substr_chrs_c_2 == '\\\\': + case $substr_chrs_c_2 == '\\/': + if (($delim == '"' && $substr_chrs_c_2 != '\\\'') || + ($delim == "'" && $substr_chrs_c_2 != '\\"')) { + $utf8 .= $chrs{++$c}; + } + break; + + case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)): + // single, escaped unicode character + $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) + . chr(hexdec(substr($chrs, ($c + 4), 2))); + $utf8 .= $this->utf162utf8($utf16); + $c += 5; + break; + + case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): + $utf8 .= $chrs{$c}; + break; + + case ($ord_chrs_c & 0xE0) == 0xC0: + // characters U-00000080 - U-000007FF, mask 110XXXXX + //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 2); + ++$c; + break; + + case ($ord_chrs_c & 0xF0) == 0xE0: + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 3); + $c += 2; + break; + + case ($ord_chrs_c & 0xF8) == 0xF0: + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 4); + $c += 3; + break; + + case ($ord_chrs_c & 0xFC) == 0xF8: + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 5); + $c += 4; + break; + + case ($ord_chrs_c & 0xFE) == 0xFC: + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 6); + $c += 5; + break; + + } + + } + + return $utf8; + + } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) { + // array, or object notation + + if ($str{0} == '[') { + $stk = array(SERVICES_JSON_IN_ARR); + $arr = array(); + } else { + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = array(); + } else { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = new stdClass(); + } + } + + array_push($stk, array('what' => SERVICES_JSON_SLICE, + 'where' => 0, + 'delim' => false)); + + $chrs = substr($str, 1, -1); + $chrs = $this->reduce_string($chrs); + + if ($chrs == '') { + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } else { + return $obj; + + } + } + + //print("\nparsing {$chrs}\n"); + + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c <= $strlen_chrs; ++$c) { + + $top = end($stk); + $substr_chrs_c_2 = substr($chrs, $c, 2); + + if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) { + // found a comma that is not inside a string, array, etc., + // OR we've reached the end of the character list + $slice = substr($chrs, $top['where'], ($c - $top['where'])); + array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false)); + //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + // we are in an array, so just push an element onto the stack + array_push($arr, $this->decode($slice)); + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + // we are in an object, so figure + // out the property name and set an + // element in an associative array, + // for now + $parts = array(); + + if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // "name":value pair + $key = $this->decode($parts[1]); + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // name:value pair, where name is unquoted + $key = $parts[1]; + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } + + } + + } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { + // found a quote, and we are not inside a string + array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c})); + //print("Found start of string at {$c}\n"); + + } elseif (($chrs{$c} == $top['delim']) && + ($top['what'] == SERVICES_JSON_IN_STR) && + (($chrs{$c - 1} != '\\') || + ($chrs{$c - 1} == '\\' && $chrs{$c - 2} == '\\'))) { + // found a quote, we're in a string, and it's not escaped + array_pop($stk); + //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '[') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-bracket, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false)); + //print("Found start of array at {$c}\n"); + + } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { + // found a right-bracket, and we're in an array + array_pop($stk); + //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '{') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-brace, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false)); + //print("Found start of object at {$c}\n"); + + } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { + // found a right-brace, and we're in an object + array_pop($stk); + //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($substr_chrs_c_2 == '/*') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a comment start, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false)); + $c++; + //print("Found start of comment at {$c}\n"); + + } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { + // found a comment end, and we're in one now + array_pop($stk); + $c++; + + for ($i = $top['where']; $i <= $c; ++$i) + $chrs = substr_replace($chrs, ' ', $i, 1); + + //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } + + } + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + return $obj; + + } + + } + } + } + + /** + * @todo Ultimately, this should just call PEAR::isError() + */ + function isError($data, $code = null) + { + if (class_exists('pear')) { + return PEAR::isError($data, $code); + } elseif (is_object($data) && (get_class($data) == 'services_json_error' || + is_subclass_of($data, 'services_json_error'))) { + return true; + } + + return false; + } +} + +if (class_exists('PEAR_Error')) { + + class Services_JSON_Error extends PEAR_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + parent::PEAR_Error($message, $code, $mode, $options, $userinfo); + } + } + +} else { + + /** + * @todo Ultimately, this class shall be descended from PEAR_Error + */ + class Services_JSON_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + + } + } + +} + +?> diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php new file mode 100644 index 00000000..6aa08e00 --- /dev/null +++ b/includes/api/ApiFormatXml.php @@ -0,0 +1,161 @@ +<?php + + +/* + * Created on Sep 19, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiFormatBase.php'); +} + +class ApiFormatXml extends ApiFormatBase { + + public function __construct($main, $format) { + parent :: __construct($main, $format); + } + + public function getMimeType() { + return 'text/xml'; + } + + public function getNeedsRawData() { + return true; + } + + public function execute() { + $xmlindent = null; + extract($this->extractRequestParams()); + + if ($xmlindent || $this->getIsHtml()) + $xmlindent = -2; + else + $xmlindent = null; + + $this->printText('<?xml version="1.0" encoding="utf-8"?>'); + $this->recXmlPrint('api', $this->getResultData(), $xmlindent); + } + + /** + * This method takes an array and converts it into an xml. + * There are several noteworthy cases: + * + * If array contains a key '_element', then the code assumes that ALL other keys are not important and replaces them with the value['_element']. + * Example: name='root', value = array( '_element'=>'page', 'x', 'y', 'z') creates <root> <page>x</page> <page>y</page> <page>z</page> </root> + * + * If any of the array's element key is '*', then the code treats all other key->value pairs as attributes, and the value['*'] as the element's content. + * Example: name='root', value = array( '*'=>'text', 'lang'=>'en', 'id'=>10) creates <root lang='en' id='10'>text</root> + * + * If neither key is found, all keys become element names, and values become element content. + * The method is recursive, so the same rules apply to any sub-arrays. + */ + function recXmlPrint($elemName, $elemValue, $indent) { + if (!is_null($indent)) { + $indent += 2; + $indstr = "\n" . str_repeat(" ", $indent); + } else { + $indstr = ''; + } + + switch (gettype($elemValue)) { + case 'array' : + + if (isset ($elemValue['*'])) { + $subElemContent = $elemValue['*']; + unset ($elemValue['*']); + } else { + $subElemContent = null; + } + + if (isset ($elemValue['_element'])) { + $subElemIndName = $elemValue['_element']; + unset ($elemValue['_element']); + } else { + $subElemIndName = null; + } + + $indElements = array (); + $subElements = array (); + foreach ($elemValue as $subElemId => & $subElemValue) { + if (gettype($subElemId) === 'integer') { + if (!is_array($subElemValue)) + ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has a scalar indexed value."); + $indElements[] = $subElemValue; + unset ($elemValue[$subElemId]); + } elseif (is_array($subElemValue)) { + $subElements[$subElemId] = $subElemValue; + unset ($elemValue[$subElemId]); + } + } + + if (is_null($subElemIndName) && !empty ($indElements)) + ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has integer keys without _element value"); + + if (!empty ($subElements) && !empty ($indElements) && !is_null($subElemContent)) + ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has content and subelements"); + + if (!is_null($subElemContent)) { + $this->printText($indstr . wfElement($elemName, $elemValue, $subElemContent)); + } elseif (empty ($indElements) && empty ($subElements)) { + $this->printText($indstr . wfElement($elemName, $elemValue)); + } else { + $this->printText($indstr . wfElement($elemName, $elemValue, null)); + + foreach ($subElements as $subElemId => & $subElemValue) + $this->recXmlPrint($subElemId, $subElemValue, $indent); + + foreach ($indElements as $subElemId => & $subElemValue) + $this->recXmlPrint($subElemIndName, $subElemValue, $indent); + + $this->printText($indstr . wfCloseElement($elemName)); + } + break; + case 'object' : + // ignore + break; + default : + $this->printText($indstr . wfElement($elemName, null, $elemValue)); + break; + } + } + protected function getDescription() { + return 'Output data in XML format'; + } + + protected function getAllowedParams() { + return array ( + 'xmlindent' => false + ); + } + + protected function getParamDescription() { + return array ( + 'xmlindent' => 'Enable XML indentation' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiFormatXml.php 16725 2006-10-01 21:20:55Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php new file mode 100644 index 00000000..bd74f01a --- /dev/null +++ b/includes/api/ApiFormatYaml.php @@ -0,0 +1,55 @@ +<?php + + +/* + * Created on Sep 19, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiFormatBase.php'); +} + +class ApiFormatYaml extends ApiFormatBase { + + public function __construct($main, $format) { + parent :: __construct($main, $format); + } + + public function getMimeType() { + return 'application/yaml'; + } + + public function execute() { + require ('ApiFormatYaml_spyc.php'); + $this->printText(Spyc :: YAMLDump($this->getResultData())); + } + + protected function getDescription() { + return 'Output data in YAML format'; + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiFormatYaml.php 16725 2006-10-01 21:20:55Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiFormatYaml_spyc.php b/includes/api/ApiFormatYaml_spyc.php new file mode 100644 index 00000000..05a39e23 --- /dev/null +++ b/includes/api/ApiFormatYaml_spyc.php @@ -0,0 +1,854 @@ +<?php + /** + * Spyc -- A Simple PHP YAML Class + * @version 0.2.3 -- 2006-02-04 + * @author Chris Wanstrath <chris@ozmm.org> + * @link http://spyc.sourceforge.net/ + * @copyright Copyright 2005-2006 Chris Wanstrath + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @package Spyc + */ + + /** + * A node, used by Spyc for parsing YAML. + * @package Spyc + */ + class YAMLNode { + /**#@+ + * @access public + * @var string + */ + var $parent; + var $id; + /**#@+*/ + /** + * @access public + * @var mixed + */ + var $data; + /** + * @access public + * @var int + */ + var $indent; + /** + * @access public + * @var bool + */ + var $children = false; + + /** + * The constructor assigns the node a unique ID. + * @access public + * @return void + */ + function YAMLNode() { + $this->id = uniqid(''); + } + } + + /** + * The Simple PHP YAML Class. + * + * This class can be used to read a YAML file and convert its contents + * into a PHP array. It currently supports a very limited subsection of + * the YAML spec. + * + * Usage: + * <code> + * $parser = new Spyc; + * $array = $parser->load($file); + * </code> + * @package Spyc + */ + class Spyc { + + /** + * Load YAML into a PHP array statically + * + * The load method, when supplied with a YAML stream (string or file), + * will do its best to convert YAML in a file into a PHP array. Pretty + * simple. + * Usage: + * <code> + * $array = Spyc::YAMLLoad('lucky.yml'); + * print_r($array); + * </code> + * @access public + * @return array + * @param string $input Path of YAML file or string containing YAML + */ + function YAMLLoad($input) { + $spyc = new Spyc; + return $spyc->load($input); + } + + /** + * Dump YAML from PHP array statically + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as nothing.yml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @access public + * @static + * @return string + * @param array $array PHP array + * @param int $indent Pass in false to use the default, which is 2 + * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) + */ + public static function YAMLDump($array,$indent = false,$wordwrap = false) { + $spyc = new Spyc; + return $spyc->dump($array,$indent,$wordwrap); + } + + /** + * Load YAML into a PHP array from an instantiated object + * + * The load method, when supplied with a YAML stream (string or file path), + * will do its best to convert the YAML into a PHP array. Pretty simple. + * Usage: + * <code> + * $parser = new Spyc; + * $array = $parser->load('lucky.yml'); + * print_r($array); + * </code> + * @access public + * @return array + * @param string $input Path of YAML file or string containing YAML + */ + function load($input) { + // See what type of input we're talking about + // If it's not a file, assume it's a string + if (!empty($input) && (strpos($input, "\n") === false) + && file_exists($input)) { + $yaml = file($input); + } else { + $yaml = explode("\n",$input); + } + // Initiate some objects and values + $base = new YAMLNode; + $base->indent = 0; + $this->_lastIndent = 0; + $this->_lastNode = $base->id; + $this->_inBlock = false; + $this->_isInline = false; + + foreach ($yaml as $linenum => $line) { + $ifchk = trim($line); + + // If the line starts with a tab (instead of a space), throw a fit. + if (preg_match('/^(\t)+(\w+)/', $line)) { + $err = 'ERROR: Line '. ($linenum + 1) .' in your input YAML begins'. + ' with a tab. YAML only recognizes spaces. Please reformat.'; + die($err); + } + + if ($this->_inBlock === false && empty($ifchk)) { + continue; + } elseif ($this->_inBlock == true && empty($ifchk)) { + $last =& $this->_allNodes[$this->_lastNode]; + $last->data[key($last->data)] .= "\n"; + } elseif ($ifchk{0} != '#' && substr($ifchk,0,3) != '---') { + // Create a new node and get its indent + $node = new YAMLNode; + $node->indent = $this->_getIndent($line); + + // Check where the node lies in the hierarchy + if ($this->_lastIndent == $node->indent) { + // If we're in a block, add the text to the parent's data + if ($this->_inBlock === true) { + $parent =& $this->_allNodes[$this->_lastNode]; + $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd; + } else { + // The current node's parent is the same as the previous node's + if (isset($this->_allNodes[$this->_lastNode])) { + $node->parent = $this->_allNodes[$this->_lastNode]->parent; + } + } + } elseif ($this->_lastIndent < $node->indent) { + if ($this->_inBlock === true) { + $parent =& $this->_allNodes[$this->_lastNode]; + $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd; + } elseif ($this->_inBlock === false) { + // The current node's parent is the previous node + $node->parent = $this->_lastNode; + + // If the value of the last node's data was > or | we need to + // start blocking i.e. taking in all lines as a text value until + // we drop our indent. + $parent =& $this->_allNodes[$node->parent]; + $this->_allNodes[$node->parent]->children = true; + if (is_array($parent->data)) { + $chk = $parent->data[key($parent->data)]; + if ($chk === '>') { + $this->_inBlock = true; + $this->_blockEnd = ' '; + $parent->data[key($parent->data)] = + str_replace('>','',$parent->data[key($parent->data)]); + $parent->data[key($parent->data)] .= trim($line).' '; + $this->_allNodes[$node->parent]->children = false; + $this->_lastIndent = $node->indent; + } elseif ($chk === '|') { + $this->_inBlock = true; + $this->_blockEnd = "\n"; + $parent->data[key($parent->data)] = + str_replace('|','',$parent->data[key($parent->data)]); + $parent->data[key($parent->data)] .= trim($line)."\n"; + $this->_allNodes[$node->parent]->children = false; + $this->_lastIndent = $node->indent; + } + } + } + } elseif ($this->_lastIndent > $node->indent) { + // Any block we had going is dead now + if ($this->_inBlock === true) { + $this->_inBlock = false; + if ($this->_blockEnd = "\n") { + $last =& $this->_allNodes[$this->_lastNode]; + $last->data[key($last->data)] = + trim($last->data[key($last->data)]); + } + } + + // We don't know the parent of the node so we have to find it + // foreach ($this->_allNodes as $n) { + foreach ($this->_indentSort[$node->indent] as $n) { + if ($n->indent == $node->indent) { + $node->parent = $n->parent; + } + } + } + + if ($this->_inBlock === false) { + // Set these properties with information from our current node + $this->_lastIndent = $node->indent; + // Set the last node + $this->_lastNode = $node->id; + // Parse the YAML line and return its data + $node->data = $this->_parseLine($line); + // Add the node to the master list + $this->_allNodes[$node->id] = $node; + // Add a reference to the node in an indent array + $this->_indentSort[$node->indent][] =& $this->_allNodes[$node->id]; + // Add a reference to the node in a References array if this node + // has a YAML reference in it. + if ( + ( (is_array($node->data)) && + isset($node->data[key($node->data)]) && + (!is_array($node->data[key($node->data)])) ) + && + ( (preg_match('/^&([^ ]+)/',$node->data[key($node->data)])) + || + (preg_match('/^\*([^ ]+)/',$node->data[key($node->data)])) ) + ) { + $this->_haveRefs[] =& $this->_allNodes[$node->id]; + } elseif ( + ( (is_array($node->data)) && + isset($node->data[key($node->data)]) && + (is_array($node->data[key($node->data)])) ) + ) { + // Incomplete reference making code. Ugly, needs cleaned up. + foreach ($node->data[key($node->data)] as $d) { + if ( !is_array($d) && + ( (preg_match('/^&([^ ]+)/',$d)) + || + (preg_match('/^\*([^ ]+)/',$d)) ) + ) { + $this->_haveRefs[] =& $this->_allNodes[$node->id]; + } + } + } + } + } + } + unset($node); + + // Here we travel through node-space and pick out references (& and *) + $this->_linkReferences(); + + // Build the PHP array out of node-space + $trunk = $this->_buildArray(); + return $trunk; + } + + /** + * Dump PHP array to YAML + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as tasteful.yml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @access public + * @return string + * @param array $array PHP array + * @param int $indent Pass in false to use the default, which is 2 + * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) + */ + function dump($array,$indent = false,$wordwrap = false) { + // Dumps to some very clean YAML. We'll have to add some more features + // and options soon. And better support for folding. + + // New features and options. + if ($indent === false or !is_numeric($indent)) { + $this->_dumpIndent = 2; + } else { + $this->_dumpIndent = $indent; + } + + if ($wordwrap === false or !is_numeric($wordwrap)) { + $this->_dumpWordWrap = 40; + } else { + $this->_dumpWordWrap = $wordwrap; + } + + // New YAML document + $string = "---\n"; + + // Start at the base of the array and move through it. + foreach ($array as $key => $value) { + $string .= $this->_yamlize($key,$value,0); + } + return $string; + } + + /**** Private Properties ****/ + + /**#@+ + * @access private + * @var mixed + */ + var $_haveRefs; + var $_allNodes; + var $_lastIndent; + var $_lastNode; + var $_inBlock; + var $_isInline; + var $_dumpIndent; + var $_dumpWordWrap; + /**#@+*/ + + /**** Private Methods ****/ + + /** + * Attempts to convert a key / value array item to YAML + * @access private + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + function _yamlize($key,$value,$indent) { + if (is_array($value)) { + // It has children. What to do? + // Make it the right kind of item + $string = $this->_dumpNode($key,NULL,$indent); + // Add the indent + $indent += $this->_dumpIndent; + // Yamlize the array + $string .= $this->_yamlizeArray($value,$indent); + } elseif (!is_array($value)) { + // It doesn't have children. Yip. + $string = $this->_dumpNode($key,$value,$indent); + } + return $string; + } + + /** + * Attempts to convert an array to YAML + * @access private + * @return string + * @param $array The array you want to convert + * @param $indent The indent of the current level + */ + function _yamlizeArray($array,$indent) { + if (is_array($array)) { + $string = ''; + foreach ($array as $key => $value) { + $string .= $this->_yamlize($key,$value,$indent); + } + return $string; + } else { + return false; + } + } + + /** + * Returns YAML from a key and a value + * @access private + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + function _dumpNode($key,$value,$indent) { + // do some folding here, for blocks + if (strpos($value,"\n")) { + $value = $this->_doLiteralBlock($value,$indent); + } else { + $value = $this->_doFolding($value,$indent); + } + + $spaces = str_repeat(' ',$indent); + + if (is_int($key)) { + // It's a sequence + $string = $spaces.'- '.$value."\n"; + } else { + // It's mapped + $string = $spaces.$key.': '.$value."\n"; + } + return $string; + } + + /** + * Creates a literal block for dumping + * @access private + * @return string + * @param $value + * @param $indent int The value of the indent + */ + function _doLiteralBlock($value,$indent) { + $exploded = explode("\n",$value); + $newValue = '|'; + $indent += $this->_dumpIndent; + $spaces = str_repeat(' ',$indent); + foreach ($exploded as $line) { + $newValue .= "\n" . $spaces . trim($line); + } + return $newValue; + } + + /** + * Folds a string of text, if necessary + * @access private + * @return string + * @param $value The string you wish to fold + */ + function _doFolding($value,$indent) { + // Don't do anything if wordwrap is set to 0 + if ($this->_dumpWordWrap === 0) { + return $value; + } + + if (strlen($value) > $this->_dumpWordWrap) { + $indent += $this->_dumpIndent; + $indent = str_repeat(' ',$indent); + $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent"); + $value = ">\n".$indent.$wrapped; + } + return $value; + } + + /* Methods used in loading */ + + /** + * Finds and returns the indentation of a YAML line + * @access private + * @return int + * @param string $line A line from the YAML file + */ + function _getIndent($line) { + preg_match('/^\s{1,}/',$line,$match); + if (!empty($match[0])) { + $indent = substr_count($match[0],' '); + } else { + $indent = 0; + } + return $indent; + } + + /** + * Parses YAML code and returns an array for a node + * @access private + * @return array + * @param string $line A line from the YAML file + */ + function _parseLine($line) { + $line = trim($line); + + $array = array(); + + if (preg_match('/^-(.*):$/',$line)) { + // It's a mapped sequence + $key = trim(substr(substr($line,1),0,-1)); + $array[$key] = ''; + } elseif ($line[0] == '-' && substr($line,0,3) != '---') { + // It's a list item but not a new stream + if (strlen($line) > 1) { + $value = trim(substr($line,1)); + // Set the type of the value. Int, string, etc + $value = $this->_toType($value); + $array[] = $value; + } else { + $array[] = array(); + } + } elseif (preg_match('/^(.+):/',$line,$key)) { + // It's a key/value pair most likely + // If the key is in double quotes pull it out + if (preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) { + $value = trim(str_replace($matches[1],'',$line)); + $key = $matches[2]; + } else { + // Do some guesswork as to the key and the value + $explode = explode(':',$line); + $key = trim($explode[0]); + array_shift($explode); + $value = trim(implode(':',$explode)); + } + + // Set the type of the value. Int, string, etc + $value = $this->_toType($value); + if (empty($key)) { + $array[] = $value; + } else { + $array[$key] = $value; + } + } + return $array; + } + + /** + * Finds the type of the passed value, returns the value as the new type. + * @access private + * @param string $value + * @return mixed + */ + function _toType($value) { + if (preg_match('/^("(.*)"|\'(.*)\')/',$value,$matches)) { + $value = (string)preg_replace('/(\'\'|\\\\\')/',"'",end($matches)); + $value = preg_replace('/\\\\"/','"',$value); + } elseif (preg_match('/^\\[(.+)\\]$/',$value,$matches)) { + // Inline Sequence + + // Take out strings sequences and mappings + $explode = $this->_inlineEscape($matches[1]); + + // Propogate value array + $value = array(); + foreach ($explode as $v) { + $value[] = $this->_toType($v); + } + } elseif (strpos($value,': ')!==false && !preg_match('/^{(.+)/',$value)) { + // It's a map + $array = explode(': ',$value); + $key = trim($array[0]); + array_shift($array); + $value = trim(implode(': ',$array)); + $value = $this->_toType($value); + $value = array($key => $value); + } elseif (preg_match("/{(.+)}$/",$value,$matches)) { + // Inline Mapping + + // Take out strings sequences and mappings + $explode = $this->_inlineEscape($matches[1]); + + // Propogate value array + $array = array(); + foreach ($explode as $v) { + $array = $array + $this->_toType($v); + } + $value = $array; + } elseif (strtolower($value) == 'null' or $value == '' or $value == '~') { + $value = NULL; + } elseif (ctype_digit($value)) { + $value = (int)$value; + } elseif (in_array(strtolower($value), + array('true', 'on', '+', 'yes', 'y'))) { + $value = TRUE; + } elseif (in_array(strtolower($value), + array('false', 'off', '-', 'no', 'n'))) { + $value = FALSE; + } elseif (is_numeric($value)) { + $value = (float)$value; + } else { + // Just a normal string, right? + $value = trim(preg_replace('/#(.+)$/','',$value)); + } + + return $value; + } + + /** + * Used in inlines to check for more inlines or quoted strings + * @access private + * @return array + */ + function _inlineEscape($inline) { + // There's gotta be a cleaner way to do this... + // While pure sequences seem to be nesting just fine, + // pure mappings and mappings with sequences inside can't go very + // deep. This needs to be fixed. + + // Check for strings + $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; + if (preg_match_all($regex,$inline,$strings)) { + $saved_strings[] = $strings[0][0]; + $inline = preg_replace($regex,'YAMLString',$inline); + } + unset($regex); + + // Check for sequences + if (preg_match_all('/\[(.+)\]/U',$inline,$seqs)) { + $inline = preg_replace('/\[(.+)\]/U','YAMLSeq',$inline); + $seqs = $seqs[0]; + } + + // Check for mappings + if (preg_match_all('/{(.+)}/U',$inline,$maps)) { + $inline = preg_replace('/{(.+)}/U','YAMLMap',$inline); + $maps = $maps[0]; + } + + $explode = explode(', ',$inline); + + // Re-add the strings + if (!empty($saved_strings)) { + $i = 0; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLString')) { + $explode[$key] = str_replace('YAMLString',$saved_strings[$i],$value); + ++$i; + } + } + } + + // Re-add the sequences + if (!empty($seqs)) { + $i = 0; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLSeq') !== false) { + $explode[$key] = str_replace('YAMLSeq',$seqs[$i],$value); + ++$i; + } + } + } + + // Re-add the mappings + if (!empty($maps)) { + $i = 0; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLMap') !== false) { + $explode[$key] = str_replace('YAMLMap',$maps[$i],$value); + ++$i; + } + } + } + + return $explode; + } + + /** + * Builds the PHP array from all the YAML nodes we've gathered + * @access private + * @return array + */ + function _buildArray() { + $trunk = array(); + + if (!isset($this->_indentSort[0])) { + return $trunk; + } + + foreach ($this->_indentSort[0] as $n) { + if (empty($n->parent)) { + $this->_nodeArrayizeData($n); + // Check for references and copy the needed data to complete them. + $this->_makeReferences($n); + // Merge our data with the big array we're building + $trunk = $this->_array_kmerge($trunk,$n->data); + } + } + + return $trunk; + } + + /** + * Traverses node-space and sets references (& and *) accordingly + * @access private + * @return bool + */ + function _linkReferences() { + if (is_array($this->_haveRefs)) { + foreach ($this->_haveRefs as $node) { + if (!empty($node->data)) { + $key = key($node->data); + // If it's an array, don't check. + if (is_array($node->data[$key])) { + foreach ($node->data[$key] as $k => $v) { + $this->_linkRef($node,$key,$k,$v); + } + } else { + $this->_linkRef($node,$key); + } + } + } + } + return true; + } + + function _linkRef(&$n,$key,$k = NULL,$v = NULL) { + if (empty($k) && empty($v)) { + // Look for &refs + if (preg_match('/^&([^ ]+)/',$n->data[$key],$matches)) { + // Flag the node so we know it's a reference + $this->_allNodes[$n->id]->ref = substr($matches[0],1); + $this->_allNodes[$n->id]->data[$key] = + substr($n->data[$key],strlen($matches[0])+1); + // Look for *refs + } elseif (preg_match('/^\*([^ ]+)/',$n->data[$key],$matches)) { + $ref = substr($matches[0],1); + // Flag the node as having a reference + $this->_allNodes[$n->id]->refKey = $ref; + } + } elseif (!empty($k) && !empty($v)) { + if (preg_match('/^&([^ ]+)/',$v,$matches)) { + // Flag the node so we know it's a reference + $this->_allNodes[$n->id]->ref = substr($matches[0],1); + $this->_allNodes[$n->id]->data[$key][$k] = + substr($v,strlen($matches[0])+1); + // Look for *refs + } elseif (preg_match('/^\*([^ ]+)/',$v,$matches)) { + $ref = substr($matches[0],1); + // Flag the node as having a reference + $this->_allNodes[$n->id]->refKey = $ref; + } + } + } + + /** + * Finds the children of a node and aids in the building of the PHP array + * @access private + * @param int $nid The id of the node whose children we're gathering + * @return array + */ + function _gatherChildren($nid) { + $return = array(); + $node =& $this->_allNodes[$nid]; + foreach ($this->_allNodes as $z) { + if ($z->parent == $node->id) { + // We found a child + $this->_nodeArrayizeData($z); + // Check for references + $this->_makeReferences($z); + // Merge with the big array we're returning + // The big array being all the data of the children of our parent node + $return = $this->_array_kmerge($return,$z->data); + } + } + return $return; + } + + /** + * Turns a node's data and its children's data into a PHP array + * + * @access private + * @param array $node The node which you want to arrayize + * @return boolean + */ + function _nodeArrayizeData(&$node) { + if (is_array($node->data) && $node->children == true) { + // This node has children, so we need to find them + $childs = $this->_gatherChildren($node->id); + // We've gathered all our children's data and are ready to use it + $key = key($node->data); + $key = empty($key) ? 0 : $key; + // If it's an array, add to it of course + if (is_array($node->data[$key])) { + $node->data[$key] = $this->_array_kmerge($node->data[$key],$childs); + } else { + $node->data[$key] = $childs; + } + } elseif (!is_array($node->data) && $node->children == true) { + // Same as above, find the children of this node + $childs = $this->_gatherChildren($node->id); + $node->data = array(); + $node->data[] = $childs; + } + + // We edited $node by reference, so just return true + return true; + } + + /** + * Traverses node-space and copies references to / from this object. + * @access private + * @param object $z A node whose references we wish to make real + * @return bool + */ + function _makeReferences(&$z) { + // It is a reference + if (isset($z->ref)) { + $key = key($z->data); + // Copy the data to this object for easy retrieval later + $this->ref[$z->ref] =& $z->data[$key]; + // It has a reference + } elseif (isset($z->refKey)) { + if (isset($this->ref[$z->refKey])) { + $key = key($z->data); + // Copy the data from this object to make the node a real reference + $z->data[$key] =& $this->ref[$z->refKey]; + } + } + return true; + } + + + /** + * Merges arrays and maintains numeric keys. + * + * An ever-so-slightly modified version of the array_kmerge() function posted + * to php.net by mail at nospam dot iaindooley dot com on 2004-04-08. + * + * http://us3.php.net/manual/en/function.array-merge.php#41394 + * + * @access private + * @param array $arr1 + * @param array $arr2 + * @return array + */ + function _array_kmerge($arr1,$arr2) { + if(!is_array($arr1)) + $arr1 = array(); + + if(!is_array($arr2)) + $arr2 = array(); + + $keys1 = array_keys($arr1); + $keys2 = array_keys($arr2); + $keys = array_merge($keys1,$keys2); + $vals1 = array_values($arr1); + $vals2 = array_values($arr2); + $vals = array_merge($vals1,$vals2); + $ret = array(); + + foreach($keys as $key) { + list($unused,$val) = each($vals); + // This is the good part! If a key already exists, but it's part of a + // sequence (an int), just keep addin numbers until we find a fresh one. + if (isset($ret[$key]) and is_int($key)) { + while (array_key_exists($key, $ret)) { + $key++; + } + } + $ret[$key] = $val; + } + + return $ret; + } + } +?>
\ No newline at end of file diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php new file mode 100644 index 00000000..33fb67fd --- /dev/null +++ b/includes/api/ApiHelp.php @@ -0,0 +1,55 @@ +<?php + + +/* + * Created on Sep 6, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiBase.php'); +} + +class ApiHelp extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + /** + * Stub module for displaying help when no parameters are given + */ + public function execute() { + $this->dieUsage('', 'help'); + } + + protected function getDescription() { + return array ( + 'Display this help screen.' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiHelp.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php new file mode 100644 index 00000000..2aa571c1 --- /dev/null +++ b/includes/api/ApiLogin.php @@ -0,0 +1,122 @@ +<?php + + +/* + * Created on Sep 19, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiBase.php'); +} + +class ApiLogin extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action, 'lg'); + } + + public function execute() { + $name = $password = $domain = null; + extract($this->extractRequestParams()); + + $params = new FauxRequest(array ( + 'wpName' => $name, + 'wpPassword' => $password, + 'wpDomain' => $domain, + 'wpRemember' => '' + )); + + $result = array (); + + $loginForm = new LoginForm($params); + switch ($loginForm->authenticateUserData()) { + case LoginForm :: SUCCESS : + global $wgUser; + + $wgUser->setOption('rememberpassword', 1); + $wgUser->setCookies(); + + $result['result'] = 'Success'; + $result['lguserid'] = $_SESSION['wsUserID']; + $result['lgusername'] = $_SESSION['wsUserName']; + $result['lgtoken'] = $_SESSION['wsToken']; + break; + + case LoginForm :: NO_NAME : + $result['result'] = 'NoName'; + break; + case LoginForm :: ILLEGAL : + $result['result'] = 'Illegal'; + break; + case LoginForm :: WRONG_PLUGIN_PASS : + $result['result'] = 'WrongPluginPass'; + break; + case LoginForm :: NOT_EXISTS : + $result['result'] = 'NotExists'; + break; + case LoginForm :: WRONG_PASS : + $result['result'] = 'WrongPass'; + break; + case LoginForm :: EMPTY_PASS : + $result['result'] = 'EmptyPass'; + break; + default : + ApiBase :: dieDebug(__METHOD__, 'Unhandled case value'); + } + + $this->getResult()->addValue(null, 'login', $result); + } + + protected function getAllowedParams() { + return array ( + 'name' => '', + 'password' => '', + 'domain' => null + ); + } + + protected function getParamDescription() { + return array ( + 'name' => 'User Name', + 'password' => 'Password', + 'domain' => 'Domain (optional)' + ); + } + + protected function getDescription() { + return array ( + 'This module is used to login and get the authentication tokens.' + ); + } + + protected function getExamples() { + return array( + 'api.php?action=login&lgname=user&lgpassword=password' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiLogin.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?> diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php new file mode 100644 index 00000000..046d7d7c --- /dev/null +++ b/includes/api/ApiMain.php @@ -0,0 +1,226 @@ +<?php + + +/* + * Created on Sep 4, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiBase.php'); +} + +class ApiMain extends ApiBase { + + private $mPrinter, $mModules, $mModuleNames, $mFormats, $mFormatNames; + private $mApiStartTime, $mResult, $mShowVersions, $mEnableWrite; + + /** + * Constructor + * $apiStartTime - time of the originating call for profiling purposes + * $modules - an array of actions (keys) and classes that handle them (values) + */ + public function __construct($apiStartTime, $modules, $formats, $enableWrite) { + // Special handling for the main module: $parent === $this + parent :: __construct($this, 'main'); + + $this->mModules = $modules; + $this->mModuleNames = array_keys($modules); + $this->mFormats = $formats; + $this->mFormatNames = array_keys($formats); + $this->mApiStartTime = $apiStartTime; + $this->mResult = new ApiResult($this); + $this->mShowVersions = false; + $this->mEnableWrite = $enableWrite; + } + + public function & getResult() { + return $this->mResult; + } + + public function getShowVersions() { + return $this->mShowVersions; + } + + public function requestWriteMode() { + if (!$this->mEnableWrite) + $this->dieUsage('Editing of this site is disabled. Make sure the $wgEnableWriteAPI=true; ' . + 'statement is included in the site\'s LocalSettings.php file', 'readonly'); + } + + protected function getAllowedParams() { + return array ( + 'format' => array ( + ApiBase :: PARAM_DFLT => API_DEFAULT_FORMAT, + ApiBase :: PARAM_TYPE => $this->mFormatNames + ), + 'action' => array ( + ApiBase :: PARAM_DFLT => 'help', + ApiBase :: PARAM_TYPE => $this->mModuleNames + ), + 'version' => false + ); + } + + protected function getParamDescription() { + return array ( + 'format' => 'The format of the output', + 'action' => 'What action you would like to perform', + 'version' => 'When showing help, include version for each module' + ); + } + + public function execute() { + $this->profileIn(); + $action = $format = $version = null; + try { + extract($this->extractRequestParams()); + $this->mShowVersions = $version; + + // Create an appropriate printer + $this->mPrinter = new $this->mFormats[$format] ($this, $format); + + // Instantiate and execute module requested by the user + $module = new $this->mModules[$action] ($this, $action); + $module->profileIn(); + $module->execute(); + $module->profileOut(); + $this->printResult(false); + + } catch (UsageException $e) { + + // Printer may not be initialized if the extractRequestParams() fails for the main module + if (!isset ($this->mPrinter)) + $this->mPrinter = new $this->mFormats[API_DEFAULT_FORMAT] ($this, API_DEFAULT_FORMAT); + $this->printResult(true); + + } + $this->profileOut(); + } + + /** + * Internal printer + */ + private function printResult($isError) { + $printer = $this->mPrinter; + $printer->profileIn(); + $printer->initPrinter($isError); + if (!$printer->getNeedsRawData()) + $this->getResult()->SanitizeData(); + $printer->execute(); + $printer->closePrinter(); + $printer->profileOut(); + } + + protected function getDescription() { + return array ( + '', + 'This API allows programs to access various functions of MediaWiki software.', + 'For more details see API Home Page @ http://meta.wikimedia.org/wiki/API', + '' + ); + } + + public function mainDieUsage($description, $errorCode, $httpRespCode = 0) { + $this->mResult->Reset(); + if ($httpRespCode === 0) + header($errorCode, true); + else + header($errorCode, true, $httpRespCode); + + $data = array ( + 'code' => $errorCode, + 'info' => $description + ); + ApiResult :: setContent($data, $this->makeHelpMsg()); + $this->mResult->addValue(null, 'error', $data); + + throw new UsageException($description, $errorCode); + } + + /** + * Override the parent to generate help messages for all available modules. + */ + public function makeHelpMsg() { + + // Use parent to make default message for the main module + $msg = parent :: makeHelpMsg(); + + $astriks = str_repeat('*** ', 10); + $msg .= "\n\n$astriks Modules $astriks\n\n"; + foreach ($this->mModules as $moduleName => $moduleClass) { + $msg .= "* action=$moduleName *"; + $module = new $this->mModules[$moduleName] ($this, $moduleName); + $msg2 = $module->makeHelpMsg(); + if ($msg2 !== false) + $msg .= $msg2; + $msg .= "\n"; + } + + $msg .= "\n$astriks Formats $astriks\n\n"; + foreach ($this->mFormats as $moduleName => $moduleClass) { + $msg .= "* format=$moduleName *"; + $module = new $this->mFormats[$moduleName] ($this, $moduleName); + $msg2 = $module->makeHelpMsg(); + if ($msg2 !== false) + $msg .= $msg2; + $msg .= "\n"; + } + + return $msg; + } + + private $mIsBot = null; + public function isBot() { + if (!isset ($this->mIsBot)) { + global $wgUser; + $this->mIsBot = $wgUser->isAllowed('bot'); + } + return $this->mIsBot; + } + + public function getVersion() { + $vers = array (); + $vers[] = __CLASS__ . ': $Id: ApiMain.php 16820 2006-10-06 01:02:14Z yurik $'; + $vers[] = ApiBase :: getBaseVersion(); + $vers[] = ApiFormatBase :: getBaseVersion(); + $vers[] = ApiQueryBase :: getBaseVersion(); + return $vers; + } +} + +/** +* @desc This exception will be thrown when dieUsage is called to stop module execution. +*/ +class UsageException extends Exception { + + private $codestr; + + public function __construct($message, $codestr) { + parent :: __construct($message); + $this->codestr = $codestr; + } + public function __toString() { + return "{$this->codestr}: {$this->message}"; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php new file mode 100644 index 00000000..d2384b39 --- /dev/null +++ b/includes/api/ApiPageSet.php @@ -0,0 +1,514 @@ +<?php + + +/* + * Created on Sep 24, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +class ApiPageSet extends ApiQueryBase { + + private $mAllPages; // [ns][dbkey] => page_id or 0 when missing + private $mGoodTitles, $mMissingTitles, $mMissingPageIDs, $mRedirectTitles, $mNormalizedTitles; + private $mResolveRedirects, $mPendingRedirectIDs; + + private $mRequestedPageFields; + + public function __construct($query, $resolveRedirects = false) { + parent :: __construct($query, __CLASS__); + + $this->mAllPages = array (); + $this->mGoodTitles = array (); + $this->mMissingTitles = array (); + $this->mMissingPageIDs = array (); + $this->mRedirectTitles = array (); + $this->mNormalizedTitles = array (); + + $this->mRequestedPageFields = array (); + $this->mResolveRedirects = $resolveRedirects; + if($resolveRedirects) + $this->mPendingRedirectIDs = array(); + } + + public function isResolvingRedirects() { + return $this->mResolveRedirects; + } + + public function requestField($fieldName) { + $this->mRequestedPageFields[$fieldName] = null; + } + + public function getCustomField($fieldName) { + return $this->mRequestedPageFields[$fieldName]; + } + + /** + * Get fields that modules have requested from the page table + */ + public function getPageTableFields() { + // Ensure we get minimum required fields + $pageFlds = array ( + 'page_id' => null, + 'page_namespace' => null, + 'page_title' => null + ); + + // only store non-default fields + $this->mRequestedPageFields = array_diff_key($this->mRequestedPageFields, $pageFlds); + + if ($this->mResolveRedirects) + $pageFlds['page_is_redirect'] = null; + + return array_keys(array_merge($pageFlds, $this->mRequestedPageFields)); + } + + /** + * Title objects that were found in the database. + * @return array page_id (int) => Title (obj) + */ + public function getGoodTitles() { + return $this->mGoodTitles; + } + + /** + * Returns the number of unique pages (not revisions) in the set. + */ + public function getGoodTitleCount() { + return count($this->getGoodTitles()); + } + + /** + * Title objects that were NOT found in the database. + * @return array of Title objects + */ + public function getMissingTitles() { + return $this->mMissingTitles; + } + + /** + * Page IDs that were not found in the database + * @return array of page IDs + */ + public function getMissingPageIDs() { + return $this->mMissingPageIDs; + } + + /** + * Get a list of redirects when doing redirect resolution + * @return array prefixed_title (string) => prefixed_title (string) + */ + public function getRedirectTitles() { + return $this->mRedirectTitles; + } + + /** + * Get a list of title normalizations - maps the title given + * with its normalized version. + * @return array raw_prefixed_title (string) => prefixed_title (string) + */ + public function getNormalizedTitles() { + return $this->mNormalizedTitles; + } + + /** + * Get the list of revision IDs (requested with revids= parameter) + */ + public function getRevisionIDs() { + $this->dieUsage(__METHOD__ . ' is not implemented', 'notimplemented'); + } + + /** + * Returns the number of revisions (requested with revids= parameter) + */ + public function getRevisionCount() { + return 0; // TODO: implement + } + + /** + * Populate from the request parameters + */ + public function execute() { + $this->profileIn(); + $titles = $pageids = $revids = null; + extract($this->extractRequestParams()); + + // Only one of the titles/pageids/revids is allowed at the same time + $dataSource = null; + if (isset ($titles)) + $dataSource = 'titles'; + if (isset ($pageids)) { + if (isset ($dataSource)) + $this->dieUsage("Cannot use 'pageids' at the same time as '$dataSource'", 'multisource'); + $dataSource = 'pageids'; + } + if (isset ($revids)) { + if (isset ($dataSource)) + $this->dieUsage("Cannot use 'revids' at the same time as '$dataSource'", 'multisource'); + $dataSource = 'revids'; + } + + switch ($dataSource) { + case 'titles' : + $this->initFromTitles($titles); + break; + case 'pageids' : + $this->initFromPageIds($pageids); + break; + case 'revids' : + $this->initFromRevIDs($revids); + break; + default : + // Do nothing - some queries do not need any of the data sources. + break; + } + $this->profileOut(); + } + + /** + * Initialize PageSet from a list of Titles + */ + public function populateFromTitles($titles) { + $this->profileIn(); + $this->initFromTitles($titles); + $this->profileOut(); + } + + /** + * Initialize PageSet from a list of Page IDs + */ + public function populateFromPageIDs($pageIDs) { + $this->profileIn(); + $pageIDs = array_map('intval', $pageIDs); // paranoia + $this->initFromPageIds($pageIDs); + $this->profileOut(); + } + + /** + * Initialize PageSet from a rowset returned from the database + */ + public function populateFromQueryResult($db, $queryResult) { + $this->profileIn(); + $this->initFromQueryResult($db, $queryResult); + $this->profileOut(); + } + + /** + * Extract all requested fields from the row received from the database + */ + public function processDbRow($row) { + $pageId = intval($row->page_id); + + // Store Title object in various data structures + $title = Title :: makeTitle($row->page_namespace, $row->page_title); + $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId; + + if ($this->mResolveRedirects && $row->page_is_redirect == '1') { + $this->mPendingRedirectIDs[$pageId] = $title; + } else { + $this->mGoodTitles[$pageId] = $title; + } + + foreach ($this->mRequestedPageFields as $fieldName => & $fieldValues) + $fieldValues[$pageId] = $row-> $fieldName; + } + + public function finishPageSetGeneration() { + $this->profileIn(); + $this->resolvePendingRedirects(); + $this->profileOut(); + } + + /** + * This method populates internal variables with page information + * based on the given array of title strings. + * + * Steps: + * #1 For each title, get data from `page` table + * #2 If page was not found in the DB, store it as missing + * + * Additionally, when resolving redirects: + * #3 If no more redirects left, stop. + * #4 For each redirect, get its links from `pagelinks` table. + * #5 Substitute the original LinkBatch object with the new list + * #6 Repeat from step #1 + */ + private function initFromTitles($titles) { + $db = $this->getDB(); + + // Get validated and normalized title objects + $linkBatch = $this->processTitlesStrArray($titles); + $set = $linkBatch->constructSet('page', $db); + + // Get pageIDs data from the `page` table + $this->profileDBIn(); + $res = $db->select('page', $this->getPageTableFields(), $set, __METHOD__); + $this->profileDBOut(); + + // Hack: get the ns:titles stored in array(ns => array(titles)) format + $this->initFromQueryResult($db, $res, $linkBatch->data, true); // process Titles + + // Resolve any found redirects + $this->resolvePendingRedirects(); + } + + private function initFromPageIds($pageids) { + $db = $this->getDB(); + + $set = array ( + 'page_id' => $pageids + ); + + // Get pageIDs data from the `page` table + $this->profileDBIn(); + $res = $db->select('page', $this->getPageTableFields(), $set, __METHOD__); + $this->profileDBOut(); + + $this->initFromQueryResult($db, $res, array_flip($pageids), false); // process PageIDs + + // Resolve any found redirects + $this->resolvePendingRedirects(); + } + + /** + * Iterate through the result of the query on 'page' table, + * and for each row create and store title object and save any extra fields requested. + * @param $db Database + * @param $res DB Query result + * @param $remaining Array of either pageID or ns/title elements (optional). + * If given, any missing items will go to $mMissingPageIDs and $mMissingTitles + * @param $processTitles bool Must be provided together with $remaining. + * If true, treat $remaining as an array of [ns][title] + * If false, treat it as an array of [pageIDs] + * @return Array of redirect IDs (only when resolving redirects) + */ + private function initFromQueryResult($db, $res, &$remaining = null, $processTitles = null) { + if (!is_null($remaining) && is_null($processTitles)) + $this->dieDebug('Missing $processTitles parameter when $remaining is provided'); + + while ($row = $db->fetchObject($res)) { + + $pageId = intval($row->page_id); + + // Remove found page from the list of remaining items + if (isset($remaining)) { + if ($processTitles) + unset ($remaining[$row->page_namespace][$row->page_title]); + else + unset ($remaining[$pageId]); + } + + // Store any extra fields requested by modules + $this->processDbRow($row); + } + $db->freeResult($res); + + if(isset($remaining)) { + // Any items left in the $remaining list are added as missing + if($processTitles) { + // The remaining titles in $remaining are non-existant pages + foreach ($remaining as $ns => $dbkeys) { + foreach ($dbkeys as $dbkey => $nothing) { + $this->mMissingTitles[] = Title :: makeTitle($ns, $dbkey); + $this->mAllPages[$ns][$dbkey] = 0; + } + } + } + else + { + // The remaining pageids do not exist + if(empty($this->mMissingPageIDs)) + $this->mMissingPageIDs = array_keys($remaining); + else + $this->mMissingPageIDs = array_merge($this->mMissingPageIDs, array_keys($remaining)); + } + } + } + + private function initFromRevIDs($revids) { + $this->dieUsage(__METHOD__ . ' is not implemented', 'notimplemented'); + } + + private function resolvePendingRedirects() { + + if($this->mResolveRedirects) { + $db = $this->getDB(); + $pageFlds = $this->getPageTableFields(); + + // Repeat until all redirects have been resolved + // The infinite loop is prevented by keeping all known pages in $this->mAllPages + while (!empty ($this->mPendingRedirectIDs)) { + + // Resolve redirects by querying the pagelinks table, and repeat the process + // Create a new linkBatch object for the next pass + $linkBatch = $this->getRedirectTargets(); + + if ($linkBatch->isEmpty()) + break; + + $set = $linkBatch->constructSet('page', $db); + if(false === $set) + break; + + // Get pageIDs data from the `page` table + $this->profileDBIn(); + $res = $db->select('page', $pageFlds, $set, __METHOD__); + $this->profileDBOut(); + + // Hack: get the ns:titles stored in array(ns => array(titles)) format + $this->initFromQueryResult($db, $res, $linkBatch->data, true); + } + } + } + + private function getRedirectTargets() { + + $linkBatch = new LinkBatch(); + $db = $this->getDB(); + + // find redirect targets for all redirect pages + $this->profileDBIn(); + $res = $db->select('pagelinks', array ( + 'pl_from', + 'pl_namespace', + 'pl_title' + ), array ( + 'pl_from' => array_keys($this->mPendingRedirectIDs + )), __METHOD__); + $this->profileDBOut(); + + while ($row = $db->fetchObject($res)) { + + $plfrom = intval($row->pl_from); + + // Bug 7304 workaround + // ( http://bugzilla.wikipedia.org/show_bug.cgi?id=7304 ) + // A redirect page may have more than one link. + // This code will only use the first link returned. + if (isset ($this->mPendingRedirectIDs[$plfrom])) { // remove line when bug 7304 is fixed + + $titleStrFrom = $this->mPendingRedirectIDs[$plfrom]->getPrefixedText(); + $titleStrTo = Title :: makeTitle($row->pl_namespace, $row->pl_title)->getPrefixedText(); + unset ($this->mPendingRedirectIDs[$plfrom]); // remove line when bug 7304 is fixed + + // Avoid an infinite loop by checking if we have already processed this target + if (!isset ($this->mAllPages[$row->pl_namespace][$row->pl_title])) { + $linkBatch->add($row->pl_namespace, $row->pl_title); + } + } else { + // This redirect page has more than one link. + // This is very slow, but safer until bug 7304 is resolved + $title = Title :: newFromID($plfrom); + $titleStrFrom = $title->getPrefixedText(); + + $article = new Article($title); + $text = $article->getContent(); + $titleTo = Title :: newFromRedirect($text); + $titleStrTo = $titleTo->getPrefixedText(); + + if (is_null($titleStrTo)) + ApiBase :: dieDebug(__METHOD__, 'Bug7304 workaround: redir target from {$title->getPrefixedText()} not found'); + + // Avoid an infinite loop by checking if we have already processed this target + if (!isset ($this->mAllPages[$titleTo->getNamespace()][$titleTo->getDBkey()])) { + $linkBatch->addObj($titleTo); + } + } + + $this->mRedirectTitles[$titleStrFrom] = $titleStrTo; + } + $db->freeResult($res); + + // All IDs must exist in the page table + if (!empty($this->mPendingRedirectIDs[$plfrom])) + $this->dieDebug('Invalid redirect IDs were found'); + + return $linkBatch; + } + + /** + * Given an array of title strings, convert them into Title objects. + * This method validates access rights for the title, + * and appends normalization values to the output. + * + * @return LinkBatch of title objects. + */ + private function processTitlesStrArray($titles) { + + $linkBatch = new LinkBatch(); + + foreach ($titles as $titleString) { + $titleObj = Title :: newFromText($titleString); + + // Validation + if (!$titleObj) + $this->dieUsage("bad title $titleString", 'invalidtitle'); + if ($titleObj->getNamespace() < 0) + $this->dieUsage("No support for special page $titleString has been implemented", 'unsupportednamespace'); + if (!$titleObj->userCanRead()) + $this->dieUsage("No read permission for $titleString", 'titleaccessdenied'); + + $linkBatch->addObj($titleObj); + + // Make sure we remember the original title that was given to us + // This way the caller can correlate new titles with the originally requested, + // i.e. namespace is localized or capitalization is different + if ($titleString !== $titleObj->getPrefixedText()) { + $this->mNormalizedTitles[$titleString] = $titleObj->getPrefixedText(); + } + } + + return $linkBatch; + } + + protected function getAllowedParams() { + return array ( + 'titles' => array ( + ApiBase :: PARAM_ISMULTI => true + ), + 'pageids' => array ( + ApiBase :: PARAM_TYPE => 'integer', + ApiBase :: PARAM_ISMULTI => true + ), + 'revids' => array ( + ApiBase :: PARAM_TYPE => 'integer', + ApiBase :: PARAM_ISMULTI => true + ) + ); + } + + protected function getParamDescription() { + return array ( + 'titles' => 'A list of titles to work on', + 'pageids' => 'A list of page IDs to work on', + 'revids' => 'A list of revision IDs to work on' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiPageSet.php 16820 2006-10-06 01:02:14Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php new file mode 100644 index 00000000..985bde63 --- /dev/null +++ b/includes/api/ApiQuery.php @@ -0,0 +1,354 @@ +<?php + + +/* + * Created on Sep 7, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiBase.php'); +} + +class ApiQuery extends ApiBase { + + private $mPropModuleNames, $mListModuleNames, $mMetaModuleNames; + private $mPageSet; + + private $mQueryPropModules = array ( + 'info' => 'ApiQueryInfo', + 'revisions' => 'ApiQueryRevisions' + ); + // 'categories' => 'ApiQueryCategories', + // 'imageinfo' => 'ApiQueryImageinfo', + // 'langlinks' => 'ApiQueryLanglinks', + // 'links' => 'ApiQueryLinks', + // 'templates' => 'ApiQueryTemplates', + + private $mQueryListModules = array ( + 'allpages' => 'ApiQueryAllpages' + ); + // 'backlinks' => 'ApiQueryBacklinks', + // 'categorymembers' => 'ApiQueryCategorymembers', + // 'embeddedin' => 'ApiQueryEmbeddedin', + // 'imagelinks' => 'ApiQueryImagelinks', + // 'logevents' => 'ApiQueryLogevents', + // 'recentchanges' => 'ApiQueryRecentchanges', + // 'usercontribs' => 'ApiQueryUsercontribs', + // 'users' => 'ApiQueryUsers', + // 'watchlist' => 'ApiQueryWatchlist', + + private $mQueryMetaModules = array ( + 'siteinfo' => 'ApiQuerySiteinfo' + ); + // 'userinfo' => 'ApiQueryUserinfo', + + private $mSlaveDB = null; + + public function __construct($main, $action) { + parent :: __construct($main, $action); + $this->mPropModuleNames = array_keys($this->mQueryPropModules); + $this->mListModuleNames = array_keys($this->mQueryListModules); + $this->mMetaModuleNames = array_keys($this->mQueryMetaModules); + + // Allow the entire list of modules at first, + // but during module instantiation check if it can be used as a generator. + $this->mAllowedGenerators = array_merge($this->mListModuleNames, $this->mPropModuleNames); + } + + public function getDB() { + if (!isset ($this->mSlaveDB)) + $this->mSlaveDB = & wfGetDB(DB_SLAVE); + return $this->mSlaveDB; + } + + public function getPageSet() { + return $this->mPageSet; + } + + /** + * Query execution happens in the following steps: + * #1 Create a PageSet object with any pages requested by the user + * #2 If using generator, execute it to get a new PageSet object + * #3 Instantiate all requested modules. + * This way the PageSet object will know what shared data is required, + * and minimize DB calls. + * #4 Output all normalization and redirect resolution information + * #5 Execute all requested modules + */ + public function execute() { + $prop = $list = $meta = $generator = $redirects = null; + extract($this->extractRequestParams()); + + // + // Create PageSet + // + $this->mPageSet = new ApiPageSet($this, $redirects); + + // Instantiate required modules + $modules = array (); + if (isset ($prop)) + foreach ($prop as $moduleName) + $modules[] = new $this->mQueryPropModules[$moduleName] ($this, $moduleName); + if (isset ($list)) + foreach ($list as $moduleName) + $modules[] = new $this->mQueryListModules[$moduleName] ($this, $moduleName); + if (isset ($meta)) + foreach ($meta as $moduleName) + $modules[] = new $this->mQueryMetaModules[$moduleName] ($this, $moduleName); + + // Modules may optimize data requests through the $this->getPageSet() object + // Execute all requested modules. + foreach ($modules as $module) { + $module->requestExtraData(); + } + + // + // If given, execute generator to substitute user supplied data with generated data. + // + if (isset ($generator)) + $this->executeGeneratorModule($generator, $redirects); + + // + // Populate page information for the given pageSet + // + $this->mPageSet->execute(); + + // + // Record page information (title, namespace, if exists, etc) + // + $this->outputGeneralPageInfo(); + + // + // Execute all requested modules. + // + foreach ($modules as $module) { + $module->profileIn(); + $module->execute(); + $module->profileOut(); + } + } + + private function outputGeneralPageInfo() { + + $pageSet = $this->getPageSet(); + + // Title normalizations + $normValues = array (); + foreach ($pageSet->getNormalizedTitles() as $rawTitleStr => $titleStr) { + $normValues[] = array ( + 'from' => $rawTitleStr, + 'to' => $titleStr + ); + } + + if (!empty ($normValues)) { + ApiResult :: setIndexedTagName($normValues, 'n'); + $this->getResult()->addValue('query', 'normalized', $normValues); + } + + // Show redirect information + $redirValues = array (); + foreach ($pageSet->getRedirectTitles() as $titleStrFrom => $titleStrTo) { + $redirValues[] = array ( + 'from' => $titleStrFrom, + 'to' => $titleStrTo + ); + } + + if (!empty ($redirValues)) { + ApiResult :: setIndexedTagName($redirValues, 'r'); + $this->getResult()->addValue('query', 'redirects', $redirValues); + } + + // + // Page elements + // + $pages = array (); + + // Report any missing titles + $fakepageid = -1; + foreach ($pageSet->getMissingTitles() as $title) { + $pages[$fakepageid--] = array ( + 'ns' => $title->getNamespace(), 'title' => $title->getPrefixedText(), 'missing' => ''); + } + + // Report any missing page ids + foreach ($pageSet->getMissingPageIDs() as $pageid) { + $pages[$pageid] = array ( + 'id' => $pageid, + 'missing' => '' + ); + } + + // Output general page information for found titles + foreach ($pageSet->getGoodTitles() as $pageid => $title) { + $pages[$pageid] = array ( + 'ns' => $title->getNamespace(), 'title' => $title->getPrefixedText(), 'id' => $pageid); + } + + if (!empty ($pages)) { + ApiResult :: setIndexedTagName($pages, 'page'); + $this->getResult()->addValue('query', 'pages', $pages); + } + } + + protected function executeGeneratorModule($generatorName, $redirects) { + + // Find class that implements requested generator + if (isset ($this->mQueryListModules[$generatorName])) { + $className = $this->mQueryListModules[$generatorName]; + } + elseif (isset ($this->mQueryPropModules[$generatorName])) { + $className = $this->mQueryPropModules[$generatorName]; + } else { + ApiBase :: dieDebug(__METHOD__, "Unknown generator=$generatorName"); + } + + // Use current pageset as the result, and create a new one just for the generator + $resultPageSet = $this->mPageSet; + $this->mPageSet = new ApiPageSet($this, $redirects); + + // Create and execute the generator + $generator = new $className ($this, $generatorName); + if (!$generator instanceof ApiQueryGeneratorBase) + $this->dieUsage("Module $generatorName cannot be used as a generator", "badgenerator"); + + $generator->setGeneratorMode(); + $generator->requestExtraData(); + + // execute current pageSet to get the data for the generator module + $this->mPageSet->execute(); + + // populate resultPageSet with the generator output + $generator->profileIn(); + $generator->executeGenerator($resultPageSet); + $resultPageSet->finishPageSetGeneration(); + $generator->profileOut(); + + // Swap the resulting pageset back in + $this->mPageSet = $resultPageSet; + } + + protected function getAllowedParams() { + return array ( + 'prop' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => $this->mPropModuleNames + ), + 'list' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => $this->mListModuleNames + ), + 'meta' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => $this->mMetaModuleNames + ), + 'generator' => array ( + ApiBase :: PARAM_TYPE => $this->mAllowedGenerators + ), + 'redirects' => false + ); + } + + /** + * Override the parent to generate help messages for all available query modules. + */ + public function makeHelpMsg() { + + // Use parent to make default message for the query module + $msg = parent :: makeHelpMsg(); + + // Make sure the internal object is empty + // (just in case a sub-module decides to optimize during instantiation) + $this->mPageSet = null; + + $astriks = str_repeat('--- ', 8); + $msg .= "\n$astriks Query: Prop $astriks\n\n"; + $msg .= $this->makeHelpMsgHelper($this->mQueryPropModules, 'prop'); + $msg .= "\n$astriks Query: List $astriks\n\n"; + $msg .= $this->makeHelpMsgHelper($this->mQueryListModules, 'list'); + $msg .= "\n$astriks Query: Meta $astriks\n\n"; + $msg .= $this->makeHelpMsgHelper($this->mQueryMetaModules, 'meta'); + + return $msg; + } + + private function makeHelpMsgHelper($moduleList, $paramName) { + + $moduleDscriptions = array (); + + foreach ($moduleList as $moduleName => $moduleClass) { + $msg = "* $paramName=$moduleName *"; + $module = new $moduleClass ($this, $moduleName, null); + $msg2 = $module->makeHelpMsg(); + if ($msg2 !== false) + $msg .= $msg2; + if ($module instanceof ApiQueryGeneratorBase) + $msg .= "Generator:\n This module may be used as a generator\n"; + $moduleDscriptions[] = $msg; + } + + return implode("\n", $moduleDscriptions); + } + + /** + * Override to add extra parameters from PageSet + */ + public function makeHelpMsgParameters() { + $psModule = new ApiPageSet($this); + return $psModule->makeHelpMsgParameters() . parent :: makeHelpMsgParameters(); + } + + protected function getParamDescription() { + return array ( + 'prop' => 'Which properties to get for the titles/revisions/pageids', + 'list' => 'Which lists to get', + 'meta' => 'Which meta data to get about the site', + 'generator' => 'Use the output of a list as the input for other prop/list/meta items', + 'redirects' => 'Automatically resolve redirects' + ); + } + + protected function getDescription() { + return array ( + 'Query API module allows applications to get needed pieces of data from the MediaWiki databases,', + 'and is loosely based on the Query API interface currently available on all MediaWiki servers.', + 'All data modifications will first have to use query to acquire a token to prevent abuse from malicious sites.' + ); + } + + protected function getExamples() { + return array ( + 'api.php?action=query&prop=revisions&meta=siteinfo&titles=Main%20Page&rvprop=user|comment' + ); + } + + public function getVersion() { + $psModule = new ApiPageSet($this); + $vers = array (); + $vers[] = __CLASS__ . ': $Id: ApiQuery.php 16820 2006-10-06 01:02:14Z yurik $'; + $vers[] = $psModule->getVersion(); + return $vers; + } +} +?> diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php new file mode 100644 index 00000000..51330d62 --- /dev/null +++ b/includes/api/ApiQueryAllpages.php @@ -0,0 +1,183 @@ +<?php + + +/* + * Created on Sep 25, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +class ApiQueryAllpages extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'ap'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + if ($resultPageSet->isResolvingRedirects()) + $this->dieUsage('Use "gapfilterredir=nonredirects" option instead of "redirects" when using allpages as a generator', 'params'); + + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + $limit = $from = $namespace = $filterredir = null; + extract($this->extractRequestParams()); + + $db = $this->getDB(); + + $where = array ( + 'page_namespace' => $namespace + ); + + if (isset ($from)) { + $where[] = 'page_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($from)); + } + + if ($filterredir === 'redirects') { + $where['page_is_redirect'] = 1; + } + elseif ($filterredir === 'nonredirects') { + $where['page_is_redirect'] = 0; + } + + if (is_null($resultPageSet)) { + $fields = array ( + 'page_id', + 'page_namespace', + 'page_title' + ); + } else { + $fields = $resultPageSet->getPageTableFields(); + } + + $this->profileDBIn(); + $res = $db->select('page', $fields, $where, __CLASS__ . '::' . __METHOD__, array ( + 'USE INDEX' => 'name_title', + 'LIMIT' => $limit +1, + 'ORDER BY' => 'page_namespace, page_title' + )); + $this->profileDBOut(); + + $data = array (); + $count = 0; + while ($row = $db->fetchObject($res)) { + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + $msg = array ( + 'continue' => $this->encodeParamName('from' + ) . '=' . ApiQueryBase :: keyToTitle($row->page_title)); + $this->getResult()->addValue('query-status', 'allpages', $msg); + break; + } + + $title = Title :: makeTitle($row->page_namespace, $row->page_title); + // skip any pages that user has no rights to read + if ($title->userCanRead()) { + + if (is_null($resultPageSet)) { + $id = intval($row->page_id); + $data[] = $id; // in generator mode, just assemble a list of page IDs. + } else { + $resultPageSet->processDbRow($row); + } + } + } + $db->freeResult($res); + + if (is_null($resultPageSet)) { + ApiResult :: setIndexedTagName($data, 'p'); + $this->getResult()->addValue('query', 'allpages', $data); + } + } + + protected function getAllowedParams() { + + global $wgContLang; + $validNamespaces = array (); + foreach (array_keys($wgContLang->getNamespaces()) as $ns) { + if ($ns >= 0) + $validNamespaces[] = $ns; // strval($ns); + } + + return array ( + 'from' => null, + 'namespace' => array ( + ApiBase :: PARAM_DFLT => 0, + ApiBase :: PARAM_TYPE => $validNamespaces + ), + 'filterredir' => array ( + ApiBase :: PARAM_DFLT => 'all', + ApiBase :: PARAM_TYPE => array ( + 'all', + 'redirects', + 'nonredirects' + ) + ), + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX1 => 500, + ApiBase :: PARAM_MAX2 => 5000 + ) + ); + } + + protected function getParamDescription() { + return array ( + 'from' => 'The page title to start enumerating from.', + 'namespace' => 'The namespace to enumerate. Default 0 (Main).', + 'filterredir' => 'Which pages to list: "all" (default), "redirects", or "nonredirects"', + 'limit' => 'How many total pages to return' + ); + } + + protected function getDescription() { + return 'Enumerate all pages sequentially in a given namespace'; + } + + protected function getExamples() { + return array ( + 'Simple Use', + ' api.php?action=query&list=allpages', + ' api.php?action=query&list=allpages&apfrom=B&aplimit=5', + 'Using as Generator', + ' Show info about 4 pages starting at the letter "T"', + ' api.php?action=query&generator=allpages&gaplimit=4&gapfrom=T&prop=info', + ' Show content of first 2 non-redirect pages begining at "Re"', + ' api.php?action=query&generator=allpages&gaplimit=2&gapfilterredir=nonredirects&gapfrom=Re&prop=revisions&rvprop=content' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryAllpages.php 16820 2006-10-06 01:02:14Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php new file mode 100644 index 00000000..574f742e --- /dev/null +++ b/includes/api/ApiQueryBase.php @@ -0,0 +1,112 @@ +<?php + + +/* + * Created on Sep 7, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiBase.php'); +} + +abstract class ApiQueryBase extends ApiBase { + + private $mQueryModule; + + public function __construct($query, $moduleName, $paramPrefix = '') { + parent :: __construct($query->getMain(), $moduleName, $paramPrefix); + $this->mQueryModule = $query; + } + + /** + * Override this method to request extra fields from the pageSet + * using $this->getPageSet()->requestField('fieldName') + */ + public function requestExtraData() { + } + + /** + * Get the main Query module + */ + public function getQuery() { + return $this->mQueryModule; + } + + /** + * Get the Query database connection (readonly) + */ + protected function getDB() { + return $this->getQuery()->getDB(); + } + + /** + * Get the PageSet object to work on + * @return ApiPageSet data + */ + protected function getPageSet() { + return $this->mQueryModule->getPageSet(); + } + + public static function titleToKey($title) { + return str_replace(' ', '_', $title); + } + + public static function keyToTitle($key) { + return str_replace('_', ' ', $key); + } + + public static function getBaseVersion() { + return __CLASS__ . ': $Id: ApiQueryBase.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} + +abstract class ApiQueryGeneratorBase extends ApiQueryBase { + + private $mIsGenerator; + + public function __construct($query, $moduleName, $paramPrefix = '') { + parent :: __construct($query, $moduleName, $paramPrefix); + $mIsGenerator = false; + } + + public function setGeneratorMode() { + $this->mIsGenerator = true; + } + + /** + * Overrides base class to prepend 'g' to every generator parameter + */ + public function encodeParamName($paramName) { + if ($this->mIsGenerator) + return 'g' . parent :: encodeParamName($paramName); + else + return parent :: encodeParamName($paramName); + } + + /** + * Execute this module as a generator + * @param $resultPageSet PageSet: All output should be appended to this object + */ + public abstract function executeGenerator($resultPageSet); +} +?>
\ No newline at end of file diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php new file mode 100644 index 00000000..de651b00 --- /dev/null +++ b/includes/api/ApiQueryInfo.php @@ -0,0 +1,82 @@ +<?php + + +/* + * Created on Sep 25, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +class ApiQueryInfo extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName); + } + + public function requestExtraData() { + $pageSet = $this->getPageSet(); + $pageSet->requestField('page_is_redirect'); + $pageSet->requestField('page_touched'); + $pageSet->requestField('page_latest'); + } + + public function execute() { + + $pageSet = $this->getPageSet(); + $titles = $pageSet->getGoodTitles(); + $result = & $this->getResult(); + + $pageIsRedir = $pageSet->getCustomField('page_is_redirect'); + $pageTouched = $pageSet->getCustomField('page_touched'); + $pageLatest = $pageSet->getCustomField('page_latest'); + + foreach ($titles as $pageid => $title) { + $pageInfo = array ('touched' => $pageTouched[$pageid], 'lastrevid' => $pageLatest[$pageid]); + + if ($pageIsRedir[$pageid]) + $pageInfo['redirect'] = ''; + + $result->addValue(array ( + 'query', + 'pages' + ), $pageid, $pageInfo); + } + } + + protected function getDescription() { + return 'Get basic page information such as namespace, title, last touched date, ...'; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&prop=info&titles=Main%20Page' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryInfo.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php new file mode 100644 index 00000000..f6097bad --- /dev/null +++ b/includes/api/ApiQueryRevisions.php @@ -0,0 +1,320 @@ +<?php + + +/* + * Created on Sep 7, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +class ApiQueryRevisions extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'rv'); + } + + public function execute() { + $limit = $startid = $endid = $start = $end = $dir = $prop = null; + extract($this->extractRequestParams()); + + $db = $this->getDB(); + + // true when ordered by timestamp from older to newer, false otherwise + $dirNewer = ($dir === 'newer'); + + // If any of those parameters are used, work in 'enumeration' mode. + // Enum mode can only be used when exactly one page is provided. + // Enumerating revisions on multiple pages make it extremelly + // difficult to manage continuations and require additional sql indexes + $enumRevMode = ($limit !== 0 || $startid !== 0 || $endid !== 0 || $dirNewer || isset ($start) || isset ($end)); + + $pageSet = $this->getPageSet(); + $pageCount = $pageSet->getGoodTitleCount(); + $revCount = $pageSet->getRevisionCount(); + + // Optimization -- nothing to do + if ($revCount === 0 && $pageCount === 0) + return; + + if ($revCount > 0 && $pageCount > 0) + $this->dieUsage('The revids= parameter may not be used with titles, pageids, or generator options.', 'revids'); + + if ($revCount > 0 && $enumRevMode) + $this->dieUsage('The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).', 'revids'); + + if ($revCount === 0 && $pageCount > 1 && $enumRevMode) + $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, start, and end parameters may only be used on a single page.', 'multpages'); + + $tables = array ( + 'revision' + ); + $fields = array ( + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_minor_edit' + ); + $conds = array ( + 'rev_deleted' => 0 + ); + $options = array (); + + $showTimestamp = $showUser = $showComment = $showContent = false; + if (isset ($prop)) { + foreach ($prop as $p) { + switch ($p) { + case 'timestamp' : + $fields[] = 'rev_timestamp'; + $showTimestamp = true; + break; + case 'user' : + $fields[] = 'rev_user'; + $fields[] = 'rev_user_text'; + $showUser = true; + break; + case 'comment' : + $fields[] = 'rev_comment'; + $showComment = true; + break; + case 'content' : + $tables[] = 'text'; + $conds[] = 'rev_text_id=old_id'; + $fields[] = 'old_id'; + $fields[] = 'old_text'; + $fields[] = 'old_flags'; + $showContent = true; + break; + default : + ApiBase :: dieDebug(__METHOD__, "unknown prop $p"); + } + } + } + + $userMax = ($showContent ? 50 : 500); + $botMax = ($showContent ? 200 : 10000); + + if ($enumRevMode) { + + // This is mostly to prevent parameter errors (and optimize sql?) + if ($startid !== 0 && isset ($start)) + $this->dieUsage('start and startid cannot be used together', 'badparams'); + + if ($endid !== 0 && isset ($end)) + $this->dieUsage('end and endid cannot be used together', 'badparams'); + + // This code makes an assumption that sorting by rev_id and rev_timestamp produces + // the same result. This way users may request revisions starting at a given time, + // but to page through results use the rev_id returned after each page. + // Switching to rev_id removes the potential problem of having more than + // one row with the same timestamp for the same page. + // The order needs to be the same as start parameter to avoid SQL filesort. + $options['ORDER BY'] = ($startid !== 0 ? 'rev_id' : 'rev_timestamp') . ($dirNewer ? '' : ' DESC'); + + $before = ($dirNewer ? '<=' : '>='); + $after = ($dirNewer ? '>=' : '<='); + + if ($startid !== 0) + $conds[] = 'rev_id' . $after . intval($startid); + if ($endid !== 0) + $conds[] = 'rev_id' . $before . intval($endid); + if (isset ($start)) + $conds[] = 'rev_timestamp' . $after . $db->addQuotes($start); + if (isset ($end)) + $conds[] = 'rev_timestamp' . $before . $db->addQuotes($end); + + // must manually initialize unset limit + if (!isset ($limit)) + $limit = 10; + + $this->validateLimit($this->encodeParamName('limit'), $limit, 1, $userMax, $botMax); + + // There is only one ID, use it + $conds['rev_page'] = array_pop(array_keys($pageSet->getGoodTitles())); + + } + elseif ($pageCount > 0) { + // When working in multi-page non-enumeration mode, + // limit to the latest revision only + $tables[] = 'page'; + $conds[] = 'page_id=rev_page'; + $conds[] = 'page_latest=rev_id'; + $this->validateLimit('page_count', $pageCount, 1, $userMax, $botMax); + + // Get all page IDs + $conds['page_id'] = array_keys($pageSet->getGoodTitles()); + + $limit = $pageCount; // assumption testing -- we should never get more then $pageCount rows. + } + elseif ($revCount > 0) { + $this->validateLimit('rev_count', $revCount, 1, $userMax, $botMax); + + // Get all revision IDs + $conds['rev_id'] = array_keys($pageSet->getRevisionIDs()); + + $limit = $revCount; // assumption testing -- we should never get more then $revCount rows. + } else + ApiBase :: dieDebug(__METHOD__, 'param validation?'); + + $options['LIMIT'] = $limit +1; + + $this->profileDBIn(); + $res = $db->select($tables, $fields, $conds, __METHOD__, $options); + $this->profileDBOut(); + + $data = array (); + $count = 0; + while ($row = $db->fetchObject($res)) { + + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + if (!$enumRevMode) + ApiBase :: dieDebug(__METHOD__, 'Got more rows then expected'); // bug report + + $startStr = 'startid=' . $row->rev_id; + $msg = array ( + 'continue' => $startStr + ); + $this->getResult()->addValue('query-status', 'revisions', $msg); + break; + } + + $vals = array ( + 'revid' => intval($row->rev_id + ), 'oldid' => intval($row->rev_text_id)); + + if ($row->rev_minor_edit) { + $vals['minor'] = ''; + } + + if ($showTimestamp) + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rev_timestamp); + + if ($showUser) { + $vals['user'] = $row->rev_user_text; + if (!$row->rev_user) + $vals['anon'] = ''; + } + + if ($showComment) + $vals['comment'] = $row->rev_comment; + + if ($showContent) { + ApiResult :: setContent($vals, Revision :: getRevisionText($row)); + } + + $this->getResult()->addValue(array ( + 'query', + 'pages', + intval($row->rev_page + ), 'revisions'), intval($row->rev_id), $vals); + } + $db->freeResult($res); + + // Ensure that all revisions are shown as '<r>' elements + $data = & $this->getResultData(); + foreach ($data['query']['pages'] as & $page) { + if (is_array($page) && array_key_exists('revisions', $page)) { + ApiResult :: setIndexedTagName($page['revisions'], 'rev'); + } + } + } + + protected function getAllowedParams() { + return array ( + 'prop' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'timestamp', + 'user', + 'comment', + 'content' + ) + ), + 'limit' => array ( + ApiBase :: PARAM_DFLT => 0, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 0, + ApiBase :: PARAM_MAX1 => 50, + ApiBase :: PARAM_MAX2 => 500 + ), + 'startid' => 0, + 'endid' => 0, + 'start' => array ( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'end' => array ( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'dir' => array ( + ApiBase :: PARAM_DFLT => 'older', + ApiBase :: PARAM_TYPE => array ( + 'newer', + 'older' + ) + ) + ); + } + + protected function getParamDescription() { + return array ( + 'prop' => 'Which properties to get for each revision: user|timestamp|comment|content', + 'limit' => 'limit how many revisions will be returned (enum)', + 'startid' => 'from which revision id to start enumeration (enum)', + 'endid' => 'stop revision enumeration on this revid (enum)', + 'start' => 'from which revision timestamp to start enumeration (enum)', + 'end' => 'enumerate up to this timestamp (enum)', + 'dir' => 'direction of enumeration - towards "newer" or "older" revisions (enum)' + ); + } + + protected function getDescription() { + return array ( + 'Get revision information.', + 'This module may be used in several ways:', + ' 1) Get data about a set of pages (last revision), by setting titles or pageids parameter.', + ' 2) Get revisions for one given page, by using titles/pageids with start/end/limit params.', + ' 3) Get data about a set of revisions by setting their IDs with revids parameter.', + 'All parameters marked as (enum) may only be used with a single page (#2).' + ); + } + + protected function getExamples() { + return array ( + 'Get data with content for the last revision of titles "API" and "Main Page":', + ' api.php?action=query&prop=revisions&titles=API|Main%20Page&rvprop=timestamp|user|comment|content', + 'Get last 5 revisions of the "Main Page":', + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment', + 'Get first 5 revisions of the "Main Page":', + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvdir=newer', + 'Get first 5 revisions of the "Main Page" made after 2006-05-01:', + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvdir=newer&rvstart=20060501000000' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryRevisions.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php new file mode 100644 index 00000000..27c3f187 --- /dev/null +++ b/includes/api/ApiQuerySiteinfo.php @@ -0,0 +1,113 @@ +<?php + + +/* + * Created on Sep 25, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +class ApiQuerySiteinfo extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'si'); + } + + public function execute() { + $prop = null; + extract($this->extractRequestParams()); + + foreach ($prop as $p) { + switch ($p) { + + case 'general' : + + global $wgSitename, $wgVersion, $wgCapitalLinks; + $data = array (); + $mainPage = Title :: newFromText(wfMsgForContent('mainpage')); + $data['mainpage'] = $mainPage->getText(); + $data['base'] = $mainPage->getFullUrl(); + $data['sitename'] = $wgSitename; + $data['generator'] = "MediaWiki $wgVersion"; + $data['case'] = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; // 'case-insensitive' option is reserved for future + $this->getResult()->addValue('query', $p, $data); + break; + + case 'namespaces' : + + global $wgContLang; + $data = array (); + foreach ($wgContLang->getFormattedNamespaces() as $ns => $title) { + $data[$ns] = array ( + 'id' => $ns + ); + ApiResult :: setContent($data[$ns], $title); + } + ApiResult :: setIndexedTagName($data, 'ns'); + $this->getResult()->addValue('query', $p, $data); + break; + + default : + ApiBase :: dieDebug(__METHOD__, "Unknown prop=$p"); + } + } + } + + protected function getAllowedParams() { + return array ( + 'prop' => array ( + ApiBase :: PARAM_DFLT => 'general', + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'general', + 'namespaces' + ) + ) + ); + } + + protected function getParamDescription() { + return array ( + 'prop' => array ( + 'Which sysinfo properties to get:', + ' "general" - Overall system information', + ' "namespaces" - List of registered namespaces (localized)' + ) + ); + } + + protected function getDescription() { + return 'Return general information about the site.'; + } + + protected function getExamples() { + return 'api.php?action=query&meta=siteinfo&siprop=general|namespaces'; + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php new file mode 100644 index 00000000..67fbf41e --- /dev/null +++ b/includes/api/ApiResult.php @@ -0,0 +1,153 @@ +<?php + + +/* + * Created on Sep 4, 2006 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiBase.php'); +} + +class ApiResult extends ApiBase { + + private $mData; + + /** + * Constructor + */ + public function __construct($main) { + parent :: __construct($main, 'result'); + $this->Reset(); + } + + public function Reset() { + $this->mData = array (); + } + + function & getData() { + return $this->mData; + } + + /** + * Add an output value to the array by name. + * Verifies that value with the same name has not been added before. + */ + public static function setElement(& $arr, $name, $value) { + if ($arr === null || $name === null || $value === null || !is_array($arr) || is_array($name)) + ApiBase :: dieDebug(__METHOD__, 'Bad parameter'); + + if (!isset ($arr[$name])) { + $arr[$name] = $value; + } + elseif (is_array($arr[$name]) && is_array($value)) { + $merged = array_intersect_key($arr[$name], $value); + if (empty ($merged)) + $arr[$name] += $value; + else + ApiBase :: dieDebug(__METHOD__, "Attempting to merge element $name"); + } else + ApiBase :: dieDebug(__METHOD__, "Attempting to add element $name=$value, existing value is {$arr[$name]}"); + } + + /** + * Adds the content element to the array. + * Use this function instead of hardcoding the '*' element. + */ + public static function setContent(& $arr, $value) { + if (is_array($value)) + ApiBase :: dieDebug(__METHOD__, 'Bad parameter'); + ApiResult :: setElement($arr, '*', $value); + } + + // public static function makeContentElement($tag, $value) { + // $result = array(); + // ApiResult::setContent($result, ) + // } + // + /** + * In case the array contains indexed values (in addition to named), + * all indexed values will have the given tag name. + */ + public static function setIndexedTagName(& $arr, $tag) { + // Do not use setElement() as it is ok to call this more than once + if ($arr === null || $tag === null || !is_array($arr) || is_array($tag)) + ApiBase :: dieDebug(__METHOD__, 'Bad parameter'); + $arr['_element'] = $tag; + } + + /** + * Add value to the output data at the given path. + * Path is an indexed array, each element specifing the branch at which to add the new value + * Setting $path to array('a','b','c') is equivalent to data['a']['b']['c'] = $value + */ + public function addValue($path, $name, $value) { + + $data = & $this->getData(); + + if (isset ($path)) { + if (is_array($path)) { + foreach ($path as $p) { + if (!isset ($data[$p])) + $data[$p] = array (); + $data = & $data[$p]; + } + } else { + if (!isset ($data[$path])) + $data[$path] = array (); + $data = & $data[$path]; + } + } + + ApiResult :: setElement($data, $name, $value); + } + + /** + * Recursivelly removes any elements from the array that begin with an '_'. + * The content element '*' is the only special element that is left. + * Use this method when the entire data object gets sent to the user. + */ + public function SanitizeData() { + ApiResult :: SanitizeDataInt($this->mData); + } + + private static function SanitizeDataInt(& $data) { + foreach ($data as $key => & $value) { + if ($key[0] === '_') { + unset ($data[$key]); + } + elseif (is_array($value)) { + ApiResult :: SanitizeDataInt($value); + } + } + } + + public function execute() { + ApiBase :: dieDebug(__METHOD__, 'execute() is not supported on Result object'); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiResult.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?>
\ No newline at end of file diff --git a/includes/memcached-client.php b/includes/memcached-client.php index 697509e8..b1ba778a 100644 --- a/includes/memcached-client.php +++ b/includes/memcached-client.php @@ -221,6 +221,16 @@ class memcached */ var $_timeout_microseconds; + /** + * Connect timeout in seconds + */ + var $_connect_timeout; + + /** + * Number of connection attempts for each server + */ + var $_connect_attempts; + // }}} // }}} // {{{ methods @@ -250,6 +260,9 @@ class memcached $this->_timeout_seconds = 1; $this->_timeout_microseconds = 0; + + $this->_connect_timeout = 0.01; + $this->_connect_attempts = 3; } // }}} @@ -675,22 +688,33 @@ class memcached * * @param interger $sock Socket to connect * @param string $host Host:IP to connect to - * @param float $timeout (optional) Timeout value, defaults to 0.25s * * @return boolean * @access private */ - function _connect_sock (&$sock, $host, $timeout = 0.25) + function _connect_sock (&$sock, $host) { list ($ip, $port) = explode(":", $host); - if ($this->_persistant == 1) - { - $sock = @pfsockopen($ip, $port, $errno, $errstr, $timeout); - } else - { - $sock = @fsockopen($ip, $port, $errno, $errstr, $timeout); + $sock = false; + $timeout = $this->_connect_timeout; + for ($i = 0; !$sock && $i < $this->_connect_attempts; $i++) { + if ($i > 0) { + # Sleep until the timeout, in case it failed fast + $elapsed = microtime(true) - $t; + if ( $elapsed < $timeout ) { + usleep(($timeout - $elapsed) * 1e6); + } + $timeout *= 2; + } + $t = microtime(true); + if ($this->_persistant == 1) + { + $sock = @pfsockopen($ip, $port, $errno, $errstr, $timeout); + } else + { + $sock = @fsockopen($ip, $port, $errno, $errstr, $timeout); + } } - if (!$sock) { if ($this->_debug) $this->_debugprint( "Error connecting to $host: $errstr\n" ); diff --git a/includes/normal/CleanUpTest.php b/includes/normal/CleanUpTest.php index 4e147cfd..30ec6a95 100644 --- a/includes/normal/CleanUpTest.php +++ b/includes/normal/CleanUpTest.php @@ -412,7 +412,7 @@ class CleanUpTest extends PHPUnit_TestCase { } -$suite =& new PHPUnit_TestSuite( 'CleanUpTest' ); +$suite = new PHPUnit_TestSuite( 'CleanUpTest' ); $result = PHPUnit::run( $suite ); echo $result->toString(); diff --git a/includes/normal/RandomTest.php b/includes/normal/RandomTest.php index 3a5a407b..e2601366 100644 --- a/includes/normal/RandomTest.php +++ b/includes/normal/RandomTest.php @@ -65,8 +65,8 @@ function showDiffs( $a, $b ) { $ota = explode( "\n", str_replace( "\r\n", "\n", $a ) ); $nta = explode( "\n", str_replace( "\r\n", "\n", $b ) ); - $diffs =& new Diff( $ota, $nta ); - $formatter =& new TableDiffFormatter(); + $diffs = new Diff( $ota, $nta ); + $formatter = new TableDiffFormatter(); $funky = $formatter->format( $diffs ); preg_match_all( '/<span class="diffchange">(.*?)<\/span>/', $funky, $matches ); foreach( $matches[1] as $bit ) { @@ -104,4 +104,4 @@ while( true ) { $norm = ''; } -?>
\ No newline at end of file +?> diff --git a/includes/normal/UtfNormal.php b/includes/normal/UtfNormal.php index d8641993..af3809d5 100644 --- a/includes/normal/UtfNormal.php +++ b/includes/normal/UtfNormal.php @@ -33,7 +33,7 @@ */ /** */ -require_once 'UtfNormalUtil.php'; +require_once dirname(__FILE__).'/UtfNormalUtil.php'; global $utfCombiningClass, $utfCanonicalComp, $utfCanonicalDecomp; $utfCombiningClass = NULL; diff --git a/includes/templates/NoLocalSettings.php b/includes/templates/NoLocalSettings.php new file mode 100644 index 00000000..e71dd396 --- /dev/null +++ b/includes/templates/NoLocalSettings.php @@ -0,0 +1,48 @@ +<?php +# Prevent XSS +if ( isset( $wgVersion ) ) { + $wgVersion = htmlspecialchars( $wgVersion ); +} else { + $wgVersion = 'VERSION'; +} +# Set the path in case we hit a page such as /index.php/Main_Page +# Could use <base href> but then we have to worry about http[s]/port #/etc. +$path = ''; +if( isset( $_SERVER['SCRIPT_NAME'] )) { + $path = htmlspecialchars( preg_replace('/index.php/', '', $_SERVER['SCRIPT_NAME']) ); +} +?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'> + <head> + <title>MediaWiki <?php echo $wgVersion ?></title> + <meta http-equiv='Content-Type' content='text/html; charset=utf-8' /> + <style type='text/css' media='screen, projection'> + html, body { + color: #000; + background-color: #fff; + font-family: sans-serif; + text-align: center; + } + + h1 { + font-size: 150%; + } + </style> + </head> + <body> + <img src="<?php echo $path ?>skins/common/images/mediawiki.png" alt='The MediaWiki logo' /> + + <h1>MediaWiki <?php echo $wgVersion ?></h1> + <div class='error'> + <?php + if ( file_exists( 'config/LocalSettings.php' ) ) { + echo( 'To complete the installation, move <tt>config/LocalSettings.php</tt> to the parent directory.' ); + } else { + echo( "Please <a href=\"${path}config/index.php\" title='setup'> set up the wiki</a> first." ); + } + ?> + + </div> + </body> +</html> diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php index 66368669..83ef4920 100644 --- a/includes/templates/Userlogin.php +++ b/includes/templates/Userlogin.php @@ -37,6 +37,7 @@ class UserloginTemplate extends QuickTemplate { <td align='right'><label for='wpName1'><?php $this->msg('yourname') ?>:</label></td> <td align='left'> <input type='text' class='loginText' name="wpName" id="wpName1" + tabindex="1" value="<?php $this->text('name') ?>" size='20' /> </td> </tr> @@ -44,6 +45,7 @@ class UserloginTemplate extends QuickTemplate { <td align='right'><label for='wpPassword1'><?php $this->msg('yourpassword') ?>:</label></td> <td align='left'> <input type='password' class='loginPassword' name="wpPassword" id="wpPassword1" + tabindex="2" value="<?php $this->text('password') ?>" size='20' /> </td> </tr> @@ -56,7 +58,8 @@ class UserloginTemplate extends QuickTemplate { <tr> <td align='right'><?php $this->msg( 'yourdomainname' ) ?>:</td> <td align='left'> - <select name="wpDomain" value="<?php $this->text( 'domain' ) ?>"> + <select name="wpDomain" value="<?php $this->text( 'domain' ) ?>" + tabindex="3"> <?php echo $doms ?> </select> </td> @@ -66,6 +69,7 @@ class UserloginTemplate extends QuickTemplate { <td></td> <td align='left'> <input type='checkbox' name="wpRemember" + tabindex="4" value="1" id="wpRemember" <?php if( $this->data['remember'] ) { ?>checked="checked"<?php } ?> /> <label for="wpRemember"><?php $this->msg('remembermypassword') ?></label> @@ -74,7 +78,8 @@ class UserloginTemplate extends QuickTemplate { <tr> <td></td> <td align='left' style="white-space:nowrap"> - <input type='submit' name="wpLoginattempt" id="wpLoginattempt" value="<?php $this->msg('login') ?>" /> <?php if( $this->data['useemail'] ) { ?><input type='submit' name="wpMailmypassword" id="wpMailmypassword" + <input type='submit' name="wpLoginattempt" id="wpLoginattempt" tabindex="5" value="<?php $this->msg('login') ?>" /> <?php if( $this->data['useemail'] ) { ?><input type='submit' name="wpMailmypassword" id="wpMailmypassword" + tabindex="6" value="<?php $this->msg('mailmypassword') ?>" /> <?php } ?> </td> @@ -113,6 +118,7 @@ class UsercreateTemplate extends QuickTemplate { <td align='right'><label for='wpName2'><?php $this->msg('yourname') ?>:</label></td> <td align='left'> <input type='text' class='loginText' name="wpName" id="wpName2" + tabindex="1" value="<?php $this->text('name') ?>" size='20' /> </td> </tr> @@ -120,6 +126,7 @@ class UsercreateTemplate extends QuickTemplate { <td align='right'><label for='wpPassword2'><?php $this->msg('yourpassword') ?>:</label></td> <td align='left'> <input type='password' class='loginPassword' name="wpPassword" id="wpPassword2" + tabindex="2" value="<?php $this->text('password') ?>" size='20' /> </td> </tr> @@ -132,7 +139,8 @@ class UsercreateTemplate extends QuickTemplate { <tr> <td align='right'><?php $this->msg( 'yourdomainname' ) ?>:</td> <td align='left'> - <select name="wpDomain" value="<?php $this->text( 'domain' ) ?>"> + <select name="wpDomain" value="<?php $this->text( 'domain' ) ?>" + tabindex="3"> <?php echo $doms ?> </select> </td> @@ -142,24 +150,27 @@ class UsercreateTemplate extends QuickTemplate { <td align='right'><label for='wpRetype'><?php $this->msg('yourpasswordagain') ?>:</label></td> <td align='left'> <input type='password' class='loginPassword' name="wpRetype" id="wpRetype" + tabindex="4" value="<?php $this->text('retype') ?>" size='20' /> </td> </tr> <tr> <?php if( $this->data['useemail'] ) { ?> - <td align='right'><label for='wpEmail'><?php $this->msg('youremail') ?>:</label></td> + <td align='right'><label for='wpEmail'><?php $this->msg('youremail') ?></label></td> <td align='left'> <input type='text' class='loginText' name="wpEmail" id="wpEmail" + tabindex="5" value="<?php $this->text('email') ?>" size='20' /> </td> <?php } ?> <?php if( $this->data['userealname'] ) { ?> </tr> <tr> - <td align='right'><label for='wpRealName'><?php $this->msg('yourrealname') ?>:</label></td> + <td align='right'><label for='wpRealName'><?php $this->msg('yourrealname') ?></label></td> <td align='left'> <input type='text' class='loginText' name="wpRealName" id="wpRealName" + tabindex="6" value="<?php $this->text('realname') ?>" size='20' /> </td> <?php } ?> @@ -168,6 +179,7 @@ class UsercreateTemplate extends QuickTemplate { <td></td> <td align='left'> <input type='checkbox' name="wpRemember" + tabindex="7" value="1" id="wpRemember" <?php if( $this->data['remember'] ) { ?>checked="checked"<?php } ?> /> <label for="wpRemember"><?php $this->msg('remembermypassword') ?></label> @@ -177,9 +189,11 @@ class UsercreateTemplate extends QuickTemplate { <td></td> <td align='left'> <input type='submit' name="wpCreateaccount" id="wpCreateaccount" + tabindex="8" value="<?php $this->msg('createaccount') ?>" /> <?php if( $this->data['createemail'] ) { ?> <input type='submit' name="wpCreateaccountMail" id="wpCreateaccountMail" + tabindex="9" value="<?php $this->msg('createaccountmail') ?>" /> <?php } ?> </td> |