diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2006-10-11 20:21:25 +0000 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2006-10-11 20:21:25 +0000 |
commit | d81f562b712f2387fa02290bf2ca86392ab356f2 (patch) | |
tree | d666cdefbe6ac320827a2c6cb473581b46e22c4c /includes | |
parent | 183851b06bd6c52f3cae5375f433da720d410447 (diff) |
Aktualisierung auf Version 1.8.1
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-& |