summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
Diffstat (limited to 'includes')
-rw-r--r--includes/AjaxDispatcher.php51
-rw-r--r--includes/AjaxFunctions.php34
-rw-r--r--includes/AjaxResponse.php32
-rw-r--r--includes/Article.php345
-rw-r--r--includes/AuthPlugin.php5
-rw-r--r--includes/AutoLoader.php50
-rw-r--r--includes/BagOStuff.php134
-rw-r--r--includes/Block.php23
-rw-r--r--includes/CacheDependency.php2
-rw-r--r--includes/CategoryPage.php37
-rw-r--r--includes/Categoryfinder.php2
-rw-r--r--includes/ChangesList.php11
-rw-r--r--includes/CoreParserFunctions.php27
-rw-r--r--includes/Credits.php2
-rw-r--r--includes/Database.php173
-rw-r--r--includes/DatabaseFunctions.php2
-rw-r--r--includes/DatabaseOracle.php18
-rw-r--r--includes/DatabasePostgres.php98
-rw-r--r--includes/DateFormatter.php2
-rw-r--r--includes/DefaultSettings.php319
-rw-r--r--includes/Defines.php58
-rw-r--r--includes/DifferenceEngine.php153
-rw-r--r--includes/DjVuImage.php2
-rw-r--r--includes/EditPage.php258
-rw-r--r--includes/EmaillingJob.php25
-rw-r--r--includes/EnotifNotifyJob.php26
-rw-r--r--includes/Exception.php2
-rw-r--r--includes/Exif.php4
-rw-r--r--includes/Export.php4
-rw-r--r--includes/ExternalEdit.php4
-rw-r--r--includes/ExternalStore.php7
-rw-r--r--includes/ExternalStoreDB.php2
-rw-r--r--includes/ExternalStoreHttp.php2
-rw-r--r--includes/FakeTitle.php2
-rw-r--r--includes/FileDeleteForm.php220
-rw-r--r--includes/FileRevertForm.php165
-rw-r--r--includes/FileStore.php8
-rw-r--r--includes/GlobalFunctions.php186
-rw-r--r--includes/HTMLCacheUpdate.php32
-rw-r--r--includes/HTMLFileCache.php2
-rw-r--r--includes/HTMLForm.php53
-rw-r--r--includes/HistoryBlob.php2
-rw-r--r--includes/Hooks.php16
-rw-r--r--includes/HttpFunctions.php38
-rw-r--r--includes/IP.php47
-rw-r--r--includes/ImageFunctions.php111
-rw-r--r--includes/ImageGallery.php103
-rw-r--r--includes/ImagePage.php544
-rw-r--r--includes/ImageQueryPage.php8
-rw-r--r--includes/JobQueue.php112
-rw-r--r--includes/Licenses.php2
-rw-r--r--includes/LinkBatch.php33
-rw-r--r--includes/LinkCache.php2
-rw-r--r--includes/LinkFilter.php2
-rw-r--r--includes/Linker.php574
-rw-r--r--includes/LinksUpdate.php16
-rw-r--r--includes/LoadBalancer.php2
-rw-r--r--includes/LogPage.php71
-rw-r--r--includes/MacBinary.php4
-rw-r--r--includes/MagicWord.php178
-rw-r--r--includes/Math.php49
-rw-r--r--includes/MediaTransformOutput.php98
-rw-r--r--includes/MemcachedSessions.php2
-rw-r--r--includes/MessageCache.php33
-rw-r--r--includes/Metadata.php2
-rw-r--r--includes/MimeMagic.php538
-rw-r--r--includes/Namespace.php88
-rw-r--r--includes/ObjectCache.php4
-rw-r--r--includes/OutputHandler.php75
-rw-r--r--includes/OutputPage.php281
-rw-r--r--includes/PageHistory.php26
-rw-r--r--includes/PageQueryPage.php2
-rw-r--r--includes/Pager.php122
-rw-r--r--includes/Parser.php420
-rw-r--r--includes/ParserCache.php2
-rw-r--r--includes/ParserOptions.php2
-rw-r--r--includes/ParserOutput.php59
-rw-r--r--includes/PatrolLog.php18
-rw-r--r--includes/Profiler.php5
-rw-r--r--includes/ProfilerSimple.php2
-rw-r--r--includes/ProfilerSimpleUDP.php2
-rw-r--r--includes/ProfilerStub.php2
-rw-r--r--includes/ProtectionForm.php90
-rw-r--r--includes/ProxyTools.php2
-rw-r--r--includes/QueryPage.php18
-rw-r--r--includes/RawPage.php2
-rw-r--r--includes/RecentChange.php72
-rw-r--r--includes/RefreshLinksJob.php48
-rw-r--r--includes/Revision.php2
-rw-r--r--includes/Sanitizer.php92
-rw-r--r--includes/SearchEngine.php21
-rw-r--r--includes/SearchMySQL.php6
-rw-r--r--includes/SearchMySQL4.php2
-rw-r--r--includes/SearchOracle.php2
-rw-r--r--includes/SearchPostgres.php3
-rw-r--r--includes/SearchTsearch2.php2
-rw-r--r--includes/SearchUpdate.php2
-rw-r--r--includes/Setup.php74
-rw-r--r--includes/SiteConfiguration.php2
-rw-r--r--includes/SiteStats.php62
-rw-r--r--includes/Skin.php104
-rw-r--r--includes/SkinTemplate.php60
-rw-r--r--includes/SpecialAllmessages.php5
-rw-r--r--includes/SpecialAllpages.php112
-rw-r--r--includes/SpecialAncientpages.php2
-rw-r--r--includes/SpecialBlockip.php69
-rw-r--r--includes/SpecialBlockme.php2
-rw-r--r--includes/SpecialBooksources.php22
-rw-r--r--includes/SpecialBrokenRedirects.php8
-rw-r--r--includes/SpecialCategories.php13
-rw-r--r--includes/SpecialConfirmemail.php23
-rw-r--r--includes/SpecialContributions.php129
-rw-r--r--includes/SpecialDeadendpages.php4
-rw-r--r--includes/SpecialDisambiguations.php4
-rw-r--r--includes/SpecialDoubleRedirects.php9
-rw-r--r--includes/SpecialEmailuser.php11
-rw-r--r--includes/SpecialExport.php46
-rw-r--r--includes/SpecialFewestrevisions.php2
-rw-r--r--includes/SpecialImagelist.php20
-rw-r--r--includes/SpecialImport.php31
-rw-r--r--includes/SpecialIpblocklist.php168
-rw-r--r--includes/SpecialListredirects.php2
-rw-r--r--includes/SpecialListusers.php28
-rw-r--r--includes/SpecialLockdb.php2
-rw-r--r--includes/SpecialLog.php106
-rw-r--r--includes/SpecialLonelypages.php4
-rw-r--r--includes/SpecialLongpages.php2
-rw-r--r--includes/SpecialMIMEsearch.php31
-rw-r--r--includes/SpecialMostcategories.php3
-rw-r--r--includes/SpecialMostimages.php2
-rw-r--r--includes/SpecialMostlinked.php2
-rw-r--r--includes/SpecialMostlinkedcategories.php2
-rw-r--r--includes/SpecialMostlinkedtemplates.php131
-rw-r--r--includes/SpecialMostrevisions.php2
-rw-r--r--includes/SpecialMovepage.php65
-rw-r--r--includes/SpecialNewimages.php5
-rw-r--r--includes/SpecialNewpages.php22
-rw-r--r--includes/SpecialPage.php30
-rw-r--r--includes/SpecialPopularpages.php2
-rw-r--r--includes/SpecialPreferences.php295
-rw-r--r--includes/SpecialPrefixindex.php26
-rw-r--r--includes/SpecialProtectedpages.php132
-rw-r--r--includes/SpecialRandompage.php2
-rw-r--r--includes/SpecialRandomredirect.php2
-rw-r--r--includes/SpecialRecentchanges.php16
-rw-r--r--includes/SpecialRecentchangeslinked.php3
-rw-r--r--includes/SpecialResetpass.php4
-rw-r--r--includes/SpecialRevisiondelete.php2
-rw-r--r--includes/SpecialSearch.php15
-rw-r--r--includes/SpecialShortpages.php2
-rw-r--r--includes/SpecialSpecialpages.php6
-rw-r--r--includes/SpecialStatistics.php89
-rw-r--r--includes/SpecialUncategorizedcategories.php2
-rw-r--r--includes/SpecialUncategorizedimages.php2
-rw-r--r--includes/SpecialUncategorizedpages.php2
-rw-r--r--includes/SpecialUncategorizedtemplates.php31
-rw-r--r--includes/SpecialUndelete.php146
-rw-r--r--includes/SpecialUnlockdb.php2
-rw-r--r--includes/SpecialUnusedcategories.php4
-rw-r--r--includes/SpecialUnusedimages.php2
-rw-r--r--includes/SpecialUnusedtemplates.php5
-rw-r--r--includes/SpecialUnwatchedpages.php2
-rw-r--r--includes/SpecialUpload.php900
-rw-r--r--includes/SpecialUploadMogile.php2
-rw-r--r--includes/SpecialUserlogin.php34
-rw-r--r--includes/SpecialUserlogout.php2
-rw-r--r--includes/SpecialUserrights.php225
-rw-r--r--includes/SpecialVersion.php17
-rw-r--r--includes/SpecialWantedcategories.php2
-rw-r--r--includes/SpecialWantedpages.php66
-rw-r--r--includes/SpecialWatchlist.php194
-rw-r--r--includes/SpecialWhatlinkshere.php35
-rw-r--r--includes/SpecialWithoutinterwiki.php10
-rw-r--r--includes/SquidUpdate.php21
-rw-r--r--includes/StreamFile.php5
-rw-r--r--includes/StringUtils.php2
-rw-r--r--includes/StubObject.php2
-rw-r--r--includes/Title.php338
-rw-r--r--includes/User.php284
-rw-r--r--includes/UserMailer.php195
-rw-r--r--includes/Utf8Case.php2
-rw-r--r--includes/WatchedItem.php17
-rw-r--r--includes/WatchlistEditor.php493
-rw-r--r--includes/WebRequest.php92
-rw-r--r--includes/WebResponse.php2
-rw-r--r--includes/WebStart.php2
-rw-r--r--includes/Wiki.php39
-rw-r--r--includes/WikiError.php2
-rw-r--r--includes/Xml.php180
-rw-r--r--includes/XmlFunctions.php5
-rw-r--r--includes/ZhClient.php1
-rw-r--r--includes/ZhConversion.php1
-rw-r--r--includes/api/ApiBase.php165
-rw-r--r--includes/api/ApiFeedWatchlist.php121
-rw-r--r--includes/api/ApiFormatBase.php41
-rw-r--r--includes/api/ApiFormatJson.php31
-rw-r--r--includes/api/ApiFormatJson_json.php2
-rw-r--r--includes/api/ApiFormatPhp.php6
-rw-r--r--includes/api/ApiFormatWddx.php6
-rw-r--r--includes/api/ApiFormatXml.php6
-rw-r--r--includes/api/ApiFormatYaml.php6
-rw-r--r--includes/api/ApiFormatYaml_spyc.php2
-rw-r--r--includes/api/ApiHelp.php8
-rw-r--r--includes/api/ApiLogin.php139
-rw-r--r--includes/api/ApiMain.php268
-rw-r--r--includes/api/ApiOpenSearch.php14
-rw-r--r--includes/api/ApiPageSet.php115
-rw-r--r--includes/api/ApiQuery.php253
-rw-r--r--includes/api/ApiQueryAllLinks.php179
-rw-r--r--includes/api/ApiQueryAllUsers.php204
-rw-r--r--includes/api/ApiQueryAllpages.php104
-rw-r--r--includes/api/ApiQueryBacklinks.php125
-rw-r--r--includes/api/ApiQueryBase.php247
-rw-r--r--includes/api/ApiQueryCategories.php157
-rw-r--r--includes/api/ApiQueryCategoryMembers.php238
-rw-r--r--includes/api/ApiQueryExtLinksUsage.php200
-rw-r--r--includes/api/ApiQueryExternalLinks.php93
-rw-r--r--includes/api/ApiQueryImageInfo.php156
-rw-r--r--includes/api/ApiQueryImages.php118
-rw-r--r--includes/api/ApiQueryInfo.php126
-rw-r--r--includes/api/ApiQueryLangLinks.php94
-rw-r--r--includes/api/ApiQueryLinks.php162
-rw-r--r--includes/api/ApiQueryLogEvents.php155
-rw-r--r--includes/api/ApiQueryRecentChanges.php117
-rw-r--r--includes/api/ApiQueryRevisions.php187
-rw-r--r--includes/api/ApiQuerySearch.php151
-rw-r--r--includes/api/ApiQuerySiteinfo.php202
-rw-r--r--includes/api/ApiQueryUserContributions.php231
-rw-r--r--includes/api/ApiQueryUserInfo.php133
-rw-r--r--includes/api/ApiQueryWatchlist.php133
-rw-r--r--includes/api/ApiResult.php42
-rw-r--r--includes/cbt/CBTCompiler.php2
-rw-r--r--includes/cbt/CBTProcessor.php2
-rw-r--r--includes/filerepo/ArchivedFile.php108
-rw-r--r--includes/filerepo/FSRepo.php530
-rw-r--r--includes/filerepo/File.php1133
-rw-r--r--includes/filerepo/FileRepo.php404
-rw-r--r--includes/filerepo/FileRepoStatus.php171
-rw-r--r--includes/filerepo/ForeignDBFile.php42
-rw-r--r--includes/filerepo/ForeignDBRepo.php57
-rw-r--r--includes/filerepo/ICRepo.php313
-rw-r--r--includes/filerepo/LocalFile.php1573
-rw-r--r--includes/filerepo/LocalRepo.php65
-rw-r--r--includes/filerepo/OldLocalFile.php232
-rw-r--r--includes/filerepo/README41
-rw-r--r--includes/filerepo/RepoGroup.php150
-rw-r--r--includes/filerepo/UnregisteredLocalFile.php109
-rw-r--r--includes/media/BMP.php10
-rw-r--r--includes/media/Bitmap.php91
-rw-r--r--includes/media/DjVu.php16
-rw-r--r--includes/media/Generic.php179
-rw-r--r--includes/media/SVG.php21
-rw-r--r--includes/memcached-client.php40
-rw-r--r--includes/normal/CleanUpTest.php2
-rw-r--r--includes/normal/RandomTest.php2
-rw-r--r--includes/normal/Utf8Test.php2
-rw-r--r--includes/normal/UtfNormal.php55
-rw-r--r--includes/normal/UtfNormalBench.php2
-rw-r--r--includes/normal/UtfNormalGenerate.php2
-rw-r--r--includes/normal/UtfNormalTest.php2
-rw-r--r--includes/normal/UtfNormalUtil.php2
-rw-r--r--includes/proxy_check.php2
-rw-r--r--includes/templates/NoLocalSettings.php5
-rw-r--r--includes/templates/Userlogin.php49
264 files changed, 17208 insertions, 4971 deletions
diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php
index ca129029..7b85ed20 100644
--- a/includes/AjaxDispatcher.php
+++ b/includes/AjaxDispatcher.php
@@ -1,10 +1,9 @@
<?php
+/**
+ * Handle ajax requests and send them to the proper handler.
+ */
-if( !defined( 'MEDIAWIKI' ) ) {
- die( 1 );
-}
-
-if ( ! $wgUseAjax ) {
+if( !(defined( 'MEDIAWIKI' ) && $wgUseAjax ) ) {
die( 1 );
}
@@ -15,10 +14,16 @@ require_once( 'AjaxFunctions.php' );
* @addtogroup Ajax
*/
class AjaxDispatcher {
- var $mode;
- var $func_name;
- var $args;
+ /** The way the request was made, either a 'get' or a 'post' */
+ private $mode;
+ /** Name of the requested handler */
+ private $func_name;
+
+ /** Arguments passed */
+ private $args;
+
+ /** Load up our object with user supplied data */
function __construct() {
wfProfileIn( __METHOD__ );
@@ -32,24 +37,41 @@ class AjaxDispatcher {
$this->mode = "post";
}
- if ($this->mode == "get") {
+ switch( $this->mode ) {
+
+ case 'get':
$this->func_name = isset( $_GET["rs"] ) ? $_GET["rs"] : '';
if (! empty($_GET["rsargs"])) {
$this->args = $_GET["rsargs"];
} else {
$this->args = array();
}
- } else {
+ break;
+
+ case 'post':
$this->func_name = isset( $_POST["rs"] ) ? $_POST["rs"] : '';
if (! empty($_POST["rsargs"])) {
$this->args = $_POST["rsargs"];
} else {
$this->args = array();
}
+ break;
+
+ default:
+ return;
+ # Or we could throw an exception:
+ #throw new MWException( __METHOD__ . ' called without any data (mode empty).' );
+
}
+
wfProfileOut( __METHOD__ );
}
+ /** Pass the request to our internal function.
+ * BEWARE! Data are passed as they have been supplied by the user,
+ * they should be carefully handled in the function processing the
+ * request.
+ */
function performAction() {
global $wgAjaxExportList, $wgOut;
@@ -62,8 +84,13 @@ class AjaxDispatcher {
wfHttpError( 400, 'Bad Request',
"unknown function " . (string) $this->func_name );
} else {
+ if ( strpos( $this->func_name, '::' ) !== false ) {
+ $func = explode( '::', $this->func_name, 2 );
+ } else {
+ $func = $this->func_name;
+ }
try {
- $result = call_user_func_array($this->func_name, $this->args);
+ $result = call_user_func_array($func, $this->args);
if ( $result === false || $result === NULL ) {
wfHttpError( 500, 'Internal Error',
@@ -93,4 +120,4 @@ class AjaxDispatcher {
}
}
-?>
+
diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php
index 86f853db..4fb76dcc 100644
--- a/includes/AjaxFunctions.php
+++ b/includes/AjaxFunctions.php
@@ -136,22 +136,29 @@ function wfSajaxSearch( $term ) {
/**
* Called for AJAX watch/unwatch requests.
- * @param $pageID Integer ID of the page to be watched/unwatched
+ * @param $pagename Prefixed title string for page to watch/unwatch
* @param $watch String 'w' to watch, 'u' to unwatch
- * @return String '<w#>' or '<u#>' on successful watch or unwatch, respectively, or '<err#>' on error (invalid XML in case we want to add HTML sometime)
+ * @return String '<w#>' or '<u#>' on successful watch or unwatch,
+ * respectively, followed by an HTML message to display in the alert box; or
+ * '<err#>' on error
*/
-function wfAjaxWatch($pageID = "", $watch = "") {
- if(wfReadOnly())
- return '<err#>'; // redirect to action=(un)watch, which will display the database lock message
+function wfAjaxWatch($pagename = "", $watch = "") {
+ if(wfReadOnly()) {
+ // redirect to action=(un)watch, which will display the database lock
+ // message
+ return '<err#>';
+ }
- if(('w' !== $watch && 'u' !== $watch) || !is_numeric($pageID))
+ if('w' !== $watch && 'u' !== $watch) {
return '<err#>';
+ }
$watch = 'w' === $watch;
- $pageID = intval($pageID);
- $title = Title::newFromID($pageID);
- if(!$title)
+ $title = Title::newFromText($pagename);
+ if(!$title) {
+ // Invalid title
return '<err#>';
+ }
$article = new Article($title);
$watching = $title->userIsWatching();
@@ -170,7 +177,10 @@ function wfAjaxWatch($pageID = "", $watch = "") {
$dbw->commit();
}
}
-
- return $watch ? '<w#>' : '<u#>';
+ if( $watch ) {
+ return '<w#>'.wfMsgExt( 'addedwatchtext', array( 'parse' ), $title->getPrefixedText() );
+ } else {
+ return '<u#>'.wfMsgExt( 'removedwatchtext', array( 'parse' ), $title->getPrefixedText() );
+ }
}
-?>
+
diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php
index cb4af1b5..8fa08539 100644
--- a/includes/AjaxResponse.php
+++ b/includes/AjaxResponse.php
@@ -8,14 +8,27 @@ if( !defined( 'MEDIAWIKI' ) ) {
* @addtogroup Ajax
*/
class AjaxResponse {
- var $mCacheDuration;
- var $mVary;
- var $mDisabled;
- var $mText;
- var $mResponseCode;
- var $mLastModified;
- var $mContentType;
+ /** Number of seconds to get the response cached by a proxy */
+ private $mCacheDuration;
+
+ /** HTTP header Content-Type */
+ private $mContentType;
+
+ /** @todo document */
+ private $mDisabled;
+
+ /** Date for the HTTP header Last-modified */
+ private $mLastModified;
+
+ /** HTTP response code */
+ private $mResponseCode;
+
+ /** HTTP Vary header */
+ private $mVary;
+
+ /** Content of our HTTP response */
+ private $mText;
function __construct( $text = NULL ) {
$this->mCacheDuration = NULL;
@@ -52,18 +65,21 @@ class AjaxResponse {
$this->mDisabled = true;
}
+ /** Add content to the response */
function addText( $text ) {
if ( ! $this->mDisabled && $text ) {
$this->mText .= $text;
}
}
+ /** Output text */
function printText() {
if ( ! $this->mDisabled ) {
print $this->mText;
}
}
+ /** Construct the header and output it */
function sendHeaders() {
global $wgUseSquid, $wgUseESI;
@@ -204,4 +220,4 @@ class AjaxResponse {
return true;
}
}
-?>
+
diff --git a/includes/Article.php b/includes/Article.php
index 0130ceba..7ba55c54 100644
--- a/includes/Article.php
+++ b/includes/Article.php
@@ -37,6 +37,18 @@ class Article {
/**@}}*/
/**
+ * Constants used by internal components to get rollback results
+ */
+ const SUCCESS = 0; // Operation successful
+ const PERM_DENIED = 1; // Permission denied
+ const BLOCKED = 2; // User has been blocked
+ const READONLY = 3; // Wiki is in read-only mode
+ const BAD_TOKEN = 4; // Invalid token specified
+ const BAD_TITLE = 5; // $this is not a valid Article
+ const ALREADY_ROLLED = 6; // Someone else already rolled this back. $from and $summary will be set
+ const ONLY_AUTHOR = 7; // User is the only author of the page
+
+ /**
* Constructor and clear the article
* @param $title Reference to a Title object.
* @param $oldId Integer revision ID, null to fetch from request, zero for current
@@ -257,13 +269,16 @@ class Article {
'page_random',
'page_touched',
'page_latest',
- 'page_len' ) ;
- wfRunHooks( 'ArticlePageDataBefore', array( &$this , &$fields ) ) ;
- $row = $dbr->selectRow( 'page',
+ 'page_len',
+ );
+ wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) );
+ $row = $dbr->selectRow(
+ 'page',
$fields,
$conditions,
- 'Article::pageData' );
- wfRunHooks( 'ArticlePageDataAfter', array( &$this , &$row ) ) ;
+ __METHOD__
+ );
+ wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) );
return $row ;
}
@@ -500,6 +515,10 @@ class Article {
* @return bool
*/
function isCurrent() {
+ # If no oldid, this is the current version.
+ if ($this->getOldID() == 0)
+ return true;
+
return $this->exists() &&
isset( $this->mRevision ) &&
$this->mRevision->isCurrent();
@@ -604,7 +623,7 @@ class Article {
function view() {
global $wgUser, $wgOut, $wgRequest, $wgContLang;
global $wgEnableParserCache, $wgStylePath, $wgUseRCPatrol, $wgParser;
- global $wgUseTrackbacks, $wgNamespaceRobotPolicies;
+ global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies;
$sk = $wgUser->getSkin();
wfProfileIn( __METHOD__ );
@@ -632,6 +651,8 @@ class Article {
# Discourage indexing of printable versions, but encourage following
if( $wgOut->isPrintable() ) {
$policy = 'noindex,follow';
+ } elseif ( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) {
+ $policy = $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()];
} elseif( isset( $wgNamespaceRobotPolicies[$ns] ) ) {
# Honour customised robot policies for this namespace
$policy = $wgNamespaceRobotPolicies[$ns];
@@ -678,10 +699,12 @@ class Article {
}
# Should the parser cache be used?
- $pcache = $wgEnableParserCache &&
- intval( $wgUser->getOption( 'stubthreshold' ) ) == 0 &&
- $this->exists() &&
- empty( $oldid );
+ $pcache = $wgEnableParserCache
+ && intval( $wgUser->getOption( 'stubthreshold' ) ) == 0
+ && $this->exists()
+ && empty( $oldid )
+ && !$this->mTitle->isCssOrJsPage()
+ && !$this->mTitle->isCssJsSubpage();
wfDebug( 'Article::view using parser cache: ' . ($pcache ? 'yes' : 'no' ) . "\n" );
if ( $wgUser->getOption( 'stubthreshold' ) ) {
wfIncrStats( 'pcache_miss_stub' );
@@ -718,9 +741,12 @@ class Article {
}
$outputDone = false;
- wfRunHooks( 'ArticleViewHeader', array( &$this ) );
+ wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$pcache ) );
if ( $pcache ) {
if ( $wgOut->tryParserCache( $this, $wgUser ) ) {
+ // Ensure that UI elements requiring revision ID have
+ // the correct version information.
+ $wgOut->setRevisionId( $this->mLatest );
$outputDone = true;
}
}
@@ -768,15 +794,24 @@ class Article {
}
if( !$outputDone ) {
$wgOut->setRevisionId( $this->getRevIdFetched() );
- # wrap user css and user js in pre and don't parse
- # XXX: use $this->mTitle->usCssJsSubpage() when php is fixed/ a workaround is found
- if (
- $ns == NS_USER &&
- preg_match('/\\/[\\w]+\\.(?:css|js)$/', $this->mTitle->getDBkey())
- ) {
- $wgOut->addWikiText( wfMsg('clearyourcache'));
- $wgOut->addHTML( '<pre>'.htmlspecialchars($this->mContent)."\n</pre>" );
- } else if ( $rt = Title::newFromRedirect( $text ) ) {
+
+ // Pages containing custom CSS or JavaScript get special treatment
+ if( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) {
+ $wgOut->addHtml( wfMsgExt( 'clearyourcache', 'parse' ) );
+
+ // Give hooks a chance to customise the output
+ if( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->mTitle, $wgOut ) ) ) {
+ // Wrap the whole lot in a <pre> and don't parse
+ $m = array();
+ preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
+ $wgOut->addHtml( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
+ $wgOut->addHtml( htmlspecialchars( $this->mContent ) );
+ $wgOut->addHtml( "\n</pre>\n" );
+ }
+
+ }
+
+ elseif ( $rt = Title::newFromRedirect( $text ) ) {
# Display redirect
$imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr';
$imageUrl = $wgStylePath.'/common/images/redirect' . $imageDir . '.png';
@@ -869,8 +904,8 @@ class Article {
$rmvtxt = "";
if ($wgUser->isAllowed( 'trackback' )) {
$delurl = $this->mTitle->getFullURL("action=deletetrackback&tbid="
- . $o->tb_id . "&token=" . $wgUser->editToken());
- $rmvtxt = wfMsg('trackbackremove', $delurl);
+ . $o->tb_id . "&token=" . urlencode( $wgUser->editToken() ) );
+ $rmvtxt = wfMsg( 'trackbackremove', htmlspecialchars( $delurl ) );
}
$tbtext .= wfMsg(strlen($o->tb_ex) ? 'trackbackexcerpt' : 'trackback',
$o->tb_title,
@@ -1146,7 +1181,7 @@ class Article {
if( $section == 'new' ) {
# Inserting a new section
- $subject = $summary ? "== {$summary} ==\n\n" : '';
+ $subject = $summary ? wfMsgForContent('newsectionheaderdefaultlevel',$summary) . "\n\n" : '';
$text = strlen( trim( $oldtext ) ) > 0
? "{$oldtext}\n\n{$subject}{$text}"
: "{$subject}{$text}";
@@ -1172,7 +1207,7 @@ class Article {
# If this is a comment, add the summary as headline
if ( $comment && $summary != "" ) {
- $text = "== {$summary} ==\n\n".$text;
+ $text = wfMsgForContent('newsectionheaderdefaultlevel',$summary) . "\n\n".$text;
}
$this->doEdit( $text, $summary, $flags );
@@ -1219,7 +1254,10 @@ class Article {
}
}
- $this->doRedirect( $this->isRedirect( $text ), $sectionanchor );
+ $extraq = ''; // Give extensions a chance to modify URL query on update
+ wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraq ) );
+
+ $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraq );
}
return $good;
}
@@ -1359,6 +1397,7 @@ class Article {
$dbw->commit();
}
} else {
+ $revision = null;
// Keep the same revision ID, but do some updates on it
$revisionId = $this->getRevIdFetched();
// Update page_touched, this is usually implicit in the page update
@@ -1426,19 +1465,18 @@ class Article {
# Clear caches
Article::onArticleCreate( $this->mTitle );
- wfRunHooks( 'ArticleInsertComplete', array( &$this, &$wgUser, $text,
- $summary, $flags & EDIT_MINOR,
- null, null, &$flags ) );
+ wfRunHooks( 'ArticleInsertComplete', array( &$this, &$wgUser, $text, $summary,
+ $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
}
if ( $good && !( $flags & EDIT_DEFER_UPDATES ) ) {
wfDoUpdates();
}
- wfRunHooks( 'ArticleSaveComplete',
- array( &$this, &$wgUser, $text,
- $summary, $flags & EDIT_MINOR,
- null, null, &$flags ) );
+ if ( $good ) {
+ wfRunHooks( 'ArticleSaveComplete', array( &$this, &$wgUser, $text, $summary,
+ $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
+ }
wfProfileOut( __METHOD__ );
return $good;
@@ -1458,12 +1496,14 @@ class Article {
* @param boolean $noRedir Add redirect=no
* @param string $sectionAnchor section to redirect to, including "#"
*/
- function doRedirect( $noRedir = false, $sectionAnchor = '' ) {
+ function doRedirect( $noRedir = false, $sectionAnchor = '', $extraq = '' ) {
global $wgOut;
if ( $noRedir ) {
$query = 'redirect=no';
+ if( $extraq )
+ $query .= "&$query";
} else {
- $query = '';
+ $query = $extraq;
}
$wgOut->redirect( $this->mTitle->getFullURL( $query ) . $sectionAnchor );
}
@@ -1690,7 +1730,13 @@ class Article {
}
# Prepare a null revision to be added to the history
- $comment = $wgContLang->ucfirst( wfMsgForContent( $protect ? 'protectedarticle' : 'unprotectedarticle', $this->mTitle->getPrefixedText() ) );
+ $modified = $current != '' && $protect;
+ if ( $protect ) {
+ $comment_type = $modified ? 'modifiedarticleprotection' : 'protectedarticle';
+ } else {
+ $comment_type = 'unprotectedarticle';
+ }
+ $comment = $wgContLang->ucfirst( wfMsgForContent( $comment_type, $this->mTitle->getPrefixedText() ) );
foreach( $limit as $action => $restrictions ) {
# Check if the group level required to edit also can protect pages
@@ -1744,7 +1790,7 @@ class Article {
$log = new LogPage( 'protect' );
if( $protect ) {
- $log->addEntry( 'protect', $this->mTitle, trim( $reason . " [$updated]$cascade_description$expiry_description" ) );
+ $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason . " [$updated]$cascade_description$expiry_description" ) );
} else {
$log->addEntry( 'unprotect', $this->mTitle, $reason );
}
@@ -1994,11 +2040,10 @@ class Article {
/**
- * Fetch deletion log
+ * Show relevant lines from the deletion log
*/
- function showLogExtract( &$out ) {
- # Show relevant lines from the deletion log:
- $out->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
+ function showLogExtract( $out ) {
+ $out->addHtml( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . '</h2>' );
$logViewer = new LogViewer(
new LogReader(
new FauxRequest(
@@ -2080,7 +2125,8 @@ class Article {
'ar_text_id' => 'rev_text_id',
'ar_text' => '\'\'', // Be explicit to appease
'ar_flags' => '\'\'', // MySQL's "strict mode"...
- 'ar_len' => 'rev_len'
+ 'ar_len' => 'rev_len',
+ 'ar_page_id' => 'page_id',
), array(
'page_id' => $id,
'page_id = rev_page'
@@ -2132,60 +2178,52 @@ class Article {
}
/**
- * Revert a modification
- */
- function rollback() {
- global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol;
-
+ * Roll back the most recent consecutive set of edits to a page
+ * from the same user; fails if there are no eligible edits to
+ * roll back to, e.g. user is the sole contributor
+ *
+ * @param string $fromP - Name of the user whose edits to rollback.
+ * @param string $summary - Custom summary. Set to default summary if empty.
+ * @param string $token - Rollback token.
+ * @param bool $bot - If true, mark all reverted edits as bot.
+ *
+ * @param array $resultDetails contains result-specific dict of additional values
+ * ALREADY_ROLLED : 'current' (rev)
+ * SUCCESS : 'summary' (str), 'current' (rev), 'target' (rev)
+ *
+ * @return self::SUCCESS on succes, self::* on failure
+ */
+ public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails ) {
+ global $wgUser, $wgUseRCPatrol;
+ $resultDetails = null;
+
if( $wgUser->isAllowed( 'rollback' ) ) {
if( $wgUser->isBlocked() ) {
- $wgOut->blockedPage();
- return;
+ return self::BLOCKED;
}
} else {
- $wgOut->permissionRequired( 'rollback' );
- return;
+ return self::PERM_DENIED;
}
-
+
if ( wfReadOnly() ) {
- $wgOut->readOnlyPage( $this->getContent() );
- return;
- }
- if( !$wgUser->matchEditToken( $wgRequest->getVal( 'token' ),
- array( $this->mTitle->getPrefixedText(),
- $wgRequest->getVal( 'from' ) ) ) ) {
- $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
- $wgOut->addWikiText( wfMsg( 'sessionfailure' ) );
- return;
+ return self::READONLY;
}
- $dbw = wfGetDB( DB_MASTER );
-
- # Enhanced rollback, marks edits rc_bot=1
- $bot = $wgRequest->getBool( 'bot' );
+ if( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) )
+ return self::BAD_TOKEN;
- # Replace all this user's current edits with the next one down
+ $dbw = wfGetDB( DB_MASTER );
# Get the last editor
$current = Revision::newFromTitle( $this->mTitle );
if( is_null( $current ) ) {
# Something wrong... no page?
- $wgOut->addHTML( wfMsg( 'notanarticle' ) );
- return;
+ return self::BAD_TITLE;
}
- $from = str_replace( '_', ' ', $wgRequest->getVal( 'from' ) );
+ $from = str_replace( '_', ' ', $fromP );
if( $from != $current->getUserText() ) {
- $wgOut->setPageTitle( wfMsg('rollbackfailed') );
- $wgOut->addWikiText( wfMsg( 'alreadyrolled',
- htmlspecialchars( $this->mTitle->getPrefixedText()),
- htmlspecialchars( $from ),
- htmlspecialchars( $current->getUserText() ) ) );
- if( $current->getComment() != '') {
- $wgOut->addHTML(
- wfMsg( 'editcomment',
- $wgUser->getSkin()->formatComment( $current->getComment() ) ) );
- }
- return;
+ $resultDetails = array( 'current' => $current );
+ return self::ALREADY_ROLLED;
}
# Get the last edit not by this guy
@@ -2203,11 +2241,9 @@ class Article {
);
if( $s === false ) {
# Something wrong
- $wgOut->setPageTitle(wfMsg('rollbackfailed'));
- $wgOut->addHTML( wfMsg( 'cantrollback' ) );
- return;
+ return self::ONLY_AUTHOR;
}
-
+
$set = array();
if ( $bot ) {
# Mark all reverted edits as bot
@@ -2220,27 +2256,100 @@ class Article {
if ( $set ) {
$dbw->update( 'recentchanges', $set,
- array( /* WHERE */
- 'rc_cur_id' => $current->getPage(),
- 'rc_user_text' => $current->getUserText(),
- "rc_timestamp > '{$s->rev_timestamp}'",
- ), __METHOD__
- );
+ array( /* WHERE */
+ 'rc_cur_id' => $current->getPage(),
+ 'rc_user_text' => $current->getUserText(),
+ "rc_timestamp > '{$s->rev_timestamp}'",
+ ), __METHOD__
+ );
}
# Get the edit summary
$target = Revision::newFromId( $s->rev_id );
- $newComment = wfMsgForContent( 'revertpage', $target->getUserText(), $from );
- $newComment = $wgRequest->getText( 'summary', $newComment );
+ if( empty( $summary ) )
+ $summary = wfMsgForContent( 'revertpage', $target->getUserText(), $from );
+
+ # Save
+ $flags = EDIT_UPDATE | EDIT_MINOR;
+ if( $bot )
+ $flags |= EDIT_FORCE_BOT;
+ $this->doEdit( $target->getText(), $summary, $flags );
+
+ $resultDetails = array(
+ 'summary' => $summary,
+ 'current' => $current,
+ 'target' => $target,
+ );
+ return self::SUCCESS;
+ }
- # Save it!
- $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
- $wgOut->addHTML( '<h2>' . htmlspecialchars( $newComment ) . "</h2>\n<hr />\n" );
+ /**
+ * User interface for rollback operations
+ */
+ function rollback() {
+ global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol;
- $this->updateArticle( $target->getText(), $newComment, 1, $this->mTitle->userIsWatching(), $bot );
+ $details = null;
+ $result = $this->doRollback(
+ $wgRequest->getVal( 'from' ),
+ $wgRequest->getText( 'summary' ),
+ $wgRequest->getVal( 'token' ),
+ $wgRequest->getBool( 'bot' ),
+ $details
+ );
+
+ switch( $result ) {
+ case self::BLOCKED:
+ $wgOut->blockedPage();
+ break;
+ case self::PERM_DENIED:
+ $wgOut->permissionRequired( 'rollback' );
+ break;
+ case self::READONLY:
+ $wgOut->readOnlyPage( $this->getContent() );
+ break;
+ case self::BAD_TOKEN:
+ $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
+ $wgOut->addWikiText( wfMsg( 'sessionfailure' ) );
+ break;
+ case self::BAD_TITLE:
+ $wgOut->addHtml( wfMsg( 'notanarticle' ) );
+ break;
+ case self::ALREADY_ROLLED:
+ $current = $details['current'];
+ $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
+ $wgOut->addWikiText(
+ wfMsg( 'alreadyrolled',
+ htmlspecialchars( $this->mTitle->getPrefixedText() ),
+ htmlspecialchars( $wgRequest->getVal( 'from' ) ),
+ htmlspecialchars( $current->getUserText() )
+ )
+ );
+ if( $current->getComment() != '' ) {
+ $wgOut->addHtml( wfMsg( 'editcomment',
+ $wgUser->getSkin()->formatComment( $current->getComment() ) ) );
+ }
+ break;
+ case self::ONLY_AUTHOR:
+ $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
+ $wgOut->addHtml( wfMsg( 'cantrollback' ) );
+ break;
+ case self::SUCCESS:
+ $current = $details['current'];
+ $target = $details['target'];
+ $wgOut->setPageTitle( wfMsg( 'actioncomplete' ) );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() )
+ . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() );
+ $new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() )
+ . $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() );
+ $wgOut->addHtml( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) );
+ $wgOut->returnToMain( false, $this->mTitle );
+ break;
+ default:
+ throw new MWException( __METHOD__ . ": Unknown return value `{$result}`" );
+ }
- $wgOut->returnToMain( false );
}
@@ -2268,7 +2377,7 @@ class Article {
/**
* Do standard deferred updates after page edit.
* Update links tables, site stats, search index and message cache.
- * Every 1000th edit, prune the recent changes table.
+ * Every 100th edit, prune the recent changes table.
*
* @private
* @param $text New text of the article
@@ -2296,12 +2405,11 @@ class Article {
$u = new LinksUpdate( $this->mTitle, $poutput );
$u->doUpdate();
- if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
- wfSeedRandom();
- if ( 0 == mt_rand( 0, 999 ) ) {
- # Periodically flush old entries from the recentchanges table.
+ if( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
+ if ( 0 == mt_rand( 0, 99 ) ) {
+ // Flush old entries from the `recentchanges` table; we do this on
+ // random requests so as to avoid an increase in writes for no good reason
global $wgRCMaxAge;
-
$dbw = wfGetDB( DB_MASTER );
$cutoff = $dbw->timestamp( time() - $wgRCMaxAge );
$recentchanges = $dbw->tableName( 'recentchanges' );
@@ -2410,7 +2518,12 @@ class Article {
$userlinks = $sk->userLink( $revision->getUser(), $revision->getUserText() )
. $sk->userToolLinks( $revision->getUser(), $revision->getUserText() );
- $r = "\n\t\t\t\t<div id=\"mw-revision-info\">" . wfMsg( 'revision-info', $td, $userlinks ) . "</div>\n" .
+ $m = wfMsg( 'revision-info-current' );
+ $infomsg = $current && !wfEmptyMsg( 'revision-info-current', $m ) && $m != '-'
+ ? 'revision-info-current'
+ : 'revision-info';
+
+ $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . wfMsg( $infomsg, $td, $userlinks ) . "</div>\n" .
"\n\t\t\t\t<div id=\"mw-revision-nav\">" . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t";
$wgOut->setSubtitle( $r );
}
@@ -2701,16 +2814,22 @@ class Article {
$page = $this->mTitle->getSubjectPage();
$wgOut->setPagetitle( $page->getPrefixedText() );
- $wgOut->setSubtitle( wfMsg( 'infosubtitle' ));
-
- # first, see if the page exists at all.
- $exists = $page->getArticleId() != 0;
- if( !$exists ) {
- if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
- $wgOut->addHTML(wfMsgWeirdKey ( $this->mTitle->getText() ) );
+ $wgOut->setPageTitleActionText( wfMsg( 'info_short' ) );
+ $wgOut->setSubtitle( wfMsg( 'infosubtitle' ) );
+
+ if( !$this->mTitle->exists() ) {
+ $wgOut->addHtml( '<div class="noarticletext">' );
+ if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ // This doesn't quite make sense; the user is asking for
+ // information about the _page_, not the message... -- RC
+ $wgOut->addHtml( htmlspecialchars( wfMsgWeirdKey( $this->mTitle->getText() ) ) );
} else {
- $wgOut->addHTML(wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' ) );
+ $msg = $wgUser->isLoggedIn()
+ ? 'noarticletext'
+ : 'noarticletextanon';
+ $wgOut->addHtml( wfMsgExt( $msg, 'parse' ) );
}
+ $wgOut->addHtml( '</div>' );
} else {
$dbr = wfGetDB( DB_SLAVE );
$wl_clause = array(
@@ -2959,5 +3078,3 @@ class Article {
}
}
-
-?>
diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php
index 9395032f..87a79438 100644
--- a/includes/AuthPlugin.php
+++ b/includes/AuthPlugin.php
@@ -219,9 +219,10 @@ class AuthPlugin {
* forget the & on your function declaration.
*
* @param $user User object.
+ * @param $autocreate bool True if user is being autocreated on login
* @public
*/
- function initUser( &$user ) {
+ function initUser( $user, $autocreate=false ) {
# Override this to do something.
}
@@ -234,4 +235,4 @@ class AuthPlugin {
}
}
-?>
+
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php
index 3d7bcdf3..25c728cd 100644
--- a/includes/AutoLoader.php
+++ b/includes/AutoLoader.php
@@ -22,6 +22,7 @@ function __autoload($className) {
'TurckBagOStuff' => 'includes/BagOStuff.php',
'APCBagOStuff' => 'includes/BagOStuff.php',
'eAccelBagOStuff' => 'includes/BagOStuff.php',
+ 'XCacheBagOStuff' => 'includes/BagOStuff.php',
'DBABagOStuff' => 'includes/BagOStuff.php',
'Block' => 'includes/Block.php',
'HTMLFileCache' => 'includes/HTMLFileCache.php',
@@ -93,17 +94,20 @@ function __autoload($className) {
'HistoryBlobStub' => 'includes/HistoryBlob.php',
'HistoryBlobCurStub' => 'includes/HistoryBlob.php',
'HTMLCacheUpdate' => 'includes/HTMLCacheUpdate.php',
- 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php',
'Http' => 'includes/HttpFunctions.php',
- 'Image' => 'includes/Image.php',
- 'ArchivedFile' => 'includes/Image.php',
'IP' => 'includes/IP.php',
'ThumbnailImage' => 'includes/Image.php',
'ImageGallery' => 'includes/ImageGallery.php',
'ImagePage' => 'includes/ImagePage.php',
'ImageHistoryList' => 'includes/ImagePage.php',
'ImageRemote' => 'includes/ImageRemote.php',
+ 'FileDeleteForm' => 'includes/FileDeleteForm.php',
+ 'FileRevertForm' => 'includes/FileRevertForm.php',
'Job' => 'includes/JobQueue.php',
+ 'EmaillingJob' => 'includes/EmaillingJob.php',
+ 'EnotifNotifyJob' => 'includes/EnotifNotifyJob.php',
+ 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php',
+ 'RefreshLinksJob' => 'includes/RefreshLinksJob.php',
'Licenses' => 'includes/Licenses.php',
'License' => 'includes/Licenses.php',
'LinkBatch' => 'includes/LinkBatch.php',
@@ -115,6 +119,7 @@ function __autoload($className) {
'LogPage' => 'includes/LogPage.php',
'MacBinary' => 'includes/MacBinary.php',
'MagicWord' => 'includes/MagicWord.php',
+ 'MagicWordArray' => 'includes/MagicWord.php',
'MathRenderer' => 'includes/Math.php',
'MediaTransformOutput' => 'includes/MediaTransformOutput.php',
'ThumbnailImage' => 'includes/MediaTransformOutput.php',
@@ -191,6 +196,7 @@ function __autoload($className) {
'MostimagesPage' => 'includes/SpecialMostimages.php',
'MostlinkedPage' => 'includes/SpecialMostlinked.php',
'MostlinkedCategoriesPage' => 'includes/SpecialMostlinkedcategories.php',
+ 'SpecialMostlinkedtemplates' => 'includes/SpecialMostlinkedtemplates.php',
'MostrevisionsPage' => 'includes/SpecialMostrevisions.php',
'FewestrevisionsPage' => 'includes/SpecialFewestrevisions.php',
'MovePageForm' => 'includes/SpecialMovepage.php',
@@ -209,6 +215,7 @@ function __autoload($className) {
'ShortPagesPage' => 'includes/SpecialShortpages.php',
'UncategorizedCategoriesPage' => 'includes/SpecialUncategorizedcategories.php',
'UncategorizedPagesPage' => 'includes/SpecialUncategorizedpages.php',
+ 'UncategorizedTemplatesPage' => 'includes/SpecialUncategorizedtemplates.php',
'PageArchive' => 'includes/SpecialUndelete.php',
'UndeleteForm' => 'includes/SpecialUndelete.php',
'DBUnlockForm' => 'includes/SpecialUnlockdb.php',
@@ -244,10 +251,28 @@ function __autoload($className) {
'WikiError' => 'includes/WikiError.php',
'WikiErrorMsg' => 'includes/WikiError.php',
'WikiXmlError' => 'includes/WikiError.php',
- 'XCacheBagOStuff' => 'includes/BagOStuff.php',
'Xml' => 'includes/Xml.php',
'ZhClient' => 'includes/ZhClient.php',
'memcached' => 'includes/memcached-client.php',
+ 'EmaillingJob' => 'includes/JobQueue.php',
+ 'WatchlistEditor' => 'includes/WatchlistEditor.php',
+
+ # filerepo
+ 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
+ 'File' => 'includes/filerepo/File.php',
+ 'FileRepo' => 'includes/filerepo/FileRepo.php',
+ 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
+ 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php',
+ 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php',
+ 'FSRepo' => 'includes/filerepo/FSRepo.php',
+ 'Image' => 'includes/filerepo/LocalFile.php',
+ 'LocalFile' => 'includes/filerepo/LocalFile.php',
+ 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php',
+ 'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php',
+ 'LocalRepo' => 'includes/filerepo/LocalRepo.php',
+ 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php',
+ 'RepoGroup' => 'includes/filerepo/RepoGroup.php',
+ 'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php',
# Media
'BitmapHandler' => 'includes/media/Bitmap.php',
@@ -287,14 +312,27 @@ function __autoload($className) {
'ApiPageSet' => 'includes/api/ApiPageSet.php',
'ApiQuery' => 'includes/api/ApiQuery.php',
'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php',
+ 'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php',
+ 'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php',
'ApiQueryBase' => 'includes/api/ApiQueryBase.php',
+ 'ApiQueryGeneratorBase' => 'includes/api/ApiQueryBase.php',
'ApiQueryBacklinks' => 'includes/api/ApiQueryBacklinks.php',
+ 'ApiQueryCategories' => 'includes/api/ApiQueryCategories.php',
+ 'ApiQueryCategoryMembers' => 'includes/api/ApiQueryCategoryMembers.php',
'ApiQueryContributions' => 'includes/api/ApiQueryUserContributions.php',
+ 'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php',
+ 'ApiQueryExtLinksUsage' => 'includes/api/ApiQueryExtLinksUsage.php',
+ 'ApiQueryImages' => 'includes/api/ApiQueryImages.php',
+ 'ApiQueryImageInfo' => 'includes/api/ApiQueryImageInfo.php',
'ApiQueryInfo' => 'includes/api/ApiQueryInfo.php',
+ 'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php',
+ 'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php',
'ApiQueryLogEvents' => 'includes/api/ApiQueryLogEvents.php',
'ApiQueryRecentChanges'=> 'includes/api/ApiQueryRecentChanges.php',
'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php',
+ 'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php',
'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php',
+ 'ApiQueryUserInfo' => 'includes/api/ApiQueryUserInfo.php',
'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php',
'ApiResult' => 'includes/api/ApiResult.php',
);
@@ -346,6 +384,4 @@ function wfLoadAllExtensions() {
require( $file );
}
}
-}
-
-?>
+} \ No newline at end of file
diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php
index e8abb74e..a40d020e 100644
--- a/includes/BagOStuff.php
+++ b/includes/BagOStuff.php
@@ -172,7 +172,7 @@ class HashBagOStuff extends BagOStuff {
*/
var $bag;
- function HashBagOStuff() {
+ function __construct() {
$this->bag = array();
}
@@ -222,7 +222,7 @@ abstract class SqlBagOStuff extends BagOStuff {
var $table;
var $lastexpireall = 0;
- function SqlBagOStuff($tablename = 'objectcache') {
+ function __construct($tablename = 'objectcache') {
$this->table = $tablename;
}
@@ -238,8 +238,8 @@ abstract class SqlBagOStuff extends BagOStuff {
}
if($row=$this->_fetchobject($res)) {
$this->_debug("get: retrieved data; exp time is " . $row->exptime);
- if ( $row->exptime != $this->_maxdatetime() &&
- wfTimestamp( TS_UNIX, $row->exptime ) < time() )
+ if ( $row->exptime != $this->_maxdatetime() &&
+ wfTimestamp( TS_UNIX, $row->exptime ) < time() )
{
$this->_debug("get: key has expired, deleting");
$this->delete($key);
@@ -253,6 +253,9 @@ abstract class SqlBagOStuff extends BagOStuff {
}
function set($key,$value,$exptime=0) {
+ if ( wfReadOnly() ) {
+ return false;
+ }
$exptime = intval($exptime);
if($exptime < 0) $exptime = 0;
if($exptime == 0) {
@@ -272,6 +275,9 @@ abstract class SqlBagOStuff extends BagOStuff {
}
function delete($key,$time=0) {
+ if ( wfReadOnly() ) {
+ return false;
+ }
$this->_query(
"DELETE FROM $0 WHERE keyname='$1'", $key );
return true; /* ? */
@@ -339,12 +345,18 @@ abstract class SqlBagOStuff extends BagOStuff {
function expireall() {
/* Remove any items that have expired */
+ if ( wfReadOnly() ) {
+ return false;
+ }
$now = $this->_fromunixtime( time() );
$this->_query( "DELETE FROM $0 WHERE exptime < '$now'" );
}
function deleteall() {
/* Clear *all* items from cache table */
+ if ( wfReadOnly() ) {
+ return false;
+ }
$this->_query( "DELETE FROM $0" );
}
@@ -495,55 +507,22 @@ class TurckBagOStuff extends BagOStuff {
*
*/
class APCBagOStuff extends BagOStuff {
- public function get( $key ) {
- return apc_fetch($key);
- }
-
- public function set( $key, $value, $exptime = 0 ) {
- return apc_store($key, $value, $exptime);
- }
-
- public function delete( $key, $time = 0 ) {
- return apc_delete( $key );
- }
-
- public function add($key, $value, $exptime=0) {
- return apc_add( $key, $value, $exptime );
- }
-}
-
-
-/**
- * Wrapper for XCache object caching functions
- *
- */
-class XCacheBagOStuff extends BagOStuff {
- public function get( $key ) {
- return (xcache_isset($key) ? unserialize(xcache_get($key)) : false);
- }
-
- public function set( $key, $value, $exptime = 0 ) {
- return xcache_set($key, serialize($value), $exptime);
- }
-
- public function delete( $key, $time = 0 ) {
- return xcache_unset( $key );
- }
-
- public function incr($key, $value=1) {
- return (xcache_isset($key) && xcache_inc($key. $value));
- }
-
- public function decr($key, $value=1) {
- return (xcache_isset($key) && xcache_dec($key. $value));
+ function get($key) {
+ $val = apc_fetch($key);
+ if ( is_string( $val ) ) {
+ $val = unserialize( $val );
+ }
+ return $val;
}
-
- public function add($key, $value, $exptime=0) {
- return (!xcache_isset($key) && $this->set( $key, $value, $exptime ));
+
+ function set($key, $value, $exptime=0) {
+ apc_store($key, serialize($value), $exptime);
+ return true;
}
-
- public function replace($key, $value, $exptime=0) {
- return (xcache_isset($key) && $this->set( $key, $value, $exptime ));
+
+ function delete($key, $time=0) {
+ apc_delete($key);
+ return true;
}
}
@@ -586,11 +565,57 @@ class eAccelBagOStuff extends BagOStuff {
}
/**
+ * Wrapper for XCache object caching functions; identical interface
+ * to the APC wrapper
+ */
+class XCacheBagOStuff extends BagOStuff {
+
+ /**
+ * Get a value from the XCache object cache
+ *
+ * @param string $key Cache key
+ * @return mixed
+ */
+ public function get( $key ) {
+ $val = xcache_get( $key );
+ if( is_string( $val ) )
+ $val = unserialize( $val );
+ return $val;
+ }
+
+ /**
+ * Store a value in the XCache object cache
+ *
+ * @param string $key Cache key
+ * @param mixed $value Object to store
+ * @param int $expire Expiration time
+ * @return bool
+ */
+ public function set( $key, $value, $expire = 0 ) {
+ xcache_set( $key, serialize( $value ), $expire );
+ return true;
+ }
+
+ /**
+ * Remove a value from the XCache object cache
+ *
+ * @param string $key Cache key
+ * @param int $time Not used in this implementation
+ * @return bool
+ */
+ public function delete( $key, $time = 0 ) {
+ xcache_unset( $key );
+ return true;
+ }
+
+}
+
+/**
* @todo document
*/
class DBABagOStuff extends BagOStuff {
var $mHandler, $mFile, $mReader, $mWriter, $mDisabled;
-
+
function __construct( $handler = 'db3', $dir = false ) {
if ( $dir === false ) {
global $wgTmpDirectory;
@@ -617,7 +642,7 @@ class DBABagOStuff extends BagOStuff {
if ( !is_string( $blob ) ) {
return array( null, 0 );
} else {
- return array(
+ return array(
unserialize( substr( $blob, 11 ) ),
intval( substr( $blob, 0, 10 ) )
);
@@ -684,6 +709,7 @@ class DBABagOStuff extends BagOStuff {
function delete( $key, $time = 0 ) {
wfProfileIn( __METHOD__ );
+ wfDebug( __METHOD__."($key)\n" );
$handle = $this->getWriter();
if ( !$handle ) {
return false;
@@ -718,5 +744,5 @@ class DBABagOStuff extends BagOStuff {
return $ret;
}
}
+
-?>
diff --git a/includes/Block.php b/includes/Block.php
index 94bfa5b4..3688d7cf 100644
--- a/includes/Block.php
+++ b/includes/Block.php
@@ -15,7 +15,8 @@
class Block
{
/* public*/ var $mAddress, $mUser, $mBy, $mReason, $mTimestamp, $mAuto, $mId, $mExpiry,
- $mRangeStart, $mRangeEnd, $mAnonOnly, $mEnableAutoblock, $mHideName;
+ $mRangeStart, $mRangeEnd, $mAnonOnly, $mEnableAutoblock, $mHideName,
+ $mBlockEmail;
/* private */ var $mNetworkBits, $mIntegerAddr, $mForUpdate, $mFromMaster, $mByName;
const EB_KEEP_EXPIRED = 1;
@@ -24,7 +25,7 @@ class Block
function __construct( $address = '', $user = 0, $by = 0, $reason = '',
$timestamp = '' , $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0,
- $hideName = 0 )
+ $hideName = 0, $blockEmail = 0 )
{
$this->mId = 0;
# Expand valid IPv6 addresses
@@ -40,7 +41,7 @@ class Block
$this->mExpiry = self::decodeExpiry( $expiry );
$this->mEnableAutoblock = $enableAutoblock;
$this->mHideName = $hideName;
-
+ $this->mBlockEmail = $blockEmail;
$this->mForUpdate = false;
$this->mFromMaster = false;
$this->mByName = false;
@@ -76,7 +77,7 @@ class Block
$this->mAddress = $this->mReason = $this->mTimestamp = '';
$this->mId = $this->mAnonOnly = $this->mCreateAccount =
$this->mEnableAutoblock = $this->mAuto = $this->mUser =
- $this->mBy = $this->mHideName = 0;
+ $this->mBy = $this->mHideName = $this->mBlockEmail = 0;
$this->mByName = false;
}
@@ -262,6 +263,7 @@ class Block
$this->mAnonOnly = $row->ipb_anon_only;
$this->mCreateAccount = $row->ipb_create_account;
$this->mEnableAutoblock = $row->ipb_enable_autoblock;
+ $this->mBlockEmail = $row->ipb_block_email;
$this->mHideName = $row->ipb_deleted;
$this->mId = $row->ipb_id;
$this->mExpiry = self::decodeExpiry( $row->ipb_expiry );
@@ -371,6 +373,7 @@ class Block
# Unset ipb_enable_autoblock for IP blocks, makes no sense
if ( !$this->mUser ) {
$this->mEnableAutoblock = 0;
+ $this->mBlockEmail = 0; //Same goes for email...
}
# Don't collide with expired blocks
@@ -392,7 +395,8 @@ class Block
'ipb_expiry' => self::encodeExpiry( $this->mExpiry, $dbw ),
'ipb_range_start' => $this->mRangeStart,
'ipb_range_end' => $this->mRangeEnd,
- 'ipb_deleted' => $this->mHideName
+ 'ipb_deleted' => $this->mHideName,
+ 'ipb_block_email' => $this->mBlockEmail
), 'Block::insert', array( 'IGNORE' )
);
$affected = $dbw->affectedRows();
@@ -438,9 +442,6 @@ class Block
* @return bool Whether or not an autoblock was inserted.
*/
function doAutoblock( $autoblockip, $justInserted = false ) {
- # Check if this IP address is already blocked
- $dbw = wfGetDB( DB_MASTER );
-
# If autoblocks are disabled, go away.
if ( !$this->mEnableAutoblock ) {
return;
@@ -627,11 +628,11 @@ class Block
/**
* Decode expiry which has come from the DB
*/
- static function decodeExpiry( $expiry ) {
+ static function decodeExpiry( $expiry, $timestampType = TS_MW ) {
if ( $expiry == '' || $expiry == Block::infinity() ) {
return Block::infinity();
} else {
- return wfTimestamp( TS_MW, $expiry );
+ return wfTimestamp( $timestampType, $expiry );
}
}
@@ -698,4 +699,4 @@ class Block
}
}
-?>
+
diff --git a/includes/CacheDependency.php b/includes/CacheDependency.php
index bb5c5437..1d48c383 100644
--- a/includes/CacheDependency.php
+++ b/includes/CacheDependency.php
@@ -344,4 +344,4 @@ class ConstantDependency extends CacheDependency {
}
}
-?>
+
diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php
index 356f9ea2..76a388a6 100644
--- a/includes/CategoryPage.php
+++ b/includes/CategoryPage.php
@@ -90,6 +90,11 @@ class CategoryViewer {
$this->getImageSection() .
$this->getCategoryBottom();
+ // Give a proper message if category is empty
+ if ( $r == '' ) {
+ $r = wfMsgExt( 'category-empty', array( 'parse' ) );
+ }
+
wfProfileOut( __METHOD__ );
return $r;
}
@@ -101,7 +106,7 @@ class CategoryViewer {
$this->children_start_char = array();
if( $this->showGallery ) {
$this->gallery = new ImageGallery();
- $this->gallery->setParsing();
+ $this->gallery->setHideBadImages();
}
}
@@ -147,7 +152,7 @@ class CategoryViewer {
/**
* Add a page in the image namespace
*/
- function addImage( $title, $sortkey, $pageLength ) {
+ function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) {
if ( $this->showGallery ) {
$image = new Image( $title );
if( $this->flip ) {
@@ -156,18 +161,18 @@ class CategoryViewer {
$this->gallery->add( $image );
}
} else {
- $this->addPage( $title, $sortkey, $pageLength );
+ $this->addPage( $title, $sortkey, $pageLength, $isRedirect );
}
}
/**
* Add a miscellaneous page
*/
- function addPage( $title, $sortkey, $pageLength ) {
+ function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) {
global $wgContLang;
- $this->articles[] = $this->getSkin()->makeSizeLinkObj(
- $pageLength, $title, $wgContLang->convert( $title->getPrefixedText() )
- );
+ $this->articles[] = $isRedirect
+ ? '<span class="redirect-in-category">' . $this->getSkin()->makeKnownLinkObj( $title ) . '</span>'
+ : $this->getSkin()->makeSizeLinkObj( $pageLength, $title );
$this->articles_start_char[] = $wgContLang->convert( $wgContLang->firstChar( $sortkey ) );
}
@@ -194,7 +199,7 @@ class CategoryViewer {
}
$res = $dbr->select(
array( 'page', 'categorylinks' ),
- array( 'page_title', 'page_namespace', 'page_len', 'cl_sortkey' ),
+ array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey' ),
array( $pageCondition,
'cl_from = page_id',
'cl_to' => $this->title->getDBKey()),
@@ -219,23 +224,25 @@ class CategoryViewer {
if( $title->getNamespace() == NS_CATEGORY ) {
$this->addSubcategory( $title, $x->cl_sortkey, $x->page_len );
- } elseif( $title->getNamespace() == NS_IMAGE ) {
- $this->addImage( $title, $x->cl_sortkey, $x->page_len );
+ } elseif( $this->showGallery && $title->getNamespace() == NS_IMAGE ) {
+ $this->addImage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect );
} else {
- $this->addPage( $title, $x->cl_sortkey, $x->page_len );
+ $this->addPage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect );
}
}
$dbr->freeResult( $res );
}
function getCategoryTop() {
- $r = "<br style=\"clear:both;\"/>\n";
+ $r = '';
if( $this->until != '' ) {
$r .= $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit );
} elseif( $this->nextPage != '' || $this->from != '' ) {
$r .= $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit );
}
- return $r;
+ return $r == ''
+ ? $r
+ : "<br style=\"clear:both;\"/>\n" . $r;
}
function getSubcategorySection() {
@@ -352,7 +359,7 @@ class CategoryViewer {
}
$cont_msg = "";
if ( $articles_start_char[$index] == $prev_start_char )
- $cont_msg = wfMsgHtml('listingcontinuesabbrev');
+ $cont_msg = ' ' . wfMsgHtml( 'listingcontinuesabbrev' );
$r .= "<h3>" . htmlspecialchars( $articles_start_char[$index] ) . "$cont_msg</h3>\n<ul>";
$prev_start_char = $articles_start_char[$index];
}
@@ -423,4 +430,4 @@ class CategoryViewer {
}
-?>
+
diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php
index 7faae935..b94dcf5e 100644
--- a/includes/Categoryfinder.php
+++ b/includes/Categoryfinder.php
@@ -189,4 +189,4 @@ class Categoryfinder {
} # END OF CLASS "Categoryfinder"
-?>
+
diff --git a/includes/ChangesList.php b/includes/ChangesList.php
index 751e1226..8d0f9508 100644
--- a/includes/ChangesList.php
+++ b/includes/ChangesList.php
@@ -9,7 +9,7 @@ class RCCacheEntry extends RecentChange
var $curlink , $difflink, $lastlink , $usertalklink , $versionlink ;
var $userlink, $timestamp, $watched;
- function newFromParent( $rc ) {
+ static function newFromParent( $rc ) {
$rc2 = new RCCacheEntry;
$rc2->mAttribs = $rc->mAttribs;
$rc2->mExtra = $rc->mExtra;
@@ -171,7 +171,8 @@ class ChangesList {
? 'rcid='.$rc->mAttribs['rc_id']
: '';
$articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params );
- if($watched) $articlelink = '<strong>'.$articlelink.'</strong>';
+ if( $watched )
+ $articlelink = "<strong class=\"mw-watched\">{$articlelink}</strong>";
global $wgContLang;
$articlelink .= $wgContLang->getDirMark();
@@ -204,7 +205,7 @@ class ChangesList {
*/
function usePatrol() {
global $wgUseRCPatrol, $wgUser;
- return( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) );
+ return( $wgUseRCPatrol && ($wgUser->isAllowed('patrol') || $wgUser->isAllowed('patrolmarks')) );
}
/**
@@ -568,7 +569,7 @@ class EnhancedChangesList extends ChangesList {
function maybeWatchedLink( $link, $watched=false ) {
if( $watched ) {
// FIXME: css style might be more appropriate
- return '<strong>' . $link . '</strong>';
+ return '<strong class="mw-watched">' . $link . '</strong>';
} else {
return $link;
}
@@ -703,4 +704,4 @@ class EnhancedChangesList extends ChangesList {
}
}
-?>
+
diff --git a/includes/CoreParserFunctions.php b/includes/CoreParserFunctions.php
index 72ceb45f..a5f45016 100644
--- a/includes/CoreParserFunctions.php
+++ b/includes/CoreParserFunctions.php
@@ -97,15 +97,20 @@ class CoreParserFunctions {
return $parser->getFunctionLang()->convertPlural( $text, $arg0, $arg1, $arg2, $arg3, $arg4 );
}
- static function displaytitle( $parser, $param = '' ) {
- $parserOptions = new ParserOptions;
- $local_parser = clone $parser;
- $t2 = $local_parser->parse ( $param, $parser->mTitle, $parserOptions, false );
- $parser->mOutput->mHTMLtitle = $t2->GetText();
-
- # Add subtitle
- $t = $parser->mTitle->getPrefixedText();
- $parser->mOutput->mSubtitle .= wfMsg('displaytitle', $t);
+ /**
+ * Override the title of the page when viewed,
+ * provided we've been given a title which
+ * will normalise to the canonical title
+ *
+ * @param Parser $parser Parent parser
+ * @param string $text Desired title text
+ * @return string
+ */
+ static function displaytitle( $parser, $text = '' ) {
+ $text = trim( Sanitizer::decodeCharReferences( $text ) );
+ $title = Title::newFromText( $text );
+ if( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) )
+ $parser->mOutput->setDisplayTitle( $text );
return '';
}
@@ -156,7 +161,7 @@ class CoreParserFunctions {
static 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 )
+ return ( $string !== '' && (int)$length > 0 && strlen( trim( (string)$char ) ) > 0 )
? str_pad( $string, $length, (string)$char, $direction )
: $string;
}
@@ -193,4 +198,4 @@ class CoreParserFunctions {
return '';
}
}
-?>
+
diff --git a/includes/Credits.php b/includes/Credits.php
index 87382a86..580a8d92 100644
--- a/includes/Credits.php
+++ b/includes/Credits.php
@@ -185,4 +185,4 @@ function creditOthersLink($article) {
return $skin->makeKnownLink($article->mTitle->getPrefixedText(), wfMsg('others'), 'action=credits');
}
-?>
+
diff --git a/includes/Database.php b/includes/Database.php
index 3fd6ad16..4f8c7d5e 100644
--- a/includes/Database.php
+++ b/includes/Database.php
@@ -214,7 +214,7 @@ border=\"0\" ALT=\"Google\"></A>
$cache = new HTMLFileCache( $t );
if( $cache->isFileCached() ) {
- // FIXME: $msg is not defined on the next line.
+ // @todo, FIXME: $msg is not defined on the next line.
$msg = '<p style="color: red"><b>'.$msg."<br />\n" .
$cachederror . "</b></p>\n";
@@ -441,6 +441,14 @@ class Database {
}
/**
+ * Returns true if this database does an implicit order by when the column has an index
+ * For example: SELECT page_title FROM page LIMIT 1
+ */
+ function implicitOrderby() {
+ return true;
+ }
+
+ /**
* Returns true if this database can do a native search on IP columns
* e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
*/
@@ -448,6 +456,13 @@ class Database {
return false;
}
+ /**
+ * Returns true if this database can use functional indexes
+ */
+ function functionalIndexes() {
+ return false;
+ }
+
/**#@+
* Get function
*/
@@ -582,7 +597,7 @@ class Database {
@/**/$this->mConn = mysql_connect( $server, $user, $password, true );
}
if ($this->mConn === false) {
- $iplus = $i + 1;
+ #$iplus = $i + 1;
#wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
}
}
@@ -678,9 +693,12 @@ class Database {
* Usually aborts on failure. If errors are explicitly ignored, returns success.
*
* @param $sql String: SQL query
- * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST comment (you can use __METHOD__ or add some extra info)
- * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors... maybe best to catch the exception instead?
- * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure if $tempIgnore set
+ * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST
+ * comment (you can use __METHOD__ or add some extra info)
+ * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors...
+ * maybe best to catch the exception instead?
+ * @return true for a successful write query, ResultWrapper object for a successful read query,
+ * or false on failure if $tempIgnore set
* @throws DBQueryError Thrown when the database returns an error of any kind
*/
public function query( $sql, $fname = '', $tempIgnore = false ) {
@@ -765,7 +783,7 @@ class Database {
wfProfileOut( $queryProf );
wfProfileOut( $totalProf );
}
- return $ret;
+ return $this->resultObject( $ret );
}
/**
@@ -909,6 +927,9 @@ class Database {
* Free a result object
*/
function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
if ( !@/**/mysql_free_result( $res ) ) {
throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
}
@@ -924,6 +945,9 @@ class Database {
* @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchObject( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
@/**/$row = mysql_fetch_object( $res );
if( $this->lastErrno() ) {
throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
@@ -940,6 +964,9 @@ class Database {
* @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
@/**/$row = mysql_fetch_array( $res );
if ( $this->lastErrno() ) {
throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
@@ -951,6 +978,9 @@ class Database {
* Get the number of rows in a result object
*/
function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
@/**/$n = mysql_num_rows( $res );
if( $this->lastErrno() ) {
throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
@@ -962,14 +992,24 @@ class Database {
* Get the number of fields in a result object
* See documentation for mysql_num_fields()
*/
- function numFields( $res ) { return mysql_num_fields( $res ); }
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_num_fields( $res );
+ }
/**
* Get a field name in a result object
* See documentation for mysql_field_name():
* http://www.php.net/mysql_field_name
*/
- function fieldName( $res, $n ) { return mysql_field_name( $res, $n ); }
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_field_name( $res, $n );
+ }
/**
* Get the inserted value of an auto-increment row
@@ -987,7 +1027,12 @@ class Database {
* Change the position of the cursor in a result object
* See mysql_data_seek()
*/
- function dataSeek( $res, $row ) { return mysql_data_seek( $res, $row ); }
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_data_seek( $res, $row );
+ }
/**
* Get the last error number
@@ -1091,6 +1136,7 @@ class Database {
}
if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
//if (isset($options['LIMIT'])) {
@@ -1101,7 +1147,7 @@ class Database {
if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
- if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
# Various MySQL extensions
if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
@@ -1175,7 +1221,6 @@ class Database {
if (isset($options['EXPLAIN'])) {
$sql = 'EXPLAIN ' . $sql;
}
-
return $this->query( $sql, $fname );
}
@@ -1352,9 +1397,9 @@ class Database {
function fieldInfo( $table, $field ) {
$table = $this->tableName( $table );
$res = $this->query( "SELECT * FROM $table LIMIT 1" );
- $n = mysql_num_fields( $res );
+ $n = mysql_num_fields( $res->result );
for( $i = 0; $i < $n; $i++ ) {
- $meta = mysql_fetch_field( $res, $i );
+ $meta = mysql_fetch_field( $res->result, $i );
if( $field == $meta->name ) {
return new MySQLField($meta);
}
@@ -1366,6 +1411,9 @@ class Database {
* mysql_field_type() wrapper
*/
function fieldType( $res, $index ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
return mysql_field_type( $res, $index );
}
@@ -1455,6 +1503,7 @@ class Database {
* (for the log)
* @param array $options An array of UPDATE options, can be one or
* more of IGNORE, LOW_PRIORITY
+ * @return bool
*/
function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
$table = $this->tableName( $table );
@@ -1463,7 +1512,7 @@ class Database {
if ( $conds != '*' ) {
$sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
}
- $this->query( $sql, $fname );
+ return $this->query( $sql, $fname );
}
/**
@@ -1498,8 +1547,15 @@ class Database {
$list .= "($value)";
} elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
$list .= "$value";
- } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array ($value) ) {
+ } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
$list .= $field." IN (".$this->makeList($value).") ";
+ } elseif( is_null($value) ) {
+ if ( $mode == LIST_AND || $mode == LIST_OR ) {
+ $list .= "$field IS ";
+ } elseif ( $mode == LIST_SET ) {
+ $list .= "$field = ";
+ }
+ $list .= 'NULL';
} else {
if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
$list .= "$field = ";
@@ -1534,7 +1590,7 @@ class Database {
global $wgSharedDB;
# Skip quoted literals
if ( $name{0} != '`' ) {
- if ( $this->mTablePrefix !== '' && strpos( '.', $name ) === false ) {
+ if ( $this->mTablePrefix !== '' && strpos( $name, '.' ) === false ) {
$name = "{$this->mTablePrefix}$name";
}
if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) {
@@ -1570,7 +1626,7 @@ class Database {
* This is handy when you need to construct SQL for joins
*
* Example:
- * list( $user, $watchlist ) = $dbr->tableNames('user','watchlist');
+ * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist');
* $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
* WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
*/
@@ -2001,7 +2057,12 @@ class Database {
*/
function resultObject( $result ) {
if( empty( $result ) ) {
- return NULL;
+ return false;
+ } elseif ( $result instanceof ResultWrapper ) {
+ return $result;
+ } elseif ( $result === true ) {
+ // Successful write query
+ return $result;
} else {
return new ResultWrapper( $this, $result );
}
@@ -2046,8 +2107,7 @@ class Database {
*/
function getLag() {
$res = $this->query( 'SHOW PROCESSLIST' );
- # Find slave SQL thread. Assumed to be the second one running, which is a bit
- # dubious, but unfortunately there's no easy rigorous way
+ # Find slave SQL thread
while ( $row = $this->fetchObject( $res ) ) {
/* This should work for most situations - when default db
* for thread is not specified, it had no events executed,
@@ -2176,7 +2236,7 @@ class Database {
$cmd = $this->replaceVars( $cmd );
$res = $this->query( $cmd, __METHOD__, true );
if ( $resultCallback ) {
- call_user_func( $resultCallback, $this->resultObject( $res ) );
+ call_user_func( $resultCallback, $res );
}
if ( false === $res ) {
@@ -2244,40 +2304,55 @@ class DatabaseMysql extends Database {
* Result wrapper for grabbing data queried by someone else
* @addtogroup Database
*/
-class ResultWrapper {
- var $db, $result;
+class ResultWrapper implements Iterator {
+ var $db, $result, $pos = 0, $currentRow = null;
/**
- * @todo document
+ * Create a new result object from a result resource and a Database object
*/
- function ResultWrapper( &$database, $result ) {
- $this->db =& $database;
- $this->result =& $result;
+ function ResultWrapper( $database, $result ) {
+ $this->db = $database;
+ if ( $result instanceof ResultWrapper ) {
+ $this->result = $result->result;
+ } else {
+ $this->result = $result;
+ }
}
/**
- * @todo document
+ * Get the number of rows in a result object
*/
function numRows() {
return $this->db->numRows( $this->result );
}
/**
- * @todo document
+ * Fetch the next row from the given result object, in object form.
+ * Fields can be retrieved with $row->fieldname, with fields acting like
+ * member variables.
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchObject() {
return $this->db->fetchObject( $this->result );
}
/**
- * @todo document
+ * Fetch the next row from the given result object, in associative array
+ * form. Fields are retrieved with $row['fieldname'].
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchRow() {
return $this->db->fetchRow( $this->result );
}
/**
- * @todo document
+ * Free a result object
*/
function free() {
$this->db->freeResult( $this->result );
@@ -2285,16 +2360,48 @@ class ResultWrapper {
unset( $this->db );
}
+ /**
+ * Change the position of the cursor in a result object
+ * See mysql_data_seek()
+ */
function seek( $row ) {
$this->db->dataSeek( $this->result, $row );
}
-
+
+ /*********************
+ * Iterator functions
+ * Note that using these in combination with the non-iterator functions
+ * above may cause rows to be skipped or repeated.
+ */
+
function rewind() {
if ($this->numRows()) {
$this->db->dataSeek($this->result, 0);
}
+ $this->pos = 0;
+ $this->currentRow = null;
+ }
+
+ function current() {
+ if ( is_null( $this->currentRow ) ) {
+ $this->next();
+ }
+ return $this->currentRow;
+ }
+
+ function key() {
+ return $this->pos;
}
+ function next() {
+ $this->pos++;
+ $this->currentRow = $this->fetchObject();
+ return $this->currentRow;
+ }
+
+ function valid() {
+ return $this->current() !== false;
+ }
}
-?>
+
diff --git a/includes/DatabaseFunctions.php b/includes/DatabaseFunctions.php
index 4b31b4f0..a4a0444f 100644
--- a/includes/DatabaseFunctions.php
+++ b/includes/DatabaseFunctions.php
@@ -399,4 +399,4 @@ function wfUseIndexClause( $index, $dbi = DB_SLAVE ) {
return false;
}
}
-?>
+
diff --git a/includes/DatabaseOracle.php b/includes/DatabaseOracle.php
index 2b720df7..38485481 100644
--- a/includes/DatabaseOracle.php
+++ b/includes/DatabaseOracle.php
@@ -128,6 +128,9 @@ class DatabaseOracle extends Database {
function implicitGroupby() {
return false;
}
+ function implicitOrderby() {
+ return false;
+ }
function searchableIPs() {
return true;
}
@@ -659,7 +662,7 @@ echo "error!\n";
#if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE';
#if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE';
- if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
$useIndex = $this->useIndexClause( $options['USE INDEX'] );
@@ -675,11 +678,20 @@ echo "error!\n";
}
function ping() {
- wfDebug( "Function ping() not written for DatabasePostgres.php yet");
+ wfDebug( "Function ping() not written for DatabaseOracle.php yet");
return true;
}
+ /**
+ * How lagged is this slave?
+ *
+ * @return int
+ */
+ public function getLag() {
+ # Not implemented for Oracle
+ return 0;
+ }
} // end DatabaseOracle class
-?>
+
diff --git a/includes/DatabasePostgres.php b/includes/DatabasePostgres.php
index 07b3339d..32c061a0 100644
--- a/includes/DatabasePostgres.php
+++ b/includes/DatabasePostgres.php
@@ -102,9 +102,15 @@ class DatabasePostgres extends Database {
function implicitGroupby() {
return false;
}
+ function implicitOrderby() {
+ return false;
+ }
function searchableIPs() {
return true;
}
+ function functionalIndexes() {
+ return true;
+ }
static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0)
{
@@ -155,6 +161,7 @@ class DatabasePostgres extends Database {
$this->mOpened = true;
## If this is the initial connection, setup the schema stuff and possibly create the user
+ ## TODO: Move this out of open()
if (defined('MEDIAWIKI_INSTALL')) {
global $wgDBname, $wgDBuser, $wgDBpassword, $wgDBsuperuser, $wgDBmwschema,
$wgDBts2schema;
@@ -504,12 +511,18 @@ class DatabasePostgres extends Database {
}
function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
if ( !@pg_free_result( $res ) ) {
throw new DBUnexpectedError($this, "Unable to free Postgres result\n" );
}
}
function fetchObject( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
@$row = pg_fetch_object( $res );
# FIXME: HACK HACK HACK HACK debug
@@ -523,6 +536,9 @@ class DatabasePostgres extends Database {
}
function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
@$row = pg_fetch_array( $res );
if( pg_last_error($this->mConn) ) {
throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
@@ -531,14 +547,27 @@ class DatabasePostgres extends Database {
}
function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
@$n = pg_num_rows( $res );
if( pg_last_error($this->mConn) ) {
throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
}
return $n;
}
- function numFields( $res ) { return pg_num_fields( $res ); }
- function fieldName( $res, $n ) { return pg_field_name( $res, $n ); }
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return pg_num_fields( $res );
+ }
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return pg_field_name( $res, $n );
+ }
/**
* This must be called after nextSequenceVal
@@ -547,7 +576,13 @@ class DatabasePostgres extends Database {
return $this->mInsertId;
}
- function dataSeek( $res, $row ) { return pg_result_seek( $res, $row ); }
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return pg_result_seek( $res, $row );
+ }
+
function lastError() {
if ( $this->mConn ) {
return pg_last_error();
@@ -561,7 +596,7 @@ class DatabasePostgres extends Database {
}
function affectedRows() {
- if( !isset( $this->mLastResult ) )
+ if( !isset( $this->mLastResult ) or ! $this->mLastResult )
return 0;
return pg_affected_rows( $this->mLastResult );
@@ -601,13 +636,9 @@ class DatabasePostgres extends Database {
if ( !$res ) {
return NULL;
}
-
while ( $row = $this->fetchObject( $res ) ) {
if ( $row->indexname == $index ) {
return $row;
-
- // BUG: !!!! This code needs to be synced up with database.php
-
}
}
return false;
@@ -666,7 +697,7 @@ class DatabasePostgres extends Database {
$sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
if ( $multi ) {
- if ( $wgDBversion >= 8.1 ) {
+ if ( $wgDBversion >= 8.2 ) {
$first = true;
foreach ( $args as $row ) {
if ( $first ) {
@@ -920,10 +951,10 @@ class DatabasePostgres extends Database {
. "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
. "AND c.relkind IN ('" . implode("','", $types) . "')";
$res = $this->query( $SQL );
- $count = $res ? pg_num_rows($res) : 0;
+ $count = $res ? $res->numRows() : 0;
if ($res)
$this->freeResult( $res );
- return $count;
+ return $count ? true : false;
}
/*
@@ -953,7 +984,7 @@ END;
$this->addQuotes($trigger)));
if (!$res)
return NULL;
- $rows = pg_num_rows($res);
+ $rows = $res->numRows();
$this->freeResult($res);
return $rows;
}
@@ -977,7 +1008,7 @@ END;
$res = $this->query($SQL);
if (!$res)
return NULL;
- $rows = pg_num_rows($res);
+ $rows = $res->numRows();
$this->freeResult($res);
return $rows;
}
@@ -990,7 +1021,12 @@ END;
$SQL = "SELECT rolname FROM pg_catalog.pg_namespace n, pg_catalog.pg_roles r "
."WHERE n.nspowner=r.oid AND n.nspname = '$eschema'";
$res = $this->query( $SQL );
- $owner = $res ? pg_num_rows($res) ? pg_fetch_result($res, 0, 0) : false : false;
+ if ( $res && $res->numRows() ) {
+ $row = $res->fetchObject();
+ $owner = $row->rolname;
+ } else {
+ $owner = false;
+ }
if ($res)
$this->freeResult($res);
return $owner;
@@ -1008,7 +1044,7 @@ END;
. "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema' "
. "AND a.attrelid = c.oid AND a.attname = '$ecol'";
$res = $this->query( $SQL, $fname );
- $count = $res ? pg_num_rows($res) : 0;
+ $count = $res ? $res->numRows() : 0;
if ($res)
$this->freeResult( $res );
return $count;
@@ -1102,10 +1138,10 @@ END;
$this->doQuery("COMMIT");
}
- function encodeBlob($b) {
- return array('bytea',pg_escape_bytea($b));
+ function encodeBlob( $b ) {
+ return pg_escape_bytea( $b );
}
- function decodeBlob($b) {
+ function decodeBlob( $b ) {
return pg_unescape_bytea( $b );
}
@@ -1133,8 +1169,7 @@ END;
}
/**
- * Returns an optional USE INDEX clause to go after the table, and a
- * string to go at the end of the query
+ * Various select options
*
* @private
*
@@ -1144,7 +1179,7 @@ END;
*/
function makeSelectOptions( $options ) {
$preLimitTail = $postLimitTail = '';
- $startOpts = '';
+ $startOpts = $useIndex = '';
$noKeyOptions = array();
foreach ( $options as $key => $option ) {
@@ -1154,6 +1189,7 @@ END;
}
if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY " . $options['GROUP BY'];
+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY " . $options['ORDER BY'];
//if (isset($options['LIMIT'])) {
@@ -1164,13 +1200,7 @@ END;
if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
- if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
-
- if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
- $useIndex = $this->useIndexClause( $options['USE INDEX'] );
- } else {
- $useIndex = '';
- }
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
}
@@ -1183,8 +1213,16 @@ END;
wfDebug( "Function ping() not written for DatabasePostgres.php yet");
return true;
}
-
+
+ /**
+ * How lagged is this slave?
+ *
+ */
+ public function getLag() {
+ # Not implemented for PostgreSQL
+ return false;
+ }
} // end DatabasePostgres class
-?>
+
diff --git a/includes/DateFormatter.php b/includes/DateFormatter.php
index 88a64453..bbad6d15 100644
--- a/includes/DateFormatter.php
+++ b/includes/DateFormatter.php
@@ -282,4 +282,4 @@ class DateFormatter
}
}
-?>
+
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index d8f9a621..1ed8779a 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -27,11 +27,11 @@ if( !defined( 'MEDIAWIKI' ) ) {
* Create a site configuration object
* Not used for much in a default install
*/
-require_once( 'includes/SiteConfiguration.php' );
+require_once( "$IP/includes/SiteConfiguration.php" );
$wgConf = new SiteConfiguration;
/** MediaWiki version number */
-$wgVersion = '1.10.2';
+$wgVersion = '1.11.0';
/** Name of the site. It must be changed in LocalSettings.php */
$wgSitename = 'MediaWiki';
@@ -125,8 +125,9 @@ $wgUsePathInfo =
* in LocalSettings.php. Generally you should not need to change this
* unless you don't like seeing "index.php".
*/
-$wgScript = false; /// defaults to "{$wgScriptPath}/index.php"
-$wgRedirectScript = false; /// defaults to "{$wgScriptPath}/redirect.php"
+$wgScriptExtension = '.php'; /// extension to append to script names by default
+$wgScript = false; /// defaults to "{$wgScriptPath}/index{$wgScriptExtension}"
+$wgRedirectScript = false; /// defaults to "{$wgScriptPath}/redirect{$wgScriptExtension}"
/**#@-*/
@@ -163,16 +164,6 @@ $wgUploadBaseUrl = "";
/**#@-*/
/**
- * By default deleted files are simply discarded; to save them and
- * make it possible to undelete images, create a directory which
- * is writable to the web server but is not exposed to the internet.
- *
- * Set $wgSaveDeletedFiles to true and set up the save path in
- * $wgFileStore['deleted']['directory'].
- */
-$wgSaveDeletedFiles = false;
-
-/**
* New file storage paths; currently used only for deleted files.
* Set it like this:
*
@@ -180,10 +171,61 @@ $wgSaveDeletedFiles = false;
*
*/
$wgFileStore = array();
-$wgFileStore['deleted']['directory'] = null; // Don't forget to set this.
+$wgFileStore['deleted']['directory'] = false;// Defaults to $wgUploadDirectory/deleted
$wgFileStore['deleted']['url'] = null; // Private
$wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split
+/**#@+
+ * File repository structures
+ *
+ * $wgLocalFileRepo is a single repository structure, and $wgForeignFileRepo is
+ * a an array of such structures. Each repository structure is an associative
+ * array of properties configuring the repository.
+ *
+ * Properties required for all repos:
+ * class The class name for the repository. May come from the core or an extension.
+ * The core repository classes are LocalRepo, ForeignDBRepo, FSRepo.
+ *
+ * name A unique name for the repository.
+ *
+ * For all core repos:
+ * url Base public URL
+ * hashLevels The number of directory levels for hash-based division of files
+ * thumbScriptUrl The URL for thumb.php (optional, not recommended)
+ * transformVia404 Whether to skip media file transformation on parse and rely on a 404
+ * handler instead.
+ * initialCapital Equivalent to $wgCapitalLinks, determines whether filenames implicitly
+ * start with a capital letter. The current implementation may give incorrect
+ * description page links when the local $wgCapitalLinks and initialCapital
+ * are mismatched.
+ * pathDisclosureProtection
+ * May be 'paranoid' to remove all parameters from error messages, 'none' to
+ * leave the paths in unchanged, or 'simple' to replace paths with
+ * placeholders. Default for LocalRepo is 'simple'.
+ *
+ * These settings describe a foreign MediaWiki installation. They are optional, and will be ignored
+ * for local repositories:
+ * descBaseUrl URL of image description pages, e.g. http://en.wikipedia.org/wiki/Image:
+ * scriptDirUrl URL of the MediaWiki installation, equivalent to $wgScriptPath, e.g.
+ * http://en.wikipedia.org/w
+ *
+ * articleUrl Equivalent to $wgArticlePath, e.g. http://en.wikipedia.org/wiki/$1
+ * fetchDescription Fetch the text of the remote file description page. Equivalent to
+ * $wgFetchCommonsDescriptions.
+ *
+ * ForeignDBRepo:
+ * dbType, dbServer, dbUser, dbPassword, dbName, dbFlags
+ * equivalent to the corresponding member of $wgDBservers
+ * tablePrefix Table prefix, the foreign wiki's $wgDBprefix
+ * hasSharedCache True if the wiki's shared cache is accessible via the local $wgMemc
+ *
+ * The default is to initialise these arrays from the MW<1.11 backwards compatible settings:
+ * $wgUploadPath, $wgThumbnailScriptPath, $wgSharedUploadDirectory, etc.
+ */
+$wgLocalFileRepo = false;
+$wgForeignFileRepos = array();
+/**#@-*/
+
/**
* Allowed title characters -- regex character class
* Don't change this unless you know what you're doing
@@ -261,34 +303,34 @@ $wgAntivirus= NULL;
*
* @global array $wgAntivirusSetup
*/
-$wgAntivirusSetup= array(
+$wgAntivirusSetup = array(
#setup for clamav
'clamav' => array (
'command' => "clamscan --no-summary ",
- 'codemap'=> array (
- "0"=> AV_NO_VIRUS, #no virus
- "1"=> AV_VIRUS_FOUND, #virus found
- "52"=> AV_SCAN_ABORTED, #unsupported file format (probably imune)
- "*"=> AV_SCAN_FAILED, #else scan failed
+ 'codemap' => array (
+ "0" => AV_NO_VIRUS, # no virus
+ "1" => AV_VIRUS_FOUND, # virus found
+ "52" => AV_SCAN_ABORTED, # unsupported file format (probably imune)
+ "*" => AV_SCAN_FAILED, # else scan failed
),
- 'messagepattern'=> '/.*?:(.*)/sim',
+ 'messagepattern' => '/.*?:(.*)/sim',
),
#setup for f-prot
'f-prot' => array (
'command' => "f-prot ",
- 'codemap'=> array (
- "0"=> AV_NO_VIRUS, #no virus
- "3"=> AV_VIRUS_FOUND, #virus found
- "6"=> AV_VIRUS_FOUND, #virus found
- "*"=> AV_SCAN_FAILED, #else scan failed
+ 'codemap' => array (
+ "0" => AV_NO_VIRUS, # no virus
+ "3" => AV_VIRUS_FOUND, # virus found
+ "6" => AV_VIRUS_FOUND, # virus found
+ "*" => AV_SCAN_FAILED, # else scan failed
),
- 'messagepattern'=> '/.*?Infection:(.*)$/m',
+ 'messagepattern' => '/.*?Infection:(.*)$/m',
),
);
@@ -355,6 +397,10 @@ $wgActionPaths = array();
* no file of the given name is found in the local repository (for [[Image:..]],
* [[Media:..]] links). Thumbnails will also be looked for and generated in this
* directory.
+ *
+ * Note that these configuration settings can now be defined on a per-
+ * repository basis for an arbitrary number of file repositories, using the
+ * $wgForeignFileRepos variable.
*/
$wgUseSharedUploads = false;
/** Full path on the web server where shared uploads can be found */
@@ -392,7 +438,7 @@ $wgUploadNavigationUrl = false;
* apache servers don't have read/write access to the thumbnail path.
*
* Example:
- * $wgThumbnailScriptPath = "{$wgScriptPath}/thumb.php";
+ * $wgThumbnailScriptPath = "{$wgScriptPath}/thumb{$wgScriptExtension}";
*/
$wgThumbnailScriptPath = false;
$wgSharedThumbnailScriptPath = false;
@@ -600,6 +646,19 @@ $wgDBmysql5 = false;
$wgLocalDatabases = array();
/**
+ * For multi-wiki clusters with multiple master servers; if an alternate
+ * is listed for the requested database, a connection to it will be opened
+ * instead of to the current wiki's regular master server when cross-wiki
+ * data operations are done from here.
+ *
+ * Requires that the other server be accessible by network, with the same
+ * username/password as the primary.
+ *
+ * eg $wgAlternateMaster['enwiki'] = 'ariel';
+ */
+$wgAlternateMaster = array();
+
+/**
* Object cache settings
* See Defines.php for types
*/
@@ -619,7 +678,6 @@ $wgLinkCacheMemcached = false; # Not fully tested
$wgUseMemCached = false;
$wgMemCachedDebug = false; # Will be set to false in Setup.php, if the server isn't working
$wgMemCachedServers = array( '127.0.0.1:11000' );
-$wgMemCachedDebug = false;
$wgMemCachedPersistent = false;
/**
@@ -806,6 +864,7 @@ $wgRedirectSources = false;
$wgShowIPinHeader = true; # For non-logged in users
$wgMaxNameChars = 255; # Maximum number of bytes in username
+$wgMaxSigChars = 255; # Maximum number of Unicode characters in signature
$wgMaxArticleSize = 2048; # Maximum article size in kilobytes
$wgExtraSubtitle = '';
@@ -869,8 +928,12 @@ $wgColorErrors = true;
$wgShowExceptionDetails = false;
/**
- * disable experimental dmoz-like category browsing. Output things like:
- * Encyclopedia > Music > Style of Music > Jazz
+ * Expose backend server host names through the API and various HTML comments
+ */
+$wgShowHostnames = false;
+
+/**
+ * Use experimental, DMOZ-like category browser
*/
$wgUseCategoryBrowser = false;
@@ -920,9 +983,10 @@ $wgHitcounterUpdateFreq = 1;
# Basic user rights and block settings
$wgSysopUserBans = true; # Allow sysops to ban logged-in users
-$wgSysopRangeBans = true; # Allow sysops to ban IP ranges
-$wgAutoblockExpiry = 86400; # Number of seconds before autoblock entries expire
+$wgSysopRangeBans = true; # Allow sysops to ban IP ranges
+$wgAutoblockExpiry = 86400; # Number of seconds before autoblock entries expire
$wgBlockAllowsUTEdit = false; # Blocks allow users to edit their own user talk page
+$wgSysopEmailBans = true; # Allow sysops to ban users from accessing Emailuser
# Pages anonymous user may see as an array, e.g.:
# array ( "Main Page", "Special:Userlogin", "Wikipedia:Help");
@@ -947,6 +1011,11 @@ $wgEmailConfirmToEdit=false;
* combined with the permissions of all groups that a given user is listed
* in in the user_groups table.
*
+ * Note: Don't set $wgGroupPermissions = array(); unless you know what you're
+ * doing! This will wipe all permissions, and may mean that your users are
+ * unable to perform certain essential tasks or access new functionality
+ * when new permissions are introduced and default grants established.
+ *
* Functionality to make pages inaccessible has not been extensively tested
* for security. Use at your own risk!
*
@@ -1009,6 +1078,7 @@ $wgGroupPermissions['sysop']['unwatchedpages'] = true;
$wgGroupPermissions['sysop']['autoconfirmed'] = true;
$wgGroupPermissions['sysop']['upload_by_url'] = true;
$wgGroupPermissions['sysop']['ipblock-exempt'] = true;
+$wgGroupPermissions['sysop']['blockemail'] = true;
// Permission to change users' group assignments
$wgGroupPermissions['bureaucrat']['userrights'] = true;
@@ -1033,8 +1103,14 @@ $wgGroupPermissions['bureaucrat']['userrights'] = true;
$wgRestrictionTypes = array( 'edit', 'move' );
/**
- * Set of permission keys that can be selected via action=protect.
- * 'autoconfirm' allows all registerd users if $wgAutoConfirmAge is 0.
+ * Rights which can be required for each protection level (via action=protect)
+ *
+ * You can add a new protection level that requires a specific
+ * permission by manipulating this array. The ordering of elements
+ * dictates the order on the protection form's lists.
+ *
+ * '' will be ignored (i.e. unprotected)
+ * 'sysop' is quietly rewritten to 'protect' for backwards compatibility
*/
$wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' );
@@ -1074,7 +1150,20 @@ $wgAutoConfirmAge = 0;
$wgAutoConfirmCount = 0;
//$wgAutoConfirmCount = 50;
-
+/**
+ * These settings can be used to give finer control over who can assign which
+ * groups at Special:Userrights. Example configuration:
+ *
+ * // Bureaucrat can add any group
+ * $wgAddGroups['bureaucrat'] = true;
+ * // Bureaucrats can only remove bots and sysops
+ * $wgRemoveGroups['bureaucrat'] = array( 'bot', 'sysop' );
+ * // Sysops can make bots
+ * $wgAddGroups['sysop'] = array( 'bot' );
+ * // Sysops can disable other sysops in an emergency, and disable bots
+ * $wgRemoveGroups['sysop'] = array( 'sysop', 'bot' );
+ */
+$wgAddGroups = $wgRemoveGroups = array();
# Proxy scanner settings
#
@@ -1125,7 +1214,7 @@ $wgCacheEpoch = '20030516000000';
* to ensure that client-side caches don't keep obsolete copies of global
* styles.
*/
-$wgStyleVersion = '63';
+$wgStyleVersion = '97';
# Server-side caching:
@@ -1174,6 +1263,21 @@ $wgEnotifRevealEditorAddress = false; # UPO; reply-to address may be filled with
$wgEnotifMinorEdits = true; # UPO; false: "minor edits" on pages do not trigger notification mails.
# # Attention: _every_ change on a user_talk page trigger a notification mail (if the user is not yet notified)
+# Send a generic mail instead of a personalised mail for each user. This
+# always uses UTC as the time zone, and doesn't include the username.
+#
+# For pages with many users watching, this can significantly reduce mail load.
+# Has no effect when using sendmail rather than SMTP;
+
+$wgEnotifImpersonal = false;
+
+# Maximum number of users to mail at once when using impersonal mail. Should
+# match the limit on your mail server.
+$wgEnotifMaxRecips = 500;
+
+# Send mails via the job queue.
+$wgEnotifUseJobQ = false;
+
/**
* Array of usernames who will be sent a notification email for every change which occurs on a wiki
*/
@@ -1230,7 +1334,11 @@ $wgSquidMaxage = 18000;
/**
* A list of proxy servers (ips if possible) to purge on changes don't specify
- * ports here (80 is default)
+ * ports here (80 is default). When mediawiki is running behind a proxy, its
+ * address should be listed in $wgSquidServers otherwise mediawiki won't rely
+ * on the X-FORWARDED-FOR header to determine the user IP address and
+ * all users will appear to come from the proxy IP address. Don't use domain
+ * names here, only IP adresses.
*/
# $wgSquidServers = array('127.0.0.1');
$wgSquidServers = array();
@@ -1283,6 +1391,18 @@ $wgWantedPagesThreshold = 1;
$wgAllowSlowParserFunctions = false;
/**
+ * Maps jobs to their handling classes; extensions
+ * can add to this to provide custom jobs
+ */
+$wgJobClasses = array(
+ 'refreshLinks' => 'RefreshLinksJob',
+ 'htmlCacheUpdate' => 'HTMLCacheUpdateJob',
+ 'html_cache_update' => 'HTMLCacheUpdateJob', // backwards-compatible
+ 'sendMail' => 'EmaillingJob',
+ 'enotifNotify' => 'EnotifNotifyJob',
+);
+
+/**
* To use inline TeX, you need to compile 'texvc' (in the 'math' subdirectory of
* the MediaWiki package and have latex, dvips, gs (ghostscript), andconvert
* (ImageMagick) installed and available in the PATH.
@@ -1322,7 +1442,11 @@ $wgDebugFunctionEntry = 0;
/** Lots of debugging output from SquidUpdate.php */
$wgDebugSquid = false;
+/** Whereas to count the number of time an article is viewed.
+ * Does not work if pages are cached (for example with squid).
+ */
$wgDisableCounters = false;
+
$wgDisableTextSearch = false;
$wgDisableSearchContext = false;
/**
@@ -1335,6 +1459,13 @@ $wgEnableUploads = false;
/**
* Show EXIF data, on by default if available.
* Requires PHP's EXIF extension: http://www.php.net/manual/en/ref.exif.php
+ *
+ * NOTE FOR WINDOWS USERS:
+ * To enable EXIF functions, add the folloing lines to the
+ * "Windows extensions" section of php.ini:
+ *
+ * extension=extensions/php_mbstring.dll
+ * extension=extensions/php_exif.dll
*/
$wgShowEXIF = function_exists( 'exif_read_data' );
@@ -1373,7 +1504,7 @@ $wgAntiLockFlags = 0;
$wgDiff3 = '/usr/bin/diff3';
/**
- * We can also compress text in the old revisions table. If this is set on, old
+ * We can also compress text stored in the 'text' table. If this is set on, new
* revisions will be compressed on page save if zlib support is available. Any
* compressed revisions will be decompressed on load regardless of this setting
* *but will not be readable at all* if zlib support is not available.
@@ -1389,7 +1520,7 @@ $wgFileExtensions = array( 'png', 'gif', 'jpg', 'jpeg' );
/** Files with these extensions will never be allowed as uploads. */
$wgFileBlacklist = array(
# HTML may contain cookie-stealing JavaScript and web bugs
- 'html', 'htm', 'js', 'jsb',
+ 'html', 'htm', 'js', 'jsb', 'mhtml', 'mht',
# PHP scripts may execute arbitrary code on the server
'php', 'phtml', 'php3', 'php4', 'php5', 'phps',
# Other types that may be interpreted by some servers
@@ -1445,10 +1576,15 @@ $wgNamespacesToBeSearchedDefault = array(
NS_MAIN => true,
);
-/** If set, a bold ugly notice will show up at the top of every page. */
+/**
+ * Site notice shown at the top of each page
+ *
+ * This message can contain wiki text, and can also be set through the
+ * MediaWiki:Sitenotice page. You can also provide a separate message for
+ * logged-out users using the MediaWiki:Anonnotice page.
+ */
$wgSiteNotice = '';
-
#
# Images settings
#
@@ -1463,6 +1599,7 @@ $wgMediaHandlers = array(
'image/gif' => 'BitmapHandler',
'image/x-ms-bmp' => 'BmpHandler',
'image/svg+xml' => 'SvgHandler',
+ 'image/svg' => 'SvgHandler',
'image/vnd.djvu' => 'DjVuHandler',
);
@@ -1780,6 +1917,28 @@ $wgExtensionFunctions = array();
$wgSkinExtensionFunctions = array();
/**
+ * Extension messages files
+ * Associative array mapping extension name to the filename where messages can be found.
+ * The file must create a variable called $messages.
+ * When the messages are needed, the extension should call wfLoadMessagesFile()
+ */
+$wgExtensionMessagesFiles = array();
+
+/**
+ * Parser output hooks.
+ * This is an associative array where the key is an extension-defined tag
+ * (typically the extension name), and the value is a PHP callback.
+ * These will be called as an OutputPageParserOutput hook, if the relevant
+ * tag has been registered with the parser output object.
+ *
+ * Registration is done with $pout->addOutputHook( $tag, $data ).
+ *
+ * The callback has the form:
+ * function outputHook( $outputPage, $parserOutput, $data ) { ... }
+ */
+$wgParserOutputHooks = array();
+
+/**
* List of valid skin names.
* The key should be the name in all lower case, the value should be a display name.
* The default skins will be added later, by Skin::getSkinNames(). Use
@@ -1937,6 +2096,13 @@ $wgThumbLimits = array(
);
/**
+ * Adjust width of upright images when parameter 'upright' is used
+ * This allows a nicer look for upright images without the need to fix the width
+ * by hardcoded px in wiki sourcecode.
+ */
+$wgThumbUpright = 0.75;
+
+/**
* On category pages, show thumbnail gallery for images belonging to that
* category instead of listing them as articles.
*/
@@ -1978,7 +2144,13 @@ $wgBrowserBlackList = array(
* @link http://en.wikipedia.org/w/index.php?title=User%3A%C6var_Arnfj%F6r%F0_Bjarmason%2Ftestme&diff=12356041&oldid=12355864
* @link http://en.wikipedia.org/wiki/Template%3AOS9
*/
- '/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/'
+ '/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/',
+
+ /**
+ * Google wireless transcoder, seems to eat a lot of chars alive
+ * http://it.wikipedia.org/w/index.php?title=Luciano_Ligabue&diff=prev&oldid=8857361
+ */
+ '/^Mozilla\/4\.0 \(compatible; MSIE 6.0; Windows NT 5.0; Google Wireless Transcoder;\)/'
);
/**
@@ -2075,7 +2247,7 @@ $wgLogTypes = array( '',
* Extensions with custom log types may add to this array.
*/
$wgLogNames = array(
- '' => 'log',
+ '' => 'all-logs-page',
'block' => 'blocklogpage',
'protect' => 'protectlogpage',
'rights' => 'rightslog',
@@ -2114,12 +2286,14 @@ $wgLogActions = array(
'block/block' => 'blocklogentry',
'block/unblock' => 'unblocklogentry',
'protect/protect' => 'protectedarticle',
+ 'protect/modify' => 'modifiedarticleprotection',
'protect/unprotect' => 'unprotectedarticle',
'rights/rights' => 'rightslogentry',
'delete/delete' => 'deletedarticle',
'delete/restore' => 'undeletedarticle',
'delete/revision' => 'revdelete-logentry',
'upload/upload' => 'uploadedimage',
+ 'upload/overwrite' => 'overwroteimage',
'upload/revert' => 'uploadedimage',
'move/move' => '1movedto2',
'move/move_redir' => '1movedto2_redir',
@@ -2178,6 +2352,16 @@ $wgNoFollowNsExceptions = array();
$wgNamespaceRobotPolicies = array();
/**
+ * Robot policies per article.
+ * These override the per-namespace robot policies.
+ * Must be in the form of an array where the key part is a properly
+ * canonicalised text form title and the value is a robot policy.
+ * Example:
+ * $wgArticleRobotPolicies = array( 'Main Page' => 'noindex' );
+ */
+$wgArticleRobotPolicies = array();
+
+/**
* Specifies the minimal length of a user password. If set to
* 0, empty passwords are allowed.
*/
@@ -2385,7 +2569,7 @@ $wgUpdateRowsPerQuery = 10;
/**
* Enable AJAX framework
*/
-$wgUseAjax = false;
+$wgUseAjax = true;
/**
* Enable auto suggestion for the search bar
@@ -2405,12 +2589,22 @@ $wgAjaxExportList = array( );
* Requires $wgUseAjax to be true too.
* Causes wfAjaxWatch to be added to $wgAjaxExportList
*/
-$wgAjaxWatch = false;
+$wgAjaxWatch = true;
+
+/**
+ * Enable AJAX check for file overwrite, pre-upload
+ */
+$wgAjaxUploadDestCheck = true;
+
+/**
+ * Enable previewing licences via AJAX
+ */
+$wgAjaxLicensePreview = true;
/**
* Allow DISPLAYTITLE to change title display
*/
-$wgAllowDisplayTitle = false ;
+$wgAllowDisplayTitle = true;
/**
* Array of usernames which may not be registered or logged in from
@@ -2491,13 +2685,28 @@ $wgDjvuPostProcessor = 'pnmtojpeg';
$wgDjvuOutputExtension = 'jpg';
/**
-* Enable direct access to the data API
-* through api.php
-*/
+ * Enable the MediaWiki API for convenient access to
+ * machine-readable data via api.php
+ *
+ * See http://www.mediawiki.org/wiki/API
+ */
$wgEnableAPI = true;
+
+/**
+ * Allow the API to be used to perform write operations
+ * (page edits, rollback, etc.) when an authorised user
+ * accesses it
+ */
$wgEnableWriteAPI = false;
/**
+ * API module extensions
+ * Associative array mapping module name to class name.
+ * Extension modules may override the core modules.
+ */
+$wgAPIModules = array();
+
+/**
* Parser test suite files to be run by parserTests.php when no specific
* filename is passed to it.
*
@@ -2532,4 +2741,10 @@ $wgEnableCascadingProtection = true;
*/
$wgDisableOutputCompression = false;
-?>
+/**
+ * If lag is higher than $wgSlaveLagWarning, show a warning in some special
+ * pages (like watchlist). If the lag is higher than $wgSlaveLagCritical,
+ * show a more obvious warning.
+ */
+$wgSlaveLagWarning = 10;
+$wgSlaveLagCritical = 30;
diff --git a/includes/Defines.php b/includes/Defines.php
index 98e76277..c923c256 100644
--- a/includes/Defines.php
+++ b/includes/Defines.php
@@ -205,5 +205,61 @@ define( 'LIST_SET', 2 );
define( 'LIST_NAMES', 3);
define( 'LIST_OR', 4);
+/**
+ * Unicode and normalisation related
+ */
+define( 'UNICODE_HANGUL_FIRST', 0xac00 );
+define( 'UNICODE_HANGUL_LAST', 0xd7a3 );
+
+define( 'UNICODE_HANGUL_LBASE', 0x1100 );
+define( 'UNICODE_HANGUL_VBASE', 0x1161 );
+define( 'UNICODE_HANGUL_TBASE', 0x11a7 );
+
+define( 'UNICODE_HANGUL_LCOUNT', 19 );
+define( 'UNICODE_HANGUL_VCOUNT', 21 );
+define( 'UNICODE_HANGUL_TCOUNT', 28 );
+define( 'UNICODE_HANGUL_NCOUNT', UNICODE_HANGUL_VCOUNT * UNICODE_HANGUL_TCOUNT );
+
+define( 'UNICODE_HANGUL_LEND', UNICODE_HANGUL_LBASE + UNICODE_HANGUL_LCOUNT - 1 );
+define( 'UNICODE_HANGUL_VEND', UNICODE_HANGUL_VBASE + UNICODE_HANGUL_VCOUNT - 1 );
+define( 'UNICODE_HANGUL_TEND', UNICODE_HANGUL_TBASE + UNICODE_HANGUL_TCOUNT - 1 );
+
+define( 'UNICODE_SURROGATE_FIRST', 0xd800 );
+define( 'UNICODE_SURROGATE_LAST', 0xdfff );
+define( 'UNICODE_MAX', 0x10ffff );
+define( 'UNICODE_REPLACEMENT', 0xfffd );
+
+
+define( 'UTF8_HANGUL_FIRST', "\xea\xb0\x80" /*codepointToUtf8( UNICODE_HANGUL_FIRST )*/ );
+define( 'UTF8_HANGUL_LAST', "\xed\x9e\xa3" /*codepointToUtf8( UNICODE_HANGUL_LAST )*/ );
+
+define( 'UTF8_HANGUL_LBASE', "\xe1\x84\x80" /*codepointToUtf8( UNICODE_HANGUL_LBASE )*/ );
+define( 'UTF8_HANGUL_VBASE', "\xe1\x85\xa1" /*codepointToUtf8( UNICODE_HANGUL_VBASE )*/ );
+define( 'UTF8_HANGUL_TBASE', "\xe1\x86\xa7" /*codepointToUtf8( UNICODE_HANGUL_TBASE )*/ );
+
+define( 'UTF8_HANGUL_LEND', "\xe1\x84\x92" /*codepointToUtf8( UNICODE_HANGUL_LEND )*/ );
+define( 'UTF8_HANGUL_VEND', "\xe1\x85\xb5" /*codepointToUtf8( UNICODE_HANGUL_VEND )*/ );
+define( 'UTF8_HANGUL_TEND', "\xe1\x87\x82" /*codepointToUtf8( UNICODE_HANGUL_TEND )*/ );
+
+define( 'UTF8_SURROGATE_FIRST', "\xed\xa0\x80" /*codepointToUtf8( UNICODE_SURROGATE_FIRST )*/ );
+define( 'UTF8_SURROGATE_LAST', "\xed\xbf\xbf" /*codepointToUtf8( UNICODE_SURROGATE_LAST )*/ );
+define( 'UTF8_MAX', "\xf4\x8f\xbf\xbf" /*codepointToUtf8( UNICODE_MAX )*/ );
+define( 'UTF8_REPLACEMENT', "\xef\xbf\xbd" /*codepointToUtf8( UNICODE_REPLACEMENT )*/ );
+#define( 'UTF8_REPLACEMENT', '!' );
+
+define( 'UTF8_OVERLONG_A', "\xc1\xbf" );
+define( 'UTF8_OVERLONG_B', "\xe0\x9f\xbf" );
+define( 'UTF8_OVERLONG_C', "\xf0\x8f\xbf\xbf" );
+
+# These two ranges are illegal
+define( 'UTF8_FDD0', "\xef\xb7\x90" /*codepointToUtf8( 0xfdd0 )*/ );
+define( 'UTF8_FDEF', "\xef\xb7\xaf" /*codepointToUtf8( 0xfdef )*/ );
+define( 'UTF8_FFFE', "\xef\xbf\xbe" /*codepointToUtf8( 0xfffe )*/ );
+define( 'UTF8_FFFF', "\xef\xbf\xbf" /*codepointToUtf8( 0xffff )*/ );
+
+define( 'UTF8_HEAD', false );
+define( 'UTF8_TAIL', true );
+
+
+
-?>
diff --git a/includes/DifferenceEngine.php b/includes/DifferenceEngine.php
index af65ce3a..99bb4798 100644
--- a/includes/DifferenceEngine.php
+++ b/includes/DifferenceEngine.php
@@ -6,6 +6,14 @@
*/
/**
+ * Constant to indicate diff cache compatibility.
+ * Bump this when changing the diff formatting in a way that
+ * fixes important bugs or such to force cached diff views to
+ * clear.
+ */
+define( 'MW_DIFF_VERSION', '1.11a' );
+
+/**
* @todo document
* @public
* @addtogroup DifferenceEngine
@@ -148,8 +156,42 @@ CONTROL;
} else {
$rollback = '';
}
- if( $wgUseRCPatrol && $this->mRcidMarkPatrolled != 0 && $wgUser->isAllowed( 'patrol' ) ) {
- $patrol = ' [' . $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$this->mRcidMarkPatrolled}" ) . ']';
+
+ // Prepare a change patrol link, if applicable
+ if( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) {
+ // If we've been given an explicit change identifier, use it; saves time
+ if( $this->mRcidMarkPatrolled ) {
+ $rcid = $this->mRcidMarkPatrolled;
+ } else {
+ // Look for an unpatrolled change corresponding to this diff
+ $change = RecentChange::newFromConds(
+ array(
+ // Add redundant timestamp condition so we can use the
+ // existing index
+ 'rc_timestamp' => $this->mNewRev->getTimestamp(),
+ 'rc_this_oldid' => $this->mNewid,
+ 'rc_last_oldid' => $this->mOldid,
+ 'rc_patrolled' => 0,
+ ),
+ __METHOD__
+ );
+ if( $change instanceof RecentChange ) {
+ $rcid = $change->mAttribs['rc_id'];
+ } else {
+ // None found
+ $rcid = 0;
+ }
+ }
+ // Build the link
+ if( $rcid ) {
+ $patrol = ' [' . $sk->makeKnownLinkObj(
+ $this->mTitle,
+ wfMsgHtml( 'markaspatrolleddiff' ),
+ "action=markpatrolled&rcid={$rcid}"
+ ) . ']';
+ } else {
+ $patrol = '';
+ }
} else {
$patrol = '';
}
@@ -176,14 +218,14 @@ CONTROL;
wfMsg( 'minoreditletter') ) . ' ';
}
- $oldHeader = "<strong>{$this->mOldtitle}</strong><br />" .
- $sk->revUserTools( $this->mOldRev ) . "<br />" .
- $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly ) . "<br />" .
- $prevlink;
- $newHeader = "<strong>{$this->mNewtitle}</strong><br />" .
- $sk->revUserTools( $this->mNewRev ) . " $rollback<br />" .
- $newminor . $sk->revComment( $this->mNewRev, !$diffOnly ) . "<br />" .
- $nextlink . $patrol;
+ $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $this->mOldtitle . '</strong></div>' .
+ '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev ) . "</div>" .
+ '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly ) . "</div>" .
+ '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
+ $newHeader = '<div id="mw-diff-ntitle1"><strong>' .$this->mNewtitle . '</strong></div>' .
+ '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev ) . " $rollback</div>" .
+ '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly ) . "</div>" .
+ '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
$this->showDiff( $oldHeader, $newHeader );
@@ -282,24 +324,39 @@ CONTROL;
* Returns false if the diff could not be generated, otherwise returns true
*/
function showDiff( $otitle, $ntitle ) {
- global $wgOut;
- $diff = $this->getDiff( $otitle, $ntitle );
+ global $wgOut, $wgRequest;
+ $diff = $this->getDiff( $otitle, $ntitle, $wgRequest->getVal( 'action' ) == 'purge' );
if ( $diff === false ) {
$wgOut->addWikitext( wfMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" ) );
return false;
} else {
+ $this->showDiffStyle();
$wgOut->addHTML( $diff );
return true;
}
}
+
+ /**
+ * Add style sheets and supporting JS for diff display.
+ */
+ function showDiffStyle() {
+ global $wgStylePath, $wgStyleVersion, $wgOut;
+ $wgOut->addStyle( 'common/diff.css' );
+
+ // JS is needed to detect old versions of Mozilla to work around an annoyance bug.
+ $wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" );
+ }
/**
- * Get diff table, including header
- * Note that the interface has changed, it's no longer static.
- * Returns false on error
+ * Get complete diff table, including header
+ *
+ * @param Title $otitle Old title
+ * @param Title $ntitle New title
+ * @param bool $skipCache Skip the diff cache for this request?
+ * @return mixed
*/
- function getDiff( $otitle, $ntitle ) {
- $body = $this->getDiffBody();
+ function getDiff( $otitle, $ntitle, $skipCache = false ) {
+ $body = $this->getDiffBody( $skipCache );
if ( $body === false ) {
return false;
} else {
@@ -310,19 +367,20 @@ CONTROL;
/**
* Get the diff table body, without header
- * Results are cached
- * Returns false on error
+ *
+ * @param bool $skipCache Skip cache for this request?
+ * @return mixed
*/
- function getDiffBody() {
+ function getDiffBody( $skipCache = false ) {
global $wgMemc;
$fname = 'DifferenceEngine::getDiffBody';
wfProfileIn( $fname );
// Cacheable?
$key = false;
- if ( $this->mOldid && $this->mNewid ) {
+ if ( $this->mOldid && $this->mNewid && !$skipCache ) {
// Try cache
- $key = wfMemcKey( 'diff', 'oldid', $this->mOldid, 'newid', $this->mNewid );
+ $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
$difftext = $wgMemc->get( $key );
if ( $difftext ) {
wfIncrStats( 'diff_cache_hit' );
@@ -488,10 +546,14 @@ CONTROL;
$ntitle = '<span class="history-deleted">'.$ntitle.'</span>';
}
$header = "
- <table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>
+ <table class='diff'>
+ <col class='diff-marker' />
+ <col class='diff-content' />
+ <col class='diff-marker' />
+ <col class='diff-content' />
<tr>
- <td colspan='2' width='50%' align='center' class='diff-otitle'>{$otitle}</td>
- <td colspan='2' width='50%' align='center' class='diff-ntitle'>{$ntitle}</td>
+ <td colspan='2' class='diff-otitle'>{$otitle}</td>
+ <td colspan='2' class='diff-ntitle'>{$ntitle}</td>
</tr>
";
@@ -530,16 +592,15 @@ CONTROL;
}
// Load the new revision object
- if( $this->mNewid ) {
- $this->mNewRev = Revision::newFromId( $this->mNewid );
- } else {
- $this->mNewRev = Revision::newFromTitle( $this->mTitle );
- }
-
- if( is_null( $this->mNewRev ) ) {
+ $this->mNewRev = $this->mNewid
+ ? Revision::newFromId( $this->mNewid )
+ : Revision::newFromTitle( $this->mTitle );
+ if( !$this->mNewRev instanceof Revision )
return false;
- }
-
+
+ // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
+ $this->mNewid = $this->mNewRev->getId();
+
// Set assorted variables
$timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
$this->mNewPage = $this->mNewRev->getTitle();
@@ -588,7 +649,8 @@ CONTROL;
$oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid );
$this->mOldtitle = "<a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) )
. "</a> (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
- //now that we considered old rev, we can make undo link (bug 8133, multi-edit undo)
+
+ // Add an "undo" link
$newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
$this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)";
}
@@ -1721,8 +1783,8 @@ class TableDiffFormatter extends DiffFormatter
}
function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
- $r = '<tr><td colspan="2" align="left"><strong><!--LINE '.$xbeg."--></strong></td>\n" .
- '<td colspan="2" align="left"><strong><!--LINE '.$ybeg."--></strong></td></tr>\n";
+ $r = '<tr><td colspan="2" class="diff-lineno"><!--LINE '.$xbeg."--></td>\n" .
+ '<td colspan="2" class="diff-lineno"><!--LINE '.$ybeg."--></td></tr>\n";
return $r;
}
@@ -1738,17 +1800,25 @@ class TableDiffFormatter extends DiffFormatter
# HTML-escape parameter before calling this
function addedLine( $line ) {
- return "<td>+</td><td class='diff-addedline'>{$line}</td>";
+ return $this->wrapLine( '+', 'diff-addedline', $line );
}
# HTML-escape parameter before calling this
function deletedLine( $line ) {
- return "<td>-</td><td class='diff-deletedline'>{$line}</td>";
+ return $this->wrapLine( '-', 'diff-deletedline', $line );
}
# HTML-escape parameter before calling this
function contextLine( $line ) {
- return "<td> </td><td class='diff-context'>{$line}</td>";
+ return $this->wrapLine( ' ', 'diff-context', $line );
+ }
+
+ private function wrapLine( $marker, $class, $line ) {
+ if( $line !== '' ) {
+ // The <div> wrapper is needed for 'overflow: auto' style to scroll properly
+ $line = "<div>$line</div>";
+ }
+ return "<td class='diff-marker'>$marker</td><td class='$class'>$line</td>";
}
function emptyLine() {
@@ -1801,4 +1871,5 @@ class TableDiffFormatter extends DiffFormatter
}
}
-?>
+
+
diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php
index 1e423565..b48aaffd 100644
--- a/includes/DjVuImage.php
+++ b/includes/DjVuImage.php
@@ -104,7 +104,9 @@ class DjVuImage {
}
function getInfo() {
+ wfSuppressWarnings();
$file = fopen( $this->mFilename, 'rb' );
+ wfRestoreWarnings();
if( $file === false ) {
wfDebug( __METHOD__ . ": missing or failed file read\n" );
return false;
diff --git a/includes/EditPage.php b/includes/EditPage.php
index bec6e300..cceb053d 100644
--- a/includes/EditPage.php
+++ b/includes/EditPage.php
@@ -20,6 +20,7 @@ class EditPage {
var $firsttime;
var $lastDelete;
var $mTokenOk = false;
+ var $mTokenOkExceptSuffix = false;
var $mTriedSave = false;
var $tooBig = false;
var $kblength = false;
@@ -97,6 +98,11 @@ class EditPage {
$text = $this->mArticle->getContent();
+ if ($undo > 0 && $undoafter > 0 && $undo < $undoafter) {
+ # If they got undoafter and undo round the wrong way, switch them
+ list( $undo, $undoafter ) = array( $undoafter, $undo );
+ }
+
if ( $undo > 0 && $undo > $undoafter ) {
# Undoing a specific edit overrides section editing; section-editing
# doesn't work with undoing.
@@ -292,7 +298,6 @@ class EditPage {
*/
function edit() {
global $wgOut, $wgUser, $wgRequest, $wgTitle;
- global $wgEmailConfirmToEdit;
if ( ! wfRunHooks( 'AlternateEdit', array( &$this ) ) )
return;
@@ -313,57 +318,38 @@ class EditPage {
return;
}
- if ( ! $this->mTitle->userCan( 'edit' ) ) {
- wfDebug( "$fname: user can't edit\n" );
- $wgOut->readOnlyPage( $this->getContent(), true );
- wfProfileOut( $fname );
- return;
- }
- wfDebug( "$fname: Checking blocks\n" );
- if ( !$this->preview && !$this->diff && $wgUser->isBlockedFrom( $this->mTitle, !$this->save ) ) {
- # When previewing, don't check blocked state - will get caught at save time.
- # Also, check when starting edition is done against slave to improve performance.
- wfDebug( "$fname: user is blocked\n" );
- $this->blockedPage();
- wfProfileOut( $fname );
- return;
- }
- if ( !$wgUser->isAllowed('edit') ) {
- if ( $wgUser->isAnon() ) {
- wfDebug( "$fname: user must log in\n" );
- $this->userNotLoggedInPage();
- wfProfileOut( $fname );
- return;
- } else {
- wfDebug( "$fname: read-only page\n" );
- $wgOut->readOnlyPage( $this->getContent(), true );
- wfProfileOut( $fname );
- return;
+ $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser);
+ if( !$this->mTitle->exists() )
+ $permErrors += $this->mTitle->getUserPermissionsErrors( 'create', $wgUser);
+
+ # Ignore some permissions errors.
+ $remove = array();
+ foreach( $permErrors as $error ) {
+ if ($this->preview || $this->diff &&
+ ($error[0] == 'blockedtext' || $error[0] == 'autoblockedtext'))
+ {
+ // Don't worry about blocks when previewing/diffing
+ $remove[] = $error;
+ }
+
+ if ($error[0] == 'readonlytext')
+ {
+ if ($this->edit) {
+ $this->formtype = 'preview';
+ } elseif ($this->save || $this->preview || $this->diff) {
+ $remove[] = $error;
+ }
}
}
- if ($wgEmailConfirmToEdit && !$wgUser->isEmailConfirmed()) {
- wfDebug("$fname: user must confirm e-mail address\n");
- $this->userNotConfirmedPage();
- wfProfileOut($fname);
- return;
- }
- if ( !$this->mTitle->userCan( 'create' ) && !$this->mTitle->exists() ) {
- wfDebug( "$fname: no create permission\n" );
- $this->noCreatePermission();
+ # array_diff returns elements in $permErrors that are not in $remove.
+ $permErrors = array_diff( $permErrors, $remove );
+
+ if ( !empty($permErrors) )
+ {
+ wfDebug( "$fname: User can't edit\n" );
+ $wgOut->readOnlyPage( $this->getContent(), true, $permErrors );
wfProfileOut( $fname );
return;
- }
- if ( wfReadOnly() ) {
- wfDebug( "$fname: read-only mode is engaged\n" );
- if( $this->save || $this->preview ) {
- $this->formtype = 'preview';
- } else if ( $this->diff ) {
- $this->formtype = 'diff';
- } else {
- $wgOut->readOnlyPage( $this->getContent() );
- wfProfileOut( $fname );
- return;
- }
} else {
if ( $this->save ) {
$this->formtype = 'save';
@@ -410,9 +396,10 @@ class EditPage {
}
}
- if(!$this->mTitle->getArticleID() && ('initial' == $this->formtype || $this->firsttime )) { # new article
+ # Show applicable editing introductions
+ if( $this->formtype == 'initial' || $this->firsttime )
$this->showIntro();
- }
+
if( $this->mTitle->isTalkPage() ) {
$wgOut->addWikiText( wfMsg( 'talkpagetext' ) );
}
@@ -449,17 +436,30 @@ class EditPage {
}
/**
- * Return true if this page should be previewed when the edit form
- * is initially opened.
+ * Should we show a preview when the edit form is first shown?
+ *
* @return bool
- * @private
*/
- function previewOnOpen() {
- global $wgUser;
- return $this->section != 'new' &&
- ( ( $wgUser->getOption( 'previewonfirst' ) && $this->mTitle->exists() ) ||
- ( $this->mTitle->getNamespace() == NS_CATEGORY &&
- !$this->mTitle->exists() ) );
+ private function previewOnOpen() {
+ global $wgRequest, $wgUser;
+ if( $wgRequest->getVal( 'preview' ) == 'yes' ) {
+ // Explicit override from request
+ return true;
+ } elseif( $wgRequest->getVal( 'preview' ) == 'no' ) {
+ // Explicit override from request
+ return false;
+ } elseif( $this->section == 'new' ) {
+ // Nothing *to* preview for new sections
+ return false;
+ } elseif( ( $wgRequest->getVal( 'preload' ) !== '' || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) {
+ // Standard preference behaviour
+ return true;
+ } elseif( !$this->mTitle->exists() && $this->mTitle->getNamespace() == NS_CATEGORY ) {
+ // Categories are special
+ return true;
+ } else {
+ return false;
+ }
}
/**
@@ -547,9 +547,10 @@ class EditPage {
$this->summary = '';
$this->edittime = '';
$this->starttime = wfTimestampNow();
+ $this->edit = false;
$this->preview = false;
$this->save = false;
- $this->diff = false;
+ $this->diff = false;
$this->minoredit = false;
$this->watchthis = false;
$this->recreate = false;
@@ -575,35 +576,45 @@ class EditPage {
*/
function tokenOk( &$request ) {
global $wgUser;
- if( $wgUser->isAnon() ) {
- # Anonymous users may not have a session
- # open. Check for suffix anyway.
- $this->mTokenOk = ( EDIT_TOKEN_SUFFIX == $request->getVal( 'wpEditToken' ) );
- } else {
- $this->mTokenOk = $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
- }
+ $token = $request->getVal( 'wpEditToken' );
+ $this->mTokenOk = $wgUser->matchEditToken( $token );
+ $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
return $this->mTokenOk;
}
- /** */
- function showIntro() {
+ /**
+ * Show all applicable editing introductions
+ */
+ private function showIntro() {
global $wgOut, $wgUser;
- $addstandardintro=true;
- if($this->editintro) {
- $introtitle=Title::newFromText($this->editintro);
- if(isset($introtitle) && $introtitle->userCanRead()) {
- $rev=Revision::newFromTitle($introtitle);
- if($rev) {
- $wgOut->addSecondaryWikiText($rev->getText());
- $addstandardintro=false;
- }
- }
- }
- if($addstandardintro) {
- if ( $wgUser->isLoggedIn() )
+ if( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
+ if( $wgUser->isLoggedIn() ) {
$wgOut->addWikiText( wfMsg( 'newarticletext' ) );
- else
+ } else {
$wgOut->addWikiText( wfMsg( 'newarticletextanon' ) );
+ }
+ $this->showDeletionLog( $wgOut );
+ }
+ }
+
+ /**
+ * Attempt to show a custom editing introduction, if supplied
+ *
+ * @return bool
+ */
+ private function showCustomIntro() {
+ if( $this->editintro ) {
+ $title = Title::newFromText( $this->editintro );
+ if( $title instanceof Title && $title->exists() && $title->userCanRead() ) {
+ global $wgOut;
+ $revision = Revision::newFromTitle( $title );
+ $wgOut->addSecondaryWikiText( $revision->getText() );
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
}
}
@@ -762,7 +773,7 @@ class EditPage {
if ( $this->isConflict) {
wfDebug( "EditPage::editForm conflict! getting section '$this->section' for time '$this->edittime' (article time '" .
- $this->mArticle->getTimestamp() . "'\n" );
+ $this->mArticle->getTimestamp() . "')\n" );
$text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime);
}
else {
@@ -777,7 +788,7 @@ class EditPage {
# Suppress edit conflict with self, except for section edits where merging is required.
if ( ( $this->section == '' ) && ( 0 != $userid ) && ( $this->mArticle->getUser() == $userid ) ) {
- wfDebug( "Suppressing edit conflict, same user.\n" );
+ wfDebug( "EditPage::editForm Suppressing edit conflict, same user.\n" );
$this->isConflict = false;
} else {
# switch from section editing to normal editing in edit conflict
@@ -786,11 +797,11 @@ class EditPage {
if( $this->mergeChangesInto( $text ) ){
// Successful merge! Maybe we should tell the user the good news?
$this->isConflict = false;
- wfDebug( "Suppressing edit conflict, successful merge.\n" );
+ wfDebug( "EditPage::editForm Suppressing edit conflict, successful merge.\n" );
} else {
$this->section = '';
$this->textbox1 = $text;
- wfDebug( "Keeping edit conflict, failed merge.\n" );
+ wfDebug( "EditPage::editForm Keeping edit conflict, failed merge.\n" );
}
}
}
@@ -831,6 +842,10 @@ class EditPage {
}
if( $this->summary != '' ) {
$sectionanchor = $this->sectionAnchor( $this->summary );
+ # This is a new section, so create a link to the new section
+ # in the revision summary.
+ $this->summary = wfMsgForContent('newsectionsummary') .
+ " [[{$this->mTitle->getPrefixedText()}#{$this->summary}|{$this->summary}]]";
}
} elseif( $this->section != '' ) {
# Try to get a section anchor from the section source, redirect to edited section if header found
@@ -909,6 +924,10 @@ class EditPage {
# Enabled article-related sidebar, toplinks, etc.
$wgOut->setArticleRelated( true );
+ if ( $this->formtype == 'preview' ) {
+ $wgOut->setPageTitleActionText( wfMsg( 'preview' ) );
+ }
+
if ( $this->isConflict ) {
$s = wfMsg( 'editconflict', $this->mTitle->getPrefixedText() );
$wgOut->setPageTitle( $s );
@@ -1002,13 +1021,14 @@ class EditPage {
}
if ( $this->mTitle->isCascadeProtected() ) {
# Is this page under cascading protection from some source pages?
- list($cascadeSources, $restrictions) = $this->mTitle->getCascadeProtectionSources();
+ list($cascadeSources, /* $restrictions */) = $this->mTitle->getCascadeProtectionSources();
if ( count($cascadeSources) > 0 ) {
# Explain, and list the titles responsible
$notice = wfMsgExt( 'cascadeprotectedwarning', array('parsemag'), count($cascadeSources) ) . "\n";
- foreach( $cascadeSources as $id => $page )
+ foreach( $cascadeSources as $page ) {
$notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
}
+ }
$wgOut->addWikiText( $notice );
}
@@ -1089,7 +1109,7 @@ class EditPage {
}
if ( 'diff' == $this->formtype ) {
- $wgOut->addHTML( $this->getDiff() );
+ $this->showDiff();
}
}
@@ -1115,7 +1135,7 @@ class EditPage {
if( !$this->preview && !$this->diff ) {
$wgOut->setOnloadHandler( 'document.editform.wpTextbox1.focus()' );
}
- $templates = ($this->preview || $this->section) ? $this->mPreviewTemplates : $this->mArticle->getUsedTemplates();
+ $templates = ($this->preview || $this->section != '') ? $this->mPreviewTemplates : $this->mArticle->getUsedTemplates();
$formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != '');
global $wgUseMetadataEdit ;
@@ -1229,10 +1249,7 @@ END
* include the constant suffix to prevent editing from
* broken text-mangling proxies.
*/
- if ( $wgUser->isLoggedIn() )
- $token = htmlspecialchars( $wgUser->editToken() );
- else
- $token = EDIT_TOKEN_SUFFIX;
+ $token = htmlspecialchars( $wgUser->editToken() );
$wgOut->addHTML( "\n<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" );
@@ -1271,7 +1288,7 @@ END
}
if ( $this->formtype == 'diff') {
- $wgOut->addHTML( $this->getDiff() );
+ $this->showDiff();
}
}
@@ -1292,6 +1309,7 @@ END
if($this->mTitle->getNamespace() == NS_CATEGORY) {
$this->mArticle->openShowCategory();
}
+ wfRunHooks( 'OutputPageBeforeHTML',array( &$wgOut, &$text ) );
$wgOut->addHTML( $text );
if($this->mTitle->getNamespace() == NS_CATEGORY) {
$this->mArticle->closeShowCategory();
@@ -1363,7 +1381,11 @@ END
wfProfileIn( $fname );
if ( $this->mTriedSave && !$this->mTokenOk ) {
- $msg = 'session_fail_preview';
+ if ( $this->mTokenOkExceptSuffix ) {
+ $msg = 'token_suffix_mismatch';
+ } else {
+ $msg = 'session_fail_preview';
+ }
} else {
$msg = 'previewnote';
}
@@ -1414,6 +1436,9 @@ END
$previewHTML = $parserOutput->getText();
$wgOut->addParserOutputNoText( $parserOutput );
+
+ # ParserOutput might have altered the page title, so reset it
+ $wgOut->setPageTitle( wfMsg( 'editing', $this->mTitle->getPrefixedText() ) );
foreach ( $parserOutput->getTemplates() as $ns => $template)
foreach ( array_keys( $template ) as $dbk)
@@ -1497,7 +1522,7 @@ END
$wgOut->setArticleRelated( false );
$wgOut->addWikiText( wfMsg( 'nosuchsectiontext', $this->section ) );
- $wgOut->returnToMain( false );
+ $wgOut->returnToMain( false, $this->mTitle->getPrefixedUrl() );
}
/**
@@ -1893,10 +1918,8 @@ END
*
* If this is a section edit, we'll replace the section as for final
* save and then make a comparison.
- *
- * @return string HTML
*/
- function getDiff() {
+ function showDiff() {
$oldtext = $this->mArticle->fetchContent();
$newtext = $this->mArticle->replaceSection(
$this->section, $this->textbox1, $this->summary, $this->edittime );
@@ -1907,11 +1930,13 @@ END
$de = new DifferenceEngine( $this->mTitle );
$de->setText( $oldtext, $newtext );
$difftext = $de->getDiff( $oldtitle, $newtitle );
+ $de->showDiffStyle();
} else {
$difftext = '';
}
- return '<div id="wikiDiff">' . $difftext . '</div>';
+ global $wgOut;
+ $wgOut->addHtml( '<div id="wikiDiff">' . $difftext . '</div>' );
}
/**
@@ -2034,7 +2059,32 @@ END
$wgOut->setPageTitle( wfMsg( 'nocreatetitle' ) );
$wgOut->addWikiText( wfMsg( 'nocreatetext' ) );
}
-
+
+ /**
+ * If there are rows in the deletion log for this page, show them,
+ * along with a nice little note for the user
+ *
+ * @param OutputPage $out
+ */
+ private function showDeletionLog( $out ) {
+ $title = $this->mArticle->getTitle();
+ $reader = new LogReader(
+ new FauxRequest(
+ array(
+ 'page' => $title->getPrefixedText(),
+ 'type' => 'delete',
+ )
+ )
+ );
+ if( $reader->hasRows() ) {
+ $out->addHtml( '<div id="mw-recreate-deleted-warn">' );
+ $out->addWikiText( wfMsg( 'recreate-deleted-warn' ) );
+ $viewer = new LogViewer( $reader );
+ $viewer->showList( $out );
+ $out->addHtml( '</div>' );
+ }
+ }
+
}
-?>
+
diff --git a/includes/EmaillingJob.php b/includes/EmaillingJob.php
new file mode 100644
index 00000000..73d71c56
--- /dev/null
+++ b/includes/EmaillingJob.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * Old job used for sending single notification emails;
+ * kept for backwards-compatibility
+ */
+class EmaillingJob extends Job {
+
+ function __construct( $title, $params, $id = 0 ) {
+ parent::__construct( 'sendMail', Title::newMainPage(), $params, $id );
+ }
+
+ function run() {
+ userMailer(
+ $this->params['to'],
+ $this->params['from'],
+ $this->params['subj'],
+ $this->params['body'],
+ $this->params['replyto']
+ );
+ return true;
+ }
+
+}
+
diff --git a/includes/EnotifNotifyJob.php b/includes/EnotifNotifyJob.php
new file mode 100644
index 00000000..70d1de69
--- /dev/null
+++ b/includes/EnotifNotifyJob.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * Job for email notification mails
+ */
+class EnotifNotifyJob extends Job {
+
+ function __construct( $title, $params, $id = 0 ) {
+ parent::__construct( 'enotifNotify', $title, $params, $id );
+ }
+
+ function run() {
+ $enotif = new EmailNotification();
+ $enotif->actuallyNotifyOnPageChange(
+ User::newFromName( $this->params['editor'], false ),
+ $this->title,
+ $this->params['timestamp'],
+ $this->params['summary'],
+ $this->params['minorEdit'],
+ $this->params['oldid']
+ );
+ return true;
+ }
+
+}
+
diff --git a/includes/Exception.php b/includes/Exception.php
index 4cf0b7ba..02819cc9 100644
--- a/includes/Exception.php
+++ b/includes/Exception.php
@@ -235,4 +235,4 @@ function wfExceptionHandler( $e ) {
exit( 1 );
}
-?>
+
diff --git a/includes/Exif.php b/includes/Exif.php
index 3a06ca1b..d98a8e0d 100644
--- a/includes/Exif.php
+++ b/includes/Exif.php
@@ -405,7 +405,7 @@ class Exif {
*
* @return int
*/
- function version() {
+ public static function version() {
return 1; // We don't need no bloddy constants!
}
@@ -1131,4 +1131,4 @@ define( 'MW_EXIF_UNDEFINED', Exif::UNDEFINED );
define( 'MW_EXIF_SLONG', Exif::SLONG );
define( 'MW_EXIF_SRATIONAL', Exif::SRATIONAL );
-?>
+
diff --git a/includes/Export.php b/includes/Export.php
index 9307795d..c3ef9451 100644
--- a/includes/Export.php
+++ b/includes/Export.php
@@ -558,7 +558,7 @@ class Dump7ZipOutput extends DumpPipeOutput {
$command = "7za a -bd -si " . wfEscapeShellArg( $file );
// Suppress annoying useless crap from p7zip
// Unfortunately this could suppress real error messages too
- $command .= " >/dev/null 2>&1";
+ $command .= ' >' . wfGetNull() . ' 2>&1';
parent::DumpPipeOutput( $command );
}
}
@@ -767,4 +767,4 @@ function xmlsafe( $string ) {
return $string;
}
-?>
+
diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php
index c8ed8bde..f3fc22e3 100644
--- a/includes/ExternalEdit.php
+++ b/includes/ExternalEdit.php
@@ -46,7 +46,7 @@ class ExternalEdit {
$extension="wiki";
} elseif($this->mMode=="file") {
$type="Edit file";
- $image = new Image( $this->mTitle );
+ $image = wfLocalFile( $this->mTitle );
$img_url = $image->getURL();
if(strpos($img_url,"://")) {
$url = $img_url;
@@ -72,4 +72,4 @@ CONTROL;
echo $control;
}
}
-?>
+
diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php
index fb66b652..5efc6e25 100644
--- a/includes/ExternalStore.php
+++ b/includes/ExternalStore.php
@@ -41,10 +41,9 @@ class ExternalStore {
return false;
$class='ExternalStore'.ucfirst($proto);
- /* Preloaded modules might exist, especially ones serving multiple protocols */
+ /* Any custom modules should be added to $wgAutoLoadClasses for on-demand loading */
if (!class_exists($class)) {
- if (!include_once($class.'.php'))
- return false;
+ return false;
}
$store=new $class();
return $store;
@@ -66,4 +65,4 @@ class ExternalStore {
}
}
}
-?>
+
diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php
index 7b4ffc2f..f9046f74 100644
--- a/includes/ExternalStoreDB.php
+++ b/includes/ExternalStoreDB.php
@@ -144,4 +144,4 @@ class ExternalStoreDB {
return "DB://$cluster/$id";
}
}
-?>
+
diff --git a/includes/ExternalStoreHttp.php b/includes/ExternalStoreHttp.php
index e6656986..cff6c4d4 100644
--- a/includes/ExternalStoreHttp.php
+++ b/includes/ExternalStoreHttp.php
@@ -19,4 +19,4 @@ class ExternalStoreHttp {
* whatever, for initial ext storage
*/
}
-?>
+
diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php
index 293bdaf0..b63ae505 100644
--- a/includes/FakeTitle.php
+++ b/includes/FakeTitle.php
@@ -84,4 +84,4 @@ class FakeTitle {
function trackbackRDF() { $this->error(); }
}
-?>
+
diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php
new file mode 100644
index 00000000..ee165cd1
--- /dev/null
+++ b/includes/FileDeleteForm.php
@@ -0,0 +1,220 @@
+<?php
+
+/**
+ * File deletion user interface
+ *
+ * @addtogroup Media
+ * @author Rob Church <robchur@gmail.com>
+ */
+class FileDeleteForm {
+
+ private $title = null;
+ private $file = null;
+
+ private $oldfile = null;
+ private $oldimage = '';
+
+ /**
+ * Constructor
+ *
+ * @param File $file File we're deleting
+ */
+ public function __construct( $file ) {
+ $this->title = $file->getTitle();
+ $this->file = $file;
+ }
+
+ /**
+ * Fulfil the request; shows the form or deletes the file,
+ * pending authentication, confirmation, etc.
+ */
+ public function execute() {
+ global $wgOut, $wgRequest, $wgUser;
+ $this->setHeaders();
+
+ if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ } elseif( !$wgUser->isLoggedIn() ) {
+ $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
+ return;
+ } elseif( !$wgUser->isAllowed( 'delete' ) ) {
+ $wgOut->permissionError( 'delete' );
+ return;
+ } elseif( $wgUser->isBlocked() ) {
+ $wgOut->blockedPage();
+ return;
+ }
+
+ $this->oldimage = $wgRequest->getText( 'oldimage', false );
+ $token = $wgRequest->getText( 'wpEditToken' );
+ if( $this->oldimage && !$this->isValidOldSpec() ) {
+ $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->oldimage ) );
+ return;
+ }
+ if( $this->oldimage )
+ $this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->oldimage );
+
+ if( !$this->haveDeletableFile() ) {
+ $wgOut->addHtml( $this->prepareMessage( 'filedelete-nofile' ) );
+ $wgOut->addReturnTo( $this->title );
+ return;
+ }
+
+ // Perform the deletion if appropriate
+ if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) {
+ $comment = $wgRequest->getText( 'wpReason' );
+ if( $this->oldimage ) {
+ $status = $this->file->deleteOld( $this->oldimage, $comment );
+ if( $status->ok ) {
+ // Need to do a log item
+ $log = new LogPage( 'delete' );
+ $log->addEntry( 'delete', $this->title, wfMsg( 'deletedrevision' , $this->oldimage ) );
+ }
+ } else {
+ $status = $this->file->delete( $comment );
+ if( $status->ok ) {
+ // Need to delete the associated article
+ $article = new Article( $this->title );
+ $article->doDeleteArticle( $comment );
+ }
+ }
+ if( !$status->isGood() )
+ $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) );
+ if( $status->ok ) {
+ $wgOut->addHtml( $this->prepareMessage( 'filedelete-success' ) );
+ // Return to the main page if we just deleted all versions of the
+ // file, otherwise go back to the description page
+ $wgOut->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() );
+ }
+ return;
+ }
+
+ $this->showForm();
+ $this->showLogEntries();
+ }
+
+ /**
+ * Show the confirmation form
+ */
+ private function showForm() {
+ global $wgOut, $wgUser, $wgRequest;
+
+ $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) );
+ $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) );
+ $form .= '<fieldset><legend>' . wfMsgHtml( 'filedelete-legend' ) . '</legend>';
+ $form .= $this->prepareMessage( 'filedelete-intro' );
+
+ $form .= '<p>' . Xml::inputLabel( wfMsg( 'filedelete-comment' ), 'wpReason', 'wpReason',
+ 60, $wgRequest->getText( 'wpReason' ) ) . '</p>';
+ $form .= '<p>' . Xml::submitButton( wfMsg( 'filedelete-submit' ) ) . '</p>';
+ $form .= '</fieldset>';
+ $form .= '</form>';
+
+ $wgOut->addHtml( $form );
+ }
+
+ /**
+ * Show deletion log fragments pertaining to the current file
+ */
+ private function showLogEntries() {
+ global $wgOut;
+ $wgOut->addHtml( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
+ $reader = new LogViewer(
+ new LogReader(
+ new FauxRequest(
+ array(
+ 'type' => 'delete',
+ 'page' => $this->title->getPrefixedText(),
+ )
+ )
+ )
+ );
+ $reader->showList( $wgOut );
+ }
+
+ /**
+ * Prepare a message referring to the file being deleted,
+ * showing an appropriate message depending upon whether
+ * it's a current file or an old version
+ *
+ * @param string $message Message base
+ * @return string
+ */
+ private function prepareMessage( $message ) {
+ global $wgLang, $wgServer;
+ if( $this->oldimage ) {
+ return wfMsgExt(
+ "{$message}-old",
+ 'parse',
+ $this->title->getText(),
+ $wgLang->date( $this->getTimestamp(), true ),
+ $wgLang->time( $this->getTimestamp(), true ),
+ $wgServer . $this->file->getArchiveUrl( $this->oldimage )
+ );
+ } else {
+ return wfMsgExt(
+ $message,
+ 'parse',
+ $this->title->getText()
+ );
+ }
+ }
+
+ /**
+ * Set headers, titles and other bits
+ */
+ private function setHeaders() {
+ global $wgOut, $wgUser;
+ $wgOut->setPageTitle( wfMsg( 'filedelete', $this->title->getText() ) );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ $wgOut->setSubtitle( wfMsg( 'filedelete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->title ) ) );
+ }
+
+ /**
+ * Is the provided `oldimage` value valid?
+ *
+ * @return bool
+ */
+ private function isValidOldSpec() {
+ return strlen( $this->oldimage ) >= 16
+ && strpos( $this->oldimage, '/' ) === false
+ && strpos( $this->oldimage, '\\' ) === false;
+ }
+
+ /**
+ * Could we delete the file specified? If an `oldimage`
+ * value was provided, does it correspond to an
+ * existing, local, old version of this file?
+ *
+ * @return bool
+ */
+ private function haveDeletableFile() {
+ return $this->oldimage
+ ? $this->oldfile && $this->oldfile->exists() && $this->oldfile->isLocal()
+ : $this->file && $this->file->exists() && $this->file->isLocal();
+ }
+
+ /**
+ * Prepare the form action
+ *
+ * @return string
+ */
+ private function getAction() {
+ $q = array();
+ $q[] = 'action=delete';
+ if( $this->oldimage )
+ $q[] = 'oldimage=' . urlencode( $this->oldimage );
+ return $this->title->getLocalUrl( implode( '&', $q ) );
+ }
+
+ /**
+ * Extract the timestamp of the old version
+ *
+ * @return string
+ */
+ private function getTimestamp() {
+ return $this->oldfile->getTimestamp();
+ }
+
+} \ No newline at end of file
diff --git a/includes/FileRevertForm.php b/includes/FileRevertForm.php
new file mode 100644
index 00000000..55f21fff
--- /dev/null
+++ b/includes/FileRevertForm.php
@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * File reversion user interface
+ *
+ * @addtogroup Media
+ * @author Rob Church <robchur@gmail.com>
+ */
+class FileRevertForm {
+
+ private $title = null;
+ private $file = null;
+ private $oldimage = '';
+ private $timestamp = false;
+
+ /**
+ * Constructor
+ *
+ * @param File $file File we're reverting
+ */
+ public function __construct( $file ) {
+ $this->title = $file->getTitle();
+ $this->file = $file;
+ }
+
+ /**
+ * Fulfil the request; shows the form or reverts the file,
+ * pending authentication, confirmation, etc.
+ */
+ public function execute() {
+ global $wgOut, $wgRequest, $wgUser, $wgLang, $wgServer;
+ $this->setHeaders();
+
+ if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ } elseif( !$wgUser->isLoggedIn() ) {
+ $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
+ return;
+ } elseif( !$this->title->userCan( 'edit' ) ) {
+ // The standard read-only thing doesn't make a whole lot of sense
+ // here; surely it should show the image or something? -- RC
+ $article = new Article( $this->title );
+ $wgOut->readOnlyPage( $article->getContent(), true );
+ return;
+ } elseif( $wgUser->isBlocked() ) {
+ $wgOut->blockedPage();
+ return;
+ }
+
+ $this->oldimage = $wgRequest->getText( 'oldimage' );
+ $token = $wgRequest->getText( 'wpEditToken' );
+ if( !$this->isValidOldSpec() ) {
+ $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->oldimage ) );
+ return;
+ }
+
+ if( !$this->haveOldVersion() ) {
+ $wgOut->addHtml( wfMsgExt( 'filerevert-badversion', 'parse' ) );
+ $wgOut->returnToMain( false, $this->title );
+ return;
+ }
+
+ // Perform the reversion if appropriate
+ if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) {
+ $source = $this->file->getArchiveVirtualUrl( $this->oldimage );
+ $comment = $wgRequest->getText( 'wpComment' );
+ // TODO: Preserve file properties from database instead of reloading from file
+ $status = $this->file->upload( $source, $comment, $comment );
+ if( $status->isGood() ) {
+ $wgOut->addHtml( wfMsgExt( 'filerevert-success', 'parse', $this->title->getText(),
+ $wgLang->date( $this->getTimestamp(), true ),
+ $wgLang->time( $this->getTimestamp(), true ),
+ $wgServer . $this->file->getArchiveUrl( $this->oldimage ) ) );
+ $wgOut->returnToMain( false, $this->title );
+ } else {
+ $wgOut->addWikiText( $status->getWikiText() );
+ }
+ return;
+ }
+
+ // Show the form
+ $this->showForm();
+ }
+
+ /**
+ * Show the confirmation form
+ */
+ private function showForm() {
+ global $wgOut, $wgUser, $wgRequest, $wgLang, $wgContLang, $wgServer;
+ $timestamp = $this->getTimestamp();
+
+ $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) );
+ $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) );
+ $form .= '<fieldset><legend>' . wfMsgHtml( 'filerevert-legend' ) . '</legend>';
+ $form .= wfMsgExt( 'filerevert-intro', 'parse', $this->title->getText(),
+ $wgLang->date( $timestamp, true ), $wgLang->time( $timestamp, true ), $wgServer . $this->file->getArchiveUrl( $this->oldimage ) );
+ $form .= '<p>' . Xml::inputLabel( wfMsg( 'filerevert-comment' ), 'wpComment', 'wpComment',
+ 60, wfMsgForContent( 'filerevert-defaultcomment',
+ $wgContLang->date( $timestamp, false, false ), $wgContLang->time( $timestamp, false, false ) ) ) . '</p>';
+ $form .= '<p>' . Xml::submitButton( wfMsg( 'filerevert-submit' ) ) . '</p>';
+ $form .= '</fieldset>';
+ $form .= '</form>';
+
+ $wgOut->addHtml( $form );
+ }
+
+ /**
+ * Set headers, titles and other bits
+ */
+ private function setHeaders() {
+ global $wgOut, $wgUser;
+ $wgOut->setPageTitle( wfMsg( 'filerevert', $this->title->getText() ) );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ $wgOut->setSubtitle( wfMsg( 'filerevert-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->title ) ) );
+ }
+
+ /**
+ * Is the provided `oldimage` value valid?
+ *
+ * @return bool
+ */
+ private function isValidOldSpec() {
+ return strlen( $this->oldimage ) >= 16
+ && strpos( $this->oldimage, '/' ) === false
+ && strpos( $this->oldimage, '\\' ) === false;
+ }
+
+ /**
+ * Does the provided `oldimage` value correspond
+ * to an existing, local, old version of this file?
+ *
+ * @return bool
+ */
+ private function haveOldVersion() {
+ $file = wfFindFile( $this->title, $this->oldimage );
+ return $file && $file->exists() && $file->isLocal();
+ }
+
+ /**
+ * Prepare the form action
+ *
+ * @return string
+ */
+ private function getAction() {
+ $q = array();
+ $q[] = 'action=revert';
+ $q[] = 'oldimage=' . urlencode( $this->oldimage );
+ return $this->title->getLocalUrl( implode( '&', $q ) );
+ }
+
+ /**
+ * Extract the timestamp of the old version
+ *
+ * @return string
+ */
+ private function getTimestamp() {
+ if( $this->timestamp === false ) {
+ $file = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->oldimage );
+ $this->timestamp = $file->getTimestamp();
+ }
+ return $this->timestamp;
+ }
+
+} \ No newline at end of file
diff --git a/includes/FileStore.php b/includes/FileStore.php
index dcec71c5..1554d66e 100644
--- a/includes/FileStore.php
+++ b/includes/FileStore.php
@@ -219,7 +219,7 @@ class FileStore {
* Confirm that the given file key is valid.
* Note that a valid key may refer to a file that does not exist.
*
- * Key should consist of a 32-digit base-36 SHA-1 hash and
+ * Key should consist of a 31-digit base-36 SHA-1 hash and
* an optional alphanumeric extension, all lowercase.
* The whole must not exceed 64 characters.
*
@@ -227,7 +227,7 @@ class FileStore {
* @return boolean
*/
static function validKey( $key ) {
- return preg_match( '/^[0-9a-z]{32}(\.[0-9a-z]{1,31})?$/', $key );
+ return preg_match( '/^[0-9a-z]{31,32}(\.[0-9a-z]{1,31})?$/', $key );
}
@@ -249,7 +249,7 @@ class FileStore {
return false;
}
- $base36 = wfBaseConvert( $hash, 16, 36, 32 );
+ $base36 = wfBaseConvert( $hash, 16, 36, 31 );
if( $extension == '' ) {
$key = $base36;
} else {
@@ -376,4 +376,4 @@ class FSTransaction {
*/
class FSException extends MWException { }
-?>
+
diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
index 1ffde741..67cc1f39 100644
--- a/includes/GlobalFunctions.php
+++ b/includes/GlobalFunctions.php
@@ -429,18 +429,11 @@ function wfMsgReal( $key, $args, $useDB = true, $forContent=false, $transform =
* @param $key String:
*/
function wfMsgWeirdKey ( $key ) {
- $subsource = str_replace ( ' ' , '_' , $key ) ;
- $source = wfMsgForContentNoTrans( $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 ( wfEmptyMsg( $subsource, $source ) ) {
- # Didn't work either, return blank text
- $source = "" ;
- }
- return $source ;
+ $source = wfMsgGetKey( $key, false, true, false );
+ if ( wfEmptyMsg( $key, $source ) )
+ return "";
+ else
+ return $source;
}
/**
@@ -454,6 +447,17 @@ function wfMsgWeirdKey ( $key ) {
function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) {
global $wgParser, $wgContLang, $wgMessageCache, $wgLang;
+ /* <Vyznev> btw, is all that code in wfMsgGetKey() that check
+ * if the message cache exists of not really necessary, or is
+ * it just paranoia?
+ * <TimStarling> Vyznev: it's probably not necessary
+ * <TimStarling> I think I wrote it in an attempt to report DB
+ * connection errors properly
+ * <TimStarling> but eventually we gave up on using the
+ * message cache for that and just hard-coded the strings
+ * <TimStarling> it may have other uses, it's not mere paranoia
+ */
+
if ( is_object( $wgMessageCache ) )
$transstat = $wgMessageCache->getTransform();
@@ -468,16 +472,18 @@ function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) {
$lang = &$wgLang;
}
- wfSuppressWarnings();
+ # MessageCache::get() does this already, Language::getMessage() doesn't
+ # ISSUE: Should we try to handle "message/lang" here too?
+ $key = str_replace( ' ' , '_' , $wgContLang->lcfirst( $key ) );
+ wfSuppressWarnings();
if( is_object( $lang ) ) {
$message = $lang->getMessage( $key );
} else {
$message = false;
}
wfRestoreWarnings();
- if($message === false)
- $message = Language::getMessage($key);
+
if ( $transform && strstr( $message, '{{' ) !== false ) {
$message = $wgParser->transformMsg($message, $wgMessageCache->getParserOptions() );
}
@@ -586,7 +592,7 @@ function wfMsgExt( $key, $options ) {
} elseif ( in_array('parseinline', $options) ) {
$string = $wgOut->parse( $string, true, true );
$m = array();
- if( preg_match( "~^<p>(.*)\n?</p>$~", $string, $m ) ) {
+ if( preg_match( '/^<p>(.*)\n?<\/p>$/sU', $string, $m ) ) {
$string = $m[1];
}
} elseif ( in_array('parsemag', $options) ) {
@@ -695,14 +701,14 @@ function wfHostname() {
* @return string
*/
function wfReportTime() {
- global $wgRequestTime;
+ global $wgRequestTime, $wgShowHostnames;
$now = wfTime();
$elapsed = $now - $wgRequestTime;
- $com = sprintf( "<!-- Served by %s in %01.3f secs. -->",
- wfHostname(), $elapsed );
- return $com;
+ return $wgShowHostnames
+ ? sprintf( "<!-- Served by %s in %01.3f secs. -->", wfHostname(), $elapsed )
+ : sprintf( "<!-- Served in %01.3f secs. -->", $elapsed );
}
/**
@@ -813,7 +819,7 @@ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) {
if ( $po < 0 ) { $po = 0; }
$q = "limit={$limit}&offset={$po}";
if ( '' != $query ) { $q .= '&'.$query; }
- $plink = '<a href="' . $title->escapeLocalUrl( $q ) . "\">{$prev}</a>";
+ $plink = '<a href="' . $title->escapeLocalUrl( $q ) . "\" class=\"mw-prevlink\">{$prev}</a>";
} else { $plink = $prev; }
$no = $offset + $limit;
@@ -823,7 +829,7 @@ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) {
if ( $atend ) {
$nlink = $next;
} else {
- $nlink = '<a href="' . $title->escapeLocalUrl( $q ) . "\">{$next}</a>";
+ $nlink = '<a href="' . $title->escapeLocalUrl( $q ) . "\" class=\"mw-nextlink\">{$next}</a>";
}
$nums = wfNumLink( $offset, 20, $title, $query ) . ' | ' .
wfNumLink( $offset, 50, $title, $query ) . ' | ' .
@@ -844,7 +850,7 @@ function wfNumLink( $offset, $limit, &$title, $query = '' ) {
$q .= 'limit='.$limit.'&offset='.$offset;
$fmtLimit = $wgLang->formatNum( $limit );
- $s = '<a href="' . $title->escapeLocalUrl( $q ) . "\">{$fmtLimit}</a>";
+ $s = '<a href="' . $title->escapeLocalUrl( $q ) . "\" class=\"mw-numlink\">{$fmtLimit}</a>";
return $s;
}
@@ -1657,47 +1663,11 @@ function wfTempDir() {
* Make directory, and make all parent directories if they don't exist
*/
function wfMkdirParents( $fullDir, $mode = 0777 ) {
- if ( strval( $fullDir ) === '' ) {
+ if( strval( $fullDir ) === '' )
return true;
- }
-
- # Go back through the paths to find the first directory that exists
- $currentDir = $fullDir;
- $createList = array();
- while ( strval( $currentDir ) !== '' && !file_exists( $currentDir ) ) {
- # Strip trailing slashes
- $currentDir = rtrim( $currentDir, '/\\' );
-
- # Add to create list
- $createList[] = $currentDir;
-
- # Find next delimiter searching from the end
- $p = max( strrpos( $currentDir, '/' ), strrpos( $currentDir, '\\' ) );
- if ( $p === false ) {
- $currentDir = false;
- } else {
- $currentDir = substr( $currentDir, 0, $p );
- }
- }
-
- if ( count( $createList ) == 0 ) {
- # Directory specified already exists
+ if( file_exists( $fullDir ) )
return true;
- } elseif ( $currentDir === false ) {
- # Went all the way back to root and it apparently doesn't exist
- return false;
- }
-
- # Now go forward creating directories
- $createList = array_reverse( $createList );
- foreach ( $createList as $dir ) {
- # use chmod to override the umask, as suggested by the PHP manual
- if ( !mkdir( $dir, $mode ) || !chmod( $dir, $mode ) ) {
- wfDebugLog( 'mkdir', "Unable to create directory $dir\n" );
- return false;
- }
- }
- return true;
+ return mkdir( str_replace( '/', DIRECTORY_SEPARATOR, $fullDir ), $mode, true );
}
/**
@@ -1761,7 +1731,7 @@ function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) {
* @return bool
*/
function wfEmptyMsg( $msg, $wfMsgOut ) {
- return $wfMsgOut === "&lt;$msg&gt;";
+ return $wfMsgOut === htmlspecialchars( "<$msg>" );
}
/**
@@ -1902,11 +1872,15 @@ function wfRegexReplacement( $string ) {
* We'll consider it so always, as we don't want \s in our Unix paths either.
*
* @param string $path
+ * @param string $suffix to remove if present
* @return string
*/
-function wfBaseName( $path ) {
+function wfBaseName( $path, $suffix='' ) {
+ $encSuffix = ($suffix == '')
+ ? ''
+ : ( '(?:' . preg_quote( $suffix, '#' ) . ')?' );
$matches = array();
- if( preg_match( '#([^/\\\\]*)[/\\\\]*$#', $path, $matches ) ) {
+ if( preg_match( "#([^/\\\\]*?){$encSuffix}[/\\\\]*$#", $path, $matches ) ) {
return $matches[1];
} else {
return '';
@@ -2266,4 +2240,84 @@ function &wfGetDB( $db = DB_LAST, $groups = array() ) {
$ret = $wgLoadBalancer->getConnection( $db, true, $groups );
return $ret;
}
-?>
+
+/**
+ * Find a file.
+ * Shortcut for RepoGroup::singleton()->findFile()
+ * @param mixed $title Title object or string. May be interwiki.
+ * @param mixed $time Requested time for an archived image, or false for the
+ * current version. An image object will be returned which
+ * existed at or before the specified time.
+ * @return File, or false if the file does not exist
+ */
+function wfFindFile( $title, $time = false ) {
+ return RepoGroup::singleton()->findFile( $title, $time );
+}
+
+/**
+ * Get an object referring to a locally registered file.
+ * Returns a valid placeholder object if the file does not exist.
+ */
+function wfLocalFile( $title ) {
+ return RepoGroup::singleton()->getLocalRepo()->newFile( $title );
+}
+
+/**
+ * Should low-performance queries be disabled?
+ *
+ * @return bool
+ */
+function wfQueriesMustScale() {
+ global $wgMiserMode;
+ return $wgMiserMode
+ || ( SiteStats::pages() > 100000
+ && SiteStats::edits() > 1000000
+ && SiteStats::users() > 10000 );
+}
+
+/**
+ * Get the path to a specified script file, respecting file
+ * extensions; this is a wrapper around $wgScriptExtension etc.
+ *
+ * @param string $script Script filename, sans extension
+ * @return string
+ */
+function wfScript( $script = 'index' ) {
+ global $wgScriptPath, $wgScriptExtension;
+ return "{$wgScriptPath}/{$script}{$wgScriptExtension}";
+}
+
+/**
+ * Convenience function converts boolean values into "true"
+ * or "false" (string) values
+ *
+ * @param bool $value
+ * @return string
+ */
+function wfBoolToStr( $value ) {
+ return $value ? 'true' : 'false';
+}
+
+/**
+ * Load an extension messages file
+ */
+function wfLoadExtensionMessages( $extensionName ) {
+ global $wgExtensionMessagesFiles, $wgMessageCache;
+ if ( !empty( $wgExtensionMessagesFiles[$extensionName] ) ) {
+ $wgMessageCache->loadMessagesFile( $wgExtensionMessagesFiles[$extensionName] );
+ // Prevent double-loading
+ $wgExtensionMessagesFiles[$extensionName] = false;
+ }
+}
+
+/**
+ * Get a platform-independent path to the null file, e.g.
+ * /dev/null
+ *
+ * @return string
+ */
+function wfGetNull() {
+ return wfIsWindows()
+ ? 'NUL'
+ : '/dev/null';
+} \ No newline at end of file
diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php
index 9a0b6a08..260439b2 100644
--- a/includes/HTMLCacheUpdate.php
+++ b/includes/HTMLCacheUpdate.php
@@ -67,13 +67,13 @@ class HTMLCacheUpdate
break;
}
}
- if ( $id !== false ) {
- // One less on the end to avoid duplicating the boundary
- $job = new HTMLCacheUpdateJob( $this->mTitle, $this->mTable, $start, $id - 1 );
- } else {
- $job = new HTMLCacheUpdateJob( $this->mTitle, $this->mTable, $start, false );
- }
- $jobs[] = $job;
+
+ $params = array(
+ 'table' => $this->mTable,
+ 'start' => $start,
+ 'end' => ( $id !== false ? $id - 1 : false ),
+ );
+ $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params );
$start = $id;
} while ( $start );
@@ -193,20 +193,14 @@ class HTMLCacheUpdateJob extends Job {
/**
* Construct a job
* @param Title $title The title linked to
- * @param string $table The name of the link table.
- * @param integer $start Beginning page_id or false for open interval
- * @param integer $end End page_id or false for open interval
+ * @param array $params Job parameters (table, start and end page_ids)
* @param integer $id job_id
*/
- function __construct( $title, $table, $start, $end, $id = 0 ) {
- $params = array(
- 'table' => $table,
- 'start' => $start,
- 'end' => $end );
+ function __construct( $title, $params, $id = 0 ) {
parent::__construct( 'htmlCacheUpdate', $title, $params, $id );
- $this->table = $table;
- $this->start = intval( $start );
- $this->end = intval( $end );
+ $this->table = $params['table'];
+ $this->start = $params['start'];
+ $this->end = $params['end'];
}
function run() {
@@ -229,4 +223,4 @@ class HTMLCacheUpdateJob extends Job {
return true;
}
}
-?>
+
diff --git a/includes/HTMLFileCache.php b/includes/HTMLFileCache.php
index 1d3778b2..a7466814 100644
--- a/includes/HTMLFileCache.php
+++ b/includes/HTMLFileCache.php
@@ -154,4 +154,4 @@ class HTMLFileCache {
}
-?>
+
diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php
index 715c8c88..69ec1007 100644
--- a/includes/HTMLForm.php
+++ b/includes/HTMLForm.php
@@ -1,7 +1,6 @@
<?php
/**
- * This file contain a class to easily build HTML forms as well as custom
- * functions used by SpecialUserrights.php
+ * This file contain a class to easily build HTML forms
*/
/**
@@ -106,53 +105,3 @@ class HTMLForm {
"<textarea name=\"{$varname}\" rows=\"5\" cols=\"{$size}\">{$s}</textarea>\n";
}
} // end class
-
-/** 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.
- * @param $multiple Boolean: A multiple elements select.
- * @param $size Integer: number of elements to be shown ignored for non-multiple (default 6).
- * @param $reverse Boolean: if true, multiple select will hide selected elements (default false).
- * @todo Document $selectmsg
-*/
-function HTMLSelectGroups($selectname, $selectmsg, $selected=array(), $multiple=false, $size=6, $reverse=false) {
- $groups = User::getAllGroups();
- $out = htmlspecialchars( wfMsg( $selectmsg ) );
- $out .= "<br />";
-
- if( $multiple ) {
- $attribs = array(
- 'name' => $selectname . '[]',
- 'multiple'=> 'multiple',
- 'size' => $size );
- } else {
- $attribs = array( 'name' => $selectname );
- }
- $attribs['style'] = 'width: 100%';
- $out .= wfElement( 'select', $attribs, null );
-
- foreach( $groups as $group ) {
- $attribs = array( 'value' => $group );
- if( $multiple ) {
- // for multiple will only show the things we want
- if( !in_array( $group, $selected ) xor $reverse ) {
- continue;
- }
- } else {
- if( in_array( $group, $selected ) ) {
- $attribs['selected'] = 'selected';
- }
- }
- $out .= wfElement( 'option', $attribs, User::getGroupName( $group ) ) . "\n";
- }
-
- $out .= "</select>\n";
- return $out;
-}
-
-?>
diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php
index 9dfd6d61..984ee2d4 100644
--- a/includes/HistoryBlob.php
+++ b/includes/HistoryBlob.php
@@ -310,4 +310,4 @@ class HistoryBlobCurStub {
}
-?>
+
diff --git a/includes/Hooks.php b/includes/Hooks.php
index b428b08d..20103db4 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -119,6 +119,20 @@ function wfRunHooks($event, $args = null) {
global $wgOut;
$wgOut->showFatalError($retval);
return false;
+ } elseif( $retval === null ) {
+ if( is_array( $callback ) ) {
+ if( is_object( $callback[0] ) ) {
+ $prettyClass = get_class( $callback[0] );
+ } else {
+ $prettyClass = strval( $callback[0] );
+ }
+ $prettyFunc = $prettyClass . '::' . strval( $callback[1] );
+ } else {
+ $prettyFunc = strval( $callback );
+ }
+ throw new MWException( "Detected bug in an extension! " .
+ "Hook $prettyFunc failed to return a value; " .
+ "should return true to continue hook processing or false to abort." );
} else if (!$retval) {
return false;
}
@@ -126,4 +140,4 @@ function wfRunHooks($event, $args = null) {
return true;
}
-?>
+
diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php
index a9fb13ca..6ea3abd0 100644
--- a/includes/HttpFunctions.php
+++ b/includes/HttpFunctions.php
@@ -4,14 +4,23 @@
* Various HTTP related functions
*/
class Http {
+ static function get( $url, $timeout = 'default' ) {
+ return Http::request( "GET", $url, $timeout );
+ }
+
+ static function post( $url, $timeout = 'default' ) {
+ return Http::request( "POST", $url, $timeout );
+ }
+
/**
* Get the contents of a file by HTTP
*
* if $timeout is 'default', $wgHTTPTimeout is used
*/
- static function get( $url, $timeout = 'default' ) {
+ static function request( $method, $url, $timeout = 'default' ) {
global $wgHTTPTimeout, $wgHTTPProxy, $wgVersion, $wgTitle;
+ wfDebug( __METHOD__ . ": $method $url\n" );
# Use curl if available
if ( function_exists( 'curl_init' ) ) {
$c = curl_init( $url );
@@ -26,6 +35,10 @@ class Http {
}
curl_setopt( $c, CURLOPT_TIMEOUT, $timeout );
curl_setopt( $c, CURLOPT_USERAGENT, "MediaWiki/$wgVersion" );
+ if ( $method == 'POST' )
+ curl_setopt( $c, CURLOPT_POST, true );
+ else
+ curl_setopt( $c, CURLOPT_CUSTOMREQUEST, $method );
# Set the referer to $wgTitle, even in command-line mode
# This is useful for interwiki transclusion, where the foreign
@@ -45,12 +58,29 @@ class Http {
if ( curl_getinfo( $c, CURLINFO_HTTP_CODE ) != 200 ) {
$text = false;
}
+ # Don't return truncated output
+ if ( curl_errno( $c ) != CURLE_OK ) {
+ $text = false;
+ }
curl_close( $c );
} else {
- # Otherwise use file_get_contents, or its compatibility function from GlobalFunctions.php
+ # Otherwise use file_get_contents...
# This may take 3 minutes to time out, and doesn't have local fetch capabilities
+
+ global $wgVersion;
+ $headers = array( "User-Agent: MediaWiki/$wgVersion" );
+ if( strcasecmp( $method, 'post' ) == 0 ) {
+ // Required for HTTP 1.0 POSTs
+ $headers[] = "Content-Length: 0";
+ }
+ $opts = array(
+ 'http' => array(
+ 'method' => $method,
+ 'header' => implode( "\r\n", $headers ) ) );
+ $ctx = stream_context_create($opts);
+
$url_fopen = ini_set( 'allow_url_fopen', 1 );
- $text = file_get_contents( $url );
+ $text = file_get_contents( $url, false, $ctx );
ini_set( 'allow_url_fopen', $url_fopen );
}
return $text;
@@ -88,4 +118,4 @@ class Http {
return false;
}
}
-?>
+
diff --git a/includes/IP.php b/includes/IP.php
index 8a2756c9..db712c3b 100644
--- a/includes/IP.php
+++ b/includes/IP.php
@@ -22,7 +22,12 @@ define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)');
define( 'RE_IPV6_ADD', '(:(:' . RE_IPV6_WORD . '){1,7}|' . RE_IPV6_WORD . '(:{1,2}' . RE_IPV6_WORD . '|::$){1,7})' );
define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX );
// This might be useful for regexps used elsewhere, matches any IPv6 or IPv6 address or network
-define( 'IP_ADDRESS_STRING', RE_IP_ADD . '(\/' . RE_IP_PREFIX . '|)|' . RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)');
+define( 'IP_ADDRESS_STRING',
+ '(?:' .
+ RE_IP_ADD . '(\/' . RE_IP_PREFIX . '|)' .
+ '|' .
+ RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)' .
+ ')' );
/**
* A collection of public static functions to play with IP address
@@ -109,13 +114,14 @@ class IP {
* @return string
*/
public static function sanitizeIP( $ip ) {
- if ( !$ip ) return null;
+ $ip = trim( $ip );
+ if ( $ip === '' ) return null;
// Trim and return IPv4 addresses
- if ( self::isIPv4($ip) ) return trim($ip);
+ if ( self::isIPv4($ip) ) return $ip;
// Only IPv6 addresses can be expanded
if ( !self::isIPv6($ip) ) return $ip;
// Remove any whitespaces, convert to upper case
- $ip = strtoupper( trim($ip) );
+ $ip = strtoupper( $ip );
// Expand zero abbreviations
if ( strpos( $ip, '::' ) !== false ) {
$ip = str_replace('::', str_repeat(':0', 8 - substr_count($ip, ':')) . ':', $ip);
@@ -462,22 +468,29 @@ class IP {
* @return valid dotted quad IPv4 address or null
*/
public static function canonicalize( $addr ) {
- if ( self::isValid( $addr ) )
- return $addr;
+ if ( self::isValid( $addr ) )
+ return $addr;
+
+ // Annoying IPv6 representations like ::ffff:1.2.3.4
+ if ( strpos($addr,':') !==false && strpos($addr,'.') !==false ) {
+ $addr = str_replace( '.', ':', $addr );
+ if( IP::isIPv6( $addr ) )
+ return $addr;
+ }
- // IPv6 loopback address
- $m = array();
- if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) )
- return '127.0.0.1';
+ // IPv6 loopback address
+ $m = array();
+ if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) )
+ return '127.0.0.1';
- // IPv4-mapped and IPv4-compatible IPv6 addresses
- if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) )
- return $m[1];
- if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD . ':' . RE_IPV6_WORD . '$/i', $addr, $m ) )
- return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) );
+ // IPv4-mapped and IPv4-compatible IPv6 addresses
+ if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) )
+ return $m[1];
+ if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD . ':' . RE_IPV6_WORD . '$/i', $addr, $m ) )
+ return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) );
- return null; // give up
+ return null; // give up
}
}
-?>
+
diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php
index d04110d4..3e87c994 100644
--- a/includes/ImageFunctions.php
+++ b/includes/ImageFunctions.php
@@ -1,113 +1,4 @@
<?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 thumbnail
- * 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
@@ -256,4 +147,4 @@ function wfFitBoxWidth( $boxWidth, $boxHeight, $maxHeight ) {
}
-?>
+
diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php
index fba7714c..64f266f6 100644
--- a/includes/ImageGallery.php
+++ b/includes/ImageGallery.php
@@ -17,11 +17,17 @@ class ImageGallery
var $mImages, $mShowBytes, $mShowFilename;
var $mCaption = false;
var $mSkin = false;
+ var $mRevisionId = 0;
/**
- * Is the gallery on a wiki page (i.e. not a special page)
+ * Hide blacklisted images?
*/
- var $mParsing;
+ var $mHideBadImages;
+
+ /**
+ * Registered parser object for output callbacks
+ */
+ var $mParser;
/**
* Contextual title, used when images are being screened
@@ -31,6 +37,8 @@ class ImageGallery
private $mPerRow = 4; // How many images wide should the gallery be?
private $mWidths = 120, $mHeights = 120; // How wide/tall each thumbnail should be
+
+ private $mAttribs = array();
/**
* Create a new image gallery object.
@@ -39,14 +47,22 @@ class ImageGallery
$this->mImages = array();
$this->mShowBytes = true;
$this->mShowFilename = true;
- $this->mParsing = false;
+ $this->mParser = false;
+ $this->mHideBadImages = false;
}
/**
- * Set the "parse" bit so we know to hide "bad" images
+ * Register a parser object
*/
- function setParsing( $val = true ) {
- $this->mParsing = $val;
+ function setParser( $parser ) {
+ $this->mParser = $parser;
+ }
+
+ /**
+ * Set bad image flag
+ */
+ function setHideBadImages( $flag = true ) {
+ $this->mHideBadImages = $flag;
}
/**
@@ -127,22 +143,30 @@ class ImageGallery
/**
* Add an image to the gallery.
*
- * @param $image Image object that is added to the gallery
+ * @param $title Title object of the image that is added to the gallery
* @param $html String: additional HTML text to be shown. The name and size of the image are always shown.
*/
- function add( $image, $html='' ) {
- $this->mImages[] = array( &$image, $html );
- wfDebug( "ImageGallery::add " . $image->getName() . "\n" );
+ function add( $title, $html='' ) {
+ if ( $title instanceof File ) {
+ // Old calling convention
+ $title = $title->getTitle();
+ }
+ $this->mImages[] = array( $title, $html );
+ wfDebug( "ImageGallery::add " . $title->getText() . "\n" );
}
/**
* Add an image at the beginning of the gallery.
*
- * @param $image Image object that is added to the gallery
+ * @param $title Title object of the image that is added to the gallery
* @param $html String: Additional HTML text to be shown. The name and size of the image are always shown.
*/
- function insert( $image, $html='' ) {
- array_unshift( $this->mImages, array( &$image, $html ) );
+ function insert( $title, $html='' ) {
+ if ( $title instanceof File ) {
+ // Old calling convention
+ $title = $title->getTitle();
+ }
+ array_unshift( $this->mImages, array( &$title, $html ) );
}
@@ -172,6 +196,19 @@ class ImageGallery
function setShowFilename( $f ) {
$this->mShowFilename = ( $f == true);
}
+
+ /**
+ * Set arbitrary attributes to go on the HTML gallery output element.
+ * Should be suitable for a &lt;table&gt; element.
+ *
+ * Note -- if taking from user input, you should probably run through
+ * Sanitizer::validateAttributes() first.
+ *
+ * @param array of HTML attribute pairs
+ */
+ function setAttributes( $attribs ) {
+ $this->mAttribs = $attribs;
+ }
/**
* Return a HTML representation of the image gallery
@@ -188,23 +225,33 @@ class ImageGallery
$sk = $this->getSkin();
- $s = '<table class="gallery" cellspacing="0" cellpadding="0">';
+ $attribs = Sanitizer::mergeAttributes(
+ array(
+ 'class' => 'gallery',
+ 'cellspacing' => '0',
+ 'cellpadding' => '0' ),
+ $this->mAttribs );
+ $s = Xml::openElement( 'table', $attribs );
if( $this->mCaption )
$s .= "\n\t<caption>{$this->mCaption}</caption>";
$params = array( 'width' => $this->mWidths, 'height' => $this->mHeights );
$i = 0;
foreach ( $this->mImages as $pair ) {
- $img =& $pair[0];
+ $nt = $pair[0];
$text = $pair[1];
+
+ # Give extensions a chance to select the file revision for us
+ $time = false;
+ wfRunHooks( 'BeforeGalleryFindFile', array( &$this, &$nt, &$time ) );
- $nt = $img->getTitle();
+ $img = wfFindFile( $nt, $time );
- if( $nt->getNamespace() != NS_IMAGE ) {
+ if( $nt->getNamespace() != NS_IMAGE || !$img ) {
# We're dealing with a non-image, spit out the name and be done with it.
$thumbhtml = "\n\t\t\t".'<div style="height: '.($this->mHeights*1.25+2).'px;">'
. htmlspecialchars( $nt->getText() ) . '</div>';
- } elseif( $this->mParsing && wfIsBadImage( $nt->getDBkey(), $this->getContextTitle() ) ) {
+ } elseif( $this->mHideBadImages && wfIsBadImage( $nt->getDBkey(), $this->getContextTitle() ) ) {
# The image is blacklisted, just show it as a text link.
$thumbhtml = "\n\t\t\t".'<div style="height: '.($this->mHeights*1.25+2).'px;">'
. $sk->makeKnownLinkObj( $nt, htmlspecialchars( $nt->getText() ) ) . '</div>';
@@ -214,15 +261,26 @@ class ImageGallery
. htmlspecialchars( $img->getLastError() ) . '</div>';
} else {
$vpad = floor( ( 1.25*$this->mHeights - $thumb->height ) /2 ) - 2;
- $thumbhtml = "\n\t\t\t".'<div class="thumb" style="padding: ' . $vpad . 'px 0; width: '.($this->mWidths+30).'px;">'
- . $sk->makeKnownLinkObj( $nt, $thumb->toHtml() ) . '</div>';
+
+ $thumbhtml = "\n\t\t\t".
+ '<div class="thumb" style="padding: ' . $vpad . 'px 0; width: ' .($this->mWidths+30).'px;">'
+ # Auto-margin centering for block-level elements. Needed now that we have video
+ # handlers since they may emit block-level elements as opposed to simple <img> tags.
+ # ref http://css-discuss.incutio.com/?page=CenteringBlockElement
+ . '<div style="margin-left: auto; margin-right: auto; width: ' .$this->mWidths.'px;">'
+ . $thumb->toHtml( array( 'desc-link' => true ) ) . '</div></div>';
+
+ // Call parser transform hook
+ if ( $this->mParser && $img->getHandler() ) {
+ $img->getHandler()->parserTransformHook( $this->mParser, $img );
+ }
}
//TODO
//$ul = $sk->makeLink( $wgContLang->getNsText( Namespace::getUser() ) . ":{$ut}", $ut );
if( $this->mShowBytes ) {
- if( $img->exists() ) {
+ if( $img ) {
$nb = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'),
$wgLang->formatNum( $img->getSize() ) );
} else {
@@ -292,4 +350,5 @@ class ImageGallery
}
} //class
-?>
+
+
diff --git a/includes/ImagePage.php b/includes/ImagePage.php
index 13f8e46a..3cf6d0ac 100644
--- a/includes/ImagePage.php
+++ b/includes/ImagePage.php
@@ -16,8 +16,18 @@ if( !defined( 'MEDIAWIKI' ) )
class ImagePage extends Article {
/* private */ var $img; // Image object this page is shown for
+ /* private */ var $repo;
var $mExtraDescription = false;
+ function __construct( $title ) {
+ parent::__construct( $title );
+ $this->img = wfFindFile( $this->mTitle );
+ if ( !$this->img ) {
+ $this->img = wfLocalFile( $this->mTitle );
+ }
+ $this->repo = $this->img->repo;
+ }
+
/**
* Handler for action=render
* Include body text only; none of the image extras
@@ -31,8 +41,6 @@ class ImagePage extends Article {
function view() {
global $wgOut, $wgShowEXIF, $wgRequest, $wgUser;
- $this->img = new Image( $this->mTitle );
-
$diff = $wgRequest->getVal( 'diff' );
$diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
@@ -40,10 +48,10 @@ class ImagePage extends Article {
return Article::view();
if ($wgShowEXIF && $this->img->exists()) {
- $exif = $this->img->getExifData();
- $showmeta = count($exif) ? true : false;
+ // FIXME: bad interface, see note on MediaHandler::formatMetadata().
+ $formattedMetadata = $this->img->formatMetadata();
+ $showmeta = $formattedMetadata !== false;
} else {
- $exif = false;
$showmeta = false;
}
@@ -76,12 +84,12 @@ class ImagePage extends Article {
$this->imageHistory();
$this->imageLinks();
- if ( $exif ) {
+ if ( $showmeta ) {
global $wgStylePath, $wgStyleVersion;
$expand = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-expand' ) ) );
$collapse = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-collapse' ) ) );
$wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ). "\n" );
- $wgOut->addWikiText( $this->makeMetadataTable( $exif ) );
+ $wgOut->addWikiText( $this->makeMetadataTable( $formattedMetadata ) );
$wgOut->addHTML(
"<script type=\"text/javascript\" src=\"$wgStylePath/common/metadata.js?$wgStyleVersion\"></script>\n" .
"<script type=\"text/javascript\">attachMetadataToggle('mw_metadata', '$expand', '$collapse');</script>\n" );
@@ -100,9 +108,9 @@ class ImagePage extends Article {
global $wgLang;
$r = '<ul id="filetoc">
<li><a href="#file">' . $wgLang->getNsText( NS_IMAGE ) . '</a></li>
- <li><a href="#filehistory">' . wfMsgHtml( 'imghistory' ) . '</a></li>
+ <li><a href="#filehistory">' . wfMsgHtml( 'filehist' ) . '</a></li>
<li><a href="#filelinks">' . wfMsgHtml( 'imagelinks' ) . '</a></li>' .
- ($metadata ? '<li><a href="#metadata">' . wfMsgHtml( 'metadata' ) . '</a></li>' : '') . '
+ ($metadata ? ' <li><a href="#metadata">' . wfMsgHtml( 'metadata' ) . '</a></li>' : '') . '
</ul>';
return $r;
}
@@ -110,57 +118,39 @@ class ImagePage extends Article {
/**
* Make a table with metadata to be shown in the output page.
*
+ * FIXME: bad interface, see note on MediaHandler::formatMetadata().
+ *
* @access private
*
* @param array $exif The array containing the EXIF data
* @return string
*/
- function makeMetadataTable( $exif ) {
+ function makeMetadataTable( $metadata ) {
$r = wfMsg( 'metadata-help' ) . "\n\n";
$r .= "{| id=mw_metadata class=mw_metadata\n";
- $visibleFields = $this->visibleMetadataFields();
- foreach( $exif as $k => $v ) {
- $tag = strtolower( $k );
- $msg = wfMsg( "exif-$tag" );
- $class = "exif-$tag";
- if( !in_array( $tag, $visibleFields ) ) {
- $class .= ' collapsable';
+ foreach ( $metadata as $type => $stuff ) {
+ foreach ( $stuff as $v ) {
+ $class = Sanitizer::escapeId( $v['id'] );
+ if( $type == 'collapsed' ) {
+ $class .= ' collapsable';
+ }
+ $r .= "|- class=\"$class\"\n";
+ $r .= "!| {$v['name']}\n";
+ $r .= "|| {$v['value']}\n";
}
- $r .= "|- class=\"$class\"\n";
- $r .= "!| $msg\n";
- $r .= "|| $v\n";
}
$r .= '|}';
return $r;
}
/**
- * Get a list of EXIF metadata items which should be displayed when
- * the metadata table is collapsed.
- *
- * @return array of strings
- * @access private
- */
- function visibleMetadataFields() {
- $fields = array();
- $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) );
- foreach( $lines as $line ) {
- $matches = array();
- if( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
- $fields[] = $matches[1];
- }
- }
- return $fields;
- }
-
- /**
* Overloading Article's getContent method.
*
* Omit noarticletext if sharedupload; text will be fetched from the
* shared upload server if possible.
*/
function getContent() {
- if( $this->img && $this->img->fromSharedDirectory && 0 == $this->getID() ) {
+ if( $this->img && !$this->img->isLocal() && 0 == $this->getID() ) {
return '';
}
return Article::getContent();
@@ -203,12 +193,15 @@ class ImagePage extends Article {
$mime = $this->img->getMimeType();
$showLink = false;
$linkAttribs = array( 'href' => $full_url );
+ $longDesc = $this->img->getLongDesc();
+
+ wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this , &$wgOut ) ) ;
- if ( $this->img->allowInlineDisplay() and $width and $height) {
+ if ( $this->img->allowInlineDisplay() ) {
# image
# "Download high res version" link below the image
- $msgsize = wfMsgHtml('file-info-size', $width_orig, $height_orig, $sk->formatSize( $this->img->getSize() ), $mime );
+ #$msgsize = wfMsgHtml('file-info-size', $width_orig, $height_orig, $sk->formatSize( $this->img->getSize() ), $mime );
# We'll show a thumbnail of this image
if ( $width > $maxWidth || $height > $maxHeight ) {
# Calculate the thumbnail size.
@@ -242,21 +235,20 @@ class ImagePage extends Article {
} else {
$anchorclose .=
$msgsmall .
- '<br />' . Xml::tags( 'a', $linkAttribs, $msgbig ) . ' ' . $msgsize;
+ '<br />' . Xml::tags( 'a', $linkAttribs, $msgbig ) . ' ' . $longDesc;
}
if ( $this->img->isMultipage() ) {
$wgOut->addHTML( '<table class="multipageimage"><tr><td>' );
}
- $imgAttribs = array(
- 'border' => 0,
- 'alt' => $this->img->getTitle()->getPrefixedText()
- );
-
if ( $thumbnail ) {
+ $options = array(
+ 'alt' => $this->img->getTitle()->getPrefixedText(),
+ 'file-link' => true,
+ );
$wgOut->addHTML( '<div class="fullImageLink" id="file">' .
- $thumbnail->toHtml( $imgAttribs, $linkAttribs ) .
+ $thumbnail->toHtml( $options ) .
$anchorclose . '</div>' );
}
@@ -265,8 +257,8 @@ class ImagePage extends Article {
if ( $page > 1 ) {
$label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false );
- $link = $sk->makeLinkObj( $this->mTitle, $label, 'page='. ($page-1) );
- $thumb1 = $sk->makeThumbLinkObj( $this->img, $link, $label, 'none',
+ $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page-1) );
+ $thumb1 = $sk->makeThumbLinkObj( $this->mTitle, $this->img, $link, $label, 'none',
array( 'page' => $page - 1 ) );
} else {
$thumb1 = '';
@@ -274,8 +266,8 @@ class ImagePage extends Article {
if ( $page < $count ) {
$label = wfMsg( 'imgmultipagenext' );
- $link = $sk->makeLinkObj( $this->mTitle, $label, 'page='. ($page+1) );
- $thumb2 = $sk->makeThumbLinkObj( $this->img, $link, $label, 'none',
+ $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page+1) );
+ $thumb2 = $sk->makeThumbLinkObj( $this->mTitle, $this->img, $link, $label, 'none',
array( 'page' => $page + 1 ) );
} else {
$thumb2 = '';
@@ -304,9 +296,9 @@ class ImagePage extends Article {
if ($this->img->isSafeFile()) {
$icon= $this->img->iconThumb();
- $wgOut->addHTML( '<div class="fullImageLink" id="file"><a href="' . $full_url . '">' .
- $icon->toHtml() .
- '</a></div>' );
+ $wgOut->addHTML( '<div class="fullImageLink" id="file">' .
+ $icon->toHtml( array( 'desc-link' => true ) ) .
+ '</div>' );
}
$showLink = true;
@@ -314,42 +306,32 @@ class ImagePage extends Article {
if ($showLink) {
- // Workaround for incorrect MIME type on SVGs uploaded in previous versions
- if ($mime == 'image/svg') $mime = 'image/svg+xml';
-
$filename = wfEscapeWikiText( $this->img->getName() );
- $info = wfMsg( 'file-info', $sk->formatSize( $this->img->getSize() ), $mime );
- $infores = '';
-
- // Check for MIME type. Other types may have more information in the future.
- if (substr($mime,0,9) == 'image/svg' ) {
- $infores = wfMsg('file-svg', $width_orig, $height_orig ) . '<br />';
- }
global $wgContLang;
$dirmark = $wgContLang->getDirMark();
if (!$this->img->isSafeFile()) {
$warning = wfMsg( 'mediawarning' );
- $wgOut->addWikiText( <<<END
-<div class="fullMedia">$infores
+ $wgOut->addWikiText( <<<EOT
+<div class="fullMedia">
<span class="dangerousLink">[[Media:$filename|$filename]]</span>$dirmark
-<span class="fileInfo"> $info</span>
+<span class="fileInfo"> $longDesc</span>
</div>
<div class="mediaWarning">$warning</div>
-END
+EOT
);
} else {
- $wgOut->addWikiText( <<<END
-<div class="fullMedia">$infores
-[[Media:$filename|$filename]]$dirmark <span class="fileInfo"> $info</span>
+ $wgOut->addWikiText( <<<EOT
+<div class="fullMedia">
+[[Media:$filename|$filename]]$dirmark <span class="fileInfo"> $longDesc</span>
</div>
-END
+EOT
);
}
}
- if($this->img->fromSharedDirectory) {
+ if(!$this->img->isLocal()) {
$this->printSharedImageText();
}
} else {
@@ -363,27 +345,21 @@ END
}
function printSharedImageText() {
- global $wgRepositoryBaseUrl, $wgFetchCommonsDescriptions, $wgOut, $wgUser;
-
- $url = $wgRepositoryBaseUrl . urlencode($this->mTitle->getDBkey());
- $sharedtext = "<div class='sharedUploadNotice'>" . wfMsgWikiHtml("sharedupload");
- if ($wgRepositoryBaseUrl && !$wgFetchCommonsDescriptions) {
+ global $wgOut, $wgUser;
+ $descUrl = $this->img->getDescriptionUrl();
+ $descText = $this->img->getDescriptionText();
+ $s = "<div class='sharedUploadNotice'>" . wfMsgWikiHtml("sharedupload");
+ if ( $descUrl && !$descText) {
$sk = $wgUser->getSkin();
- $title = SpecialPage::getTitleFor( 'Upload' );
- $link = $sk->makeKnownLinkObj($title, wfMsgHtml('shareduploadwiki-linktext'),
- array( 'wpDestFile' => urlencode( $this->img->getName() )));
- $sharedtext .= " " . wfMsgWikiHtml('shareduploadwiki', $link);
+ $link = $sk->makeExternalLink( $descUrl, wfMsg('shareduploadwiki-linktext') );
+ $s .= " " . wfMsgWikiHtml('shareduploadwiki', $link);
}
- $sharedtext .= "</div>";
- $wgOut->addHTML($sharedtext);
-
- if ($wgRepositoryBaseUrl && $wgFetchCommonsDescriptions) {
- $renderUrl = wfAppendQuery( $url, 'action=render' );
- wfDebug( "Fetching shared description from $renderUrl\n" );
- $text = Http::get( $renderUrl );
- if ($text)
- $this->mExtraDescription = $text;
+ $s .= "</div>";
+ $wgOut->addHTML($s);
+
+ if ( $descText ) {
+ $this->mExtraDescription = $descText;
}
}
@@ -400,7 +376,7 @@ END
function uploadLinksBox() {
global $wgUser, $wgOut;
- if( $this->img->fromSharedDirectory )
+ if( !$this->img->isLocal() )
return;
$sk = $wgUser->getSkin();
@@ -408,7 +384,7 @@ END
$wgOut->addHtml( '<br /><ul>' );
# "Upload a new version of this file" link
- if( $wgUser->isAllowed( 'reupload' ) ) {
+ if( UploadForm::userCanReUpload($wgUser,$this->img->name) ) {
$ulink = $sk->makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) );
$wgOut->addHtml( "<li><div class='plainlinks'>{$ulink}</div></li>" );
}
@@ -439,25 +415,31 @@ END
$line = $this->img->nextHistoryLine();
if ( $line ) {
- $list = new ImageHistoryList( $sk );
+ $list = new ImageHistoryList( $sk, $this->img );
+ $file = $this->repo->newFileFromRow( $line );
+ $dims = $file->getDimensionsString();
$s = $list->beginImageHistoryList() .
$list->imageHistoryLine( true, wfTimestamp(TS_MW, $line->img_timestamp),
$this->mTitle->getDBkey(), $line->img_user,
$line->img_user_text, $line->img_size, $line->img_description,
- $line->img_width, $line->img_height
+ $dims
);
while ( $line = $this->img->nextHistoryLine() ) {
- $s .= $list->imageHistoryLine( false, $line->img_timestamp,
- $line->oi_archive_name, $line->img_user,
- $line->img_user_text, $line->img_size, $line->img_description,
- $line->img_width, $line->img_height
+ $file = $this->repo->newFileFromRow( $line );
+ $dims = $file->getDimensionsString();
+ $s .= $list->imageHistoryLine( false, $line->oi_timestamp,
+ $line->oi_archive_name, $line->oi_user,
+ $line->oi_user_text, $line->oi_size, $line->oi_description,
+ $dims
);
}
$s .= $list->endImageHistoryList();
} else { $s=''; }
$wgOut->addHTML( $s );
+ $this->img->resetHistory(); // free db resources
+
# Exist check because we don't want to show this on pages where an image
# doesn't exist along with the noimage message, that would suck. -ævar
if( $wgUseExternalEditor && $this->img->exists() ) {
@@ -496,207 +478,31 @@ END
$wgOut->addHTML( "</ul>\n" );
}
- function delete()
- {
- 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->permissionRequired( 'delete' );
- return;
- }
- if ( $wgUser->isBlocked() ) {
- return $this->blockedIPpage();
- }
- if ( wfReadOnly() ) {
- $wgOut->readOnlyPage();
- return;
- }
-
- # Better double-check that it hasn't been deleted yet!
- $wgOut->setPagetitle( wfMsg( 'confirmdelete' ) );
- if ( ( !is_null( $image ) )
- && ( '' == trim( $image ) ) ) {
- $wgOut->showFatalError( wfMsg( 'cannotdelete' ) );
- return;
- }
-
- $this->img = new Image( $this->mTitle );
-
- # Deleting old images doesn't require confirmation
- if ( !is_null( $oldimage ) || $confirm ) {
- if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $oldimage ) ) {
- $this->doDelete( $reason );
- } else {
- $wgOut->showFatalError( wfMsg( 'sessionfailure' ) );
- }
- return;
- }
-
- if ( !is_null( $image ) ) {
- $q = '&image=' . urlencode( $image );
- } else if ( !is_null( $oldimage ) ) {
- $q = '&oldimage=' . urlencode( $oldimage );
- } else {
- $q = '';
- }
- return $this->confirmDelete( $q, $wgRequest->getText( 'wpReason' ) );
- }
-
- /*
- * Delete an image.
- * @param $reason User provided reason for deletion.
- */
- function doDelete( $reason ) {
- global $wgOut, $wgRequest;
-
- $oldimage = $wgRequest->getVal( 'oldimage' );
-
- if ( !is_null( $oldimage ) ) {
- if ( strlen( $oldimage ) < 16 ) {
- $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) );
- return;
- }
- if ( strstr( $oldimage, "/" ) || strstr( $oldimage, "\\" ) ) {
- $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) );
- return;
- }
- if ( !$this->doDeleteOldImage( $oldimage ) ) {
- return;
- }
- $deleted = $oldimage;
- } else {
- $ok = $this->img->delete( $reason );
- if( !$ok ) {
- # If the deletion operation actually failed, bug out:
- $wgOut->showFileDeleteError( $this->img->getName() );
- return;
- }
-
- # Image itself is now gone, and database is cleaned.
- # Now we remove the image description page.
-
- $article = new Article( $this->mTitle );
- $article->doDeleteArticle( $reason ); # ignore errors
-
- $deleted = $this->img->getName();
- }
-
- $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
-
- $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]';
- $text = wfMsg( 'deletedtext', $deleted, $loglink );
-
- $wgOut->addWikiText( $text );
-
- $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() );
- }
-
/**
- * @return success
+ * Delete the file, or an earlier version of it
*/
- function doDeleteOldImage( $oldimage )
- {
- global $wgOut;
-
- $ok = $this->img->deleteOld( $oldimage, '' );
- if( !$ok ) {
- # If we actually have a file and can't delete it, throw an error.
- # Something went awry...
- $wgOut->showFileDeleteError( "$oldimage" );
- } else {
- # Log the deletion
- $log = new LogPage( 'delete' );
- $log->addEntry( 'delete', $this->mTitle, wfMsg('deletedrevision',$oldimage) );
- }
- return $ok;
- }
-
- function revert() {
- global $wgOut, $wgRequest, $wgUser;
-
- $oldimage = $wgRequest->getText( 'oldimage' );
- if ( strlen( $oldimage ) < 16 ) {
- $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) );
- return;
- }
- if ( strstr( $oldimage, "/" ) || strstr( $oldimage, "\\" ) ) {
- $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) );
- return;
- }
-
- if ( wfReadOnly() ) {
- $wgOut->readOnlyPage();
- return;
- }
- if( $wgUser->isAnon() ) {
- $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
+ public function delete() {
+ if( !$this->img->exists() || !$this->img->isLocal() ) {
+ // Standard article deletion
+ Article::delete();
return;
}
- if ( ! $this->mTitle->userCan( 'edit' ) ) {
- $wgOut->readOnlyPage( $this->getContent(), true );
- return;
- }
- if ( $wgUser->isBlocked() ) {
- return $this->blockedIPpage();
- }
- if( !$wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $oldimage ) ) {
- $wgOut->showErrorPage( 'internalerror', 'sessionfailure' );
- return;
- }
- $name = substr( $oldimage, 15 );
-
- $dest = wfImageDir( $name );
- $archive = wfImageArchiveDir( $name );
- $curfile = "{$dest}/{$name}";
-
- if ( !is_dir( $dest ) ) wfMkdirParents( $dest );
- if ( !is_dir( $archive ) ) wfMkdirParents( $archive );
-
- if ( ! is_file( $curfile ) ) {
- $wgOut->showFileNotFoundError( htmlspecialchars( $curfile ) );
- return;
- }
- $oldver = wfTimestampNow() . "!{$name}";
-
- if ( ! rename( $curfile, "${archive}/{$oldver}" ) ) {
- $wgOut->showFileRenameError( $curfile, "${archive}/{$oldver}" );
- return;
- }
- if ( ! copy( "{$archive}/{$oldimage}", $curfile ) ) {
- $wgOut->showFileCopyError( "${archive}/{$oldimage}", $curfile );
- return;
- }
-
- # Record upload and update metadata cache
- $img = Image::newFromName( $name );
- $img->recordUpload( $oldver, wfMsg( "reverted" ) );
-
- $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
- $wgOut->addHTML( wfMsg( 'imagereverted' ) );
-
- $descTitle = $img->getTitle();
- $wgOut->returnToMain( false, $descTitle->getPrefixedText() );
+ $deleter = new FileDeleteForm( $this->img );
+ $deleter->execute();
}
- function blockedIPpage() {
- $edit = new EditPage( $this );
- return $edit->blockedIPpage();
+ /**
+ * Revert the file to an earlier version
+ */
+ public function revert() {
+ $reverter = new FileRevertForm( $this->img );
+ $reverter->execute();
}
/**
* Override handling of action=purge
*/
function doPurge() {
- $this->img = new Image( $this->mTitle );
if( $this->img->exists() ) {
wfDebug( "ImagePage::doPurge purging " . $this->img->getName() . "\n" );
$update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' );
@@ -709,82 +515,120 @@ END
parent::doPurge();
}
+ /**
+ * Display an error with a wikitext description
+ */
+ function showError( $description ) {
+ global $wgOut;
+ $wgOut->setPageTitle( wfMsg( "internalerror" ) );
+ $wgOut->setRobotpolicy( "noindex,nofollow" );
+ $wgOut->setArticleRelated( false );
+ $wgOut->enableClientCache( false );
+ $wgOut->addWikiText( $description );
+ }
+
}
/**
- * @todo document
+ * Builds the image revision log shown on image pages
+ *
* @addtogroup Media
*/
class ImageHistoryList {
- function ImageHistoryList( &$skin ) {
- $this->skin =& $skin;
- }
- function beginImageHistoryList() {
- $s = "\n" .
- Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'imghistory' ) ) .
- "\n<p>" . wfMsg( 'imghistlegend' ) . "</p>\n".'<ul class="special">';
- return $s;
- }
+ protected $img, $skin, $title, $repo;
- function endImageHistoryList() {
- $s = "</ul>\n";
- return $s;
+ public function __construct( $skin, $img ) {
+ $this->skin = $skin;
+ $this->img = $img;
+ $this->title = $img->getTitle();
}
- function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $width, $height ) {
- global $wgUser, $wgLang, $wgTitle, $wgContLang;
-
- $datetime = $wgLang->timeanddate( $timestamp, true );
- $del = wfMsgHtml( 'deleteimg' );
- $delall = wfMsgHtml( 'deleteimgcompletely' );
- $cur = wfMsgHtml( 'cur' );
+ public function beginImageHistoryList() {
+ global $wgOut, $wgUser;
+ return Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'filehist' ) )
+ . $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) )
+ . Xml::openElement( 'table', array( 'class' => 'filehistory' ) ) . "\n"
+ . '<tr><td></td>'
+ . ( $this->img->isLocal() && $wgUser->isAllowed( 'delete' ) ? '<td></td>' : '' )
+ . '<th>' . wfMsgHtml( 'filehist-datetime' ) . '</th>'
+ . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>'
+ . '<th>' . wfMsgHtml( 'filehist-dimensions' ) . '</th>'
+ . '<th class="mw-imagepage-filesize">' . wfMsgHtml( 'filehist-filesize' ) . '</th>'
+ . '<th>' . wfMsgHtml( 'filehist-comment' ) . '</th>'
+ . "</tr>\n";
+ }
- if ( $iscur ) {
- $url = Image::imageUrl( $img );
- $rlink = $cur;
- if ( $wgUser->isAllowed('delete') ) {
- $link = $wgTitle->escapeLocalURL( 'image=' . $wgTitle->getPartialURL() .
- '&action=delete' );
- $style = $this->skin->getInternalLinkAttributes( $link, $delall );
+ public function endImageHistoryList() {
+ return "</table>\n";
+ }
- $dlink = '<a href="'.$link.'"'.$style.'>'.$delall.'</a>';
- } else {
- $dlink = $del;
- }
+ public function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $dims ) {
+ global $wgUser, $wgLang, $wgContLang;
+ $local = $this->img->isLocal();
+ $row = '';
+
+ // Deletion link
+ if( $local && $wgUser->isAllowed( 'delete' ) ) {
+ $row .= '<td>';
+ $q = array();
+ $q[] = 'action=delete';
+ if( !$iscur )
+ $q[] = 'oldimage=' . urlencode( $img );
+ $row .= '(' . $this->skin->makeKnownLinkObj(
+ $this->title,
+ wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ),
+ implode( '&', $q )
+ ) . ')';
+ $row .= '</td>';
+ }
+
+ // Reversion link/current indicator
+ $row .= '<td>';
+ if( $iscur ) {
+ $row .= '(' . wfMsgHtml( 'filehist-current' ) . ')';
+ } elseif( $local && $wgUser->isLoggedIn() && $this->title->userCan( 'edit' ) ) {
+ $q = array();
+ $q[] = 'action=revert';
+ $q[] = 'oldimage=' . urlencode( $img );
+ $q[] = 'wpEditToken=' . urlencode( $wgUser->editToken( $img ) );
+ $row .= '(' . $this->skin->makeKnownLinkObj(
+ $this->title,
+ wfMsgHtml( 'filehist-revert' ),
+ implode( '&', $q )
+ ) . ')';
+ }
+ $row .= '</td>';
+
+ // Date/time and image link
+ $row .= '<td>';
+ $url = $iscur ? $this->img->getUrl() : $this->img->getArchiveUrl( $img );
+ $row .= Xml::element(
+ 'a',
+ array( 'href' => $url ),
+ $wgLang->timeAndDate( $timestamp, true )
+ );
+ $row .= '</td>';
+
+ // Uploading user
+ $row .= '<td>';
+ if( $local ) {
+ $row .= $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext );
} else {
- $url = htmlspecialchars( wfImageArchiveUrl( $img ) );
- if( $wgUser->getID() != 0 && $wgTitle->userCan( 'edit' ) ) {
- $token = urlencode( $wgUser->editToken( $img ) );
- $rlink = $this->skin->makeKnownLinkObj( $wgTitle,
- wfMsgHtml( 'revertimg' ), 'action=revert&oldimage=' .
- urlencode( $img ) . "&wpEditToken=$token" );
- $dlink = $this->skin->makeKnownLinkObj( $wgTitle,
- $del, 'action=delete&oldimage=' . urlencode( $img ) .
- "&wpEditToken=$token" );
- } else {
- # Having live active links for non-logged in users
- # means that bots and spiders crawling our site can
- # inadvertently change content. Baaaad idea.
- $rlink = wfMsgHtml( 'revertimg' );
- $dlink = $del;
- }
+ $row .= htmlspecialchars( $usertext );
}
-
- $userlink = $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext );
- $nbytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ),
- $wgLang->formatNum( $size ) );
- $widthheight = wfMsgHtml( 'widthheight', $width, $height );
- $style = $this->skin->getInternalLinkAttributes( $url, $datetime );
+ $row .= '</td>';
- $s = "<li> ({$dlink}) ({$rlink}) <a href=\"{$url}\"{$style}>{$datetime}</a> . . {$userlink} . . {$widthheight} ({$nbytes})";
+ // Image dimensions
+ $row .= '<td>' . htmlspecialchars( $dims ) . '</td>';
- $s .= $this->skin->commentBlock( $description, $wgTitle );
- $s .= "</li>\n";
- return $s;
- }
+ // File size
+ $row .= '<td class="mw-imagepage-filesize">' . $this->skin->formatSize( $size ) . '</td>';
-}
+ // Comment
+ $row .= '<td>' . $this->skin->formatComment( $description, $this->title ) . '</td>';
+ return "<tr>{$row}</tr>\n";
+ }
-?>
+}
diff --git a/includes/ImageQueryPage.php b/includes/ImageQueryPage.php
index 93f090a1..8948ddc6 100644
--- a/includes/ImageQueryPage.php
+++ b/includes/ImageQueryPage.php
@@ -30,8 +30,8 @@ class ImageQueryPage extends QueryPage {
# $num [should update this to use a Pager]
for( $i = 0; $i < $num && $row = $dbr->fetchObject( $res ); $i++ ) {
$image = $this->prepareImage( $row );
- if( $image instanceof Image ) {
- $gallery->add( $image, $this->getCellHtml( $row ) );
+ if( $image ) {
+ $gallery->add( $image->getTitle(), $this->getCellHtml( $row ) );
}
}
@@ -49,7 +49,7 @@ class ImageQueryPage extends QueryPage {
$namespace = isset( $row->namespace ) ? $row->namespace : NS_IMAGE;
$title = Title::makeTitleSafe( $namespace, $row->title );
return ( $title instanceof Title && $title->getNamespace() == NS_IMAGE )
- ? new Image( $title )
+ ? wfFindFile( $title )
: null;
}
@@ -65,4 +65,4 @@ class ImageQueryPage extends QueryPage {
}
-?>
+
diff --git a/includes/JobQueue.php b/includes/JobQueue.php
index 140130fa..a2780bdb 100644
--- a/includes/JobQueue.php
+++ b/includes/JobQueue.php
@@ -4,6 +4,8 @@ if ( !defined( 'MEDIAWIKI' ) ) {
die( "This file is part of MediaWiki, it is not a valid entry point\n" );
}
+require_once('UserMailer.php');
+
/**
* Class to both describe a background job and handle jobs.
*/
@@ -37,6 +39,46 @@ abstract class Job {
*/
/**
+ * Pop a job of a certain type. This tries less hard than pop() to
+ * actually find a job; it may be adversely affected by concurrent job
+ * runners.
+ */
+ static function pop_type($type) {
+ wfProfilein( __METHOD__ );
+
+ $dbw = wfGetDB( DB_MASTER );
+
+
+ $row = $dbw->selectRow( 'job', '*', array( 'job_cmd' => $type ), __METHOD__,
+ array( 'LIMIT' => 1 ));
+
+ if ($row === false) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ /* Ensure we "own" this row */
+ $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
+ $affected = $dbw->affectedRows();
+
+ if ($affected == 0) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ $namespace = $row->job_namespace;
+ $dbkey = $row->job_title;
+ $title = Title::makeTitleSafe( $namespace, $dbkey );
+ $job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), $row->job_id );
+
+ $dbw->delete( 'job', $job->insertFields(), __METHOD__ );
+ $dbw->immediateCommit();
+
+ wfProfileOut( __METHOD__ );
+ return $job;
+ }
+
+ /**
* Pop a job off the front of the queue
* @static
* @param $offset Number of jobs to skip
@@ -125,20 +167,23 @@ abstract class Job {
}
/**
- * Create an object of a subclass
+ * Create the appropriate object to handle a specific job
+ *
+ * @param string $command Job command
+ * @param Title $title Associated title
+ * @param array $params Job parameters
+ * @param int $id Job identifier
+ * @return Job
*/
static function factory( $command, $title, $params = false, $id = 0 ) {
- switch ( $command ) {
- case 'refreshLinks':
- return new RefreshLinksJob( $title, $params, $id );
- case 'htmlCacheUpdate':
- case 'html_cache_update': # BC
- return new HTMLCacheUpdateJob( $title, $params['table'], $params['start'], $params['end'], $id );
- default:
- throw new MWException( "Invalid job command \"$command\"" );
+ global $wgJobClasses;
+ if( isset( $wgJobClasses[$command] ) ) {
+ $class = $wgJobClasses[$command];
+ return new $class( $title, $params, $id );
}
+ throw new MWException( "Invalid job command `{$command}`" );
}
-
+
static function makeBlob( $params ) {
if ( $params !== false ) {
return serialize( $params );
@@ -245,50 +290,3 @@ abstract class Job {
}
}
-
-/**
- * Background job to update links for a given title.
- */
-class RefreshLinksJob extends Job {
- function __construct( $title, $params = '', $id = 0 ) {
- parent::__construct( 'refreshLinks', $title, $params, $id );
- }
-
- /**
- * Run a refreshLinks job
- * @return boolean success
- */
- function run() {
- global $wgParser;
- wfProfileIn( __METHOD__ );
-
- $linkCache =& LinkCache::singleton();
- $linkCache->clear();
-
- if ( is_null( $this->title ) ) {
- $this->error = "refreshLinks: Invalid title";
- wfProfileOut( __METHOD__ );
- return false;
- }
-
- $revision = Revision::newFromTitle( $this->title );
- if ( !$revision ) {
- $this->error = 'refreshLinks: Article not found "' . $this->title->getPrefixedDBkey() . '"';
- wfProfileOut( __METHOD__ );
- return false;
- }
-
- wfProfileIn( __METHOD__.'-parse' );
- $options = new ParserOptions;
- $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, true, true, $revision->getId() );
- wfProfileOut( __METHOD__.'-parse' );
- wfProfileIn( __METHOD__.'-update' );
- $update = new LinksUpdate( $this->title, $parserOutput, false );
- $update->doUpdate();
- wfProfileOut( __METHOD__.'-update' );
- wfProfileOut( __METHOD__ );
- return true;
- }
-}
-
-?>
diff --git a/includes/Licenses.php b/includes/Licenses.php
index f4586ae5..6a034468 100644
--- a/includes/Licenses.php
+++ b/includes/Licenses.php
@@ -172,4 +172,4 @@ class License {
$this->text = strrev( $text );
}
}
-?>
+
diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php
index 065c540a..8ab3393e 100644
--- a/includes/LinkBatch.php
+++ b/includes/LinkBatch.php
@@ -3,7 +3,7 @@
/**
* Class representing a list of titles
* The execute() method checks them all for existence and adds them to a LinkCache object
- +
+ *
* @addtogroup Cache
*/
class LinkBatch {
@@ -156,19 +156,26 @@ class LinkBatch {
} else {
$sql .= ' OR ';
}
- $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title IN (";
-
- $firstTitle = true;
- foreach( $dbkeys as $dbkey => $unused ) {
- if ( $firstTitle ) {
- $firstTitle = false;
- } else {
- $sql .= ',';
+
+ if (count($dbkeys)==1) { // avoid multiple-reference syntax if simple equality can be used
+
+ $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title=".
+ $db->addQuotes(current(array_keys($dbkeys))).
+ ")";
+ } else {
+ $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title IN (";
+
+ $firstTitle = true;
+ foreach( $dbkeys as $dbkey => $unused ) {
+ if ( $firstTitle ) {
+ $firstTitle = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= $db->addQuotes( $dbkey );
}
- $sql .= $db->addQuotes( $dbkey );
+ $sql .= '))';
}
-
- $sql .= '))';
}
if ( $first && $firstTitle ) {
# No titles added
@@ -179,4 +186,4 @@ class LinkBatch {
}
}
-?>
+
diff --git a/includes/LinkCache.php b/includes/LinkCache.php
index 53fb640a..7c49d88e 100644
--- a/includes/LinkCache.php
+++ b/includes/LinkCache.php
@@ -169,4 +169,4 @@ class LinkCache {
$this->mBadLinks = array();
}
}
-?>
+
diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php
index 39341d5d..ee668f08 100644
--- a/includes/LinkFilter.php
+++ b/includes/LinkFilter.php
@@ -105,4 +105,4 @@ class LinkFilter {
return $like;
}
}
-?>
+
diff --git a/includes/Linker.php b/includes/Linker.php
index b12e2ad0..9397b800 100644
--- a/includes/Linker.php
+++ b/includes/Linker.php
@@ -12,6 +12,12 @@
* @addtogroup Skins
*/
class Linker {
+
+ /**
+ * Flags for userToolLinks()
+ */
+ const TOOL_LINKS_NOBLOCK = 1;
+
function __construct() {}
/**
@@ -96,7 +102,7 @@ class Linker {
wfProfileIn( 'Linker::makeLink' );
$nt = Title::newFromText( $title );
if ($nt) {
- $result = $this->makeLinkObj( Title::newFromText( $title ), $text, $query, $trail );
+ $result = $this->makeLinkObj( $nt, $text, $query, $trail );
} else {
wfDebug( 'Invalid title passed to Linker::makeLink(): "'.$title."\"\n" );
$result = $text == "" ? $title : $text;
@@ -218,32 +224,39 @@ class Linker {
$retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix );
} else {
wfProfileIn( $fname.'-immediate' );
+
+ # Handles links to special pages wich do not exist in the database:
+ if( $nt->getNamespace() == NS_SPECIAL ) {
+ if( SpecialPage::exists( $nt->getDbKey() ) ) {
+ $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix );
+ } else {
+ $retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix );
+ }
+ wfProfileOut( $fname.'-immediate' );
+ wfProfileOut( $fname );
+ return $retVal;
+ }
+
# Work out link colour immediately
$aid = $nt->getArticleID() ;
if ( 0 == $aid ) {
$retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix );
} else {
- $threshold = $wgUser->getOption('stubthreshold') ;
- if ( $threshold > 0 ) {
- $dbr = wfGetDB( DB_SLAVE );
- $s = $dbr->selectRow(
- array( 'page' ),
- array( 'page_len',
- 'page_namespace',
- 'page_is_redirect' ),
- array( 'page_id' => $aid ), $fname ) ;
- if ( $s !== false ) {
- $size = $s->page_len;
- if ( $s->page_is_redirect OR $s->page_namespace != NS_MAIN ) {
- $size = $threshold*2 ; # Really big
- }
- } else {
- $size = $threshold*2 ; # Really big
+ $stub = false;
+ if ( $nt->isContentPage() ) {
+ $threshold = $wgUser->getOption('stubthreshold');
+ if ( $threshold > 0 ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $s = $dbr->selectRow(
+ array( 'page' ),
+ array( 'page_len',
+ 'page_is_redirect' ),
+ array( 'page_id' => $aid ), $fname ) ;
+ $stub = ( $s !== false && !$s->page_is_redirect &&
+ $s->page_len < $threshold );
}
- } else {
- $size = 1 ;
}
- if ( $size < $threshold ) {
+ if ( $stub ) {
$retVal = $this->makeStubLinkObj( $nt, $text, $query, $trail, $prefix );
} else {
$retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix );
@@ -324,7 +337,9 @@ class Linker {
$fname = 'Linker::makeBrokenLinkObj';
wfProfileIn( $fname );
- if ( '' == $query ) {
+ if( $nt->getNamespace() == NS_SPECIAL ) {
+ $q = $query;
+ } else if ( '' == $query ) {
$q = 'action=edit';
} else {
$q = 'action=edit&'.$query;
@@ -418,42 +433,118 @@ class Linker {
return $s;
}
- /** @todo document */
- function makeImageLinkObj( $nt, $label, $alt, $align = '', $params = array(), $framed = false,
- $thumb = false, $manual_thumb = '', $valign = '' )
+ /**
+ * Creates the HTML source for images
+ * @deprecated use makeImageLink2
+ *
+ * @param object $title
+ * @param string $label label text
+ * @param string $alt alt text
+ * @param string $align horizontal alignment: none, left, center, right)
+ * @param array $handlerParams Parameters to be passed to the media handler
+ * @param boolean $framed shows image in original size in a frame
+ * @param boolean $thumb shows image as thumbnail in a frame
+ * @param string $manualthumb image name for the manual thumbnail
+ * @param string $valign vertical alignment: baseline, sub, super, top, text-top, middle, bottom, text-bottom
+ * @return string
+ */
+ function makeImageLinkObj( $title, $label, $alt, $align = '', $handlerParams = array(), $framed = false,
+ $thumb = false, $manualthumb = '', $valign = '', $time = false )
{
- global $wgContLang, $wgUser, $wgThumbLimits;
-
- $img = new Image( $nt );
+ $frameParams = array( 'alt' => $alt, 'caption' => $label );
+ if ( $align ) {
+ $frameParams['align'] = $align;
+ }
+ if ( $framed ) {
+ $frameParams['framed'] = true;
+ }
+ if ( $thumb ) {
+ $frameParams['thumbnail'] = true;
+ }
+ if ( $manualthumb ) {
+ $frameParams['manualthumb'] = $manualthumb;
+ }
+ if ( $valign ) {
+ $frameParams['valign'] = $valign;
+ }
+ $file = wfFindFile( $title, $time );
+ return $this->makeImageLink2( $title, $file, $frameParams, $handlerParams );
+ }
- if ( !$img->allowInlineDisplay() && $img->exists() ) {
- return $this->makeKnownLinkObj( $nt );
+ /**
+ * Make an image link
+ * @param Title $title Title object
+ * @param File $file File object, or false if it doesn't exist
+ *
+ * @param array $frameParams Associative array of parameters external to the media handler.
+ * Boolean parameters are indicated by presence or absence, the value is arbitrary and
+ * will often be false.
+ * thumbnail If present, downscale and frame
+ * manualthumb Image name to use as a thumbnail, instead of automatic scaling
+ * framed Shows image in original size in a frame
+ * frameless Downscale but don't frame
+ * upright If present, tweak default sizes for portrait orientation
+ * upright_factor Fudge factor for "upright" tweak (default 0.75)
+ * border If present, show a border around the image
+ * align Horizontal alignment (left, right, center, none)
+ * valign Vertical alignment (baseline, sub, super, top, text-top, middle,
+ * bottom, text-bottom)
+ * alt Alternate text for image (i.e. alt attribute). Plain text.
+ * caption HTML for image caption.
+ *
+ * @param array $handlerParams Associative array of media handler parameters, to be passed
+ * to transform(). Typical keys are "width" and "page".
+ */
+ function makeImageLink2( Title $title, $file, $frameParams = array(), $handlerParams = array() ) {
+ global $wgContLang, $wgUser, $wgThumbLimits, $wgThumbUpright;
+ if ( $file && !$file->allowInlineDisplay() ) {
+ wfDebug( __METHOD__.': '.$title->getPrefixedDBkey()." does not allow inline display\n" );
+ return $this->makeKnownLinkObj( $title );
}
- $error = $prefix = $postfix = '';
- $page = isset( $params['page'] ) ? $params['page'] : false;
+ // Shortcuts
+ $fp =& $frameParams;
+ $hp =& $handlerParams;
- if ( 'center' == $align )
+ // Clean up parameters
+ $page = isset( $hp['page'] ) ? $hp['page'] : false;
+ if ( !isset( $fp['align'] ) ) $fp['align'] = '';
+ if ( !isset( $fp['alt'] ) ) $fp['alt'] = '';
+
+ $prefix = $postfix = '';
+
+ if ( 'center' == $fp['align'] )
{
$prefix = '<div class="center">';
$postfix = '</div>';
- $align = 'none';
+ $fp['align'] = 'none';
}
+ if ( $file && !isset( $hp['width'] ) ) {
+ $hp['width'] = $file->getWidth( $page );
- if ( !isset( $params['width'] ) ) {
- $params['width'] = $img->getWidth( $page );
- if( $thumb || $framed ) {
+ if( isset( $fp['thumbnail'] ) || isset( $fp['framed'] ) || isset( $fp['frameless'] ) || !$hp['width'] ) {
$wopt = $wgUser->getOption( 'thumbsize' );
if( !isset( $wgThumbLimits[$wopt] ) ) {
$wopt = User::getDefaultOption( 'thumbsize' );
}
- $params['width'] = min( $params['width'], $wgThumbLimits[$wopt] );
+ // Reduce width for upright images when parameter 'upright' is used
+ if ( isset( $fp['upright'] ) && $fp['upright'] == 0 ) {
+ $fp['upright'] = $wgThumbUpright;
+ }
+ // Use width which is smaller: real image width or user preference width
+ // For caching health: If width scaled down due to upright parameter, round to full __0 pixel to avoid the creation of a lot of odd thumbs
+ $prefWidth = isset( $fp['upright'] ) ?
+ round( $wgThumbLimits[$wopt] * $fp['upright'], -1 ) :
+ $wgThumbLimits[$wopt];
+ if ( $hp['width'] <= 0 || $prefWidth < $hp['width'] ) {
+ $hp['width'] = $prefWidth;
+ }
}
}
- if ( $thumb || $framed ) {
+ if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) ) {
# Create a thumbnail. Alignment depends on language
# writing direction, # right aligned for left-to-right-
@@ -462,159 +553,174 @@ class Linker {
#
# If thumbnail width has not been provided, it is set
# to the default user option as specified in Language*.php
- if ( $align == '' ) {
- $align = $wgContLang->isRTL() ? 'left' : 'right';
+ if ( $fp['align'] == '' ) {
+ $fp['align'] = $wgContLang->isRTL() ? 'left' : 'right';
}
- return $prefix.$this->makeThumbLinkObj( $img, $label, $alt, $align, $params, $framed, $manual_thumb ).$postfix;
+ return $prefix.$this->makeThumbLink2( $title, $file, $fp, $hp ).$postfix;
}
- if ( $params['width'] && $img->exists() ) {
+ if ( $file && $hp['width'] ) {
# Create a resized image, without the additional thumbnail features
- $thumb = $img->transform( $params );
+ $thumb = $file->transform( $hp );
} else {
$thumb = false;
}
- if ( $page ) {
- $query = 'page=' . urlencode( $page );
- } else {
- $query = '';
- }
- $u = $nt->getLocalURL( $query );
- $imgAttribs = array(
- 'alt' => $alt,
- 'longdesc' => $u
- );
- if ( $valign ) {
- $imgAttribs['style'] = "vertical-align: $valign";
- }
- $linkAttribs = array(
- 'href' => $u,
- 'class' => 'image',
- 'title' => $alt
- );
-
if ( !$thumb ) {
- $s = $this->makeBrokenImageLinkObj( $img->getTitle() );
+ $s = $this->makeBrokenImageLinkObj( $title );
} else {
- $s = $thumb->toHtml( $imgAttribs, $linkAttribs );
+ $s = $thumb->toHtml( array(
+ 'desc-link' => true,
+ 'alt' => $fp['alt'],
+ 'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false ,
+ 'img-class' => isset( $fp['border'] ) ? 'thumbborder' : false ) );
}
- if ( '' != $align ) {
- $s = "<div class=\"float{$align}\"><span>{$s}</span></div>";
+ if ( '' != $fp['align'] ) {
+ $s = "<div class=\"float{$fp['align']}\"><span>{$s}</span></div>";
}
return str_replace("\n", ' ',$prefix.$s.$postfix);
}
/**
* Make HTML for a thumbnail including image, border and caption
- * $img is an Image object
+ * @param Title $title
+ * @param File $file File object or false if it doesn't exist
*/
- function makeThumbLinkObj( $img, $label = '', $alt, $align = 'right', $params = array(), $framed=false , $manual_thumb = "" ) {
+ function makeThumbLinkObj( Title $title, $file, $label = '', $alt, $align = 'right', $params = array(), $framed=false , $manualthumb = "" ) {
+ $frameParams = array(
+ 'alt' => $alt,
+ 'caption' => $label,
+ 'align' => $align
+ );
+ if ( $framed ) $frameParams['framed'] = true;
+ if ( $manualthumb ) $frameParams['manualthumb'] = $manualthumb;
+ return $this->makeThumbLink2( $title, $file, $frameParams, $params );
+ }
+
+ function makeThumbLink2( Title $title, $file, $frameParams = array(), $handlerParams = array() ) {
global $wgStylePath, $wgContLang;
- $thumbUrl = '';
- $error = '';
+ $exists = $file && $file->exists();
+
+ # Shortcuts
+ $fp =& $frameParams;
+ $hp =& $handlerParams;
- $page = isset( $params['page'] ) ? $params['page'] : false;
+ $page = isset( $hp['page'] ) ? $hp['page'] : false;
+ if ( !isset( $fp['align'] ) ) $fp['align'] = 'right';
+ if ( !isset( $fp['alt'] ) ) $fp['alt'] = '';
+ if ( !isset( $fp['caption'] ) ) $fp['caption'] = '';
- if ( empty( $params['width'] ) ) {
- $params['width'] = 180;
+ if ( empty( $hp['width'] ) ) {
+ // Reduce width for upright images when parameter 'upright' is used
+ $hp['width'] = isset( $fp['upright'] ) ? 130 : 180;
}
$thumb = false;
- if ( $manual_thumb != '' ) {
- # Use manually specified thumbnail
- $manual_title = Title::makeTitleSafe( NS_IMAGE, $manual_thumb );
- if( $manual_title ) {
- $manual_img = new Image( $manual_title );
- $thumb = $manual_img->getUnscaledThumb();
- }
- } elseif ( $framed ) {
- // Use image dimensions, don't scale
- $thumb = $img->getUnscaledThumb( $page );
- } else {
- $thumb = $img->transform( $params );
- }
- if ( $thumb ) {
- $outerWidth = $thumb->getWidth() + 2;
+ if ( !$exists ) {
+ $outerWidth = $hp['width'] + 2;
} else {
- $outerWidth = $params['width'] + 2;
+ if ( isset( $fp['manualthumb'] ) ) {
+ # Use manually specified thumbnail
+ $manual_title = Title::makeTitleSafe( NS_IMAGE, $fp['manualthumb'] );
+ if( $manual_title ) {
+ $manual_img = wfFindFile( $manual_title );
+ if ( $manual_img ) {
+ $thumb = $manual_img->getUnscaledThumb();
+ } else {
+ $exists = false;
+ }
+ }
+ } elseif ( isset( $fp['framed'] ) ) {
+ // Use image dimensions, don't scale
+ $thumb = $file->getUnscaledThumb( $page );
+ } else {
+ # Do not present an image bigger than the source, for bitmap-style images
+ # This is a hack to maintain compatibility with arbitrary pre-1.10 behaviour
+ $srcWidth = $file->getWidth( $page );
+ if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
+ $hp['width'] = $srcWidth;
+ }
+ $thumb = $file->transform( $hp );
+ }
+
+ if ( $thumb ) {
+ $outerWidth = $thumb->getWidth() + 2;
+ } else {
+ $outerWidth = $hp['width'] + 2;
+ }
}
$query = $page ? 'page=' . urlencode( $page ) : '';
- $u = $img->getTitle()->getLocalURL( $query );
+ $url = $title->getLocalURL( $query );
$more = htmlspecialchars( wfMsg( 'thumbnail-more' ) );
$magnifyalign = $wgContLang->isRTL() ? 'left' : 'right';
$textalign = $wgContLang->isRTL() ? ' style="text-align:right"' : '';
- $s = "<div class=\"thumb t{$align}\"><div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
- if ( !$thumb ) {
- $s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) );
+ $s = "<div class=\"thumb t{$fp['align']}\"><div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
+ if( !$exists ) {
+ $s .= $this->makeBrokenImageLinkObj( $title );
$zoomicon = '';
- } elseif( !$img->exists() ) {
- $s .= $this->makeBrokenImageLinkObj( $img->getTitle() );
+ } elseif ( !$thumb ) {
+ $s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) );
$zoomicon = '';
} else {
- $imgAttribs = array(
- 'alt' => $alt,
- 'longdesc' => $u,
- 'class' => 'thumbimage'
- );
- $linkAttribs = array(
- 'href' => $u,
- 'class' => 'internal',
- 'title' => $alt
- );
-
- $s .= $thumb->toHtml( $imgAttribs, $linkAttribs );
- if ( $framed ) {
+ $s .= $thumb->toHtml( array(
+ 'alt' => $fp['alt'],
+ 'img-class' => 'thumbimage',
+ 'desc-link' => true ) );
+ if ( isset( $fp['framed'] ) ) {
$zoomicon="";
} else {
$zoomicon = '<div class="magnify" style="float:'.$magnifyalign.'">'.
- '<a href="'.$u.'" class="internal" title="'.$more.'">'.
+ '<a href="'.$url.'" class="internal" title="'.$more.'">'.
'<img src="'.$wgStylePath.'/common/images/magnify-clip.png" ' .
'width="15" height="11" alt="" /></a></div>';
}
}
- $s .= ' <div class="thumbcaption"'.$textalign.'>'.$zoomicon.$label."</div></div></div>";
+ $s .= ' <div class="thumbcaption"'.$textalign.'>'.$zoomicon.$fp['caption']."</div></div></div>";
return str_replace("\n", ' ', $s);
}
/**
- * Pass a title object, not a title string
+ * Make a "broken" link to an image
+ *
+ * @param Title $title Image title
+ * @param string $text Link label
+ * @param string $query Query string
+ * @param string $trail Link trail
+ * @param string $prefix Link prefix
+ * @return string
*/
- function makeBrokenImageLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
- # Fail gracefully
- if ( ! isset($nt) ) {
- # throw new MWException();
+ public function makeBrokenImageLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ global $wgEnableUploads;
+ if( $title instanceof Title ) {
+ wfProfileIn( __METHOD__ );
+ if( $wgEnableUploads ) {
+ $upload = SpecialPage::getTitleFor( 'Upload' );
+ if( $text == '' )
+ $text = htmlspecialchars( $title->getPrefixedText() );
+ $q = 'wpDestFile=' . $title->getPartialUrl();
+ if( $query != '' )
+ $q .= '&' . $query;
+ list( $inside, $trail ) = self::splitTrail( $trail );
+ $style = $this->getInternalLinkAttributesObj( $title, $text, 'yes' );
+ wfProfileOut( __METHOD__ );
+ return '<a href="' . $upload->escapeLocalUrl( $q ) . '"'
+ . $style . '>' . $prefix . $text . $inside . '</a>' . $trail;
+ } else {
+ wfProfileOut( __METHOD__ );
+ return $this->makeKnownLinkObj( $title, $text, $query, $trail, $prefix );
+ }
+ } else {
return "<!-- ERROR -->{$prefix}{$text}{$trail}";
}
-
- $fname = 'Linker::makeBrokenImageLinkObj';
- wfProfileIn( $fname );
-
- $q = 'wpDestFile=' . urlencode( $nt->getDBkey() );
- if ( '' != $query ) {
- $q .= "&$query";
- }
- $uploadTitle = SpecialPage::getTitleFor( 'Upload' );
- $url = $uploadTitle->escapeLocalURL( $q );
-
- if ( '' == $text ) {
- $text = htmlspecialchars( $nt->getPrefixedText() );
- }
- $style = $this->getInternalLinkAttributesObj( $nt, $text, "yes" );
- list( $inside, $trail ) = Linker::splitTrail( $trail );
- $s = "<a href=\"{$url}\"{$style}>{$prefix}{$text}{$inside}</a>{$trail}";
-
- wfProfileOut( $fname );
- return $s;
}
- /** @todo document */
- function makeMediaLink( $name, /* wtf?! */ $url, $alt = '' ) {
+ /** @deprecated use Linker::makeMediaLinkObj() */
+ function makeMediaLink( $name, $unused = '', $text = '' ) {
$nt = Title::makeTitleSafe( NS_IMAGE, $name );
- return $this->makeMediaLinkObj( $nt, $alt );
+ return $this->makeMediaLinkObj( $nt, $text );
}
/**
@@ -632,13 +738,13 @@ class Linker {
### HOTFIX. Instead of breaking, return empty string.
return $text;
} else {
- $img = new Image( $title );
- if( $img->exists() ) {
+ $img = wfFindFile( $title );
+ if( $img ) {
$url = $img->getURL();
$class = 'internal';
} else {
$upload = SpecialPage::getTitleFor( 'Upload' );
- $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $img->getName() ) );
+ $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $title->getDbKey() ) );
$class = 'new';
}
$alt = htmlspecialchars( $title->getText() );
@@ -694,15 +800,18 @@ class Linker {
}
/**
- * @param $userId Integer: user id in database.
- * @param $userText String: user name in database.
- * @param $redContribsWhenNoEdits Bool: return a red contribs link when the user had no edits and this is true.
- * @return string HTML fragment with talk and/or block links
+ * Generate standard user tool links (talk, contributions, block link, etc.)
+ *
+ * @param int $userId User identifier
+ * @param string $userText User name or IP address
+ * @param bool $redContribsWhenNoEdits Should the contributions link be red if the user has no edits?
+ * @param int $flags Customisation flags (e.g. self::TOOL_LINKS_NOBLOCK)
+ * @return string
*/
- public function userToolLinks( $userId, $userText, $redContribsWhenNoEdits = false ) {
+ public function userToolLinks( $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0 ) {
global $wgUser, $wgDisableAnonTalk, $wgSysopUserBans;
$talkable = !( $wgDisableAnonTalk && 0 == $userId );
- $blockable = ( $wgSysopUserBans || 0 == $userId );
+ $blockable = ( $wgSysopUserBans || 0 == $userId ) && !$flags & self::TOOL_LINKS_NOBLOCK;
$items = array();
if( $talkable ) {
@@ -711,7 +820,7 @@ class Linker {
if( $userId ) {
// check if the user has an edit
if( $redContribsWhenNoEdits && User::edits( $userId ) == 0 ) {
- $style = "class='new'";
+ $style = " class='new'";
} else {
$style = '';
}
@@ -818,16 +927,33 @@ class Linker {
function formatComment($comment, $title = NULL, $local = false) {
wfProfileIn( __METHOD__ );
- global $wgContLang;
+ # Sanitize text a bit:
$comment = str_replace( "\n", " ", $comment );
$comment = htmlspecialchars( $comment );
- # The pattern for autogen comments is / * foo * /, which makes for
- # some nasty regex.
- # We look for all comments, match any text before and after the comment,
- # add a separator where needed and format the comment itself with CSS
+ # Render autocomments and make links:
+ $comment = $this->formatAutoComments( $comment, $title, $local );
+ $comment = $this->formatLinksInComment( $comment );
+
+ wfProfileOut( __METHOD__ );
+ return $comment;
+ }
+
+ /**
+ * The pattern for autogen comments is / * foo * /, which makes for
+ * some nasty regex.
+ * We look for all comments, match any text before and after the comment,
+ * add a separator where needed and format the comment itself with CSS
+ * Called by Linker::formatComment.
+ *
+ * @param $comment Comment text
+ * @param $title An optional title object used to links to sections
+ *
+ * @todo Document the $local parameter.
+ */
+ private function formatAutocomments( $comment, $title = NULL, $local = false ) {
$match = array();
- while (preg_match('/(.*)\/\*\s*(.*?)\s*\*\/(.*)/', $comment,$match)) {
+ while (preg_match('!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment,$match)) {
$pre=$match[1];
$auto=$match[2];
$post=$match[3];
@@ -859,10 +985,23 @@ class Linker {
$comment=$pre.$auto.$post;
}
- # format regular and media links - all other wiki formatting
- # is ignored
+ return $comment;
+ }
+
+ /**
+ * Formats wiki links and media links in text; all other wiki formatting
+ * is ignored
+ *
+ * @param string $comment Text to format links in
+ * @return string
+ */
+ public function formatLinksInComment( $comment ) {
+ global $wgContLang;
+
$medians = '(?:' . preg_quote( Namespace::getCanonicalName( NS_MEDIA ), '/' ) . '|';
$medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):';
+
+ $match = array();
while(preg_match('/\[\[:?(.*?)(\|(.*?))*\]\](.*)$/',$comment,$match)) {
# Handle link renaming [[foo|text]] will show link as "text"
if( "" != $match[3] ) {
@@ -889,7 +1028,7 @@ class Linker {
}
$comment = preg_replace( $linkRegexp, StringUtils::escapeRegexReplacement( $thelink ), $comment, 1 );
}
- wfProfileOut( __METHOD__ );
+
return $comment;
}
@@ -914,7 +1053,7 @@ class Linker {
return " <span class=\"comment\">($formatted)</span>";
}
}
-
+
/**
* Wrap and format the given revision's comment block, if the current
* user is allowed to view it.
@@ -981,31 +1120,66 @@ class Linker {
. "</script>\n";
}
- /** @todo document */
+ /**
+ * Used to generate section edit links that point to "other" pages
+ * (sections that are really part of included pages).
+ *
+ * @param $title Title string.
+ * @param $section Integer: section number.
+ */
public function editSectionLinkForOther( $title, $section ) {
- global $wgContLang;
-
$title = Title::newFromText( $title );
- $editurl = '&section='.$section;
- $url = $this->makeKnownLinkObj( $title, wfMsg('editsection'), 'action=edit'.$editurl );
-
- return "<span class=\"editsection\">[".$url."]</span>";
-
+ return $this->doEditSectionLink( $title, $section, '', 'EditSectionLinkForOther' );
}
/**
- * @param $title Title object.
+ * @param $nt Title object.
* @param $section Integer: section number.
* @param $hint Link String: title, or default if omitted or empty
*/
- public function editSectionLink( $nt, $section, $hint='' ) {
- global $wgContLang;
+ public function editSectionLink( Title $nt, $section, $hint='' ) {
+ if( $hint != '' ) {
+ $hint = wfMsgHtml( 'editsectionhint', htmlspecialchars( $hint ) );
+ $hint = " title=\"$hint\"";
+ }
+ return $this->doEditSectionLink( $nt, $section, $hint, 'EditSectionLink' );
+ }
+ /**
+ * Implement editSectionLink and editSectionLinkForOther.
+ *
+ * @param $nt Title object
+ * @param $section Integer, section number
+ * @param $hint String, for HTML title attribute
+ * @param $hook String, name of hook to run
+ * @return String, HTML to use for edit link
+ */
+ protected function doEditSectionLink( Title $nt, $section, $hint, $hook ) {
+ global $wgContLang;
$editurl = '&section='.$section;
- $hint = ( $hint=='' ) ? '' : ' title="' . wfMsgHtml( 'editsectionhint', htmlspecialchars( $hint ) ) . '"';
- $url = $this->makeKnownLinkObj( $nt, wfMsg('editsection'), 'action=edit'.$editurl, '', '', '', $hint );
+ $url = $this->makeKnownLinkObj(
+ $nt,
+ wfMsg('editsection'),
+ 'action=edit'.$editurl,
+ '', '', '', $hint
+ );
+ $result = null;
- return "<span class=\"editsection\">[".$url."]</span>";
+ // The two hooks have slightly different interfaces . . .
+ if( $hook == 'EditSectionLink' ) {
+ wfRunHooks( $hook, array( &$this, $nt, $section, $hint, $url, &$result ) );
+ } elseif( $hook == 'EditSectionLinkForOther' ) {
+ wfRunHooks( $hook, array( &$this, $nt, $section, $url, &$result ) );
+ }
+
+ // For reverse compatibility, add the brackets *after* the hook is run,
+ // and even add them to hook-provided text.
+ if( is_null( $result ) ) {
+ $result = wfMsg( 'editsection-brackets', $url );
+ } else {
+ $result = wfMsg( 'editsection-brackets', $result );
+ }
+ return "<span class=\"editsection\">$result</span>";
}
/**
@@ -1061,15 +1235,28 @@ class Linker {
* @param Revision $rev
*/
function generateRollback( $rev ) {
- global $wgUser, $wgRequest;
+ return '<span class="mw-rollback-link">['
+ . $this->buildRollbackLink( $rev )
+ . ']</span>';
+ }
+
+ /**
+ * Build a raw rollback link, useful for collections of "tool" links
+ *
+ * @param Revision $rev
+ * @return string
+ */
+ public function buildRollbackLink( $rev ) {
+ global $wgRequest, $wgUser;
$title = $rev->getTitle();
-
- $extraRollback = $wgRequest->getBool( 'bot' ) ? '&bot=1' : '';
- $extraRollback .= '&token=' . urlencode(
- $wgUser->editToken( array( $title->getPrefixedText(), $rev->getUserText() ) ) );
- return '<span class="mw-rollback-link">['. $this->makeKnownLinkObj( $title,
- wfMsg('rollbacklink'),
- 'action=rollback&from=' . urlencode( $rev->getUserText() ) . $extraRollback ) .']</span>';
+ $extra = $wgRequest->getBool( 'bot' ) ? '&bot=1' : '';
+ $extra .= '&token=' . urlencode( $wgUser->editToken( array( $title->getPrefixedText(),
+ $rev->getUserText() ) ) );
+ return $this->makeKnownLinkObj(
+ $title,
+ wfMsgHtml( 'rollbacklink' ),
+ 'action=rollback&from=' . urlencode( $rev->getUserText() ) . $extra
+ );
}
/**
@@ -1133,28 +1320,7 @@ class Linker {
*/
public function formatSize( $size ) {
global $wgLang;
- // For small sizes no decimal places necessary
- $round = 0;
- if( $size > 1024 ) {
- $size = $size / 1024;
- if( $size > 1024 ) {
- $size = $size / 1024;
- // For MB and bigger two decimal places are smarter
- $round = 2;
- if( $size > 1024 ) {
- $size = $size / 1024;
- $msg = 'size-gigabytes';
- } else {
- $msg = 'size-megabytes';
- }
- } else {
- $msg = 'size-kilobytes';
- }
- } else {
- $msg = 'size-bytes';
- }
- $size = round( $size, $round );
- return wfMsgHtml( $msg, $wgLang->formatNum( $size ) );
+ return htmlspecialchars( $wgLang->formatSize( $size ) );
}
/**
@@ -1208,4 +1374,6 @@ class Linker {
}
}
-?>
+
+
+
diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php
index 856c665d..9bcd9d67 100644
--- a/includes/LinksUpdate.php
+++ b/includes/LinksUpdate.php
@@ -9,7 +9,7 @@ class LinksUpdate {
/**@{{
* @private
*/
- var $mId, //!< Page ID of the article linked from
+ var $mId, //!< Page ID of the article linked from
$mTitle, //!< Title object of the article linked from
$mLinks, //!< Map of title strings to IDs for the links in the document
$mImages, //!< DB keys of the images used, in the array key only
@@ -24,10 +24,10 @@ class LinksUpdate {
/**
* Constructor
- * Initialize private variables
- * @param $title Integer: FIXME
- * @param $parserOutput FIXME
- * @param $recursive Boolean: FIXME, default 'true'.
+ *
+ * @param Title $title Title of the page we're updating
+ * @param ParserOutput $parserOutput Output from a full parse of this page
+ * @param bool $recursive Queue jobs for recursive updates?
*/
function LinksUpdate( $title, $parserOutput, $recursive = true ) {
global $wgAntiLockFlags;
@@ -64,6 +64,8 @@ class LinksUpdate {
}
$this->mRecursive = $recursive;
+
+ wfRunHooks( 'LinksUpdateConstructed', array( &$this ) );
}
/**
@@ -188,7 +190,7 @@ class LinksUpdate {
break;
}
$title = Title::makeTitle( $row->page_namespace, $row->page_title );
- $jobs[] = Job::factory( 'refreshLinks', $title );
+ $jobs[] = new RefreshLinksJob( $title, '' );
}
Job::batchInsert( $jobs );
}
@@ -594,4 +596,4 @@ class LinksUpdate {
return $arr;
}
}
-?>
+
diff --git a/includes/LoadBalancer.php b/includes/LoadBalancer.php
index 4ebe26c7..65a6d5a6 100644
--- a/includes/LoadBalancer.php
+++ b/includes/LoadBalancer.php
@@ -646,4 +646,4 @@ class LoadBalancer {
}
}
-?>
+
diff --git a/includes/LogPage.php b/includes/LogPage.php
index af03bbba..8982b59f 100644
--- a/includes/LogPage.php
+++ b/includes/LogPage.php
@@ -55,42 +55,52 @@ class LogPage {
$dbw = wfGetDB( DB_MASTER );
$uid = $wgUser->getID();
+ $log_id = $dbw->nextSequenceValue( 'log_log_id_seq' );
$this->timestamp = $now = wfTimestampNow();
- $dbw->insert( 'logging',
- array(
- 'log_type' => $this->type,
- 'log_action' => $this->action,
- 'log_timestamp' => $dbw->timestamp( $now ),
- 'log_user' => $uid,
- 'log_namespace' => $this->target->getNamespace(),
- 'log_title' => $this->target->getDBkey(),
- 'log_comment' => $this->comment,
- 'log_params' => $this->params
- ), $fname
+ $data = array(
+ 'log_type' => $this->type,
+ 'log_action' => $this->action,
+ 'log_timestamp' => $dbw->timestamp( $now ),
+ 'log_user' => $uid,
+ 'log_namespace' => $this->target->getNamespace(),
+ 'log_title' => $this->target->getDBkey(),
+ 'log_comment' => $this->comment,
+ 'log_params' => $this->params
);
+ # log_id doesn't exist on Wikimedia servers yet, and it's a tricky
+ # schema update to do. Hack it for now to ignore the field on MySQL.
+ if ( !is_null( $log_id ) ) {
+ $data['log_id'] = $log_id;
+ }
+ $dbw->insert( 'logging', $data, $fname );
+
# And update recentchanges
if ( $this->updateRecentChanges ) {
$titleObj = SpecialPage::getTitleFor( 'Log', $this->type );
- $rcComment = $this->actionText;
- if( '' != $this->comment ) {
- if ($rcComment == '')
- $rcComment = $this->comment;
- else
- $rcComment .= ': ' . $this->comment;
- }
-
+ $rcComment = $this->getRcComment();
RecentChange::notifyLog( $now, $titleObj, $wgUser, $rcComment, '',
$this->type, $this->action, $this->target, $this->comment, $this->params );
}
return true;
}
+ public function getRcComment() {
+ $rcComment = $this->actionText;
+ if( '' != $this->comment ) {
+ if ($rcComment == '')
+ $rcComment = $this->comment;
+ else
+ $rcComment .= ': ' . $this->comment;
+ }
+ return $rcComment;
+ }
+
/**
* @static
*/
- function validTypes() {
+ public static function validTypes() {
global $wgLogTypes;
return $wgLogTypes;
}
@@ -98,7 +108,7 @@ class LogPage {
/**
* @static
*/
- function isLogType( $type ) {
+ public static function isLogType( $type ) {
return in_array( $type, LogPage::validTypes() );
}
@@ -120,7 +130,7 @@ class LogPage {
* @todo handle missing log types
* @static
*/
- function logHeader( $type ) {
+ static function logHeader( $type ) {
global $wgLogHeaders;
return wfMsg( $wgLogHeaders[$type] );
}
@@ -128,7 +138,7 @@ class LogPage {
/**
* @static
*/
- function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks=false, $translate=false ) {
+ static function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks=false, $translate=false ) {
global $wgLang, $wgContLang, $wgLogActions;
$key = "$type/$action";
@@ -145,14 +155,17 @@ class LogPage {
switch( $type ) {
case 'move':
$titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' );
- $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), $params[0] );
+ $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) );
break;
case 'block':
if( substr( $title->getText(), 0, 1 ) == '#' ) {
$titleLink = $title->getText();
} else {
- $titleLink = $skin->makeLinkObj( $title, $title->getText() );
- $titleLink .= ' (' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $title->getDBkey() ), wfMsg( 'contribslink' ) ) . ')';
+ // TODO: Store the user identifier in the parameters
+ // to make this faster for future log entries
+ $id = User::idFromName( $title->getText() );
+ $titleLink = $skin->userLink( $id, $title->getText() )
+ . $skin->userToolLinks( $id, $title->getText(), false, Linker::TOOL_LINKS_NOBLOCK );
}
break;
case 'rights':
@@ -233,7 +246,7 @@ class LogPage {
* Create a blob from a parameter array
* @static
*/
- function makeParamBlob( $params ) {
+ static function makeParamBlob( $params ) {
return implode( "\n", $params );
}
@@ -241,7 +254,7 @@ class LogPage {
* Extract a parameter array from a blob
* @static
*/
- function extractParams( $blob ) {
+ static function extractParams( $blob ) {
if ( $blob === '' ) {
return array();
} else {
@@ -285,4 +298,4 @@ class LogPage {
}
-?>
+
diff --git a/includes/MacBinary.php b/includes/MacBinary.php
index 2f6ad4f4..da357e52 100644
--- a/includes/MacBinary.php
+++ b/includes/MacBinary.php
@@ -100,7 +100,7 @@ class MacBinary {
fseek( $this->handle, 0 );
$head = fread( $this->handle, 128 );
- $this->hexdump( $head );
+ #$this->hexdump( $head );
if( strlen( $head ) < 128 ) {
wfDebug( "$fname: couldn't read full MacBinary header\n" );
@@ -268,4 +268,4 @@ class MacBinary {
}
}
-?>
+
diff --git a/includes/MagicWord.php b/includes/MagicWord.php
index bf72a0c8..f7a9400d 100644
--- a/includes/MagicWord.php
+++ b/includes/MagicWord.php
@@ -86,7 +86,6 @@ class MagicWord {
'subjectpagename',
'subjectpagenamee',
'numberofusers',
- 'rawsuffix',
'newsectionlink',
'numberofpages',
'currentversion',
@@ -387,6 +386,181 @@ class MagicWord {
function isCaseSensitive() {
return $this->mCaseSensitive;
}
+
+ function getId() {
+ return $this->mId;
+ }
}
-?>
+/**
+ * Class for handling an array of magic words
+ */
+class MagicWordArray {
+ var $names = array();
+ var $hash;
+ var $baseRegex, $regex;
+
+ function __construct( $names = array() ) {
+ $this->names = $names;
+ }
+
+ /**
+ * Add a magic word by name
+ */
+ public function add( $name ) {
+ global $wgContLang;
+ $this->names[] = $name;
+ $this->hash = $this->baseRegex = $this->regex = null;
+ }
+
+ /**
+ * Add a number of magic words by name
+ */
+ public function addArray( $names ) {
+ $this->names = array_merge( $this->names, array_values( $names ) );
+ $this->hash = $this->baseRegex = $this->regex = null;
+ }
+
+ /**
+ * Get a 2-d hashtable for this array
+ */
+ function getHash() {
+ if ( is_null( $this->hash ) ) {
+ global $wgContLang;
+ $this->hash = array( 0 => array(), 1 => array() );
+ foreach ( $this->names as $name ) {
+ $magic = MagicWord::get( $name );
+ $case = intval( $magic->isCaseSensitive() );
+ foreach ( $magic->getSynonyms() as $syn ) {
+ if ( !$case ) {
+ $syn = $wgContLang->lc( $syn );
+ }
+ $this->hash[$case][$syn] = $name;
+ }
+ }
+ }
+ return $this->hash;
+ }
+
+ /**
+ * Get the base regex
+ */
+ function getBaseRegex() {
+ if ( is_null( $this->baseRegex ) ) {
+ $this->baseRegex = array( 0 => '', 1 => '' );
+ foreach ( $this->names as $name ) {
+ $magic = MagicWord::get( $name );
+ $case = intval( $magic->isCaseSensitive() );
+ foreach ( $magic->getSynonyms() as $i => $syn ) {
+ $group = "(?P<{$i}_{$name}>" . preg_quote( $syn, '/' ) . ')';
+ if ( $this->baseRegex[$case] === '' ) {
+ $this->baseRegex[$case] = $group;
+ } else {
+ $this->baseRegex[$case] .= '|' . $group;
+ }
+ }
+ }
+ }
+ return $this->baseRegex;
+ }
+
+ /**
+ * Get an unanchored regex
+ */
+ function getRegex() {
+ if ( is_null( $this->regex ) ) {
+ $base = $this->getBaseRegex();
+ $this->regex = array( '', '' );
+ if ( $this->baseRegex[0] !== '' ) {
+ $this->regex[0] = "/{$base[0]}/iuS";
+ }
+ if ( $this->baseRegex[1] !== '' ) {
+ $this->regex[1] = "/{$base[1]}/S";
+ }
+ }
+ return $this->regex;
+ }
+
+ /**
+ * Get a regex for matching variables
+ */
+ function getVariableRegex() {
+ return str_replace( "\\$1", "(.*?)", $this->getRegex() );
+ }
+
+ /**
+ * Get an anchored regex for matching variables
+ */
+ function getVariableStartToEndRegex() {
+ $base = $this->getBaseRegex();
+ $newRegex = array( '', '' );
+ if ( $base[0] !== '' ) {
+ $newRegex[0] = str_replace( "\\$1", "(.*?)", "/^(?:{$base[0]})$/iuS" );
+ }
+ if ( $base[1] !== '' ) {
+ $newRegex[1] = str_replace( "\\$1", "(.*?)", "/^(?:{$base[1]})$/S" );
+ }
+ return $newRegex;
+ }
+
+ /**
+ * Parse a match array from preg_match
+ */
+ function parseMatch( $m ) {
+ reset( $m );
+ while ( list( $key, $value ) = each( $m ) ) {
+ if ( $key === 0 || $value === '' ) {
+ continue;
+ }
+ $parts = explode( '_', $key, 2 );
+ if ( count( $parts ) != 2 ) {
+ // This shouldn't happen
+ // continue;
+ throw new MWException( __METHOD__ . ': bad parameter name' );
+ }
+ list( /* $synIndex */, $magicName ) = $parts;
+ $paramValue = next( $m );
+ return array( $magicName, $paramValue );
+ }
+ // This shouldn't happen either
+ throw new MWException( __METHOD__.': parameter not found' );
+ return array( false, false );
+ }
+
+ /**
+ * Match some text, with parameter capture
+ * Returns an array with the magic word name in the first element and the
+ * parameter in the second element.
+ * Both elements are false if there was no match.
+ */
+ public function matchVariableStartToEnd( $text ) {
+ global $wgContLang;
+ $regexes = $this->getVariableStartToEndRegex();
+ foreach ( $regexes as $regex ) {
+ if ( $regex !== '' ) {
+ $m = false;
+ if ( preg_match( $regex, $text, $m ) ) {
+ return $this->parseMatch( $m );
+ }
+ }
+ }
+ return array( false, false );
+ }
+
+ /**
+ * Match some text, without parameter capture
+ * Returns the magic word name, or false if there was no capture
+ */
+ public function matchStartToEnd( $text ) {
+ $hash = $this->getHash();
+ if ( isset( $hash[1][$text] ) ) {
+ return $hash[1][$text];
+ }
+ global $wgContLang;
+ $lc = $wgContLang->lc( $text );
+ if ( isset( $hash[0][$lc] ) ) {
+ return $hash[0][$lc];
+ }
+ return false;
+ }
+}
diff --git a/includes/Math.php b/includes/Math.php
index 88934e5f..2771d04c 100644
--- a/includes/Math.php
+++ b/includes/Math.php
@@ -20,8 +20,9 @@ class MathRenderer {
var $mathml = '';
var $conservativeness = 0;
- function __construct( $tex ) {
+ function __construct( $tex, $params=array() ) {
$this->tex = $tex;
+ $this->params = $params;
}
function setOutputMode( $mode ) {
@@ -157,8 +158,8 @@ class MathRenderer {
$dbw = wfGetDB( DB_MASTER );
$dbw->replace( 'math', array( 'math_inputhash' ),
array(
- 'math_inputhash' => $md5_sql,
- 'math_outputhash' => $outmd5_sql,
+ 'math_inputhash' => $dbw->encodeBlob($md5_sql),
+ 'math_outputhash' => $dbw->encodeBlob($outmd5_sql),
'math_html_conservativeness' => $this->conservativeness,
'math_html' => $this->html,
'math_mathml' => $this->mathml,
@@ -186,13 +187,13 @@ class MathRenderer {
$dbr = wfGetDB( DB_SLAVE );
$rpage = $dbr->selectRow( 'math',
array( 'math_outputhash','math_html_conservativeness','math_html','math_mathml' ),
- array( 'math_inputhash' => pack("H32", $this->md5)), # Binary packed, not hex
+ array( 'math_inputhash' => $dbr->encodeBlob(pack("H32", $this->md5))), # Binary packed, not hex
$fname
);
if( $rpage !== false ) {
# Tailing 0x20s can get dropped by the database, add it back on if necessary:
- $xhash = unpack( 'H32md5', $rpage->math_outputhash . " " );
+ $xhash = unpack( 'H32md5', $dbr->decodeBlob($rpage->math_outputhash) . " " );
$this->hash = $xhash ['md5'];
$this->conservativeness = $rpage->math_html_conservativeness;
@@ -233,24 +234,44 @@ class MathRenderer {
*/
function _doRender() {
if( $this->mode == MW_MATH_MATHML && $this->mathml != '' ) {
- return "<math xmlns='http://www.w3.org/1998/Math/MathML'>{$this->mathml}</math>";
+ return Xml::tags( 'math',
+ $this->_attribs( 'math',
+ array( 'xmlns' => 'http://www.w3.org/1998/Math/MathML' ) ),
+ $this->mathml );
}
if (($this->mode == MW_MATH_PNG) || ($this->html == '') ||
(($this->mode == MW_MATH_SIMPLE) && ($this->conservativeness != 2)) ||
(($this->mode == MW_MATH_MODERN || $this->mode == MW_MATH_MATHML) && ($this->conservativeness == 0))) {
return $this->_linkToMathImage();
} else {
- return '<span class="texhtml">'.$this->html.'</span>';
+ return Xml::tags( 'span',
+ $this->_attribs( 'span',
+ array( 'class' => 'texhtml' ) ),
+ $this->html );
}
}
+
+ function _attribs( $tag, $defaults=array(), $overrides=array() ) {
+ $attribs = Sanitizer::validateTagAttributes( $this->params, $tag );
+ $attribs = Sanitizer::mergeAttributes( $defaults, $attribs );
+ $attribs = Sanitizer::mergeAttributes( $attribs, $overrides );
+ return $attribs;
+ }
function _linkToMathImage() {
global $wgMathPath;
- $url = htmlspecialchars( "$wgMathPath/" . substr($this->hash, 0, 1)
+ $url = "$wgMathPath/" . substr($this->hash, 0, 1)
.'/'. substr($this->hash, 1, 1) .'/'. substr($this->hash, 2, 1)
- . "/{$this->hash}.png" );
- $alt = trim(str_replace("\n", ' ', htmlspecialchars( $this->tex )));
- return "<img class='tex' src=\"$url\" alt=\"$alt\" />";
+ . "/{$this->hash}.png";
+
+ return Xml::element( 'img',
+ $this->_attribs(
+ 'img',
+ array(
+ 'class' => 'tex',
+ 'alt' => $this->tex ),
+ array(
+ 'src' => $url ) ) );
}
function _getHashPath() {
@@ -262,11 +283,11 @@ class MathRenderer {
return $path;
}
- public static function renderMath( $tex ) {
+ public static function renderMath( $tex, $params=array() ) {
global $wgUser;
- $math = new MathRenderer( $tex );
+ $math = new MathRenderer( $tex, $params );
$math->setOutputMode( $wgUser->getOption('math'));
return $math->render();
}
}
-?>
+
diff --git a/includes/MediaTransformOutput.php b/includes/MediaTransformOutput.php
index 60057e3a..c6cf9ac2 100644
--- a/includes/MediaTransformOutput.php
+++ b/includes/MediaTransformOutput.php
@@ -1,11 +1,13 @@
<?php
/**
- * Base class for the output of MediaHandler::doTransform() and Image::transform().
+ * Base class for the output of MediaHandler::doTransform() and File::transform().
*
* @addtogroup Media
*/
abstract class MediaTransformOutput {
+ var $file, $width, $height, $url, $page, $path;
+
/**
* Get the width of the output box
*/
@@ -36,12 +38,23 @@ abstract class MediaTransformOutput {
/**
* Fetch HTML for this transform output
- * @param array $attribs Advisory associative array of HTML attributes supplied
- * by the linker. These can be incorporated into the output in any way.
- * @param array $linkAttribs Attributes of a suggested enclosing <a> tag.
- * May be ignored.
+ *
+ * @param array $options Associative array of options. Boolean options
+ * should be indicated with a value of true for true, and false or
+ * absent for false.
+ *
+ * alt Alternate text or caption
+ * desc-link Boolean, show a description link
+ * file-link Boolean, show a file download link
+ * valign vertical-align property, if the output is an inline element
+ * img-class Class applied to the <img> tag, if there is such a tag
+ *
+ * For images, desc-link and file-link are implemented as a click-through. For
+ * sounds and videos, they may be displayed in other ways.
+ *
+ * @return string
*/
- abstract function toHtml( $attribs = array() , $linkAttribs = false );
+ abstract function toHtml( $options = array() );
/**
* This will be overridden to return true in error classes
@@ -60,6 +73,19 @@ abstract class MediaTransformOutput {
return $contents;
}
}
+
+ function getDescLinkAttribs( $alt = false ) {
+ $query = $this->page ? ( 'page=' . urlencode( $this->page ) ) : '';
+ $title = $this->file->getTitle();
+ if ( strval( $alt ) === '' ) {
+ $alt = $title->getText();
+ }
+ return array(
+ 'href' => $this->file->getTitle()->getLocalURL( $query ),
+ 'class' => 'image',
+ 'title' => $alt
+ );
+ }
}
@@ -74,7 +100,8 @@ class ThumbnailImage extends MediaTransformOutput {
* @param string $url URL path to the thumb
* @private
*/
- function ThumbnailImage( $url, $width, $height, $path = false ) {
+ function ThumbnailImage( $file, $url, $width, $height, $path = false, $page = false ) {
+ $this->file = $file;
$this->url = $url;
# These should be integers when they get here.
# If not, there's a bug somewhere. But let's at
@@ -82,28 +109,56 @@ class ThumbnailImage extends MediaTransformOutput {
$this->width = round( $width );
$this->height = round( $height );
$this->path = $path;
+ $this->page = $page;
}
/**
* Return HTML <img ... /> tag for the thumbnail, will include
* width and height attributes and a blank alt text (as required).
+ *
+ * @param array $options Associative array of options. Boolean options
+ * should be indicated with a value of true for true, and false or
+ * absent for false.
*
- * You can set or override additional attributes by passing an
- * associative array of name => data pairs. The data will be escaped
- * for HTML output, so should be in plaintext.
+ * alt Alternate text or caption
+ * desc-link Boolean, show a description link
+ * file-link Boolean, show a file download link
+ * valign vertical-align property, if the output is an inline element
+ * img-class Class applied to the <img> tag, if there is such a tag
*
- * If $linkAttribs is given, the image will be enclosed in an <a> tag.
+ * For images, desc-link and file-link are implemented as a click-through. For
+ * sounds and videos, they may be displayed in other ways.
*
- * @param array $attribs
- * @param array $linkAttribs
* @return string
* @public
*/
- function toHtml( $attribs = array(), $linkAttribs = false ) {
- $attribs['src'] = $this->url;
- $attribs['width'] = $this->width;
- $attribs['height'] = $this->height;
- if( !isset( $attribs['alt'] ) ) $attribs['alt'] = '';
+ function toHtml( $options = array() ) {
+ if ( count( func_get_args() ) == 2 ) {
+ throw new MWException( __METHOD__ .' called in the old style' );
+ }
+
+ $alt = empty( $options['alt'] ) ? '' : $options['alt'];
+ if ( !empty( $options['desc-link'] ) ) {
+ $linkAttribs = $this->getDescLinkAttribs( $alt );
+ } elseif ( !empty( $options['file-link'] ) ) {
+ $linkAttribs = array( 'href' => $this->file->getURL() );
+ } else {
+ $linkAttribs = false;
+ }
+
+ $attribs = array(
+ 'alt' => $alt,
+ 'src' => $this->url,
+ 'width' => $this->width,
+ 'height' => $this->height,
+ 'border' => 0,
+ );
+ if ( !empty( $options['valign'] ) ) {
+ $attribs['style'] = "vertical-align: {$options['valign']}";
+ }
+ if ( !empty( $options['img-class'] ) ) {
+ $attribs['class'] = $options['img-class'];
+ }
return $this->linkWrap( $linkAttribs, Xml::element( 'img', $attribs ) );
}
@@ -130,7 +185,7 @@ class MediaTransformError extends MediaTransformOutput {
$this->path = false;
}
- function toHtml( $attribs = array(), $linkAttribs = false ) {
+ function toHtml( $options = array() ) {
return "<table class=\"MediaTransformError\" style=\"" .
"width: {$this->width}px; height: {$this->height}px;\"><tr><td>" .
$this->htmlMsg .
@@ -158,9 +213,10 @@ class MediaTransformError extends MediaTransformOutput {
class TransformParameterError extends MediaTransformError {
function __construct( $params ) {
parent::__construct( 'thumbnail_error',
- max( @$params['width'], 180 ), max( @$params['height'], 180 ),
+ max( isset( $params['width'] ) ? $params['width'] : 0, 180 ),
+ max( isset( $params['height'] ) ? $params['height'] : 0, 180 ),
wfMsg( 'thumbnail_invalid_params' ) );
}
}
-?>
+
diff --git a/includes/MemcachedSessions.php b/includes/MemcachedSessions.php
index 3bcf5535..3b248cf0 100644
--- a/includes/MemcachedSessions.php
+++ b/includes/MemcachedSessions.php
@@ -69,4 +69,4 @@ function memsess_gc( $maxlifetime ) {
session_set_save_handler( 'memsess_open', 'memsess_close', 'memsess_read', 'memsess_write', 'memsess_destroy', 'memsess_gc' );
-?>
+
diff --git a/includes/MessageCache.php b/includes/MessageCache.php
index e2cbf5f6..10c95a7e 100644
--- a/includes/MessageCache.php
+++ b/includes/MessageCache.php
@@ -23,6 +23,7 @@ class MessageCache {
var $mExtensionMessages = array();
var $mInitialised = false;
var $mDeferred = true;
+ var $mAllMessagesLoaded;
function __construct( &$memCached, $useDB, $expiry, $memcPrefix) {
wfProfileIn( __METHOD__ );
@@ -627,7 +628,6 @@ class MessageCache {
/**
* Add a 2-D array of messages by lang. Useful for extensions.
- * Introduced in 1.9. Please do not use it for now, for backwards compatibility.
*
* @param array $messages The array to be added
*/
@@ -670,12 +670,39 @@ class MessageCache {
}
}
- static function loadAllMessages() {
+ function loadAllMessages() {
+ global $wgExtensionMessagesFiles;
+ if ( $this->mAllMessagesLoaded ) {
+ return;
+ }
+ $this->mAllMessagesLoaded = true;
+
# Some extensions will load their messages when you load their class file
wfLoadAllExtensions();
# Others will respond to this hook
wfRunHooks( 'LoadAllMessages' );
+ # Some register their messages in $wgExtensionMessagesFiles
+ foreach ( $wgExtensionMessagesFiles as $name => $file ) {
+ if ( $file ) {
+ $this->loadMessagesFile( $file );
+ $wgExtensionMessagesFiles[$name] = false;
+ }
+ }
# Still others will respond to neither, they are EVIL. We sometimes need to know!
}
+
+ /**
+ * Load messages from a given file
+ */
+ function loadMessagesFile( $filename ) {
+ $magicWords = false;
+ require( $filename );
+ $this->addMessagesByLang( $messages );
+
+ if ( $magicWords !== false ) {
+ global $wgContLang;
+ $wgContLang->addMagicWordsByLang( $magicWords );
+ }
+ }
}
-?>
+
diff --git a/includes/Metadata.php b/includes/Metadata.php
index b995b223..f5b0b247 100644
--- a/includes/Metadata.php
+++ b/includes/Metadata.php
@@ -365,4 +365,4 @@ function getKnownLicenses() {
return $knownLicenses;
}
-?>
+
diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php
index db35535d..264a3595 100644
--- a/includes/MimeMagic.php
+++ b/includes/MimeMagic.php
@@ -111,56 +111,67 @@ class MimeMagic {
* --- load mime.types ---
*/
- global $wgMimeTypeFile;
+ global $wgMimeTypeFile, $IP;
- $types= MM_WELL_KNOWN_MIME_TYPES;
+ $types = MM_WELL_KNOWN_MIME_TYPES;
- if ($wgMimeTypeFile) {
- if (is_file($wgMimeTypeFile) and is_readable($wgMimeTypeFile)) {
- wfDebug("MimeMagic::MimeMagic: loading mime types from $wgMimeTypeFile\n");
-
- $types.= "\n";
- $types.= file_get_contents($wgMimeTypeFile);
+ if ( $wgMimeTypeFile == 'includes/mime.types' ) {
+ $wgMimeTypeFile = "$IP/$wgMimeTypeFile";
+ }
+
+ if ( $wgMimeTypeFile ) {
+ if ( is_file( $wgMimeTypeFile ) and is_readable( $wgMimeTypeFile ) ) {
+ wfDebug( __METHOD__.": loading mime types from $wgMimeTypeFile\n" );
+ $types .= "\n";
+ $types .= file_get_contents( $wgMimeTypeFile );
+ } else {
+ wfDebug( __METHOD__.": can't load mime types from $wgMimeTypeFile\n" );
}
- else wfDebug("MimeMagic::MimeMagic: can't load mime types from $wgMimeTypeFile\n");
+ } else {
+ wfDebug( __METHOD__.": no mime types file defined, using build-ins only.\n" );
}
- else wfDebug("MimeMagic::MimeMagic: no mime types file defined, using build-ins only.\n");
- $types= str_replace(array("\r\n","\n\r","\n\n","\r\r","\r"),"\n",$types);
- $types= str_replace("\t"," ",$types);
+ $types = str_replace( array( "\r\n", "\n\r", "\n\n", "\r\r", "\r" ), "\n", $types );
+ $types = str_replace( "\t", " ", $types );
- $this->mMimeToExt= array();
- $this->mToMime= array();
+ $this->mMimeToExt = array();
+ $this->mToMime = array();
- $lines= explode("\n",$types);
- foreach ($lines as $s) {
- $s= trim($s);
- if (empty($s)) continue;
- if (strpos($s,'#')===0) continue;
+ $lines = explode( "\n",$types );
+ foreach ( $lines as $s ) {
+ $s = trim( $s );
+ if ( empty( $s ) ) continue;
+ if ( strpos( $s, '#' ) === 0 ) continue;
- $s= strtolower($s);
- $i= strpos($s,' ');
+ $s = strtolower( $s );
+ $i = strpos( $s, ' ' );
- if ($i===false) continue;
+ if ( $i === false ) continue;
#print "processing MIME line $s<br>";
- $mime= substr($s,0,$i);
- $ext= trim(substr($s,$i+1));
+ $mime = substr( $s, 0, $i );
+ $ext = trim( substr($s, $i+1 ) );
- if (empty($ext)) continue;
+ if ( empty( $ext ) ) continue;
- if ( !empty($this->mMimeToExt[$mime])) $this->mMimeToExt[$mime] .= ' '.$ext;
- else $this->mMimeToExt[$mime]= $ext;
+ if ( !empty( $this->mMimeToExt[$mime] ) ) {
+ $this->mMimeToExt[$mime] .= ' ' . $ext;
+ } else {
+ $this->mMimeToExt[$mime] = $ext;
+ }
- $extensions= explode(' ',$ext);
+ $extensions = explode( ' ', $ext );
- foreach ($extensions as $e) {
- $e= trim($e);
- if (empty($e)) continue;
+ foreach ( $extensions as $e ) {
+ $e = trim( $e );
+ if ( empty( $e ) ) continue;
- if ( !empty($this->mExtToMime[$e])) $this->mExtToMime[$e] .= ' '.$mime;
- else $this->mExtToMime[$e]= $mime;
+ if ( !empty( $this->mExtToMime[$e] ) ) {
+ $this->mExtToMime[$e] .= ' ' . $mime;
+ } else {
+ $this->mExtToMime[$e] = $mime;
+ }
}
}
@@ -169,62 +180,69 @@ class MimeMagic {
*/
global $wgMimeInfoFile;
+ if ( $wgMimeInfoFile == 'includes/mime.info' ) {
+ $wgMimeInfoFile = "$IP/$wgMimeInfoFile";
+ }
- $info= MM_WELL_KNOWN_MIME_INFO;
-
- if ($wgMimeInfoFile) {
- if (is_file($wgMimeInfoFile) and is_readable($wgMimeInfoFile)) {
- wfDebug("MimeMagic::MimeMagic: loading mime info from $wgMimeInfoFile\n");
+ $info = MM_WELL_KNOWN_MIME_INFO;
- $info.= "\n";
- $info.= file_get_contents($wgMimeInfoFile);
+ if ( $wgMimeInfoFile ) {
+ if ( is_file( $wgMimeInfoFile ) and is_readable( $wgMimeInfoFile ) ) {
+ wfDebug( __METHOD__.": loading mime info from $wgMimeInfoFile\n" );
+ $info .= "\n";
+ $info .= file_get_contents( $wgMimeInfoFile );
+ } else {
+ wfDebug(__METHOD__.": can't load mime info from $wgMimeInfoFile\n");
}
- else wfDebug("MimeMagic::MimeMagic: can't load mime info from $wgMimeInfoFile\n");
+ } else {
+ wfDebug(__METHOD__.": no mime info file defined, using build-ins only.\n");
}
- else wfDebug("MimeMagic::MimeMagic: no mime info file defined, using build-ins only.\n");
- $info= str_replace(array("\r\n","\n\r","\n\n","\r\r","\r"),"\n",$info);
- $info= str_replace("\t"," ",$info);
+ $info = str_replace( array( "\r\n", "\n\r", "\n\n", "\r\r", "\r" ), "\n", $info);
+ $info = str_replace( "\t", " ", $info );
- $this->mMimeTypeAliases= array();
- $this->mMediaTypes= array();
+ $this->mMimeTypeAliases = array();
+ $this->mMediaTypes = array();
- $lines= explode("\n",$info);
- foreach ($lines as $s) {
- $s= trim($s);
- if (empty($s)) continue;
- if (strpos($s,'#')===0) continue;
+ $lines = explode( "\n", $info );
+ foreach ( $lines as $s ) {
+ $s = trim( $s );
+ if ( empty( $s ) ) continue;
+ if ( strpos( $s, '#' ) === 0 ) continue;
- $s= strtolower($s);
- $i= strpos($s,' ');
+ $s = strtolower( $s );
+ $i = strpos( $s, ' ' );
- if ($i===false) continue;
+ if ( $i === false ) continue;
#print "processing MIME INFO line $s<br>";
- $match= array();
- if (preg_match('!\[\s*(\w+)\s*\]!',$s,$match)) {
- $s= preg_replace('!\[\s*(\w+)\s*\]!','',$s);
- $mtype= trim(strtoupper($match[1]));
+ $match = array();
+ if ( preg_match( '!\[\s*(\w+)\s*\]!', $s, $match ) ) {
+ $s = preg_replace( '!\[\s*(\w+)\s*\]!', '', $s );
+ $mtype = trim( strtoupper( $match[1] ) );
+ } else {
+ $mtype = MEDIATYPE_UNKNOWN;
}
- else $mtype= MEDIATYPE_UNKNOWN;
- $m= explode(' ',$s);
+ $m = explode( ' ', $s );
- if (!isset($this->mMediaTypes[$mtype])) $this->mMediaTypes[$mtype]= array();
+ if ( !isset( $this->mMediaTypes[$mtype] ) ) {
+ $this->mMediaTypes[$mtype] = array();
+ }
- foreach ($m as $mime) {
- $mime= trim($mime);
- if (empty($mime)) continue;
+ foreach ( $m as $mime ) {
+ $mime = trim( $mime );
+ if ( empty( $mime ) ) continue;
- $this->mMediaTypes[$mtype][]= $mime;
+ $this->mMediaTypes[$mtype][] = $mime;
}
- if (sizeof($m)>1) {
- $main= $m[0];
- for ($i=1; $i<sizeof($m); $i+= 1) {
- $mime= $m[$i];
- $this->mMimeTypeAliases[$mime]= $main;
+ if ( sizeof( $m ) > 1 ) {
+ $main = $m[0];
+ for ( $i=1; $i<sizeof($m); $i += 1 ) {
+ $mime = $m[$i];
+ $this->mMimeTypeAliases[$mime] = $main;
}
}
}
@@ -244,14 +262,14 @@ class MimeMagic {
/** returns a list of file extensions for a given mime type
* as a space separated string.
*/
- function getExtensionsForType($mime) {
- $mime= strtolower($mime);
+ function getExtensionsForType( $mime ) {
+ $mime = strtolower( $mime );
- $r= @$this->mMimeToExt[$mime];
+ $r = @$this->mMimeToExt[$mime];
- if (@!$r and isset($this->mMimeTypeAliases[$mime])) {
- $mime= $this->mMimeTypeAliases[$mime];
- $r= @$this->mMimeToExt[$mime];
+ if ( @!$r and isset( $this->mMimeTypeAliases[$mime] ) ) {
+ $mime = $this->mMimeTypeAliases[$mime];
+ $r = @$this->mMimeToExt[$mime];
}
return $r;
@@ -260,22 +278,22 @@ class MimeMagic {
/** returns a list of mime types for a given file extension
* as a space separated string.
*/
- function getTypesForExtension($ext) {
- $ext= strtolower($ext);
+ function getTypesForExtension( $ext ) {
+ $ext = strtolower( $ext );
- $r= isset( $this->mExtToMime[$ext] ) ? $this->mExtToMime[$ext] : null;
+ $r = isset( $this->mExtToMime[$ext] ) ? $this->mExtToMime[$ext] : null;
return $r;
}
/** returns a single mime type for a given file extension.
* This is always the first type from the list returned by getTypesForExtension($ext).
*/
- function guessTypesForExtension($ext) {
- $m= $this->getTypesForExtension( $ext );
- if( is_null($m) ) return NULL;
+ function guessTypesForExtension( $ext ) {
+ $m = $this->getTypesForExtension( $ext );
+ if ( is_null( $m ) ) return NULL;
- $m= trim( $m );
- $m= preg_replace('/\s.*$/','',$m);
+ $m = trim( $m );
+ $m = preg_replace( '/\s.*$/', '', $m );
return $m;
}
@@ -285,17 +303,17 @@ class MimeMagic {
* returns true if a match was found, NULL if the mime type is unknown,
* and false if the mime type is known but no matches where found.
*/
- function isMatchingExtension($extension,$mime) {
- $ext= $this->getExtensionsForType($mime);
+ function isMatchingExtension( $extension, $mime ) {
+ $ext = $this->getExtensionsForType( $mime );
- if (!$ext) {
+ if ( !$ext ) {
return NULL; //unknown
}
- $ext= explode(' ',$ext);
+ $ext = explode( ' ', $ext );
- $extension= strtolower($extension);
- if (in_array($extension,$ext)) {
+ $extension = strtolower( $extension );
+ if ( in_array( $extension, $ext ) ) {
return true;
}
@@ -347,16 +365,18 @@ class MimeMagic {
* or misinterpreter by the default mime detection (namely xml based formats like XHTML or SVG).
*
* @param string $file The file to check
- * @param bool $useExt switch for allowing to use the file extension to guess the mime type. true by default.
+ * @param mixed $ext The file extension, or true to extract it from the filename.
+ * Set it to false to ignore the extension.
*
* @return string the mime type of $file
*/
- function guessMimeType( $file, $useExt=true ) {
- $fname = 'MimeMagic::guessMimeType';
- $mime= $this->detectMimeType($file,$useExt);
+ function guessMimeType( $file, $ext = true ) {
+ $mime = $this->detectMimeType( $file, $ext );
// Read a chunk of the file
+ wfSuppressWarnings();
$f = fopen( $file, "rt" );
+ wfRestoreWarnings();
if( !$f ) return "unknown/unknown";
$head = fread( $f, 1024 );
fclose( $f );
@@ -369,67 +389,88 @@ class MimeMagic {
$mime = "application/x-msmetafile";
}
- if (strpos($mime,"text/")===0 || $mime==="application/xml") {
+ if ( strpos( $mime, "text/" ) === 0 || $mime === "application/xml" ) {
- $xml_type= NULL;
- $script_type= NULL;
+ $xml_type = NULL;
+ $script_type = NULL;
/*
* look for XML formats (XHTML and SVG)
*/
- if ($mime==="text/sgml" ||
- $mime==="text/plain" ||
- $mime==="text/html" ||
- $mime==="text/xml" ||
- $mime==="application/xml") {
-
- if (substr($head,0,5)=="<?xml") $xml_type= "ASCII";
- elseif (substr($head,0,8)=="\xef\xbb\xbf<?xml") $xml_type= "UTF-8";
- elseif (substr($head,0,10)=="\xfe\xff\x00<\x00?\x00x\x00m\x00l") $xml_type= "UTF-16BE";
- elseif (substr($head,0,10)=="\xff\xfe<\x00?\x00x\x00m\x00l\x00") $xml_type= "UTF-16LE";
+ if ($mime === "text/sgml" ||
+ $mime === "text/plain" ||
+ $mime === "text/html" ||
+ $mime === "text/xml" ||
+ $mime === "application/xml") {
+
+ if ( substr( $head, 0, 5 ) == "<?xml" ) {
+ $xml_type = "ASCII";
+ } elseif ( substr( $head, 0, 8 ) == "\xef\xbb\xbf<?xml") {
+ $xml_type = "UTF-8";
+ } elseif ( substr( $head, 0, 10 ) == "\xfe\xff\x00<\x00?\x00x\x00m\x00l" ) {
+ $xml_type = "UTF-16BE";
+ } elseif ( substr( $head, 0, 10 ) == "\xff\xfe<\x00?\x00x\x00m\x00l\x00") {
+ $xml_type = "UTF-16LE";
+ }
- if ($xml_type) {
- if ($xml_type!=="UTF-8" && $xml_type!=="ASCII") $head= iconv($xml_type,"ASCII//IGNORE",$head);
+ if ( $xml_type ) {
+ if ( $xml_type !== "UTF-8" && $xml_type !== "ASCII" ) {
+ $head = iconv( $xml_type, "ASCII//IGNORE", $head );
+ }
- $match= array();
- $doctype= "";
- $tag= "";
+ $match = array();
+ $doctype = "";
+ $tag = "";
- if (preg_match('%<!DOCTYPE\s+[\w-]+\s+PUBLIC\s+["'."'".'"](.*?)["'."'".'"].*>%sim',$head,$match)) $doctype= $match[1];
- if (preg_match('%<(\w+).*>%sim',$head,$match)) $tag= $match[1];
+ if ( preg_match( '%<!DOCTYPE\s+[\w-]+\s+PUBLIC\s+["'."'".'"](.*?)["'."'".'"].*>%sim',
+ $head, $match ) ) {
+ $doctype = $match[1];
+ }
+ if ( preg_match( '%<(\w+).*>%sim', $head, $match ) ) {
+ $tag = $match[1];
+ }
#print "<br>ANALYSING $file ($mime): doctype= $doctype; tag= $tag<br>";
- if (strpos($doctype,"-//W3C//DTD SVG")===0) $mime= "image/svg+xml";
- elseif ($tag==="svg") $mime= "image/svg+xml";
- elseif (strpos($doctype,"-//W3C//DTD XHTML")===0) $mime= "text/html";
- elseif ($tag==="html") $mime= "text/html";
+ if ( strpos( $doctype, "-//W3C//DTD SVG" ) === 0 ) {
+ $mime = "image/svg+xml";
+ } elseif ( $tag === "svg" ) {
+ $mime = "image/svg+xml";
+ } elseif ( strpos( $doctype, "-//W3C//DTD XHTML" ) === 0 ) {
+ $mime = "text/html";
+ } elseif ( $tag === "html" ) {
+ $mime = "text/html";
+ }
}
}
/*
* look for shell scripts
*/
- if (!$xml_type) {
- $script_type= NULL;
-
- #detect by shebang
- if (substr($head,0,2)=="#!") $script_type= "ASCII";
- elseif (substr($head,0,5)=="\xef\xbb\xbf#!") $script_type= "UTF-8";
- elseif (substr($head,0,7)=="\xfe\xff\x00#\x00!") $script_type= "UTF-16BE";
- elseif (substr($head,0,7)=="\xff\xfe#\x00!") $script_type= "UTF-16LE";
+ if ( !$xml_type ) {
+ $script_type = NULL;
+
+ # detect by shebang
+ if ( substr( $head, 0, 2) == "#!" ) {
+ $script_type = "ASCII";
+ } elseif ( substr( $head, 0, 5) == "\xef\xbb\xbf#!" ) {
+ $script_type = "UTF-8";
+ } elseif ( substr( $head, 0, 7) == "\xfe\xff\x00#\x00!" ) {
+ $script_type = "UTF-16BE";
+ } elseif ( substr( $head, 0, 7 ) == "\xff\xfe#\x00!" ) {
+ $script_type= "UTF-16LE";
+ }
- if ($script_type) {
- if ($script_type!=="UTF-8" && $script_type!=="ASCII") $head= iconv($script_type,"ASCII//IGNORE",$head);
+ if ( $script_type ) {
+ if ( $script_type !== "UTF-8" && $script_type !== "ASCII") {
+ $head = iconv( $script_type, "ASCII//IGNORE", $head);
+ }
- $match= array();
- $prog= "";
+ $match = array();
- if (preg_match('%/?([^\s]+/)(w+)%sim',$head,$match)) {
- $script= $match[2]; // FIXME: $script variable not used; should this be "$prog = $match[2];" instead?
+ if ( preg_match( '%/?([^\s]+/)(\w+)%', $head, $match ) ) {
+ $mime = "application/x-{$match[2]}";
}
-
- $mime= "application/x-$prog";
}
}
@@ -450,42 +491,43 @@ class MimeMagic {
( strpos( $head, "<\x00?\x00\t" ) !== false ) ||
( strpos( $head, "<\x00?\x00=" ) !== false ) ) {
- $mime= "application/x-php";
+ $mime = "application/x-php";
}
}
}
- if (isset($this->mMimeTypeAliases[$mime])) $mime= $this->mMimeTypeAliases[$mime];
+ if ( isset( $this->mMimeTypeAliases[$mime] ) ) {
+ $mime = $this->mMimeTypeAliases[$mime];
+ }
- wfDebug("$fname: final mime type of $file: $mime\n");
+ wfDebug(__METHOD__.": final mime type of $file: $mime\n");
return $mime;
}
/** Internal mime type detection, please use guessMimeType() for application code instead.
* Detection is done using an external program, if $wgMimeDetectorCommand is set.
* Otherwise, the fileinfo extension and mime_content_type are tried (in this order), if they are available.
- * If the dections fails and $useExt is true, the mime type is guessed from the file extension, using guessTypesForExtension.
+ * If the dections fails and $ext is not false, the mime type is guessed from the file extension, using
+ * guessTypesForExtension.
* If the mime type is still unknown, getimagesize is used to detect the mime type if the file is an image.
* If no mime type can be determined, this function returns "unknown/unknown".
*
* @param string $file The file to check
- * @param bool $useExt switch for allowing to use the file extension to guess the mime type. true by default.
+ * @param mixed $ext The file extension, or true to extract it from the filename.
+ * Set it to false to ignore the extension.
*
* @return string the mime type of $file
* @access private
*/
- function detectMimeType( $file, $useExt=true ) {
- $fname = 'MimeMagic::detectMimeType';
-
+ function detectMimeType( $file, $ext = true ) {
global $wgMimeDetectorCommand;
- $m= NULL;
- if ($wgMimeDetectorCommand) {
- $fn= wfEscapeShellArg($file);
- $m= `$wgMimeDetectorCommand $fn`;
- }
- else if (function_exists("finfo_open") && function_exists("finfo_file")) {
+ $m = NULL;
+ if ( $wgMimeDetectorCommand ) {
+ $fn = wfEscapeShellArg( $file );
+ $m = `$wgMimeDetectorCommand $fn`;
+ } elseif ( function_exists( "finfo_open" ) && function_exists( "finfo_file" ) ) {
# This required the fileinfo extension by PECL,
# see http://pecl.php.net/package/fileinfo
@@ -500,13 +542,12 @@ class MimeMagic {
$mime_magic_resource = finfo_open(FILEINFO_MIME); /* return mime type ala mimetype extension */
if ($mime_magic_resource) {
- $m= finfo_file($mime_magic_resource, $file);
-
- finfo_close($mime_magic_resource);
+ $m = finfo_file( $mime_magic_resource, $file );
+ finfo_close( $mime_magic_resource );
+ } else {
+ wfDebug( __METHOD__.": finfo_open failed on ".FILEINFO_MIME."!\n" );
}
- else wfDebug("$fname: finfo_open failed on ".FILEINFO_MIME."!\n");
- }
- else if (function_exists("mime_content_type")) {
+ } elseif ( function_exists( "mime_content_type" ) ) {
# 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.
@@ -517,93 +558,99 @@ class MimeMagic {
# 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);
+ $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");
+ wfDebug( __METHOD__.": (re)detected $file as image/vnd.djvu\n" );
$m = 'image/vnd.djvu';
}
}
+ } else {
+ wfDebug( __METHOD__.": no magic mime detector found!\n" );
}
- else wfDebug("$fname: no magic mime detector found!\n");
- if ($m) {
- #normalize
- $m= preg_replace('![;, ].*$!','',$m); #strip charset, etc
- $m= trim($m);
- $m= strtolower($m);
+ if ( $m ) {
+ # normalize
+ $m = preg_replace( '![;, ].*$!', '', $m ); #strip charset, etc
+ $m = trim( $m );
+ $m = strtolower( $m );
- if (strpos($m,'unknown')!==false) $m= NULL;
- else {
- wfDebug("$fname: magic mime type of $file: $m\n");
+ if ( strpos( $m, 'unknown' ) !== false ) {
+ $m = NULL;
+ } else {
+ wfDebug( __METHOD__.": magic mime type of $file: $m\n" );
return $m;
}
}
- #if still not known, use getimagesize to find out the type of image
- #TODO: skip things that do not have a well-known image extension? Would that be safe?
+ # if still not known, use getimagesize to find out the type of image
+ # TODO: skip things that do not have a well-known image extension? Would that be safe?
wfSuppressWarnings();
$gis = getimagesize( $file );
wfRestoreWarnings();
- $notAnImage= false;
-
- if ($gis && is_array($gis) && $gis[2]) {
- switch ($gis[2]) {
- case IMAGETYPE_GIF: $m= "image/gif"; break;
- case IMAGETYPE_JPEG: $m= "image/jpeg"; break;
- case IMAGETYPE_PNG: $m= "image/png"; break;
- case IMAGETYPE_SWF: $m= "application/x-shockwave-flash"; break;
- case IMAGETYPE_PSD: $m= "application/photoshop"; break;
- case IMAGETYPE_BMP: $m= "image/bmp"; break;
- case IMAGETYPE_TIFF_II: $m= "image/tiff"; break;
- case IMAGETYPE_TIFF_MM: $m= "image/tiff"; break;
- case IMAGETYPE_JPC: $m= "image"; break;
- case IMAGETYPE_JP2: $m= "image/jpeg2000"; break;
- case IMAGETYPE_JPX: $m= "image/jpeg2000"; break;
- case IMAGETYPE_JB2: $m= "image"; break;
- case IMAGETYPE_SWC: $m= "application/x-shockwave-flash"; break;
- case IMAGETYPE_IFF: $m= "image/vnd.xiff"; break;
- case IMAGETYPE_WBMP: $m= "image/vnd.wap.wbmp"; break;
- case IMAGETYPE_XBM: $m= "image/x-xbitmap"; break;
+ $notAnImage = false;
+
+ if ( $gis && is_array($gis) && $gis[2] ) {
+
+ switch ( $gis[2] ) {
+ case IMAGETYPE_GIF: $m = "image/gif"; break;
+ case IMAGETYPE_JPEG: $m = "image/jpeg"; break;
+ case IMAGETYPE_PNG: $m = "image/png"; break;
+ case IMAGETYPE_SWF: $m = "application/x-shockwave-flash"; break;
+ case IMAGETYPE_PSD: $m = "application/photoshop"; break;
+ case IMAGETYPE_BMP: $m = "image/bmp"; break;
+ case IMAGETYPE_TIFF_II: $m = "image/tiff"; break;
+ case IMAGETYPE_TIFF_MM: $m = "image/tiff"; break;
+ case IMAGETYPE_JPC: $m = "image"; break;
+ case IMAGETYPE_JP2: $m = "image/jpeg2000"; break;
+ case IMAGETYPE_JPX: $m = "image/jpeg2000"; break;
+ case IMAGETYPE_JB2: $m = "image"; break;
+ case IMAGETYPE_SWC: $m = "application/x-shockwave-flash"; break;
+ case IMAGETYPE_IFF: $m = "image/vnd.xiff"; break;
+ case IMAGETYPE_WBMP: $m = "image/vnd.wap.wbmp"; break;
+ case IMAGETYPE_XBM: $m = "image/x-xbitmap"; break;
}
- if ($m) {
- wfDebug("$fname: image mime type of $file: $m\n");
+ if ( $m ) {
+ wfDebug( __METHOD__.": image mime type of $file: $m\n" );
return $m;
}
- else $notAnImage= true;
+ else {
+ $notAnImage = true;
+ }
} else {
// Also test DjVu
$deja = new DjVuImage( $file );
if( $deja->isValid() ) {
- wfDebug("$fname: detected $file as image/vnd.djvu\n");
+ wfDebug( __METHOD__.": detected $file as image/vnd.djvu\n" );
return 'image/vnd.djvu';
}
}
- #if desired, look at extension as a fallback.
- if ($useExt) {
+ # if desired, look at extension as a fallback.
+ if ( $ext === true ) {
$i = strrpos( $file, '.' );
- $e= strtolower( $i ? substr( $file, $i + 1 ) : '' );
-
- $m= $this->guessTypesForExtension($e);
+ $ext = strtolower( $i ? substr( $file, $i + 1 ) : '' );
+ }
+ if ( $ext ) {
+ $m = $this->guessTypesForExtension( $ext );
- #TODO: if $notAnImage is set, do not trust the file extension if
+ # TODO: if $notAnImage is set, do not trust the file extension if
# the results is one of the image types that should have been recognized
# by getimagesize
- if ($m) {
- wfDebug("$fname: extension mime type of $file: $m\n");
+ if ( $m ) {
+ wfDebug( __METHOD__.": extension mime type of $file: $m\n" );
return $m;
}
}
#unknown type
- wfDebug("$fname: failed to guess mime type for $file!\n");
+ wfDebug( __METHOD__.": failed to guess mime type for $file!\n" );
return "unknown/unknown";
}
@@ -623,61 +670,61 @@ class MimeMagic {
*
* @return (int?string?) a value to be used with the MEDIATYPE_xxx constants.
*/
- function getMediaType($path=NULL,$mime=NULL) {
+ function getMediaType( $path = NULL, $mime = NULL ) {
if( !$mime && !$path ) return MEDIATYPE_UNKNOWN;
- #if mime type is unknown, guess it
- if( !$mime ) $mime= $this->guessMimeType($path,false);
+ # If mime type is unknown, guess it
+ if( !$mime ) $mime = $this->guessMimeType( $path, false );
- #special code for ogg - detect if it's video (theora),
- #else label it as sound.
- if( $mime=="application/ogg" && file_exists($path) ) {
+ # Special code for ogg - detect if it's video (theora),
+ # else label it as sound.
+ if( $mime == "application/ogg" && file_exists( $path ) ) {
// Read a chunk of the file
$f = fopen( $path, "rt" );
- if( !$f ) return MEDIATYPE_UNKNOWN;
+ if ( !$f ) return MEDIATYPE_UNKNOWN;
$head = fread( $f, 256 );
fclose( $f );
- $head= strtolower( $head );
+ $head = strtolower( $head );
- #This is an UGLY HACK, file should be parsed correctly
- if( strpos($head,'theora')!==false ) return MEDIATYPE_VIDEO;
- elseif( strpos($head,'vorbis')!==false ) return MEDIATYPE_AUDIO;
- elseif( strpos($head,'flac')!==false ) return MEDIATYPE_AUDIO;
- elseif( strpos($head,'speex')!==false ) return MEDIATYPE_AUDIO;
+ # This is an UGLY HACK, file should be parsed correctly
+ if ( strpos( $head, 'theora' ) !== false ) return MEDIATYPE_VIDEO;
+ elseif ( strpos( $head, 'vorbis' ) !== false ) return MEDIATYPE_AUDIO;
+ elseif ( strpos( $head, 'flac' ) !== false ) return MEDIATYPE_AUDIO;
+ elseif ( strpos( $head, 'speex' ) !== false ) return MEDIATYPE_AUDIO;
else return MEDIATYPE_MULTIMEDIA;
}
- #check for entry for full mime type
+ # check for entry for full mime type
if( $mime ) {
- $type= $this->findMediaType($mime);
- if( $type!==MEDIATYPE_UNKNOWN ) return $type;
+ $type = $this->findMediaType( $mime );
+ if( $type !== MEDIATYPE_UNKNOWN ) return $type;
}
- #check for entry for file extension
- $e= NULL;
- if( $path ) {
+ # Check for entry for file extension
+ $e = NULL;
+ if ( $path ) {
$i = strrpos( $path, '.' );
- $e= strtolower( $i ? substr( $path, $i + 1 ) : '' );
+ $e = strtolower( $i ? substr( $path, $i + 1 ) : '' );
- #TODO: look at multi-extension if this fails, parse from full path
+ # TODO: look at multi-extension if this fails, parse from full path
- $type= $this->findMediaType('.'.$e);
- if( $type!==MEDIATYPE_UNKNOWN ) return $type;
+ $type = $this->findMediaType( '.' . $e );
+ if ( $type !== MEDIATYPE_UNKNOWN ) return $type;
}
- #check major mime type
+ # Check major mime type
if( $mime ) {
- $i= strpos($mime,'/');
+ $i = strpos( $mime, '/' );
if( $i !== false ) {
- $major= substr($mime,0,$i);
- $type= $this->findMediaType($major);
- if( $type!==MEDIATYPE_UNKNOWN ) return $type;
+ $major = substr( $mime, 0, $i );
+ $type = $this->findMediaType( $major );
+ if( $type !== MEDIATYPE_UNKNOWN ) return $type;
}
}
- if( !$type ) $type= MEDIATYPE_UNKNOWN;
+ if( !$type ) $type = MEDIATYPE_UNKNOWN;
return $type;
}
@@ -689,25 +736,26 @@ class MimeMagic {
* This funktion relies on the mapping defined by $this->mMediaTypes
* @access private
*/
- function findMediaType($extMime) {
-
- if (strpos($extMime,'.')===0) { #if it's an extension, look up the mime types
- $m= $this->getTypesForExtension(substr($extMime,1));
- if (!$m) return MEDIATYPE_UNKNOWN;
-
- $m= explode(' ',$m);
- }
- else { #normalize mime type
- if (isset($this->mMimeTypeAliases[$extMime])) {
- $extMime= $this->mMimeTypeAliases[$extMime];
+ function findMediaType( $extMime ) {
+ if ( strpos( $extMime, '.' ) === 0 ) { #if it's an extension, look up the mime types
+ $m = $this->getTypesForExtension( substr( $extMime, 1 ) );
+ if ( !$m ) return MEDIATYPE_UNKNOWN;
+
+ $m = explode( ' ', $m );
+ } else {
+ # Normalize mime type
+ if ( isset( $this->mMimeTypeAliases[$extMime] ) ) {
+ $extMime = $this->mMimeTypeAliases[$extMime];
}
- $m= array($extMime);
+ $m = array($extMime);
}
- foreach ($m as $mime) {
- foreach ($this->mMediaTypes as $type => $codes) {
- if (in_array($mime,$codes,true)) return $type;
+ foreach ( $m as $mime ) {
+ foreach ( $this->mMediaTypes as $type => $codes ) {
+ if ( in_array($mime, $codes, true ) ) {
+ return $type;
+ }
}
}
@@ -715,4 +763,4 @@ class MimeMagic {
}
}
-?>
+
diff --git a/includes/Namespace.php b/includes/Namespace.php
index dd67b55a..f4df3bac 100644
--- a/includes/Namespace.php
+++ b/includes/Namespace.php
@@ -44,54 +44,67 @@ if( is_array( $wgExtraNamespaces ) ) {
class Namespace {
/**
- * Check if the given namespace might be moved
+ * Can pages in the given namespace be moved?
+ *
+ * @param int $index Namespace index
* @return bool
*/
- static function isMovable( $index ) {
+ public static function isMovable( $index ) {
return !( $index < NS_MAIN || $index == NS_IMAGE || $index == NS_CATEGORY );
}
/**
- * Check if the given namespace is not a talk page
+ * Is the given namespace is a subject (non-talk) namespace?
+ *
+ * @param int $index Namespace index
* @return bool
*/
- static function isMain( $index ) {
- return ! Namespace::isTalk( $index );
+ public static function isMain( $index ) {
+ return !self::isTalk( $index );
}
/**
- * Check if the give namespace is a talk page
+ * Is the given namespace a talk namespace?
+ *
+ * @param int $index Namespace index
* @return bool
*/
- static function isTalk( $index ) {
- return ($index > NS_MAIN) // Special namespaces are negative
- && ($index % 2); // Talk namespaces are odd-numbered
+ public static function isTalk( $index ) {
+ return $index > NS_MAIN
+ && $index % 2;
}
/**
- * Get the talk namespace corresponding to the given index
+ * Get the talk namespace index for a given namespace
+ *
+ * @param int $index Namespace index
+ * @return int
*/
- static function getTalk( $index ) {
- if ( Namespace::isTalk( $index ) ) {
- return $index;
- } else {
- # FIXME
- return $index + 1;
- }
+ public static function getTalk( $index ) {
+ return self::isTalk( $index )
+ ? $index
+ : $index + 1;
}
- static function getSubject( $index ) {
- if ( Namespace::isTalk( $index ) ) {
- return $index - 1;
- } else {
- return $index;
- }
+ /**
+ * Get the subject namespace index for a given namespace
+ *
+ * @param int $index Namespace index
+ * @return int
+ */
+ public static function getSubject( $index ) {
+ return self::isTalk( $index )
+ ? $index - 1
+ : $index;
}
/**
* Returns the canonical (English Wikipedia) name for a given index
+ *
+ * @param int $index Namespace index
+ * @return string
*/
- static function getCanonicalName( $index ) {
+ public static function getCanonicalName( $index ) {
global $wgCanonicalNamespaceNames;
return $wgCanonicalNamespaceNames[$index];
}
@@ -99,8 +112,11 @@ class Namespace {
/**
* Returns the index for a given canonical name, or NULL
* The input *must* be converted to lower case first
+ *
+ * @param string $name Namespace name
+ * @return int
*/
- static function getCanonicalIndex( $name ) {
+ public static function getCanonicalIndex( $name ) {
global $wgCanonicalNamespaceNames;
static $xNamespaces = false;
if ( $xNamespaces === false ) {
@@ -118,10 +134,12 @@ class Namespace {
/**
* Can this namespace ever have a talk namespace?
+ *
* @param $index Namespace index
+ * @return bool
*/
- static function canTalk( $index ) {
- return( $index >= NS_MAIN );
+ public static function canTalk( $index ) {
+ return $index >= NS_MAIN;
}
/**
@@ -134,8 +152,16 @@ class Namespace {
public static function isContent( $index ) {
global $wgContentNamespaces;
return $index == NS_MAIN || in_array( $index, $wgContentNamespaces );
- }
+ }
+
+ /**
+ * Can pages in a namespace be watched?
+ *
+ * @param int $index
+ * @return bool
+ */
+ public static function isWatchable( $index ) {
+ return $index >= NS_MAIN;
+ }
-}
-
-?>
+} \ No newline at end of file
diff --git a/includes/ObjectCache.php b/includes/ObjectCache.php
index 3b43dd53..7d9caf8a 100644
--- a/includes/ObjectCache.php
+++ b/includes/ObjectCache.php
@@ -71,7 +71,7 @@ function &wfGetCache( $inputType ) {
} elseif ( function_exists( 'apc_fetch') ) {
$wgCaches[CACHE_ACCEL] = new APCBagOStuff;
} elseif( function_exists( 'xcache_get' ) ) {
- $wgCaches[CACHE_ACCEL] = new XCacheBagOStuff;
+ $wgCaches[CACHE_ACCEL] = new XCacheBagOStuff();
} elseif ( function_exists( 'mmcache_get' ) ) {
$wgCaches[CACHE_ACCEL] = new TurckBagOStuff;
} else {
@@ -123,4 +123,4 @@ function &wfGetParserCacheStorage() {
return $ret;
}
-?>
+
diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php
index d7e7c90f..d8ac12b5 100644
--- a/includes/OutputHandler.php
+++ b/includes/OutputHandler.php
@@ -18,30 +18,67 @@ function wfOutputHandler( $s ) {
}
/**
+ * Get the "file extension" that some client apps will estimate from
+ * the currently-requested URL.
+ * This isn't on WebRequest because we need it when things aren't initialized
+ * @private
+ */
+function wfRequestExtension() {
+ /// @fixme -- this sort of dupes some code in WebRequest::getRequestUrl()
+ if( isset( $_SERVER['REQUEST_URI'] ) ) {
+ // Strip the query string...
+ list( $path ) = explode( '?', $_SERVER['REQUEST_URI'], 2 );
+ } elseif( isset( $_SERVER['SCRIPT_NAME'] ) ) {
+ // Probably IIS. QUERY_STRING appears separately.
+ $path = $_SERVER['SCRIPT_NAME'];
+ } else {
+ // Can't get the path from the server? :(
+ return '';
+ }
+
+ $period = strrpos( $path, '.' );
+ if( $period !== false ) {
+ return strtolower( substr( $path, $period ) );
+ }
+ return '';
+}
+
+/**
* Handler that compresses data with gzip if allowed by the Accept header.
* Unlike ob_gzhandler, it works for HEAD requests too.
*/
function wfGzipHandler( $s ) {
- if ( function_exists( 'gzencode' ) && !headers_sent() ) {
- $tokens = preg_split( '/[,; ]/', $_SERVER['HTTP_ACCEPT_ENCODING'] );
- if ( in_array( 'gzip', $tokens ) ) {
- header( 'Content-Encoding: gzip' );
- $s = gzencode( $s, 3 );
-
- # Set vary header if it hasn't been set already
- $headers = headers_list();
- $foundVary = false;
- foreach ( $headers as $header ) {
- if ( substr( $header, 0, 5 ) == 'Vary:' ) {
- $foundVary = true;
- break;
- }
- }
- if ( !$foundVary ) {
- header( 'Vary: Accept-Encoding' );
- }
+ if( !function_exists( 'gzencode' ) || headers_sent() ) {
+ return $s;
+ }
+
+ $ext = wfRequestExtension();
+ if( $ext == '.gz' || $ext == '.tgz' ) {
+ // Don't do gzip compression if the URL path ends in .gz or .tgz
+ // This confuses Safari and triggers a download of the page,
+ // even though it's pretty clearly labeled as viewable HTML.
+ // Bad Safari! Bad!
+ return $s;
+ }
+
+ $tokens = preg_split( '/[,; ]/', $_SERVER['HTTP_ACCEPT_ENCODING'] );
+ if ( in_array( 'gzip', $tokens ) ) {
+ header( 'Content-Encoding: gzip' );
+ $s = gzencode( $s, 3 );
+ }
+
+ // Set vary header if it hasn't been set already
+ $headers = headers_list();
+ $foundVary = false;
+ foreach ( $headers as $header ) {
+ if ( substr( $header, 0, 5 ) == 'Vary:' ) {
+ $foundVary = true;
+ break;
}
}
+ if ( !$foundVary ) {
+ header( 'Vary: Accept-Encoding' );
+ }
return $s;
}
@@ -61,4 +98,4 @@ function wfDoContentLength( $length ) {
}
}
-?>
+
diff --git a/includes/OutputPage.php b/includes/OutputPage.php
index 03e832a4..06467157 100644
--- a/includes/OutputPage.php
+++ b/includes/OutputPage.php
@@ -15,6 +15,7 @@ class OutputPage {
var $mLastModified, $mETag, $mCategoryLinks;
var $mScripts, $mLinkColours, $mPageLinkTitle;
+ var $mAllowUserJs;
var $mSuppressQuickbar;
var $mOnloadHandler;
var $mDoNothing;
@@ -27,12 +28,15 @@ class OutputPage {
var $mNewSectionLink = false;
var $mNoGallery = false;
+ var $mPageTitleActionText = '';
/**
* Constructor
* Initialise private variables
*/
function __construct() {
+ global $wgAllowUserJs;
+ $this->mAllowUserJs = $wgAllowUserJs;
$this->mMetatags = $this->mKeywords = $this->mLinktags = array();
$this->mHTMLtitle = $this->mPagetitle = $this->mBodytext =
$this->mRedirect = $this->mLastModified =
@@ -51,6 +55,7 @@ class OutputPage {
$this->mETag = false;
$this->mRevisionId = null;
$this->mNewSectionLink = false;
+ $this->mTemplateIds = array();
}
public function redirect( $url, $responsecode = '302' ) {
@@ -71,6 +76,13 @@ class OutputPage {
function addMeta( $name, $val ) { array_push( $this->mMetatags, array( $name, $val ) ); }
function addKeyword( $text ) { array_push( $this->mKeywords, $text ); }
function addScript( $script ) { $this->mScripts .= "\t\t".$script; }
+ function addStyle( $style ) {
+ global $wgStylePath, $wgStyleVersion;
+ $this->addLink(
+ array(
+ 'rel' => 'stylesheet',
+ 'href' => $wgStylePath . '/' . $style . '?' . $wgStyleVersion ) );
+ }
/**
* Add a self-contained script tag with the given contents
@@ -97,6 +109,10 @@ class OutputPage {
$this->mHeadItems[$name] = $value;
}
+ function hasHeadItem( $name ) {
+ return isset( $this->mHeadItems[$name] );
+ }
+
function setETag($tag) { $this->mETag = $tag; }
function setArticleBodyOnly($only) { $this->mArticleBodyOnly = $only; }
function getArticleBodyOnly($only) { return $this->mArticleBodyOnly; }
@@ -146,7 +162,11 @@ class OutputPage {
# Wed, 20 Aug 2003 06:51:19 GMT; length=5202
# this breaks strtotime().
$modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] );
+
+ wfSuppressWarnings(); // E_STRICT system time bitching
$modsinceTime = strtotime( $modsince );
+ wfRestoreWarnings();
+
$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 );
@@ -174,26 +194,13 @@ class OutputPage {
}
}
+ function setPageTitleActionText( $text ) {
+ $this->mPageTitleActionText = $text;
+ }
+
function getPageTitleActionText () {
- global $action;
- switch($action) {
- case 'edit':
- case 'delete':
- case 'protect':
- case 'unprotect':
- case 'watch':
- case 'unwatch':
- // Display title is already customized
- return '';
- case 'history':
- return wfMsg('history_short');
- case 'submit':
- // FIXME: bug 2735; not correct for special pages etc
- return wfMsg('preview');
- case 'info':
- return wfMsg('info_short');
- default:
- return '';
+ if ( isset( $this->mPageTitleActionText ) ) {
+ return $this->mPageTitleActionText;
}
}
@@ -283,6 +290,9 @@ class OutputPage {
public function suppressQuickbar() { $this->mSuppressQuickbar = true; }
public function isQuickbarSuppressed() { return $this->mSuppressQuickbar; }
+ public function disallowUserJs() { $this->mAllowUserJs = false; }
+ public function isUserJsAllowed() { return $this->mAllowUserJs; }
+
public function addHTML( $text ) { $this->mBodytext .= $text; }
public function clearHTML() { $this->mBodytext = ''; }
public function getHTML() { return $this->mBodytext; }
@@ -363,14 +373,24 @@ class OutputPage {
if ( $parserOutput->getCacheTime() == -1 ) {
$this->enableClientCache( false );
}
- if ( $parserOutput->mHTMLtitle != "" ) {
- $this->mPagetitle = $parserOutput->mHTMLtitle ;
- }
- if ( $parserOutput->mSubtitle != '' ) {
- $this->mSubtitle .= $parserOutput->mSubtitle ;
- }
$this->mNoGallery = $parserOutput->getNoGallery();
$this->mHeadItems = array_merge( $this->mHeadItems, (array)$parserOutput->mHeadItems );
+ // Versioning...
+ $this->mTemplateIds += (array)$parserOutput->mTemplateIds;
+
+ # Display title
+ if( ( $dt = $parserOutput->getDisplayTitle() ) !== false )
+ $this->setPageTitle( $dt );
+
+ # Hooks registered in the object
+ global $wgParserOutputHooks;
+ foreach ( $parserOutput->getOutputHooks() as $hookInfo ) {
+ list( $hookName, $data ) = $hookInfo;
+ if ( isset( $wgParserOutputHooks[$hookName] ) ) {
+ call_user_func( $wgParserOutputHooks[$hookName], $this, $parserOutput, $data );
+ }
+ }
+
wfRunHooks( 'OutputPageParserOutput', array( &$this, $parserOutput ) );
}
@@ -730,27 +750,51 @@ class OutputPage {
* @return nothing
*/
function blockedPage( $return = true ) {
- global $wgUser, $wgContLang, $wgTitle;
+ global $wgUser, $wgContLang, $wgTitle, $wgLang;
$this->setPageTitle( wfMsg( 'blockedtitle' ) );
$this->setRobotpolicy( 'noindex,nofollow' );
$this->setArticleRelated( false );
- $id = $wgUser->blockedBy();
+ $name = User::whoIs( $wgUser->blockedBy() );
$reason = $wgUser->blockedFor();
+ $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $wgUser->mBlock->mTimestamp ), true );
$ip = wfGetIP();
- if ( is_numeric( $id ) ) {
- $name = User::whoIs( $id );
- } else {
- $name = $id;
- }
$link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]";
$blockid = $wgUser->mBlock->mId;
- $this->addWikiText( wfMsg( 'blockedtext', $link, $reason, $ip, $name, $blockid ) );
-
+ $blockExpiry = $wgUser->mBlock->mExpiry;
+ if ( $blockExpiry == 'infinity' ) {
+ // Entry in database (table ipblocks) is 'infinity' but 'ipboptions' uses 'infinite' or 'indefinite'
+ // Search for localization in 'ipboptions'
+ $scBlockExpiryOptions = wfMsg( 'ipboptions' );
+ foreach ( explode( ',', $scBlockExpiryOptions ) as $option ) {
+ if ( strpos( $option, ":" ) === false )
+ continue;
+ list( $show, $value ) = explode( ":", $option );
+ if ( $value == 'infinite' || $value == 'indefinite' ) {
+ $blockExpiry = $show;
+ break;
+ }
+ }
+ } else {
+ $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true );
+ }
+
+ if ( $wgUser->mBlock->mAuto ) {
+ $msg = 'autoblockedtext';
+ } else {
+ $msg = 'blockedtext';
+ }
+
+ /* $ip returns who *is* being blocked, $intended contains who was meant to be blocked.
+ * This could be a username, an ip range, or a single ip. */
+ $intended = $wgUser->mBlock->mAddress;
+
+ $this->addWikiText( wfMsg( $msg, $link, $reason, $ip, $name, $blockid, $blockExpiry, $intended, $blockTimestamp ) );
+
# Don't auto-return to special pages
if( $return ) {
$return = $wgTitle->getNamespace() > -1 ? $wgTitle->getPrefixedText() : NULL;
@@ -759,13 +803,13 @@ class OutputPage {
}
/**
- * Outputs a pretty page to explain why the request exploded.
+ * Output a standard error page
*
- * @param string $title Message key for page title.
- * @param string $msg Message key for page text.
- * @return nothing
+ * @param string $title Message key for page title
+ * @param string $msg Message key for page text
+ * @param array $params Message parameters
*/
- public function showErrorPage( $title, $msg ) {
+ public function showErrorPage( $title, $msg, $params = array() ) {
global $wgTitle;
$this->mDebugtext .= 'Original title: ' .
@@ -776,12 +820,36 @@ class OutputPage {
$this->setArticleRelated( false );
$this->enableClientCache( false );
$this->mRedirect = '';
-
$this->mBodytext = '';
- $this->addWikiText( wfMsg( $msg ) );
+
+ array_unshift( $params, 'parse' );
+ array_unshift( $params, $msg );
+ $this->addHtml( call_user_func_array( 'wfMsgExt', $params ) );
+
$this->returnToMain( false );
}
+ /**
+ * Output a standard permission error page
+ *
+ * @param array $errors Error message keys
+ */
+ public function showPermissionsErrorPage( $errors )
+ {
+ global $wgTitle;
+
+ $this->mDebugtext .= 'Original title: ' .
+ $wgTitle->getPrefixedText() . "\n";
+ $this->setPageTitle( wfMsg( 'permissionserrors' ) );
+ $this->setHTMLTitle( wfMsg( 'permissionserrors' ) );
+ $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setArticleRelated( false );
+ $this->enableClientCache( false );
+ $this->mRedirect = '';
+ $this->mBodytext = '';
+ $this->addWikiText( $this->formatPermissionsErrorMessage( $errors ) );
+ }
+
/** @deprecated */
public function errorpage( $title, $msg ) {
throw new ErrorPageError( $title, $msg );
@@ -898,38 +966,75 @@ class OutputPage {
}
/**
+ * @param array $errors An array of arrays returned by Title::getUserPermissionsErrors
+ * @return string The error-messages, formatted into a list.
+ */
+ public function formatPermissionsErrorMessage( $errors ) {
+ $text = '';
+
+ if (sizeof( $errors ) > 1) {
+
+ $text .= wfMsgExt( 'permissionserrorstext', array( 'parse' ), count( $errors ) ) . "\n";
+ $text .= '<ul class="permissions-errors">' . "\n";
+
+ foreach( $errors as $error )
+ {
+ $text .= '<li>';
+ $text .= call_user_func_array( 'wfMsg', $error );
+ $text .= "</li>\n";
+ }
+ $text .= '</ul>';
+ } else {
+ $text .= call_user_func_array( 'wfMsg', $errors[0]);
+ }
+
+ return $text;
+ }
+
+ /**
* @todo document
* @param bool $protected Is the reason the page can't be reached because it's protected?
* @param mixed $source
+ * @param bool $protected, page is protected?
+ * @param array $reason, array of arrays( msg, args )
*/
- public function readOnlyPage( $source = null, $protected = false ) {
+ public function readOnlyPage( $source = null, $protected = false, $reasons = array() ) {
global $wgUser, $wgReadOnlyFile, $wgReadOnly, $wgTitle;
$skin = $wgUser->getSkin();
$this->setRobotpolicy( 'noindex,nofollow' );
$this->setArticleRelated( false );
-
- if( $protected ) {
+
+ if ( !empty($reasons) ) {
$this->setPageTitle( wfMsg( 'viewsource' ) );
$this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) );
- list( $cascadeSources, $restrictions ) = $wgTitle->getCascadeProtectionSources();
+ $this->addWikiText( $this->formatPermissionsErrorMessage( $reasons ) );
+ } else if( $protected ) {
+ $this->setPageTitle( wfMsg( 'viewsource' ) );
+ $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) );
+ list( $cascadeSources, /* $restrictions */ ) = $wgTitle->getCascadeProtectionSources();
- # Determine if protection is due to the page being a system message
- # and show an appropriate explanation
+ // Show an appropriate explanation depending upon the reason
+ // for the protection...all of these should be moved to the
+ // callers
if( $wgTitle->getNamespace() == NS_MEDIAWIKI ) {
+ // User isn't allowed to edit the interface
$this->addWikiText( wfMsg( 'protectedinterface' ) );
- } if ( $cascadeSources && count($cascadeSources) > 0 ) {
- $titles = '';
-
- foreach ( $cascadeSources as $title ) {
- $titles .= '* [[:' . $title->getPrefixedText() . "]]\n";
- }
-
- $notice = wfMsgExt( 'cascadeprotected', array('parsemag'), count($cascadeSources) ) . "\n$titles";
-
- $this->addWikiText( $notice );
+ } elseif( $cascadeSources && ( $count = count( $cascadeSources ) ) > 0 ) {
+ // Cascading protection
+ $titles = '';
+ foreach( $cascadeSources as $title )
+ $titles .= "* [[:" . $title->getPrefixedText() . "]]\n";
+ $this->addWikiText( wfMsgExt( 'cascadeprotected', 'parsemag', $count ) . "\n{$titles}" );
+ } elseif( !$wgTitle->isProtected( 'edit' ) && $wgTitle->isNamespaceProtected() ) {
+ // Namespace protection
+ $ns = $wgTitle->getNamespace() == NS_MAIN
+ ? wfMsg( 'nstab-main' )
+ : $wgTitle->getNsText();
+ $this->addWikiText( wfMsg( 'namespaceprotected', $ns ) );
} else {
+ // Standard protection
$this->addWikiText( wfMsg( 'protectedpagetext' ) );
}
} else {
@@ -950,8 +1055,8 @@ class OutputPage {
htmlspecialchars( $source ) . "\n</textarea>";
$this->addHTML( $text );
}
- $article = new Article($wgTitle);
- $this->addHTML( $skin->formatTemplates($article->getUsedTemplates()) );
+ $article = new Article( $wgTitle );
+ $this->addHTML( $skin->formatTemplates( $article->getUsedTemplates() ) );
$this->returnToMain( false );
}
@@ -1016,12 +1121,25 @@ class OutputPage {
}
/**
- * return from error messages or notes
- * @param $auto automatically redirect the user after 10 seconds
- * @param $returnto page title to return to. Default is Main Page.
+ * Add a "return to" link pointing to a specified title
+ *
+ * @param Title $title Title to link
+ */
+ public function addReturnTo( $title ) {
+ global $wgUser;
+ $link = wfMsg( 'returnto', $wgUser->getSkin()->makeLinkObj( $title ) );
+ $this->addHtml( "<p>{$link}</p>\n" );
+ }
+
+ /**
+ * Add a "return to" link pointing to a specified title,
+ * or the title indicated in the request, or else the main page
+ *
+ * @param null $unused No longer used
+ * @param Title $returnto Title to return to
*/
- public function returnToMain( $auto = true, $returnto = NULL ) {
- global $wgUser, $wgOut, $wgRequest;
+ public function returnToMain( $unused = null, $returnto = NULL ) {
+ global $wgRequest;
if ( $returnto == NULL ) {
$returnto = $wgRequest->getText( 'returnto' );
@@ -1040,14 +1158,7 @@ class OutputPage {
$titleObj = Title::newMainPage();
}
- $sk = $wgUser->getSkin();
- $link = $sk->makeLinkObj( $titleObj, '' );
-
- $r = wfMsg( 'returnto', $link );
- if ( $auto ) {
- $wgOut->addMeta( 'http:Refresh', '10;url=' . $titleObj->escapeFullURL() );
- }
- $wgOut->addHTML( "\n<p>$r</p>\n" );
+ $this->addReturnTo( $titleObj );
}
/**
@@ -1114,7 +1225,7 @@ class OutputPage {
$ret .= "<link rel='stylesheet' type='text/css' $media href='$printsheet' />\n";
$sk = $wgUser->getSkin();
- $ret .= $sk->getHeadScripts();
+ $ret .= $sk->getHeadScripts( $this->mAllowUserJs );
$ret .= $this->mScripts;
$ret .= $sk->getUserStyles();
$ret .= $this->getHeadItems();
@@ -1191,10 +1302,30 @@ class OutputPage {
/**
* Show an "add new section" link?
*
- * @return bool True if the parser output instructs us to add one
+ * @return bool
*/
public function showNewSectionLink() {
return $this->mNewSectionLink;
}
+
+ /**
+ * Show a warning about slave lag
+ *
+ * If the lag is higher than $wgSlaveLagCritical seconds,
+ * then the warning is a bit more obvious. If the lag is
+ * lower than $wgSlaveLagWarning, then no warning is shown.
+ *
+ * @param int $lag Slave lag
+ */
+ public function showLagWarning( $lag ) {
+ global $wgSlaveLagWarning, $wgSlaveLagCritical;
+ if( $lag >= $wgSlaveLagWarning ) {
+ $message = $lag < $wgSlaveLagCritical
+ ? 'lag-warn-normal'
+ : 'lag-warn-high';
+ $warning = wfMsgExt( $message, 'parse', $lag );
+ $this->addHtml( "<div class=\"mw-{$message}\">\n{$warning}\n</div>\n" );
+ }
+ }
+
}
-?>
diff --git a/includes/PageHistory.php b/includes/PageHistory.php
index b1cf41f0..d84c3515 100644
--- a/includes/PageHistory.php
+++ b/includes/PageHistory.php
@@ -62,6 +62,7 @@ class PageHistory {
* Setup page variables.
*/
$wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
+ $wgOut->setPageTitleActionText( wfMsg( 'history_short' ) );
$wgOut->setArticleFlag( false );
$wgOut->setArticleRelated( true );
$wgOut->setRobotpolicy( 'noindex,nofollow' );
@@ -244,8 +245,26 @@ class PageHistory {
if( $row->rev_deleted & Revision::DELETED_TEXT ) {
$s .= ' ' . wfMsgHtml( 'deletedrev' );
}
- if( $wgUser->isAllowed( 'rollback' ) && $latest ) {
- $s .= ' '.$this->mSkin->generateRollback( $rev );
+
+ $tools = array();
+
+ if ( !is_null( $next ) && is_object( $next ) ) {
+ if( $wgUser->isAllowed( 'rollback' ) && $latest ) {
+ $tools[] = '<span class="mw-rollback-link">'
+ . $this->mSkin->buildRollbackLink( $rev )
+ . '</span>';
+ }
+
+ $undolink = $this->mSkin->makeKnownLinkObj(
+ $this->mTitle,
+ wfMsgHtml( 'editundo' ),
+ 'action=edit&undoafter=' . $next->rev_id . '&undo=' . $rev->getId()
+ );
+ $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>";
+ }
+
+ if( $tools ) {
+ $s .= ' (' . implode( ' | ', $tools ) . ')';
}
wfRunHooks( 'PageHistoryLineEnding', array( &$row , &$s ) );
@@ -589,4 +608,5 @@ class PageHistoryPager extends ReverseChronologicalPager {
}
}
-?>
+
+
diff --git a/includes/PageQueryPage.php b/includes/PageQueryPage.php
index 5b82ebf6..53b7765e 100644
--- a/includes/PageQueryPage.php
+++ b/includes/PageQueryPage.php
@@ -23,4 +23,4 @@ class PageQueryPage extends QueryPage {
}
}
-?>
+
diff --git a/includes/Pager.php b/includes/Pager.php
index a475dc16..70d0873c 100644
--- a/includes/Pager.php
+++ b/includes/Pager.php
@@ -12,41 +12,42 @@ interface Pager {
/**
* 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.
+ * 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:
+ * 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 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.
*
- * * 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.
+ * * 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.
*
- * * 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.
+ * 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.
*
* @addtogroup Pager
*/
@@ -75,9 +76,9 @@ abstract class IndexPager implements Pager {
global $wgRequest, $wgUser;
$this->mRequest = $wgRequest;
- # NB: the offset is quoted, not validated. It is treated as an arbitrary string
- # to support the widest variety of index types. Be careful outputting it into
- # HTML!
+ # 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' );
# Use consistent behavior for the limit options
@@ -106,6 +107,9 @@ abstract class IndexPager implements Pager {
$this->mResult = $this->reallyDoQuery( $this->mOffset, $queryLimit, $descending );
$this->extractResultInfo( $this->mOffset, $queryLimit, $this->mResult );
$this->mQueryDone = true;
+
+ $this->preprocessResults( $this->mResult );
+ $this->mResult->rewind(); // Paranoia
wfProfileOut( $fname );
}
@@ -131,9 +135,10 @@ abstract class IndexPager implements Pager {
$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.
+ # 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();
@@ -160,21 +165,22 @@ abstract class IndexPager implements Pager {
}
/**
- * Do a query with specified parameters, rather than using the object context
+ * 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 ) {
+ function reallyDoQuery( $offset, $limit, $descending ) {
$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 ) {
+ if ( $descending ) {
$options['ORDER BY'] = $this->mIndexField;
$operator = '>';
} else {
@@ -190,6 +196,13 @@ abstract class IndexPager implements Pager {
}
/**
+ * Pre-process results; useful for performing batch existence checks, etc.
+ *
+ * @param ResultWrapper $result Result wrapper
+ */
+ protected function preprocessResults( $result ) {}
+
+ /**
* Get the formatted result list. Calls getStartBody(), formatRow() and
* getEndBody(), concatenates the results and returns them.
*/
@@ -331,9 +344,10 @@ abstract class IndexPager implements Pager {
}
/**
- * 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.
+ * 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();
@@ -667,20 +681,22 @@ abstract class TablePager extends IndexPager {
}
/**
- * Return true if the named field should be sortable by the UI, false otherwise
+ * 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 &nbsp; for empty cells. Do not include the <td> and </td>.
+ * Format a table cell. The return value should be HTML, but use an empty
+ * string not &nbsp; for empty cells. Do not include the <td> and </td>.
+ *
+ * The current result row is available as $this->mCurrentRow, in case you
+ * need more context.
*
* @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 );
@@ -690,10 +706,10 @@ abstract class TablePager extends IndexPager {
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.
+ * 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 8e36e170..32e7f2a8 100644
--- a/includes/Parser.php
+++ b/includes/Parser.php
@@ -1,5 +1,7 @@
<?php
+
/**
+ *
* File for Parser and related classes
*
* @addtogroup Parser
@@ -10,7 +12,7 @@
* changes in an incompatible way, so the parser cache
* can automatically discard old data.
*/
-define( 'MW_PARSER_VERSION', '1.6.1' );
+define( 'MW_PARSER_VERSION', '1.6.2' );
define( 'RLH_FOR_UPDATE', 1 );
@@ -95,8 +97,9 @@ class Parser
* @private
*/
# Persistent:
- var $mTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables;
-
+ var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
+ $mImageParams, $mImageParamsMagicArray;
+
# Cleared with clearState():
var $mOutput, $mAutonumber, $mDTopen, $mStripState;
var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
@@ -126,11 +129,12 @@ class Parser
*/
function Parser() {
$this->mTagHooks = array();
+ $this->mTransparentTagHooks = array();
$this->mFunctionHooks = array();
$this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
$this->mFirstCall = true;
}
-
+
/**
* Do various kinds of initialisation on the first call of the parser
*/
@@ -138,12 +142,12 @@ class Parser
if ( !$this->mFirstCall ) {
return;
}
-
+
wfProfileIn( __METHOD__ );
global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions;
-
+
$this->setHook( 'pre', array( $this, 'renderPreTag' ) );
-
+
$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 );
@@ -306,7 +310,7 @@ class Parser
$fixtags = array(
# french spaces, last one Guillemet-left
# only if there is something before the space
- '/(.) (?=\\?|:|;|!|\\302\\273)/' => '\\1&nbsp;\\2',
+ '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&nbsp;\\2',
# french spaces, Guillemet-right
'/(\\302\\253) /' => '\\1&nbsp;',
);
@@ -327,6 +331,26 @@ class Parser
wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
+//!JF Move to its own function
+
+ $uniq_prefix = $this->mUniqPrefix;
+ $matches = array();
+ $elements = array_keys( $this->mTransparentTagHooks );
+ $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+
+ foreach( $matches as $marker => $data ) {
+ list( $element, $content, $params, $tag ) = $data;
+ $tagName = strtolower( $element );
+ if( isset( $this->mTransparentTagHooks[$tagName] ) ) {
+ $output = call_user_func_array( $this->mTransparentTagHooks[$tagName],
+ array( $content, $params, $this ) );
+ } else {
+ $output = $tag;
+ }
+ $this->mStripState->general->setPair( $marker, $output );
+ }
+ $text = $this->mStripState->unstripGeneral( $text );
+
$text = Sanitizer::normalizeCharReferences( $text );
if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
@@ -398,12 +422,15 @@ class Parser
* Expand templates and variables in the text, producing valid, static wikitext.
* Also removes comments.
*/
- function preprocess( $text, $title, $options ) {
+ function preprocess( $text, $title, $options, $revid = null ) {
wfProfileIn( __METHOD__ );
$this->clearState();
$this->setOutputType( OT_PREPROCESS );
$this->mOptions = $options;
$this->mTitle = $title;
+ if( $revid !== null ) {
+ $this->mRevisionId = $revid;
+ }
wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
$text = $this->strip( $text, $this->mStripState );
wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
@@ -449,7 +476,7 @@ class Parser
* @param $text Source text string.
* @param $uniq_prefix
*
- * @private
+ * @public
* @static
*/
function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
@@ -480,7 +507,7 @@ class Parser
$inside = $p[4];
}
- $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . '-QINU';
+ $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . "-QINU\x07";
$stripped .= $marker;
if ( $close === '/>' ) {
@@ -587,7 +614,8 @@ class Parser
$output = Xml::escapeTagsOnly( $content );
break;
case 'math':
- $output = $wgContLang->armourMath( MathRenderer::renderMath( $content ) );
+ $output = $wgContLang->armourMath(
+ MathRenderer::renderMath( $content, $params ) );
break;
case 'gallery':
$output = $this->renderImageGallery( $content, $params );
@@ -725,7 +753,7 @@ class Parser
$descriptorspec = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
- 2 => array('file', '/dev/null', 'a') // FIXME: this line in UNIX-specific, it generates a warning on Windows, because /dev/null is not a valid Windows file.
+ 2 => array('file', wfGetNull(), 'a')
);
$pipes = array();
$process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
@@ -1000,7 +1028,7 @@ class Parser
$text = strtr( $text, array( '<noinclude>' => '', '</noinclude>' => '') );
$text = StringUtils::delimiterReplace( '<includeonly>', '</includeonly>', '', $text );
- $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ) );
+ $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), array(), array_keys( $this->mTransparentTagHooks ) );
$text = $this->replaceVariables( $text, $args );
wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
@@ -1797,11 +1825,15 @@ class Parser
$this->mOutput->addImage( $nt->getDBkey() );
continue;
} elseif( $ns == NS_SPECIAL ) {
- $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
+ if( SpecialPage::exists( $nt->getDBkey() ) ) {
+ $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
+ } else {
+ $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
+ }
continue;
} elseif( $ns == NS_IMAGE ) {
- $img = new Image( $nt );
- if( $img->exists() ) {
+ $img = wfFindFile( $nt );
+ if( $img ) {
// 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.
@@ -1918,15 +1950,22 @@ 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() ) {
+ $hash = strpos( $target, '#' );
+ if( $hash !== false ) {
+ $suffix = substr( $target, $hash );
+ $target = substr( $target, 0, $hash );
+ } else {
+ $suffix = '';
+ }
+ # bug 7425
+ $target = trim( $target );
# Look at the first character
if( $target != '' && $target{0} == '/' ) {
# / at end means we don't want the slash to be shown
+ $m = array();
$trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
if( $trailingSlashes ) {
$noslash = $target = substr( $target, 1, -strlen($m[0][0]) );
@@ -1934,9 +1973,9 @@ class Parser
$noslash = substr( $target, 1 );
}
- $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash);
+ $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash) . $suffix;
if( '' === $text ) {
- $text = $target;
+ $text = $target . $suffix;
} # this might be changed for ugliness reasons
} else {
# check for .. subpage backlinks
@@ -1954,13 +1993,14 @@ class Parser
if( substr( $nodotdot, -1, 1 ) == '/' ) {
$nodotdot = substr( $nodotdot, 0, -1 );
if( '' === $text ) {
- $text = $nodotdot;
+ $text = $nodotdot . $suffix;
}
}
$nodotdot = trim( $nodotdot );
if( $nodotdot != '' ) {
$ret .= '/' . $nodotdot;
}
+ $ret .= $suffix;
}
}
}
@@ -2406,6 +2446,8 @@ class Parser
$oldtz = getenv( 'TZ' );
putenv( 'TZ='.$wgLocaltimezone );
}
+
+ wfSuppressWarnings(); // E_STRICT system time bitching
$localTimestamp = date( 'YmdHis', $ts );
$localMonth = date( 'm', $ts );
$localMonthName = date( 'n', $ts );
@@ -2418,20 +2460,21 @@ class Parser
if ( isset( $wgLocaltimezone ) ) {
putenv( 'TZ='.$oldtz );
}
+ wfRestoreWarnings();
switch ( $index ) {
case 'currentmonth':
- return $varCache[$index] = $wgContLang->formatNum( date( 'm', $ts ) );
+ return $varCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) );
case 'currentmonthname':
- return $varCache[$index] = $wgContLang->getMonthName( date( 'n', $ts ) );
+ return $varCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
case 'currentmonthnamegen':
- return $varCache[$index] = $wgContLang->getMonthNameGen( date( 'n', $ts ) );
+ return $varCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
case 'currentmonthabbrev':
- return $varCache[$index] = $wgContLang->getMonthAbbreviation( date( 'n', $ts ) );
+ return $varCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
case 'currentday':
- return $varCache[$index] = $wgContLang->formatNum( date( 'j', $ts ) );
+ return $varCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) );
case 'currentday2':
- return $varCache[$index] = $wgContLang->formatNum( date( 'd', $ts ) );
+ return $varCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) );
case 'localmonth':
return $varCache[$index] = $wgContLang->formatNum( $localMonth );
case 'localmonthname':
@@ -2445,25 +2488,25 @@ class Parser
case 'localday2':
return $varCache[$index] = $wgContLang->formatNum( $localDay2 );
case 'pagename':
- return $this->mTitle->getText();
+ return wfEscapeWikiText( $this->mTitle->getText() );
case 'pagenamee':
return $this->mTitle->getPartialURL();
case 'fullpagename':
- return $this->mTitle->getPrefixedText();
+ return wfEscapeWikiText( $this->mTitle->getPrefixedText() );
case 'fullpagenamee':
return $this->mTitle->getPrefixedURL();
case 'subpagename':
- return $this->mTitle->getSubpageText();
+ return wfEscapeWikiText( $this->mTitle->getSubpageText() );
case 'subpagenamee':
return $this->mTitle->getSubpageUrlForm();
case 'basepagename':
- return $this->mTitle->getBaseText();
+ return wfEscapeWikiText( $this->mTitle->getBaseText() );
case 'basepagenamee':
return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
case 'talkpagename':
if( $this->mTitle->canTalk() ) {
$talkPage = $this->mTitle->getTalkPage();
- return $talkPage->getPrefixedText();
+ return wfEscapeWikiText( $talkPage->getPrefixedText() );
} else {
return '';
}
@@ -2476,7 +2519,7 @@ class Parser
}
case 'subjectpagename':
$subjPage = $this->mTitle->getSubjectPage();
- return $subjPage->getPrefixedText();
+ return wfEscapeWikiText( $subjPage->getPrefixedText() );
case 'subjectpagenamee':
$subjPage = $this->mTitle->getSubjectPage();
return $subjPage->getPrefixedUrl();
@@ -2505,19 +2548,19 @@ class Parser
case 'subjectspacee':
return( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
case 'currentdayname':
- return $varCache[$index] = $wgContLang->getWeekdayName( date( 'w', $ts ) + 1 );
+ return $varCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
case 'currentyear':
- return $varCache[$index] = $wgContLang->formatNum( date( 'Y', $ts ), true );
+ return $varCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
case 'currenttime':
return $varCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
case 'currenthour':
- return $varCache[$index] = $wgContLang->formatNum( date( 'H', $ts ), true );
+ return $varCache[$index] = $wgContLang->formatNum( gmdate( '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 ) );
+ return $varCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
case 'currentdow':
- return $varCache[$index] = $wgContLang->formatNum( date( 'w', $ts ) );
+ return $varCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) );
case 'localdayname':
return $varCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
case 'localyear':
@@ -3089,7 +3132,7 @@ class Parser
$found = false; //access denied
wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() );
} else {
- $articleContent = $this->fetchTemplate( $title );
+ list($articleContent,$title) = $this->fetchTemplateAndtitle( $title );
if ( $articleContent !== false ) {
$found = true;
$text = $articleContent;
@@ -3220,6 +3263,7 @@ class Parser
PREG_SPLIT_DELIM_CAPTURE);
$text = '';
$nsec = $headingOffset;
+
for( $i = 0; $i < count($m); $i += 2 ) {
$text .= $m[$i];
if (!isset($m[$i + 1]) || $m[$i + 1] == "") continue;
@@ -3255,13 +3299,26 @@ class Parser
/**
* Fetch the unparsed text of a template and register a reference to it.
*/
- function fetchTemplate( $title ) {
- $text = false;
+ function fetchTemplateAndtitle( $title ) {
+ $text = $skip = false;
+ $finalTitle = $title;
// Loop to fetch the article, with up to 1 redirect
for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
- $rev = Revision::newFromTitle( $title );
- $this->mOutput->addTemplate( $title, $title->getArticleID() );
- if ( $rev ) {
+ # Give extensions a chance to select the revision instead
+ $id = false; // Assume current
+ wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( &$this, &$title, &$skip, &$id ) );
+
+ if( $skip ) {
+ $text = false;
+ $this->mOutput->addTemplate( $title, $title->getArticleID(), null );
+ break;
+ }
+ $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title );
+ $rev_id = $rev ? $rev->getId() : 0;
+
+ $this->mOutput->addTemplate( $title, $title->getArticleID(), $rev_id );
+
+ if( $rev ) {
$text = $rev->getText();
} elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
global $wgLang;
@@ -3278,9 +3335,15 @@ class Parser
break;
}
// Redirect?
+ $finalTitle = $title;
$title = Title::newFromRedirect( $text );
}
- return $text;
+ return array($text,$finalTitle);
+ }
+
+ function fetchTemplate( $title ) {
+ $rv = $this->fetchTemplateAndtitle($title);
+ return $rv[0];
}
/**
@@ -3375,7 +3438,13 @@ class Parser
}
/**
- * Detect __TOC__ magic word and set a placeholder
+ * Find the first __TOC__ magic word and set a <!--MWTOC-->
+ * placeholder that will then be replaced by the real TOC in
+ * ->formatHeadings, this works because at this points real
+ * comments will have already been discarded by the sanitizer.
+ *
+ * Any additional __TOC__ magic words left over will be discarded
+ * as there can only be one TOC on the page.
*/
function stripToc( $text ) {
# if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
@@ -3453,17 +3522,13 @@ class Parser
$enoughToc = true;
}
- # Never ever show TOC if no headers
- if( $numMatches < 1 ) {
- $enoughToc = false;
- }
-
# We need this to perform operations on the HTML
$sk = $this->mOptions->getSkin();
# headline counter
$headlineCount = 0;
$sectionCount = 0; # headlineCount excluding template sections
+ $numVisible = 0;
# Ugh .. the TOC should have neat indentation levels which can be
# passed to the skin functions. These are determined here
@@ -3504,7 +3569,9 @@ class Parser
$toclevel++;
$sublevelCount[$toclevel] = 0;
if( $toclevel<$wgMaxTocLevel ) {
+ $prevtoclevel = $toclevel;
$toc .= $sk->tocIndent();
+ $numVisible++;
}
}
elseif ( $level < $prevlevel && $toclevel > 1 ) {
@@ -3528,7 +3595,12 @@ class Parser
}
}
if( $toclevel<$wgMaxTocLevel ) {
- $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
+ if($prevtoclevel < $wgMaxTocLevel) {
+ # Unindent only if the previous toc level was shown :p
+ $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
+ } else {
+ $toc .= $sk->tocLineEnd();
+ }
}
}
else {
@@ -3569,12 +3641,21 @@ class Parser
"\$this->mInterwikiLinkHolders['texts'][\$1]",
$canonized_headline );
- # strip out HTML
- $canonized_headline = preg_replace( '/<.*?' . '>/','',$canonized_headline );
- $tocline = trim( $canonized_headline );
+ # Strip out HTML (other than plain <sup> and <sub>: bug 8393)
+ $tocline = preg_replace(
+ array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ),
+ array( '', '<$1>'),
+ $canonized_headline
+ );
+ $tocline = trim( $tocline );
+
+ # For the anchor, strip out HTML-y stuff period
+ $canonized_headline = preg_replace( '/<.*?'.'>/', '', $canonized_headline );
+ $canonized_headline = trim( $canonized_headline );
+
# Save headline for section edit hint before it's escaped
- $headline_hint = trim( $canonized_headline );
- $canonized_headline = Sanitizer::escapeId( $tocline );
+ $headline_hint = $canonized_headline;
+ $canonized_headline = Sanitizer::escapeId( $canonized_headline );
$refers[$headlineCount] = $canonized_headline;
# count how many in assoc. array so we can track dupes in anchors
@@ -3611,9 +3692,14 @@ class Parser
$sectionCount++;
}
+ # Never ever show TOC if no headers
+ if( $numVisible < 1 ) {
+ $enoughToc = false;
+ }
+
if( $enoughToc ) {
- if( $toclevel<$wgMaxTocLevel ) {
- $toc .= $sk->tocUnindent( $toclevel - 1 );
+ if( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
+ $toc .= $sk->tocUnindent( $prevtoclevel - 1 );
}
$toc = $sk->tocList( $toc );
}
@@ -3759,11 +3845,16 @@ class Parser
* @private
*/
function getUserSig( &$user ) {
+ global $wgMaxSigChars;
+
$username = $user->getName();
$nickname = $user->getOption( 'nickname' );
$nickname = $nickname === '' ? $username : $nickname;
-
- if( $user->getBoolOption( 'fancysig' ) !== false ) {
+
+ if( mb_strlen( $nickname ) > $wgMaxSigChars ) {
+ $nickname = $username;
+ wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
+ } elseif( $user->getBoolOption( 'fancysig' ) !== false ) {
# Sig. might contain markup; validate this
if( $this->validateSig( $nickname ) !== false ) {
# Validated; clean up (if needed) and return it
@@ -3903,6 +3994,14 @@ class Parser
return $oldVal;
}
+ function setTransparentTagHook( $tag, $callback ) {
+ $tag = strtolower( $tag );
+ $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
+ $this->mTransparentTagHooks[$tag] = $callback;
+
+ return $oldVal;
+ }
+
/**
* Create a function, e.g. {{sum:1|2|3}}
* The callback function should have the form:
@@ -4018,6 +4117,8 @@ class Parser
$this->mOutput->addLink( $title, $id );
} elseif ( $linkCache->isBadLink( $pdbk ) ) {
$colours[$pdbk] = 0;
+ } elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) {
+ $colours[$pdbk] = 0;
} else {
# Not in the link cache, add it to the query
if ( !isset( $current ) ) {
@@ -4055,16 +4156,11 @@ class Parser
$linkCache->addGoodLinkObj( $s->page_id, $title );
$this->mOutput->addLink( $title, $s->page_id );
- if ( $threshold > 0 ) {
- $size = $s->page_len;
- if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) {
- $colours[$pdbk] = 1;
- } else {
- $colours[$pdbk] = 2;
- }
- } else {
- $colours[$pdbk] = 1;
- }
+ $colours[$pdbk] = ( $threshold == 0 || (
+ $s->page_len >= $threshold || # always true if $threshold <= 0
+ $s->page_is_redirect ||
+ !Namespace::isContent( $s->page_namespace ) )
+ ? 1 : 2 );
}
}
wfProfileOut( $fname.'-check' );
@@ -4104,7 +4200,7 @@ class Parser
}
// process categories, check if a category exists in some variant
- foreach( $categories as $category){
+ foreach( $categories as $category ){
$variants = $wgContLang->convertLinkToAllVariants($category);
foreach($variants as $variant){
if($variant != $category){
@@ -4324,8 +4420,11 @@ class Parser
$ig->setContextTitle( $this->mTitle );
$ig->setShowBytes( false );
$ig->setShowFilename( false );
- $ig->setParsing();
+ $ig->setParser( $this );
+ $ig->setHideBadImages();
+ $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
$ig->useSkin( $this->mOptions->getSkin() );
+ $ig->mRevisionId = $this->mRevisionId;
if( isset( $params['caption'] ) ) {
$caption = $params['caption'];
@@ -4342,6 +4441,8 @@ class Parser
if( isset( $params['heights'] ) ) {
$ig->setHeights( $params['heights'] );
}
+
+ wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
$lines = explode( "\n", $text );
foreach ( $lines as $line ) {
@@ -4373,7 +4474,7 @@ class Parser
);
$html = $pout->getText();
- $ig->add( new Image( $nt ), $html );
+ $ig->add( $nt, $html );
# Only add real images (bug #5586)
if ( $nt->getNamespace() == NS_IMAGE ) {
@@ -4383,10 +4484,50 @@ class Parser
return $ig->toHTML();
}
+ function getImageParams( $handler ) {
+ if ( $handler ) {
+ $handlerClass = get_class( $handler );
+ } else {
+ $handlerClass = '';
+ }
+ if ( !isset( $this->mImageParams[$handlerClass] ) ) {
+ // Initialise static lists
+ static $internalParamNames = array(
+ 'horizAlign' => array( 'left', 'right', 'center', 'none' ),
+ 'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
+ 'bottom', 'text-bottom' ),
+ 'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
+ 'upright', 'border' ),
+ );
+ static $internalParamMap;
+ if ( !$internalParamMap ) {
+ $internalParamMap = array();
+ foreach ( $internalParamNames as $type => $names ) {
+ foreach ( $names as $name ) {
+ $magicName = str_replace( '-', '_', "img_$name" );
+ $internalParamMap[$magicName] = array( $type, $name );
+ }
+ }
+ }
+
+ // Add handler params
+ $paramMap = $internalParamMap;
+ if ( $handler ) {
+ $handlerParamMap = $handler->getParamMap();
+ foreach ( $handlerParamMap as $magic => $paramName ) {
+ $paramMap[$magic] = array( 'handler', $paramName );
+ }
+ }
+ $this->mImageParams[$handlerClass] = $paramMap;
+ $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
+ }
+ return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
+ }
+
/**
* Parse image options text and use it to make an image
*/
- function makeImage( $nt, $options ) {
+ function makeImage( $title, $options ) {
# @TODO: let the MediaHandler specify its transform parameters
#
# Check if the options text is of the form "options|alt text"
@@ -4398,6 +4539,9 @@ class Parser
# * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
# * center center the image
# * framed Keep original image size, no magnify-button.
+ # * frameless like 'thumb' but without a frame. Keeps user preferences for width
+ # * upright reduce width for upright images, rounded to full __0 px
+ # * border draw a 1px border around the image
# vertical-align values (no % or length right now):
# * baseline
# * sub
@@ -4407,67 +4551,66 @@ class Parser
# * middle
# * bottom
# * text-bottom
+
+ $parts = array_map( 'trim', explode( '|', $options) );
+ $sk = $this->mOptions->getSkin();
+ # Give extensions a chance to select the file revision for us
+ $skip = $time = false;
+ wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time ) );
- $part = array_map( 'trim', explode( '|', $options) );
-
- $mwAlign = array();
- $alignments = array( 'left', 'right', 'center', 'none', 'baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom' );
- foreach ( $alignments as $alignment ) {
- $mwAlign[$alignment] =& MagicWord::get( 'img_'.$alignment );
+ if ( $skip ) {
+ return $sk->makeLinkObj( $title );
}
- $mwThumb =& MagicWord::get( 'img_thumbnail' );
- $mwManualThumb =& MagicWord::get( 'img_manualthumb' );
- $mwWidth =& MagicWord::get( 'img_width' );
- $mwFramed =& MagicWord::get( 'img_framed' );
- $mwPage =& MagicWord::get( 'img_page' );
- $caption = '';
- $params = array();
- $framed = $thumb = false;
- $manual_thumb = '' ;
- $align = $valign = '';
- $sk = $this->mOptions->getSkin();
+ # Get parameter map
+ $file = wfFindFile( $title, $time );
+ $handler = $file ? $file->getHandler() : false;
- foreach( $part as $val ) {
- if ( !is_null( $mwThumb->matchVariableStartToEnd($val) ) ) {
- $thumb=true;
- } elseif ( ! is_null( $match = $mwManualThumb->matchVariableStartToEnd($val) ) ) {
- # use manually specified thumbnail
- $thumb=true;
- $manual_thumb = $match;
- } else {
- foreach( $alignments as $alignment ) {
- if ( ! is_null( $mwAlign[$alignment]->matchVariableStartToEnd($val) ) ) {
- switch ( $alignment ) {
- case 'left': case 'right': case 'center': case 'none':
- $align = $alignment; break;
- default:
- $valign = $alignment;
- }
- continue 2;
- }
- }
- if ( ! is_null( $match = $mwPage->matchVariableStartToEnd($val) ) ) {
- # Select a page in a multipage document
- $params['page'] = $match;
- } elseif ( !isset( $params['width'] ) && ! is_null( $match = $mwWidth->matchVariableStartToEnd($val) ) ) {
- wfDebug( "img_width match: $match\n" );
- # $match is the image width in pixels
+ list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
+
+ # Process the input parameters
+ $caption = '';
+ $params = array( 'frame' => array(), 'handler' => array(),
+ 'horizAlign' => array(), 'vertAlign' => array() );
+ foreach( $parts as $part ) {
+ list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
+ if ( isset( $paramMap[$magicName] ) ) {
+ list( $type, $paramName ) = $paramMap[$magicName];
+ $params[$type][$paramName] = $value;
+
+ // Special case; width and height come in one variable together
+ if( $type == 'handler' && $paramName == 'width' ) {
$m = array();
- if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $match, $m ) ) {
- $params['width'] = intval( $m[1] );
- $params['height'] = intval( $m[2] );
+ if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $value, $m ) ) {
+ $params[$type]['width'] = intval( $m[1] );
+ $params[$type]['height'] = intval( $m[2] );
} else {
- $params['width'] = intval($match);
+ $params[$type]['width'] = intval( $value );
}
- } elseif ( ! is_null( $mwFramed->matchVariableStartToEnd($val) ) ) {
- $framed=true;
- } else {
- $caption = $val;
+ }
+ } else {
+ $caption = $part;
+ }
+ }
+
+ # Process alignment parameters
+ if ( $params['horizAlign'] ) {
+ $params['frame']['align'] = key( $params['horizAlign'] );
+ }
+ if ( $params['vertAlign'] ) {
+ $params['frame']['valign'] = key( $params['vertAlign'] );
+ }
+
+ # Validate the handler parameters
+ if ( $handler ) {
+ foreach ( $params['handler'] as $name => $value ) {
+ if ( !$handler->validateParam( $name, $value ) ) {
+ unset( $params['handler'][$name] );
}
}
}
+
# Strip bad stuff out of the alt text
$alt = $this->replaceLinkHoldersText( $caption );
@@ -4477,8 +4620,18 @@ class Parser
$alt = $this->mStripState->unstripBoth( $alt );
$alt = Sanitizer::stripAllTags( $alt );
+ $params['frame']['alt'] = $alt;
+ $params['frame']['caption'] = $caption;
+
# Linker does the rest
- return $sk->makeImageLinkObj( $nt, $caption, $alt, $align, $params, $framed, $thumb, $manual_thumb, $valign );
+ $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'] );
+
+ # Give the handler a chance to modify the parser object
+ if ( $handler ) {
+ $handler->parserTransformHook( $this, $file );
+ }
+
+ return $ret;
}
/**
@@ -4517,7 +4670,7 @@ class Parser
/**#@+
* Accessor
*/
- function getTags() { return array_keys( $this->mTagHooks ); }
+ function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); }
/**#@-*/
@@ -4537,6 +4690,10 @@ class Parser
* for "replace", the whole page with the section replaced.
*/
private function extractSections( $text, $section, $mode, $newtext='' ) {
+ # I.... _hope_ this is right.
+ # Otherwise, sometimes we don't have things initialized properly.
+ $this->clearState();
+
# strip NOWIKI etc. to avoid confusion (true-parameter causes HTML
# comments to be stripped as well)
$stripState = new StripState;
@@ -4554,7 +4711,7 @@ class Parser
# now that we can be sure that no pseudo-sections are in the source,
# split it up by section
$uniq = preg_quote( $this->uniqPrefix(), '/' );
- $comment = "(?:$uniq-!--.*?QINU)";
+ $comment = "(?:$uniq-!--.*?QINU\x07)";
$secs = preg_split(
"/
(
@@ -4717,7 +4874,6 @@ class Parser
: $this->mTitle->getPrefixedText();
}
}
-
}
/**
@@ -4770,5 +4926,3 @@ class StripState {
return $text;
}
}
-
-?>
diff --git a/includes/ParserCache.php b/includes/ParserCache.php
index 1489fcf9..129b7132 100644
--- a/includes/ParserCache.php
+++ b/includes/ParserCache.php
@@ -116,4 +116,4 @@ class ParserCache {
}
-?>
+
diff --git a/includes/ParserOptions.php b/includes/ParserOptions.php
index e335720f..2200bfea 100644
--- a/includes/ParserOptions.php
+++ b/includes/ParserOptions.php
@@ -116,4 +116,4 @@ class ParserOptions
}
}
-?>
+
diff --git a/includes/ParserOutput.php b/includes/ParserOutput.php
index 03f1819c..d4daf1d1 100644
--- a/includes/ParserOutput.php
+++ b/includes/ParserOutput.php
@@ -14,13 +14,18 @@ class ParserOutput
$mTitleText, # title text of the chosen language variant
$mLinks, # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken.
$mTemplates, # 2-D map of NS/DBK to ID for the template references. ID=zero for broken.
+ $mTemplateIds, # 2-D map of NS/DBK to rev ID for the template references. ID=zero for broken.
$mImages, # DB keys of the images used, in the array key only
$mExternalLinks, # External link URLs, in the key only
- $mHTMLtitle, # Display HTML title
- $mSubtitle, # Additional subtitle
$mNewSection, # Show a new section link?
$mNoGallery, # No gallery on category page? (__NOGALLERY__)
- $mHeadItems; # Items to put in the <head> section
+ $mHeadItems, # Items to put in the <head> section
+ $mOutputHooks; # Hook tags as per $wgParserOutputHooks
+
+ /**
+ * Overridden title for display
+ */
+ private $displayTitle = false;
function ParserOutput( $text = '', $languageLinks = array(), $categoryLinks = array(),
$containsOldMagic = false, $titletext = '' )
@@ -36,15 +41,15 @@ class ParserOutput
$this->mTemplates = array();
$this->mImages = array();
$this->mExternalLinks = array();
- $this->mHTMLtitle = "" ;
- $this->mSubtitle = "" ;
$this->mNewSection = false;
$this->mNoGallery = false;
$this->mHeadItems = array();
+ $this->mTemplateIds = array();
+ $this->mOutputHooks = array();
}
function getText() { return $this->mText; }
- function &getLanguageLinks() { return $this->mLanguageLinks; }
+ function &getLanguageLinks() { return $this->mLanguageLinks; }
function getCategoryLinks() { return array_keys( $this->mCategories ); }
function &getCategories() { return $this->mCategories; }
function getCacheTime() { return $this->mCacheTime; }
@@ -55,6 +60,7 @@ class ParserOutput
function &getExternalLinks() { return $this->mExternalLinks; }
function getNoGallery() { return $this->mNoGallery; }
function getSubtitle() { return $this->mSubtitle; }
+ function getOutputHooks() { return (array)$this->mOutputHooks; }
function containsOldMagic() { return $this->mContainsOldMagic; }
function setText( $text ) { return wfSetVar( $this->mText, $text ); }
@@ -63,13 +69,15 @@ class ParserOutput
function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); }
function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); }
function setTitleText( $t ) { return wfSetVar($this->mTitleText, $t); }
- function setSubtitle( $st ) { return wfSetVar( $this->mSubtitle, $st ); }
function addCategory( $c, $sort ) { $this->mCategories[$c] = $sort; }
- function addImage( $name ) { $this->mImages[$name] = 1; }
function addLanguageLink( $t ) { $this->mLanguageLinks[] = $t; }
function addExternalLink( $url ) { $this->mExternalLinks[$url] = 1; }
+ function addOutputHook( $hook, $data = false ) {
+ $this->mOutputHooks[] = array( $hook, $data );
+ }
+
function setNewSection( $value ) {
$this->mNewSection = (bool)$value;
}
@@ -88,14 +96,22 @@ class ParserOutput
}
$this->mLinks[$ns][$dbk] = $id;
}
+
+ function addImage( $name ) {
+ $this->mImages[$name] = 1;
+ }
- function addTemplate( $title, $id ) {
+ function addTemplate( $title, $page_id, $rev_id ) {
$ns = $title->getNamespace();
$dbk = $title->getDBkey();
if ( !isset( $this->mTemplates[$ns] ) ) {
$this->mTemplates[$ns] = array();
}
- $this->mTemplates[$ns][$dbk] = $id;
+ $this->mTemplates[$ns][$dbk] = $page_id;
+ if ( !isset( $this->mTemplateIds[$ns] ) ) {
+ $this->mTemplateIds[$ns] = array();
+ }
+ $this->mTemplateIds[$ns][$dbk] = $rev_id; // For versioning
}
/**
@@ -128,6 +144,27 @@ class ParserOutput
$this->mHeadItems[] = $section;
}
}
+
+ /**
+ * Override the title to be used for display
+ * -- this is assumed to have been validated
+ * (check equal normalisation, etc.)
+ *
+ * @param string $text Desired title text
+ */
+ public function setDisplayTitle( $text ) {
+ $this->displayTitle = $text;
+ }
+
+ /**
+ * Get the title to be used for display
+ *
+ * @return string
+ */
+ public function getDisplayTitle() {
+ return $this->displayTitle;
+ }
+
}
-?>
+
diff --git a/includes/PatrolLog.php b/includes/PatrolLog.php
index a22839ff..35cb4a02 100644
--- a/includes/PatrolLog.php
+++ b/includes/PatrolLog.php
@@ -46,14 +46,19 @@ class PatrolLog {
# these conditions would have gone into recentchanges, which we aren't
# supposed to be updating
if( is_object( $skin ) ) {
- list( $cur, $prev, $auto ) = $params;
+ list( $cur, /* $prev */, $auto ) = $params;
# Standard link to the page in question
$link = $skin->makeLinkObj( $title );
- # Generate a diff link
- $bits[] = 'oldid=' . urlencode( $cur );
- $bits[] = 'diff=prev';
- $bits = implode( '&', $bits );
- $diff = $skin->makeLinkObj( $title, htmlspecialchars( wfMsg( 'patrol-log-diff', $cur ) ), $bits );
+ if( $title->exists() ) {
+ # Generate a diff link
+ $bits[] = 'oldid=' . urlencode( $cur );
+ $bits[] = 'diff=prev';
+ $bits = implode( '&', $bits );
+ $diff = $skin->makeKnownLinkObj( $title, htmlspecialchars( wfMsg( 'patrol-log-diff', $cur ) ), $bits );
+ } else {
+ # Don't bother with a diff link, it's useless
+ $diff = htmlspecialchars( wfMsg( 'patrol-log-diff', $cur ) );
+ }
# Indicate whether or not the patrolling was automatic
$auto = $auto ? wfMsgHtml( 'patrol-log-auto' ) : '';
# Put it all together
@@ -80,4 +85,3 @@ class PatrolLog {
}
-?> \ No newline at end of file
diff --git a/includes/Profiler.php b/includes/Profiler.php
index da3a82ed..8e1cd147 100644
--- a/includes/Profiler.php
+++ b/includes/Profiler.php
@@ -301,6 +301,9 @@ class Profiler {
* @static
*/
function logToDB($name, $timeSum, $eventCount) {
+ # Do not log anything if database is readonly (bug 5375)
+ if( wfReadOnly() ) { return; }
+
# Warning: $wguname is a live patch, it should be moved to Setup.php
global $wguname, $wgProfilePerHost;
@@ -361,4 +364,4 @@ class Profiler {
}
-?>
+
diff --git a/includes/ProfilerSimple.php b/includes/ProfilerSimple.php
index f43c7dfc..b07f2517 100644
--- a/includes/ProfilerSimple.php
+++ b/includes/ProfilerSimple.php
@@ -122,4 +122,4 @@ class ProfilerSimple extends Profiler {
}
}
}
-?>
+
diff --git a/includes/ProfilerSimpleUDP.php b/includes/ProfilerSimpleUDP.php
index 500f1cbd..7d2f7e21 100644
--- a/includes/ProfilerSimpleUDP.php
+++ b/includes/ProfilerSimpleUDP.php
@@ -37,4 +37,4 @@ class ProfilerSimpleUDP extends ProfilerSimple {
socket_sendto($sock,$packet,$plength,0x100,$wgUDPProfilerHost,$wgUDPProfilerPort);
}
}
-?>
+
diff --git a/includes/ProfilerStub.php b/includes/ProfilerStub.php
index 4cf0aa44..c41845a4 100644
--- a/includes/ProfilerStub.php
+++ b/includes/ProfilerStub.php
@@ -23,4 +23,4 @@ function wfGetProfilingOutput( $s, $e ) {}
function wfProfileClose() {}
$wgProfiling = false;
-?>
+
diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php
index 3cafbd55..c249ec12 100644
--- a/includes/ProtectionForm.php
+++ b/includes/ProtectionForm.php
@@ -76,11 +76,12 @@ class ProtectionForm {
}
function execute() {
- global $wgRequest;
+ global $wgRequest, $wgOut;
if( $wgRequest->wasPosted() ) {
if( $this->save() ) {
- global $wgOut;
- $wgOut->redirect( $this->mTitle->getFullUrl() );
+ $article = new Article( $this->mTitle );
+ $q = $article->isRedirect() ? 'redirect=no' : '';
+ $wgOut->redirect( $this->mTitle->getFullUrl( $q ) );
}
} else {
$this->show();
@@ -99,7 +100,7 @@ class ProtectionForm {
return;
}
- list( $cascadeSources, $restrictions ) = $this->mTitle->getCascadeProtectionSources();
+ list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
if ( "" != $err ) {
$wgOut->setSubtitle( wfMsgHtml( 'formerror' ) );
@@ -188,6 +189,13 @@ class ProtectionForm {
if( !$ok ) {
throw new FatalError( "Unknown error at restriction save time." );
}
+
+ if( $wgRequest->getCheck( 'mwProtectWatch' ) ) {
+ $this->mArticle->doWatch();
+ } elseif( $this->mTitle->userIsWatching() ) {
+ $this->mArticle->doUnwatch();
+ }
+
return $ok;
}
@@ -232,18 +240,18 @@ class ProtectionForm {
$out .= "</tbody>\n";
$out .= "</table>\n";
- global $wgEnableCascadingProtection;
-
- if ($wgEnableCascadingProtection)
- $out .= $this->buildCascadeInput();
-
$out .= "<table>\n";
$out .= "<tbody>\n";
+ global $wgEnableCascadingProtection;
+ if( $wgEnableCascadingProtection )
+ $out .= '<tr><td></td><td>' . $this->buildCascadeInput() . "</td></tr>\n";
+
$out .= $this->buildExpiryInput();
if( !$this->disabled ) {
$out .= "<tr><td>" . $this->buildReasonInput() . "</td></tr>\n";
+ $out .= "<tr><td></td><td>" . $this->buildWatchInput() . "</td></tr>\n";
$out .= "<tr><td></td><td>" . $this->buildSubmit() . "</td></tr>\n";
}
@@ -270,22 +278,28 @@ class ProtectionForm {
$out = wfOpenElement( 'select', $attribs );
foreach( $wgRestrictionLevels as $key ) {
- $out .= $this->buildOption( $key, $selected );
+ $out .= Xml::option( $this->getOptionLabel( $key ), $key, $key == $selected );
}
$out .= "</select>\n";
return $out;
}
- function buildOption( $key, $selected ) {
- $text = ( $key == '' )
- ? wfMsg( 'protect-default' )
- : wfMsg( "protect-level-$key" );
- $selectedAttrib = ($selected == $key)
- ? array( 'selected' => 'selected' )
- : array();
- return wfElement( 'option',
- array( 'value' => $key ) + $selectedAttrib,
- $text );
+ /**
+ * Prepare the label for a protection selector option
+ *
+ * @param string $permission Permission required
+ * @return string
+ */
+ private function getOptionLabel( $permission ) {
+ if( $permission == '' ) {
+ return wfMsg( 'protect-default' );
+ } else {
+ $key = "protect-level-{$permission}";
+ $msg = wfMsg( $key );
+ if( wfEmptyMsg( $key, $msg ) )
+ $msg = wfMsg( 'protect-fallback', $permission );
+ return $msg;
+ }
}
function buildReasonInput() {
@@ -309,22 +323,21 @@ class ProtectionForm {
}
function buildExpiryInput() {
- $id = 'mwProtect-expiry';
-
- $ci = "<tr> <td align=\"right\">";
- $ci .= wfElement( 'label', array (
- 'id' => "$id-label",
- 'for' => $id ),
- wfMsg( 'protectexpiry' ) );
- $ci .= "</td> <td align=\"left\">";
- $ci .= wfElement( 'input', array(
- 'size' => 60,
- 'name' => $id,
- 'id' => $id,
- 'value' => $this->mExpiry ) + $this->disabledAttrib );
- $ci .= "</td></tr>";
-
- return $ci;
+ $attribs = array( 'id' => 'expires' ) + $this->disabledAttrib;
+ return '<tr>'
+ . '<td><label for="expires">' . wfMsgExt( 'protectexpiry', array( 'parseinline' ) ) . '</label></td>'
+ . '<td>' . Xml::input( 'mwProtect-expiry', 60, $this->mExpiry, $attribs ) . '</td>'
+ . '</tr>';
+ }
+
+ function buildWatchInput() {
+ global $wgUser;
+ return Xml::checkLabel(
+ wfMsg( 'watchthis' ),
+ 'mwProtectWatch',
+ 'mwProtectWatch',
+ $this->mTitle->userIsWatching() || $wgUser->getOption( 'watchdefault' )
+ );
}
function buildSubmit() {
@@ -360,7 +373,7 @@ class ProtectionForm {
* @access private
*/
function showLogExtract( &$out ) {
- # Show relevant lines from the deletion log:
+ # Show relevant lines from the protection log:
$out->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'protect' ) ) . "</h2>\n" );
$logViewer = new LogViewer(
new LogReader(
@@ -369,6 +382,5 @@ class ProtectionForm {
'type' => 'protect' ) ) ) );
$logViewer->showList( $out );
}
-}
-?>
+} \ No newline at end of file
diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php
index f72b640f..6585de42 100644
--- a/includes/ProxyTools.php
+++ b/includes/ProxyTools.php
@@ -260,4 +260,4 @@ function wfIsAOLProxy( $ip ) {
-?>
+
diff --git a/includes/QueryPage.php b/includes/QueryPage.php
index 143c8be6..06710b6d 100644
--- a/includes/QueryPage.php
+++ b/includes/QueryPage.php
@@ -25,6 +25,7 @@ $wgQueryPages = array(
array( 'MostcategoriesPage', 'Mostcategories' ),
array( 'MostimagesPage', 'Mostimages' ),
array( 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ),
+ array( 'SpecialMostlinkedtemplates', 'Mostlinkedtemplates' ),
array( 'MostlinkedPage', 'Mostlinked' ),
array( 'MostrevisionsPage', 'Mostrevisions' ),
array( 'FewestrevisionsPage', 'Fewestrevisions' ),
@@ -33,6 +34,7 @@ $wgQueryPages = array(
array( 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ),
array( 'UncategorizedPagesPage', 'Uncategorizedpages' ),
array( 'UncategorizedImagesPage', 'Uncategorizedimages' ),
+ array( 'UncategorizedTemplatesPage', 'Uncategorizedtemplates' ),
array( 'UnusedCategoriesPage', 'Unusedcategories' ),
array( 'UnusedimagesPage', 'Unusedimages' ),
array( 'WantedCategoriesPage', 'Wantedcategories' ),
@@ -332,7 +334,8 @@ class QueryPage {
$num = $dbr->numRows($res);
$this->preprocessResults( $dbr, $res );
- $sk = $wgUser->getSkin();
+
+ $wgOut->addHtml( XML::openElement( 'div', array('class' => 'mw-spcontent') ) );
# Top header and navigation
if( $shownavigation ) {
@@ -347,6 +350,7 @@ class QueryPage {
# No results to show, so don't bother with "showing X of Y" etc.
# -- just let the user know and give up now
$wgOut->addHtml( '<p>' . wfMsgHtml( 'specialpage-empty' ) . '</p>' );
+ $wgOut->addHtml( XML::closeElement( 'div' ) );
return;
}
}
@@ -365,6 +369,8 @@ class QueryPage {
if( $shownavigation ) {
$wgOut->addHtml( '<p>' . $paging . '</p>' );
}
+
+ $wgOut->addHtml( XML::closeElement( 'div' ) );
return $num;
}
@@ -397,7 +403,7 @@ class QueryPage {
? ' class="not-patrolled"'
: '';
$html[] = $this->listoutput
- ? $format
+ ? $line
: "<li{$attr}>{$line}</li>\n";
}
}
@@ -411,7 +417,7 @@ class QueryPage {
? ' class="not-patrolled"'
: '';
$html[] = $this->listoutput
- ? $format
+ ? $line
: "<li{$attr}>{$line}</li>\n";
}
}
@@ -428,11 +434,11 @@ class QueryPage {
}
function openList( $offset ) {
- return "<ol start='" . ( $offset + 1 ) . "' class='special'>";
+ return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
}
function closeList() {
- return '</ol>';
+ return "</ol>\n";
}
/**
@@ -525,4 +531,4 @@ class QueryPage {
}
}
-?>
+
diff --git a/includes/RawPage.php b/includes/RawPage.php
index 93484829..9df94e50 100644
--- a/includes/RawPage.php
+++ b/includes/RawPage.php
@@ -220,4 +220,4 @@ class RawPage {
return $text;
}
}
-?>
+
diff --git a/includes/RecentChange.php b/includes/RecentChange.php
index fced4343..79f32d0c 100644
--- a/includes/RecentChange.php
+++ b/includes/RecentChange.php
@@ -80,6 +80,31 @@ class RecentChange
return NULL;
}
}
+
+ /**
+ * Find the first recent change matching some specific conditions
+ *
+ * @param array $conds Array of conditions
+ * @param mixed $fname Override the method name in profiling/logs
+ * @return RecentChange
+ */
+ public static function newFromConds( $conds, $fname = false ) {
+ if( $fname === false )
+ $fname = __METHOD__;
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select(
+ 'recentchanges',
+ '*',
+ $conds,
+ $fname
+ );
+ if( $res instanceof ResultWrapper && $res->numRows() > 0 ) {
+ $row = $res->fetchObject();
+ $res->free();
+ return self::newFromRow( $row );
+ }
+ return null;
+ }
# Accessors
@@ -195,10 +220,11 @@ class RecentChange
global $wgUseEnotif;
if( $wgUseEnotif ) {
# this would be better as an extension hook
+ global $wgUser;
include_once( "UserMailer.php" );
$enotif = new EmailNotification();
$title = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
- $enotif->notifyOnPageChange( $title,
+ $enotif->notifyOnPageChange( $wgUser, $title,
$this->mAttribs['rc_timestamp'],
$this->mAttribs['rc_comment'],
$this->mAttribs['rc_minor'],
@@ -209,24 +235,30 @@ class RecentChange
wfRunHooks( 'RecentChange_save', array( &$this ) );
}
- # Marks a certain row as patrolled
- function markPatrolled( $rcid )
- {
- $fname = 'RecentChange::markPatrolled';
-
+ /**
+ * Mark a given change as patrolled
+ *
+ * @param mixed $change RecentChange or corresponding rc_id
+ */
+ public static function markPatrolled( $change ) {
+ $rcid = $change instanceof RecentChange
+ ? $change->mAttribs['rc_id']
+ : $change;
$dbw = wfGetDB( DB_MASTER );
-
- $dbw->update( 'recentchanges',
- array( /* SET */
+ $dbw->update(
+ 'recentchanges',
+ array(
'rc_patrolled' => 1
- ), array( /* WHERE */
+ ),
+ array(
'rc_id' => $rcid
- ), $fname
+ ),
+ __METHOD__
);
}
# Makes an entry in the database corresponding to an edit
- /*static*/ function notifyEdit( $timestamp, &$title, $minor, &$user, $comment,
+ public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment,
$oldId, $lastTimestamp, $bot = "default", $ip = '', $oldSize = 0, $newSize = 0,
$newId = 0)
{
@@ -280,10 +312,8 @@ class RecentChange
* 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",
+ public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot = 'default',
$ip='', $size = 0, $newId = 0 )
{
if ( !$ip ) {
@@ -292,7 +322,7 @@ class RecentChange
$ip = '';
}
}
- if ( $bot == 'default' ) {
+ if ( $bot === 'default' ) {
$bot = $user->isAllowed( 'bot' );
}
@@ -331,7 +361,7 @@ class RecentChange
}
# Makes an entry in the database corresponding to a rename
- /*static*/ function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false )
+ public static function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false )
{
if ( !$ip ) {
$ip = wfGetIP();
@@ -372,17 +402,17 @@ class RecentChange
$rc->save();
}
- /* static */ function notifyMoveToNew( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) {
+ public static function notifyMoveToNew( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) {
RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, false );
}
- /* static */ function notifyMoveOverRedirect( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) {
+ public static function notifyMoveOverRedirect( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) {
RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, true );
}
# A log entry is different to an edit in that previous revisions are
# not kept
- /*static*/ function notifyLog( $timestamp, &$title, &$user, $comment, $ip='',
+ public static function notifyLog( $timestamp, &$title, &$user, $comment, $ip='',
$type, $action, $target, $logComment, $params )
{
if ( !$ip ) {
@@ -595,4 +625,4 @@ class RecentChange
}
}
}
-?>
+
diff --git a/includes/RefreshLinksJob.php b/includes/RefreshLinksJob.php
new file mode 100644
index 00000000..367d994f
--- /dev/null
+++ b/includes/RefreshLinksJob.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * Background job to update links for a given title.
+ */
+class RefreshLinksJob extends Job {
+
+ function __construct( $title, $params = '', $id = 0 ) {
+ parent::__construct( 'refreshLinks', $title, $params, $id );
+ }
+
+ /**
+ * Run a refreshLinks job
+ * @return boolean success
+ */
+ function run() {
+ global $wgParser;
+ wfProfileIn( __METHOD__ );
+
+ $linkCache =& LinkCache::singleton();
+ $linkCache->clear();
+
+ if ( is_null( $this->title ) ) {
+ $this->error = "refreshLinks: Invalid title";
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ $revision = Revision::newFromTitle( $this->title );
+ if ( !$revision ) {
+ $this->error = 'refreshLinks: Article not found "' . $this->title->getPrefixedDBkey() . '"';
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ wfProfileIn( __METHOD__.'-parse' );
+ $options = new ParserOptions;
+ $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, true, true, $revision->getId() );
+ wfProfileOut( __METHOD__.'-parse' );
+ wfProfileIn( __METHOD__.'-update' );
+ $update = new LinksUpdate( $this->title, $parserOutput, false );
+ $update->doUpdate();
+ wfProfileOut( __METHOD__.'-update' );
+ wfProfileOut( __METHOD__ );
+ return true;
+ }
+}
+
diff --git a/includes/Revision.php b/includes/Revision.php
index 71f214e3..39470923 100644
--- a/includes/Revision.php
+++ b/includes/Revision.php
@@ -888,4 +888,4 @@ define( 'MW_REV_DELETED_USER', Revision::DELETED_USER );
define( 'MW_REV_DELETED_RESTRICTED', Revision::DELETED_RESTRICTED );
-?>
+
diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php
index fa5416dc..f2dcbf94 100644
--- a/includes/Sanitizer.php
+++ b/includes/Sanitizer.php
@@ -330,6 +330,9 @@ $wgHtmlEntityAliases = array(
* @addtogroup Parser
*/
class Sanitizer {
+ const NONE = 0;
+ const INITIAL_NONLETTER = 1;
+
/**
* Cleans up HTML, removes dangerous tags and attributes, and
* removes HTML comments
@@ -339,7 +342,7 @@ class Sanitizer {
* @param array $args for the processing callback
* @return string
*/
- static function removeHTMLtags( $text, $processCallback = null, $args = array() ) {
+ static function removeHTMLtags( $text, $processCallback = null, $args = array(), $extratags = array() ) {
global $wgUseTidy;
static $htmlpairs, $htmlsingle, $htmlsingleonly, $htmlnest, $tabletags,
@@ -349,13 +352,13 @@ class Sanitizer {
if ( !$staticInitialised ) {
- $htmlpairs = array( # Tags that must be closed
+ $htmlpairs = array_merge( $extratags, 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'
);
@@ -566,6 +569,7 @@ class Sanitizer {
*
* - Discards attributes not on a whitelist for the given element
* - Unsafe style attributes are discarded
+ * - Invalid id attributes are reencoded
*
* @param array $attribs
* @param string $element
@@ -575,7 +579,27 @@ class Sanitizer {
* @todo Check for unique id attribute :P
*/
static function validateTagAttributes( $attribs, $element ) {
- $whitelist = array_flip( Sanitizer::attributeWhitelist( $element ) );
+ return Sanitizer::validateAttributes( $attribs,
+ Sanitizer::attributeWhitelist( $element ) );
+ }
+
+ /**
+ * Take an array of attribute names and values and normalize or discard
+ * illegal values for the given whitelist.
+ *
+ * - Discards attributes not the given whitelist
+ * - Unsafe style attributes are discarded
+ * - Invalid id attributes are reencoded
+ *
+ * @param array $attribs
+ * @param array $whitelist list of allowed attribute names
+ * @return array
+ *
+ * @todo Check for legal values where the DTD limits things.
+ * @todo Check for unique id attribute :P
+ */
+ static function validateAttributes( $attribs, $whitelist ) {
+ $whitelist = array_flip( $whitelist );
$out = array();
foreach( $attribs as $attribute => $value ) {
if( !isset( $whitelist[$attribute] ) ) {
@@ -602,6 +626,33 @@ class Sanitizer {
}
/**
+ * Merge two sets of HTML attributes.
+ * Conflicting items in the second set will override those
+ * in the first, except for 'class' attributes which will be
+ * combined.
+ *
+ * @todo implement merging for other attributes such as style
+ * @param array $a
+ * @param array $b
+ * @return array
+ */
+ static function mergeAttributes( $a, $b ) {
+ $out = array_merge( $a, $b );
+ if( isset( $a['class'] )
+ && isset( $b['class'] )
+ && $a['class'] !== $b['class'] ) {
+
+ $out['class'] = implode( ' ',
+ array_unique(
+ preg_split( '/\s+/',
+ $a['class'] . ' ' . $b['class'],
+ -1,
+ PREG_SPLIT_NO_EMPTY ) ) );
+ }
+ return $out;
+ }
+
+ /**
* Pick apart some CSS and check it for forbidden or unsafe structures.
* Returns a sanitized string, or false if it was just too evil.
*
@@ -730,20 +781,29 @@ class Sanitizer {
* name attributes
* @see http://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with the id attribute
*
- * @static
- *
- * @param string $id
+ * @param string $id Id to validate
+ * @param int $flags Currently only two values: Sanitizer::INITIAL_NONLETTER
+ * (default) permits initial non-letter characters,
+ * such as if you're adding a prefix to them.
+ * Sanitizer::NONE will prepend an 'x' if the id
+ * would otherwise start with a nonletter.
* @return string
*/
- static function escapeId( $id ) {
+ static function escapeId( $id, $flags = Sanitizer::INITIAL_NONLETTER ) {
static $replace = array(
'%3A' => ':',
'%' => '.'
);
$id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) );
-
- return str_replace( array_keys( $replace ), array_values( $replace ), $id );
+ $id = str_replace( array_keys( $replace ), array_values( $replace ), $id );
+
+ if( ~$flags & Sanitizer::INITIAL_NONLETTER
+ && !preg_match( '/[a-zA-Z]/', $id[0] ) ) {
+ // Initial character must be a letter!
+ $id = "x$id";
+ }
+ return $id;
}
/**
@@ -1159,6 +1219,11 @@ class Sanitizer {
# 11.2.6
'td' => array_merge( $common, $tablecell, $tablealign ),
'th' => array_merge( $common, $tablecell, $tablealign ),
+
+ # 13.2
+ # Not usually allowed, but may be used for extension-style hooks
+ # such as <math> when it is rasterized
+ 'img' => array_merge( $common, array( 'alt' ) ),
# 15.2.1
'tt' => $common,
@@ -1185,6 +1250,11 @@ class Sanitizer {
'rb' => $common,
'rt' => $common, #array_merge( $common, array( 'rbspan' ) ),
'rp' => $common,
+
+ # MathML root element, where used for extensions
+ # 'title' may not be 100% valid here; it's XHTML
+ # http://www.w3.org/TR/REC-MathML/
+ 'math' => array( 'class', 'style', 'id', 'title' ),
);
return $whitelist;
}
@@ -1274,4 +1344,4 @@ class Sanitizer {
}
-?>
+
diff --git a/includes/SearchEngine.php b/includes/SearchEngine.php
index 24795ba9..11fc3deb 100644
--- a/includes/SearchEngine.php
+++ b/includes/SearchEngine.php
@@ -40,12 +40,10 @@ class SearchEngine {
* If an exact title match can be find, or a very slightly close match,
* return the title. If no match, returns NULL.
*
- * @static
* @param string $term
* @return Title
- * @private
*/
- function getNearMatch( $searchterm ) {
+ public static function getNearMatch( $searchterm ) {
global $wgContLang;
$allSearchTerms = array($searchterm);
@@ -124,8 +122,8 @@ class SearchEngine {
# There may have been a funny upload, or it may be on a shared
# file repository such as Wikimedia Commons.
if( $title->getNamespace() == NS_IMAGE ) {
- $image = new Image( $title );
- if( $image->exists() ) {
+ $image = wfFindFile( $title );
+ if( $image ) {
return $title;
}
}
@@ -176,9 +174,8 @@ class SearchEngine {
/**
* Make a list of searchable namespaces and their canonical names.
* @return array
- * @access public
*/
- function searchableNamespaces() {
+ public static function searchableNamespaces() {
global $wgContLang;
$arr = array();
foreach( $wgContLang->getNamespaces() as $ns => $name ) {
@@ -325,6 +322,14 @@ class SearchResultSet {
function next() {
return false;
}
+
+ /**
+ * Frees the result set, if applicable.
+ * @ access public
+ */
+ function free() {
+ // ...
+ }
}
@@ -366,4 +371,4 @@ class SearchEngineDummy {
function searchtitle() {}
function searchtext() {}
}
-?>
+
diff --git a/includes/SearchMySQL.php b/includes/SearchMySQL.php
index 0e02a684..905075ef 100644
--- a/includes/SearchMySQL.php
+++ b/includes/SearchMySQL.php
@@ -200,6 +200,10 @@ class MySQLSearchResultSet extends SearchResultSet {
return new SearchResult( $row );
}
}
+
+ function free() {
+ $this->mResultSet->free();
+ }
}
-?>
+
diff --git a/includes/SearchMySQL4.php b/includes/SearchMySQL4.php
index 97ce3850..6d2bbfef 100644
--- a/includes/SearchMySQL4.php
+++ b/includes/SearchMySQL4.php
@@ -65,4 +65,4 @@ class SearchMySQL4 extends SearchMySQL {
return " MATCH($field) AGAINST('$searchon' IN BOOLEAN MODE) ";
}
}
-?>
+
diff --git a/includes/SearchOracle.php b/includes/SearchOracle.php
index c9a675e6..95c59288 100644
--- a/includes/SearchOracle.php
+++ b/includes/SearchOracle.php
@@ -232,4 +232,4 @@ class OracleSearchResultSet extends SearchResultSet {
}
}
-?>
+
diff --git a/includes/SearchPostgres.php b/includes/SearchPostgres.php
index 7c3580e7..cf9e6981 100644
--- a/includes/SearchPostgres.php
+++ b/includes/SearchPostgres.php
@@ -64,6 +64,7 @@ class SearchPostgres extends SearchEngine {
$term = preg_replace('/:/', ' ', $term);
$searchstring = '';
+ $m = array();
if( preg_match_all('/([-!]?)(\S+)\s*/', $term, $m, PREG_SET_ORDER ) ) {
foreach( $m as $terms ) {
if (strlen($terms[1])) {
@@ -232,4 +233,4 @@ class PostgresSearchResultSet extends SearchResultSet {
}
-?>
+
diff --git a/includes/SearchTsearch2.php b/includes/SearchTsearch2.php
index b504f034..06eaa72d 100644
--- a/includes/SearchTsearch2.php
+++ b/includes/SearchTsearch2.php
@@ -119,4 +119,4 @@ class SearchTsearch2 extends SearchEngine {
}
-?>
+
diff --git a/includes/SearchUpdate.php b/includes/SearchUpdate.php
index 724197c1..849d6dc7 100644
--- a/includes/SearchUpdate.php
+++ b/includes/SearchUpdate.php
@@ -112,4 +112,4 @@ class SearchUpdateMyISAM extends SearchUpdate {
# Inherits everything
}
-?>
+
diff --git a/includes/Setup.php b/includes/Setup.php
index 47ba494f..66bae0a8 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -28,8 +28,8 @@ if ( !isset( $wgVersion ) ) {
}
// Set various default paths sensibly...
-if( $wgScript === false ) $wgScript = "$wgScriptPath/index.php";
-if( $wgRedirectScript === false ) $wgRedirectScript = "$wgScriptPath/redirect.php";
+if( $wgScript === false ) $wgScript = "$wgScriptPath/index$wgScriptExtension";
+if( $wgRedirectScript === false ) $wgRedirectScript = "$wgScriptPath/redirect$wgScriptExtension";
if( $wgArticlePath === false ) {
if( $wgUsePathInfo ) {
@@ -54,6 +54,67 @@ if( $wgTmpDirectory === false ) $wgTmpDirectory = "{$wgUploadDirectory}/tmp";
if( $wgReadOnlyFile === false ) $wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR";
if( $wgFileCacheDirectory === false ) $wgFileCacheDirectory = "{$wgUploadDirectory}/cache";
+if ( empty( $wgFileStore['deleted']['directory'] ) ) {
+ $wgFileStore['deleted']['directory'] = "{$wgUploadDirectory}/deleted";
+}
+
+
+/**
+ * Initialise $wgLocalFileRepo from backwards-compatible settings
+ */
+if ( !$wgLocalFileRepo ) {
+ $wgLocalFileRepo = array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'directory' => $wgUploadDirectory,
+ 'url' => $wgUploadBaseUrl ? $wgUploadBaseUrl . $wgUploadPath : $wgUploadPath,
+ 'hashLevels' => $wgHashedUploadDirectory ? 2 : 0,
+ 'thumbScriptUrl' => $wgThumbnailScriptPath,
+ 'transformVia404' => !$wgGenerateThumbnailOnParse,
+ 'initialCapital' => $wgCapitalLinks,
+ 'deletedDir' => $wgFileStore['deleted']['directory'],
+ 'deletedHashLevels' => $wgFileStore['deleted']['hash']
+ );
+}
+/**
+ * Initialise shared repo from backwards-compatible settings
+ */
+if ( $wgUseSharedUploads ) {
+ if ( $wgSharedUploadDBname ) {
+ $wgForeignFileRepos[] = array(
+ 'class' => 'ForeignDBRepo',
+ 'name' => 'shared',
+ 'directory' => $wgSharedUploadDirectory,
+ 'url' => $wgSharedUploadPath,
+ 'hashLevels' => $wgHashedSharedUploadDirectory ? 2 : 0,
+ 'thumbScriptUrl' => $wgSharedThumbnailScriptPath,
+ 'transformVia404' => !$wgGenerateThumbnailOnParse,
+ 'dbType' => $wgDBtype,
+ 'dbServer' => $wgDBserver,
+ 'dbUser' => $wgDBuser,
+ 'dbPassword' => $wgDBpassword,
+ 'dbName' => $wgSharedUploadDBname,
+ 'dbFlags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT,
+ 'tablePrefix' => $wgSharedUploadDBprefix,
+ 'hasSharedCache' => $wgCacheSharedUploads,
+ 'descBaseUrl' => $wgRepositoryBaseUrl,
+ 'fetchDescription' => $wgFetchCommonsDescriptions,
+ );
+ } else {
+ $wgForeignFileRepos[] = array(
+ 'class' => 'FSRepo',
+ 'name' => 'shared',
+ 'directory' => $wgSharedUploadDirectory,
+ 'url' => $wgSharedUploadPath,
+ 'hashLevels' => $wgHashedSharedUploadDirectory ? 2 : 0,
+ 'thumbScriptUrl' => $wgSharedThumbnailScriptPath,
+ 'transformVia404' => !$wgGenerateThumbnailOnParse,
+ 'descBaseUrl' => $wgRepositoryBaseUrl,
+ 'fetchDescription' => $wgFetchCommonsDescriptions,
+ );
+ }
+}
+
require_once( "$IP/includes/AutoLoader.php" );
wfProfileIn( $fname.'-exception' );
@@ -167,6 +228,10 @@ if ( !$wgDBservers ) {
$wgLoadBalancer = new StubObject( 'wgLoadBalancer', 'LoadBalancer',
array( $wgDBservers, false, $wgMasterWaitTimeout, true ) );
$wgContLang = new StubContLang;
+
+// Now that variant lists may be available...
+$wgRequest->interpolateTitle();
+
$wgUser = new StubUser;
$wgLang = new StubUserLang;
$wgOut = new StubObject( 'wgOut', 'OutputPage' );
@@ -199,6 +264,9 @@ $wgPostCommitUpdateList = array();
if ( $wgAjaxSearch ) $wgAjaxExportList[] = 'wfSajaxSearch';
if ( $wgAjaxWatch ) $wgAjaxExportList[] = 'wfAjaxWatch';
+if ( $wgAjaxUploadDestCheck ) $wgAjaxExportList[] = 'UploadForm::ajaxGetExistsWarning';
+if( $wgAjaxLicensePreview )
+ $wgAjaxExportList[] = 'UploadForm::ajaxGetLicensePreview';
wfSeedRandom();
@@ -232,4 +300,4 @@ $wgFullyInitialised = true;
wfProfileOut( $fname.'-extensions' );
wfProfileOut( $fname );
-?>
+
diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php
index 0968460c..353f5b3a 100644
--- a/includes/SiteConfiguration.php
+++ b/includes/SiteConfiguration.php
@@ -116,4 +116,4 @@ class SiteConfiguration {
}
}
-?>
+
diff --git a/includes/SiteStats.php b/includes/SiteStats.php
index e320a196..d7b9161a 100644
--- a/includes/SiteStats.php
+++ b/includes/SiteStats.php
@@ -5,7 +5,7 @@
*/
class SiteStats {
static $row, $loaded = false;
- static $admins;
+ static $admins, $jobs;
static $pageCount = array();
static function recache() {
@@ -27,24 +27,26 @@ class SiteStats {
$dbr = wfGetDB( DB_SLAVE );
self::$row = $dbr->selectRow( 'site_stats', '*', false, __METHOD__ );
}
+
+ self::$loaded = true;
}
static function loadAndLazyInit() {
wfDebug( __METHOD__ . ": reading site_stats from slave\n" );
$row = self::doLoad( wfGetDB( DB_SLAVE ) );
-
- if( $row === false ) {
- // Might have just been initialzed during this request?
- wfDebug( __METHOD__ . ": site_stats missing on slave\n" );
+
+ if( !self::isSane( $row ) ) {
+ // Might have just been initialized during this request? Underflow?
+ wfDebug( __METHOD__ . ": site_stats damaged or missing on slave\n" );
$row = self::doLoad( wfGetDB( DB_MASTER ) );
}
- if( $row === false ) {
+ if( !self::isSane( $row ) ) {
// Normally the site_stats table is initialized at install time.
- // Some manual construction scenarios may leave the table empty,
- // however, for instance when importing from a dump into a clean
- // schema with mwdumper.
- wfDebug( __METHOD__ . ": initializing empty site_stats\n" );
+ // Some manual construction scenarios may leave the table empty or
+ // broken, however, for instance when importing from a dump into a
+ // clean schema with mwdumper.
+ wfDebug( __METHOD__ . ": initializing damaged or missing site_stats\n" );
global $IP;
require_once "$IP/maintenance/initStats.inc";
@@ -56,8 +58,8 @@ class SiteStats {
$row = self::doLoad( wfGetDB( DB_MASTER ) );
}
- if( $row === false ) {
- wfDebug( __METHOD__ . ": init of site_stats failed o_O\n" );
+ if( !self::isSane( $row ) ) {
+ wfDebug( __METHOD__ . ": site_stats persistently nonsensical o_O\n" );
}
return $row;
}
@@ -104,6 +106,18 @@ class SiteStats {
return self::$admins;
}
+ static function jobs() {
+ if ( !isset( self::$jobs ) ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ self::$jobs = $dbr->estimateRowCount('job');
+ /* Zero rows still do single row read for row that doesn't exist, but people are annoyed by that */
+ if (self::$jobs == 1) {
+ self::$jobs = 0;
+ }
+ }
+ return self::$jobs;
+ }
+
static function pagesInNs( $ns ) {
wfProfileIn( __METHOD__ );
if( !isset( self::$pageCount[$ns] ) ) {
@@ -114,6 +128,28 @@ class SiteStats {
return $pageCount[$ns];
}
+ /** Is the provided row of site stats sane, or should it be regenerated? */
+ private static function isSane( $row ) {
+ if(
+ $row === false
+ or $row->ss_total_pages < $row->ss_good_articles
+ or $row->ss_total_edits < $row->ss_total_pages
+ or $row->ss_users < $row->ss_admins
+ ) {
+ return false;
+ }
+ // Now check for underflow/overflow
+ foreach( array( 'total_views', 'total_edits', 'good_articles',
+ 'total_pages', 'users', 'admins', 'images' ) as $member ) {
+ if(
+ $row->{"ss_$member"} > 2000000000
+ or $row->{"ss_$member"} < 0
+ ) {
+ return false;
+ }
+ }
+ return true;
+ }
}
@@ -200,4 +236,4 @@ class SiteStatsUpdate {
*/
}
}
-?>
+
diff --git a/includes/Skin.php b/includes/Skin.php
index 0ca95f7e..f9e17057 100644
--- a/includes/Skin.php
+++ b/includes/Skin.php
@@ -22,6 +22,7 @@ class Skin extends Linker {
var $rcMoveIndex;
var $mWatchLinkNum = 0; // Appended to end of watch link id's
/**#@-*/
+ protected $mRevisionId; // The revision ID we're looking at, null if not applicable.
protected $skinname = 'standard' ;
/** Constructor, call parent constructor */
@@ -292,19 +293,21 @@ class Skin extends Linker {
* The odd calling convention is for backwards compatibility
*/
static function makeGlobalVariablesScript( $data ) {
- global $wgStylePath, $wgUser;
+ global $wgScript, $wgStylePath, $wgUser;
global $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgLang;
global $wgTitle, $wgCanonicalNamespaceNames, $wgOut, $wgArticle;
global $wgBreakFrames, $wgRequest;
+ global $wgUseAjax, $wgAjaxWatch;
$ns = $wgTitle->getNamespace();
$nsname = isset( $wgCanonicalNamespaceNames[ $ns ] ) ? $wgCanonicalNamespaceNames[ $ns ] : $wgTitle->getNsText();
-
+
$vars = array(
'skin' => $data['skinname'],
'stylepath' => $wgStylePath,
'wgArticlePath' => $wgArticlePath,
'wgScriptPath' => $wgScriptPath,
+ 'wgScript' => $wgScript,
'wgServer' => $wgServer,
'wgCanonicalNamespace' => $nsname,
'wgCanonicalSpecialPageName' => SpecialPage::resolveAlias( $wgTitle->getDBKey() ),
@@ -312,6 +315,8 @@ class Skin extends Linker {
'wgPageName' => $wgTitle->getPrefixedDBKey(),
'wgTitle' => $wgTitle->getText(),
'wgAction' => $wgRequest->getText( 'action', 'view' ),
+ 'wgRestrictionEdit' => $wgTitle->getRestrictions( 'edit' ),
+ 'wgRestrictionMove' => $wgTitle->getRestrictions( 'move' ),
'wgArticleId' => $wgTitle->getArticleId(),
'wgIsArticle' => $wgOut->isArticle(),
'wgUserName' => $wgUser->isAnon() ? NULL : $wgUser->getName(),
@@ -330,24 +335,33 @@ class Skin extends Linker {
$vars['wgLivepreviewMessageError'] = wfMsg( 'livepreview-error' );
}
+ if($wgUseAjax && $wgAjaxWatch && $wgUser->isLoggedIn() ) {
+ $msgs = (object)array();
+ foreach ( array( 'watch', 'unwatch', 'watching', 'unwatching' ) as $msgName ) {
+ $msgs->{$msgName . 'Msg'} = wfMsg( $msgName );
+ }
+ $vars['wgAjaxWatch'] = $msgs;
+ }
+
return self::makeVariablesScript( $vars );
}
- function getHeadScripts() {
- global $wgStylePath, $wgUser, $wgAllowUserJs, $wgJsMimeType, $wgStyleVersion;
+ function getHeadScripts( $allowUserJs ) {
+ global $wgStylePath, $wgUser, $wgJsMimeType, $wgStyleVersion;
$r = self::makeGlobalVariablesScript( array( 'skinname' => $this->getSkinName() ) );
$r .= "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/wikibits.js?$wgStyleVersion\"></script>\n";
global $wgUseSiteJs;
if ($wgUseSiteJs) {
- if ($wgUser->isLoggedIn()) {
- $r .= "<script type=\"$wgJsMimeType\" src=\"".htmlspecialchars(self::makeUrl('-','action=raw&smaxage=0&gen=js'))."\"><!-- site js --></script>\n";
- } else {
- $r .= "<script type=\"$wgJsMimeType\" src=\"".htmlspecialchars(self::makeUrl('-','action=raw&gen=js'))."\"><!-- site js --></script>\n";
- }
- }
- if( $wgAllowUserJs && $wgUser->isLoggedIn() ) {
+ $jsCache = $wgUser->isLoggedIn() ? '&smaxage=0' : '';
+ $r .= "<script type=\"$wgJsMimeType\" src=\"".
+ htmlspecialchars(self::makeUrl('-',
+ "action=raw$jsCache&gen=js&useskin=" .
+ urlencode( $this->getSkinName() ) ) ) .
+ "\"><!-- site js --></script>\n";
+ }
+ if( $allowUserJs && $wgUser->isLoggedIn() ) {
$userpage = $wgUser->getUserPage();
$userjs = htmlspecialchars( self::makeUrl(
$userpage->getPrefixedText().'/'.$this->getSkinName().'.js',
@@ -385,7 +399,8 @@ class Skin extends Linker {
function getUserStylesheet() {
global $wgStylePath, $wgRequest, $wgContLang, $wgSquidMaxage, $wgStyleVersion;
$sheet = $this->getStylesheet();
- $s = "@import \"$wgStylePath/common/common.css?$wgStyleVersion\";\n";
+ $s = "@import \"$wgStylePath/common/shared.css?$wgStyleVersion\";\n";
+ $s .= "@import \"$wgStylePath/common/oldshared.css?$wgStyleVersion\";\n";
$s .= "@import \"$wgStylePath/$sheet?$wgStyleVersion\";\n";
if($wgContLang->isRTL()) $s .= "@import \"$wgStylePath/common/common_rtl.css?$wgStyleVersion\";\n";
@@ -398,36 +413,28 @@ class Skin extends Linker {
}
/**
- * This returns MediaWiki:Common.js. For some bizarre reason, it does
- * *not* return any custom user JS from user subpages. Huh?
+ * This returns MediaWiki:Common.js, and derived classes may add other JS.
+ * Despite its name, it does *not* return any custom user JS from user
+ * subpages. The returned script is sitewide and publicly cacheable and
+ * therefore must not include anything that varies according to user,
+ * interface language, etc. (although it may vary by skin). See
+ * makeGlobalVariablesScript for things that can vary per page view and are
+ * not cacheable.
*
- * @return string
+ * @return string Raw JavaScript to be returned
*/
- function getUserJs() {
+ public function getUserJs() {
wfProfileIn( __METHOD__ );
global $wgStylePath;
$s = "/* generated javascript */\n";
- $s .= "var skin = '{$this->skinname}';\nvar stylepath = '{$wgStylePath}';";
+ $s .= "var skin = '" . Xml::escapeJsString( $this->getSkinName() ) . "';\n";
+ $s .= "var stylepath = '" . Xml::escapeJsString( $wgStylePath ) . "';";
$s .= "\n\n/* MediaWiki:Common.js */\n";
$commonJs = wfMsgForContent('common.js');
if ( !wfEmptyMsg ( 'common.js', $commonJs ) ) {
$s .= $commonJs;
}
-
- global $wgUseAjax, $wgAjaxWatch;
- if($wgUseAjax && $wgAjaxWatch) {
- $s .= "
-
-/* AJAX (un)watch (see /skins/common/ajaxwatch.js) */
-var wgAjaxWatch = {
- watchMsg: '". str_replace( array("'", "\n"), array("\\'", ' '), wfMsgExt( 'watch', array() ) )."',
- unwatchMsg: '". str_replace( array("'", "\n"), array("\\'", ' '), wfMsgExt( 'unwatch', array() ) )."',
- watchingMsg: '". str_replace( array("'", "\n"), array("\\'", ' '), wfMsgExt( 'watching', array() ) )."',
- unwatchingMsg: '". str_replace( array("'", "\n"), array("\\'", ' '), wfMsgExt( 'unwatching', array() ) )."'
-};";
- }
-
wfProfileOut( __METHOD__ );
return $s;
}
@@ -701,7 +708,9 @@ END;
*/
function bottomScripts() {
global $wgJsMimeType;
- return "\n\t\t<script type=\"$wgJsMimeType\">if (window.runOnloadHook) runOnloadHook();</script>\n";
+ $bottomScriptText = "\n\t\t<script type=\"$wgJsMimeType\">if (window.runOnloadHook) runOnloadHook();</script>\n";
+ wfRunHooks( 'SkinAfterBottomScripts', array( $this, &$bottomScriptText ) );
+ return $bottomScriptText;
}
/** @return string Retrievied from HTML text */
@@ -739,8 +748,8 @@ END;
if ( $wgOut->isArticleRelated() ) {
if ( $wgTitle->getNamespace() == NS_IMAGE ) {
$name = $wgTitle->getDBkey();
- $image = new Image( $wgTitle );
- if( $image->exists() ) {
+ $image = wfFindFile( $wgTitle );
+ if( $image ) {
$link = htmlspecialchars( $image->getURL() );
$style = $this->getInternalLinkAttributes( $link, $name );
$s .= " | <a href=\"{$link}\"{$style}>{$name}</a>";
@@ -1427,29 +1436,6 @@ END;
return $s;
}
- function dateLink() {
- $t1 = Title::newFromText( gmdate( 'F j' ) );
- $t2 = Title::newFromText( gmdate( 'Y' ) );
-
- $id = $t1->getArticleID();
-
- if ( 0 == $id ) {
- $s = $this->makeBrokenLink( $t1->getText() );
- } else {
- $s = $this->makeKnownLink( $t1->getText() );
- }
- $s .= ', ';
-
- $id = $t2->getArticleID();
-
- if ( 0 == $id ) {
- $s .= $this->makeBrokenLink( $t2->getText() );
- } else {
- $s .= $this->makeKnownLink( $t2->getText() );
- }
- return $s;
- }
-
function talkLink() {
global $wgTitle;
@@ -1668,5 +1654,5 @@ END;
wfProfileOut( $fname );
return $bar;
}
-}
-?>
+
+} \ No newline at end of file
diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php
index cddd2195..6ce40606 100644
--- a/includes/SkinTemplate.php
+++ b/includes/SkinTemplate.php
@@ -179,7 +179,7 @@ class SkinTemplate extends Skin {
$this->usercss = $this->userjs = $this->userjsprev = false;
$this->setupUserCss();
- $this->setupUserJs();
+ $this->setupUserJs( $out->isUserJsAllowed() );
$this->titletxt = $this->mTitle->getPrefixedText();
wfProfileOut( "$fname-stuff" );
@@ -285,11 +285,11 @@ class SkinTemplate extends Skin {
$tpl->setRef( 'userjsprev', $this->userjsprev);
global $wgUseSiteJs;
if ($wgUseSiteJs) {
- if($this->loggedin) {
- $tpl->set( 'jsvarurl', self::makeUrl('-','action=raw&smaxage=0&gen=js') );
- } else {
- $tpl->set( 'jsvarurl', self::makeUrl('-','action=raw&gen=js') );
- }
+ $jsCache = $this->loggedin ? '&smaxage=0' : '';
+ $tpl->set( 'jsvarurl',
+ self::makeUrl('-',
+ "action=raw$jsCache&gen=js&useskin=" .
+ urlencode( $this->getSkinName() ) ) );
} else {
$tpl->set('jsvarurl', false);
}
@@ -440,7 +440,8 @@ class SkinTemplate extends Skin {
// XXX: attach this from javascript, same with section editing
if($this->iseditable && $wgUser->getOption("editondblclick") )
{
- $tpl->set('body_ondblclick', 'document.location = "' .$content_actions['edit']['href'] .'";');
+ $encEditUrl = wfEscapeJsString( $this->mTitle->getLocalUrl( $this->editUrlOptions() ) );
+ $tpl->set('body_ondblclick', 'document.location = "' . $encEditUrl . '";');
} else {
$tpl->set('body_ondblclick', false);
}
@@ -591,7 +592,7 @@ class SkinTemplate extends Skin {
if( $selected ) {
$classes[] = 'selected';
}
- if( $checkEdit && $title->getArticleId() == 0 ) {
+ if( $checkEdit && !$title->isAlwaysKnown() && $title->getArticleId() == 0 ) {
$classes[] = 'new';
$query = 'action=edit';
}
@@ -822,7 +823,6 @@ class SkinTemplate extends Skin {
global $wgEnableUploads, $wgUploadNavigationUrl;
$action = $wgRequest->getText( 'action' );
- $oldid = $wgRequest->getVal( 'oldid' );
$nav_urls = array();
$nav_urls['mainpage'] = array( 'href' => self::makeMainPageUrl() );
@@ -852,21 +852,16 @@ class SkinTemplate extends Skin {
);
// Also add a "permalink" while we're at it
- if ( (int)$oldid ) {
+ if ( $this->mRevisionId ) {
$nav_urls['permalink'] = array(
'text' => wfMsg( 'permalink' ),
- 'href' => ''
+ 'href' => $wgTitle->getLocalURL( "oldid=$this->mRevisionId" )
);
- } else {
- $revid = $wgArticle ? $wgArticle->getLatest() : 0;
- if ( !( $revid == 0 ) )
- $nav_urls['permalink'] = array(
- 'text' => wfMsg( 'permalink' ),
- 'href' => $wgTitle->getLocalURL( "oldid=$revid" )
- );
}
-
- wfRunHooks( 'SkinTemplateBuildNavUrlsNav_urlsAfterPermalink', array( &$this, &$nav_urls, &$oldid, &$revid ) );
+
+ // Copy in case this undocumented, shady hook tries to mess with internals
+ $revid = $this->mRevisionId;
+ wfRunHooks( 'SkinTemplateBuildNavUrlsNav_urlsAfterPermalink', array( &$this, &$nav_urls, &$revid, &$revid ) );
}
if( $this->mTitle->getNamespace() != NS_SPECIAL ) {
@@ -896,10 +891,19 @@ class SkinTemplate extends Skin {
$ip = false;
}
- if($id || $ip) { # both anons and non-anons have contri list
+ if($id || $ip) { # both anons and non-anons have contribs list
$nav_urls['contributions'] = array(
'href' => self::makeSpecialUrlSubpage( 'Contributions', $this->mTitle->getText() )
);
+
+ if( $id ) {
+ $logPage = SpecialPage::getTitleFor( 'Log' );
+ $nav_urls['log'] = array( 'href' => $logPage->getLocalUrl( 'user='
+ . $this->mTitle->getPartialUrl() ) );
+ } else {
+ $nav_urls['log'] = false;
+ }
+
if ( $wgUser->isAllowed( 'block' ) ) {
$nav_urls['blockip'] = array(
'href' => self::makeSpecialUrlSubpage( 'Blockip', $this->mTitle->getText() )
@@ -909,6 +913,7 @@ class SkinTemplate extends Skin {
}
} else {
$nav_urls['contributions'] = false;
+ $nav_urls['log'] = false;
$nav_urls['blockip'] = false;
}
$nav_urls['emailuser'] = false;
@@ -974,9 +979,12 @@ class SkinTemplate extends Skin {
# If we use the site's dynamic CSS, throw that in, too
if ( $wgUseSiteCss ) {
$query = "usemsgcache=yes&action=raw&ctype=text/css&smaxage=$wgSquidMaxage";
+ $skinquery = '';
+ if (($us = $wgRequest->getVal('useskin', '')) !== '')
+ $skinquery = "&useskin=$us";
$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";
+ $sitecss .= '@import "' . self::makeUrl( '-', "action=raw&gen=css$siteargs$skinquery" ) . '";' . "\n";
}
# If we use any dynamic CSS, make a little CDATA block out of it.
@@ -990,14 +998,14 @@ class SkinTemplate extends Skin {
/**
* @private
*/
- function setupUserJs() {
+ function setupUserJs( $allowUserJs ) {
$fname = 'SkinTemplate::setupUserJs';
wfProfileIn( $fname );
- global $wgRequest, $wgAllowUserJs, $wgJsMimeType;
+ global $wgRequest, $wgJsMimeType;
$action = $wgRequest->getText('action');
- if( $wgAllowUserJs && $this->loggedin ) {
+ if( $allowUserJs && $this->loggedin ) {
if( $this->mTitle->isJsSubpage() and $this->userCanPreview( $action ) ) {
# XXX: additional security check/prompt?
$this->userjsprev = '/*<![CDATA[*/ ' . $wgRequest->getText('wpTextbox1') . ' /*]]>*/';
@@ -1177,4 +1185,4 @@ class QuickTemplate {
return ($msg != '-') && ($msg != ''); # ????
}
}
-?>
+
diff --git a/includes/SpecialAllmessages.php b/includes/SpecialAllmessages.php
index 0862cd17..4ba01e29 100644
--- a/includes/SpecialAllmessages.php
+++ b/includes/SpecialAllmessages.php
@@ -25,7 +25,8 @@ function wfSpecialAllmessages() {
$navText = wfMsg( 'allmessagestext' );
# Make sure all extension messages are available
- MessageCache::loadAllMessages();
+
+ $wgMessageCache->loadAllMessages();
$sortedArray = array_merge( Language::getMessagesFor( 'en' ), $wgMessageCache->getExtensionMessagesFor( 'en' ) );
ksort( $sortedArray );
@@ -196,4 +197,4 @@ $mw
return $txt;
}
-?>
+
diff --git a/includes/SpecialAllpages.php b/includes/SpecialAllpages.php
index 03e164bd..07ff120b 100644
--- a/includes/SpecialAllpages.php
+++ b/includes/SpecialAllpages.php
@@ -19,10 +19,7 @@ function wfSpecialAllpages( $par=NULL, $specialPage ) {
$indexPage = new SpecialAllpages();
- if( !in_array($namespace, array_keys($namespaces)) )
- $namespace = 0;
-
- $wgOut->setPagetitle( $namespace > 0 ?
+ $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) ) ?
wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) :
wfMsg( 'allarticles' )
);
@@ -53,41 +50,44 @@ class SpecialAllpages {
* @param string $from Article name we are starting listing at.
*/
function namespaceForm ( $namespace = NS_MAIN, $from = '' ) {
- global $wgScript;
+ global $wgScript, $wgContLang;
$t = SpecialPage::getTitleFor( $this->name );
-
- $namespaceselect = HTMLnamespaceselector($namespace, null);
-
- $frombox = "<input type='text' size='20' name='from' id='nsfrom' value=\""
- . htmlspecialchars ( $from ) . '"/>';
- $submitbutton = '<input type="submit" value="' . wfMsgHtml( 'allpagessubmit' ) . '" />';
-
- $out = "<div class='namespaceoptions'><form method='get' action='{$wgScript}'>";
- $out .= '<input type="hidden" name="title" value="'.$t->getPrefixedText().'" />';
- $out .= "
-<table id='nsselect' class='allpages'>
- <tr>
- <td align='right'>" . wfMsgHtml($this->nsfromMsg) . "</td>
- <td align='left'><label for='nsfrom'>$frombox</label></td>
- </tr>
- <tr>
- <td align='right'><label for='namespace'>" . wfMsgHtml('namespace') . "</label></td>
- <td align='left'>
- $namespaceselect $submitbutton
- </td>
- </tr>
-</table>
-";
- $out .= '</form></div>';
- return $out;
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+ $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) );
+ $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
+ $out .= Xml::hidden( 'title', $t->getPrefixedText() );
+ $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) );
+ $out .= "<tr>
+ <td align='$align'>" .
+ Xml::label( wfMsg( $this->nsfromMsg ), 'nsfrom' ) .
+ "</td>
+ <td>" .
+ Xml::input( 'from', 20, htmlspecialchars ( $from ), array( 'id' => 'nsfrom' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td align='$align'>" .
+ Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
+ "</td>
+ <td>" .
+ Xml::namespaceSelector( $namespace, null ) .
+ Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
+ "</td>
+ </tr>";
+ $out .= Xml::closeElement( 'table' );
+ $out .= Xml::closeElement( 'form' );
+ $out .= Xml::closeElement( 'div' );
+ return $out;
}
/**
* @param integer $namespace (default NS_MAIN)
*/
function showToplevel ( $namespace = NS_MAIN, $including = false ) {
- global $wgOut;
+ global $wgOut, $wgContLang;
$fname = "indexShowToplevel";
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
# TODO: Either make this *much* faster or cache the title index points
# in the querycache table.
@@ -101,7 +101,11 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) {
$lines = $wgMemc->get( $key );
if( !is_array( $lines ) ) {
- $firstTitle = $dbr->selectField( 'page', 'page_title', $where, $fname, array( 'LIMIT' => 1 ) );
+ $options = array( 'LIMIT' => 1 );
+ if ( ! $dbr->implicitOrderby() ) {
+ $options['ORDER BY'] = 'page_title';
+ }
+ $firstTitle = $dbr->selectField( 'page', 'page_title', $where, $fname, $options );
$lastTitle = $firstTitle;
# This array is going to hold the page_titles in order.
@@ -170,8 +174,8 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) {
$morelinks = '';
if ( $morelinks != '' ) {
$out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
- $out2 .= '<tr valign="top"><td align="left">' . $nsForm;
- $out2 .= '</td><td align="right" style="font-size: smaller; margin-bottom: 1em;">';
+ $out2 .= '<tr valign="top"><td>' . $nsForm;
+ $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">';
$out2 .= $morelinks . '</td></tr></table><hr />';
} else {
$out2 = $nsForm . '<hr />';
@@ -187,6 +191,8 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) {
* @param integer $namespace (Default NS_MAIN)
*/
function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) {
+ global $wgContLang;
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
$inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) );
$outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) );
$queryparams = ($namespace ? "namespace=$namespace" : '');
@@ -196,9 +202,9 @@ function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) {
$out = wfMsgHtml(
'alphaindexline',
"<a href=\"$link\">$inpointf</a></td><td><a href=\"$link\">",
- "</a></td><td align=\"left\"><a href=\"$link\">$outpointf</a>"
+ "</a></td><td><a href=\"$link\">$outpointf</a>"
);
- return '<tr><td align="right">'.$out.'</td></tr>';
+ return '<tr><td align="' . $align . '">'.$out.'</td></tr>';
}
/**
@@ -209,14 +215,20 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
global $wgOut, $wgUser, $wgContLang;
$fname = 'indexShowChunk';
-
$sk = $wgUser->getSkin();
$fromList = $this->getNamespaceKeyAndText($namespace, $from);
+ $namespaces = $wgContLang->getNamespaces();
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+
$n = 0;
-
+
if ( !$fromList ) {
$out = wfMsgWikiHtml( 'allpagesbadtitle' );
+ } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) {
+ // Show errormessage and reset to NS_MAIN
+ $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace );
+ $namespace = NS_MAIN;
} else {
list( $namespace, $fromKey, $from ) = $fromList;
@@ -285,8 +297,11 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
} else {
# The previous chunk is not complete, need to link to the very first title
# available in the database
- $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), $fname, array( 'LIMIT' => 1) );
-
+ $options = array( 'LIMIT' => 1 );
+ if ( ! $dbr->implicitOrderby() ) {
+ $options['ORDER BY'] = 'page_title';
+ }
+ $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), $fname, $options );
# Show the previous link if it s not the current requested chunk
if( $from != $reallyFirstPage_title ) {
$prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title );
@@ -298,8 +313,8 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
$nsForm = $this->namespaceForm ( $namespace, $from );
$out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
- $out2 .= '<tr valign="top"><td align="left">' . $nsForm;
- $out2 .= '</td><td align="right" style="font-size: smaller; margin-bottom: 1em;">' .
+ $out2 .= '<tr valign="top"><td>' . $nsForm;
+ $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' .
$sk->makeKnownLink( $wgContLang->specialPage( "Allpages" ),
wfMsgHtml ( 'allpages' ) );
@@ -324,11 +339,16 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
$wgOut->addHtml( $out2 . $out );
if( isset($prevLink) or isset($nextLink) ) {
- $wgOut->addHtml( '<hr/><p style="font-size: smaller; float: right;">' );
- if( isset( $prevLink ) )
- $wgOut->addHTML( $prevLink . ' | ');
- if( isset( $nextLink ) )
+ $wgOut->addHtml( '<hr /><p style="font-size: smaller; float: ' . $align . '">' );
+ if( isset( $prevLink ) ) {
+ $wgOut->addHTML( $prevLink );
+ }
+ if( isset( $prevLink ) && isset( $nextLink ) ) {
+ $wgOut->addHTML( ' | ' );
+ }
+ if( isset( $nextLink ) ) {
$wgOut->addHTML( $nextLink );
+ }
$wgOut->addHTML( '</p>' );
}
diff --git a/includes/SpecialAncientpages.php b/includes/SpecialAncientpages.php
index c0bbb7ba..dee8fbde 100644
--- a/includes/SpecialAncientpages.php
+++ b/includes/SpecialAncientpages.php
@@ -60,4 +60,4 @@ function wfSpecialAncientpages() {
$app->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialBlockip.php b/includes/SpecialBlockip.php
index 5f47fa13..942ebe8b 100644
--- a/includes/SpecialBlockip.php
+++ b/includes/SpecialBlockip.php
@@ -43,6 +43,7 @@ function wfSpecialBlockip( $par ) {
*/
class IPBlockForm {
var $BlockAddress, $BlockExpiry, $BlockReason;
+# var $BlockEmail;
function IPBlockForm( $par ) {
global $wgRequest, $wgUser;
@@ -60,12 +61,13 @@ class IPBlockForm {
$this->BlockAnonOnly = $wgRequest->getBool( 'wpAnonOnly', $byDefault );
$this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', $byDefault );
$this->BlockEnableAutoblock = $wgRequest->getBool( 'wpEnableAutoblock', $byDefault );
+ $this->BlockEmail = $wgRequest->getBool( 'wpEmailBan', false );
# Re-check user's rights to hide names, very serious, defaults to 0
- $this->BlockHideName = $wgRequest->getBool( 'wpHideName', 0 ) && $wgUser->isAllowed( 'hideuser' );
+ $this->BlockHideName = ( $wgRequest->getBool( 'wpHideName', 0 ) && $wgUser->isAllowed( 'hideuser' ) ) ? 1 : 0;
}
function showForm( $err ) {
- global $wgOut, $wgUser, $wgSysopUserBans;
+ global $wgOut, $wgUser, $wgSysopUserBans, $wgContLang;
$wgOut->setPagetitle( wfMsg( 'blockip' ) );
$wgOut->addWikiText( wfMsg( 'blockiptext' ) );
@@ -84,6 +86,7 @@ class IPBlockForm {
$titleObj = SpecialPage::getTitleFor( 'Blockip' );
$action = $titleObj->escapeLocalURL( "action=submit" );
+ $alignRight = $wgContLang->isRtl() ? 'left' : 'right';
if ( "" != $err ) {
$wgOut->setSubtitle( wfMsgHtml( 'formerror' ) );
@@ -141,7 +144,7 @@ class IPBlockForm {
$blockReasonList .= $optgroup;
}
- $token = htmlspecialchars( $wgUser->editToken() );
+ $token = $wgUser->editToken();
global $wgStylePath, $wgStyleVersion;
$wgOut->addHTML( "
@@ -150,8 +153,8 @@ class IPBlockForm {
<form id=\"blockip\" method=\"post\" action=\"{$action}\">
<table border='0'>
<tr>
- <td align=\"right\">{$mIpaddress}:</td>
- <td align=\"left\">
+ <td align=\"$alignRight\">{$mIpaddress}</td>
+ <td>
" . Xml::input( 'wpBlockAddress', 45, $this->BlockAddress,
array(
'tabindex' => '1',
@@ -162,8 +165,8 @@ class IPBlockForm {
<tr>");
if ($showblockoptions) {
$wgOut->addHTML("
- <td align=\"right\">{$mIpbexpiry}:</td>
- <td align=\"left\">
+ <td align=\"$alignRight\">{$mIpbexpiry}</td>
+ <td>
<select tabindex='2' id='wpBlockExpiry' name=\"wpBlockExpiry\" onchange=\"considerChangingExpiryFocus()\">
$blockExpiryFormOptions
</select>
@@ -173,8 +176,8 @@ class IPBlockForm {
$wgOut->addHTML("
</tr>
<tr id='wpBlockOther'>
- <td align=\"right\">{$mIpbother}:</td>
- <td align=\"left\">
+ <td align=\"$alignRight\">{$mIpbother}</td>
+ <td>
" . Xml::input( 'wpBlockOther', 45, $this->BlockOther,
array( 'tabindex' => '3', 'id' => 'mw-bi-other' ) ) . "
</td>
@@ -182,8 +185,8 @@ class IPBlockForm {
if ( $blockReasonList != '' ) {
$wgOut->addHTML("
<tr>
- <td align=\"right\">{$mIpbreasonother}:</td>
- <td align=\"left\">
+ <td align=\"$alignRight\">{$mIpbreasonother}</td>
+ <td>
<select tabindex='4' id=\"wpBlockReasonList\" name=\"wpBlockReasonList\">
$blockReasonList
</select>
@@ -192,15 +195,16 @@ class IPBlockForm {
}
$wgOut->addHTML("
<tr id=\"wpBlockReason\">
- <td align=\"right\">{$mIpbreason}:</td>
- <td align=\"left\">
+ <td align=\"$alignRight\">{$mIpbreason}</td>
+ <td>
" . Xml::input( 'wpBlockReason', 45, $this->BlockReason,
- array( 'tabindex' => '5', 'id' => 'mw-bi-reason' ) ) . "
+ array( 'tabindex' => '5', 'id' => 'mw-bi-reason',
+ 'maxlength'=> '200' ) ) . "
</td>
</tr>
<tr id='wpAnonOnlyRow'>
<td>&nbsp;</td>
- <td align=\"left\">
+ <td>
" . wfCheckLabel( wfMsgHtml( 'ipbanononly' ),
'wpAnonOnly', 'wpAnonOnly', $this->BlockAnonOnly,
array( 'tabindex' => '6' ) ) . "
@@ -208,7 +212,7 @@ class IPBlockForm {
</tr>
<tr id='wpCreateAccountRow'>
<td>&nbsp;</td>
- <td align=\"left\">
+ <td>
" . wfCheckLabel( wfMsgHtml( 'ipbcreateaccount' ),
'wpCreateAccount', 'wpCreateAccount', $this->BlockCreateAccount,
array( 'tabindex' => '7' ) ) . "
@@ -216,7 +220,7 @@ class IPBlockForm {
</tr>
<tr id='wpEnableAutoblockRow'>
<td>&nbsp;</td>
- <td align=\"left\">
+ <td>
" . wfCheckLabel( wfMsgHtml( 'ipbenableautoblock' ),
'wpEnableAutoblock', 'wpEnableAutoblock', $this->BlockEnableAutoblock,
array( 'tabindex' => '8' ) ) . "
@@ -228,7 +232,7 @@ class IPBlockForm {
$wgOut->addHTML("
<tr>
<td>&nbsp;</td>
- <td align=\"left\">
+ <td>
" . wfCheckLabel( wfMsgHtml( 'ipbhidename' ),
'wpHideName', 'wpHideName', $this->BlockHideName,
array( 'tabindex' => '9' ) ) . "
@@ -236,12 +240,27 @@ class IPBlockForm {
</tr>
");
}
+
+ global $wgSysopEmailBans;
+
+ if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) {
+ $wgOut->addHTML("
+ <tr id='wpEnableEmailBan'>
+ <td>&nbsp;</td>
+ <td>
+ " . wfCheckLabel( wfMsgHtml( 'ipbemailban' ),
+ 'wpEmailBan', 'wpEmailBan', $this->BlockEmail,
+ array( 'tabindex' => '10' )) . "
+ </td>
+ </tr>
+ ");
+ }
$wgOut->addHTML("
<tr>
<td style='padding-top: 1em'>&nbsp;</td>
- <td style='padding-top: 1em' align=\"left\">
- " . Xml::submitButton( wfMsgHtml( 'ipbsubmit' ),
- array( 'name' => 'wpBlock', 'tabindex' => '10' ) ) . "
+ <td style='padding-top: 1em'>
+ " . Xml::submitButton( wfMsg( 'ipbsubmit' ),
+ array( 'name' => 'wpBlock', 'tabindex' => '11' ) ) . "
</td>
</tr>
</table>" .
@@ -354,10 +373,10 @@ class IPBlockForm {
# Create block
# Note: for a user block, ipb_address is only for display purposes
-
$block = new Block( $this->BlockAddress, $userId, $wgUser->getID(),
$reasonstr, wfTimestampNow(), 0, $expiry, $this->BlockAnonOnly,
- $this->BlockCreateAccount, $this->BlockEnableAutoblock, $this->BlockHideName);
+ $this->BlockCreateAccount, $this->BlockEnableAutoblock, $this->BlockHideName,
+ $this->BlockEmail);
if (wfRunHooks('BlockIp', array(&$block, &$wgUser))) {
@@ -418,6 +437,8 @@ class IPBlockForm {
$flags[] = 'nocreate';
if( !$this->BlockEnableAutoblock )
$flags[] = 'noautoblock';
+ if ( $this->BlockEmail )
+ $flags[] = 'noemail';
return implode( ',', $flags );
}
@@ -471,4 +492,4 @@ class IPBlockForm {
}
}
}
-?>
+
diff --git a/includes/SpecialBlockme.php b/includes/SpecialBlockme.php
index c2cb1a58..da2757ac 100644
--- a/includes/SpecialBlockme.php
+++ b/includes/SpecialBlockme.php
@@ -36,4 +36,4 @@ function wfSpecialBlockme() {
$wgOut->addWikiText( $success );
}
-?>
+
diff --git a/includes/SpecialBooksources.php b/includes/SpecialBooksources.php
index d3136ea4..5f103495 100644
--- a/includes/SpecialBooksources.php
+++ b/includes/SpecialBooksources.php
@@ -14,20 +14,20 @@ class SpecialBookSources extends SpecialPage {
* ISBN passed to the page, if any
*/
private $isbn = '';
-
+
/**
* Constructor
*/
public function __construct() {
parent::__construct( 'Booksources' );
}
-
+
/**
* Show the special page
*
* @param $isbn ISBN passed as a subpage parameter
*/
- public function execute( $isbn = false ) {
+ public function execute( $isbn ) {
global $wgOut, $wgRequest;
$this->setHeaders();
$this->isbn = $this->cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) );
@@ -36,7 +36,7 @@ class SpecialBookSources extends SpecialPage {
if( strlen( $this->isbn ) > 0 )
$this->showList();
}
-
+
/**
* Trim ISBN and remove characters which aren't required
*
@@ -46,7 +46,7 @@ class SpecialBookSources extends SpecialPage {
private function cleanIsbn( $isbn ) {
return trim( preg_replace( '![^0-9X]!', '', $isbn ) );
}
-
+
/**
* Generate a form to allow users to enter an ISBN
*
@@ -64,7 +64,7 @@ class SpecialBookSources extends SpecialPage {
$form .= '</fieldset>';
return $form;
}
-
+
/**
* Determine where to get the list of book sources from,
* format and output them
@@ -73,19 +73,19 @@ class SpecialBookSources extends SpecialPage {
*/
private function showList() {
global $wgOut, $wgContLang;
-
+
# Hook to allow extensions to insert additional HTML,
# e.g. for API-interacting plugins and so on
wfRunHooks( 'BookInformation', array( $this->isbn, &$wgOut ) );
-
+
# Check for a local page such as Project:Book_sources and use that if available
- $title = Title::makeTitleSafe( NS_PROJECT, wfMsg( 'booksources' ) ); # Should this be wfMsgForContent()? -- RC
+ $title = Title::makeTitleSafe( NS_PROJECT, wfMsgForContent( 'booksources' ) ); # Show list in content language
if( is_object( $title ) && $title->exists() ) {
$rev = Revision::newFromTitle( $title );
$wgOut->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) );
return true;
}
-
+
# Fall back to the defaults given in the language file
$wgOut->addWikiText( wfMsgNoTrans( 'booksources-text' ) );
$wgOut->addHtml( '<ul>' );
@@ -110,4 +110,4 @@ class SpecialBookSources extends SpecialPage {
}
-?>
+
diff --git a/includes/SpecialBrokenRedirects.php b/includes/SpecialBrokenRedirects.php
index 208a7e1f..1fb48350 100644
--- a/includes/SpecialBrokenRedirects.php
+++ b/includes/SpecialBrokenRedirects.php
@@ -20,8 +20,7 @@ class BrokenRedirectsPage extends PageQueryPage {
function isSyndicated() { return false; }
function getPageHeader( ) {
- global $wgOut;
- return $wgOut->parse( wfMsg( 'brokenredirectstext' ) );
+ return wfMsgExt( 'brokenredirectstext', array( 'parse' ) );
}
function getSQL() {
@@ -36,7 +35,8 @@ class BrokenRedirectsPage extends PageQueryPage {
FROM $redirect AS rd
JOIN $page p1 ON (rd.rd_from=p1.page_id)
LEFT JOIN $page AS p2 ON (rd_namespace=p2.page_namespace AND rd_title=p2.page_title )
- WHERE p2.page_namespace IS NULL";
+ WHERE rd_namespace >= 0
+ AND p2.page_namespace IS NULL";
return $sql;
}
@@ -92,4 +92,4 @@ function wfSpecialBrokenRedirects() {
return $sbr->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialCategories.php b/includes/SpecialCategories.php
index 45e1ae6c..596569ed 100644
--- a/includes/SpecialCategories.php
+++ b/includes/SpecialCategories.php
@@ -53,14 +53,11 @@ class CategoryPager extends AlphabeticPager {
function formatRow($result) {
global $wgLang;
$title = Title::makeTitle( NS_CATEGORY, $result->cl_to );
- return (
- '<li>' .
- $this->getSkin()->makeLinkObj( $title, $title->getText() )
- . ' ' .
- wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
- $wgLang->formatNum( $result->count ) )
- . "</li>\n" );
+ $titleText = $this->getSkin()->makeLinkObj( $title, htmlspecialchars( $title->getText() ) );
+ $count = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->count ) );
+ return Xml::tags('li', null, "$titleText ($count)" ) . "\n";
}
}
-?>
+
diff --git a/includes/SpecialConfirmemail.php b/includes/SpecialConfirmemail.php
index 58e55899..ba419f25 100644
--- a/includes/SpecialConfirmemail.php
+++ b/includes/SpecialConfirmemail.php
@@ -1,23 +1,21 @@
<?php
/**
- * Main execution point
- *
- * @param $par Parameters passed to the page
- */
-function wfSpecialConfirmemail( $par ) {
- $form = new EmailConfirmation();
- $form->execute( $par );
-}
-
-/**
* Special page allows users to request email confirmation message, and handles
* processing of the confirmation code when the link in the email is followed
*
* @addtogroup SpecialPage
+ * @author Brion Vibber
* @author Rob Church <robchur@gmail.com>
*/
-class EmailConfirmation extends SpecialPage {
+class EmailConfirmation extends UnlistedSpecialPage {
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ parent::__construct( 'Confirmemail' );
+ }
/**
* Main execution point
@@ -26,6 +24,7 @@ class EmailConfirmation extends SpecialPage {
*/
function execute( $code ) {
global $wgUser, $wgOut;
+ $this->setHeaders();
if( empty( $code ) ) {
if( $wgUser->isLoggedIn() ) {
if( User::isValidEmailAddr( $wgUser->getEmail() ) ) {
@@ -102,4 +101,4 @@ class EmailConfirmation extends SpecialPage {
}
-?>
+
diff --git a/includes/SpecialContributions.php b/includes/SpecialContributions.php
index 82c8d608..cc1b2e6f 100644
--- a/includes/SpecialContributions.php
+++ b/includes/SpecialContributions.php
@@ -9,15 +9,21 @@ class ContribsPager extends IndexPager {
var $messages, $target;
var $namespace = '', $mDb;
- function __construct( $target, $namespace = false ) {
- global $wgUser;
-
+ function __construct( $target, $namespace = false, $year = false, $month = false ) {
parent::__construct();
foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist minoreditletter' ) as $msg ) {
$this->messages[$msg] = wfMsgExt( $msg, array( 'escape') );
}
$this->target = $target;
$this->namespace = $namespace;
+
+ $year = intval($year);
+ $month = intval($month);
+
+ $this->year = ($year > 0 && $year < 10000) ? $year : false;
+ $this->month = ($month > 0 && $month < 13) ? $month : false;
+ $this->getDateCond();
+
$this->mDb = wfGetDB( DB_SLAVE, 'contributions' );
}
@@ -29,7 +35,7 @@ class ContribsPager extends IndexPager {
function getQueryInfo() {
list( $index, $userCond ) = $this->getUserCond();
- $conds = array_merge( array( 'page_id=rev_page' ), $userCond, $this->getNamespaceCond() );
+ $conds = array_merge( array('page_id=rev_page'), $userCond, $this->getNamespaceCond() );
return array(
'tables' => array( 'page', 'revision' ),
@@ -39,7 +45,7 @@ class ContribsPager extends IndexPager {
'rev_user_text', 'rev_deleted'
),
'conds' => $conds,
- 'options' => array( 'FORCE INDEX' => $index )
+ 'options' => array( 'USE INDEX' => $index )
);
}
@@ -64,6 +70,33 @@ class ContribsPager extends IndexPager {
return array();
}
}
+
+ function getDateCond() {
+ if ( $this->year || $this->month ) {
+ // Assume this year if only a month is given
+ if ( $this->year ) {
+ $year_start = $this->year;
+ } else {
+ $year_start = substr( wfTimestampNow(), 0, 4 );
+ $thisMonth = gmdate( 'n' );
+ if( $this->month > $thisMonth ) {
+ // Future contributions aren't supposed to happen. :)
+ $year_start--;
+ }
+ }
+
+ if ( $this->month ) {
+ $month_end = str_pad($this->month + 1, 2, '0', STR_PAD_LEFT);
+ $year_end = $year_start;
+ } else {
+ $month_end = 0;
+ $year_end = $year_start + 1;
+ }
+ $ts_end = str_pad($year_end . $month_end, 14, '0' );
+
+ $this->mOffset = $ts_end;
+ }
+ }
function getIndexField() {
return 'rev_timestamp';
@@ -110,7 +143,7 @@ class ContribsPager extends IndexPager {
function formatRow( $row ) {
wfProfileIn( __METHOD__ );
- global $wgLang, $wgUser;
+ global $wgLang, $wgUser, $wgContLang;
$sk = $this->getSkin();
$rev = new Revision( $row );
@@ -138,8 +171,15 @@ class ContribsPager extends IndexPager {
}
$histlink='('.$sk->makeKnownLinkObj( $page, $this->messages['hist'], 'action=history' ) . ')';
- $comment = $sk->revComment( $rev );
+ $comment = $wgContLang->getDirMark() . $sk->revComment( $rev );
$d = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true );
+
+ if( $this->target == 'newbies' ) {
+ $userlink = ' . . ' . $sk->userLink( $row->rev_user, $row->rev_user_text );
+ $userlink .= ' (' . $sk->userTalkLink( $row->rev_user, $row->rev_user_text ) . ') ';
+ } else {
+ $userlink = '';
+ }
if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
$d = '<span class="history-deleted">' . $d . '</span>';
@@ -151,7 +191,7 @@ class ContribsPager extends IndexPager {
$mflag = '';
}
- $ret = "{$d} {$histlink} {$difftext} {$mflag} {$link} {$comment} {$topmarktext}";
+ $ret = "{$d} {$histlink} {$difftext} {$mflag} {$link}{$userlink}{$comment} {$topmarktext}";
if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
$ret .= ' ' . wfMsgHtml( 'deletedrev' );
}
@@ -159,6 +199,16 @@ class ContribsPager extends IndexPager {
wfProfileOut( __METHOD__ );
return $ret;
}
+
+ /**
+ * Get the Database object in use
+ *
+ * @return Database
+ */
+ public function getDatabase() {
+ return $this->mDb;
+ }
+
}
/**
@@ -218,16 +268,48 @@ function wfSpecialContributions( $par = null ) {
if ( $wgUser->isAllowed( 'rollback' ) && $wgRequest->getBool( 'bot' ) ) {
$options['bot'] = '1';
}
+
+ $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev';
+ # Offset overrides year/month selection
+ if ( ( $month = $wgRequest->getIntOrNull( 'month' ) ) !== null && $month !== -1 ) {
+ $options['month'] = intval( $month );
+ } else {
+ $options['month'] = '';
+ }
+ if ( ( $year = $wgRequest->getIntOrNull( 'year' ) ) !== null ) {
+ $options['year'] = intval( $year );
+ } else if( $options['month'] ) {
+ $thisMonth = intval( gmdate( 'n' ) );
+ $thisYear = intval( gmdate( 'Y' ) );
+ if( intval( $options['month'] ) > $thisMonth ) {
+ $thisYear--;
+ }
+ $options['year'] = $thisYear;
+ } else {
+ $options['year'] = '';
+ }
wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id );
$wgOut->addHTML( contributionsForm( $options ) );
+ # Show original selected options, don't apply them so as to allow paging
+ $_GET['year'] = ''; // hack for Pager
+ $_GET['month'] = ''; // hack for Pager
+ if( $skip ) {
+ $options['year'] = '';
+ $options['month'] = '';
+ }
- $pager = new ContribsPager( $target, $options['namespace'] );
+ $pager = new ContribsPager( $target, $options['namespace'], $options['year'], $options['month'] );
if ( !$pager->getNumRows() ) {
$wgOut->addWikiText( wfMsg( 'nocontribs' ) );
return;
}
+
+ # Show a message about slave lag, if applicable
+ if( ( $lag = $pager->getDatabase()->getLag() ) > 0 )
+ $wgOut->showLagWarning( $lag );
+
$wgOut->addHTML(
'<p>' . $pager->getNavigationBar() . '</p>' .
$pager->getBody() .
@@ -279,6 +361,9 @@ function contributionsSub( $nt, $id ) {
}
# Other logs link
$tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() );
+
+ wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) );
+
$links = implode( ' | ', $tools );
}
@@ -314,6 +399,14 @@ function contributionsForm( $options ) {
if ( !isset( $options['contribs'] ) ) {
$options['contribs'] = 'user';
}
+
+ if ( !isset( $options['year'] ) ) {
+ $options['year'] = '';
+ }
+
+ if ( !isset( $options['month'] ) ) {
+ $options['month'] = '';
+ }
if ( $options['contribs'] == 'newbie' ) {
$options['target'] = '';
@@ -322,7 +415,7 @@ function contributionsForm( $options ) {
$f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
foreach ( $options as $name => $value ) {
- if ( in_array( $name, array( 'namespace', 'target', 'contribs' ) ) ) {
+ if ( in_array( $name, array( 'namespace', 'target', 'contribs', 'year', 'month' ) ) ) {
continue;
}
$f .= "\t" . Xml::hidden( $name, $value ) . "\n";
@@ -335,11 +428,19 @@ function contributionsForm( $options ) {
Xml::input( 'target', 20, $options['target']) . ' '.
Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
Xml::namespaceSelector( $options['namespace'], '' ) .
+ Xml::openElement( 'p' ) .
+ Xml::label( wfMsg( 'year' ), 'year' ) . ' '.
+ Xml::input( 'year', 4, $options['year'], array('id' => 'year', 'maxlength' => 4) ) . ' '.
+ Xml::label( wfMsg( 'month' ), 'month' ) . ' '.
+ Xml::monthSelector( $options['month'], -1 ) . ' '.
Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) .
- '</fieldset>' .
+ Xml::closeElement( 'p' );
+
+ $explain = wfMsgExt( 'sp-contributions-explain', 'parseinline' );
+ if( !wfEmptyMsg( 'sp-contributions-explain', $explain ) )
+ $f .= "<p>{$explain}</p>";
+
+ $f .= '</fieldset>' .
Xml::closeElement( 'form' );
return $f;
}
-
-
-?>
diff --git a/includes/SpecialDeadendpages.php b/includes/SpecialDeadendpages.php
index 48d27add..0d94161b 100644
--- a/includes/SpecialDeadendpages.php
+++ b/includes/SpecialDeadendpages.php
@@ -15,7 +15,7 @@ class DeadendPagesPage extends PageQueryPage {
}
function getPageHeader() {
- return '<p>' . wfMsg('deadendpagestext') . '</p>';
+ return wfMsgExt( 'deadendpagestext', array( 'parse' ) );
}
/**
@@ -62,4 +62,4 @@ function wfSpecialDeadendpages() {
return $depp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialDisambiguations.php b/includes/SpecialDisambiguations.php
index da0562ab..fb1d75e9 100644
--- a/includes/SpecialDisambiguations.php
+++ b/includes/SpecialDisambiguations.php
@@ -15,8 +15,7 @@ class DisambiguationsPage extends PageQueryPage {
function getPageHeader( ) {
- global $wgOut;
- return $wgOut->parse( wfMsg( 'disambiguations-text' ) );
+ return wfMsgExt( 'disambiguations-text', array( 'parse' ) );
}
function getSQL() {
@@ -105,4 +104,3 @@ function wfSpecialDisambiguations() {
return $sd->doQuery( $offset, $limit );
}
-?> \ No newline at end of file
diff --git a/includes/SpecialDoubleRedirects.php b/includes/SpecialDoubleRedirects.php
index e7b355c5..6d46fc50 100644
--- a/includes/SpecialDoubleRedirects.php
+++ b/includes/SpecialDoubleRedirects.php
@@ -19,8 +19,7 @@ class DoubleRedirectsPage extends PageQueryPage {
function isSyndicated() { return false; }
function getPageHeader( ) {
- #FIXME : probably need to add a backlink to the maintenance page.
- return '<p>'.wfMsg("doubleredirectstext")."</p><br />\n";
+ return wfMsgExt( 'doubleredirectstext', array( 'parse' ) );
}
function getSQLText( &$dbr, $namespace = null, $title = null ) {
@@ -64,6 +63,7 @@ class DoubleRedirectsPage extends PageQueryPage {
$fname = 'DoubleRedirectsPage::formatResult';
$titleA = Title::makeTitle( $result->namespace, $result->title );
+ $linkA = $skin->makeKnownLinkObj( $titleA,'', 'redirect=no' );
if ( $result && !isset( $result->nsb ) ) {
$dbr = wfGetDB( DB_SLAVE );
@@ -75,13 +75,12 @@ class DoubleRedirectsPage extends PageQueryPage {
}
}
if ( !$result ) {
- return '';
+ return "<s>{$linkA}</s>\n";
}
$titleB = Title::makeTitle( $result->nsb, $result->tb );
$titleC = Title::makeTitle( $result->nsc, $result->tc );
- $linkA = $skin->makeKnownLinkObj( $titleA,'', 'redirect=no' );
$edit = $skin->makeBrokenLinkObj( $titleA, "(".wfMsg("qbedit").")" , 'redirect=no');
$linkB = $skin->makeKnownLinkObj( $titleB, '', 'redirect=no' );
$linkC = $skin->makeKnownLinkObj( $titleC );
@@ -102,4 +101,4 @@ function wfSpecialDoubleRedirects() {
return $sdr->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialEmailuser.php b/includes/SpecialEmailuser.php
index 900a2c32..1d5a9647 100644
--- a/includes/SpecialEmailuser.php
+++ b/includes/SpecialEmailuser.php
@@ -45,6 +45,13 @@ function wfSpecialEmailuser( $par ) {
return;
}
+ if ( $wgUser->isBlockedFromEmailUser() ) {
+ // User has been blocked from sending e-mail. Show the std blocked form.
+ wfDebug( "User is blocked from sending e-mail.\n" );
+ $wgOut->blockedPage();
+ return;
+ }
+
$f = new EmailUserForm( $nu );
if ( "success" == $action ) {
@@ -108,7 +115,7 @@ class EmailUserForm {
$titleObj = SpecialPage::getTitleFor( "Emailuser" );
$action = $titleObj->escapeLocalURL( "target=" .
urlencode( $this->target->getName() ) . "&action=submit" );
- $token = $wgUser->editToken();
+ $token = htmlspecialchars( $wgUser->editToken() );
$wgOut->addHTML( "
<form id=\"emailuser\" method=\"post\" action=\"{$action}\">
@@ -185,4 +192,4 @@ class EmailUserForm {
$wgOut->returnToMain( false, $user->getUserPage() );
}
}
-?>
+
diff --git a/includes/SpecialExport.php b/includes/SpecialExport.php
index a597fdd0..12bd4d5c 100644
--- a/includes/SpecialExport.php
+++ b/includes/SpecialExport.php
@@ -53,7 +53,7 @@ function wfExportGetPagesFromCategory( $title ) {
*
*/
function wfSpecialExport( $page = '' ) {
- global $wgOut, $wgRequest, $wgExportAllowListContributors;
+ global $wgOut, $wgRequest, $wgSitename, $wgExportAllowListContributors;
global $wgExportAllowHistory, $wgExportMaxHistory;
$curonly = true;
@@ -71,7 +71,7 @@ function wfSpecialExport( $page = '' ) {
}
}
}
- else if( $wgRequest->wasPosted() ) {
+ else if( $wgRequest->wasPosted() && $page == '' ) {
$page = $wgRequest->getText( 'pages' );
$curonly = $wgRequest->getCheck( 'curonly' );
$rawOffset = $wgRequest->getVal( 'offset' );
@@ -131,6 +131,11 @@ function wfSpecialExport( $page = '' ) {
// This should provide safer streaming for pages with history
wfResetOutputBuffers();
header( "Content-type: application/xml; charset=utf-8" );
+ if( $wgRequest->getCheck( 'wpDownload' ) ) {
+ // Provide a sane filename suggestion
+ $filename = urlencode( $wgSitename . '-' . wfTimestampNow() . '.xml' );
+ $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" );
+ }
$pages = explode( "\n", $page );
$db = wfGetDB( DB_SLAVE );
@@ -164,25 +169,28 @@ function wfSpecialExport( $page = '' ) {
return;
}
- $wgOut->addWikiText( wfMsg( "exporttext" ) );
- $titleObj = SpecialPage::getTitleFor( "Export" );
+ $self = SpecialPage::getTitleFor( 'Export' );
+ $wgOut->addHtml( wfMsgExt( 'exporttext', 'parse' ) );
+
+ $form = Xml::openElement( 'form', array( 'method' => 'post',
+ 'action' => $self->getLocalUrl( 'action=submit' ) ) );
+
+ $form .= Xml::inputLabel( wfMsg( 'export-addcattext' ) , 'catname', 'catname', 40 ) . '&nbsp;';
+ $form .= Xml::submitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '<br />';
+
+ $form .= Xml::openElement( 'textarea', array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ) );
+ $form .= htmlspecialchars( $page );
+ $form .= Xml::closeElement( 'textarea' );
+ $form .= '<br />';
- $form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalUrl() ) );
-
- $form .= wfInputLabel( wfMsg( 'export-addcattext' ), 'catname', 'catname', 40 ) . ' ';
- $form .= wfSubmitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '<br />';
-
- $form .= wfOpenElement( 'textarea', array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ) ) . htmlspecialchars($page). '</textarea><br />';
-
if( $wgExportAllowHistory ) {
- $form .= wfCheck( 'curonly', true, array( 'value' => 'true', 'id' => 'curonly' ) );
- $form .= wfLabel( wfMsg( 'exportcuronly' ), 'curonly' ) . '<br />';
+ $form .= Xml::checkLabel( wfMsg( 'exportcuronly' ), 'curonly', 'curonly', true ) . '<br />';
} else {
- $wgOut->addWikiText( wfMsg( 'exportnohistory' ) );
+ $wgOut->addHtml( wfMsgExt( 'exportnohistory', 'parse' ) );
}
- $form .= wfHidden( 'action', 'submit' );
- $form .= wfSubmitButton( wfMsg( 'export-submit' ) ) . '</form>';
+ $form .= Xml::checkLabel( wfMsg( 'export-download' ), 'wpDownload', 'wpDownload', true ) . '<br />';
+
+ $form .= Xml::submitButton( wfMsg( 'export-submit' ) );
+ $form .= Xml::closeElement( 'form' );
$wgOut->addHtml( $form );
-}
-
-?>
+} \ No newline at end of file
diff --git a/includes/SpecialFewestrevisions.php b/includes/SpecialFewestrevisions.php
index 4c0cd686..ba6db8b6 100644
--- a/includes/SpecialFewestrevisions.php
+++ b/includes/SpecialFewestrevisions.php
@@ -62,4 +62,4 @@ function wfSpecialFewestrevisions() {
$frp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialImagelist.php b/includes/SpecialImagelist.php
index 92b9ae11..1688fe7c 100644
--- a/includes/SpecialImagelist.php
+++ b/includes/SpecialImagelist.php
@@ -57,7 +57,6 @@ class ImageListPager extends TablePager {
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' ),
@@ -75,7 +74,6 @@ class ImageListPager extends TablePager {
function getQueryInfo() {
$fields = $this->getFieldNames();
- unset( $fields['links'] );
$fields = array_keys( $fields );
$fields[] = 'img_user';
return array(
@@ -112,17 +110,15 @@ class ImageListPager extends TablePager {
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 );
+ $name = $this->mCurrentRow->img_name;
+ $link = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ), $value );
+ $image = wfLocalFile( $value );
+ $url = $image->getURL();
+ $download = Xml::element('a', array( "href" => $url ), $this->mMessages['imgfile'] );
+ return "$link ($download)";
case 'img_user_text':
if ( $this->mCurrentRow->img_user ) {
$link = $this->getSkin()->makeLinkObj( Title::makeTitle( NS_USER, $value ),
@@ -132,7 +128,7 @@ class ImageListPager extends TablePager {
}
return $link;
case 'img_size':
- return $wgLang->formatNum( $value );
+ return $this->getSkin()->formatSize( $value );
case 'img_description':
return $this->getSkin()->commentBlock( $value );
}
@@ -167,4 +163,4 @@ class ImageListPager extends TablePager {
}
}
-?>
+
diff --git a/includes/SpecialImport.php b/includes/SpecialImport.php
index c7b861d0..ad5d8e64 100644
--- a/includes/SpecialImport.php
+++ b/includes/SpecialImport.php
@@ -85,7 +85,7 @@ function wfSpecialImport( $page = '' ) {
}
}
- $action = $wgTitle->escapeLocalUrl( 'action=submit' );
+ $action = $wgTitle->getLocalUrl( 'action=submit' );
if( $wgUser->isAllowed( 'importupload' ) ) {
$wgOut->addWikiText( wfMsg( "importtext" ) );
@@ -209,6 +209,9 @@ class ImportReporter {
$nullRevision = Revision::newNullRevision(
$dbw, $title->getArticleId(), $comment, true );
$nullRevision->insertOn( $dbw );
+ # Update page record
+ $article = new Article( $title );
+ $article->updateRevisionOn( $dbw, $nullRevision );
}
}
@@ -857,13 +860,22 @@ class ImportStreamSource {
}
}
- function newFromURL( $url ) {
+ function newFromURL( $url, $method = 'GET' ) {
wfDebug( __METHOD__ . ": opening $url\n" );
- # fopen-wrappers are normally turned off for security.
- ini_set( "allow_url_fopen", true );
- $ret = ImportStreamSource::newFromFile( $url );
- ini_set( "allow_url_fopen", false );
- return $ret;
+ # Use the standard HTTP fetch function; it times out
+ # quicker and sorts out user-agent problems which might
+ # otherwise prevent importing from large sites, such
+ # as the Wikimedia cluster, etc.
+ $data = Http::request( $method, $url );
+ if( $data !== false ) {
+ $file = tmpfile();
+ fwrite( $file, $data );
+ fflush( $file );
+ fseek( $file, 0 );
+ return new ImportStreamSource( $file );
+ } else {
+ return new WikiErrorMsg( 'importcantopen' );
+ }
}
public static function newFromInterwiki( $interwiki, $page, $history=false ) {
@@ -873,10 +885,11 @@ class ImportStreamSource {
} else {
$params = $history ? 'history=1' : '';
$url = $link->getFullUrl( $params );
- return ImportStreamSource::newFromURL( $url );
+ # For interwikis, use POST to avoid redirects.
+ return ImportStreamSource::newFromURL( $url, "POST" );
}
}
}
-?>
+
diff --git a/includes/SpecialIpblocklist.php b/includes/SpecialIpblocklist.php
index 8cb5729e..4f093dcb 100644
--- a/includes/SpecialIpblocklist.php
+++ b/includes/SpecialIpblocklist.php
@@ -18,30 +18,49 @@ function wfSpecialIpblocklist() {
$ipu = new IPUnblockForm( $ip, $id, $reason );
- if ( "success" == $action ) {
- $ipu->showList( $wgOut->parse( wfMsg( 'unblocked', $successip ) ) );
- } else if ( "submit" == $action && $wgRequest->wasPosted() &&
- $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
- if ( ! $wgUser->isAllowed('block') ) {
+ if( $action == 'unblock' ) {
+ # Check permissions
+ if( !$wgUser->isAllowed( 'block' ) ) {
$wgOut->permissionRequired( 'block' );
return;
}
- # Can't unblock when the database is locked
+ # Check for database lock
if( wfReadOnly() ) {
$wgOut->readOnlyPage();
return;
}
- $ipu->doSubmit();
- } else if ( "unblock" == $action ) {
- # Can't unblock when the database is locked
+ # Show unblock form
+ $ipu->showForm( '' );
+ } elseif( $action == 'submit' && $wgRequest->wasPosted()
+ && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ # Check permissions
+ if( !$wgUser->isAllowed( 'block' ) ) {
+ $wgOut->permissionRequired( 'block' );
+ return;
+ }
+ # Check for database lock
if( wfReadOnly() ) {
$wgOut->readOnlyPage();
return;
}
- $ipu->showForm( "" );
+ # Remove blocks and redirect user to success page
+ $ipu->doSubmit();
+ } elseif( $action == 'success' ) {
+ # Inform the user of a successful unblock
+ # (No need to check permissions or locks here,
+ # if something was done, then it's too late!)
+ if ( substr( $successip, 0, 1) == '#' ) {
+ // A block ID was unblocked
+ $ipu->showList( $wgOut->parse( wfMsg( 'unblocked-id', $successip ) ) );
+ } else {
+ // A username/IP was unblocked
+ $ipu->showList( $wgOut->parse( wfMsg( 'unblocked', $successip ) ) );
+ }
} else {
- $ipu->showList( "" );
+ # Just show the block list
+ $ipu->showList( '' );
}
+
}
/**
@@ -58,7 +77,7 @@ class IPUnblockForm {
}
function showForm( $err ) {
- global $wgOut, $wgUser, $wgSysopUserBans;
+ global $wgOut, $wgUser, $wgSysopUserBans, $wgContLang;
$wgOut->setPagetitle( wfMsg( 'unblockip' ) );
$wgOut->addWikiText( wfMsg( 'unblockiptext' ) );
@@ -67,7 +86,8 @@ class IPUnblockForm {
$ipr = wfMsgHtml( 'ipbreason' );
$ipus = wfMsgHtml( 'ipusubmit' );
$titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
- $action = $titleObj->escapeLocalURL( "action=submit" );
+ $action = $titleObj->getLocalURL( "action=submit" );
+ $alignRight = $wgContLang->isRtl() ? 'left' : 'right';
if ( "" != $err ) {
$wgOut->setSubtitle( wfMsg( "formerror" ) );
@@ -80,39 +100,43 @@ class IPUnblockForm {
$block = Block::newFromID( $this->id );
if ( $block ) {
$encName = htmlspecialchars( $block->getRedactedName() );
- $encId = htmlspecialchars( $this->id );
- $addressPart = $encName . "<input type='hidden' name=\"id\" value=\"$encId\" />";
+ $encId = $this->id;
+ $addressPart = $encName . Xml::hidden( 'id', $encId );
}
}
if ( !$addressPart ) {
- $addressPart = "<input tabindex='1' type='text' size='20' " .
- "name=\"wpUnblockAddress\" value=\"" . htmlspecialchars( $this->ip ) . "\" />";
+ $addressPart = Xml::input( 'wpUnblockAddress', 20, $this->ip, array( 'type' => 'text', 'tabindex' => '1' ) );
}
- $wgOut->addHTML( "
-<form id=\"unblockip\" method=\"post\" action=\"{$action}\">
- <table border='0'>
- <tr>
- <td align='right'>{$ipa}:</td>
- <td align='left'>
- {$addressPart}
- </td>
- </tr>
- <tr>
- <td align='right'>{$ipr}:</td>
- <td align='left'>
- <input tabindex='1' type='text' size='40' name=\"wpUnblockReason\" value=\"" . htmlspecialchars( $this->reason ) . "\" />
- </td>
- </tr>
- <tr>
- <td>&nbsp;</td>
- <td align='left'>
- <input tabindex='2' type='submit' name=\"wpBlock\" value=\"{$ipus}\" />
- </td>
- </tr>
- </table>
- <input type='hidden' name='wpEditToken' value=\"{$token}\" />
-</form>\n" );
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'unblockip' ) ) .
+ Xml::openElement( 'table', array( 'border' => '0' ) ).
+ "<tr>
+ <td align='$alignRight'>
+ {$ipa}
+ </td>
+ <td>
+ {$addressPart}
+ </td>
+ </tr>
+ <tr>
+ <td align='$alignRight'>
+ {$ipr}
+ </td>
+ <td>" .
+ Xml::input( 'wpUnblockReason', 40, $this->reason, array( 'type' => 'text', 'tabindex' => '2' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>" .
+ Xml::submitButton( $ipus, array( 'name' => 'wpBlock', 'tabindex' => '3' ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::hidden( 'wpEditToken', $token ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
}
@@ -200,46 +224,34 @@ class IPUnblockForm {
}
}
- # TODO: difference message between
- # a) an real empty list and
- # b) requested ip/username not on list
$pager = new IPBlocklistPager( $this, $conds );
if ( $pager->getNumRows() ) {
- $s = $this->searchForm() .
- $pager->getNavigationBar();
- $s .= "<ul>" .
- $pager->getBody() .
- "</ul>";
- $s .= $pager->getNavigationBar();
+ $wgOut->addHTML(
+ $this->searchForm() .
+ $pager->getNavigationBar() .
+ Xml::tags( 'ul', null, $pager->getBody() ) .
+ $pager->getNavigationBar()
+ );
+ } elseif ( $this->ip != '') {
+ $wgOut->addHTML( $this->searchForm() );
+ $wgOut->addWikiText( wfMsg( 'ipblocklist-no-results' ) );
} else {
- $s = $this->searchForm() .
- '<p>' . wfMsgHTML( 'ipblocklistempty' ) . '</p>';
+ $wgOut->addWikiText( wfMsg( 'ipblocklist-empty' ) );
}
- $wgOut->addHTML( $s );
}
function searchForm() {
global $wgTitle, $wgScript, $wgRequest;
return
- wfElement( 'form', array(
- 'action' => $wgScript ),
- null ) .
- wfHidden( 'title', $wgTitle->getPrefixedDbKey() ) .
- wfElement( 'input', array(
- 'type' => 'hidden',
- 'name' => 'action',
- 'value' => 'search' ) ).
- wfElement( 'input', array(
- 'type' => 'hidden',
- 'name' => 'limit',
- 'value' => $wgRequest->getText( 'limit' ) ) ) .
- wfElement( 'input', array(
- 'name' => 'ip',
- 'value' => $this->ip ) ) .
- wfElement( 'input', array(
- 'type' => 'submit',
- 'value' => wfMsg( 'ipblocklist-submit' ) ) ) .
- '</form>';
+ Xml::tags( 'form', array( 'action' => $wgScript ),
+ Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'ipblocklist-legend' ) ) .
+ Xml::inputLabel( wfMsg( 'ipblocklist-username' ), 'ip', 'ip', /* size */ false, $this->ip ) .
+ '&nbsp;' .
+ Xml::submitButton( wfMsg( 'ipblocklist-submit' ) ) .
+ Xml::closeElement( 'fieldset' )
+ );
}
/**
@@ -257,7 +269,7 @@ class IPUnblockForm {
if( is_null( $msg ) ) {
$msg = array();
$keys = array( 'infiniteblock', 'expiringblock', 'contribslink', 'unblocklink',
- 'anononlyblock', 'createaccountblock', 'noautoblockblock' );
+ 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock' );
foreach( $keys as $key ) {
$msg[$key] = wfMsgHtml( $key );
}
@@ -275,8 +287,8 @@ class IPUnblockForm {
if( $block->mAuto ) {
$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( SpecialPage::getSafeTitleFor( 'Contributions', $block->mAddress ), $msg['contribslink'] ) . ')';
+ $target = $sk->userLink( $block->mUser, $block->mAddress )
+ . $sk->userToolLinks( $block->mUser, $block->mAddress, false, Linker::TOOL_LINKS_NOBLOCK );
}
$formattedTime = $wgLang->timeanddate( $block->mTimestamp, true );
@@ -298,6 +310,10 @@ class IPUnblockForm {
$properties[] = $msg['noautoblockblock'];
}
+ if ( $block->mBlockEmail && $block->mUser ) {
+ $properties[] = $msg['emailblock'];
+ }
+
$properties = implode( ', ', $properties );
$line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $properties ) );
@@ -383,4 +399,4 @@ class IPBlocklistPager extends ReverseChronologicalPager {
}
}
-?>
+
diff --git a/includes/SpecialListredirects.php b/includes/SpecialListredirects.php
index 09dc2b39..581ea55b 100644
--- a/includes/SpecialListredirects.php
+++ b/includes/SpecialListredirects.php
@@ -62,4 +62,4 @@ function wfSpecialListredirects() {
$lrp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialListusers.php b/includes/SpecialListusers.php
index 42498430..460d4259 100644
--- a/includes/SpecialListusers.php
+++ b/includes/SpecialListusers.php
@@ -103,7 +103,6 @@ class UsersPager extends AlphabeticPager {
$this->doQuery();
}
$batch = new LinkBatch;
- $db = $this->mDb;
$this->mResult->rewind();
@@ -116,35 +115,30 @@ class UsersPager extends AlphabeticPager {
}
function getPageHeader( ) {
- global $wgRequest;
+ global $wgScript, $wgRequest;
$self = $this->getTitle();
# Form tag
- $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $self->getLocalUrl() ) ) .
+ $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
'<fieldset>' .
Xml::element( 'legend', array(), wfMsg( 'listusers' ) );
+ $out .= Xml::hidden( 'title', $self->getPrefixedDbKey() );
# Username field
$out .= Xml::label( wfMsg( 'listusersfrom' ), 'offset' ) . ' ' .
Xml::input( 'username', 20, $this->requestedUser, array( 'id' => 'offset' ) ) . ' ';
- if( $this->mLimit )
- $out .= Xml::hidden( 'limit', $this->mLimit );
-
# Group drop-down list
$out .= Xml::label( wfMsg( 'group' ), 'group' ) . ' ' .
Xml::openElement('select', array( 'name' => 'group', 'id' => 'group' ) ) .
- Xml::option( wfMsg( 'group-all' ), '' ); # Item for "all groups"
-
- $groups = User::getAllGroups();
- foreach( $groups as $group ) {
- $attribs = array( 'value' => $group );
- $attribs['selected'] = ( $group == $this->requestedGroup ) ? 'selected' : '';
- $out .= Xml::option( User::getGroupName( $group ), $attribs['value'], $attribs['selected'] );
- }
+ Xml::option( wfMsg( 'group-all' ), '' );
+ foreach( User::getAllGroups() as $group )
+ $out .= Xml::option( User::getGroupName( $group ), $group, $group == $this->requestedGroup );
$out .= Xml::closeElement( 'select' ) . ' ';
# Submit button and form bottom
+ if( $this->mLimit )
+ $out .= Xml::hidden( 'limit', $this->mLimit );
$out .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
'</fieldset>' .
Xml::closeElement( 'form' );
@@ -204,10 +198,6 @@ class UsersPager extends AlphabeticPager {
function wfSpecialListusers( $par = null ) {
global $wgRequest, $wgOut;
- list( $limit, $offset ) = wfCheckLimits();
-
- $groupTarget = isset($par) ? $par : $wgRequest->getVal( 'group' );
-
$up = new UsersPager($par);
# getBody() first to check, if empty
@@ -224,4 +214,4 @@ function wfSpecialListusers( $par = null ) {
$wgOut->addHTML( $s );
}
-?>
+
diff --git a/includes/SpecialLockdb.php b/includes/SpecialLockdb.php
index db4006f5..e57717e2 100644
--- a/includes/SpecialLockdb.php
+++ b/includes/SpecialLockdb.php
@@ -131,4 +131,4 @@ END
}
-?>
+
diff --git a/includes/SpecialLog.php b/includes/SpecialLog.php
index 3c9d0960..f0794eb5 100644
--- a/includes/SpecialLog.php
+++ b/includes/SpecialLog.php
@@ -74,7 +74,8 @@ class LogReader {
// XXX This all needs to use Pager, ugly hack for now.
global $wgMiserMode;
- if ($wgMiserMode && ($this->offset >10000)) $this->offset=10000;
+ if( $wgMiserMode )
+ $this->offset = min( $this->offset, 10000 );
}
/**
@@ -123,9 +124,10 @@ class LogReader {
function limitTitle( $page , $pattern ) {
global $wgMiserMode;
$title = Title::newFromText( $page );
- if( empty( $page ) || is_null( $title ) ) {
+
+ if( strlen( $page ) == 0 || !$title instanceof Title )
return false;
- }
+
$this->title =& $title;
$this->pattern = $pattern;
$ns = $title->getNamespace();
@@ -215,6 +217,23 @@ class LogReader {
return $this->title->getPrefixedText();
}
}
+
+ /**
+ * Is there at least one row?
+ *
+ * @return bool
+ */
+ public function hasRows() {
+ # Little hack...
+ $limit = $this->limit;
+ $this->limit = 1;
+ $res = $this->db->query( $this->getQuery() );
+ $this->limit = $limit;
+ $ret = $this->db->numRows( $res ) > 0;
+ $this->db->freeResult( $res );
+ return $ret;
+ }
+
}
/**
@@ -222,19 +241,25 @@ class LogReader {
* @addtogroup SpecialPage
*/
class LogViewer {
+ const NO_ACTION_LINK = 1;
+
/**
* @var LogReader $reader
*/
var $reader;
var $numResults = 0;
+ var $flags = 0;
/**
* @param LogReader &$reader where to get our data from
+ * @param integer $flags Bitwise combination of flags:
+ * self::NO_ACTION_LINK Don't show restore/unblock/block links
*/
- function LogViewer( &$reader ) {
+ function LogViewer( &$reader, $flags = 0 ) {
global $wgUser;
$this->skin = $wgUser->getSkin();
$this->reader =& $reader;
+ $this->flags = $flags;
}
/**
@@ -325,7 +350,7 @@ class LogViewer {
* @private
*/
function logLine( $s ) {
- global $wgLang, $wgUser;;
+ global $wgLang, $wgUser, $wgContLang;
$skin = $wgUser->getSkin();
$title = Title::makeTitle( $s->log_namespace, $s->log_title );
$time = $wgLang->timeanddate( wfTimestamp(TS_MW, $s->log_timestamp), true );
@@ -340,42 +365,47 @@ class LogViewer {
}
$userLink = $this->skin->userLink( $s->log_user, $s->user_name ) . $this->skin->userToolLinksRedContribs( $s->log_user, $s->user_name );
- $comment = $this->skin->commentBlock( $s->log_comment );
+ $comment = $wgContLang->getDirMark() . $this->skin->commentBlock( $s->log_comment );
$paramArray = LogPage::extractParams( $s->log_params );
$revert = '';
// show revertmove link
- if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) {
- $destTitle = Title::newFromText( $paramArray[0] );
- if ( $destTitle ) {
- $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
- wfMsg( 'revertmove' ),
- 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
- '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
- '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
- '&wpMovetalk=0' ) . ')';
+ if ( !( $this->flags & self::NO_ACTION_LINK ) ) {
+ if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) {
+ $destTitle = Title::newFromText( $paramArray[0] );
+ if ( $destTitle ) {
+ $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
+ wfMsg( 'revertmove' ),
+ 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
+ '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
+ '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
+ '&wpMovetalk=0' ) . ')';
+ }
+ // show undelete link
+ } elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) {
+ $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
+ wfMsg( 'undeletebtn' ) ,
+ 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')';
+
+ // show unblock link
+ } elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) {
+ $revert = '(' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ),
+ wfMsg( 'unblocklink' ),
+ 'action=unblock&ip=' . urlencode( $s->log_title ) ) . ')';
+ // show change protection link
+ } elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) {
+ $revert = '(' . $skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' ) . ')';
+ // show user tool links for self created users
+ // TODO: The extension should be handling this, get it out of core!
+ } elseif ( $s->log_action == 'create2' ) {
+ if( isset( $paramArray[0] ) ) {
+ $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true );
+ } else {
+ # Fall back to a blue contributions link
+ $revert = $this->skin->userToolLinks( 1, $s->log_title );
+ }
+ # Suppress $comment from old entries, not needed and can contain incorrect links
+ $comment = '';
}
- // show undelete link
- } elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) {
- $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
- wfMsg( 'undeletebtn' ) ,
- 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')';
-
- // show unblock link
- } elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) {
- $revert = '(' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ),
- wfMsg( 'unblocklink' ),
- 'action=unblock&ip=' . urlencode( $s->log_title ) ) . ')';
- // show change protection link
- } elseif ( $s->log_action == 'protect' && $wgUser->isAllowed( 'protect' ) ) {
- $revert = '(' . $skin->makeKnownLink( $title->getPrefixedDBkey() ,
- wfMsg( 'protect_change' ),
- 'action=unprotect' ) . ')';
- // show user tool links for self created users
- } elseif ( $s->log_action == 'create2' ) {
- $revert = $this->skin->userToolLinksRedContribs( $s->log_user, $s->log_title );
- // do not show $comment for self created accounts. It includes wrong user tool links:
- // 'blockip' for users w/o block allowance and broken links for very long usernames (bug 4756)
- $comment = '';
}
$action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true, true );
@@ -497,4 +527,4 @@ class LogViewer {
}
-?>
+
diff --git a/includes/SpecialLonelypages.php b/includes/SpecialLonelypages.php
index 430af7a7..e652f9d4 100644
--- a/includes/SpecialLonelypages.php
+++ b/includes/SpecialLonelypages.php
@@ -15,7 +15,7 @@ class LonelyPagesPage extends PageQueryPage {
return "Lonelypages";
}
function getPageHeader() {
- return '<p>' . wfMsg('lonelypagestext') . '</p>';
+ return wfMsgExt( 'lonelypagestext', array( 'parse' ) );
}
function sortDescending() {
@@ -57,4 +57,4 @@ function wfSpecialLonelypages() {
return $lpp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialLongpages.php b/includes/SpecialLongpages.php
index 40659889..a8a1e199 100644
--- a/includes/SpecialLongpages.php
+++ b/includes/SpecialLongpages.php
@@ -30,4 +30,4 @@ function wfSpecialLongpages() {
$lpp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialMIMEsearch.php b/includes/SpecialMIMEsearch.php
index d50efc02..c89c1af6 100644
--- a/includes/SpecialMIMEsearch.php
+++ b/includes/SpecialMIMEsearch.php
@@ -66,7 +66,7 @@ class MIMEsearchPage extends QueryPage {
$text = $wgContLang->convert( $nt->getText() );
$plink = $skin->makeLink( $nt->getPrefixedText(), $text );
- $download = $skin->makeMediaLink( $nt->getText(), 'fuck me!', wfMsgHtml( 'download' ) );
+ $download = $skin->makeMediaLinkObj( $nt, wfMsgHtml( 'download' ) );
$bytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'),
$wgLang->formatNum( $result->img_size ) );
$dimensions = wfMsgHtml( 'widthheight', $wgLang->formatNum( $result->img_width ),
@@ -87,33 +87,16 @@ function wfSpecialMIMEsearch( $par = null ) {
$mime = isset( $par ) ? $par : $wgRequest->getText( 'mime' );
$wgOut->addHTML(
- wfElement( 'form',
+ Xml::openElement( 'form',
array(
'id' => 'specialmimesearch',
'method' => 'get',
'action' => $wgTitle->escapeLocalUrl()
- ),
- null
+ )
) .
- wfOpenElement( 'label' ) .
- wfMsgHtml( 'mimetype' ) .
- wfElement( 'input', array(
- 'type' => 'text',
- 'size' => 20,
- 'name' => 'mime',
- 'value' => $mime
- ),
- ''
- ) .
- ' ' .
- wfElement( 'input', array(
- 'type' => 'submit',
- 'value' => wfMsg( 'ilsubmit' )
- ),
- ''
- ) .
- wfCloseElement( 'label' ) .
- wfCloseElement( 'form' )
+ Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) .
+ Xml::submitButton( wfMsg( 'ilsubmit' ) ) .
+ Xml::closeElement( 'form' )
);
list( $major, $minor ) = wfSpecialMIMEsearchParse( $mime );
@@ -155,4 +138,4 @@ function wfSpecialMIMEsearchValidType( $type ) {
return in_array( $type, $types );
}
-?>
+
diff --git a/includes/SpecialMostcategories.php b/includes/SpecialMostcategories.php
index df2b9adf..589b96ee 100644
--- a/includes/SpecialMostcategories.php
+++ b/includes/SpecialMostcategories.php
@@ -38,6 +38,7 @@ class MostcategoriesPage extends QueryPage {
function formatResult( $skin, $result ) {
global $wgLang;
$title = Title::makeTitleSafe( $result->namespace, $result->title );
+ if ( !$title instanceof Title ) { throw new MWException('Invalid title in database'); }
$count = wfMsgExt( 'ncategories', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->value ) );
$link = $skin->makeKnownLinkObj( $title, $title->getText() );
return wfSpecialList( $link, $count );
@@ -55,4 +56,4 @@ function wfSpecialMostcategories() {
$wpp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialMostimages.php b/includes/SpecialMostimages.php
index 9d16f389..beb42fc1 100644
--- a/includes/SpecialMostimages.php
+++ b/includes/SpecialMostimages.php
@@ -52,4 +52,4 @@ function wfSpecialMostimages() {
$wpp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialMostlinked.php b/includes/SpecialMostlinked.php
index ab089cf8..b4de0a0e 100644
--- a/includes/SpecialMostlinked.php
+++ b/includes/SpecialMostlinked.php
@@ -90,4 +90,4 @@ function wfSpecialMostlinked() {
$wpp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialMostlinkedcategories.php b/includes/SpecialMostlinkedcategories.php
index 725e5b39..d0a99b3b 100644
--- a/includes/SpecialMostlinkedcategories.php
+++ b/includes/SpecialMostlinkedcategories.php
@@ -72,4 +72,4 @@ function wfSpecialMostlinkedCategories() {
$wpp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialMostlinkedtemplates.php b/includes/SpecialMostlinkedtemplates.php
new file mode 100644
index 00000000..e7e7afcc
--- /dev/null
+++ b/includes/SpecialMostlinkedtemplates.php
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * Special page lists templates with a large number of
+ * transclusion links, i.e. "most used" templates
+ *
+ * @addtogroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+class SpecialMostlinkedtemplates extends QueryPage {
+
+ /**
+ * Name of the report
+ *
+ * @return string
+ */
+ public function getName() {
+ return 'Mostlinkedtemplates';
+ }
+
+ /**
+ * Is this report expensive, i.e should it be cached?
+ *
+ * @return bool
+ */
+ public function isExpensive() {
+ return true;
+ }
+
+ /**
+ * Is there a feed available?
+ *
+ * @return bool
+ */
+ public function isSyndicated() {
+ return false;
+ }
+
+ /**
+ * Sort the results in descending order?
+ *
+ * @return bool
+ */
+ public function sortDescending() {
+ return true;
+ }
+
+ /**
+ * Generate SQL for the report
+ *
+ * @return string
+ */
+ public function getSql() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $templatelinks = $dbr->tableName( 'templatelinks' );
+ $name = $dbr->addQuotes( $this->getName() );
+ return "SELECT {$name} AS type,
+ " . NS_TEMPLATE . " AS namespace,
+ tl_title AS title,
+ COUNT(*) AS value
+ FROM {$templatelinks}
+ WHERE tl_namespace = " . NS_TEMPLATE . "
+ GROUP BY 1, 2, 3";
+ }
+
+ /**
+ * Pre-cache page existence to speed up link generation
+ *
+ * @param Database $dbr Database connection
+ * @param int $res Result pointer
+ */
+ public function preprocessResults( $dbr, $res ) {
+ $batch = new LinkBatch();
+ while( $row = $dbr->fetchObject( $res ) ) {
+ $title = Title::makeTitleSafe( $row->namespace, $row->title );
+ $batch->addObj( $title );
+ }
+ $batch->execute();
+ if( $dbr->numRows( $res ) > 0 )
+ $dbr->dataSeek( $res, 0 );
+ }
+
+ /**
+ * Format a result row
+ *
+ * @param Skin $skin Skin to use for UI elements
+ * @param object $result Result row
+ * @return string
+ */
+ public function formatResult( $skin, $result ) {
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ if( $title instanceof Title ) {
+ return wfSpecialList(
+ $skin->makeLinkObj( $title ),
+ $this->makeWlhLink( $title, $skin, $result )
+ );
+ } else {
+ $tsafe = htmlspecialchars( $result->title );
+ return "Invalid title in result set; {$tsafe}";
+ }
+ }
+
+ /**
+ * Make a "what links here" link for a given title
+ *
+ * @param Title $title Title to make the link for
+ * @param Skin $skin Skin to use
+ * @param object $result Result row
+ * @return string
+ */
+ private function makeWlhLink( $title, $skin, $result ) {
+ global $wgLang;
+ $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' );
+ $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $result->value ) );
+ return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() );
+ }
+
+}
+
+/**
+ * Execution function
+ *
+ * @param mixed $par Parameters passed to the page
+ */
+function wfSpecialMostlinkedtemplates( $par = false ) {
+ list( $limit, $offset ) = wfCheckLimits();
+ $mlt = new SpecialMostlinkedtemplates();
+ $mlt->doQuery( $offset, $limit );
+}
+
diff --git a/includes/SpecialMostrevisions.php b/includes/SpecialMostrevisions.php
index 59157056..9479a583 100644
--- a/includes/SpecialMostrevisions.php
+++ b/includes/SpecialMostrevisions.php
@@ -63,4 +63,4 @@ function wfSpecialMostrevisions() {
$wpp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialMovepage.php b/includes/SpecialMovepage.php
index d8f01874..cfc434ae 100644
--- a/includes/SpecialMovepage.php
+++ b/includes/SpecialMovepage.php
@@ -12,7 +12,7 @@ function wfSpecialMovepage( $par = null ) {
# Check rights
if ( !$wgUser->isAllowed( 'move' ) ) {
- $wgOut->showErrorPage( 'movenologin', 'movenologintext' );
+ $wgOut->showPermissionsErrorPage( array( $wgUser->isAnon() ? 'movenologintext' : 'movenotallowed' ) );
return;
}
@@ -105,14 +105,10 @@ class MovePageForm {
if ( $err == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) {
$wgOut->addWikiText( wfMsg( 'delete_and_move_text', $encNewTitle ) );
$movepagebtn = wfMsgHtml( 'delete_and_move' );
- $confirmText = wfMsgHtml( 'delete_and_move_confirm' );
$submitVar = 'wpDeleteAndMove';
$confirm = "
<tr>
- <td align='$end'>
- <input type='checkbox' name='wpConfirm' id='wpConfirm' value=\"true\" />
- </td>
- <td align='$start'><label for='wpConfirm'>{$confirmText}</label></td>
+ <td></td><td>" . Xml::checkLabel( wfMsg( 'delete_and_move_confirm' ), 'wpConfirm', 'wpConfirm' ) . "</td>
</tr>";
$err = '';
} else {
@@ -131,7 +127,6 @@ class MovePageForm {
$movearticle = wfMsgHtml( 'movearticle' );
$newtitle = wfMsgHtml( 'newtitle' );
- $movetalk = wfMsgHtml( 'movetalk' );
$movereason = wfMsgHtml( 'movereason' );
$titleObj = SpecialPage::getTitleFor( 'Movepage' );
@@ -149,18 +144,18 @@ class MovePageForm {
<form id=\"movepage\" method=\"post\" action=\"{$action}\">
<table border='0'>
<tr>
- <td align='$end'>{$movearticle}:</td>
+ <td align='$end'>{$movearticle}</td>
<td align='$start'><strong>{$oldTitle}</strong></td>
</tr>
<tr>
- <td align='$end'><label for='wpNewTitle'>{$newtitle}:</label></td>
+ <td align='$end'><label for='wpNewTitle'>{$newtitle}</label></td>
<td align='$start'>
<input type='text' size='40' name='wpNewTitle' id='wpNewTitle' value=\"{$encNewTitle}\" />
<input type='hidden' name=\"wpOldTitle\" value=\"{$encOldTitle}\" />
</td>
</tr>
<tr>
- <td align='$end' valign='top'><br /><label for='wpReason'>{$movereason}:</label></td>
+ <td align='$end' valign='top'><br /><label for='wpReason'>{$movereason}</label></td>
<td align='$start' valign='top'><br />
<textarea cols='60' rows='2' name='wpReason' id='wpReason'>{$encReason}</textarea>
</td>
@@ -169,20 +164,16 @@ class MovePageForm {
if ( $considerTalk ) {
$wgOut->addHTML( "
<tr>
- <td align='$end'>
- <input type='checkbox' id=\"wpMovetalk\" name=\"wpMovetalk\"{$moveTalkChecked} value=\"1\" />
- </td>
- <td><label for=\"wpMovetalk\">{$movetalk}</label></td>
+ <td></td><td>" . Xml::checkLabel( wfMsg( 'movetalk' ), 'wpMovetalk', 'wpMovetalk', $moveTalkChecked ) . "</td>
</tr>" );
}
-
+
$watchChecked = $this->watch || $wgUser->getBoolOption( 'watchmoves' ) || $ot->userIsWatching();
$watch = '<tr>';
- $watch .= "<td align=\"$end\">" . Xml::check( 'wpWatch', $watchChecked, array( 'id' => 'watch' ) ) . '</td>';
- $watch .= '<td>' . Xml::label( wfMsg( 'move-watch' ), 'watch' ) . '</td>';
+ $watch .= '<td></td><td>' . Xml::checkLabel( wfMsg( 'move-watch' ), 'wpWatch', 'watch', $watchChecked ) . '</td>';
$watch .= '</tr>';
$wgOut->addHtml( $watch );
-
+
$wgOut->addHTML( "
{$confirm}
<tr>
@@ -275,32 +266,38 @@ class MovePageForm {
}
function showSuccess() {
- global $wgOut, $wgRequest, $wgRawHtml;
+ global $wgOut, $wgRequest, $wgUser;
+
+ $old = Title::newFromText( $wgRequest->getVal( 'oldtitle' ) );
+ $new = Title::newFromText( $wgRequest->getVal( 'newtitle' ) );
+
+ if( is_null( $old ) || is_null( $new ) ) {
+ throw new ErrorPageError( 'badtitle', 'badtitletext' );
+ }
$wgOut->setPagetitle( wfMsg( 'movepage' ) );
$wgOut->setSubtitle( wfMsg( 'pagemovedsub' ) );
- $oldText = wfEscapeWikiText( $wgRequest->getVal('oldtitle') );
- $newText = wfEscapeWikiText( $wgRequest->getVal('newtitle') );
- $talkmoved = $wgRequest->getVal('talkmoved');
+ $talkmoved = $wgRequest->getVal( 'talkmoved' );
+ $oldUrl = $old->getFullUrl( 'redirect=no' );
+ $newUrl = $new->getFullURl();
+ $oldText = $old->getPrefixedText();
+ $newText = $new->getPrefixedText();
+ $oldLink = "<span class='plainlinks'>[$oldUrl $oldText]</span>";
+ $newLink = "<span class='plainlinks'>[$newUrl $newText]</span>";
- $text = wfMsg( 'pagemovedtext', $oldText, $newText );
-
- $allowHTML = $wgRawHtml;
- $wgRawHtml = false;
- $wgOut->addWikiText( $text );
- $wgRawHtml = $allowHTML;
+ $s = wfMsg( 'movepage-moved', $oldLink, $newLink, $oldText, $newText );
if ( $talkmoved == 1 ) {
- $wgOut->addWikiText( wfMsg( 'talkpagemoved' ) );
+ $s .= "\n\n" . wfMsg( 'talkpagemoved' );
} elseif( 'articleexists' == $talkmoved ) {
- $wgOut->addWikiText( wfMsg( 'talkexists' ) );
+ $s .= "\n\n" . wfMsg( 'talkexists' );
} else {
- $oldTitle = Title::newFromText( $oldText );
- if ( isset( $oldTitle ) && !$oldTitle->isTalkPage() && $talkmoved != 'notalkpage' ) {
- $wgOut->addWikiText( wfMsg( 'talkpagenotmoved', wfMsg( $talkmoved ) ) );
+ if( !$old->isTalkPage() && $talkmoved != 'notalkpage' ) {
+ $s .= "\n\n" . wfMsg( 'talkpagenotmoved', wfMsg( $talkmoved ) );
}
}
+ $wgOut->addWikiText( $s );
}
function showLogFragment( $title, &$out ) {
@@ -311,4 +308,4 @@ class MovePageForm {
}
}
-?>
+
diff --git a/includes/SpecialNewimages.php b/includes/SpecialNewimages.php
index 72b169b1..f81a70f4 100644
--- a/includes/SpecialNewimages.php
+++ b/includes/SpecialNewimages.php
@@ -135,10 +135,9 @@ function wfSpecialNewimages( $par, $specialPage ) {
$ut = $s->img_user_text;
$nt = Title::newFromText( $name, NS_IMAGE );
- $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" );
+ $gallery->add( $nt, "$ul<br />\n<i>".$wgLang->timeanddate( $s->img_timestamp, true )."</i><br />\n" );
$timestamp = wfTimestamp( TS_MW, $s->img_timestamp );
if( empty( $firstTimestamp ) ) {
@@ -205,4 +204,4 @@ function wfSpecialNewimages( $par, $specialPage ) {
}
}
-?>
+
diff --git a/includes/SpecialNewpages.php b/includes/SpecialNewpages.php
index 48037a73..abd5e018 100644
--- a/includes/SpecialNewpages.php
+++ b/includes/SpecialNewpages.php
@@ -36,12 +36,19 @@ class NewPagesPage extends QueryPage {
}
}
+ private function makeNamespaceWhere() {
+ return $this->namespace !== 'all'
+ ? ' AND rc_namespace = ' . intval( $this->namespace )
+ : '';
+ }
+
function getSQL() {
global $wgUser, $wgUseRCPatrol;
$usepatrol = ( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) ? 1 : 0;
$dbr = wfGetDB( DB_SLAVE );
list( $recentchanges, $page ) = $dbr->tableNamesN( 'recentchanges', 'page' );
+ $nsfilter = $this->makeNamespaceWhere();
$uwhere = $this->makeUserWhere( $dbr );
# FIXME: text will break with compression
@@ -62,7 +69,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
+ {$nsfilter}
+ AND page_is_redirect = 0
{$uwhere}";
}
@@ -130,11 +138,13 @@ class NewPagesPage extends QueryPage {
* @return string
*/
function getPageHeader() {
+ global $wgScript;
$self = SpecialPage::getTitleFor( $this->getName() );
- $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) );
+ $form = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
+ $form .= Xml::hidden( 'title', $self->getPrefixedDBkey() );
# Namespace selector
$form .= '<table><tr><td align="right">' . Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '</td>';
- $form .= '<td>' . Xml::namespaceSelector( $this->namespace ) . '</td></tr>';
+ $form .= '<td>' . Xml::namespaceSelector( $this->namespace, 'all' ) . '</td></tr>';
# Username filter
$form .= '<tr><td align="right">' . Xml::label( wfMsg( 'newpages-username' ), 'mw-np-username' ) . '</td>';
$form .= '<td>' . Xml::input( 'username', 30, $this->username, array( 'id' => 'mw-np-username' ) ) . '</td></tr>';
@@ -186,7 +196,7 @@ function wfSpecialNewpages($par, $specialPage) {
}
}
} else {
- if( $ns = $wgRequest->getInt( 'namespace', 0 ) )
+ if( $ns = $wgRequest->getText( 'namespace', NS_MAIN ) )
$namespace = $ns;
if( $un = $wgRequest->getText( 'username' ) )
$username = $un;
@@ -199,6 +209,4 @@ function wfSpecialNewpages($par, $specialPage) {
if ( ! $npp->doFeed( $wgRequest->getVal( 'feed' ), $limit ) )
$npp->doQuery( $offset, $limit, $shownavigation );
-}
-
-?>
+} \ No newline at end of file
diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php
index cf882509..89fd15bb 100644
--- a/includes/SpecialPage.php
+++ b/includes/SpecialPage.php
@@ -94,12 +94,14 @@ class SpecialPage
'Uncategorizedpages' => array( 'SpecialPage', 'Uncategorizedpages' ),
'Uncategorizedcategories' => array( 'SpecialPage', 'Uncategorizedcategories' ),
'Uncategorizedimages' => array( 'SpecialPage', 'Uncategorizedimages' ),
+ 'Uncategorizedtemplates' => array( 'SpecialPage', 'Uncategorizedtemplates' ),
'Unusedcategories' => array( 'SpecialPage', 'Unusedcategories' ),
'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ),
'Wantedpages' => array( 'IncludableSpecialPage', 'Wantedpages' ),
'Wantedcategories' => array( 'SpecialPage', 'Wantedcategories' ),
'Mostlinked' => array( 'SpecialPage', 'Mostlinked' ),
'Mostlinkedcategories' => array( 'SpecialPage', 'Mostlinkedcategories' ),
+ 'Mostlinkedtemplates' => array( 'SpecialPage', 'Mostlinkedtemplates' ),
'Mostcategories' => array( 'SpecialPage', 'Mostcategories' ),
'Mostimages' => array( 'SpecialPage', 'Mostimages' ),
'Mostrevisions' => array( 'SpecialPage', 'Mostrevisions' ),
@@ -177,7 +179,7 @@ class SpecialPage
}
if( $wgEmailAuthentication ) {
- self::$mList['Confirmemail'] = array( 'UnlistedSpecialPage', 'Confirmemail' );
+ self::$mList['Confirmemail'] = 'EmailConfirmation';
}
# Add extension special pages
@@ -275,6 +277,30 @@ class SpecialPage
}
/**
+ * Check if a given name exist as a special page or as a special page alias
+ * @param $name string: name of a special page
+ * @return boolean: true if a special page exists with this name
+ */
+ static function exists( $name ) {
+ global $wgContLang;
+ if ( !self::$mListInitialised ) {
+ self::initList();
+ }
+ if( !self::$mAliases ) {
+ self::initAliasList();
+ }
+
+ # Remove special pages inline parameters:
+ $bits = explode( '/', $name );
+ $name = $wgContLang->caseFold($bits[0]);
+
+ return
+ array_key_exists( $name, self::$mList )
+ or array_key_exists( $name, self::$mAliases )
+ ;
+ }
+
+ /**
* Find the object with a given name and return it (or NULL)
* @static
* @param string $name
@@ -798,4 +824,4 @@ class SpecialMycontributions extends UnlistedSpecialPage {
}
}
-?>
+
diff --git a/includes/SpecialPopularpages.php b/includes/SpecialPopularpages.php
index cd2f60e7..af0ed269 100644
--- a/includes/SpecialPopularpages.php
+++ b/includes/SpecialPopularpages.php
@@ -66,4 +66,4 @@ function wfSpecialPopularpages() {
return $ppp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialPreferences.php b/includes/SpecialPreferences.php
index 5ca818cd..a36be289 100644
--- a/includes/SpecialPreferences.php
+++ b/includes/SpecialPreferences.php
@@ -97,6 +97,8 @@ class PreferencesForm {
if ( !preg_match( '/^[a-z\-]*$/', $this->mUserLanguage ) ) {
$this->mUserLanguage = 'nolanguage';
}
+
+ wfRunHooks( "InitPreferencesForm", array( $this, $request ) );
}
function execute() {
@@ -211,19 +213,23 @@ class PreferencesForm {
if ( '' != $this->mNewpass && $wgAuth->allowPasswordChange() ) {
if ( $this->mNewpass != $this->mRetypePass ) {
+ wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'badretype' ) );
$this->mainPrefsForm( 'error', wfMsg( 'badretype' ) );
return;
}
if (!$wgUser->checkPassword( $this->mOldpass )) {
+ wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'wrongpassword' ) );
$this->mainPrefsForm( 'error', wfMsg( 'wrongpassword' ) );
return;
}
try {
$wgUser->setPassword( $this->mNewpass );
+ wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'success' ) );
$this->mNewpass = $this->mOldpass = $this->mRetypePass = '';
} catch( PasswordError $e ) {
+ wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'error' ) );
$this->mainPrefsForm( 'error', $e->getMessage() );
return;
}
@@ -237,11 +243,18 @@ class PreferencesForm {
}
# Validate the signature and clean it up as needed
- if( $this->mToggles['fancysig'] ) {
+ global $wgMaxSigChars;
+ if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) {
+ global $wgLang;
+ $this->mainPrefsForm( 'error',
+ wfMsg( 'badsiglength', $wgLang->formatNum( $wgMaxSigChars ) ) );
+ return;
+ } elseif( $this->mToggles['fancysig'] ) {
if( Parser::validateSig( $this->mNick ) !== false ) {
$this->mNick = $wgParser->cleanSig( $this->mNick );
} else {
$this->mainPrefsForm( 'error', wfMsg( 'badsig' ) );
+ return;
}
} else {
// When no fancy sig used, make sure ~{3,5} get removed.
@@ -287,9 +300,17 @@ class PreferencesForm {
$wgUser->setOption( $tname, $tvalue );
}
if (!$wgAuth->updateExternalDB($wgUser)) {
- $this->mainPrefsForm( wfMsg( 'externaldberror' ) );
+ $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) );
return;
}
+
+ $msg = '';
+ if ( !wfRunHooks( "SavePreferences", array( $this, $wgUser, &$msg ) ) ) {
+ print "(($msg))";
+ $this->mainPrefsForm( 'error', $msg );
+ return;
+ }
+
$wgUser->setCookies();
$wgUser->saveSettings();
@@ -321,6 +342,9 @@ class PreferencesForm {
$wgUser->setCookies();
$wgUser->saveSettings();
}
+ if( $oldadr != $newadr ) {
+ wfRunHooks( "PrefsEmailAudit", array( $wgUser, $oldadr, $newadr ) );
+ }
}
if( $needRedirect && $error === false ) {
@@ -381,6 +405,8 @@ class PreferencesForm {
$this->mSearchNs[$i] = $wgUser->getOption( 'searchNs'.$i );
}
}
+
+ wfRunHooks( "ResetPreferences", array( $this, $wgUser ) );
}
/**
@@ -442,6 +468,38 @@ class PreferencesForm {
}
/**
+ * Helper function for user information panel
+ * @param $td1 label for an item
+ * @param $td2 item or null
+ * @param $td3 optional help or null
+ * @return xhtml block
+ */
+ function tableRow( $td1, $td2 = null, $td3 = null ) {
+ global $wgContLang;
+
+ $align['align'] = $wgContLang->isRtl() ? 'right' : 'left';
+
+ if ( is_null( $td3 ) ) {
+ $td3 = '';
+ } else {
+ $td3 = Xml::tags( 'tr', null,
+ Xml::tags( 'td', array( 'colspan' => '2' ), $td3 )
+ );
+ }
+
+ if ( is_null( $td2 ) ) {
+ $td1 = Xml::tags( 'td', $align + array( 'colspan' => '2' ), $td1 );
+ $td2 = '';
+ } else {
+ $td1 = Xml::tags( 'td', $align, $td1 );
+ $td2 = Xml::tags( 'td', $align, $td2 );
+ }
+
+ return Xml::tags( 'tr', null, $td1 . $td2 ). $td3 . "\n";
+
+ }
+
+ /**
* @access private
*/
function mainPrefsForm( $status , $message = '' ) {
@@ -457,6 +515,8 @@ class PreferencesForm {
$wgOut->setArticleRelated( false );
$wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc.
+
if ( $this->mSuccess || 'success' == $status ) {
$wgOut->addWikitext( '<div class="successbox"><strong>'. wfMsg( 'savedprefs' ) . '</strong></div>' );
} else if ( 'error' == $status ) {
@@ -484,12 +544,7 @@ class PreferencesForm {
$this->mUsedToggles[ 'ccmeonemails' ] = true;
$this->mUsedToggles[ 'uselivepreview' ] = true;
- # Enotif
- # <FIXME>
- $this->mUserEmail = htmlspecialchars( $this->mUserEmail );
- $this->mRealName = htmlspecialchars( $this->mRealName );
- $rawNick = $this->mNick;
- $this->mNick = htmlspecialchars( $this->mNick );
+
if ( !$this->mEmailFlag ) { $emfc = 'checked="checked"'; }
else { $emfc = ''; }
@@ -503,7 +558,7 @@ class PreferencesForm {
$skin = $wgUser->getSkin();
$emailauthenticated = wfMsg('emailnotauthenticated').'<br />' .
$skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Confirmemail' ),
- wfMsg( 'emailconfirmlink' ) );
+ wfMsg( 'emailconfirmlink' ) ) . '<br />';
}
} else {
$emailauthenticated = '';
@@ -511,7 +566,7 @@ class PreferencesForm {
}
if ($this->mUserEmail == '') {
- $emailauthenticated = wfMsg( 'noemailprefs' );
+ $emailauthenticated = wfMsg( 'noemailprefs' ) . '<br />';
}
$ps = $this->namespacesCheckboxes();
@@ -527,93 +582,85 @@ class PreferencesForm {
$wgOut->addHTML( "<div id='preferences'>" );
# User data
- #
- $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg('prefs-personal') . "</legend>\n<table>\n");
+ $wgOut->addHTML(
+ Xml::openElement( 'fieldset ' ) .
+ Xml::element( 'legend', null, wfMsg('prefs-personal') ) .
+ Xml::openElement( 'table' ) .
+ $this->tableRow( Xml::element( 'h2', null, wfMsg( 'prefs-personal' ) ) )
+ );
$userInformationHtml =
- $this->addRow(
- wfMsg( 'username'),
- $wgUser->getName()
- ) .
- $this->addRow(
- wfMsg( 'uid' ),
- $wgUser->getID()
+ $this->tableRow( wfMsgHtml( 'username' ), htmlspecialchars( $wgUser->getName() ) ) .
+ $this->tableRow( wfMsgHtml( 'uid' ), htmlspecialchars( $wgUser->getID() ) ) .
+ $this->tableRow(
+ wfMsgHtml( 'prefs-edits' ),
+ $wgLang->formatNum( User::edits( $wgUser->getId() ) )
);
-
+
if( wfRunHooks( 'PreferencesUserInformationPanel', array( $this, &$userInformationHtml ) ) ) {
$wgOut->addHtml( $userInformationHtml );
}
-
- if ($wgAllowRealName) {
+ if ( $wgAllowRealName ) {
$wgOut->addHTML(
- $this->addRow(
- '<label for="wpRealName">' . wfMsg('yourrealname') . '</label>',
- "<input type='text' name='wpRealName' id='wpRealName' value=\"{$this->mRealName}\" size='25' />"
+ $this->tableRow(
+ Xml::label( wfMsg('yourrealname'), 'wpRealName' ),
+ Xml::input( 'wpRealName', 25, $this->mRealName, array( 'id' => 'wpRealName' ) ),
+ Xml::tags('div', array( 'class' => 'prefsectiontip' ),
+ wfMsgExt( 'prefs-help-realname', 'parseinline' )
+ )
)
);
}
- if ($wgEnableEmail) {
+ if ( $wgEnableEmail ) {
$wgOut->addHTML(
- $this->addRow(
- '<label for="wpUserEmail">' . wfMsg( 'youremail' ) . '</label>',
- "<input type='text' name='wpUserEmail' id='wpUserEmail' value=\"{$this->mUserEmail}\" size='25' />"
+ $this->tableRow(
+ Xml::label( wfMsg('youremail'), 'wpUserEmail' ),
+ Xml::input( 'wpUserEmail', 25, $this->mUserEmail, array( 'id' => 'wpUserEmail' ) ),
+ Xml::tags('div', array( 'class' => 'prefsectiontip' ),
+ wfMsgExt( 'prefs-help-email', 'parseinline' )
+ )
)
);
}
- global $wgParser;
- if( !empty( $this->mToggles['fancysig'] ) &&
- false === $wgParser->validateSig( $rawNick ) ) {
- $invalidSig = $this->addRow(
+ global $wgParser, $wgMaxSigChars;
+ if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) {
+ $invalidSig = $this->tableRow(
+ '&nbsp;',
+ Xml::element( 'span', array( 'class' => 'error' ),
+ wfMsg( 'badsiglength', $wgLang->formatNum( $wgMaxSigChars ) ) )
+ );
+ } elseif( !empty( $this->mToggles['fancysig'] ) &&
+ false === $wgParser->validateSig( $this->mNick ) ) {
+ $invalidSig = $this->tableRow(
'&nbsp;',
- '<span class="error">' . wfMsgHtml( 'badsig' ) . '<span>'
+ Xml::element( 'span', array( 'class' => 'error' ), wfMsg( 'badsig' ) )
);
} else {
$invalidSig = '';
}
$wgOut->addHTML(
- $this->addRow(
- '<label for="wpNick">' . wfMsg( 'yournick' ) . '</label>',
- "<input type='text' name='wpNick' id='wpNick' value=\"{$this->mNick}\" size='25' />"
+ $this->tableRow(
+ Xml::label( wfMsg( 'yournick' ), 'wpNick' ),
+ Xml::input( 'wpNick', 25, $this->mNick,
+ array(
+ 'id' => 'wpNick',
+ // Note: $wgMaxSigChars is enforced in Unicode characters,
+ // both on the backend and now in the browser.
+ // Badly-behaved requests may still try to submit
+ // an overlong string, however.
+ 'maxlength' => $wgMaxSigChars ) )
) .
$invalidSig .
- # FIXME: The <input> part should be where the &nbsp; is, getToggle() needs
- # to be changed to out return its output in two parts. -ævar
- $this->addRow(
- '&nbsp;',
- $this->getToggle( 'fancysig' )
- )
+ $this->tableRow( '&nbsp;', $this->getToggle( 'fancysig' ) )
);
- /**
- * Make sure the site language is in the list; a custom language code
- * might not have a defined name...
- */
- $languages = Language::getLanguageNames( true );
- if( !array_key_exists( $wgContLanguageCode, $languages ) ) {
- $languages[$wgContLanguageCode] = $wgContLanguageCode;
- }
- ksort( $languages );
-
- /**
- * If a bogus value is set, default to the content language.
- * Otherwise, no default is selected and the user ends up
- * with an Afrikaans interface since it's first in the list.
- */
- $selectedLang = isset( $languages[$this->mUserLanguage] ) ? $this->mUserLanguage : $wgContLanguageCode;
- $options = "\n";
- foreach( $languages as $code => $name ) {
- $selected = ($code == $selectedLang);
- $options .= Xml::option( "$code - $name", $code, $selected ) . "\n";
- }
+ list( $lsLabel, $lsSelect) = Xml::languageSelector( $this->mUserLanguage );
$wgOut->addHTML(
- $this->addRow(
- '<label for="wpUserLanguage">' . wfMsg('yourlanguage') . '</label>',
- "<select name='wpUserLanguage' id='wpUserLanguage'>$options</select>"
- )
+ $this->tableRow( $lsLabel, $lsSelect )
);
/* see if there are multiple language variants to choose from*/
@@ -621,6 +668,7 @@ class PreferencesForm {
$variants = $wgContLang->getVariants();
$variantArray = array();
+ $languages = Language::getLanguageNames( true );
foreach($variants as $v) {
$v = str_replace( '_', '-', strtolower($v));
if( array_key_exists( $v, $languages ) ) {
@@ -637,69 +685,74 @@ class PreferencesForm {
if(count($variantArray) > 1) {
$wgOut->addHtml(
- $this->addRow( wfMsg( 'yourvariant' ),
- "<select name='wpUserVariant'>$options</select>" )
+ $this->tableRow(
+ Xml::label( wfMsg( 'yourvariant' ), 'wpUserVariant' ),
+ Xml::tags( 'select',
+ array( 'name' => 'wpUserVariant', 'id' => 'wpUserVariant' ),
+ $options
+ )
+ )
);
}
}
- $wgOut->addHTML('</table>');
# Password
- if( $wgAuth->allowPasswordChange() ) {
- $this->mOldpass = htmlspecialchars( $this->mOldpass );
- $this->mNewpass = htmlspecialchars( $this->mNewpass );
- $this->mRetypePass = htmlspecialchars( $this->mRetypePass );
-
- $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'changepassword' ) . '</legend><table>');
+ if( $wgAuth->allowPasswordChange() ) {
$wgOut->addHTML(
- $this->addRow(
- '<label for="wpOldpass">' . wfMsg( 'oldpassword' ) . '</label>',
- "<input type='password' name='wpOldpass' id='wpOldpass' value=\"{$this->mOldpass}\" size='20' />"
+ $this->tableRow( Xml::element( 'h2', null, wfMsg( 'changepassword' ) ) ) .
+ $this->tableRow(
+ Xml::label( wfMsg( 'oldpassword' ), 'wpOldpass' ),
+ Xml::password( 'wpOldpass', 25, $this->mOldpass, array( 'id' => 'wpOldpass' ) )
) .
- $this->addRow(
- '<label for="wpNewpass">' . wfMsg( 'newpassword' ) . '</label>',
- "<input type='password' name='wpNewpass' id='wpNewpass' value=\"{$this->mNewpass}\" size='20' />"
+ $this->tableRow(
+ Xml::label( wfMsg( 'newpassword' ), 'wpNewpass' ),
+ Xml::password( 'wpNewpass', 25, $this->mNewpass, array( 'id' => 'wpNewpass' ) )
) .
- $this->addRow(
- '<label for="wpRetypePass">' . wfMsg( 'retypenew' ) . '</label>',
- "<input type='password' name='wpRetypePass' id='wpRetypePass' value=\"{$this->mRetypePass}\" size='20' />"
+ $this->tableRow(
+ Xml::label( wfMsg( 'retypenew' ), 'wpRetypePass' ),
+ Xml::password( 'wpRetypePass', 25, $this->mRetypePass, array( 'id' => 'wpRetypePass' ) )
) .
- "</table>\n" .
- $this->getToggle( "rememberpassword" ) . "</fieldset>\n\n" );
+ Xml::tags( 'tr', null,
+ Xml::tags( 'td', array( 'colspan' => '2' ),
+ $this->getToggle( "rememberpassword" )
+ )
+ )
+ );
}
# <FIXME>
# Enotif
- if ($wgEnableEmail) {
- $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'email' ) . '</legend>' );
+ if ( $wgEnableEmail ) {
+
+ $moreEmail = '';
+ if ($wgEnableUserEmail) {
+ $emf = wfMsg( 'allowemail' );
+ $disabled = $disableEmailPrefs ? ' disabled="disabled"' : '';
+ $moreEmail =
+ "<input type='checkbox' $emfc $disabled value='1' name='wpEmailFlag' id='wpEmailFlag' /> <label for='wpEmailFlag'>$emf</label>";
+ }
+
+
$wgOut->addHTML(
+ $this->tableRow( Xml::element( 'h2', null, wfMsg( 'email' ) ) ) .
+ $this->tableRow(
$emailauthenticated.
$enotifrevealaddr.
$enotifwatchlistpages.
$enotifusertalkpages.
- $enotifminoredits );
- if ($wgEnableUserEmail) {
- $emf = wfMsg( 'allowemail' );
- $disabled = $disableEmailPrefs ? ' disabled="disabled"' : '';
- $wgOut->addHTML(
- "<div><input type='checkbox' $emfc $disabled value='1' name='wpEmailFlag' id='wpEmailFlag' /> <label for='wpEmailFlag'>$emf</label></div>" );
- }
- $wgOut->addHtml( $this->getToggle( 'ccmeonemails' ) );
-
- $wgOut->addHTML( '</fieldset>' );
+ $enotifminoredits.
+ $moreEmail.
+ $this->getToggle( 'ccmeonemails' )
+ )
+ );
}
# </FIXME>
- # Show little "help" tips for the real name and email address options
- if( $wgAllowRealName || $wgEnableEmail ) {
- if( $wgAllowRealName )
- $tips[] = wfMsg( 'prefs-help-realname' );
- if( $wgEnableEmail )
- $tips[] = wfMsg( 'prefs-help-email' );
- $wgOut->addHtml( '<div class="prefsectiontip">' . implode( '<br />', $tips ) . '</div>' );
- }
+ $wgOut->addHTML(
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' )
+ );
- $wgOut->addHTML( '</fieldset>' );
# Quickbar
#
@@ -753,8 +806,12 @@ class PreferencesForm {
if( $wgUseTeX ) {
$wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg('math') . '</legend>' );
foreach ( $mathopts as $k => $v ) {
- $checked = $k == $this->mMath ? ' checked="checked"' : '';
- $wgOut->addHTML( "<div><label><input type='radio' name='wpMath' value=\"$k\"$checked /> ".wfMsg($v)."</label></div>\n" );
+ $checked = ($k == $this->mMath);
+ $wgOut->addHTML(
+ Xml::openElement( 'div' ) .
+ Xml::radioLabel( wfMsg( $v ), 'wpMath', $k, "mw-sp-math-$k", $checked ) .
+ Xml::closeElement( 'div' ) . "\n"
+ );
}
$wgOut->addHTML( "</fieldset>\n\n" );
}
@@ -928,8 +985,8 @@ class PreferencesForm {
# Misc
#
$wgOut->addHTML('<fieldset><legend>' . wfMsg('prefs-misc') . '</legend>');
- $wgOut->addHTML( wfInputLabel( wfMsg( 'stubthreshold' ),
- 'wpStubs', 'wpStubs', 6, $this->mStubs ) );
+ $wgOut->addHtml( '<label for="wpStubs">' . wfMsg( 'stub-threshold' ) . '</label>&nbsp;' );
+ $wgOut->addHtml( Xml::input( 'wpStubs', 6, $this->mStubs, array( 'id' => 'wpStubs' ) ) );
$msgUnderline = htmlspecialchars( wfMsg ( 'tog-underline' ) );
$msgUnderlinenever = htmlspecialchars( wfMsg ( 'underline-never' ) );
$msgUnderlinealways = htmlspecialchars( wfMsg ( 'underline-always' ) );
@@ -953,7 +1010,9 @@ class PreferencesForm {
}
$wgOut->addHTML( '</fieldset>' );
- $token = $wgUser->editToken();
+ wfRunHooks( "RenderPreferencesForm", array( $this, $wgOut ) );
+
+ $token = htmlspecialchars( $wgUser->editToken() );
$skin = $wgUser->getSkin();
$wgOut->addHTML( "
<div id='prefsubmit'>
@@ -964,11 +1023,13 @@ class PreferencesForm {
</div>
- <input type='hidden' name='wpEditToken' value='{$token}' />
+ <input type='hidden' name='wpEditToken' value=\"{$token}\" />
</div></form>\n" );
- $wgOut->addWikiText( '<div class="prefcache">' . wfMsg('clearyourcache') . '</div>' );
+ $wgOut->addHtml( Xml::tags( 'div', array( 'class' => "prefcache" ),
+ wfMsgExt( 'clearyourcache', 'parseinline' ) )
+ );
}
}
-?>
+
diff --git a/includes/SpecialPrefixindex.php b/includes/SpecialPrefixindex.php
index b7c51d49..6bb26d67 100644
--- a/includes/SpecialPrefixindex.php
+++ b/includes/SpecialPrefixindex.php
@@ -15,20 +15,14 @@ function wfSpecialPrefixIndex( $par=NULL, $specialPage ) {
$from = $wgRequest->getVal( 'from' );
$prefix = $wgRequest->getVal( 'prefix' );
$namespace = $wgRequest->getInt( 'namespace' );
-
$namespaces = $wgContLang->getNamespaces();
$indexPage = new SpecialPrefixIndex();
- if( !in_array($namespace, array_keys($namespaces)) )
- $namespace = 0;
-
- $wgOut->setPagetitle( $namespace > 0 ?
- wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) :
- wfMsg( 'allarticles' )
- );
-
-
+ $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces ) ) )
+ ? wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) )
+ : wfMsg( 'allarticles' )
+ );
if ( isset($par) ) {
$indexPage->showChunk( $namespace, $par, $specialPage->including(), $from );
@@ -67,9 +61,15 @@ function showChunk( $namespace = NS_MAIN, $prefix, $including = false, $from = n
$fromList = $this->getNamespaceKeyAndText($namespace, $from);
$prefixList = $this->getNamespaceKeyAndText($namespace, $prefix);
+ $namespaces = $wgContLang->getNamespaces();
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
if ( !$prefixList || !$fromList ) {
$out = wfMsgWikiHtml( 'allpagesbadtitle' );
+ } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) {
+ // Show errormessage and reset to NS_MAIN
+ $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace );
+ $namespace = NS_MAIN;
} else {
list( $namespace, $prefixKey, $prefix ) = $prefixList;
list( /* $fromNs */, $fromKey, $from ) = $fromList;
@@ -127,8 +127,8 @@ function showChunk( $namespace = NS_MAIN, $prefix, $including = false, $from = n
} else {
$nsForm = $this->namespaceForm ( $namespace, $prefix );
$out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
- $out2 .= '<tr valign="top"><td align="left">' . $nsForm;
- $out2 .= '</td><td align="right" style="font-size: smaller; margin-bottom: 1em;">' .
+ $out2 .= '<tr valign="top"><td>' . $nsForm;
+ $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' .
$sk->makeKnownLink( $wgContLang->specialPage( $this->name ),
wfMsg ( 'allpages' ) );
if ( isset($dbr) && $dbr && ($n == $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
@@ -146,4 +146,4 @@ function showChunk( $namespace = NS_MAIN, $prefix, $including = false, $from = n
}
}
-?>
+
diff --git a/includes/SpecialProtectedpages.php b/includes/SpecialProtectedpages.php
index 91b138ff..122ca8fc 100644
--- a/includes/SpecialProtectedpages.php
+++ b/includes/SpecialProtectedpages.php
@@ -9,6 +9,10 @@
* @addtogroup SpecialPage
*/
class ProtectedPagesForm {
+
+ protected $IdLevel = 'level';
+ protected $IdType = 'type';
+
function showList( $msg = '' ) {
global $wgOut, $wgRequest;
@@ -22,14 +26,15 @@ class ProtectedPagesForm {
Title::purgeExpiredRestrictions();
}
- $type = $wgRequest->getVal( 'type' );
- $level = $wgRequest->getVal( 'level' );
- $minsize = $wgRequest->getIntOrNull( 'minsize' );
+ $type = $wgRequest->getVal( $this->IdType );
+ $level = $wgRequest->getVal( $this->IdLevel );
+ $sizetype = $wgRequest->getVal( 'sizetype' );
+ $size = $wgRequest->getIntOrNull( 'size' );
$NS = $wgRequest->getIntOrNull( 'namespace' );
- $pager = new ProtectedPagesPager( $this, array(), $type, $level, $NS, $minsize );
+ $pager = new ProtectedPagesPager( $this, array(), $type, $level, $NS, $sizetype, $size );
- $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $minsize ) );
+ $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size ) );
if ( $pager->getNumRows() ) {
$s = $pager->getNavigationBar();
@@ -38,7 +43,7 @@ class ProtectedPagesForm {
"</ul>";
$s .= $pager->getNavigationBar();
} else {
- $s = '<p>' . wfMsgHTML( 'protectedpagesempty' ) . '</p>';
+ $s = '<p>' . wfMsgHtml( 'protectedpagesempty' ) . '</p>';
}
$wgOut->addHTML( $s );
}
@@ -65,6 +70,10 @@ class ProtectedPagesForm {
$description_items[] = $protType;
+ if ( $row->pr_cascade ) {
+ $description_items[] = wfMsg( 'protect-summary-cascade' );
+ }
+
$expiry_description = ''; $stxt = '';
if ( $row->pr_expiry != 'infinity' && strlen($row->pr_expiry) ) {
@@ -93,7 +102,7 @@ class ProtectedPagesForm {
* @param $minsize int
* @private
*/
- function showOptions( $namespace, $type='edit', $level, $minsize ) {
+ function showOptions( $namespace, $type='edit', $level, $sizetype, $size ) {
global $wgScript;
$action = htmlspecialchars( $wgScript );
$title = SpecialPage::getTitleFor( 'ProtectedPages' );
@@ -101,26 +110,40 @@ class ProtectedPagesForm {
return "<form action=\"$action\" method=\"get\">\n" .
'<fieldset>' .
Xml::element( 'legend', array(), wfMsg( 'protectedpages' ) ) .
- Xml::hidden( 'title', $special ) . "\n" .
- $this->getNamespaceMenu( $namespace ) . "\n" .
- $this->getTypeMenu( $type ) . "\n" .
+ Xml::hidden( 'title', $special ) . "&nbsp;\n" .
+ $this->getNamespaceMenu( $namespace ) . "&nbsp;\n" .
+ $this->getTypeMenu( $type ) . "&nbsp;\n" .
$this->getLevelMenu( $level ) . "<br/>\n" .
- $this->getSizeLimit( $minsize ) . "\n" .
- Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
+ $this->getSizeLimit( $sizetype, $size ) . "\n" .
+ "&nbsp;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
"</fieldset></form>";
}
- function getNamespaceMenu( $namespace=NULL ) {
- return "<label for='namespace'>" . wfMsgHtml('namespace') . "</label>" . HTMLnamespaceselector($namespace, '');
+ /**
+ * Prepare the namespace filter drop-down; standard namespace
+ * selector, sans the MediaWiki namespace
+ *
+ * @param mixed $namespace Pre-select namespace
+ * @return string
+ */
+ function getNamespaceMenu( $namespace = null ) {
+ return Xml::label( wfMsg( 'namespace' ), 'namespace' )
+ . '&nbsp;'
+ . Xml::namespaceSelector( $namespace, '' );
}
-
+
/**
* @return string Formatted HTML
* @private
*/
- function getSizeLimit( $minsize=0 ) {
- $out = Xml::input('minsize', 9, $minsize, array( 'id' => 'minsize' ) );
- return "<label for='minsize'>" . wfMsgHtml('minimum-size') . "</label>: " . $out;
+ function getSizeLimit( $sizetype, $size ) {
+ $out = Xml::radio( 'sizetype', 'min', ($sizetype=='min'), array('id' => 'wpmin') );
+ $out .= Xml::label( wfMsg("minimum-size"), 'wpmin' );
+ $out .= "&nbsp;".Xml::radio( 'sizetype', 'max', ($sizetype=='max'), array('id' => 'wpmax') );
+ $out .= Xml::label( wfMsg("maximum-size"), 'wpmax' );
+ $out .= "&nbsp;".Xml::input('size', 9, $size, array( 'id' => 'wpsize' ) );
+ $out .= ' '.wfMsgHtml('pagesize');
+ return $out;
}
/**
@@ -128,28 +151,28 @@ class ProtectedPagesForm {
* @private
*/
function getTypeMenu( $pr_type ) {
- global $wgRestrictionTypes, $wgUser;
+ global $wgRestrictionTypes;
- $out = "<select name='type'>\n";
$m = array(); // Temporary array
+ $options = array();
// First pass to load the log names
foreach( $wgRestrictionTypes as $type ) {
- $text = wfMsgHtml("restriction-$type");
+ $text = wfMsg("restriction-$type");
$m[$text] = $type;
}
- // Second pass to sort by name
- ksort($m);
-
// Third pass generates sorted XHTML content
foreach( $m as $text => $type ) {
$selected = ($type == $pr_type );
- $out .= Xml::option( $text, $type, $selected ) . "\n";
+ $options[] = Xml::option( $text, $type, $selected ) . "\n";
}
- $out .= '</select>';
- return "<label for='type'>" . wfMsgHtml('restriction-type') . "</label>: " . $out;
+ return
+ Xml::label( wfMsg('restriction-type') , $this->IdType ) . '&nbsp;' .
+ Xml::tags( 'select',
+ array( 'id' => $this->IdType, 'name' => $this->IdType ),
+ implode( "\n", $options ) );
}
/**
@@ -157,30 +180,30 @@ class ProtectedPagesForm {
* @private
*/
function getLevelMenu( $pr_level ) {
- global $wgRestrictionLevels, $wgUser;
-
- $out = "<select name='level'>\n";
- $m = array( wfMsgHtml('restriction-level-all') => 0 ); // Temporary array
+ global $wgRestrictionLevels;
+
+ $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array
+ $options = array();
// First pass to load the log names
foreach( $wgRestrictionLevels as $type ) {
if ( $type !='' && $type !='*') {
- $text = wfMsgHtml("restriction-level-$type");
+ $text = wfMsg("restriction-level-$type");
$m[$text] = $type;
}
}
- // Second pass to sort by name
- ksort($m);
-
// Third pass generates sorted XHTML content
foreach( $m as $text => $type ) {
$selected = ($type == $pr_level );
- $out .= Xml::option( $text, $type, $selected ) . "\n";
+ $options[] = Xml::option( $text, $type, $selected );
}
- $out .= '</select>';
- return "<label for='level'>" . wfMsgHtml('restriction-level') . "</label>: " . $out;
+ return
+ Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . '&nbsp;' .
+ Xml::tags( 'select',
+ array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
+ implode( "\n", $options ) );
}
}
@@ -188,16 +211,17 @@ class ProtectedPagesForm {
* @todo document
* @addtogroup Pager
*/
-class ProtectedPagesPager extends ReverseChronologicalPager {
+class ProtectedPagesPager extends AlphabeticPager {
public $mForm, $mConds;
- function __construct( $form, $conds = array(), $type, $level, $namespace, $minsize ) {
+ function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0 ) {
$this->mForm = $form;
$this->mConds = $conds;
$this->type = ( $type ) ? $type : 'edit';
$this->level = $level;
$this->namespace = $namespace;
- $this->minsize = intval($minsize);
+ $this->sizetype = $sizetype;
+ $this->size = intval($size);
parent::__construct();
}
@@ -208,8 +232,7 @@ class ProtectedPagesPager extends ReverseChronologicalPager {
$lb = new LinkBatch;
while ( $row = $this->mResult->fetchObject() ) {
- $name = str_replace( ' ', '_', $row->page_title );
- $lb->add( $row->page_namespace, $name );
+ $lb->add( $row->page_namespace, $row->page_title );
}
$lb->execute();
@@ -218,7 +241,6 @@ class ProtectedPagesPager extends ReverseChronologicalPager {
}
function formatRow( $row ) {
- $block = new Block;
return $this->mForm->formatRow( $row );
}
@@ -226,17 +248,22 @@ class ProtectedPagesPager extends ReverseChronologicalPager {
$conds = $this->mConds;
$conds[] = 'pr_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
$conds[] = 'page_id=pr_page';
- $conds[] = 'page_len>=' . $this->minsize;
$conds[] = 'pr_type=' . $this->mDb->addQuotes( $this->type );
- if ( $this->level )
+
+ if( $this->sizetype=='min' ) {
+ $conds[] = 'page_len>=' . $this->size;
+ } else if( $this->sizetype=='max' ) {
+ $conds[] = 'page_len<=' . $this->size;
+ }
+
+ if( $this->level )
$conds[] = 'pr_level=' . $this->mDb->addQuotes( $this->level );
- if ( !is_null($this->namespace) )
+ if( !is_null($this->namespace) )
$conds[] = 'page_namespace=' . $this->mDb->addQuotes( $this->namespace );
return array(
'tables' => array( 'page_restrictions', 'page' ),
- 'fields' => 'max(pr_id) AS pr_id,page_namespace,page_title,page_len,pr_type,pr_level,pr_expiry',
- 'conds' => $conds,
- 'options' => array( 'GROUP BY' => 'page_namespace,page_title,pr_level,pr_expiry,page_len,pr_type' ),
+ 'fields' => 'pr_id,page_namespace,page_title,page_len,pr_type,pr_level,pr_expiry,pr_cascade',
+ 'conds' => $conds
);
}
@@ -250,11 +277,10 @@ class ProtectedPagesPager extends ReverseChronologicalPager {
*/
function wfSpecialProtectedpages() {
- list( $limit, $offset ) = wfCheckLimits();
-
$ppForm = new ProtectedPagesForm();
$ppForm->showList();
}
-?>
+
+
diff --git a/includes/SpecialRandompage.php b/includes/SpecialRandompage.php
index e6c4abe8..42734274 100644
--- a/includes/SpecialRandompage.php
+++ b/includes/SpecialRandompage.php
@@ -105,4 +105,4 @@ class RandomPage {
}
}
-?>
+
diff --git a/includes/SpecialRandomredirect.php b/includes/SpecialRandomredirect.php
index 75a6b81d..b7aa3e49 100644
--- a/includes/SpecialRandomredirect.php
+++ b/includes/SpecialRandomredirect.php
@@ -30,4 +30,4 @@ function wfSpecialRandomredirect( $par = null ) {
$wgOut->redirect( $title->getFullUrl( 'redirect=no' ) );
}
-?>
+
diff --git a/includes/SpecialRecentchanges.php b/includes/SpecialRecentchanges.php
index 84444e62..7565481b 100644
--- a/includes/SpecialRecentchanges.php
+++ b/includes/SpecialRecentchanges.php
@@ -269,8 +269,6 @@ function wfSpecialRecentchanges( $par, $specialPage ) {
}
function rcFilterByCategories ( &$rows , $categories , $any ) {
- require_once ( 'Categoryfinder.php' ) ;
-
# Filter categories
$cats = array () ;
foreach ( $categories AS $cat ) {
@@ -685,12 +683,12 @@ function rcFormatDiffRow( $title, $oldid, $newid, $timestamp, $comment ) {
*/
function rcApplyDiffStyle( $text ) {
$styles = array(
- 'diff' => 'background-color: white;',
- 'diff-otitle' => 'background-color: white;',
- 'diff-ntitle' => 'background-color: white;',
- 'diff-addedline' => 'background: #cfc; font-size: smaller;',
- 'diff-deletedline' => 'background: #ffa; font-size: smaller;',
- 'diff-context' => 'background: #eee; font-size: smaller;',
+ 'diff' => 'background-color: white; color:black;',
+ 'diff-otitle' => 'background-color: white; color:black;',
+ 'diff-ntitle' => 'background-color: white; color:black;',
+ 'diff-addedline' => 'background: #cfc; color:black; font-size: smaller;',
+ 'diff-deletedline' => 'background: #ffa; color:black; font-size: smaller;',
+ 'diff-context' => 'background: #eee; color:black; font-size: smaller;',
'diffchange' => 'color: red; font-weight: bold; text-decoration: none;',
);
@@ -702,4 +700,4 @@ function rcApplyDiffStyle( $text ) {
return $text;
}
-?>
+
diff --git a/includes/SpecialRecentchangeslinked.php b/includes/SpecialRecentchangeslinked.php
index 14508d3a..2a8ac32d 100644
--- a/includes/SpecialRecentchangeslinked.php
+++ b/includes/SpecialRecentchangeslinked.php
@@ -35,6 +35,7 @@ function wfSpecialRecentchangeslinked( $par = NULL ) {
}
$id = $nt->getArticleId();
+ $wgOut->setPageTitle( wfMsg( 'recentchangeslinked-title', $nt->getPrefixedText() ) );
$wgOut->setSubtitle( htmlspecialchars( wfMsg( 'rclsub', $nt->getPrefixedText() ) ) );
if ( ! $days ) {
@@ -171,4 +172,4 @@ $GROUPBY
$wgOut->addHTML( $s );
}
-?>
+
diff --git a/includes/SpecialResetpass.php b/includes/SpecialResetpass.php
index dc1e53c4..281a78b6 100644
--- a/includes/SpecialResetpass.php
+++ b/includes/SpecialResetpass.php
@@ -74,6 +74,8 @@ class PasswordResetForm extends SpecialPage {
function showForm() {
global $wgOut, $wgUser, $wgRequest;
+
+ $wgOut->disallowUserJs();
$self = SpecialPage::getTitleFor( 'Resetpass' );
$form =
@@ -160,4 +162,4 @@ class PasswordResetForm extends SpecialPage {
}
}
-?>
+
diff --git a/includes/SpecialRevisiondelete.php b/includes/SpecialRevisiondelete.php
index 5c70d5ae..34e9dfbc 100644
--- a/includes/SpecialRevisiondelete.php
+++ b/includes/SpecialRevisiondelete.php
@@ -272,4 +272,4 @@ class RevisionDeleter {
}
}
-?>
+
diff --git a/includes/SpecialSearch.php b/includes/SpecialSearch.php
index fdaa8541..3fc8bab4 100644
--- a/includes/SpecialSearch.php
+++ b/includes/SpecialSearch.php
@@ -173,7 +173,8 @@ class SpecialSearch {
SpecialPage::getTitleFor( 'Search' ),
wfArrayToCGI(
$this->powerSearchOptions(),
- array( 'search' => $term ) ) );
+ array( 'search' => $term ) ),
+ ($num < $this->limit) );
$wgOut->addHTML( "<br />{$prevnext}\n" );
}
@@ -184,6 +185,7 @@ class SpecialSearch {
} else {
$wgOut->addWikiText( '==' . wfMsg( 'notitlematches' ) . "==\n" );
}
+ $titleMatches->free();
}
if( $textMatches ) {
@@ -194,6 +196,7 @@ class SpecialSearch {
# Don't show the 'no text matches' if we received title matches
$wgOut->addWikiText( '==' . wfMsg( 'notextmatches' ) . "==\n" );
}
+ $textMatches->free();
}
if ( $num == 0 ) {
@@ -320,6 +323,14 @@ class SpecialSearch {
$contextchars = $wgUser->getOption( 'contextchars', 50 );
$link = $sk->makeKnownLinkObj( $t );
+
+ //If page content is not readable, just return the title.
+ //This is not quite safe, but better than showing excerpts from non-readable pages
+ //Note that hiding the entry entirely would screw up paging.
+ if (!$t->userCanRead()) {
+ return "<li>{$link}</li>\n";
+ }
+
$revision = Revision::newFromTitle( $t );
$text = $revision->getText();
$size = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'),
@@ -403,4 +414,4 @@ class SpecialSearch {
}
}
-?>
+
diff --git a/includes/SpecialShortpages.php b/includes/SpecialShortpages.php
index 72b093e0..973656dd 100644
--- a/includes/SpecialShortpages.php
+++ b/includes/SpecialShortpages.php
@@ -89,4 +89,4 @@ function wfSpecialShortpages() {
return $spp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialSpecialpages.php b/includes/SpecialSpecialpages.php
index bb202358..a893966c 100644
--- a/includes/SpecialSpecialpages.php
+++ b/includes/SpecialSpecialpages.php
@@ -8,7 +8,9 @@
*
*/
function wfSpecialSpecialpages() {
- global $wgOut, $wgUser;
+ global $wgOut, $wgUser, $wgMessageCache;
+
+ $wgMessageCache->loadAllMessages();
$wgOut->setRobotpolicy( 'index,nofollow' );
$sk = $wgUser->getSkin();
@@ -56,4 +58,4 @@ function wfSpecialSpecialpages_gen($pages,$heading,$sk) {
$wgOut->addHTML( "</ul>\n" );
}
-?>
+
diff --git a/includes/SpecialStatistics.php b/includes/SpecialStatistics.php
index 1c9e0ab6..a29811da 100644
--- a/includes/SpecialStatistics.php
+++ b/includes/SpecialStatistics.php
@@ -1,18 +1,19 @@
<?php
+
/**
-*
-* @addtogroup SpecialPage
-*/
+ * Special page lists various statistics, including the contents of
+ * `site_stats`, plus page view details if enabled
+ *
+ * @addtogroup SpecialPage
+ */
/**
-* constructor
-*/
-function wfSpecialStatistics() {
+ * Show the special page
+ *
+ * @param mixed $par (not used)
+ */
+function wfSpecialStatistics( $par = '' ) {
global $wgOut, $wgLang, $wgRequest;
- $fname = 'wfSpecialStatistics';
-
- $action = $wgRequest->getVal( 'action' );
-
$dbr = wfGetDB( DB_SLAVE );
$views = SiteStats::views();
@@ -21,18 +22,18 @@ function wfSpecialStatistics() {
$images = SiteStats::images();
$total = SiteStats::pages();
$users = SiteStats::users();
+ $admins = SiteStats::admins();
+ $numJobs = SiteStats::jobs();
- $admins = $dbr->selectField( 'user_groups', 'COUNT(*)', array( 'ug_group' => 'sysop' ), $fname );
- $numJobs = $dbr->estimateRowCount('job');
-
- if ($action == 'raw') {
+ if( $wgRequest->getVal( 'action' ) == 'raw' ) {
$wgOut->disable();
header( 'Pragma: nocache' );
echo "total=$total;good=$good;views=$views;edits=$edits;users=$users;admins=$admins;images=$images;jobs=$numJobs\n";
return;
} else {
- $text = '==' . wfMsg( 'sitestats' ) . "==\n" ;
- $text .= wfMsgExt( 'sitestatstext', array ( 'parsemag' ),
+ $text = "__NOTOC__\n";
+ $text .= '==' . wfMsg( 'sitestats' ) . "==\n";
+ $text .= wfMsgExt( 'sitestatstext', array( 'parsemag' ),
$wgLang->formatNum( $total ),
$wgLang->formatNum( $good ),
$wgLang->formatNum( $views ),
@@ -41,44 +42,52 @@ function wfSpecialStatistics() {
$wgLang->formatNum( sprintf( '%.2f', $edits ? $views / $edits : 0 ) ),
$wgLang->formatNum( $numJobs ),
$wgLang->formatNum( $images )
- );
-
- $text .= "\n==" . wfMsg( 'userstats' ) . "==\n";
+ )."\n";
+ $text .= "==" . wfMsg( 'userstats' ) . "==\n";
$text .= wfMsgExt( 'userstatstext', array ( 'parsemag' ),
$wgLang->formatNum( $users ),
$wgLang->formatNum( $admins ),
'[[' . wfMsgForContent( 'grouppage-sysop' ) . ']]', # TODO somehow remove, kept for backwards compatibility
$wgLang->formatNum( sprintf( '%.2f', $admins / $users * 100 ) ),
User::makeGroupLinkWiki( 'sysop' )
- );
-
- $wgOut->addWikiText( $text );
+ )."\n";
global $wgDisableCounters, $wgMiserMode, $wgUser, $wgLang, $wgContLang;
if( !$wgDisableCounters && !$wgMiserMode ) {
- $page = $dbr->tableName( 'page' );
- $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>' );
+ $res = $dbr->select(
+ 'page',
+ array(
+ 'page_namespace',
+ 'page_title',
+ 'page_counter',
+ ),
+ array(
+ 'page_is_redirect' => 0,
+ 'page_counter > 0',
+ ),
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'page_counter DESC',
+ 'LIMIT' => 10,
+ )
+ );
+ if( $res->numRows() > 0 ) {
+ $text .= "==" . wfMsg( 'statistics-mostpopular' ) . "==\n";
+ while( $row = $res->fetchObject() ) {
+ $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
+ if( $title instanceof Title )
+ $text .= '* [[:' . $title->getPrefixedText() . ']] (' . $wgLang->formatNum( $row->page_counter ) . ")\n";
}
- $wgOut->addHtml( '</ol>' );
- $dbr->freeResult( $res );
+ $res->free();
}
}
$footer = wfMsg( 'statistics-footer' );
if( !wfEmptyMsg( 'statistics-footer', $footer ) && $footer != '' )
- $wgOut->addWikiText( $footer );
-
+ $text .= "\n" . $footer;
+
+ $wgOut->addWikiText( $text );
}
-}
-?>
+
+} \ No newline at end of file
diff --git a/includes/SpecialUncategorizedcategories.php b/includes/SpecialUncategorizedcategories.php
index e02c9bbd..67f87aa8 100644
--- a/includes/SpecialUncategorizedcategories.php
+++ b/includes/SpecialUncategorizedcategories.php
@@ -34,4 +34,4 @@ function wfSpecialUncategorizedcategories() {
return $lpp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialUncategorizedimages.php b/includes/SpecialUncategorizedimages.php
index 22e34669..23deefe8 100644
--- a/includes/SpecialUncategorizedimages.php
+++ b/includes/SpecialUncategorizedimages.php
@@ -44,4 +44,4 @@ function wfSpecialUncategorizedimages() {
return $uip->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialUncategorizedpages.php b/includes/SpecialUncategorizedpages.php
index 408ac726..b26f6d93 100644
--- a/includes/SpecialUncategorizedpages.php
+++ b/includes/SpecialUncategorizedpages.php
@@ -54,4 +54,4 @@ function wfSpecialUncategorizedpages() {
return $lpp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialUncategorizedtemplates.php b/includes/SpecialUncategorizedtemplates.php
new file mode 100644
index 00000000..fb785e00
--- /dev/null
+++ b/includes/SpecialUncategorizedtemplates.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * Special page lists all uncategorised pages in the
+ * template namespace
+ *
+ * @addtogroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+class UncategorizedTemplatesPage extends UncategorizedPagesPage {
+
+ var $requestedNamespace = NS_TEMPLATE;
+
+ public function getName() {
+ return 'Uncategorizedtemplates';
+ }
+
+}
+
+/**
+ * Main execution point
+ *
+ * @param mixed $par Parameter passed to the page
+ */
+function wfSpecialUncategorizedtemplates() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $utp = new UncategorizedTemplatesPage();
+ $utp->doQuery( $offset, $limit );
+}
+
+
diff --git a/includes/SpecialUndelete.php b/includes/SpecialUndelete.php
index 8e740f6d..5678a81e 100644
--- a/includes/SpecialUndelete.php
+++ b/includes/SpecialUndelete.php
@@ -23,6 +23,7 @@ function wfSpecialUndelete( $par ) {
*/
class PageArchive {
protected $title;
+ var $fileStatus;
function __construct( $title ) {
if( is_null( $title ) ) {
@@ -269,8 +270,9 @@ class PageArchive {
$restoreFiles = $restoreAll || !empty( $fileVersions );
if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) {
- $img = new Image( $this->title );
- $filesRestored = $img->restore( $fileVersions );
+ $img = wfLocalFile( $this->title );
+ $this->fileStatus = $img->restore( $fileVersions );
+ $filesRestored = $this->fileStatus->successCount;
} else {
$filesRestored = 0;
}
@@ -280,7 +282,7 @@ class PageArchive {
} else {
$textRestored = 0;
}
-
+
// Touch the log!
global $wgContLang;
$log = new LogPage( 'delete' );
@@ -303,8 +305,12 @@ class PageArchive {
if( trim( $comment ) != '' )
$reason .= ": {$comment}";
$log->addEntry( 'restore', $this->title, $reason );
-
- return true;
+
+ if ( $this->fileStatus && !$this->fileStatus->ok ) {
+ return false;
+ } else {
+ return true;
+ }
}
/**
@@ -319,18 +325,13 @@ class PageArchive {
* @return int number of revisions restored
*/
private function undeleteRevisions( $timestamps ) {
- global $wgDBtype;
-
$restoreAll = empty( $timestamps );
$dbw = wfGetDB( DB_MASTER );
- $page = $dbw->tableName( 'archive' );
# Does this page already exist? We'll have to update it...
$article = new Article( $this->title );
- $options = ( $wgDBtype == 'postgres' )
- ? '' // pg doesn't support this?
- : 'FOR UPDATE';
+ $options = 'FOR UPDATE';
$page = $dbw->selectRow( 'page',
array( 'page_id', 'page_latest' ),
array( 'page_namespace' => $this->title->getNamespace(),
@@ -422,11 +423,10 @@ class PageArchive {
}
if( $revision ) {
- # FIXME: Update latest if newer as well...
- if( $newid ) {
- // Attach the latest revision to the page...
- $article->updateRevisionOn( $dbw, $revision, $previousRevId );
-
+ // Attach the latest revision to the page...
+ $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId );
+
+ if( $newid || $wasnew ) {
// Update site stats, link tables, etc
$article->createUpdates( $revision );
}
@@ -453,6 +453,7 @@ class PageArchive {
return $restored;
}
+ function getFileStatus() { return $this->fileStatus; }
}
/**
@@ -602,17 +603,24 @@ class UndeleteForm {
$archive = new PageArchive( $this->mTargetObj );
$rev = $archive->getRevision( $timestamp );
- $wgOut->setPageTitle( wfMsg( 'undeletepage' ) );
- $link = $skin->makeKnownLinkObj( $self, htmlspecialchars( $this->mTargetObj->getPrefixedText() ),
- 'target=' . $this->mTargetObj->getPrefixedUrl() );
- $wgOut->addHtml( '<p>' . wfMsgHtml( 'undelete-revision', $link,
- htmlspecialchars( $wgLang->timeAndDate( $timestamp ) ) ) . '</p>' );
-
if( !$rev ) {
- $wgOut->addWikiText( wfMsg( 'undeleterevision-missing' ) );
+ $wgOut->addWikiTexT( wfMsg( 'undeleterevision-missing' ) );
return;
}
+ $wgOut->setPageTitle( wfMsg( 'undeletepage' ) );
+
+ $link = $skin->makeKnownLinkObj(
+ $self,
+ htmlspecialchars( $this->mTargetObj->getPrefixedText() ),
+ 'target=' . $this->mTargetObj->getPrefixedUrl()
+ );
+ $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp ) );
+ $user = $skin->userLink( $rev->getUser(), $rev->getUserText() )
+ . $skin->userToolLinks( $rev->getUser(), $rev->getUserText() );
+
+ $wgOut->addHtml( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user ) . '</p>' );
+
wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
if( $this->mPreview ) {
@@ -622,7 +630,7 @@ class UndeleteForm {
$wgOut->addHtml(
wfElement( 'textarea', array(
- 'readonly' => true,
+ 'readonly' => 'readonly',
'cols' => intval( $wgUser->getOption( 'cols' ) ),
'rows' => intval( $wgUser->getOption( 'rows' ) ) ),
$rev->getText() . "\n" ) .
@@ -673,7 +681,7 @@ class UndeleteForm {
}
/* private */ function showHistory() {
- global $wgLang, $wgUser, $wgOut;
+ global $wgLang, $wgContLang, $wgUser, $wgOut;
$sk = $wgUser->getSkin();
if ( $this->mAllowed ) {
@@ -699,10 +707,10 @@ class UndeleteForm {
# List all stored revisions
$revisions = $archive->listRevisions();
$files = $archive->listFiles();
-
+
$haveRevisions = $revisions && $revisions->numRows() > 0;
$haveFiles = $files && $files->numRows() > 0;
-
+
# Batch existence check on user and talk pages
if( $haveRevisions ) {
$batch = new LinkBatch();
@@ -727,7 +735,7 @@ class UndeleteForm {
$titleObj = SpecialPage::getTitleFor( "Undelete" );
$action = $titleObj->getLocalURL( "action=submit" );
# Start the form here
- $top = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) );
+ $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) );
$wgOut->addHtml( $top );
}
@@ -736,24 +744,48 @@ class UndeleteForm {
$logViewer = new LogViewer(
new LogReader(
new FauxRequest(
- array( 'page' => $this->mTargetObj->getPrefixedText(),
- 'type' => 'delete' ) ) ) );
+ array(
+ 'page' => $this->mTargetObj->getPrefixedText(),
+ 'type' => 'delete'
+ )
+ )
+ ), LogViewer::NO_ACTION_LINK
+ );
$logViewer->showList( $wgOut );
-
+
if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
# Format the user-visible controls (comment field, submission button)
# in a nice little table
- $table = '<fieldset><table><tr>';
- $table .= '<td colspan="2">' . wfMsgWikiHtml( 'undeleteextrahelp' ) . '</td></tr><tr>';
- $table .= '<td align="right"><strong>' . wfMsgHtml( 'undeletecomment' ) . '</strong></td>';
- $table .= '<td>' . wfInput( 'wpComment', 50, $this->mComment ) . '</td>';
- $table .= '</tr><tr><td>&nbsp;</td><td>';
- $table .= wfSubmitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore' ) );
- $table .= wfElement( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ) ) );
- $table .= '</td></tr></table></fieldset>';
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+ $table =
+ Xml::openElement( 'fieldset' ) .
+ Xml::openElement( 'table' ) .
+ "<tr>
+ <td colspan='2'>" .
+ wfMsgWikiHtml( 'undeleteextrahelp' ) .
+ "</td>
+ </tr>
+ <tr>
+ <td align='$align'>" .
+ Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
+ "</td>
+ <td>" .
+ Xml::input( 'wpComment', 50, $this->mComment ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>" .
+ Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) .
+ Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' );
+
$wgOut->addHtml( $table );
}
-
+
$wgOut->addHTML( "<h2>" . htmlspecialchars( wfMsg( "history" ) ) . "</h2>\n" );
if( $haveRevisions ) {
@@ -763,7 +795,7 @@ class UndeleteForm {
while( $row = $revisions->fetchObject() ) {
$ts = wfTimestamp( TS_MW, $row->ar_timestamp );
if ( $this->mAllowed ) {
- $checkBox = wfCheck( "ts$ts" );
+ $checkBox = Xml::check( "ts$ts" );
$pageLink = $sk->makeKnownLinkObj( $titleObj,
$wgLang->timeanddate( $ts, true ),
"target=$target&timestamp=$ts" );
@@ -782,7 +814,7 @@ class UndeleteForm {
}
$comment = $sk->commentBlock( $row->ar_comment );
$wgOut->addHTML( "<li>$checkBox $pageLink . . $userLink $stxt $comment</li>\n" );
-
+
}
$revisions->free();
$wgOut->addHTML("</ul>");
@@ -790,14 +822,13 @@ class UndeleteForm {
$wgOut->addWikiText( wfMsg( "nohistory" ) );
}
-
if( $haveFiles ) {
- $wgOut->addHtml( "<h2>" . wfMsgHtml( 'imghistory' ) . "</h2>\n" );
+ $wgOut->addHtml( "<h2>" . wfMsgHtml( 'filehist' ) . "</h2>\n" );
$wgOut->addHtml( "<ul>" );
while( $row = $files->fetchObject() ) {
$ts = wfTimestamp( TS_MW, $row->fa_timestamp );
if ( $this->mAllowed && $row->fa_storage_key ) {
- $checkBox = wfCheck( "fileid" . $row->fa_id );
+ $checkBox = Xml::check( "fileid" . $row->fa_id );
$key = urlencode( $row->fa_storage_key );
$target = urlencode( $this->mTarget );
$pageLink = $sk->makeKnownLinkObj( $titleObj,
@@ -821,12 +852,13 @@ class UndeleteForm {
$files->free();
$wgOut->addHTML( "</ul>" );
}
-
+
if ( $this->mAllowed ) {
# Slip in the hidden controls here
- $misc = wfHidden( 'target', $this->mTarget );
- $misc .= wfHidden( 'wpEditToken', $wgUser->editToken() );
- $wgOut->addHtml( $misc . '</form>' );
+ $misc = Xml::hidden( 'target', $this->mTarget );
+ $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
+ $misc .= Xml::closeElement( 'form' );
+ $wgOut->addHtml( $misc );
}
return true;
@@ -841,17 +873,25 @@ class UndeleteForm {
$this->mTargetTimestamp,
$this->mComment,
$this->mFileVersions );
-
+
if( $ok ) {
$skin = $wgUser->getSkin();
$link = $skin->makeKnownLinkObj( $this->mTargetObj );
$wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
- return true;
+ } else {
+ $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
+ }
+
+ // Show file deletion warnings and errors
+ $status = $archive->getFileStatus();
+ if ( $status && !$status->isGood() ) {
+ $wgOut->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) );
}
+ } else {
+ $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
}
- $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
return false;
}
}
-?>
+
diff --git a/includes/SpecialUnlockdb.php b/includes/SpecialUnlockdb.php
index e864a182..52025e53 100644
--- a/includes/SpecialUnlockdb.php
+++ b/includes/SpecialUnlockdb.php
@@ -107,4 +107,4 @@ END
}
}
-?>
+
diff --git a/includes/SpecialUnusedcategories.php b/includes/SpecialUnusedcategories.php
index 5cd3406b..492c5f84 100644
--- a/includes/SpecialUnusedcategories.php
+++ b/includes/SpecialUnusedcategories.php
@@ -15,7 +15,7 @@ class UnusedCategoriesPage extends QueryPage {
}
function getPageHeader() {
- return '<p>' . wfMsg('unusedcategoriestext') . '</p>';
+ return wfMsgExt( 'unusedcategoriestext', array( 'parse' ) );
}
function getSQL() {
@@ -43,4 +43,4 @@ function wfSpecialUnusedCategories() {
$uc = new UnusedCategoriesPage();
return $uc->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialUnusedimages.php b/includes/SpecialUnusedimages.php
index 6b99192a..52aa19d2 100644
--- a/includes/SpecialUnusedimages.php
+++ b/includes/SpecialUnusedimages.php
@@ -54,4 +54,4 @@ function wfSpecialUnusedimages() {
return $uip->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialUnusedtemplates.php b/includes/SpecialUnusedtemplates.php
index 8b72e8a7..79e99f3a 100644
--- a/includes/SpecialUnusedtemplates.php
+++ b/includes/SpecialUnusedtemplates.php
@@ -37,8 +37,7 @@ class UnusedtemplatesPage extends QueryPage {
}
function getPageHeader() {
- global $wgOut;
- return $wgOut->parse( wfMsg( 'unusedtemplatestext' ) );
+ return wfMsgExt( 'unusedtemplatestext', array( 'parse' ) );
}
}
@@ -49,4 +48,4 @@ function wfSpecialUnusedtemplates() {
$utp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialUnwatchedpages.php b/includes/SpecialUnwatchedpages.php
index fed0b590..b1883e97 100644
--- a/includes/SpecialUnwatchedpages.php
+++ b/includes/SpecialUnwatchedpages.php
@@ -62,4 +62,4 @@ function wfSpecialUnwatchedpages() {
$wpp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialUpload.php b/includes/SpecialUpload.php
index e07c414c..18c6dd9e 100644
--- a/includes/SpecialUpload.php
+++ b/includes/SpecialUpload.php
@@ -22,17 +22,19 @@ class UploadForm {
/**#@+
* @access private
*/
- var $mUploadFile, $mUploadDescription, $mLicense ,$mIgnoreWarning, $mUploadError;
- var $mUploadSaveName, $mUploadTempName, $mUploadSize, $mUploadOldVersion;
- var $mUploadCopyStatus, $mUploadSource, $mReUpload, $mAction, $mUpload;
- var $mOname, $mSessionKey, $mStashed, $mDestFile, $mRemoveTempFile, $mSourceType;
- var $mUploadTempFileSize = 0;
+ var $mComment, $mLicense, $mIgnoreWarning, $mCurlError;
+ var $mDestName, $mTempPath, $mFileSize, $mFileProps;
+ var $mCopyrightStatus, $mCopyrightSource, $mReUpload, $mAction, $mUploadClicked;
+ var $mSrcName, $mSessionKey, $mStashed, $mDesiredDestName, $mRemoveTempFile, $mSourceType;
+ var $mDestWarningAck, $mCurlDestHandle;
+ var $mLocalFile;
# Placeholders for text injection by hooks (must be HTML)
# extensions should take care to _append_ to the present value
var $uploadFormTextTop;
var $uploadFormTextAfterSummary;
+ const SESSION_VERSION = 1;
/**#@-*/
/**
@@ -42,10 +44,13 @@ class UploadForm {
*/
function UploadForm( &$request ) {
global $wgAllowCopyUploads;
- $this->mDestFile = $request->getText( 'wpDestFile' );
+ $this->mDesiredDestName = $request->getText( 'wpDestFile' );
+ $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' );
+ $this->mComment = $request->getText( 'wpUploadDescription' );
if( !$request->wasPosted() ) {
- # GET requests just give the main form; no data except wpDestfile.
+ # GET requests just give the main form; no data except destination
+ # filename and description
return;
}
@@ -53,23 +58,22 @@ class UploadForm {
$this->uploadFormTextTop = "";
$this->uploadFormTextAfterSummary = "";
- $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' );
$this->mReUpload = $request->getCheck( 'wpReUpload' );
- $this->mUpload = $request->getCheck( 'wpUpload' );
+ $this->mUploadClicked = $request->getCheck( 'wpUpload' );
- $this->mUploadDescription = $request->getText( 'wpUploadDescription' );
$this->mLicense = $request->getText( 'wpLicense' );
- $this->mUploadCopyStatus = $request->getText( 'wpUploadCopyStatus' );
- $this->mUploadSource = $request->getText( 'wpUploadSource' );
+ $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' );
+ $this->mCopyrightSource = $request->getText( 'wpUploadSource' );
$this->mWatchthis = $request->getBool( 'wpWatchthis' );
- $this->mSourceType = $request->getText( 'wpSourceType' );
- wfDebug( "UploadForm: watchthis is: '$this->mWatchthis'\n" );
+ $this->mSourceType = $request->getText( 'wpSourceType' );
+ $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' );
$this->mAction = $request->getVal( 'action' );
$this->mSessionKey = $request->getInt( 'wpSessionKey' );
if( !empty( $this->mSessionKey ) &&
- isset( $_SESSION['wsUploadData'][$this->mSessionKey] ) ) {
+ isset( $_SESSION['wsUploadData'][$this->mSessionKey]['version'] ) &&
+ $_SESSION['wsUploadData'][$this->mSessionKey]['version'] == self::SESSION_VERSION ) {
/**
* Confirming a temporarily stashed upload.
* We don't want path names to be forged, so we keep
@@ -77,10 +81,11 @@ class UploadForm {
* an opaque key to the user agent.
*/
$data = $_SESSION['wsUploadData'][$this->mSessionKey];
- $this->mUploadTempName = $data['mUploadTempName'];
- $this->mUploadSize = $data['mUploadSize'];
- $this->mOname = $data['mOname'];
- $this->mUploadError = 0/*UPLOAD_ERR_OK*/;
+ $this->mTempPath = $data['mTempPath'];
+ $this->mFileSize = $data['mFileSize'];
+ $this->mSrcName = $data['mSrcName'];
+ $this->mFileProps = $data['mFileProps'];
+ $this->mCurlError = 0/*UPLOAD_ERR_OK*/;
$this->mStashed = true;
$this->mRemoveTempFile = false;
} else {
@@ -100,10 +105,10 @@ class UploadForm {
* @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->mTempPath = $request->getFileTempName( 'wpUploadFile' );
+ $this->mFileSize = $request->getFileSize( 'wpUploadFile' );
+ $this->mSrcName = $request->getFileName( 'wpUploadFile' );
+ $this->mCurlError = $request->getUploadError( 'wpUploadFile' );
$this->mSessionKey = false;
$this->mStashed = false;
$this->mRemoveTempFile = false; // PHP will handle this
@@ -118,10 +123,10 @@ class UploadForm {
$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->mTempPath = $local_file;
+ $this->mFileSize = 0; # Will be set by curlCopy
+ $this->mCurlError = $this->curlCopy( $url, $local_file );
+ $this->mSrcName = array_pop( explode( '/', $url ) );
$this->mSessionKey = false;
$this->mStashed = false;
@@ -150,8 +155,8 @@ class UploadForm {
}
# Open temporary file
- $this->mUploadTempFile = @fopen( $this->mUploadTempName, "wb" );
- if( $this->mUploadTempFile === false ) {
+ $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" );
+ if( $this->mCurlDestHandle === false ) {
# Could not open temporary file to write in
$wgOut->errorPage( 'upload-file-error', 'upload-file-error-text');
return true;
@@ -169,8 +174,8 @@ class UploadForm {
// if ( $error ) print curl_error ( $ch ) ; # Debugging output
curl_close( $ch );
- fclose( $this->mUploadTempFile );
- unset( $this->mUploadTempFile );
+ fclose( $this->mCurlDestHandle );
+ unset( $this->mCurlDestHandle );
if( $error ) {
unlink( $dest );
if( wfEmptyMsg( "upload-curl-error$errornum", wfMsg("upload-curl-error$errornum") ) )
@@ -191,11 +196,11 @@ class UploadForm {
function uploadCurlCallback( $ch, $data ) {
global $wgMaxUploadSize;
$length = strlen( $data );
- $this->mUploadTempFileSize += $length;
- if( $this->mUploadTempFileSize > $wgMaxUploadSize ) {
+ $this->mFileSize += $length;
+ if( $this->mFileSize > $wgMaxUploadSize ) {
return 0;
}
- fwrite( $this->mUploadTempFile, $data );
+ fwrite( $this->mCurlDestHandle, $data );
return $length;
}
@@ -205,11 +210,11 @@ class UploadForm {
*/
function execute() {
global $wgUser, $wgOut;
- global $wgEnableUploads, $wgUploadDirectory;
+ global $wgEnableUploads;
# Check uploading enabled
if( !$wgEnableUploads ) {
- $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext' );
+ $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext', array( $this->mDesiredDestName ) );
return;
}
@@ -234,18 +239,12 @@ class UploadForm {
return;
}
- /** Check if the image directory is writeable, this is a common mistake */
- if( !is_writeable( $wgUploadDirectory ) ) {
- $wgOut->addWikiText( wfMsg( 'upload_directory_read_only', $wgUploadDirectory ) );
- return;
- }
-
if( $this->mReUpload ) {
if( !$this->unsaveUploadedFile() ) {
return;
}
$this->mainUploadForm();
- } else if( 'submit' == $this->mAction || $this->mUpload ) {
+ } else if( 'submit' == $this->mAction || $this->mUploadClicked ) {
$this->processUpload();
} else {
$this->mainUploadForm();
@@ -271,7 +270,7 @@ class UploadForm {
}
/* Check for PHP error if any, requires php 4.2 or newer */
- if( $this->mUploadError == 1/*UPLOAD_ERR_INI_SIZE*/ ) {
+ if( $this->mCurlError == 1/*UPLOAD_ERR_INI_SIZE*/ ) {
$this->mainUploadForm( wfMsgHtml( 'largefileserver' ) );
return;
}
@@ -279,23 +278,24 @@ class UploadForm {
/**
* If there was no filename or a zero size given, give up quick.
*/
- if( trim( $this->mOname ) == '' || empty( $this->mUploadSize ) ) {
+ if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) {
$this->mainUploadForm( wfMsgHtml( 'emptyfile' ) );
return;
}
# Chop off any directories in the given filename
- if( $this->mDestFile ) {
- $basename = wfBaseName( $this->mDestFile );
+ if( $this->mDesiredDestName ) {
+ $basename = $this->mDesiredDestName;
} else {
- $basename = wfBaseName( $this->mOname );
+ $basename = $this->mSrcName;
}
+ $filtered = wfBaseName( $basename );
/**
* We'll want to blacklist against *any* 'extension', and use
* only the final one for the whitelist.
*/
- list( $partname, $ext ) = $this->splitExtensions( $basename );
+ list( $partname, $ext ) = $this->splitExtensions( $filtered );
if( count( $ext ) ) {
$finalExt = $ext[count( $ext ) - 1];
@@ -310,8 +310,8 @@ class UploadForm {
$partname .= '.' . $ext[$i];
}
- if( strlen( $partname ) < 3 ) {
- $this->mainUploadForm( wfMsgHtml( 'minlength' ) );
+ if( strlen( $partname ) < 1 ) {
+ $this->mainUploadForm( wfMsgHtml( 'minlength1' ) );
return;
}
@@ -319,14 +319,14 @@ class UploadForm {
* Filter out illegal characters, and try to make a legible name
* out of it. We'll strip some silently that Title would die on.
*/
- $filtered = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $basename );
- $nt = Title::newFromText( $filtered );
+ $filtered = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $filtered );
+ $nt = Title::makeTitleSafe( NS_IMAGE, $filtered );
if( is_null( $nt ) ) {
$this->uploadError( wfMsgWikiHtml( 'illegalfilename', htmlspecialchars( $filtered ) ) );
return;
}
- $nt =& Title::makeTitle( NS_IMAGE, $nt->getDBkey() );
- $this->mUploadSaveName = $nt->getDBkey();
+ $this->mLocalFile = wfLocalFile( $nt );
+ $this->mDestName = $this->mLocalFile->getName();
/**
* If the image is protected, non-sysop users won't be able
@@ -339,7 +339,7 @@ class UploadForm {
/**
* In some cases we may forbid overwriting of existing files.
*/
- $overwrite = $this->checkOverwrite( $this->mUploadSaveName );
+ $overwrite = $this->checkOverwrite( $this->mDestName );
if( WikiError::isError( $overwrite ) ) {
return $this->uploadError( $overwrite->toString() );
}
@@ -350,9 +350,9 @@ class UploadForm {
if ($finalExt == '') {
return $this->uploadError( wfMsgExt( 'filetype-missing', array ( 'parseinline' ) ) );
} elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
- ($wgStrictFileExtensions &&
- !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) {
- return $this->uploadError( wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ) );
+ ($wgStrictFileExtensions && !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) {
+ return $this->uploadError( wfMsgExt( 'filetype-badtype', array ( 'parseinline' ),
+ htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ) );
}
/**
@@ -361,23 +361,25 @@ class UploadForm {
* probably not accept it.
*/
if( !$this->mStashed ) {
+ $this->mFileProps = File::getPropsFromPath( $this->mTempPath, $finalExt );
$this->checkMacBinary();
- $veri = $this->verify( $this->mUploadTempName, $finalExt );
+ $veri = $this->verify( $this->mTempPath, $finalExt );
if( $veri !== true ) { //it's a wiki error...
return $this->uploadError( $veri->toString() );
}
- }
- /**
- * Provide an opportunity for extensions to add futher checks
- */
- $error = '';
- if( !wfRunHooks( 'UploadVerification',
- array( $this->mUploadSaveName, $this->mUploadTempName, &$error ) ) ) {
- return $this->uploadError( $error );
+ /**
+ * Provide an opportunity for extensions to add further checks
+ */
+ $error = '';
+ if( !wfRunHooks( 'UploadVerification',
+ array( $this->mDestName, $this->mTempPath, &$error ) ) ) {
+ return $this->uploadError( $error );
+ }
}
+
/**
* Check for non-fatal conditions
*/
@@ -388,97 +390,32 @@ class UploadForm {
if( $wgCapitalLinks ) {
$filtered = ucfirst( $filtered );
}
- if( $this->mUploadSaveName != $filtered ) {
- $warning .= '<li>'.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mUploadSaveName ) ).'</li>';
+ if( $basename != $filtered ) {
+ $warning .= '<li>'.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mDestName ) ).'</li>';
}
global $wgCheckFileExtensions;
if ( $wgCheckFileExtensions ) {
if ( ! $this->checkFileExtension( $finalExt, $wgFileExtensions ) ) {
- $warning .= '<li>'.wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ).'</li>';
+ $warning .= '<li>'.wfMsgExt( 'filetype-badtype', array ( 'parseinline' ),
+ htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ).'</li>';
}
}
global $wgUploadSizeWarning;
- if ( $wgUploadSizeWarning && ( $this->mUploadSize > $wgUploadSizeWarning ) ) {
+ if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) {
$skin = $wgUser->getSkin();
$wsize = $skin->formatSize( $wgUploadSizeWarning );
- $asize = $skin->formatSize( $this->mUploadSize );
+ $asize = $skin->formatSize( $this->mFileSize );
$warning .= '<li>' . wfMsgHtml( 'large-file', $wsize, $asize ) . '</li>';
}
- if ( $this->mUploadSize == 0 ) {
+ if ( $this->mFileSize == 0 ) {
$warning .= '<li>'.wfMsgHtml( 'emptyfile' ).'</li>';
}
- global $wgUser;
- $sk = $wgUser->getSkin();
- $image = new Image( $nt );
-
- // Check for uppercase extension. We allow these filenames but check if an image
- // with lowercase extension exists already
- if ( $finalExt != strtolower( $finalExt ) ) {
- $nt_lc = Title::newFromText( $partname . '.' . strtolower( $finalExt ) );
- $image_lc = new Image( $nt_lc );
+ if ( !$this->mDestWarningAck ) {
+ $warning .= self::getExistsWarning( $this->mLocalFile );
}
-
- if( $image->exists() ) {
- $dlink = $sk->makeKnownLinkObj( $nt );
- if ( $image->allowInlineDisplay() ) {
- $dlink2 = $sk->makeImageLinkObj( $nt, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), $nt->getText(), 'right', array(), false, true );
- } elseif ( !$image->allowInlineDisplay() && $image->isSafeFile() ) {
- $icon = $image->iconThumb();
- $dlink2 = '<div style="float:right" id="mw-media-icon"><a href="' . $image->getURL() . '">' . $icon->toHtml() . '</a><br />' . $dlink . '</div>';
- } else {
- $dlink2 = '';
- }
-
- $warning .= '<li>' . wfMsgExt( 'fileexists', 'parseline', $dlink ) . '</li>' . $dlink2;
-
- } elseif ( isset( $image_lc) && $image_lc->exists() ) {
- # Check if image with lowercase extension exists.
- # It's not forbidden but in 99% it makes no sense to upload the same filename with uppercase extension
- $dlink = $sk->makeKnownLinkObj( $nt_lc );
- if ( $image_lc->allowInlineDisplay() ) {
- $dlink2 = $sk->makeImageLinkObj( $nt_lc, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), $nt_lc->getText(), 'right', array(), false, true );
- } elseif ( !$image_lc->allowInlineDisplay() && $image_lc->isSafeFile() ) {
- $icon = $image_lc->iconThumb();
- $dlink2 = '<div style="float:right" id="mw-media-icon"><a href="' . $image_lc->getURL() . '">' . $icon->toHtml() . '</a><br />' . $dlink . '</div>';
- } else {
- $dlink2 = '';
- }
-
- $warning .= '<li>' . wfMsgExt( 'fileexists-extension', 'parsemag' , $partname . '.' . $finalExt , $dlink ) . '</li>' . $dlink2;
-
- } elseif ( ( substr( $partname , 3, 3 ) == 'px-' || substr( $partname , 2, 3 ) == 'px-' ) && ereg( "[0-9]{2}" , substr( $partname , 0, 2) ) ) {
- # Check for filenames like 50px- or 180px-, these are mostly thumbnails
- $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $finalExt );
- $image_thb = new Image( $nt_thb );
- if ($image_thb->exists() ) {
- # Check if an image without leading '180px-' (or similiar) exists
- $dlink = $sk->makeKnownLinkObj( $nt_thb);
- if ( $image_thb->allowInlineDisplay() ) {
- $dlink2 = $sk->makeImageLinkObj( $nt_thb, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), $nt_thb->getText(), 'right', array(), false, true );
- } elseif ( !$image_thb->allowInlineDisplay() && $image_thb->isSafeFile() ) {
- $icon = $image_thb->iconThumb();
- $dlink2 = '<div style="float:right" id="mw-media-icon"><a href="' . $image_thb->getURL() . '">' . $icon->toHtml() . '</a><br />' . $dlink . '</div>';
- } else {
- $dlink2 = '';
- }
-
- $warning .= '<li>' . wfMsgExt( 'fileexists-thumbnail-yes', 'parsemag', $dlink ) . '</li>' . $dlink2;
- } else {
- # Image w/o '180px-' does not exists, but we do not like these filenames
- $warning .= '<li>' . wfMsgExt( 'file-thumbnail-no', 'parseinline' , substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '</li>';
- }
- }
- if ( $image->wasDeleted() ) {
- # If the file existed before and was deleted, warn the user of this
- # Don't bother doing so if the image exists now, however
- $ltitle = SpecialPage::getTitleFor( 'Log' );
- $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ), 'type=delete&page=' . $nt->getPrefixedUrl() );
- $warning .= wfOpenElement( 'li' ) . wfMsgWikiHtml( 'filewasdeleted', $llink ) . wfCloseElement( 'li' );
- }
-
if( $warning != '' ) {
/**
* Stash the file in a temporary location; the user can choose
@@ -492,91 +429,164 @@ class UploadForm {
* Try actually saving the thing...
* It will show an error form on failure.
*/
- $hasBeenMunged = !empty( $this->mSessionKey ) || $this->mRemoveTempFile;
- if( $this->saveUploadedFile( $this->mUploadSaveName,
- $this->mUploadTempName,
- $hasBeenMunged ) ) {
- /**
- * Update the upload log and create the description page
- * if it's a new file.
- */
- $img = Image::newFromName( $this->mUploadSaveName );
- $success = $img->recordUpload( $this->mUploadOldVersion,
- $this->mUploadDescription,
- $this->mLicense,
- $this->mUploadCopyStatus,
- $this->mUploadSource,
- $this->mWatchthis );
-
- if ( $success ) {
- $this->showSuccess();
- wfRunHooks( 'UploadComplete', array( &$img ) );
- } else {
- // Image::recordUpload() fails if the image went missing, which is
- // unlikely, hence the lack of a specialised message
- $wgOut->showFileNotFoundError( $this->mUploadSaveName );
+ $pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
+ $this->mCopyrightStatus, $this->mCopyrightSource );
+
+ $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
+ File::DELETE_SOURCE, $this->mFileProps );
+ if ( !$status->isGood() ) {
+ $this->showError( $status->getWikiText() );
+ } else {
+ if ( $this->mWatchthis ) {
+ global $wgUser;
+ $wgUser->addWatch( $this->mLocalFile->getTitle() );
}
+ // Success, redirect to description page
+ $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() );
+ $img = null; // @todo: added to avoid passing a ref to null - should this be defined somewhere?
+ wfRunHooks( 'UploadComplete', array( &$img ) );
}
}
/**
- * Move the uploaded file from its temporary location to the final
- * destination. If a previous version of the file exists, move
- * it into the archive subdirectory.
- *
- * @todo If the later save fails, we may have disappeared the original file.
- *
- * @param string $saveName
- * @param string $tempName full path to the temporary file
- * @param bool $useRename if true, doesn't check that the source file
- * is a PHP-managed upload temporary
+ * Do existence checks on a file and produce a warning
+ * This check is static and can be done pre-upload via AJAX
+ * Returns an HTML fragment consisting of one or more LI elements if there is a warning
+ * Returns an empty string if there is no warning
*/
- function saveUploadedFile( $saveName, $tempName, $useRename = false ) {
- global $wgOut, $wgAllowCopyUploads;
+ static function getExistsWarning( $file ) {
+ global $wgUser;
+ // Check for uppercase extension. We allow these filenames but check if an image
+ // with lowercase extension exists already
+ $warning = '';
+
+ if( strpos( $file->getName(), '.' ) == false ) {
+ $partname = $file->getName();
+ $rawExtension = '';
+ } else {
+ list( $partname, $rawExtension ) = explode( '.', $file->getName(), 2 );
+ }
+ $sk = $wgUser->getSkin();
- if ( !$useRename AND $wgAllowCopyUploads AND $this->mSourceType == 'web' ) $useRename = true;
+ if ( $rawExtension != $file->getExtension() ) {
+ // We're not using the normalized form of the extension.
+ // Normal form is lowercase, using most common of alternate
+ // extensions (eg 'jpg' rather than 'JPEG').
+ //
+ // Check for another file using the normalized form...
+ $nt_lc = Title::newFromText( $partname . '.' . $file->getExtension() );
+ $file_lc = wfLocalFile( $nt_lc );
+ } else {
+ $file_lc = false;
+ }
- $fname= "SpecialUpload::saveUploadedFile";
+ if( $file->exists() ) {
+ $dlink = $sk->makeKnownLinkObj( $file->getTitle() );
+ if ( $file->allowInlineDisplay() ) {
+ $dlink2 = $sk->makeImageLinkObj( $file->getTitle(), wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ),
+ $file->getName(), 'right', array(), false, true );
+ } elseif ( !$file->allowInlineDisplay() && $file->isSafeFile() ) {
+ $icon = $file->iconThumb();
+ $dlink2 = '<div style="float:right" id="mw-media-icon">' .
+ $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>';
+ } else {
+ $dlink2 = '';
+ }
- $dest = wfImageDir( $saveName );
- $archive = wfImageArchiveDir( $saveName );
- if ( !is_dir( $dest ) ) wfMkdirParents( $dest );
- if ( !is_dir( $archive ) ) wfMkdirParents( $archive );
+ $warning .= '<li>' . wfMsgExt( 'fileexists', 'parseline', $dlink ) . '</li>' . $dlink2;
+
+ } elseif ( $file_lc && $file_lc->exists() ) {
+ # Check if image with lowercase extension exists.
+ # It's not forbidden but in 99% it makes no sense to upload the same filename with uppercase extension
+ $dlink = $sk->makeKnownLinkObj( $nt_lc );
+ if ( $file_lc->allowInlineDisplay() ) {
+ $dlink2 = $sk->makeImageLinkObj( $nt_lc, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ),
+ $nt_lc->getText(), 'right', array(), false, true );
+ } elseif ( !$file_lc->allowInlineDisplay() && $file_lc->isSafeFile() ) {
+ $icon = $file_lc->iconThumb();
+ $dlink2 = '<div style="float:right" id="mw-media-icon">' .
+ $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>';
+ } else {
+ $dlink2 = '';
+ }
- $this->mSavedFile = "{$dest}/{$saveName}";
+ $warning .= '<li>' . wfMsgExt( 'fileexists-extension', 'parsemag', $file->getName(), $dlink ) . '</li>' . $dlink2;
- if( is_file( $this->mSavedFile ) ) {
- $this->mUploadOldVersion = gmdate( 'YmdHis' ) . "!{$saveName}";
- wfSuppressWarnings();
- $success = rename( $this->mSavedFile, "${archive}/{$this->mUploadOldVersion}" );
- wfRestoreWarnings();
+ } elseif ( ( substr( $partname , 3, 3 ) == 'px-' || substr( $partname , 2, 3 ) == 'px-' )
+ && ereg( "[0-9]{2}" , substr( $partname , 0, 2) ) )
+ {
+ # Check for filenames like 50px- or 180px-, these are mostly thumbnails
+ $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $rawExtension );
+ $file_thb = wfLocalFile( $nt_thb );
+ if ($file_thb->exists() ) {
+ # Check if an image without leading '180px-' (or similiar) exists
+ $dlink = $sk->makeKnownLinkObj( $nt_thb);
+ if ( $file_thb->allowInlineDisplay() ) {
+ $dlink2 = $sk->makeImageLinkObj( $nt_thb,
+ wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ),
+ $nt_thb->getText(), 'right', array(), false, true );
+ } elseif ( !$file_thb->allowInlineDisplay() && $file_thb->isSafeFile() ) {
+ $icon = $file_thb->iconThumb();
+ $dlink2 = '<div style="float:right" id="mw-media-icon">' .
+ $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' .
+ $dlink . '</div>';
+ } else {
+ $dlink2 = '';
+ }
- if( ! $success ) {
- $wgOut->showFileRenameError( $this->mSavedFile,
- "${archive}/{$this->mUploadOldVersion}" );
- return false;
+ $warning .= '<li>' . wfMsgExt( 'fileexists-thumbnail-yes', 'parsemag', $dlink ) .
+ '</li>' . $dlink2;
+ } else {
+ # Image w/o '180px-' does not exists, but we do not like these filenames
+ $warning .= '<li>' . wfMsgExt( 'file-thumbnail-no', 'parseinline' ,
+ substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '</li>';
}
- else wfDebug("$fname: moved file ".$this->mSavedFile." to ${archive}/{$this->mUploadOldVersion}\n");
}
- else {
- $this->mUploadOldVersion = '';
+ if ( $file->wasDeleted() ) {
+ # If the file existed before and was deleted, warn the user of this
+ # Don't bother doing so if the image exists now, however
+ $ltitle = SpecialPage::getTitleFor( 'Log' );
+ $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ),
+ 'type=delete&page=' . $file->getTitle()->getPrefixedUrl() );
+ $warning .= '<li>' . wfMsgWikiHtml( 'filewasdeleted', $llink ) . '</li>';
}
+ return $warning;
+ }
- wfSuppressWarnings();
- $success = $useRename
- ? rename( $tempName, $this->mSavedFile )
- : move_uploaded_file( $tempName, $this->mSavedFile );
- wfRestoreWarnings();
-
- if( ! $success ) {
- $wgOut->showFileCopyError( $tempName, $this->mSavedFile );
- return false;
- } else {
- wfDebug("$fname: wrote tempfile $tempName to ".$this->mSavedFile."\n");
+ static function ajaxGetExistsWarning( $filename ) {
+ $file = wfFindFile( $filename );
+ if( !$file ) {
+ // Force local file so we have an object to do further checks against
+ // if there isn't an exact match...
+ $file = wfLocalFile( $filename );
}
-
- chmod( $this->mSavedFile, 0644 );
- return true;
+ $s = '&nbsp;';
+ if ( $file ) {
+ $warning = self::getExistsWarning( $file );
+ if ( $warning !== '' ) {
+ $s = "<ul>$warning</ul>";
+ }
+ }
+ return $s;
+ }
+
+ /**
+ * Render a preview of a given license for the AJAX preview on upload
+ *
+ * @param string $license
+ * @return string
+ */
+ public static function ajaxGetLicensePreview( $license ) {
+ global $wgParser, $wgUser;
+ $text = '{{' . $license . '}}';
+ $title = Title::makeTitle( NS_IMAGE, 'Sample.jpg' );
+ $options = ParserOptions::newFromUser( $wgUser );
+
+ // Expand subst: first, then live templates...
+ $text = $wgParser->preSaveTransform( $text, $title, $wgUser, $options );
+ $output = $wgParser->parse( $text, $title, $options );
+
+ return $output->getText();
}
/**
@@ -593,19 +603,14 @@ class UploadForm {
*/
function saveTempUploadedFile( $saveName, $tempName ) {
global $wgOut;
- $archive = wfImageArchiveDir( $saveName, 'temp' );
- if ( !is_dir ( $archive ) ) wfMkdirParents( $archive );
- $stash = $archive . '/' . gmdate( "YmdHis" ) . '!' . $saveName;
-
- $success = $this->mRemoveTempFile
- ? rename( $tempName, $stash )
- : move_uploaded_file( $tempName, $stash );
- if ( !$success ) {
- $wgOut->showFileCopyError( $tempName, $stash );
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $status = $repo->storeTemp( $saveName, $tempName );
+ if ( !$status->isGood() ) {
+ $this->showError( $status->getWikiText() );
return false;
+ } else {
+ return $status->value;
}
-
- return $stash;
}
/**
@@ -618,8 +623,7 @@ class UploadForm {
* @access private
*/
function stashSession() {
- $stash = $this->saveTempUploadedFile(
- $this->mUploadSaveName, $this->mUploadTempName );
+ $stash = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath );
if( !$stash ) {
# Couldn't save the file.
@@ -628,9 +632,12 @@ class UploadForm {
$key = mt_rand( 0, 0x7fffffff );
$_SESSION['wsUploadData'][$key] = array(
- 'mUploadTempName' => $stash,
- 'mUploadSize' => $this->mUploadSize,
- 'mOname' => $this->mOname );
+ 'mTempPath' => $stash,
+ 'mFileSize' => $this->mFileSize,
+ 'mSrcName' => $this->mSrcName,
+ 'mFileProps' => $this->mFileProps,
+ 'version' => self::SESSION_VERSION,
+ );
return $key;
}
@@ -641,11 +648,10 @@ class UploadForm {
*/
function unsaveUploadedFile() {
global $wgOut;
- wfSuppressWarnings();
- $success = unlink( $this->mUploadTempName );
- wfRestoreWarnings();
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $success = $repo->freeTemp( $this->mTempPath );
if ( ! $success ) {
- $wgOut->showFileDeleteError( $this->mUploadTempName );
+ $wgOut->showFileDeleteError( $this->mTempPath );
return false;
} else {
return true;
@@ -655,24 +661,6 @@ class UploadForm {
/* -------------------------------------------------------------- */
/**
- * Show some text and linkage on successful upload.
- * @access private
- */
- function showSuccess() {
- global $wgUser, $wgOut, $wgContLang;
-
- $sk = $wgUser->getSkin();
- $ilink = $sk->makeMediaLink( $this->mUploadSaveName, Image::imageUrl( $this->mUploadSaveName ) );
- $dname = $wgContLang->getNsText( NS_IMAGE ) . ':'.$this->mUploadSaveName;
- $dlink = $sk->makeKnownLink( $dname, $dname );
-
- $wgOut->addHTML( '<h2>' . wfMsgHtml( 'successfulupload' ) . "</h2>\n" );
- $text = wfMsgWikiHtml( 'fileuploaded', $ilink, $dlink );
- $wgOut->addHTML( $text );
- $wgOut->returnToMain( false );
- }
-
- /**
* @param string $error as HTML
* @access private
*/
@@ -691,7 +679,7 @@ class UploadForm {
* @access private
*/
function uploadWarning( $warning ) {
- global $wgOut;
+ global $wgOut, $wgContLang;
global $wgUseCopyrightUpload;
$this->mSessionKey = $this->stashSession();
@@ -709,12 +697,14 @@ class UploadForm {
$reup = wfMsgWikiHtml( 'reuploaddesc' );
$titleObj = SpecialPage::getTitleFor( 'Upload' );
$action = $titleObj->escapeLocalURL( 'action=submit' );
+ $align1 = $wgContLang->isRTL() ? 'left' : 'right';
+ $align2 = $wgContLang->isRTL() ? 'right' : 'left';
if ( $wgUseCopyrightUpload )
{
$copyright = "
- <input type='hidden' name='wpUploadCopyStatus' value=\"" . htmlspecialchars( $this->mUploadCopyStatus ) . "\" />
- <input type='hidden' name='wpUploadSource' value=\"" . htmlspecialchars( $this->mUploadSource ) . "\" />
+ <input type='hidden' name='wpUploadCopyStatus' value=\"" . htmlspecialchars( $this->mCopyrightStatus ) . "\" />
+ <input type='hidden' name='wpUploadSource' value=\"" . htmlspecialchars( $this->mCopyrightSource ) . "\" />
";
} else {
$copyright = "";
@@ -724,24 +714,24 @@ class UploadForm {
<form id='uploadwarning' method='post' enctype='multipart/form-data' action='$action'>
<input type='hidden' name='wpIgnoreWarning' value='1' />
<input type='hidden' name='wpSessionKey' value=\"" . htmlspecialchars( $this->mSessionKey ) . "\" />
- <input type='hidden' name='wpUploadDescription' value=\"" . htmlspecialchars( $this->mUploadDescription ) . "\" />
+ <input type='hidden' name='wpUploadDescription' value=\"" . htmlspecialchars( $this->mComment ) . "\" />
<input type='hidden' name='wpLicense' value=\"" . htmlspecialchars( $this->mLicense ) . "\" />
- <input type='hidden' name='wpDestFile' value=\"" . htmlspecialchars( $this->mDestFile ) . "\" />
+ <input type='hidden' name='wpDestFile' value=\"" . htmlspecialchars( $this->mDesiredDestName ) . "\" />
<input type='hidden' name='wpWatchthis' value=\"" . htmlspecialchars( intval( $this->mWatchthis ) ) . "\" />
{$copyright}
<table border='0'>
<tr>
<tr>
- <td align='right'>
+ <td align='$align1'>
<input tabindex='2' type='submit' name='wpUpload' value=\"$save\" />
</td>
- <td align='left'>$iw</td>
+ <td align='$align2'>$iw</td>
</tr>
<tr>
- <td align='right'>
+ <td align='$align1'>
<input tabindex='2' type='submit' name='wpReUpload' value=\"{$reupload}\" />
</td>
- <td align='left'>$reup</td>
+ <td align='$align2'>$reup</td>
</tr>
</tr>
</table></form>\n" );
@@ -755,15 +745,44 @@ class UploadForm {
* @access private
*/
function mainUploadForm( $msg='' ) {
- global $wgOut, $wgUser;
- global $wgUseCopyrightUpload;
+ global $wgOut, $wgUser, $wgContLang;
+ global $wgUseCopyrightUpload, $wgUseAjax, $wgAjaxUploadDestCheck, $wgAjaxLicensePreview;
global $wgRequest, $wgAllowCopyUploads;
+ global $wgStylePath, $wgStyleVersion;
+
+ $useAjaxDestCheck = $wgUseAjax && $wgAjaxUploadDestCheck;
+ $useAjaxLicensePreview = $wgUseAjax && $wgAjaxLicensePreview;
+
+ $adc = wfBoolToStr( $useAjaxDestCheck );
+ $alp = wfBoolToStr( $useAjaxLicensePreview );
+
+ $wgOut->addScript( "<script type=\"text/javascript\">
+wgAjaxUploadDestCheck = {$adc};
+wgAjaxLicensePreview = {$alp};
+</script>
+<script type=\"text/javascript\" src=\"{$wgStylePath}/common/upload.js?{$wgStyleVersion}\"></script>
+ " );
if( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) )
{
wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" );
return false;
}
+
+ if( $this->mDesiredDestName && $wgUser->isAllowed( 'deletedhistory' ) ) {
+ $title = Title::makeTitleSafe( NS_IMAGE, $this->mDesiredDestName );
+ if( $title instanceof Title && ( $count = $title->isDeleted() ) > 0 ) {
+ $link = wfMsgExt(
+ $wgUser->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted',
+ array( 'parse', 'replaceafter' ),
+ $wgUser->getSkin()->makeKnownLinkObj(
+ SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ),
+ wfMsgHtml( 'restorelink', $count )
+ )
+ );
+ $wgOut->addHtml( "<div id=\"contentSub2\">{$link}</div>" );
+ }
+ }
$cols = intval($wgUser->getOption( 'cols' ));
$ew = $wgUser->getOption( 'editwidth' );
@@ -776,15 +795,15 @@ class UploadForm {
"<span class='error'>{$msg}</span>\n" );
}
$wgOut->addHTML( '<div id="uploadtext">' );
- $wgOut->addWikiText( wfMsgNoTrans( 'uploadtext', $this->mDestFile ) );
+ $wgOut->addWikiText( wfMsgNoTrans( 'uploadtext', $this->mDesiredDestName ) );
$wgOut->addHTML( '</div>' );
$sourcefilename = wfMsgHtml( 'sourcefilename' );
$destfilename = wfMsgHtml( 'destfilename' );
- $summary = wfMsgWikiHtml( 'fileuploadsummary' );
+ $summary = wfMsgExt( 'fileuploadsummary', 'parseinline' );
$licenses = new Licenses();
- $license = wfMsgHtml( 'license' );
+ $license = wfMsgExt( 'license', array( 'parseinline' ) );
$nolicense = wfMsgHtml( 'nolicense' );
$licenseshtml = $licenses->getHtml();
@@ -794,64 +813,87 @@ class UploadForm {
$titleObj = SpecialPage::getTitleFor( 'Upload' );
$action = $titleObj->escapeLocalURL();
- $encDestFile = htmlspecialchars( $this->mDestFile );
+ $encDestName = htmlspecialchars( $this->mDesiredDestName );
$watchChecked =
( $wgUser->getOption( 'watchdefault' ) ||
- ( $wgUser->getOption( 'watchcreations' ) && $this->mDestFile == '' ) )
+ ( $wgUser->getOption( 'watchcreations' ) && $this->mDesiredDestName == '' ) )
? 'checked="checked"'
: '';
+ $warningChecked = $this->mIgnoreWarning ? 'checked' : '';
// Prepare form for upload or upload/copy
if( $wgAllowCopyUploads && $wgUser->isAllowed( 'upload_by_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' />" .
+ "<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->mDesiredDestName?"":"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 />" .
+ "<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->mDesiredDestName?"":"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\")' ") .
+ ($this->mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") .
"size='40' />" .
"<input type='hidden' name='wpSourceType' value='file' />" ;
}
+ if ( $useAjaxDestCheck ) {
+ $warningRow = "<tr><td colspan='2' id='wpDestFile-warning'>&nbsp;</td></tr>";
+ $destOnkeyup = 'onkeyup="wgUploadWarningObj.keypress();"';
+ } else {
+ $warningRow = '';
+ $destOnkeyup = '';
+ }
- $wgOut->addHTML( "
- <form id='upload' method='post' enctype='multipart/form-data' action=\"$action\">
+ $encComment = htmlspecialchars( $this->mComment );
+ $align1 = $wgContLang->isRTL() ? 'left' : 'right';
+ $align2 = $wgContLang->isRTL() ? 'right' : 'left';
+
+ $wgOut->addHTML( <<<EOT
+ <form id='upload' method='post' enctype='multipart/form-data' action="$action">
<table border='0'>
<tr>
{$this->uploadFormTextTop}
- <td align='right' valign='top'><label for='wpUploadFile'>{$sourcefilename}:</label></td>
- <td align='left'>
+ <td align='$align1' valign='top'><label for='wpUploadFile'>{$sourcefilename}:</label></td>
+ <td align='$align2'>
{$filename_form}
</td>
</tr>
<tr>
- <td align='right'><label for='wpDestFile'>{$destfilename}:</label></td>
- <td align='left'>
- <input tabindex='2' type='text' name='wpDestFile' id='wpDestFile' size='40' value=\"$encDestFile\" />
+ <td align='$align1'><label for='wpDestFile'>{$destfilename}:</label></td>
+ <td align='$align2'>
+ <input tabindex='2' type='text' name='wpDestFile' id='wpDestFile' size='40'
+ value="$encDestName" $destOnkeyup />
</td>
</tr>
<tr>
- <td align='right'><label for='wpUploadDescription'>{$summary}</label></td>
- <td align='left'>
- <textarea tabindex='3' name='wpUploadDescription' id='wpUploadDescription' rows='6' cols='{$cols}'{$ew}>" . htmlspecialchars( $this->mUploadDescription ) . "</textarea>
+ <td align='$align1'><label for='wpUploadDescription'>{$summary}</label></td>
+ <td align='$align2'>
+ <textarea tabindex='3' name='wpUploadDescription' id='wpUploadDescription' rows='6'
+ cols='{$cols}'{$ew}>$encComment</textarea>
{$this->uploadFormTextAfterSummary}
</td>
</tr>
- <tr>" );
+ <tr>
+EOT
+ );
if ( $licenseshtml != '' ) {
global $wgStylePath;
$wgOut->addHTML( "
- <td align='right'><label for='wpLicense'>$license:</label></td>
- <td align='left'>
- <script type='text/javascript' src=\"$wgStylePath/common/upload.js\"></script>
+ <td align='$align1'><label for='wpLicense'>$license:</label></td>
+ <td align='$align2'>
<select name='wpLicense' id='wpLicense' tabindex='4'
onchange='licenseSelectorCheck()'>
<option value=''>$nolicense</option>
@@ -859,46 +901,53 @@ class UploadForm {
</select>
</td>
</tr>
- <tr>
- ");
+ <tr>" );
+ if( $useAjaxLicensePreview ) {
+ $wgOut->addHtml( "
+ <td></td>
+ <td id=\"mw-license-preview\"></td>
+ </tr>
+ <tr>" );
+ }
}
if ( $wgUseCopyrightUpload ) {
$filestatus = wfMsgHtml ( 'filestatus' );
- $copystatus = htmlspecialchars( $this->mUploadCopyStatus );
+ $copystatus = htmlspecialchars( $this->mCopyrightStatus );
$filesource = wfMsgHtml ( 'filesource' );
- $uploadsource = htmlspecialchars( $this->mUploadSource );
+ $uploadsource = htmlspecialchars( $this->mCopyrightSource );
$wgOut->addHTML( "
- <td align='right' nowrap='nowrap'><label for='wpUploadCopyStatus'>$filestatus:</label></td>
- <td><input tabindex='5' type='text' name='wpUploadCopyStatus' id='wpUploadCopyStatus' value=\"$copystatus\" size='40' /></td>
+ <td align='$align1' nowrap='nowrap'><label for='wpUploadCopyStatus'>$filestatus:</label></td>
+ <td><input tabindex='5' type='text' name='wpUploadCopyStatus' id='wpUploadCopyStatus'
+ value=\"$copystatus\" size='40' /></td>
</tr>
<tr>
- <td align='right'><label for='wpUploadCopyStatus'>$filesource:</label></td>
- <td><input tabindex='6' type='text' name='wpUploadSource' id='wpUploadCopyStatus' value=\"$uploadsource\" size='40' /></td>
+ <td align='$align1'><label for='wpUploadCopyStatus'>$filesource:</label></td>
+ <td><input tabindex='6' type='text' name='wpUploadSource' id='wpUploadCopyStatus'
+ value=\"$uploadsource\" size='40' /></td>
</tr>
<tr>
");
}
-
$wgOut->addHtml( "
<td></td>
<td>
<input tabindex='7' type='checkbox' name='wpWatchthis' id='wpWatchthis' $watchChecked value='true' />
<label for='wpWatchthis'>" . wfMsgHtml( 'watchthisupload' ) . "</label>
- <input tabindex='8' type='checkbox' name='wpIgnoreWarning' id='wpIgnoreWarning' value='true' />
+ <input tabindex='8' type='checkbox' name='wpIgnoreWarning' id='wpIgnoreWarning' value='true' $warningChecked/>
<label for='wpIgnoreWarning'>" . wfMsgHtml( 'ignorewarnings' ) . "</label>
</td>
</tr>
+ $warningRow
<tr>
<td></td>
- <td align='left'><input tabindex='9' type='submit' name='wpUpload' value=\"{$ulb}\" /></td>
+ <td align='$align2'><input tabindex='9' type='submit' name='wpUpload' value=\"{$ulb}\"" . $wgUser->getSkin()->tooltipAndAccesskey( 'upload' ) . " /></td>
</tr>
-
<tr>
<td></td>
- <td align='left'>
+ <td align='$align2'>
" );
$wgOut->addWikiText( wfMsgForContent( 'edittools' ) );
$wgOut->addHTML( "
@@ -906,6 +955,7 @@ class UploadForm {
</tr>
</table>
+ <input type='hidden' name='wpDestFileWarningAck' id='wpDestFileWarningAck' value=''/>
</form>" );
}
@@ -966,12 +1016,11 @@ class UploadForm {
$magic=& MimeMagic::singleton();
$mime= $magic->guessMimeType($tmpfile,false);
- $fname= "SpecialUpload::verify";
-
#check mime type, if desired
global $wgVerifyMimeType;
if ($wgVerifyMimeType) {
+ wfDebug ( "\n\nmime: <$mime> extension: <$extension>\n\n");
#check mime type against file extension
if( !$this->verifyExtension( $mime, $extension ) ) {
return new WikiErrorMsg( 'uploadcorrupt' );
@@ -998,7 +1047,7 @@ class UploadForm {
return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) );
}
- wfDebug( "$fname: all clear; passing.\n" );
+ wfDebug( __METHOD__.": all clear; passing.\n" );
return true;
}
@@ -1010,45 +1059,46 @@ class UploadForm {
* @return bool
*/
function verifyExtension( $mime, $extension ) {
- $fname = 'SpecialUpload::verifyExtension';
-
$magic =& MimeMagic::singleton();
if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' )
if ( ! $magic->isRecognizableExtension( $extension ) ) {
- wfDebug( "$fname: passing file with unknown detected mime type; unrecognized extension '$extension', can't verify\n" );
+ wfDebug( __METHOD__.": passing file with unknown detected mime type; " .
+ "unrecognized extension '$extension', can't verify\n" );
return true;
} else {
- wfDebug( "$fname: rejecting file with unknown detected mime type; recognized extension '$extension', so probably invalid file\n" );
+ wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ".
+ "recognized extension '$extension', so probably invalid file\n" );
return false;
}
$match= $magic->isMatchingExtension($extension,$mime);
if ($match===NULL) {
- wfDebug( "$fname: no file extension known for mime type $mime, passing file\n" );
+ wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" );
return true;
} elseif ($match===true) {
- wfDebug( "$fname: mime type $mime matches extension $extension, passing file\n" );
+ wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" );
#TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it!
return true;
} else {
- wfDebug( "$fname: mime type $mime mismatches file extension $extension, rejecting file\n" );
+ wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" );
return false;
}
}
- /** Heuristig for detecting files that *could* contain JavaScript instructions or
- * things that may look like HTML to a browser and are thus
- * potentially harmful. The present implementation will produce false positives in some situations.
- *
- * @param string $file Pathname to the temporary upload file
- * @param string $mime The mime type of the file
- * @param string $extension The extension of the file
- * @return bool true if the file contains something looking like embedded scripts
- */
+ /**
+ * Heuristic for detecting files that *could* contain JavaScript instructions or
+ * things that may look like HTML to a browser and are thus
+ * potentially harmful. The present implementation will produce false positives in some situations.
+ *
+ * @param string $file Pathname to the temporary upload file
+ * @param string $mime The mime type of the file
+ * @param string $extension The extension of the file
+ * @return bool true if the file contains something looking like embedded scripts
+ */
function detectScript($file, $mime, $extension) {
global $wgAllowTitlesInSVG;
@@ -1137,93 +1187,103 @@ class UploadForm {
return false;
}
- /** Generic wrapper function for a virus scanner program.
- * This relies on the $wgAntivirus and $wgAntivirusSetup variables.
- * $wgAntivirusRequired may be used to deny upload if the scan fails.
- *
- * @param string $file Pathname to the temporary upload file
- * @return mixed false if not virus is found, NULL if the scan fails or is disabled,
- * or a string containing feedback from the virus scanner if a virus was found.
- * If textual feedback is missing but a virus was found, this function returns true.
- */
+ /**
+ * Generic wrapper function for a virus scanner program.
+ * This relies on the $wgAntivirus and $wgAntivirusSetup variables.
+ * $wgAntivirusRequired may be used to deny upload if the scan fails.
+ *
+ * @param string $file Pathname to the temporary upload file
+ * @return mixed false if not virus is found, NULL if the scan fails or is disabled,
+ * or a string containing feedback from the virus scanner if a virus was found.
+ * If textual feedback is missing but a virus was found, this function returns true.
+ */
function detectVirus($file) {
global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut;
- $fname= "SpecialUpload::detectVirus";
-
- if (!$wgAntivirus) { #disabled?
- wfDebug("$fname: virus scanner disabled\n");
-
+ if ( !$wgAntivirus ) {
+ wfDebug( __METHOD__.": virus scanner disabled\n");
return NULL;
}
- if (!$wgAntivirusSetup[$wgAntivirus]) {
- wfDebug("$fname: unknown virus scanner: $wgAntivirus\n");
-
- $wgOut->addHTML( "<div class='error'>Bad configuration: unknown virus scanner: <i>$wgAntivirus</i></div>\n" ); #LOCALIZE
-
+ if ( !$wgAntivirusSetup[$wgAntivirus] ) {
+ wfDebug( __METHOD__.": unknown virus scanner: $wgAntivirus\n" );
+ # @TODO: localise
+ $wgOut->addHTML( "<div class='error'>Bad configuration: unknown virus scanner: <i>$wgAntivirus</i></div>\n" );
return "unknown antivirus: $wgAntivirus";
}
- #look up scanner configuration
- $virus_scanner= $wgAntivirusSetup[$wgAntivirus]["command"]; #command pattern
- $virus_scanner_codes= $wgAntivirusSetup[$wgAntivirus]["codemap"]; #exit-code map
- $msg_pattern= $wgAntivirusSetup[$wgAntivirus]["messagepattern"]; #message pattern
-
- $scanner= $virus_scanner; #copy, so we can resolve the pattern
+ # look up scanner configuration
+ $command = $wgAntivirusSetup[$wgAntivirus]["command"];
+ $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"];
+ $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ?
+ $wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null;
- if (strpos($scanner,"%f")===false) $scanner.= " ".wfEscapeShellArg($file); #simple pattern: append file to scan
- else $scanner= str_replace("%f",wfEscapeShellArg($file),$scanner); #complex pattern: replace "%f" with file to scan
+ if ( strpos( $command,"%f" ) === false ) {
+ # simple pattern: append file to scan
+ $command .= " " . wfEscapeShellArg( $file );
+ } else {
+ # complex pattern: replace "%f" with file to scan
+ $command = str_replace( "%f", wfEscapeShellArg( $file ), $command );
+ }
- wfDebug("$fname: running virus scan: $scanner \n");
+ wfDebug( __METHOD__.": running virus scan: $command \n" );
- #execute virus scanner
- $code= false;
+ # execute virus scanner
+ $exitCode = false;
#NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
# that does not seem to be worth the pain.
# Ask me (Duesentrieb) about it if it's ever needed.
$output = array();
- if (wfIsWindows()) exec("$scanner",$output,$code);
- else exec("$scanner 2>&1",$output,$code);
-
- $exit_code= $code; #remember for user feedback
+ if ( wfIsWindows() ) {
+ exec( "$command", $output, $exitCode );
+ } else {
+ exec( "$command 2>&1", $output, $exitCode );
+ }
- if ($virus_scanner_codes) { #map exit code to AV_xxx constants.
- if (isset($virus_scanner_codes[$code])) {
- $code= $virus_scanner_codes[$code]; # explicit mapping
- } else if (isset($virus_scanner_codes["*"])) {
- $code= $virus_scanner_codes["*"]; # fallback mapping
+ # map exit code to AV_xxx constants.
+ $mappedCode = $exitCode;
+ if ( $exitCodeMap ) {
+ if ( isset( $exitCodeMap[$exitCode] ) ) {
+ $mappedCode = $exitCodeMap[$exitCode];
+ } elseif ( isset( $exitCodeMap["*"] ) ) {
+ $mappedCode = $exitCodeMap["*"];
}
}
- if ($code===AV_SCAN_FAILED) { #scan failed (code was mapped to false by $virus_scanner_codes)
- wfDebug("$fname: failed to scan $file (code $exit_code).\n");
+ if ( $mappedCode === AV_SCAN_FAILED ) {
+ # scan failed (code was mapped to false by $exitCodeMap)
+ wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" );
- if ($wgAntivirusRequired) { return "scan failed (code $exit_code)"; }
- else { return NULL; }
- }
- else if ($code===AV_SCAN_ABORTED) { #scan failed because filetype is unknown (probably imune)
- wfDebug("$fname: unsupported file type $file (code $exit_code).\n");
+ if ( $wgAntivirusRequired ) {
+ return "scan failed (code $exitCode)";
+ } else {
+ return NULL;
+ }
+ } else if ( $mappedCode === AV_SCAN_ABORTED ) {
+ # scan failed because filetype is unknown (probably imune)
+ wfDebug( __METHOD__.": unsupported file type $file (code $exitCode).\n" );
return NULL;
- }
- else if ($code===AV_NO_VIRUS) {
- wfDebug("$fname: file passed virus scan.\n");
- return false; #no virus found
- }
- else {
- $output= join("\n",$output);
- $output= trim($output);
-
- if (!$output) $output= true; #if there's no output, return true
- else if ($msg_pattern) {
- $groups= array();
- if (preg_match($msg_pattern,$output,$groups)) {
- if ($groups[1]) $output= $groups[1];
+ } else if ( $mappedCode === AV_NO_VIRUS ) {
+ # no virus found
+ wfDebug( __METHOD__.": file passed virus scan.\n" );
+ return false;
+ } else {
+ $output = join( "\n", $output );
+ $output = trim( $output );
+
+ if ( !$output ) {
+ $output = true; #if there's no output, return true
+ } elseif ( $msgPattern ) {
+ $groups = array();
+ if ( preg_match( $msgPattern, $output, $groups ) ) {
+ if ( $groups[1] ) {
+ $output = $groups[1];
+ }
}
}
- wfDebug("$fname: FOUND VIRUS! scanner feedback: $output");
+ wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output" );
return $output;
}
}
@@ -1237,7 +1297,7 @@ class UploadForm {
* @access private
*/
function checkMacBinary() {
- $macbin = new MacBinary( $this->mUploadTempName );
+ $macbin = new MacBinary( $this->mTempPath );
if( $macbin->isValid() ) {
$dataFile = tempnam( wfTempDir(), "WikiMacBinary" );
$dataHandle = fopen( $dataFile, 'wb' );
@@ -1245,8 +1305,8 @@ class UploadForm {
wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" );
$macbin->extractData( $dataHandle );
- $this->mUploadTempName = $dataFile;
- $this->mUploadSize = $macbin->dataForkLength();
+ $this->mTempPath = $dataFile;
+ $this->mFileSize = $macbin->dataForkLength();
// We'll have to manually remove the new file if it's not kept.
$this->mRemoveTempFile = true;
@@ -1260,9 +1320,9 @@ class UploadForm {
* @access private
*/
function cleanupTempFile() {
- if( $this->mRemoveTempFile && file_exists( $this->mUploadTempName ) ) {
- wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file $this->mUploadTempName\n" );
- unlink( $this->mUploadTempName );
+ if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) {
+ wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" );
+ unlink( $this->mTempPath );
}
}
@@ -1274,18 +1334,13 @@ class UploadForm {
* @access private
*/
function checkOverwrite( $name ) {
- $img = Image::newFromName( $name );
- if( is_null( $img ) ) {
- // Uh... this shouldn't happen ;)
- // But if it does, fall through to previous behavior
- return false;
- }
+ $img = wfFindFile( $name );
$error = '';
- if( $img->exists() ) {
+ if( $img ) {
global $wgUser, $wgOut;
if( $img->isLocal() ) {
- if( !$wgUser->isAllowed( 'reupload' ) ) {
+ if( !self::userCanReUpload( $wgUser, $img->name ) ) {
$error = 'fileexists-forbidden';
}
} else {
@@ -1305,5 +1360,64 @@ class UploadForm {
return true;
}
+ /**
+ * Check if a user is the last uploader
+ *
+ * @param User $user
+ * @param string $img, image name
+ * @return bool
+ */
+ public static function userCanReUpload( User $user, $img ) {
+ if( $user->isAllowed( 'reupload' ) )
+ return true; // non-conditional
+ if( !$user->isAllowed( 'reupload-own' ) )
+ return false;
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow('image',
+ /* SELECT */ 'img_user',
+ /* WHERE */ array( 'img_name' => $img )
+ );
+ if ( !$row )
+ return false;
+
+ return $user->getID() == $row->img_user;
+ }
+
+ /**
+ * Display an error with a wikitext description
+ */
+ function showError( $description ) {
+ global $wgOut;
+ $wgOut->setPageTitle( wfMsg( "internalerror" ) );
+ $wgOut->setRobotpolicy( "noindex,nofollow" );
+ $wgOut->setArticleRelated( false );
+ $wgOut->enableClientCache( false );
+ $wgOut->addWikiText( $description );
+ }
+
+ /**
+ * Get the initial image page text based on a comment and optional file status information
+ */
+ static function getInitialPageText( $comment, $license, $copyStatus, $source ) {
+ global $wgUseCopyrightUpload;
+ if ( $wgUseCopyrightUpload ) {
+ if ( $license != '' ) {
+ $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
+ }
+ $pageText = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n" .
+ '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" .
+ "$licensetxt" .
+ '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ;
+ } else {
+ if ( $license != '' ) {
+ $filedesc = $comment == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n";
+ $pageText = $filedesc .
+ '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
+ } else {
+ $pageText = $comment;
+ }
+ }
+ return $pageText;
+ }
}
-?>
diff --git a/includes/SpecialUploadMogile.php b/includes/SpecialUploadMogile.php
index 27af62e7..438e1df4 100644
--- a/includes/SpecialUploadMogile.php
+++ b/includes/SpecialUploadMogile.php
@@ -133,4 +133,4 @@ class UploadFormMogile extends UploadForm {
}
}
}
-?>
+
diff --git a/includes/SpecialUserlogin.php b/includes/SpecialUserlogin.php
index e8f33b8d..f358c1fd 100644
--- a/includes/SpecialUserlogin.php
+++ b/includes/SpecialUserlogin.php
@@ -8,7 +8,6 @@
* constructor
*/
function wfSpecialUserlogin() {
- global $wgCommandLineMode;
global $wgRequest;
if( session_id() == '' ) {
wfSetupSession();
@@ -32,6 +31,7 @@ class LoginForm {
const WRONG_PASS = 5;
const EMPTY_PASS = 6;
const RESET_PASS = 7;
+ const ABORTED = 8;
var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted;
var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword;
@@ -264,6 +264,11 @@ class LoginForm {
$this->mainLoginForm( wfMsg( 'passwordtooshort', $wgMinimalPasswordLength ) );
return false;
}
+
+ # Set some additional data so the AbortNewAccount hook can be
+ # used for more than just username validation
+ $u->setEmail( $this->mEmail );
+ $u->setRealName( $this->mRealName );
$abortError = '';
if( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError ) ) ) {
@@ -290,7 +295,7 @@ class LoginForm {
return false;
}
- return $this->initUser( $u );
+ return $this->initUser( $u, false );
}
/**
@@ -298,10 +303,11 @@ class LoginForm {
* Give it a User object that has been initialised with a name.
*
* @param $u User object.
+ * @param $autocreate boolean -- true if this is an autocreation via auth plugin
* @return User object.
* @private
*/
- function initUser( $u ) {
+ function initUser( $u, $autocreate ) {
global $wgAuth;
$u->addToDatabase();
@@ -314,7 +320,7 @@ class LoginForm {
$u->setRealName( $this->mRealName );
$u->setToken();
- $wgAuth->initUser( $u );
+ $wgAuth->initUser( $u, $autocreate );
$u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
$u->saveSettings();
@@ -353,7 +359,7 @@ class LoginForm {
*/
if ( $wgAuth->autoCreate() && $wgAuth->userExists( $u->getName() ) ) {
if ( $wgAuth->authenticate( $u->getName(), $this->mPassword ) ) {
- $u = $this->initUser( $u );
+ $u = $this->initUser( $u, true );
} else {
return self::WRONG_PLUGIN_PASS;
}
@@ -364,8 +370,13 @@ class LoginForm {
$u->load();
}
- if (!$u->checkPassword( $this->mPassword )) {
+ // Give general extensions, such as a captcha, a chance to abort logins
+ $abort = self::ABORTED;
+ if( !wfRunHooks( 'AbortLogin', array( $u, $this->mPassword, &$abort ) ) ) {
+ return $abort;
+ }
+ if (!$u->checkPassword( $this->mPassword )) {
if( $u->checkTemporaryPassword( $this->mPassword ) ) {
// The e-mailed temporary password should not be used
// for actual logins; that's a very sloppy habit,
@@ -394,16 +405,18 @@ class LoginForm {
// reset form; bot interfaces etc will probably just
// fail cleanly here.
//
- return self::RESET_PASS;
+ $retval = self::RESET_PASS;
} else {
- return '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS;
+ $retval = '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS;
}
} else {
$wgAuth->updateUser( $u );
$wgUser = $u;
- return self::SUCCESS;
+ $retval = self::SUCCESS;
}
+ wfRunHooks( 'LoginAuthenticateAudit', array( $u, $this->mPassword, $retval ) );
+ return $retval;
}
function processLogin() {
@@ -697,6 +710,7 @@ class LoginForm {
$wgOut->setPageTitle( wfMsg( 'userlogin' ) );
$wgOut->setRobotpolicy( 'noindex,nofollow' );
$wgOut->setArticleRelated( false );
+ $wgOut->disallowUserJs(); // just in case...
$wgOut->addTemplate( $template );
}
@@ -809,4 +823,4 @@ class LoginForm {
return $skin->makeKnownLinkObj( $self, htmlspecialchars( $text ), implode( '&', $attr ) );
}
}
-?>
+
diff --git a/includes/SpecialUserlogout.php b/includes/SpecialUserlogout.php
index 9f1bdb3a..6e464ced 100644
--- a/includes/SpecialUserlogout.php
+++ b/includes/SpecialUserlogout.php
@@ -23,4 +23,4 @@ function wfSpecialUserlogout() {
}
}
-?>
+
diff --git a/includes/SpecialUserrights.php b/includes/SpecialUserrights.php
index d12a9cc4..b97e5168 100644
--- a/includes/SpecialUserrights.php
+++ b/includes/SpecialUserrights.php
@@ -27,7 +27,7 @@ class UserrightsForm extends HTMLForm {
var $action;
/** Constructor*/
- function UserrightsForm ( &$request ) {
+ public function __construct( &$request ) {
$this->mPosted = $request->wasPosted();
$this->mRequest =& $request;
$this->mName = 'userrights';
@@ -74,7 +74,7 @@ class UserrightsForm extends HTMLForm {
* @param string $reason Reason for group change
*
*/
- function saveUserGroups( $username, $removegroup, $addgroup, $reason ) {
+ function saveUserGroups( $username, $removegroup, $addgroup, $reason = '' ) {
global $wgOut;
$u = User::newFromName($username);
@@ -94,13 +94,17 @@ class UserrightsForm extends HTMLForm {
if(isset($removegroup)) {
$newGroups = array_diff($newGroups, $removegroup);
foreach( $removegroup as $group ) {
- $u->removeGroup( $group );
+ if ( $this->canRemove( $group ) ) {
+ $u->removeGroup( $group );
+ }
}
}
if(isset($addgroup)) {
$newGroups = array_merge($newGroups, $addgroup);
foreach( $addgroup as $group ) {
- $u->addGroup( $group );
+ if ( $this->canAdd( $group ) ) {
+ $u->addGroup( $group );
+ }
}
}
$newGroups = array_unique( $newGroups );
@@ -139,7 +143,7 @@ class UserrightsForm extends HTMLForm {
*/
function editUserGroupsForm($username) {
global $wgOut;
-
+
$user = User::newFromName($username);
if( is_null( $user ) ) {
$wgOut->addWikiText( wfMsg( 'nouserspecified' ) );
@@ -149,12 +153,38 @@ class UserrightsForm extends HTMLForm {
return;
}
- $groups = $user->getGroups();
- $this->showEditUserGroupsForm( $username, $groups );
+ $this->showEditUserGroupsForm( $username, $user->getGroups() );
+ $this->showLogFragment( $user, $wgOut );
}
- function showEditUserGroupsForm( $username, $groups ) {
+ /**
+ * Go through used and available groups and return the ones that this
+ * form will be able to manipulate based on the current user's system
+ * permissions.
+ *
+ * @param $groups Array: list of groups the given user is in
+ * @return Array: Tuple of addable, then removable groups
+ */
+ protected function splitGroups( $groups ) {
+ list($addable, $removable) = array_values( $this->changeableGroups() );
+ $removable = array_intersect($removable, $groups ); // Can't remove groups the user doesn't have
+ $addable = array_diff( $addable, $groups ); // Can't add groups the user does have
+
+ return array( $addable, $removable );
+ }
+
+ /**
+ * Show the form to edit group memberships.
+ *
+ * @todo make all CSS-y and semantic
+ * @param $username String: Name of user you're editing
+ * @param $groups Array: Array of groups the user is in
+ */
+ protected function showEditUserGroupsForm( $username, $groups ) {
global $wgOut, $wgUser;
+
+ list( $addable, $removable ) = $this->splitGroups( $groups );
+
$wgOut->addHTML(
Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'editGroup' ) ) .
Xml::hidden( 'user-editname', $username ) .
@@ -162,14 +192,15 @@ class UserrightsForm extends HTMLForm {
Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', array(), wfMsg( 'userrights-editusergroup' ) ) .
$wgOut->parse( wfMsg( 'editinguser', $username ) ) .
+ $this->explainRights() .
"<table border='0'>
<tr>
<td></td>
<td>
<table width='400'>
<tr>
- <td width='50%'>" . HTMLSelectGroups( 'member', $this->mName.'-groupsmember', $groups, true, 6 ) . "</td>
- <td width='50%'>" . HTMLSelectGroups( 'available', $this->mName.'-groupsavailable', $groups, true, 6, true) . "</td>
+ <td width='50%'>" . $this->removeSelect( $removable ) . "</td>
+ <td width='50%'>" . $this->addSelect( $addable ) . "</td>
</tr>
</table>
</tr>
@@ -197,5 +228,175 @@ class UserrightsForm extends HTMLForm {
Xml::closeElement( 'form' ) . "\n"
);
}
-} // end class UserrightsForm
-?>
+
+ /**
+ * Prepare a list of groups the user is able to add and remove
+ *
+ * @return string
+ */
+ private function explainRights() {
+ global $wgUser, $wgLang;
+
+ $out = array();
+ list( $add, $remove ) = array_values( $this->changeableGroups() );
+
+ if( count( $add ) > 0 )
+ $out[] = wfMsgExt( 'userrights-available-add', 'parseinline', $wgLang->listToText( $add ) );
+ if( count( $remove ) > 0 )
+ $out[] = wfMsgExt( 'userrights-available-remove', 'parseinline', $wgLang->listToText( $remove ) );
+
+ return count( $out ) > 0
+ ? implode( ' ', $out )
+ : wfMsgExt( 'userrights-available-none', 'parseinline' );
+ }
+
+ /**
+ * Adds the <select> thingie where you can select what groups to remove
+ *
+ * @param array $groups The groups that can be removed
+ * @return string XHTML <select> element
+ */
+ private function removeSelect( $groups ) {
+ return $this->doSelect( $groups, 'member' );
+ }
+
+ /**
+ * Adds the <select> thingie where you can select what groups to add
+ *
+ * @param array $groups The groups that can be added
+ * @return string XHTML <select> element
+ */
+ private function addSelect( $groups ) {
+ return $this->doSelect( $groups, 'available' );
+ }
+
+ /**
+ * Adds the <select> thingie where you can select what groups to add/remove
+ *
+ * @param array $groups The groups that can be added/removed
+ * @param string $name 'member' or 'available'
+ * @return string XHTML <select> element
+ */
+ private function doSelect( $groups, $name ) {
+ $ret = wfMsgHtml( "{$this->mName}-groups$name" ) .
+ Xml::openElement( 'select', array(
+ 'name' => "{$name}[]",
+ 'multiple' => 'multiple',
+ 'size' => '6',
+ 'style' => 'width: 100%;'
+ )
+ );
+ foreach ($groups as $group) {
+ $ret .= Xml::element( 'option', array( 'value' => $group ), User::getGroupName( $group ) );
+ }
+ $ret .= Xml::closeElement( 'select' );
+ return $ret;
+ }
+
+ /**
+ * @param string $group The name of the group to check
+ * @return bool Can we remove the group?
+ */
+ private function canRemove( $group ) {
+ // $this->changeableGroups()['remove'] doesn't work, of course. Thanks,
+ // PHP.
+ $groups = $this->changeableGroups();
+ return in_array( $group, $groups['remove'] );
+ }
+
+ /**
+ * @param string $group The name of the group to check
+ * @return bool Can we add the group?
+ */
+ private function canAdd( $group ) {
+ $groups = $this->changeableGroups();
+ return in_array( $group, $groups['add'] );
+ }
+
+ /**
+ * Returns an array of the groups that the user can add/remove.
+ *
+ * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
+ */
+ private function changeableGroups() {
+ global $wgUser;
+
+ $groups = array( 'add' => array(), 'remove' => array() );
+ $addergroups = $wgUser->getEffectiveGroups();
+
+ foreach ($addergroups as $addergroup) {
+ $groups = array_merge_recursive(
+ $groups, $this->changeableByGroup($addergroup)
+ );
+ $groups['add'] = array_unique( $groups['add'] );
+ $groups['remove'] = array_unique( $groups['remove'] );
+ }
+ return $groups;
+ }
+
+ /**
+ * Returns an array of the groups that a particular group can add/remove.
+ *
+ * @param String $group The group to check for whether it can add/remove
+ * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
+ */
+ private function changeableByGroup( $group ) {
+ global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups;
+
+ if( empty($wgGroupPermissions[$group]['userrights']) ) {
+ // This group doesn't give the right to modify anything
+ return array( 'add' => array(), 'remove' => array() );
+ }
+ if( empty($wgAddGroups[$group]) and empty($wgRemoveGroups[$group]) ) {
+ // This group gives the right to modify everything (reverse-
+ // compatibility with old "userrights lets you change
+ // everything")
+ return array(
+ 'add' => User::getAllGroups(),
+ 'remove' => User::getAllGroups()
+ );
+ }
+
+ // Okay, it's not so simple, we have to go through the arrays
+ $groups = array( 'add' => array(), 'remove' => array() );
+ if( empty($wgAddGroups[$group]) ) {
+ // Don't add anything to $groups
+ } elseif( $wgAddGroups[$group] === true ) {
+ // You get everything
+ $groups['add'] = User::getAllGroups();
+ } elseif( is_array($wgAddGroups[$group]) ) {
+ $groups['add'] = $wgAddGroups[$group];
+ }
+
+ // Same thing for remove
+ if( empty($wgRemoveGroups[$group]) ) {
+ } elseif($wgRemoveGroups[$group] === true ) {
+ $groups['remove'] = User::getAllGroups();
+ } elseif( is_array($wgRemoveGroups[$group]) ) {
+ $groups['remove'] = $wgRemoveGroups[$group];
+ }
+ return $groups;
+ }
+
+ /**
+ * Show a rights log fragment for the specified user
+ *
+ * @param User $user User to show log for
+ * @param OutputPage $output OutputPage to use
+ */
+ protected function showLogFragment( $user, $output ) {
+ $viewer = new LogViewer(
+ new LogReader(
+ new FauxRequest(
+ array(
+ 'type' => 'rights',
+ 'page' => $user->getUserPage()->getPrefixedUrl(),
+ )
+ )
+ )
+ );
+ $output->addHtml( "<h2>" . htmlspecialchars( LogPage::logName( 'rights' ) ) . "</h2>\n" );
+ $viewer->showList( $output );
+ }
+
+} \ No newline at end of file
diff --git a/includes/SpecialVersion.php b/includes/SpecialVersion.php
index 6de2da11..a6a132e0 100644
--- a/includes/SpecialVersion.php
+++ b/includes/SpecialVersion.php
@@ -148,10 +148,14 @@ class SpecialVersion {
/** Callback to sort extensions by type */
function compare( $a, $b ) {
- if ( $a['name'] === $b['name'] )
+ global $wgLang;
+ if( $a['name'] === $b['name'] ) {
return 0;
- else
- return Language::lc( $a['name'] ) > Language::lc( $b['name'] ) ? 1 : -1;
+ } else {
+ return $wgLang->lc( $a['name'] ) > $wgLang->lc( $b['name'] )
+ ? 1
+ : -1;
+ }
}
function formatCredits( $name, $version = null, $author = null, $url = null, $description = null) {
@@ -250,7 +254,10 @@ class SpecialVersion {
* @return mixed
*/
function arrayToString( $list ) {
- if ( ! is_array( $list ) ) {
+ if( is_object( $list ) ) {
+ $class = get_class( $list );
+ return "($class)";
+ } elseif ( ! is_array( $list ) ) {
return $list;
} else {
$class = get_class( $list[0] );
@@ -308,4 +315,4 @@ class SpecialVersion {
}
/**#@-*/
-?>
+
diff --git a/includes/SpecialWantedcategories.php b/includes/SpecialWantedcategories.php
index 27a9f176..f3e8966c 100644
--- a/includes/SpecialWantedcategories.php
+++ b/includes/SpecialWantedcategories.php
@@ -76,4 +76,4 @@ function wfSpecialWantedCategories() {
$wpp->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SpecialWantedpages.php b/includes/SpecialWantedpages.php
index 8b700209..5fc45a88 100644
--- a/includes/SpecialWantedpages.php
+++ b/includes/SpecialWantedpages.php
@@ -63,46 +63,48 @@ class WantedPagesPage extends QueryPage {
$db->dataSeek( $res, 0 );
}
-
- function formatResult( $skin, $result ) {
- global $wgLang;
-
+ /**
+ * Format an individual result
+ *
+ * @param Skin $skin Skin to use for UI elements
+ * @param object $result Result row
+ * @return string
+ */
+ public function formatResult( $skin, $result ) {
$title = Title::makeTitleSafe( $result->namespace, $result->title );
-
- if( $this->isCached() ) {
- # Check existence; which is stored in the link cache
- if( !$title->exists() ) {
- # Make a redlink
- $pageLink = $skin->makeBrokenLinkObj( $title );
+ if( $title instanceof Title ) {
+ if( $this->isCached() ) {
+ $pageLink = $title->exists()
+ ? '<s>' . $skin->makeLinkObj( $title ) . '</s>'
+ : $skin->makeBrokenLinkObj( $title );
} else {
- # Make a a struck-out normal link
- $pageLink = "<s>" . $skin->makeLinkObj( $title ) . "</s>";
- }
+ $pageLink = $skin->makeBrokenLinkObj( $title );
+ }
+ return wfSpecialList( $pageLink, $this->makeWlhLink( $title, $skin, $result ) );
} else {
- # Not cached? Don't bother checking existence; it can't
- $pageLink = $skin->makeBrokenLinkObj( $title );
+ $tsafe = htmlspecialchars( $result->title );
+ return "Invalid title in result set; {$tsafe}";
}
-
- # Make a link to "what links here" if it's required
- $wlhLink = $this->nlinks
- ? $this->makeWlhLink( $title, $skin,
- wfMsgExt( 'nlinks', array( 'parsemag', 'escape'),
- $wgLang->formatNum( $result->value ) ) )
- : null;
-
- return wfSpecialList($pageLink, $wlhLink);
}
/**
- * Make a "what links here" link for a specified title
- * @param $title Title to make the link for
- * @param $skin Skin to use
- * @param $text Link text
+ * Make a "what links here" link for a specified result if required
+ *
+ * @param Title $title Title to make the link for
+ * @param Skin $skin Skin to use
+ * @param object $result Result row
* @return string
*/
- function makeWlhLink( &$title, &$skin, $text ) {
- $wlhTitle = SpecialPage::getTitleFor( 'Whatlinkshere' );
- return $skin->makeKnownLinkObj( $wlhTitle, $text, 'target=' . $title->getPrefixedUrl() );
+ private function makeWlhLink( $title, $skin, $result ) {
+ global $wgLang;
+ if( $this->nlinks ) {
+ $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' );
+ $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $result->value ) );
+ return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() );
+ } else {
+ return null;
+ }
}
}
@@ -128,4 +130,4 @@ function wfSpecialWantedpages( $par = null, $specialPage ) {
$wpp->doQuery( $offset, $limit, !$inc );
}
-?>
+
diff --git a/includes/SpecialWatchlist.php b/includes/SpecialWatchlist.php
index 2e660bd5..e9aa7e68 100644
--- a/includes/SpecialWatchlist.php
+++ b/includes/SpecialWatchlist.php
@@ -15,7 +15,7 @@ require_once( dirname(__FILE__) . '/SpecialRecentchanges.php' );
* @param $par Parameter passed to the page
*/
function wfSpecialWatchlist( $par ) {
- global $wgUser, $wgOut, $wgLang, $wgRequest, $wgContLang;
+ global $wgUser, $wgOut, $wgLang, $wgRequest;
global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker;
global $wgEnotifWatchlist;
$fname = 'wfSpecialWatchlist';
@@ -30,12 +30,24 @@ function wfSpecialWatchlist( $par ) {
$llink = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogin' ), wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() );
$wgOut->addHtml( wfMsgWikiHtml( 'watchlistanontext', $llink ) );
return;
- } else {
- $wgOut->setPageTitle( wfMsg( 'watchlist' ) );
- $wgOut->setSubtitle( wfMsgWikiHtml( 'watchlistfor', htmlspecialchars( $wgUser->getName() ) ) );
}
+
+ $wgOut->setPageTitle( wfMsg( 'watchlist' ) );
+
+ $sub = wfMsgExt( 'watchlistfor', 'parseinline', $wgUser->getName() );
+ $sub .= '<br />' . WatchlistEditor::buildTools( $wgUser->getSkin() );
+ $wgOut->setSubtitle( $sub );
- if( wlHandleClear( $wgOut, $wgRequest, $par ) ) {
+ if( ( $mode = WatchlistEditor::getMode( $wgRequest, $par ) ) !== false ) {
+ $editor = new WatchlistEditor();
+ $editor->execute( $wgUser, $wgOut, $wgRequest, $mode );
+ return;
+ }
+
+ $uid = $wgUser->getId();
+ if( $wgEnotifWatchlist && $wgRequest->getVal( 'reset' ) && $wgRequest->wasPosted() ) {
+ $wgUser->clearAllNotifications( $uid );
+ $wgOut->redirect( $specialTitle->getFullUrl() );
return;
}
@@ -72,37 +84,6 @@ function wfSpecialWatchlist( $par ) {
$nameSpaceClause = '';
}
- # Watchlist editing
- $action = $wgRequest->getVal( 'action' );
- $remove = $wgRequest->getVal( 'remove' );
- $id = $wgRequest->getArray( 'id' );
-
- $uid = $wgUser->getID();
- if( $wgEnotifWatchlist && $wgRequest->getVal( 'reset' ) && $wgRequest->wasPosted() ) {
- $wgUser->clearAllNotifications( $uid );
- }
-
- # Deleting items from watchlist
- if(($action == 'submit') && isset($remove) && is_array($id)) {
- $wgOut->addWikiText( wfMsg( 'removingchecked' ) );
- $wgOut->addHTML( "<ul id=\"mw-unwatch-list\">\n" );
- foreach($id as $one) {
- $t = Title::newFromURL( $one );
- if( !is_null( $t ) ) {
- $wl = WatchedItem::fromUserTitle( $wgUser, $t );
- if( $wl->removeWatch() === false ) {
- $wgOut->addHTML( '<li class="mw-unwatch-failure">' . wfMsg( 'couldntremove', htmlspecialchars($one) ) . "</li>\n" );
- } else {
- wfRunHooks('UnwatchArticle', array(&$wgUser, new Article($t)));
- $wgOut->addHTML( '<li class="mw-unwatch-success">[[' . htmlspecialchars($one) . "]]</li>\n" );
- }
- } else {
- $wgOut->addHTML( '<li class="mw-unwatch-invalid">' . wfMsg( 'iteminvalidname', htmlspecialchars($one) ) . "</li>\n" );
- }
- }
- $wgOut->addHTML( "</ul>\n<p>" . wfMsg( 'wldone' ) . "</p>\n" );
- }
-
$dbr = wfGetDB( DB_SLAVE, 'watchlist' );
list( $page, $watchlist, $recentchanges ) = $dbr->tableNamesN( 'page', 'watchlist', 'recentchanges' );
@@ -143,7 +124,6 @@ function wfSpecialWatchlist( $par ) {
if ( $days <= 0 ) {
$andcutoff = '';
- $npages = wfMsg( 'watchlistall1' );
} else {
$andcutoff = "AND rc_timestamp > '".$dbr->timestamp( time() - intval( $days * 86400 ) )."'";
/*
@@ -152,82 +132,6 @@ function wfSpecialWatchlist( $par ) {
$s = $dbr->fetchObject( $res );
$npages = $s->n;
*/
- $npages = 40000 * $days;
- }
-
- /* Edit watchlist form */
- if($wgRequest->getBool('edit') || $par == 'edit' ) {
- $wgOut->addWikiText( wfMsgExt( 'watchlistcontains', array( 'parseinline' ), $wgLang->formatNum( $nitems ) ) .
- "\n\n" . wfMsg( 'watcheditlist' ) );
-
- $wgOut->addHTML( '<form action=\'' .
- $specialTitle->escapeLocalUrl( 'action=submit' ) .
- "' method='post'>\n" );
-
-# Patch A2
-# The following was proposed by KTurner 07.11.2004 to T.Gries
-# $sql = "SELECT distinct (wl_namespace & ~1),wl_title FROM $watchlist WHERE wl_user=$uid";
- $sql = "SELECT wl_namespace, wl_title, page_is_redirect FROM $watchlist LEFT JOIN $page ON wl_namespace = page_namespace AND wl_title = page_title WHERE wl_user=$uid";
-
- $res = $dbr->query( $sql, $fname );
-
- # Batch existence check
- $linkBatch = new LinkBatch();
- while( $row = $dbr->fetchObject( $res ) )
- $linkBatch->addObj( Title::makeTitleSafe( $row->wl_namespace, $row->wl_title ) );
- $linkBatch->execute();
-
- if( $dbr->numRows( $res ) > 0 )
- $dbr->dataSeek( $res, 0 ); # Let's do the time warp again!
-
- $sk = $wgUser->getSkin();
-
- $list = array();
- while( $s = $dbr->fetchObject( $res ) ) {
- $list[$s->wl_namespace][$s->wl_title] = $s->page_is_redirect;
- }
-
- // TODO: Display a TOC
- foreach($list as $ns => $titles) {
- if (Namespace::isTalk($ns))
- continue;
- if ($ns != NS_MAIN)
- $wgOut->addHTML( '<h2>' . $wgContLang->getFormattedNsText( $ns ) . '</h2>' );
- $wgOut->addHTML( '<ul>' );
- foreach( $titles as $title => $redir ) {
- $titleObj = Title::makeTitle( $ns, $title );
- if( is_null( $titleObj ) ) {
- $wgOut->addHTML(
- '<!-- bad title "' .
- htmlspecialchars( $s->wl_title ) . '" in namespace ' . $s->wl_namespace . " -->\n"
- );
- } else {
- global $wgContLang;
- $toolLinks = array();
- $pageLink = $sk->makeLinkObj( $titleObj );
- $toolLinks[] = $sk->makeLinkObj( $titleObj->getTalkPage(), $wgLang->getNsText( NS_TALK ) );
- if( $titleObj->exists() )
- $toolLinks[] = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml( 'history_short' ), 'action=history' );
- $toolLinks = '(' . implode( ' | ', $toolLinks ) . ')';
- $checkbox = '<input type="checkbox" name="id[]" value="' . htmlspecialchars( $titleObj->getPrefixedText() ) . '" /> ' . ( $wgContLang->isRTL() ? '&rlm;' : '&lrm;' );
- if( $redir ) {
- $spanopen = '<span class="watchlistredir">';
- $spanclosed = '</span>';
- } else {
- $spanopen = $spanclosed = '';
- }
-
- $wgOut->addHTML( "<li>{$checkbox}{$spanopen}{$pageLink}{$spanclosed} {$toolLinks}</li>\n" );
- }
- }
- $wgOut->addHTML( '</ul>' );
- }
- $wgOut->addHTML(
- wfSubmitButton( wfMsg('removechecked'), array('name' => 'remove') ) .
- "\n</form>\n"
- );
-
- return;
}
# If the watchlist is relatively short, it's simplest to zip
@@ -257,17 +161,17 @@ function wfSpecialWatchlist( $par ) {
$andLatest='';
$limitWatchlist = 'LIMIT ' . intval( $wgUser->getOption( 'wllimit' ) );
} else {
- # Top log Ids for a page are not stored
- $andLatest= 'AND (rc_this_oldid=page_latest OR rc_type=' . RC_LOG . ') ';
+ $andLatest= 'AND rc_this_oldid=page_latest';
$limitWatchlist = '';
}
- # TODO: Consider removing the third parameter
- $header .= wfMsgExt( 'watchdetails', array( 'parsemag' ), $wgLang->formatNum( $nitems ),
- $wgLang->formatNum( $npages ), '',
- $specialTitle->getFullUrl( 'edit=yes' ) );
+ $header .= wfMsgExt( 'watchlist-details', array( 'parsemag' ), $wgLang->formatNum( $nitems ) );
$wgOut->addWikiText( $header );
+ # Show a message about slave lag, if applicable
+ if( ( $lag = $dbr->getLag() ) > 0 )
+ $wgOut->showLagWarning( $lag );
+
if ( $wgEnotifWatchlist && $wgShowUpdatedMarker ) {
$wgOut->addHTML( '<form action="' .
$specialTitle->escapeLocalUrl() .
@@ -460,54 +364,4 @@ function wlCountItems( &$user, $talk = true ) {
$count = floor( $count / 2 );
return( $count );
-}
-
-/**
- * Allow the user to clear their watchlist
- *
- * @param $out Output object
- * @param $request Request object
- * @param $par Parameters passed to the watchlist page
- * @return bool True if it's been taken care of; false indicates the watchlist
- * code needs to do something further
- */
-function wlHandleClear( &$out, &$request, $par ) {
- global $wgLang;
-
- # Check this function has something to do
- if( $request->getText( 'action' ) == 'clear' || $par == 'clear' ) {
- global $wgUser;
- $out->setPageTitle( wfMsgHtml( 'clearwatchlist' ) );
- $count = wlCountItems( $wgUser );
- if( $count > 0 ) {
- # See if we're clearing or confirming
- if( $request->wasPosted() && $wgUser->matchEditToken( $request->getText( 'token' ), 'clearwatchlist' ) ) {
- # Clearing, so do it and report the result
- $dbw = wfGetDB( DB_MASTER );
- $dbw->delete( 'watchlist', array( 'wl_user' => $wgUser->mId ), 'wlHandleClear' );
- $out->addWikiText( wfMsgExt( 'watchlistcleardone', array( 'parsemag', 'escape'), $wgLang->formatNum( $count ) ) );
- $out->returnToMain();
- } else {
- # Confirming, so show a form
- $wlTitle = SpecialPage::getTitleFor( 'Watchlist' );
- $out->addHTML( wfElement( 'form', array( 'method' => 'post', 'action' => $wlTitle->getLocalUrl( 'action=clear' ) ), NULL ) );
- $out->addWikiText( wfMsgExt( 'watchlistcount', array( 'parsemag', 'escape'), $wgLang->formatNum( $count ) ) );
- $out->addWikiText( wfMsg( 'watchlistcleartext' ) );
- $out->addHTML(
- wfHidden( 'token', $wgUser->editToken( 'clearwatchlist' ) ) .
- wfElement( 'input', array( 'type' => 'submit', 'name' => 'submit', 'value' => wfMsgHtml( 'watchlistclearbutton' ) ), '' ) .
- wfCloseElement( 'form' )
- );
- }
- return( true );
- } else {
- # Nothing on the watchlist; nothing to do here
- $out->addWikiText( wfMsg( 'nowatchlist' ) );
- $out->returnToMain();
- return( true );
- }
- } else {
- return( false );
- }
-}
-?>
+} \ No newline at end of file
diff --git a/includes/SpecialWhatlinkshere.php b/includes/SpecialWhatlinkshere.php
index 277e279f..d944f6b4 100644
--- a/includes/SpecialWhatlinkshere.php
+++ b/includes/SpecialWhatlinkshere.php
@@ -56,7 +56,8 @@ class WhatLinksHerePage {
}
$this->selfTitle = Title::makeTitleSafe( NS_SPECIAL,
'Whatlinkshere/' . $this->target->getPrefixedDBkey() );
- $wgOut->setPagetitle( $this->target->getPrefixedText() );
+
+ $wgOut->setPageTitle( wfMsg( 'whatlinkshere-title', $this->target->getPrefixedText() ) );
$wgOut->setSubtitle( wfMsg( 'linklistsub' ) );
$wgOut->addHTML( wfMsg( 'whatlinkshere-barrow' ) . ' ' .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n");
@@ -105,25 +106,29 @@ class WhatLinksHerePage {
}
if ( $from ) {
- $offsetCond = "page_id >= $from";
- } else {
- $offsetCond = false;
- }
- $options['ORDER BY'] = 'page_id';
+ $from = (int)$from; // just in case
+ $tlConds[] = "tl_from >= $from";
+ $plConds[] = "pl_from >= $from";
+ }
// Read an extra row as an at-end check
$queryLimit = $limit + 1;
+
+ // enforce join order, sometimes namespace selector may
+ // trigger filesorts which are far less efficient than scanning many entries
+ $options[] = 'STRAIGHT_JOIN';
+
$options['LIMIT'] = $queryLimit;
- if ( $offsetCond ) {
- $tlConds[] = $offsetCond;
- $plConds[] = $offsetCond;
- }
$fields = array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' );
+ $options['ORDER BY'] = 'pl_from';
$plRes = $dbr->select( array( 'pagelinks', 'page' ), $fields,
$plConds, $fname, $options );
+
+ $options['ORDER BY'] = 'tl_from';
$tlRes = $dbr->select( array( 'templatelinks', 'page' ), $fields,
$tlConds, $fname, $options );
+
if ( !$dbr->numRows( $plRes ) && !$dbr->numRows( $tlRes ) ) {
if ( 0 == $level && !isset( $this->namespace ) ) {
// really no links to here
@@ -226,6 +231,14 @@ class WhatLinksHerePage {
$wgOut->addHTML( ' (' . implode( ', ', $props ) . ') ' );
}
+ # Space for utilities links, with a what-links-here link provided
+ $wlh = $this->skin->makeKnownLinkObj(
+ SpecialPage::getTitleFor( 'Whatlinkshere' ),
+ wfMsgHtml( 'whatlinkshere-links' ),
+ 'target=' . $nt->getPrefixedUrl()
+ );
+ $wgOut->addHtml( ' <span class="mw-whatlinkshere-tools">(' . $wlh . ')</span>' );
+
if ( $row->page_is_redirect ) {
if ( $level < 2 ) {
$this->showIndirectLinks( $level + 1, $nt, 500 );
@@ -312,4 +325,4 @@ class WhatLinksHerePage {
}
-?>
+
diff --git a/includes/SpecialWithoutinterwiki.php b/includes/SpecialWithoutinterwiki.php
index e5341d5d..33464586 100644
--- a/includes/SpecialWithoutinterwiki.php
+++ b/includes/SpecialWithoutinterwiki.php
@@ -12,9 +12,9 @@ class WithoutInterwikiPage extends PageQueryPage {
function getName() {
return 'Withoutinterwiki';
}
-
+
function getPageHeader() {
- return '<p>' . wfMsgHtml( 'withoutinterwiki-header' ) . '</p>';
+ return '<p>' . wfMsgExt( 'withoutinterwiki-header', array( 'parseinline' ) ) . '</p>';
}
function sortDescending() {
@@ -24,7 +24,7 @@ class WithoutInterwikiPage extends PageQueryPage {
function isExpensive() {
return true;
}
-
+
function isSyndicated() {
return false;
}
@@ -44,7 +44,7 @@ class WithoutInterwikiPage extends PageQueryPage {
AND page_namespace=" . NS_MAIN . "
AND page_is_redirect = 0";
}
-
+
}
function wfSpecialWithoutinterwiki() {
@@ -53,4 +53,4 @@ function wfSpecialWithoutinterwiki() {
$wip->doQuery( $offset, $limit );
}
-?>
+
diff --git a/includes/SquidUpdate.php b/includes/SquidUpdate.php
index 700fc8ef..5d7350a9 100644
--- a/includes/SquidUpdate.php
+++ b/includes/SquidUpdate.php
@@ -22,7 +22,7 @@ class SquidUpdate {
$this->urlArr = $urlArr;
}
- /* static */ function newFromLinksTo( &$title ) {
+ static function newFromLinksTo( &$title ) {
$fname = 'SquidUpdate::newFromLinksTo';
wfProfileIn( $fname );
@@ -49,7 +49,7 @@ class SquidUpdate {
return new SquidUpdate( $blurlArr );
}
- /* static */ function newFromTitles( &$titles, $urlArr = array() ) {
+ static function newFromTitles( &$titles, $urlArr = array() ) {
global $wgMaxSquidPurgeTitles;
if ( count( $titles ) > $wgMaxSquidPurgeTitles ) {
$titles = array_slice( $titles, 0, $wgMaxSquidPurgeTitles );
@@ -60,7 +60,7 @@ class SquidUpdate {
return new SquidUpdate( $urlArr );
}
- /* static */ function newSimplePurge( &$title ) {
+ static function newSimplePurge( &$title ) {
$urlArr = $title->getSquidURLs();
return new SquidUpdate( $urlArr );
}
@@ -74,16 +74,21 @@ class SquidUpdate {
(example: $urlArr[] = 'http://my.host/something')
XXX report broken Squids per mail or log */
- /* static */ function purge( $urlArr ) {
+ static function purge( $urlArr ) {
global $wgSquidServers, $wgHTCPMulticastAddress, $wgHTCPPort;
/*if ( (@$wgSquidServers[0]) == 'echo' ) {
echo implode("<br />\n", $urlArr) . "<br />\n";
return;
}*/
+
+ if( empty( $urlArr ) ) {
+ return;
+ }
- if ( $wgHTCPMulticastAddress && $wgHTCPPort )
- SquidUpdate::HTCPPurge( $urlArr );
+ if ( $wgHTCPMulticastAddress && $wgHTCPPort ) {
+ return SquidUpdate::HTCPPurge( $urlArr );
+ }
$fname = 'SquidUpdate::purge';
wfProfileIn( $fname );
@@ -189,7 +194,7 @@ class SquidUpdate {
wfProfileOut( $fname );
}
- /* static */ function HTCPPurge( $urlArr ) {
+ static function HTCPPurge( $urlArr ) {
global $wgHTCPMulticastAddress, $wgHTCPMulticastTTL, $wgHTCPPort;
$fname = 'SquidUpdate::HTCPPurge';
wfProfileIn( $fname );
@@ -277,4 +282,4 @@ class SquidUpdate {
return $url;
}
}
-?>
+
diff --git a/includes/StreamFile.php b/includes/StreamFile.php
index dc653e57..8ecaa4f0 100644
--- a/includes/StreamFile.php
+++ b/includes/StreamFile.php
@@ -31,6 +31,9 @@ function wfStreamFile( $fname ) {
header('Content-type: application/x-wiki');
}
+ global $wgContLanguageCode;
+ header( "Content-Disposition: inline;filename*=utf-8'$wgContLanguageCode'" . urlencode( basename( $fname ) ) );
+
if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
$modsince = preg_replace( '/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE'] );
$sinceTime = strtotime( $modsince );
@@ -69,4 +72,4 @@ function wfGetType( $filename ) {
}
}
-?>
+
diff --git a/includes/StringUtils.php b/includes/StringUtils.php
index 9a451aa8..374fb002 100644
--- a/includes/StringUtils.php
+++ b/includes/StringUtils.php
@@ -300,4 +300,4 @@ class ReplacementArray {
}
}
-?>
+
diff --git a/includes/StubObject.php b/includes/StubObject.php
index 894550cd..a9a6bde9 100644
--- a/includes/StubObject.php
+++ b/includes/StubObject.php
@@ -134,4 +134,4 @@ class StubUser extends StubObject {
}
}
-?>
+
diff --git a/includes/Title.php b/includes/Title.php
index 0ff2e807..c4db4172 100644
--- a/includes/Title.php
+++ b/includes/Title.php
@@ -40,13 +40,14 @@ class Title {
* Please use the accessor functions
*/
- /**#@+
+ /**#@+
* @private
*/
var $mTextform; # Text form (spaces not underscores) of the main part
var $mUrlform; # URL-encoded form of the main part
var $mDbkeyform; # Main part with underscores
+ var $mUserCaseDBKey; # DB key with the initial letter in the case specified by the user
var $mNamespace; # Namespace index, i.e. one of the NS_xxxx constants
var $mInterwiki; # Interwiki prefix (or null string)
var $mFragment; # Title fragment (i.e. the bit after the #)
@@ -232,7 +233,7 @@ class Title {
$t = new Title();
$t->mInterwiki = '';
$t->mFragment = '';
- $t->mNamespace = intval( $ns );
+ $t->mNamespace = $ns = intval( $ns );
$t->mDbkeyform = str_replace( ' ', '_', $title );
$t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
$t->mUrlform = wfUrlencode( $t->mDbkeyform );
@@ -268,32 +269,33 @@ class Title {
}
/**
- * Create a new Title for a redirect
- * @param string $text the redirect title text
- * @return Title the new object, or NULL if the text is not a
- * valid redirect
+ * Extract a redirect destination from a string and return the
+ * Title, or null if the text doesn't contain a valid redirect
+ *
+ * @param string $text Text with possible redirect
+ * @return Title
*/
public static function newFromRedirect( $text ) {
- $mwRedir = MagicWord::get( 'redirect' );
- $rt = NULL;
- if ( $mwRedir->matchStart( $text ) ) {
+ $redir = MagicWord::get( 'redirect' );
+ if( $redir->matchStart( $text ) ) {
+ // Extract the first link and see if it's usable
$m = array();
- if ( preg_match( '/\[{2}(.*?)(?:\||\]{2})/', $text, $m ) ) {
- # categories are escaped using : for example one can enter:
- # #REDIRECT [[:Category:Music]]. Need to remove it.
- if ( substr($m[1],0,1) == ':') {
- # We don't want to keep the ':'
- $m[1] = substr( $m[1], 1 );
- }
-
- $rt = Title::newFromText( $m[1] );
- # Disallow redirects to Special:Userlogout
- if ( !is_null($rt) && $rt->isSpecial( 'Userlogout' ) ) {
- $rt = NULL;
+ if( preg_match( '!\[{2}(.*?)(?:\||\]{2})!', $text, $m ) ) {
+ // Strip preceding colon used to "escape" categories, etc.
+ // and URL-decode links
+ if( strpos( $m[1], '%' ) !== false ) {
+ // Match behavior of inline link parsing here;
+ // don't interpret + as " " most of the time!
+ // It might be safe to just use rawurldecode instead, though.
+ $m[1] = urldecode( ltrim( $m[1], ':' ) );
}
+ $title = Title::newFromText( $m[1] );
+ // Redirects to Special:Userlogout are not permitted
+ if( $title instanceof Title && !$title->isSpecial( 'Userlogout' ) )
+ return $title;
}
}
- return $rt;
+ return null;
}
#----------------------------------------------------------------------------
@@ -556,6 +558,12 @@ class Title {
return $wgContLang->getNsText( $this->mNamespace );
}
/**
+ * Get the DB key with the initial letter case as specified by the user
+ */
+ function getUserCaseDBKey() {
+ return $this->mUserCaseDBKey;
+ }
+ /**
* Get the namespace text of the subject (rather than talk) page
* @return string
*/
@@ -988,6 +996,23 @@ class Title {
return $this->userCan( $action, false );
}
+ /**
+ * Determines if $wgUser is unable to edit this page because it has been protected
+ * by $wgNamespaceProtection.
+ *
+ * @return boolean
+ */
+ public function isNamespaceProtected() {
+ global $wgNamespaceProtection, $wgUser;
+ if( isset( $wgNamespaceProtection[ $this->mNamespace ] ) ) {
+ foreach( (array)$wgNamespaceProtection[ $this->mNamespace ] as $right ) {
+ if( $right != '' && !$wgUser->isAllowed( $right ) )
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Can $wgUser perform $action on this page?
* @param string $action action that permission needs to be checked for
@@ -995,48 +1020,124 @@ class Title {
* @return boolean
*/
public function userCan( $action, $doExpensiveQueries = true ) {
+ global $wgUser;
+ return ( $this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries ) === array());
+ }
+
+ /**
+ * Can $user perform $action on this page?
+ * @param string $action action that permission needs to be checked for
+ * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries.
+ * @return array Array of arrays of the arguments to wfMsg to explain permissions problems.
+ */
+ public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true ) {
+ $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries );
+
+ global $wgContLang;
+ global $wgLang;
+
+ if ( wfReadOnly() && $action != 'read' ) {
+ global $wgReadOnly;
+ $errors[] = array( 'readonlytext', $wgReadOnly );
+ }
+
+ global $wgEmailConfirmToEdit, $wgUser;
+
+ if ( $wgEmailConfirmToEdit && !$wgUser->isEmailConfirmed() )
+ {
+ $errors[] = array( 'confirmedittext' );
+ }
+
+ if ( $user->isBlockedFrom( $this ) ) {
+ $block = $user->mBlock;
+
+ // This is from OutputPage::blockedPage
+ // Copied at r23888 by werdna
+
+ $id = $user->blockedBy();
+ $reason = $user->blockedFor();
+ $ip = wfGetIP();
+
+ if ( is_numeric( $id ) ) {
+ $name = User::whoIs( $id );
+ } else {
+ $name = $id;
+ }
+
+ $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]";
+ $blockid = $block->mId;
+ $blockExpiry = $user->mBlock->mExpiry;
+ $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $wgUser->mBlock->mTimestamp ), true );
+
+ if ( $blockExpiry == 'infinity' ) {
+ // Entry in database (table ipblocks) is 'infinity' but 'ipboptions' uses 'infinite' or 'indefinite'
+ $scBlockExpiryOptions = wfMsg( 'ipboptions' );
+
+ foreach ( explode( ',', $scBlockExpiryOptions ) as $option ) {
+ if ( strpos( $option, ':' ) == false )
+ continue;
+
+ list ($show, $value) = explode( ":", $option );
+
+ if ( $value == 'infinite' || $value == 'indefinite' ) {
+ $blockExpiry = $show;
+ break;
+ }
+ }
+ } else {
+ $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true );
+ }
+
+ $intended = $user->mBlock->mAddress;
+
+ $errors[] = array ( ($block->mAuto ? 'autoblockedtext' : 'blockedtext'), $link, $reason, $ip, $name, $blockid, $blockExpiry, $intended, $blockTimestamp );
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Can $user perform $action on this page?
+ * This is an internal function, which checks ONLY that previously checked by userCan (i.e. it leaves out checks on wfReadOnly() and blocks)
+ * @param string $action action that permission needs to be checked for
+ * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries.
+ * @return array Array of arrays of the arguments to wfMsg to explain permissions problems.
+ */
+ private function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true ) {
$fname = 'Title::userCan';
wfProfileIn( $fname );
- global $wgUser, $wgNamespaceProtection;
+ $errors = array();
- $result = null;
- wfRunHooks( 'userCan', array( &$this, &$wgUser, $action, &$result ) );
- if ( $result !== null ) {
- wfProfileOut( $fname );
- return $result;
+ if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) {
+ return $result ? array() : array( array( 'badaccess-group0' ) );
}
if( NS_SPECIAL == $this->mNamespace ) {
- wfProfileOut( $fname );
- return false;
+ $errors[] = array('ns-specialprotected');
}
- if ( array_key_exists( $this->mNamespace, $wgNamespaceProtection ) ) {
- $nsProt = $wgNamespaceProtection[ $this->mNamespace ];
- if ( !is_array($nsProt) ) $nsProt = array($nsProt);
- foreach( $nsProt as $right ) {
- if( '' != $right && !$wgUser->isAllowed( $right ) ) {
- wfProfileOut( $fname );
- return false;
- }
- }
+ if ( $this->isNamespaceProtected() ) {
+ $ns = $this->getNamespace() == NS_MAIN
+ ? wfMsg( 'nstab-main' )
+ : $this->getNsText();
+ $errors[] = (NS_MEDIAWIKI == $this->mNamespace
+ ? array('protectedinterface')
+ : array( 'namespaceprotected', $ns ) );
}
if( $this->mDbkeyform == '_' ) {
# FIXME: Is this necessary? Shouldn't be allowed anyway...
- wfProfileOut( $fname );
- return false;
+ $errors[] = array('badaccess-group0');
}
# protect css/js subpages of user pages
# XXX: this might be better using restrictions
# XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssJsSubpage() from working
if( $this->isCssJsSubpage()
- && !$wgUser->isAllowed('editinterface')
- && !preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ) {
- wfProfileOut( $fname );
- return false;
+ && !$user->isAllowed('editinterface')
+ && !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) ) {
+ $errors[] = array('customcssjsprotected');
}
if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) {
@@ -1052,9 +1153,11 @@ class Title {
if( $cascadingSources > 0 && isset($restrictions[$action]) ) {
foreach( $restrictions[$action] as $right ) {
$right = ( $right == 'sysop' ) ? 'protect' : $right;
- if( '' != $right && !$wgUser->isAllowed( $right ) ) {
- wfProfileOut( $fname );
- return false;
+ if( '' != $right && !$user->isAllowed( $right ) ) {
+ $pages = '';
+ foreach( $cascadingSources as $page )
+ $pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
+ $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages );
}
}
}
@@ -1065,28 +1168,50 @@ class Title {
if ( $right == 'sysop' ) {
$right = 'protect';
}
- if( '' != $right && !$wgUser->isAllowed( $right ) ) {
- wfProfileOut( $fname );
- return false;
+ if( '' != $right && !$user->isAllowed( $right ) ) {
+ $errors[] = array( 'protectedpagetext' );
}
}
- if( $action == 'move' &&
- !( $this->isMovable() && $wgUser->isAllowed( 'move' ) ) ) {
- wfProfileOut( $fname );
- return false;
- }
-
if( $action == 'create' ) {
- if( ( $this->isTalkPage() && !$wgUser->isAllowed( 'createtalk' ) ) ||
- ( !$this->isTalkPage() && !$wgUser->isAllowed( 'createpage' ) ) ) {
- wfProfileOut( $fname );
- return false;
+ if( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
+ ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) {
+ $errors[] = $user->isAnon() ? array ('nocreatetext') : array ('nocreate-loggedin');
}
+ } elseif( $action == 'move' && !( $this->isMovable() && $user->isAllowed( 'move' ) ) ) {
+ $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed');
+ } else if ( !$user->isAllowed( $action ) ) {
+ $return = null;
+ $groups = array();
+ global $wgGroupPermissions;
+ foreach( $wgGroupPermissions as $key => $value ) {
+ if( isset( $value[$action] ) && $value[$action] == true ) {
+ $groupName = User::getGroupName( $key );
+ $groupPage = User::getGroupPage( $key );
+ if( $groupPage ) {
+ $skin = $user->getSkin();
+ $groups[] = '[['.$groupPage->getPrefixedText().'|'.$groupName.']]';
+ } else {
+ $groups[] = $groupName;
+ }
+ }
+ }
+ $n = count( $groups );
+ $groups = implode( ', ', $groups );
+ switch( $n ) {
+ case 0:
+ case 1:
+ case 2:
+ $return = array( "badaccess-group$n", $groups );
+ break;
+ default:
+ $return = array( 'badaccess-groups', $groups );
+ }
+ $errors[] = $return;
}
wfProfileOut( $fname );
- return true;
+ return $errors;
}
/**
@@ -1141,7 +1266,7 @@ class Title {
return $result;
}
- if( $wgUser->isAllowed('read') ) {
+ if( $wgUser->isAllowed( 'read' ) ) {
return true;
} else {
global $wgWhitelistRead;
@@ -1153,19 +1278,35 @@ class Title {
if( $this->isSpecial( 'Userlogin' ) || $this->isSpecial( 'Resetpass' ) ) {
return true;
}
-
- /** some pages are explicitly allowed */
+
+ /**
+ * Check for explicit whitelisting
+ */
$name = $this->getPrefixedText();
- if( $wgWhitelistRead && in_array( $name, $wgWhitelistRead ) ) {
+ if( $wgWhitelistRead && in_array( $name, $wgWhitelistRead, true ) )
return true;
- }
-
- # Compatibility with old settings
+
+ /**
+ * Old settings might have the title prefixed with
+ * a colon for main-namespace pages
+ */
if( $wgWhitelistRead && $this->getNamespace() == NS_MAIN ) {
- if( in_array( ':' . $name, $wgWhitelistRead ) ) {
+ if( in_array( ':' . $name, $wgWhitelistRead ) )
return true;
- }
}
+
+ /**
+ * If it's a special page, ditch the subpage bit
+ * and check again
+ */
+ if( $this->getNamespace() == NS_SPECIAL ) {
+ $name = $this->getText();
+ list( $name, /* $subpage */) = SpecialPage::resolveAliasWithSubpage( $name );
+ $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
+ if( in_array( $pure, $wgWhitelistRead, true ) )
+ return true;
+ }
+
}
return false;
}
@@ -1191,6 +1332,17 @@ class Title {
return false;
}
}
+
+ /**
+ * Could this page contain custom CSS or JavaScript, based
+ * on the title?
+ *
+ * @return bool
+ */
+ public function isCssOrJsPage() {
+ return $this->mNamespace == NS_MEDIAWIKI
+ && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
+ }
/**
* Is this a .css or .js subpage of a user page?
@@ -1251,7 +1403,7 @@ class Title {
* @return bool If the page is subject to cascading restrictions.
*/
public function isCascadeProtected() {
- list( $sources, $restrictions ) = $this->getCascadeProtectionSources( false );
+ list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
return ( $sources > 0 );
}
@@ -1749,6 +1901,7 @@ class Title {
* Don't force it for interwikis, since the other
* site might be case-sensitive.
*/
+ $this->mUserCaseDBKey = $dbkey;
if( $wgCapitalLinks && $this->mInterwiki == '') {
$dbkey = $wgContLang->ucfirst( $dbkey );
}
@@ -1763,7 +1916,14 @@ class Title {
$this->mNamespace != NS_MAIN ) {
return false;
}
-
+ // Allow IPv6 usernames to start with '::' by canonicalizing IPv6 titles.
+ // IP names are not allowed for accounts, and can only be referring to
+ // edits from the IP. Given '::' abbreviations and caps/lowercaps,
+ // there are numerous ways to present the same IP. Having sp:contribs scan
+ // them all is silly and having some show the edits and others not is
+ // inconsistent. Same for talk/userpages. Keep them normalized instead.
+ $dbkey = ($this->mNamespace == NS_USER || $this->mNamespace == NS_USER_TALK) ?
+ IP::sanitizeIP( $dbkey ) : $dbkey;
// Any remaining initial :s are illegal.
if ( $dbkey !== '' && ':' == $dbkey{0} ) {
return false;
@@ -2270,6 +2430,16 @@ class Title {
# Return true if there was no history
return $row === false;
}
+
+ /**
+ * Can this title be added to a user's watchlist?
+ *
+ * @return bool
+ */
+ public function isWatchable() {
+ return !$this->isExternal()
+ && Namespace::isWatchable( $this->getNamespace() );
+ }
/**
* Get categories to which this Title belongs and return an array of
@@ -2394,6 +2564,15 @@ class Title {
&& $this->getNamespace() == $title->getNamespace()
&& $this->getDbkey() === $title->getDbkey();
}
+
+ /**
+ * Return a string representation of this title
+ *
+ * @return string
+ */
+ public function __toString() {
+ return $this->getPrefixedText();
+ }
/**
* Check if page exists
@@ -2404,14 +2583,15 @@ class Title {
}
/**
- * Should a link should be displayed as a known link, just based on its title?
+ * Do we know that this title definitely exists, or should we otherwise
+ * consider that it exists?
*
- * Currently, a self-link with a fragment and special pages are in
- * this category. Special pages never exist in the database.
+ * @return bool
*/
public function isAlwaysKnown() {
- return $this->isExternal() || ( 0 == $this->mNamespace && "" == $this->mDbkeyform )
- || NS_SPECIAL == $this->mNamespace;
+ return $this->isExternal()
+ || ( $this->mNamespace == NS_MAIN && $this->mDbkeyform == '' )
+ || ( $this->mNamespace == NS_MEDIAWIKI && wfMsgWeirdKey( $this->mDbkeyform ) );
}
/**
@@ -2551,4 +2731,4 @@ class Title {
}
-?>
+
diff --git a/includes/User.php b/includes/User.php
index 4ecd49de..51b0b2ec 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -11,9 +11,7 @@ define( 'USER_TOKEN_LENGTH', 32 );
define( 'MW_USER_VERSION', 5 );
# Some punctuation to prevent editing from broken text-mangling proxies.
-# FIXME: this is embedded unescaped into HTML attributes in various
-# places, so we can't safely include ' or " even though we really should.
-define( 'EDIT_TOKEN_SUFFIX', '\\' );
+define( 'EDIT_TOKEN_SUFFIX', '+\\' );
/**
* Thrown by User::setPassword() on error
@@ -36,8 +34,8 @@ class PasswordError extends MWException {
class User {
/**
- * A list of default user toggles, i.e. boolean user preferences that are
- * displayed by Special:Preferences as checkboxes. This list can be
+ * A list of default user toggles, i.e. boolean user preferences that are
+ * displayed by Special:Preferences as checkboxes. This list can be
* extended via the UserToggles hook or $wgContLang->getExtraUserToggles().
*/
static public $mToggles = array(
@@ -82,7 +80,7 @@ class User {
/**
* List of member variables which are saved to the shared cache (memcached).
- * Any operation which changes the corresponding database fields must
+ * Any operation which changes the corresponding database fields must
* call a cache-clearing function.
*/
static $mCacheVars = array(
@@ -109,8 +107,8 @@ class User {
/**
* The cache variable declarations
*/
- var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
- $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
+ var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
+ $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
$mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
/**
@@ -135,7 +133,7 @@ class User {
var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
$mBlockreason, $mBlock, $mEffectiveGroups;
- /**
+ /**
* Lightweight constructor for anonymous user
* Use the User::newFrom* factory functions for other kinds of users
*/
@@ -190,7 +188,7 @@ class User {
if ( $this->mId == 0 ) {
$this->loadDefaults();
return false;
- }
+ }
# Try cache
$key = wfMemcKey( 'user', 'id', $this->mId );
@@ -199,7 +197,7 @@ class User {
# Object is expired, load from DB
$data = false;
}
-
+
if ( !$data ) {
wfDebug( "Cache miss for user {$this->mId}\n" );
# Load from DB
@@ -229,16 +227,16 @@ class User {
* Static factory method for creation from username.
*
* This is slightly less efficient than newFromId(), so use newFromId() if
- * you have both an ID and a name handy.
+ * you have both an ID and a name handy.
*
* @param string $name Username, validated by Title:newFromText()
- * @param mixed $validate Validate username. Takes the same parameters as
- * User::getCanonicalName(), except that true is accepted as an alias
+ * @param mixed $validate Validate username. Takes the same parameters as
+ * User::getCanonicalName(), except that true is accepted as an alias
* for 'valid', for BC.
- *
- * @return User object, or null if the username is invalid. If the username
+ *
+ * @return User object, or null if the username is invalid. If the username
* is not present in the database, the result will be a user object with
- * a name, zero user ID and default settings.
+ * a name, zero user ID and default settings.
* @static
*/
static function newFromName( $name, $validate = 'valid' ) {
@@ -287,7 +285,7 @@ class User {
return null;
}
}
-
+
/**
* Create a new user object using data from session or cookies. If the
* login credentials are invalid, the result is an anonymous user.
@@ -313,14 +311,14 @@ class User {
}
/**
- * Get real username given an id.
- * @param integer $id Database user id
- * @return string Realname of a user
- * @static
+ * Get the real name of a user given their identifier
+ *
+ * @param int $id Database user id
+ * @return string Real name of a user
*/
static function whoIsReal( $id ) {
$dbr = wfGetDB( DB_SLAVE );
- return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' );
+ return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
}
/**
@@ -350,9 +348,9 @@ class User {
*
* This function exists for username validation, in order to reject
* usernames which are similar in form to IP addresses. Strings such
- * as 300.300.300.300 will return true because it looks like an IP
+ * as 300.300.300.300 will return true because it looks like an IP
* address, despite not being strictly valid.
- *
+ *
* We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
* address because the usemod software would "cloak" anonymous IP
* addresses like this, if we allowed accounts like this to be created
@@ -376,8 +374,8 @@ class User {
* Check if $name is an IPv6 IP.
*/
static function isIPv6($name) {
- /*
- * if it has any non-valid characters, it can't be a valid IPv6
+ /*
+ * if it has any non-valid characters, it can't be a valid IPv6
* address.
*/
if (preg_match("/[^:a-fA-F0-9]/", $name))
@@ -422,7 +420,7 @@ class User {
|| $parsed->getNamespace()
|| strcmp( $name, $parsed->getPrefixedText() ) )
return false;
-
+
// Check an additional blacklist of troublemaker characters.
// Should these be merged into the title char list?
$unicodeBlacklist = '/[' .
@@ -436,10 +434,10 @@ class User {
if( preg_match( $unicodeBlacklist, $name ) ) {
return false;
}
-
+
return true;
}
-
+
/**
* Usernames which fail to pass this function will be blocked
* from user login and new account registrations, but may be used
@@ -454,13 +452,13 @@ class User {
static function isUsableName( $name ) {
global $wgReservedUsernames;
return
- // Must be a usable username, obviously ;)
+ // Must be a valid username, obviously ;)
self::isValidUserName( $name ) &&
-
+
// Certain names may be reserved for batch processes.
!in_array( $name, $wgReservedUsernames );
}
-
+
/**
* Usernames which fail to pass this function will be blocked
* from new account registrations, but may be used internally
@@ -477,29 +475,33 @@ class User {
static function isCreatableName( $name ) {
return
self::isUsableName( $name ) &&
-
+
// Registration-time character blacklisting...
strpos( $name, '@' ) === false;
}
/**
- * Is the input a valid password?
+ * Is the input a valid password for this user?
*
- * @param string $password
+ * @param string $password Desired password
* @return bool
*/
function isValidPassword( $password ) {
global $wgMinimalPasswordLength, $wgContLang;
$result = null;
- if( !wfRunHooks( 'isValidPassword', array( $password, &$result ) ) ) return $result;
- if ($result === false) return false;
- return (strlen( $password ) >= $wgMinimalPasswordLength) &&
- ($wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName ));
+ if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
+ return $result;
+ if( $result === false )
+ return false;
+
+ // Password needs to be long enough, and can't be the same as the username
+ return strlen( $password ) >= $wgMinimalPasswordLength
+ && $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName );
}
/**
- * Does the string match roughly an email address ?
+ * Does a string look like an email address?
*
* There used to be a regular expression here, it got removed because it
* rejected valid addresses. Actually just check if there is '@' somewhere
@@ -508,16 +510,14 @@ class User {
* @todo Check for RFC 2822 compilance (bug 959)
*
* @param string $addr email address
- * @static
* @return bool
*/
- static function isValidEmailAddr ( $addr ) {
- return ( trim( $addr ) != '' ) &&
- (false !== strpos( $addr, '@' ) );
+ public static function isValidEmailAddr( $addr ) {
+ return strpos( $addr, '@' ) !== false;
}
/**
- * Given unvalidated user input, return a canonical username, or false if
+ * Given unvalidated user input, return a canonical username, or false if
* the username is invalid.
* @param string $name
* @param mixed $validate Type of validation to use:
@@ -531,6 +531,12 @@ class User {
global $wgContLang;
$name = $wgContLang->ucfirst( $name );
+ # Reject names containing '#'; these will be cleaned up
+ # with title normalisation, but then it's too late to
+ # check elsewhere
+ if( strpos( $name, '#' ) !== false )
+ return false;
+
# Clean up name according to title rules
$t = Title::newFromText( $name );
if( is_null( $t ) ) {
@@ -570,7 +576,7 @@ class User {
* Count the number of edits of a user
*
* It should not be static and some day should be merged as proper member function / deprecated -- domas
- *
+ *
* @param int $uid The user ID to check
* @return int
* @static
@@ -627,7 +633,7 @@ class User {
}
/**
- * Set cached properties to default. Note: this no longer clears
+ * Set cached properties to default. Note: this no longer clears
* uncached lazy-initialised properties. The constructor does that instead.
*
* @private
@@ -660,7 +666,7 @@ class User {
wfProfileOut( __METHOD__ );
}
-
+
/**
* Initialise php session
* @deprecated use wfSetupSession()
@@ -707,7 +713,7 @@ class User {
# Not a valid ID, loadFromId has switched the object to anon for us
return false;
}
-
+
if ( isset( $_SESSION['wsToken'] ) ) {
$passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
$from = 'session';
@@ -721,6 +727,7 @@ class User {
}
if ( ( $sName == $this->mName ) && $passwordCorrect ) {
+ $_SESSION['wsToken'] = $this->mToken;
wfDebug( "Logged in from $from\n" );
return true;
} else {
@@ -730,11 +737,11 @@ class User {
return false;
}
}
-
+
/**
* Load user and user_group data from the database
* $this->mId must be set, this is how the user is identified.
- *
+ *
* @return true if the user exists, false if the user is anonymous
* @private
*/
@@ -766,7 +773,7 @@ class User {
$this->mEmailToken = $s->user_email_token;
$this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $s->user_email_token_expires );
$this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
- $this->mEditCount = $s->user_editcount;
+ $this->mEditCount = $s->user_editcount;
$this->getEditCount(); // revalidation for nulls
# Load group data
@@ -788,9 +795,9 @@ class User {
}
/**
- * Clear various cached data stored in this object.
- * @param string $reloadFrom Reload user and user_groups table data from a
- * given source. May be "name", "id", "defaults", "session" or false for
+ * Clear various cached data stored in this object.
+ * @param string $reloadFrom Reload user and user_groups table data from a
+ * given source. May be "name", "id", "defaults", "session" or false for
* no reload.
*/
function clearInstanceCache( $reloadFrom = false ) {
@@ -884,7 +891,7 @@ class User {
wfProfileIn( __METHOD__ );
wfDebug( __METHOD__.": checking...\n" );
- $this->mBlockedby = 0;
+ $this->mBlockedby = 0;
$this->mHideName = 0;
$ip = wfGetIP();
@@ -1112,9 +1119,16 @@ class User {
/**
* Get the user ID. Returns 0 if the user is anonymous or nonexistent.
*/
- function getID() {
- $this->load();
- return $this->mId;
+ function getID() {
+ if( $this->mId === null and $this->mName !== null
+ and User::isIP( $this->mName ) ) {
+ // Special case, we know the user is anonymous
+ return 0;
+ } elseif( $this->mId === null ) {
+ // Don't load if this was initialized from an ID
+ $this->load();
+ }
+ return $this->mId;
}
/**
@@ -1144,12 +1158,12 @@ class User {
}
/**
- * Set the user name.
+ * Set the user name.
*
- * This does not reload fields from the database according to the given
+ * This does not reload fields from the database according to the given
* name. Rather, it is used to create a temporary "nonexistent user" for
- * later addition to the database. It can also be used to set the IP
- * address for an anonymous user to something other than the current
+ * later addition to the database. It can also be used to set the IP
+ * address for an anonymous user to something other than the current
* remote IP.
*
* User::newFromName() has rougly the same function, when the named user
@@ -1211,7 +1225,7 @@ class User {
return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
}
-
+
/**
* Perform a user_newtalk check on current slaves; if the memcached data
* is funky we don't want newtalk state to get stuck on save, as that's
@@ -1311,7 +1325,7 @@ class User {
$this->invalidateCache();
}
}
-
+
/**
* Generate a current or new-future timestamp to be stored in the
* user_touched field when we update things.
@@ -1320,7 +1334,7 @@ class User {
global $wgClockSkewFudge;
return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
}
-
+
/**
* Clear user data from memcached.
* Use after applying fun updates to the database; caller's
@@ -1344,13 +1358,13 @@ class User {
$this->load();
if( $this->mId ) {
$this->mTouched = self::newTouchedTimestamp();
-
+
$dbw = wfGetDB( DB_MASTER );
$dbw->update( 'user',
array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
array( 'user_id' => $this->mId ),
__METHOD__ );
-
+
$this->clearSharedCache();
}
}
@@ -1362,7 +1376,8 @@ class User {
/**
* Encrypt a password.
- * It can eventuall salt a password @see User::addSalt()
+ * It can eventually salt a password.
+ * @see User::addSalt()
* @param string $p clear Password.
* @return string Encrypted password.
*/
@@ -1387,12 +1402,12 @@ class User {
*/
function setPassword( $str ) {
global $wgAuth;
-
+
if( $str !== null ) {
if( !$wgAuth->allowPasswordChange() ) {
throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
}
-
+
if( !$this->isValidPassword( $str ) ) {
global $wgMinimalPasswordLength;
throw new PasswordError( wfMsg( 'passwordtooshort',
@@ -1403,7 +1418,7 @@ class User {
if( !$wgAuth->setPassword( $this, $str ) ) {
throw new PasswordError( wfMsg( 'externaldberror' ) );
}
-
+
$this->setInternalPassword( $str );
return true;
@@ -1418,7 +1433,7 @@ class User {
function setInternalPassword( $str ) {
$this->load();
$this->setToken();
-
+
if( $str === null ) {
// Save an invalid hash...
$this->mPassword = '';
@@ -1480,7 +1495,7 @@ class User {
$expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
return time() < $expiry;
}
-
+
function getEmail() {
$this->load();
return $this->mEmail;
@@ -1529,7 +1544,7 @@ class User {
}
/**
- * Get the user's date preference, including some important migration for
+ * Get the user's date preference, including some important migration for
* old user rows.
*/
function getDatePreference() {
@@ -1552,7 +1567,7 @@ class User {
function getBoolOption( $oname ) {
return (bool)$this->getOption( $oname );
}
-
+
/**
* Get an option as an integer value from the source string.
* @param string $oname The option to check
@@ -1587,6 +1602,7 @@ class User {
function getRights() {
if ( is_null( $this->mRights ) ) {
$this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
+ wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
}
return $this->mRights;
}
@@ -1615,7 +1631,7 @@ class User {
$this->mEffectiveGroups[] = '*';
if( $this->mId ) {
$this->mEffectiveGroups[] = 'user';
-
+
global $wgAutoConfirmAge, $wgAutoConfirmCount;
$accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration );
@@ -1632,25 +1648,27 @@ class User {
$this->mEffectiveGroups[] = 'emailconfirmed';
}
}
+ # Hook for additional groups
+ wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
}
}
return $this->mEffectiveGroups;
}
-
+
/* Return the edit count for the user. This is where User::edits should have been */
function getEditCount() {
if ($this->mId) {
if ( !isset( $this->mEditCount ) ) {
/* Populate the count, if it has not been populated yet */
$this->mEditCount = User::edits($this->mId);
- }
+ }
return $this->mEditCount;
} else {
/* nil */
return null;
}
}
-
+
/**
* Add the user to the given group.
* This takes immediate effect.
@@ -1704,7 +1722,11 @@ class User {
* @return bool
*/
function isLoggedIn() {
- return( $this->getID() != 0 );
+ if( $this->mId === null and $this->mName !== null ) {
+ // Special-case optimization
+ return !self::isIP( $this->mName );
+ }
+ return $this->getID() != 0;
}
/**
@@ -1955,7 +1977,7 @@ class User {
$this->load();
if ( wfReadOnly() ) { return; }
if ( 0 == $this->mId ) { return; }
-
+
$this->mTouched = self::newTouchedTimestamp();
$dbw = wfGetDB( DB_MASTER );
@@ -2044,7 +2066,7 @@ class User {
}
return $newUser;
}
-
+
/**
* Add an existing user object to the database
*/
@@ -2132,6 +2154,8 @@ class User {
// extra options or other effects on the parser cache.
wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
+ // Make it a valid memcached key fragment
+ $confstr = str_replace( ' ', '_', $confstr );
$this->mHash = $confstr;
return $confstr;
}
@@ -2141,6 +2165,17 @@ class User {
return $this->mBlock && $this->mBlock->mCreateAccount;
}
+ /**
+ * Determine if the user is blocked from using Special:Emailuser.
+ *
+ * @public
+ * @return boolean
+ */
+ function isBlockedFromEmailuser() {
+ $this->getBlockedStatus();
+ return $this->mBlock && $this->mBlock->mBlockEmail;
+ }
+
function isAllowedToCreateAccount() {
return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
}
@@ -2231,7 +2266,7 @@ class User {
}
return false;
}
-
+
/**
* Check if the given clear-text password matches the temporary password
* sent by e-mail for password reset operations.
@@ -2254,16 +2289,20 @@ class User {
* @public
*/
function editToken( $salt = '' ) {
- if( !isset( $_SESSION['wsEditToken'] ) ) {
- $token = $this->generateToken();
- $_SESSION['wsEditToken'] = $token;
+ if ( $this->isAnon() ) {
+ return EDIT_TOKEN_SUFFIX;
} else {
- $token = $_SESSION['wsEditToken'];
- }
- if( is_array( $salt ) ) {
- $salt = implode( '|', $salt );
+ if( !isset( $_SESSION['wsEditToken'] ) ) {
+ $token = $this->generateToken();
+ $_SESSION['wsEditToken'] = $token;
+ } else {
+ $token = $_SESSION['wsEditToken'];
+ }
+ if( is_array( $salt ) ) {
+ $salt = implode( '|', $salt );
+ }
+ return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
}
- return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
}
/**
@@ -2288,7 +2327,6 @@ class User {
* @public
*/
function matchEditToken( $val, $salt = '' ) {
- global $wgMemc;
$sessionToken = $this->editToken( $salt );
if ( $val != $sessionToken ) {
wfDebug( "User::matchEditToken: broken session data\n" );
@@ -2297,6 +2335,14 @@ class User {
}
/**
+ * Check whether the edit token is fine except for the suffix
+ */
+ function matchEditTokenNoSuffix( $val, $salt = '' ) {
+ $sessionToken = $this->editToken( $salt );
+ return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
+ }
+
+ /**
* Generate a new e-mail confirmation token and send a confirmation
* mail to the user's given address.
*
@@ -2433,7 +2479,7 @@ class User {
return $confirmed;
}
}
-
+
/**
* Return true if there is an outstanding request for e-mail confirmation.
* @return bool
@@ -2447,6 +2493,18 @@ class User {
}
/**
+ * Get the timestamp of account creation, or false for
+ * non-existent/anonymous user accounts
+ *
+ * @return mixed
+ */
+ public function getRegistration() {
+ return $this->mId > 0
+ ? $this->mRegistration
+ : false;
+ }
+
+ /**
* @param array $groups list of groups
* @return array list of permission key names for given groups combined
* @static
@@ -2469,7 +2527,8 @@ class User {
* @static
*/
static function getGroupName( $group ) {
- MessageCache::loadAllMessages();
+ global $wgMessageCache;
+ $wgMessageCache->loadAllMessages();
$key = "group-$group";
$name = wfMsg( $key );
return $name == '' || wfEmptyMsg( $key, $name )
@@ -2483,7 +2542,8 @@ class User {
* @static
*/
static function getGroupMember( $group ) {
- MessageCache::loadAllMessages();
+ global $wgMessageCache;
+ $wgMessageCache->loadAllMessages();
$key = "group-$group-member";
$name = wfMsg( $key );
return $name == '' || wfEmptyMsg( $key, $name )
@@ -2503,7 +2563,22 @@ class User {
global $wgGroupPermissions;
return array_diff(
array_keys( $wgGroupPermissions ),
- array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) );
+ self::getImplicitGroups()
+ );
+ }
+
+ /**
+ * Get a list of implicit groups
+ *
+ * @return array
+ */
+ public static function getImplicitGroups() {
+ static $groups = null;
+ if( !is_array( $groups ) ) {
+ $groups = array( '*', 'user', 'autoconfirmed', 'emailconfirmed' );
+ wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) );
+ }
+ return $groups;
}
/**
@@ -2513,7 +2588,8 @@ class User {
* @return mixed
*/
static function getGroupPage( $group ) {
- MessageCache::loadAllMessages();
+ global $wgMessageCache;
+ $wgMessageCache->loadAllMessages();
$page = wfMsgForContent( 'grouppage-' . $group );
if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
$title = Title::newFromText( $page );
@@ -2563,7 +2639,7 @@ class User {
return $text;
}
}
-
+
/**
* Increment the user's edit-count field.
* Will have no effect for anonymous users.
@@ -2575,7 +2651,7 @@ class User {
array( 'user_editcount=user_editcount+1' ),
array( 'user_id' => $this->getId() ),
__METHOD__ );
-
+
// Lazy initialization check...
if( $dbw->affectedRows() == 0 ) {
// Pull from a slave to be less cruel to servers
@@ -2585,7 +2661,7 @@ class User {
'COUNT(rev_user)',
array( 'rev_user' => $this->getId() ),
__METHOD__ );
-
+
// Now here's a goddamn hack...
if( $dbr !== $dbw ) {
// If we actually have a slave server, the count is
@@ -2597,7 +2673,7 @@ class User {
// count we just read includes the revision that was
// just added in the working transaction.
}
-
+
$dbw->update( 'user',
array( 'user_editcount' => $count ),
array( 'user_id' => $this->getId() ),
@@ -2609,4 +2685,4 @@ class User {
}
}
-?>
+
diff --git a/includes/UserMailer.php b/includes/UserMailer.php
index 9f5f178c..835dd310 100644
--- a/includes/UserMailer.php
+++ b/includes/UserMailer.php
@@ -72,6 +72,22 @@ class MailAddress {
}
}
+function send_mail($mailer, $dest, $headers, $body)
+{
+ $mailResult =& $mailer->send($dest, $headers, $body);
+
+ # Based on the result return an error string,
+ if ($mailResult === true) {
+ return '';
+ } elseif (is_object($mailResult)) {
+ wfDebug( "PEAR::Mail failed: " . $mailResult->getMessage() . "\n" );
+ return $mailResult->getMessage();
+ } else {
+ wfDebug( "PEAR::Mail failed, unknown error result\n" );
+ return 'Mail object return unknown error.';
+ }
+}
+
/**
* This function will perform a direct (authenticated) login to
* a SMTP Server to use for mail relaying if 'wgSMTP' specifies an
@@ -85,16 +101,30 @@ class MailAddress {
* @param $replyto String: optional reply-to email (default: null).
*/
function userMailer( $to, $from, $subject, $body, $replyto=null ) {
- global $wgUser, $wgSMTP, $wgOutputEncoding, $wgErrorString;
+ global $wgSMTP, $wgOutputEncoding, $wgErrorString, $wgEnotifImpersonal;
+ global $wgEnotifMaxRecips;
if (is_array( $wgSMTP )) {
require_once( 'Mail.php' );
- $timestamp = time();
- $dest = $to->address;
+ $msgid = str_replace(" ", "_", microtime());
+ if (function_exists('posix_getpid'))
+ $msgid .= '.' . posix_getpid();
+
+ if (is_array($to)) {
+ $dest = array();
+ foreach ($to as $u)
+ $dest[] = $u->address;
+ } else
+ $dest = $to->address;
$headers['From'] = $from->toString();
- $headers['To'] = $to->toString();
+
+ if ($wgEnotifImpersonal)
+ $headers['To'] = 'undisclosed-recipients:;';
+ else
+ $headers['To'] = $to->toString();
+
if ( $replyto ) {
$headers['Reply-To'] = $replyto->toString();
}
@@ -103,7 +133,7 @@ function userMailer( $to, $from, $subject, $body, $replyto=null ) {
$headers['MIME-Version'] = '1.0';
$headers['Content-type'] = 'text/plain; charset='.$wgOutputEncoding;
$headers['Content-transfer-encoding'] = '8bit';
- $headers['Message-ID'] = "<{$timestamp}" . $wgUser->getName() . '@' . $wgSMTP['IDHost'] . '>'; // FIXME
+ $headers['Message-ID'] = "<$msgid@" . $wgSMTP['IDHost'] . '>'; // FIXME
$headers['X-Mailer'] = 'MediaWiki mailer';
// Create the mail object using the Mail::factory method
@@ -114,18 +144,16 @@ function userMailer( $to, $from, $subject, $body, $replyto=null ) {
}
wfDebug( "Sending mail via PEAR::Mail to $dest\n" );
- $mailResult =& $mail_object->send($dest, $headers, $body);
+ if (is_array($dest)) {
+ $chunks = array_chunk($dest, $wgEnotifMaxRecips);
+ foreach ($chunks as $chunk) {
+ $e = send_mail($mail_object, $chunk, $headers, $body);
+ if ($e != '')
+ return $e;
+ }
+ } else
+ return $mail_object->send($dest, $headers, $body);
- # Based on the result return an error string,
- if ($mailResult === true) {
- return '';
- } elseif (is_object($mailResult)) {
- wfDebug( "PEAR::Mail failed: " . $mailResult->getMessage() . "\n" );
- return $mailResult->getMessage();
- } else {
- wfDebug( "PEAR::Mail failed, unknown error result\n" );
- return 'Mail object return unknown error.';
- }
} else {
# In the following $headers = expression we removed "Reply-To: {$from}\r\n" , because it is treated differently
# (fifth parameter of the PHP mail function, see some lines below)
@@ -148,21 +176,37 @@ function userMailer( $to, $from, $subject, $body, $replyto=null ) {
$headers .= "{$endl}Reply-To: " . $replyto->toString();
}
- $dest = $to->toString();
-
$wgErrorString = '';
set_error_handler( 'mailErrorHandler' );
- wfDebug( "Sending mail via internal mail() function to $dest\n" );
- mail( $dest, wfQuotedPrintable( $subject ), $body, $headers );
+ wfDebug( "Sending mail via internal mail() function\n" );
+
+ if (function_exists('mail'))
+ if (is_array($to))
+ foreach ($to as $recip)
+ $sent = mail( $recip->toString(), wfQuotedPrintable( $subject ), $body, $headers );
+ else
+ $sent = mail( $to->toString(), wfQuotedPrintable( $subject ), $body, $headers );
+ else
+ $wgErrorString = 'PHP is not configured to send mail';
+
+
restore_error_handler();
if ( $wgErrorString ) {
wfDebug( "Error sending mail: $wgErrorString\n" );
+ return $wgErrorString;
+ } elseif (! $sent) {
+ //mail function only tells if there's an error
+ wfDebug( "Error sending mail\n" );
+ return 'mailer error';
+ } else {
+ return '';
}
- return $wgErrorString;
}
}
+
+
/**
* Get the mail error message in global $wgErrorString
*
@@ -204,6 +248,27 @@ class EmailNotification {
/**@}}*/
+ function notifyOnPageChange($editor, &$title, $timestamp, $summary, $minorEdit, $oldid = false) {
+ global $wgEnotifUseJobQ;
+
+ if( $title->getNamespace() < 0 )
+ return;
+
+ if ($wgEnotifUseJobQ) {
+ $params = array(
+ "editor" => $editor->getName(),
+ "timestamp" => $timestamp,
+ "summary" => $summary,
+ "minorEdit" => $minorEdit,
+ "oldid" => $oldid);
+ $job = new EnotifNotifyJob( $title, $params );
+ $job->insert();
+ } else {
+ $this->actuallyNotifyOnPageChange($editor, $title, $timestamp, $summary, $minorEdit, $oldid);
+ }
+
+ }
+
/**
* @todo document
* @param $title Title object
@@ -212,11 +277,12 @@ class EmailNotification {
* @param $minorEdit
* @param $oldid (default: false)
*/
- function notifyOnPageChange(&$title, $timestamp, $summary, $minorEdit, $oldid=false) {
+ function actuallyNotifyOnPageChange($editor, &$title, $timestamp, $summary, $minorEdit, $oldid=false) {
# we use $wgEmergencyContact as sender's address
- global $wgUser, $wgEnotifWatchlist;
+ global $wgEnotifWatchlist;
global $wgEnotifMinorEdits, $wgEnotifUserTalk, $wgShowUpdatedMarker;
+ global $wgEnotifImpersonal;
$fname = 'UserMailer::notifyOnPageChange';
wfProfileIn( $fname );
@@ -234,18 +300,20 @@ class EmailNotification {
$this->summary = $summary;
$this->minorEdit = $minorEdit;
$this->oldid = $oldid;
- $this->composeCommonMailtext();
+ $this->composeCommonMailtext($editor);
+
+ $impersonals = array();
if ( (!$minorEdit || $wgEnotifMinorEdits) ) {
if( $wgEnotifWatchlist ) {
// Send updates to watchers other than the current editor
- $userCondition = 'wl_user <> ' . intval( $wgUser->getId() );
+ $userCondition = 'wl_user <> ' . intval( $editor->getId() );
} elseif( $wgEnotifUserTalk && $title->getNamespace() == NS_USER_TALK ) {
$targetUser = User::newFromName( $title->getText() );
if( is_null( $targetUser ) ) {
wfDebug( "$fname: user-talk-only mode; no such user\n" );
$userCondition = false;
- } elseif( $targetUser->getId() == $wgUser->getId() ) {
+ } elseif( $targetUser->getId() == $editor->getId() ) {
wfDebug( "$fname: user-talk-only mode; editor is target user\n" );
$userCondition = false;
} else {
@@ -287,7 +355,10 @@ class EmailNotification {
&& ($watchingUser->isEmailConfirmed() ) ) {
# ... adjust remaining text and page edit time placeholders
# which needs to be personalized for each user
- $this->composeAndSendPersonalisedMail( $watchingUser );
+ if ($wgEnotifImpersonal)
+ $impersonals[] = $watchingUser;
+ else
+ $this->composeAndSendPersonalisedMail( $watchingUser );
} # if the watching user has an email address in the preferences
}
@@ -298,9 +369,14 @@ class EmailNotification {
global $wgUsersNotifedOnAllChanges;
foreach ( $wgUsersNotifedOnAllChanges as $name ) {
$user = User::newFromName( $name );
- $this->composeAndSendPersonalisedMail( $user );
+ if ($wgEnotifImpersonal)
+ $impersonals[] = $user;
+ else
+ $this->composeAndSendPersonalisedMail( $user );
}
+ $this->composeAndSendImpersonalMail($impersonals);
+
if ( $wgShowUpdatedMarker || $wgEnotifWatchlist ) {
# mark the changed watch-listed page with a timestamp, so that the page is
# listed with an "updated since your last visit" icon in the watch list, ...
@@ -311,19 +387,22 @@ class EmailNotification {
), array( /* WHERE */
'wl_title' => $title->getDBkey(),
'wl_namespace' => $title->getNamespace(),
+ 'wl_notificationtimestamp IS NULL'
), 'UserMailer::NotifyOnChange'
);
# FIXME what do we do on failure ?
}
+
wfProfileOut( $fname );
} # function NotifyOnChange
/**
* @private
*/
- function composeCommonMailtext() {
- global $wgUser, $wgEmergencyContact, $wgNoReplyAddress;
+ function composeCommonMailtext($editor) {
+ global $wgEmergencyContact, $wgNoReplyAddress;
global $wgEnotifFromEditor, $wgEnotifRevealEditorAddress;
+ global $wgEnotifImpersonal;
$summary = ($this->summary == '') ? ' - ' : $this->summary;
$medit = ($this->minorEdit) ? wfMsg( 'minoredit' ) : '';
@@ -353,6 +432,14 @@ class EmailNotification {
$keys['$CHANGEDORCREATED'] = wfMsgForContent( 'created' );
}
+ if ($wgEnotifImpersonal && $this->oldid)
+ /*
+ * For impersonal mail, show a diff link to the last
+ * revision.
+ */
+ $keys['$NEWPAGE'] = wfMsgForContent('enotif_lastdiff',
+ $this->title->getFullURL("oldid={$this->oldid}&diff=prev"));
+
$body = strtr( $body, $keys );
$pagetitle = $this->title->getPrefixedText();
$keys['$PAGETITLE'] = $pagetitle;
@@ -366,12 +453,12 @@ class EmailNotification {
# Reveal the page editor's address as REPLY-TO address only if
# the user has not opted-out and the option is enabled at the
# global configuration level.
- $name = $wgUser->getName();
+ $name = $editor->getName();
$adminAddress = new MailAddress( $wgEmergencyContact, 'WikiAdmin' );
- $editorAddress = new MailAddress( $wgUser );
+ $editorAddress = new MailAddress( $editor );
if( $wgEnotifRevealEditorAddress
- && ( $wgUser->getEmail() != '' )
- && $wgUser->getOption( 'enotifrevealaddr' ) ) {
+ && ( $editor->getEmail() != '' )
+ && $editor->getOption( 'enotifrevealaddr' ) ) {
if( $wgEnotifFromEditor ) {
$from = $editorAddress;
} else {
@@ -383,10 +470,11 @@ class EmailNotification {
$replyto = new MailAddress( $wgNoReplyAddress );
}
- if( $wgUser->isIP( $name ) ) {
+ if( $editor->isIP( $name ) ) {
#real anon (user:xxx.xxx.xxx.xxx)
- $subject = str_replace('$PAGEEDITOR', 'anonymous user '. $name, $subject);
- $keys['$PAGEEDITOR'] = 'anonymous user ' . $name;
+ $utext = wfMsgForContent('enotif_anon_editor', $name);
+ $subject = str_replace('$PAGEEDITOR', $utext, $subject);
+ $keys['$PAGEEDITOR'] = $utext;
$keys['$PAGEEDITOR_EMAIL'] = wfMsgForContent( 'noemailtitle' );
} else {
$subject = str_replace('$PAGEEDITOR', $name, $subject);
@@ -394,7 +482,7 @@ class EmailNotification {
$emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $name );
$keys['$PAGEEDITOR_EMAIL'] = $emailPage->getFullUrl();
}
- $userPage = $wgUser->getUserPage();
+ $userPage = $editor->getUserPage();
$keys['$PAGEEDITOR_WIKI'] = $userPage->getFullUrl();
$body = strtr( $body, $keys );
$body = wordwrap( $body, 72 );
@@ -406,8 +494,6 @@ class EmailNotification {
$this->body = $body;
}
-
-
/**
* Does the per-user customizations to a notification e-mail (name,
* timestamp in proper timezone, etc) and sends it out.
@@ -435,9 +521,32 @@ class EmailNotification {
$wgLang->timeanddate( $this->timestamp, true, false, $timecorrection ),
$body);
- $error = userMailer( $to, $this->from, $this->subject, $body, $this->replyto );
- return ($error == '');
+ return userMailer($to, $this->from, $this->subject, $body, $this->replyto);
+ }
+
+ /**
+ * Same as composeAndSendPersonalisedMail but does impersonal mail
+ * suitable for bulk mailing. Takes an array of users.
+ */
+ function composeAndSendImpersonalMail($users) {
+ global $wgLang;
+
+ if (empty($users))
+ return;
+
+ $to = array();
+ foreach ($users as $user)
+ $to[] = new MailAddress($user);
+
+ $body = str_replace(
+ array( '$WATCHINGUSERNAME',
+ '$PAGEEDITDATE'),
+ array( wfMsgForContent('enotif_impersonal_salutation'),
+ $wgLang->timeanddate($this->timestamp, true, false, false)),
+ $this->body);
+
+ return userMailer($to, $this->from, $this->subject, $body, $this->replyto);
}
} # end of class EmailNotification
-?>
+
diff --git a/includes/Utf8Case.php b/includes/Utf8Case.php
index 279c0e32..1d3af41c 100644
--- a/includes/Utf8Case.php
+++ b/includes/Utf8Case.php
@@ -1502,4 +1502,4 @@ $wikiLowerChars = array (
"\xf0\x90\x90\xa5" => "\xf0\x90\x91\x8d"
);
-?>
+
diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php
index b0376e3d..e4de67c8 100644
--- a/includes/WatchedItem.php
+++ b/includes/WatchedItem.php
@@ -14,10 +14,10 @@ class WatchedItem {
* @todo document
* @access private
*/
- function &fromUserTitle( &$user, &$title ) {
+ static function fromUserTitle( $user, $title ) {
$wl = new WatchedItem;
- $wl->mUser =& $user;
- $wl->mTitle =& $title;
+ $wl->mUser = $user;
+ $wl->mTitle = $title;
$wl->id = $user->getId();
# Patch (also) for email notification on page changes T.Gries/M.Arndt 11.09.2004
# TG patch: here we do not consider pages and their talk pages equivalent - why should we ?
@@ -116,18 +116,13 @@ class WatchedItem {
*
* @param Title $ot Page title to duplicate entries from, if present
* @param Title $nt Page title to add watches on
- * @static
*/
- function duplicateEntries( $ot, $nt ) {
+ static function duplicateEntries( $ot, $nt ) {
WatchedItem::doDuplicateEntries( $ot->getSubjectPage(), $nt->getSubjectPage() );
WatchedItem::doDuplicateEntries( $ot->getTalkPage(), $nt->getTalkPage() );
}
- /**
- * @static
- * @access private
- */
- function doDuplicateEntries( $ot, $nt ) {
+ private static function doDuplicateEntries( $ot, $nt ) {
$fname = "WatchedItem::duplicateEntries";
$oldnamespace = $ot->getNamespace();
$newnamespace = $nt->getNamespace();
@@ -165,4 +160,4 @@ class WatchedItem {
}
-?>
+
diff --git a/includes/WatchlistEditor.php b/includes/WatchlistEditor.php
new file mode 100644
index 00000000..e03225a3
--- /dev/null
+++ b/includes/WatchlistEditor.php
@@ -0,0 +1,493 @@
+<?php
+
+/**
+ * Provides the UI through which users can perform editing
+ * operations on their watchlist
+ *
+ * @addtogroup Watchlist
+ * @author Rob Church <robchur@gmail.com>
+ */
+class WatchlistEditor {
+
+ /**
+ * Editing modes
+ */
+ const EDIT_CLEAR = 1;
+ const EDIT_RAW = 2;
+ const EDIT_NORMAL = 3;
+
+ /**
+ * Main execution point
+ *
+ * @param User $user
+ * @param OutputPage $output
+ * @param WebRequest $request
+ * @param int $mode
+ */
+ public function execute( $user, $output, $request, $mode ) {
+ global $wgUser;
+ if( wfReadOnly() ) {
+ $output->readOnlyPage();
+ return;
+ }
+ switch( $mode ) {
+ case self::EDIT_CLEAR:
+ $output->setPageTitle( wfMsg( 'watchlistedit-clear-title' ) );
+ if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) {
+ $this->clearWatchlist( $user );
+ $user->invalidateCache();
+ $output->addHtml( wfMsgExt( 'watchlistedit-clear-done', 'parse' ) );
+ } else {
+ $this->showClearForm( $output, $user );
+ }
+ break;
+ case self::EDIT_RAW:
+ $output->setPageTitle( wfMsg( 'watchlistedit-raw-title' ) );
+ if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) {
+ $wanted = $this->extractTitles( $request->getText( 'titles' ) );
+ $current = $this->getWatchlist( $user );
+ if( count( $wanted ) > 0 ) {
+ $toWatch = array_diff( $wanted, $current );
+ $toUnwatch = array_diff( $current, $wanted );
+ $this->watchTitles( $toWatch, $user );
+ $this->unwatchTitles( $toUnwatch, $user );
+ $user->invalidateCache();
+ if( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 )
+ $output->addHtml( wfMsgExt( 'watchlistedit-raw-done', 'parse' ) );
+ if( ( $count = count( $toWatch ) ) > 0 ) {
+ $output->addHtml( wfMsgExt( 'watchlistedit-raw-added', 'parse', $count ) );
+ $this->showTitles( $toWatch, $output, $wgUser->getSkin() );
+ }
+ if( ( $count = count( $toUnwatch ) ) > 0 ) {
+ $output->addHtml( wfMsgExt( 'watchlistedit-raw-removed', 'parse', $count ) );
+ $this->showTitles( $toUnwatch, $output, $wgUser->getSkin() );
+ }
+ } else {
+ $this->clearWatchlist( $user );
+ $user->invalidateCache();
+ $output->addHtml( wfMsgExt( 'watchlistedit-raw-removed', 'parse', count( $current ) ) );
+ $this->showTitles( $current, $output, $wgUser->getSkin() );
+ }
+ }
+ $this->showRawForm( $output, $user );
+ break;
+ case self::EDIT_NORMAL:
+ $output->setPageTitle( wfMsg( 'watchlistedit-normal-title' ) );
+ if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) {
+ $titles = $this->extractTitles( $request->getArray( 'titles' ) );
+ $this->unwatchTitles( $titles, $user );
+ $user->invalidateCache();
+ $output->addHtml( wfMsgExt( 'watchlistedit-normal-done', 'parse',
+ $GLOBALS['wgLang']->formatNum( count( $titles ) ) ) );
+ $this->showTitles( $titles, $output, $wgUser->getSkin() );
+ }
+ $this->showNormalForm( $output, $user );
+ }
+ }
+
+ /**
+ * Check the edit token from a form submission
+ *
+ * @param WebRequest $request
+ * @param User $user
+ * @return bool
+ */
+ private function checkToken( $request, $user ) {
+ return $user->matchEditToken( $request->getVal( 'token' ), 'watchlistedit' );
+ }
+
+ /**
+ * Extract a list of titles from a blob of text, returning
+ * (prefixed) strings; unwatchable titles are ignored
+ *
+ * @param mixed $list
+ * @return array
+ */
+ private function extractTitles( $list ) {
+ $titles = array();
+ if( !is_array( $list ) ) {
+ $list = explode( "\n", trim( $list ) );
+ if( !is_array( $list ) )
+ return array();
+ }
+ foreach( $list as $text ) {
+ $text = trim( $text );
+ if( strlen( $text ) > 0 ) {
+ $title = Title::newFromText( $text );
+ if( $title instanceof Title && $title->isWatchable() )
+ $titles[] = $title->getPrefixedText();
+ }
+ }
+ return array_unique( $titles );
+ }
+
+ /**
+ * Print out a list of linked titles
+ *
+ * $titles can be an array of strings or Title objects; the former
+ * is preferred, since Titles are very memory-heavy
+ *
+ * @param array $titles An array of strings, or Title objects
+ * @param OutputPage $output
+ * @param Skin $skin
+ */
+ private function showTitles( $titles, $output, $skin ) {
+ $talk = wfMsgHtml( 'talkpagelinktext' );
+ // Do a batch existence check
+ $batch = new LinkBatch();
+ foreach( $titles as $title ) {
+ if( !$title instanceof Title )
+ $title = Title::newFromText( $title );
+ if( $title instanceof Title ) {
+ $batch->addObj( $title );
+ $batch->addObj( $title->getTalkPage() );
+ }
+ }
+ $batch->execute();
+ // Print out the list
+ $output->addHtml( "<ul>\n" );
+ foreach( $titles as $title ) {
+ if( !$title instanceof Title )
+ $title = Title::newFromText( $title );
+ if( $title instanceof Title ) {
+ $output->addHtml( "<li>" . $skin->makeLinkObj( $title )
+ . ' (' . $skin->makeLinkObj( $title->getTalkPage(), $talk ) . ")</li>\n" );
+ }
+ }
+ $output->addHtml( "</ul>\n" );
+ }
+
+ /**
+ * Count the number of titles on a user's watchlist, excluding talk pages
+ *
+ * @param User $user
+ * @return int
+ */
+ private function countWatchlist( $user ) {
+ $dbr = wfGetDB( DB_MASTER );
+ $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->getId() ), __METHOD__ );
+ $row = $dbr->fetchObject( $res );
+ return ceil( $row->count / 2 ); // Paranoia
+ }
+
+ /**
+ * Prepare a list of titles on a user's watchlist (excluding talk pages)
+ * and return an array of (prefixed) strings
+ *
+ * @param User $user
+ * @return array
+ */
+ private function getWatchlist( $user ) {
+ $list = array();
+ $dbr = wfGetDB( DB_MASTER );
+ $res = $dbr->select(
+ 'watchlist',
+ '*',
+ array(
+ 'wl_user' => $user->getId(),
+ ),
+ __METHOD__
+ );
+ if( $res->numRows() > 0 ) {
+ while( $row = $res->fetchObject() ) {
+ $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
+ if( $title instanceof Title && !$title->isTalkPage() )
+ $list[] = $title->getPrefixedText();
+ }
+ $res->free();
+ }
+ return $list;
+ }
+
+ /**
+ * Get a list of titles on a user's watchlist, excluding talk pages,
+ * and return as a two-dimensional array with namespace, title and
+ * redirect status
+ *
+ * @param User $user
+ * @return array
+ */
+ private function getWatchlistInfo( $user ) {
+ $titles = array();
+ $dbr = wfGetDB( DB_MASTER );
+ $uid = intval( $user->getId() );
+ list( $watchlist, $page ) = $dbr->tableNamesN( 'watchlist', 'page' );
+ $sql = "SELECT wl_namespace, wl_title, page_id, page_is_redirect
+ FROM {$watchlist} LEFT JOIN {$page} ON ( wl_namespace = page_namespace
+ AND wl_title = page_title ) WHERE wl_user = {$uid}";
+ $res = $dbr->query( $sql, __METHOD__ );
+ if( $res && $dbr->numRows( $res ) > 0 ) {
+ $cache = LinkCache::singleton();
+ while( $row = $dbr->fetchObject( $res ) ) {
+ $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
+ if( $title instanceof Title ) {
+ // Update the link cache while we're at it
+ if( $row->page_id ) {
+ $cache->addGoodLinkObj( $row->page_id, $title );
+ } else {
+ $cache->addBadLinkObj( $title );
+ }
+ // Ignore non-talk
+ if( !$title->isTalkPage() )
+ $titles[$row->wl_namespace][$row->wl_title] = $row->page_is_redirect;
+ }
+ }
+ }
+ return $titles;
+ }
+
+ /**
+ * Show a message indicating the number of items on the user's watchlist,
+ * and return this count for additional checking
+ *
+ * @param OutputPage $output
+ * @param User $user
+ * @return int
+ */
+ private function showItemCount( $output, $user ) {
+ if( ( $count = $this->countWatchlist( $user ) ) > 0 ) {
+ $output->addHtml( wfMsgExt( 'watchlistedit-numitems', 'parse',
+ $GLOBALS['wgLang']->formatNum( $count ) ) );
+ } else {
+ $output->addHtml( wfMsgExt( 'watchlistedit-noitems', 'parse' ) );
+ }
+ return $count;
+ }
+
+ /**
+ * Remove all titles from a user's watchlist
+ *
+ * @param User $user
+ */
+ private function clearWatchlist( $user ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->delete( 'watchlist', array( 'wl_user' => $user->getId() ), __METHOD__ );
+ }
+
+ /**
+ * Add a list of titles to a user's watchlist
+ *
+ * $titles can be an array of strings or Title objects; the former
+ * is preferred, since Titles are very memory-heavy
+ *
+ * @param array $titles An array of strings, or Title objects
+ * @param User $user
+ */
+ private function watchTitles( $titles, $user ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $rows = array();
+ foreach( $titles as $title ) {
+ if( !$title instanceof Title )
+ $title = Title::newFromText( $title );
+ if( $title instanceof Title ) {
+ $rows[] = array(
+ 'wl_user' => $user->getId(),
+ 'wl_namespace' => ( $title->getNamespace() & ~1 ),
+ 'wl_title' => $title->getDBkey(),
+ 'wl_notificationtimestamp' => null,
+ );
+ $rows[] = array(
+ 'wl_user' => $user->getId(),
+ 'wl_namespace' => ( $title->getNamespace() | 1 ),
+ 'wl_title' => $title->getDBkey(),
+ 'wl_notificationtimestamp' => null,
+ );
+ }
+ }
+ $dbw->insert( 'watchlist', $rows, __METHOD__, 'IGNORE' );
+ }
+
+ /**
+ * Remove a list of titles from a user's watchlist
+ *
+ * $titles can be an array of strings or Title objects; the former
+ * is preferred, since Titles are very memory-heavy
+ *
+ * @param array $titles An array of strings, or Title objects
+ * @param User $user
+ */
+ private function unwatchTitles( $titles, $user ) {
+ $dbw = wfGetDB( DB_MASTER );
+ foreach( $titles as $title ) {
+ if( !$title instanceof Title )
+ $title = Title::newFromText( $title );
+ if( $title instanceof Title ) {
+ $dbw->delete(
+ 'watchlist',
+ array(
+ 'wl_user' => $user->getId(),
+ 'wl_namespace' => ( $title->getNamespace() & ~1 ),
+ 'wl_title' => $title->getDBkey(),
+ ),
+ __METHOD__
+ );
+ $dbw->delete(
+ 'watchlist',
+ array(
+ 'wl_user' => $user->getId(),
+ 'wl_namespace' => ( $title->getNamespace() | 1 ),
+ 'wl_title' => $title->getDBkey(),
+ ),
+ __METHOD__
+ );
+ }
+ }
+ }
+
+ /**
+ * Show a confirmation form for users wishing to clear their watchlist
+ *
+ * @param OutputPage $output
+ * @param User $user
+ */
+ private function showClearForm( $output, $user ) {
+ global $wgUser;
+ if( ( $count = $this->showItemCount( $output, $user ) ) > 0 ) {
+ $self = SpecialPage::getTitleFor( 'Watchlist' );
+ $form = Xml::openElement( 'form', array( 'method' => 'post',
+ 'action' => $self->getLocalUrl( 'action=clear' ) ) );
+ $form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
+ $form .= '<fieldset><legend>' . wfMsgHtml( 'watchlistedit-clear-legend' ) . '</legend>';
+ $form .= wfMsgExt( 'watchlistedit-clear-confirm', 'parse' );
+ $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-clear-submit' ) ) . '</p>';
+ $form .= '</fieldset></form>';
+ $output->addHtml( $form );
+ }
+ }
+
+ /**
+ * Show the standard watchlist editing form
+ *
+ * @param OutputPage $output
+ * @param User $user
+ */
+ private function showNormalForm( $output, $user ) {
+ global $wgUser;
+ if( ( $count = $this->showItemCount( $output, $user ) ) > 0 ) {
+ $self = SpecialPage::getTitleFor( 'Watchlist' );
+ $form = Xml::openElement( 'form', array( 'method' => 'post',
+ 'action' => $self->getLocalUrl( 'action=edit' ) ) );
+ $form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
+ $form .= '<fieldset><legend>' . wfMsgHtml( 'watchlistedit-normal-legend' ) . '</legend>';
+ $form .= wfMsgExt( 'watchlistedit-normal-explain', 'parse' );
+ foreach( $this->getWatchlistInfo( $user ) as $namespace => $pages ) {
+ $form .= '<h2>' . $this->getNamespaceHeading( $namespace ) . '</h2>';
+ $form .= '<ul>';
+ foreach( $pages as $dbkey => $redirect ) {
+ $title = Title::makeTitleSafe( $namespace, $dbkey );
+ $form .= $this->buildRemoveLine( $title, $redirect, $wgUser->getSkin() );
+ }
+ $form .= '</ul>';
+ }
+ $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-normal-submit' ) ) . '</p>';
+ $form .= '</fieldset></form>';
+ $output->addHtml( $form );
+ }
+ }
+
+ /**
+ * Get the correct "heading" for a namespace
+ *
+ * @param int $namespace
+ * @return string
+ */
+ private function getNamespaceHeading( $namespace ) {
+ return $namespace == NS_MAIN
+ ? wfMsgHtml( 'blanknamespace' )
+ : htmlspecialchars( $GLOBALS['wgContLang']->getFormattedNsText( $namespace ) );
+ }
+
+ /**
+ * Build a single list item containing a check box selecting a title
+ * and a link to that title, with various additional bits
+ *
+ * @param Title $title
+ * @param bool $redirect
+ * @param Skin $skin
+ * @return string
+ */
+ private function buildRemoveLine( $title, $redirect, $skin ) {
+ $link = $skin->makeLinkObj( $title );
+ if( $redirect )
+ $link = '<span class="watchlistredir">' . $link . '</span>';
+ $tools[] = $skin->makeLinkObj( $title->getTalkPage(), wfMsgHtml( 'talkpagelinktext' ) );
+ if( $title->exists() ) {
+ $tools[] = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'history_short' ), 'action=history' );
+ }
+ if( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
+ $tools[] = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $title->getText() ), wfMsgHtml( 'contributions' ) );
+ }
+ return '<li>'
+ . Xml::check( 'titles[]', false, array( 'value' => $title->getPrefixedText() ) )
+ . $link . ' (' . implode( ' | ', $tools ) . ')' . '</li>';
+ }
+
+ /**
+ * Show a form for editing the watchlist in "raw" mode
+ *
+ * @param OutputPage $output
+ * @param User $user
+ */
+ public function showRawForm( $output, $user ) {
+ global $wgUser;
+ $this->showItemCount( $output, $user );
+ $self = SpecialPage::getTitleFor( 'Watchlist' );
+ $form = Xml::openElement( 'form', array( 'method' => 'post',
+ 'action' => $self->getLocalUrl( 'action=raw' ) ) );
+ $form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
+ $form .= '<fieldset><legend>' . wfMsgHtml( 'watchlistedit-raw-legend' ) . '</legend>';
+ $form .= wfMsgExt( 'watchlistedit-raw-explain', 'parse' );
+ $form .= Xml::label( wfMsg( 'watchlistedit-raw-titles' ), 'titles' );
+ $form .= "<br />\n";
+ $form .= Xml::openElement( 'textarea', array( 'id' => 'titles', 'name' => 'titles',
+ 'rows' => $wgUser->getIntOption( 'rows' ), 'cols' => $wgUser->getIntOption( 'cols' ) ) );
+ $titles = $this->getWatchlist( $user );
+ foreach( $titles as $title )
+ $form .= htmlspecialchars( $title ) . "\n";
+ $form .= '</textarea>';
+ $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-raw-submit' ) ) . '</p>';
+ $form .= '</fieldset></form>';
+ $output->addHtml( $form );
+ }
+
+ /**
+ * Determine whether we are editing the watchlist, and if so, what
+ * kind of editing operation
+ *
+ * @param WebRequest $request
+ * @param mixed $par
+ * @return int
+ */
+ public static function getMode( $request, $par ) {
+ $mode = strtolower( $request->getVal( 'action', $par ) );
+ switch( $mode ) {
+ case 'clear':
+ return self::EDIT_CLEAR;
+ case 'raw':
+ return self::EDIT_RAW;
+ case 'edit':
+ return self::EDIT_NORMAL;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Build a set of links for convenient navigation
+ * between watchlist viewing and editing modes
+ *
+ * @param Skin $skin Skin to use
+ * @return string
+ */
+ public static function buildTools( $skin ) {
+ $tools = array();
+ $self = SpecialPage::getTitleFor( 'Watchlist' );
+ $modes = array( 'view' => '', 'edit' => 'edit', 'raw' => 'raw', 'clear' => 'clear' );
+ foreach( $modes as $mode => $action ) {
+ $action = $action ? "action={$action}" : '';
+ $tools[] = $skin->makeKnownLinkObj( $self, wfMsgHtml( "watchlisttools-{$mode}" ), $action );
+ }
+ return implode( ' | ', $tools );
+ }
+
+}
diff --git a/includes/WebRequest.php b/includes/WebRequest.php
index 53273a22..aa9885f0 100644
--- a/includes/WebRequest.php
+++ b/includes/WebRequest.php
@@ -44,17 +44,93 @@ if ( !function_exists( '__autoload' ) ) {
class WebRequest {
function __construct() {
$this->checkMagicQuotes();
+ }
+
+ /**
+ * Check for title, action, and/or variant data in the URL
+ * and interpolate it into the GET variables.
+ * This should only be run after $wgContLang is available,
+ * as we may need the list of language variants to determine
+ * available variant URLs.
+ */
+ function interpolateTitle() {
global $wgUsePathInfo;
if ( $wgUsePathInfo ) {
- if ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) {
- # Mangled PATH_INFO
- # http://bugs.php.net/bug.php?id=31892
- # Also reported when ini_get('cgi.fix_pathinfo')==false
- $_GET['title'] = $_REQUEST['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
- } elseif ( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') && $wgUsePathInfo ) {
- $_GET['title'] = $_REQUEST['title'] = substr( $_SERVER['PATH_INFO'], 1 );
+ // PATH_INFO is mangled due to http://bugs.php.net/bug.php?id=31892
+ // And also by Apache 2.x, double slashes are converted to single slashes.
+ // So we will use REQUEST_URI if possible.
+ $matches = array();
+ if ( !empty( $_SERVER['REQUEST_URI'] ) ) {
+ // Slurp out the path portion to examine...
+ $url = $_SERVER['REQUEST_URI'];
+ if ( !preg_match( '!^https?://!', $url ) ) {
+ $url = 'http://unused' . $url;
+ }
+ $a = parse_url( $url );
+ if( $a ) {
+ $path = $a['path'];
+
+ global $wgArticlePath;
+ $matches = $this->extractTitle( $path, $wgArticlePath );
+
+ global $wgActionPaths;
+ if( !$matches && $wgActionPaths) {
+ $matches = $this->extractTitle( $path, $wgActionPaths, 'action' );
+ }
+
+ global $wgVariantArticlePath, $wgContLang;
+ if( !$matches && $wgVariantArticlePath ) {
+ $variantPaths = array();
+ foreach( $wgContLang->getVariants() as $variant ) {
+ $variantPaths[$variant] =
+ str_replace( '$2', $variant, $wgVariantArticlePath );
+ }
+ $matches = $this->extractTitle( $path, $variantPaths, 'variant' );
+ }
+ }
+ } elseif ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) {
+ // Mangled PATH_INFO
+ // http://bugs.php.net/bug.php?id=31892
+ // Also reported when ini_get('cgi.fix_pathinfo')==false
+ $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
+
+ } elseif ( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') ) {
+ // Regular old PATH_INFO yay
+ $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
+ }
+ foreach( $matches as $key => $val) {
+ $_GET[$key] = $_REQUEST[$key] = $val;
+ }
+ }
+ }
+
+ /**
+ * Internal URL rewriting function; tries to extract page title and,
+ * optionally, one other fixed parameter value from a URL path.
+ *
+ * @param string $path the URL path given from the client
+ * @param array $bases one or more URLs, optionally with $1 at the end
+ * @param string $key if provided, the matching key in $bases will be
+ * passed on as the value of this URL parameter
+ * @return array of URL variables to interpolate; empty if no match
+ */
+ private function extractTitle( $path, $bases, $key=false ) {
+ foreach( (array)$bases as $keyValue => $base ) {
+ // Find the part after $wgArticlePath
+ $base = str_replace( '$1', '', $base );
+ $baseLen = strlen( $base );
+ if( substr( $path, 0, $baseLen ) == $base ) {
+ $raw = substr( $path, $baseLen );
+ if( $raw !== '' ) {
+ $matches = array( 'title' => rawurldecode( $raw ) );
+ if( $key ) {
+ $matches[$key] = $keyValue;
+ }
+ return $matches;
+ }
}
}
+ return array();
}
private $_response;
@@ -543,4 +619,4 @@ class FauxRequest extends WebRequest {
}
-?>
+
diff --git a/includes/WebResponse.php b/includes/WebResponse.php
index 92343195..f1578885 100644
--- a/includes/WebResponse.php
+++ b/includes/WebResponse.php
@@ -17,4 +17,4 @@ class WebResponse {
}
}
-?>
+
diff --git a/includes/WebStart.php b/includes/WebStart.php
index 55c96488..a9a6ad5f 100644
--- a/includes/WebStart.php
+++ b/includes/WebStart.php
@@ -101,4 +101,4 @@ wfProfileOut( 'WebStart.php-ob_start' );
if ( !defined( 'MW_NO_SETUP' ) ) {
require_once( './includes/Setup.php' );
}
-?>
+
diff --git a/includes/Wiki.php b/includes/Wiki.php
index 612e58ee..72a6a61d 100644
--- a/includes/Wiki.php
+++ b/includes/Wiki.php
@@ -57,14 +57,18 @@ class MediaWiki {
}
function checkMaxLag( $maxLag ) {
- global $wgLoadBalancer;
+ global $wgLoadBalancer, $wgShowHostnames;
list( $host, $lag ) = $wgLoadBalancer->getMaxLag();
if ( $lag > $maxLag ) {
header( 'HTTP/1.1 503 Service Unavailable' );
header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
header( 'X-Database-Lag: ' . intval( $lag ) );
header( 'Content-Type: text/plain' );
- echo "Waiting for $host: $lag seconds lagged\n";
+ if( $wgShowHostnames ) {
+ echo "Waiting for $host: $lag seconds lagged\n";
+ } else {
+ echo "Waiting for a database server: $lag seconds lagged\n";
+ }
return false;
} else {
return true;
@@ -98,6 +102,14 @@ class MediaWiki {
$lang->findVariantLink( $title, $ret );
}
+ if ( ( $oldid = $request->getInt( 'oldid' ) )
+ && ( is_null( $ret ) || $ret->getNamespace() != NS_SPECIAL ) ) {
+ // Allow oldid to override a changed or missing title.
+ $rev = Revision::newFromId( $oldid );
+ if( $rev ) {
+ $ret = $rev->getTitle();
+ }
+ }
return $ret ;
}
@@ -106,16 +118,14 @@ class MediaWiki {
*/
function preliminaryChecks ( &$title, &$output, $request ) {
- # Debug statement for user levels
- // print_r($wgUser);
-
- $search = $request->getText( 'search' );
- if( !is_null( $search ) && $search !== '' ) {
+ if( $request->getCheck( 'search' ) ) {
// Compatibility with old search URLs which didn't use Special:Search
+ // Just check for presence here, so blank requests still
+ // show the search page when using ugly URLs (bug 8054).
+
// Do this above the read whitelist check for security...
$title = SpecialPage::getTitleFor( 'Search' );
}
- $this->setVal( 'Search', $search );
# If the user is not logged in, the Namespace:title of the article must be in
# the Read array in order for the user to see it. (We have to check here to
@@ -135,13 +145,8 @@ class MediaWiki {
global $wgRequest;
wfProfileIn( 'MediaWiki::initializeSpecialCases' );
- $search = $this->getVal('Search');
$action = $this->getVal('Action');
- if( !$this->getVal('DisableInternalSearch') && !is_null( $search ) && $search !== '' ) {
- require_once( 'includes/SpecialSearch.php' );
- $title = SpecialPage::getTitleFor( 'Search' );
- wfSpecialSearch();
- } else if( !$title or $title->getDBkey() == '' ) {
+ if( !$title or $title->getDBkey() == '' ) {
$title = SpecialPage::getTitleFor( 'Badtitle' );
# Die now before we mess up $wgArticle and the skin stops working
throw new ErrorPageError( 'badtitle', 'badtitletext' );
@@ -158,7 +163,7 @@ class MediaWiki {
$title = SpecialPage::getTitleFor( 'Badtitle' );
throw new ErrorPageError( 'badtitle', 'badtitletext' );
}
- } else if ( ( $action == 'view' ) &&
+ } else if ( ( $action == 'view' ) && !$wgRequest->wasPosted() &&
(!isset( $this->GET['title'] ) || $title->getPrefixedDBKey() != $this->GET['title'] ) &&
!count( array_diff( array_keys( $this->GET ), array( 'action', 'title' ) ) ) )
{
@@ -209,7 +214,7 @@ class MediaWiki {
* @param Title $title
* @return Article
*/
- function articleFromTitle( $title ) {
+ static function articleFromTitle( $title ) {
$article = null;
wfRunHooks('ArticleFromTitle', array( &$title, &$article ) );
if ( $article ) {
@@ -460,4 +465,4 @@ class MediaWiki {
}; /* End of class MediaWiki */
-?>
+
diff --git a/includes/WikiError.php b/includes/WikiError.php
index 064db61a..efb645bb 100644
--- a/includes/WikiError.php
+++ b/includes/WikiError.php
@@ -121,4 +121,4 @@ class WikiXmlError extends WikiError {
}
}
-?>
+
diff --git a/includes/Xml.php b/includes/Xml.php
index 0fedcfa0..fe4bb0cd 100644
--- a/includes/Xml.php
+++ b/includes/Xml.php
@@ -19,9 +19,7 @@ class Xml {
public static function element( $element, $attribs = null, $contents = '') {
$out = '<' . $element;
if( !is_null( $attribs ) ) {
- foreach( $attribs as $name => $val ) {
- $out .= ' ' . $name . '="' . Sanitizer::encodeAttribute( $val ) . '"';
- }
+ $out .= self::expandAttributes( $attribs );
}
if( is_null( $contents ) ) {
$out .= '>';
@@ -36,6 +34,26 @@ class Xml {
}
/**
+ * Given an array of ('attributename' => 'value'), it generates the code
+ * to set the XML attributes : attributename="value".
+ * The values are passed to Sanitizer::encodeAttribute.
+ * Return null if no attributes given.
+ * @param $attribs Array of attributes for an XML element
+ */
+ private static function expandAttributes( $attribs ) {
+ $out = '';
+ if( is_null( $attribs ) ) {
+ return null;
+ } elseif( is_array( $attribs ) ) {
+ foreach( $attribs as $name => $val )
+ $out .= " {$name}=\"" . Sanitizer::encodeAttribute( $val ) . '"';
+ return $out;
+ } else {
+ throw new MWException( 'Expected attribute array, got something else in ' . __METHOD__ );
+ }
+ }
+
+ /**
* Format an XML element as with self::element(), but run text through the
* UtfNormal::cleanUp() validator first to ensure that no invalid UTF-8
* is passed.
@@ -57,8 +75,12 @@ class Xml {
return self::element( $element, $attribs, $contents );
}
- // Shortcuts
- public static function openElement( $element, $attribs = null ) { return self::element( $element, $attribs, null ); }
+ /** This open an XML element */
+ public static function openElement( $element, $attribs = null ) {
+ return '<' . $element . self::expandAttributes( $attribs ) . '>';
+ }
+
+ // Shortcut
public static function closeElement( $element ) { return "</$element>"; }
/**
@@ -66,48 +88,99 @@ class Xml {
* content you have is already valid xml.
*/
public static function tags( $element, $attribs = null, $contents ) {
- return self::element( $element, $attribs, null ) . $contents . "</$element>";
+ return self::openElement( $element, $attribs ) . $contents . "</$element>";
}
/**
- * Create a namespace selector
+ * Build a drop-down box for selecting a namespace
*
- * @param $selected Mixed: the namespace which should be selected, default ''
- * @param $allnamespaces String: value of a special item denoting all namespaces. Null to not include (default)
- * @param $includehidden Bool: include hidden namespaces?
- * @return String: Html string containing the namespace selector
+ * @param mixed $selected Namespace which should be pre-selected
+ * @param mixed $all Value of an item denoting all namespaces, or null to omit
+ * @param bool $hidden Include hidden namespaces? [WTF? --RC]
+ * @return string
*/
- public static function namespaceSelector($selected = '', $allnamespaces = null, $includehidden=false) {
+ public static function namespaceSelector( $selected = '', $all = null, $hidden = false, $element_name = 'namespace' ) {
global $wgContLang;
- if( $selected !== '' ) {
- if( is_null( $selected ) ) {
- // No namespace selected; let exact match work without hitting Main
- $selected = '';
- } else {
- // Let input be numeric strings without breaking the empty match.
- $selected = intval( $selected );
- }
+ $namespaces = $wgContLang->getFormattedNamespaces();
+ $options = array();
+
+ if( !is_null( $all ) )
+ $namespaces = array( $all => wfMsg( 'namespacesall' ) ) + $namespaces;
+ foreach( $namespaces as $index => $name ) {
+ if( $index < NS_MAIN )
+ continue;
+ if( $index === 0 )
+ $name = wfMsg( 'blanknamespace' );
+ $options[] = self::option( $name, $index, $index === $selected );
}
- $s = "\n<select id='namespace' name='namespace' class='namespaceselector'>\n";
- $arr = $wgContLang->getFormattedNamespaces();
- if( !is_null($allnamespaces) ) {
- $arr = array($allnamespaces => wfMsg('namespacesall')) + $arr;
- }
- foreach ($arr as $index => $name) {
- if ($index < NS_MAIN) continue;
+
+ return Xml::openElement( 'select', array( 'id' => 'namespace', 'name' => $element_name,
+ 'class' => 'namespaceselector' ) )
+ . "\n"
+ . implode( "\n", $options )
+ . "\n"
+ . Xml::closeElement( 'select' );
+ }
+
+ /**
+ * Create a date selector
+ *
+ * @param $selected Mixed: the month which should be selected, default ''
+ * @param $allmonths String: value of a special item denoting all month. Null to not include (default)
+ * @param string $id Element identifier
+ * @return String: Html string containing the month selector
+ */
+ public static function monthSelector( $selected = '', $allmonths = null, $id = 'month' ) {
+ global $wgLang;
+ $options = array();
+ if( is_null( $selected ) )
+ $selected = '';
+ if( !is_null( $allmonths ) )
+ $options[] = self::option( wfMsg( 'monthsall' ), $allmonths, $selected === $allmonths );
+ for( $i = 1; $i < 13; $i++ )
+ $options[] = self::option( $wgLang->getMonthName( $i ), $i, $selected === $i );
+ return self::openElement( 'select', array( 'id' => $id, 'name' => 'month' ) )
+ . implode( "\n", $options )
+ . self::closeElement( 'select' );
+ }
- $name = $index !== 0 ? $name : wfMsg('blanknamespace');
+ /**
+ *
+ * @param $language The language code of the selected language
+ * @param $customisedOnly If true only languages which have some content are listed
+ * @return array of label and select
+ */
+ public static function languageSelector( $selected, $customisedOnly = true ) {
+ global $wgContLanguageCode;
+ /**
+ * Make sure the site language is in the list; a custom language code
+ * might not have a defined name...
+ */
+ $languages = Language::getLanguageNames( $customisedOnly );
+ if( !array_key_exists( $wgContLanguageCode, $languages ) ) {
+ $languages[$wgContLanguageCode] = $wgContLanguageCode;
+ }
+ ksort( $languages );
- if ($index === $selected) {
- $s .= "\t" . self::element("option",
- array("value" => $index, "selected" => "selected"),
- $name) . "\n";
- } else {
- $s .= "\t" . self::element("option", array("value" => $index), $name) . "\n";
- }
+ /**
+ * If a bogus value is set, default to the content language.
+ * Otherwise, no default is selected and the user ends up
+ * with an Afrikaans interface since it's first in the list.
+ */
+ $selected = isset( $languages[$selected] ) ? $selected : $wgContLanguageCode;
+ $options = "\n";
+ foreach( $languages as $code => $name ) {
+ $options .= Xml::option( "$code - $name", $code, ($code == $selected) ) . "\n";
}
- $s .= "</select>\n";
- return $s;
+
+ return array(
+ Xml::label( wfMsg('yourlanguage'), 'wpUserLanguage' ),
+ Xml::tags( 'select',
+ array( 'id' => 'wpUserLanguage', 'name' => 'wpUserLanguage' ),
+ $options
+ )
+ );
+
}
public static function span( $text, $class, $attribs=array() ) {
@@ -126,6 +199,14 @@ class Xml {
}
/**
+ * Convenience function to build an HTML password input field
+ * @return string HTML
+ */
+ public static function password( $name, $size=false, $value=false, $attribs=array() ) {
+ return self::input( $name, $size, $value, array_merge($attribs, array('type' => 'password')));
+ }
+
+ /**
* Internal function for use in checkboxes and radio buttons and such.
* @return array
*/
@@ -263,13 +344,22 @@ class Xml {
# To avoid any complaints about bad entity refs
"&" => "\\x26",
+
+ # Work around https://bugzilla.mozilla.org/show_bug.cgi?id=274152
+ # Encode certain Unicode formatting chars so affected
+ # versions of Gecko don't misinterpret our strings;
+ # this is a common problem with Farsi text.
+ "\xe2\x80\x8c" => "\\u200c", // ZERO WIDTH NON-JOINER
+ "\xe2\x80\x8d" => "\\u200d", // ZERO WIDTH JOINER
);
return strtr( $string, $pairs );
}
/**
* Encode a variable of unknown type to JavaScript.
- * Doesn't support hashtables just yet.
+ * Arrays are converted to JS arrays, objects are converted to JS associative
+ * arrays (objects). So cast your PHP associative arrays to objects before
+ * passing them to here.
*/
public static function encodeJsVar( $value ) {
if ( is_bool( $value ) ) {
@@ -280,13 +370,23 @@ class Xml {
$s = $value;
} elseif ( is_array( $value ) ) {
$s = '[';
- foreach ( $value as $name => $elt ) {
+ foreach ( $value as $elt ) {
if ( $s != '[' ) {
$s .= ', ';
}
$s .= self::encodeJsVar( $elt );
}
$s .= ']';
+ } elseif ( is_object( $value ) ) {
+ $s = '{';
+ foreach ( (array)$value as $name => $elt ) {
+ if ( $s != '{' ) {
+ $s .= ', ';
+ }
+ $s .= '"' . self::escapeJsString( $name ) . '": ' .
+ self::encodeJsVar( $elt );
+ }
+ $s .= '}';
} else {
$s = '"' . self::escapeJsString( $value ) . '"';
}
@@ -352,4 +452,4 @@ class Xml {
$in );
}
}
-?>
+
diff --git a/includes/XmlFunctions.php b/includes/XmlFunctions.php
index 326c4953..2e86aa7d 100644
--- a/includes/XmlFunctions.php
+++ b/includes/XmlFunctions.php
@@ -59,7 +59,4 @@ function wfIsWellFormedXml( $text ) {
}
function wfIsWellFormedXmlFragment( $text ) {
return Xml::isWellFormedXmlFragment( $text );
-}
-
-
-?>
+} \ No newline at end of file
diff --git a/includes/ZhClient.php b/includes/ZhClient.php
index fe965f65..7f2c5cbf 100644
--- a/includes/ZhClient.php
+++ b/includes/ZhClient.php
@@ -144,4 +144,3 @@ class ZhClient {
fclose($this->mFP);
}
}
-?> \ No newline at end of file
diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php
index 1ccd6a7e..1b9d884b 100644
--- a/includes/ZhConversion.php
+++ b/includes/ZhConversion.php
@@ -8453,4 +8453,3 @@ $zh2SG=array(
"房价" => "屋价",
"泡麵" => "快速面",
);
-?> \ No newline at end of file
diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php
index c4218825..b324c52f 100644
--- a/includes/api/ApiBase.php
+++ b/includes/api/ApiBase.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,7 +24,16 @@
*/
/**
- * @todo Document - e.g. Provide top-level description of this class.
+ * This abstract class implements many basic API functions, and is the base of all API classes.
+ * The class functions are divided into several areas of functionality:
+ *
+ * Module parameters: Derived classes can define getAllowedParams() to specify which parameters to expect,
+ * how to parse and validate them.
+ *
+ * Profiling: various methods to allow keeping tabs on various tasks and their time costs
+ *
+ * Self-documentation: code to allow api to document its own state.
+ *
* @addtogroup API
*/
abstract class ApiBase {
@@ -34,24 +43,24 @@ abstract class ApiBase {
const PARAM_DFLT = 0;
const PARAM_ISMULTI = 1;
const PARAM_TYPE = 2;
- const PARAM_MAX1 = 3;
+ const PARAM_MAX = 3;
const PARAM_MAX2 = 4;
const PARAM_MIN = 5;
- const LIMIT_BIG1 = 500; // Fast query, user's limit
- const LIMIT_BIG2 = 5000; // Fast query, bot's limit
- const LIMIT_SML1 = 50; // Slow query, user's limit
- const LIMIT_SML2 = 500; // Slow query, bot's limit
+ const LIMIT_BIG1 = 500; // Fast query, std user limit
+ const LIMIT_BIG2 = 5000; // Fast query, bot/sysop limit
+ const LIMIT_SML1 = 50; // Slow query, std user limit
+ const LIMIT_SML2 = 500; // Slow query, bot/sysop limit
- private $mMainModule, $mModuleName, $mParamPrefix;
+ private $mMainModule, $mModuleName, $mModulePrefix;
/**
* Constructor
*/
- public function __construct($mainModule, $moduleName, $paramPrefix = '') {
+ public function __construct($mainModule, $moduleName, $modulePrefix = '') {
$this->mMainModule = $mainModule;
$this->mModuleName = $moduleName;
- $this->mParamPrefix = $paramPrefix;
+ $this->mModulePrefix = $modulePrefix;
}
/**
@@ -67,6 +76,13 @@ abstract class ApiBase {
}
/**
+ * Get parameter prefix (usually two letters or an empty string).
+ */
+ public function getModulePrefix() {
+ return $this->mModulePrefix;
+ }
+
+ /**
* Get the name of the module as shown in the profiler log
*/
public function getModuleProfileName($db = false) {
@@ -109,6 +125,15 @@ abstract class ApiBase {
}
/**
+ * Set warning section for this module. Users should monitor this section to notice any changes in API.
+ */
+ public function setWarning($warning) {
+ $msg = array();
+ ApiResult :: setContent($msg, $warning);
+ $this->getResult()->addValue('warnings', $this->getModuleName(), $msg);
+ }
+
+ /**
* If the module may only be used with a certain format module,
* it should override this method to return an instance of that formatter.
* A value of null means the default format will be used.
@@ -191,11 +216,38 @@ abstract class ApiBase {
$prompt = 'One value: ';
if (is_array($type)) {
- $desc .= $paramPrefix . $prompt . implode(', ', $type);
- }
- elseif ($type == 'namespace') {
- // Special handling because namespaces are type-limited, yet they are not given
- $desc .= $paramPrefix . $prompt . implode(', ', ApiBase :: getValidNamespaces());
+ $choices = array();
+ $nothingPrompt = false;
+ foreach ($type as $t)
+ if ($t=='')
+ $nothingPrompt = 'Can be empty, or ';
+ else
+ $choices[] = $t;
+ $desc .= $paramPrefix . $nothingPrompt . $prompt . implode(', ', $choices);
+ } else {
+ switch ($type) {
+ case 'namespace':
+ // Special handling because namespaces are type-limited, yet they are not given
+ $desc .= $paramPrefix . $prompt . implode(', ', ApiBase :: getValidNamespaces());
+ break;
+ case 'limit':
+ $desc .= $paramPrefix . "No more than {$paramSettings[self :: PARAM_MAX]} ({$paramSettings[self :: PARAM_MAX2]} for bots) allowed.";
+ break;
+ case 'integer':
+ $hasMin = isset($paramSettings[self :: PARAM_MIN]);
+ $hasMax = isset($paramSettings[self :: PARAM_MAX]);
+ if ($hasMin || $hasMax) {
+ if (!$hasMax)
+ $intRangeStr = "The value must be no less than {$paramSettings[self :: PARAM_MIN]}";
+ elseif (!$hasMin)
+ $intRangeStr = "The value must be no more than {$paramSettings[self :: PARAM_MAX]}";
+ else
+ $intRangeStr = "The value must be between {$paramSettings[self :: PARAM_MIN]} and {$paramSettings[self :: PARAM_MAX]}";
+
+ $desc .= $paramPrefix . $intRangeStr;
+ }
+ break;
+ }
}
}
@@ -244,7 +296,7 @@ abstract class ApiBase {
* Override this method to change parameter name during runtime
*/
public function encodeParamName($paramName) {
- return $this->mParamPrefix . $paramName;
+ return $this->mModulePrefix . $paramName;
}
/**
@@ -293,7 +345,7 @@ abstract class ApiBase {
protected function getParameterFromSettings($paramName, $paramSettings) {
// Some classes may decide to change parameter names
- $paramName = $this->encodeParamName($paramName);
+ $encParamName = $this->encodeParamName($paramName);
if (!is_array($paramSettings)) {
$default = $paramSettings;
@@ -316,19 +368,19 @@ abstract class ApiBase {
if ($type == 'boolean') {
if (isset ($default) && $default !== false) {
// Having a default value of anything other than 'false' is pointless
- ApiBase :: dieDebug(__METHOD__, "Boolean param $paramName's default is set to '$default'");
+ ApiBase :: dieDebug(__METHOD__, "Boolean param $encParamName's default is set to '$default'");
}
- $value = $this->getMain()->getRequest()->getCheck($paramName);
+ $value = $this->getMain()->getRequest()->getCheck($encParamName);
} else {
- $value = $this->getMain()->getRequest()->getVal($paramName, $default);
+ $value = $this->getMain()->getRequest()->getVal($encParamName, $default);
if (isset ($value) && $type == 'namespace')
$type = ApiBase :: getValidNamespaces();
}
if (isset ($value) && ($multi || is_array($type)))
- $value = $this->parseMultiValue($paramName, $value, $multi, is_array($type) ? $type : null);
+ $value = $this->parseMultiValue($encParamName, $value, $multi, is_array($type) ? $type : null);
// More validation only when choices were not given
// choices were validated in parseMultiValue()
@@ -339,32 +391,48 @@ abstract class ApiBase {
break;
case 'string' : // nothing to do
break;
- case 'integer' : // Force everything using intval()
+ case 'integer' : // Force everything using intval() and optionally validate limits
+
$value = is_array($value) ? array_map('intval', $value) : intval($value);
+ $min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : null;
+ $max = isset ($paramSettings[self :: PARAM_MAX]) ? $paramSettings[self :: PARAM_MAX] : null;
+
+ if (!is_null($min) || !is_null($max)) {
+ $values = is_array($value) ? $value : array($value);
+ foreach ($values as $v) {
+ $this->validateLimit($paramName, $v, $min, $max);
+ }
+ }
break;
case 'limit' :
- if (!isset ($paramSettings[self :: PARAM_MAX1]) || !isset ($paramSettings[self :: PARAM_MAX2]))
- ApiBase :: dieDebug(__METHOD__, "MAX1 or MAX2 are not defined for the limit $paramName");
+ if (!isset ($paramSettings[self :: PARAM_MAX]) || !isset ($paramSettings[self :: PARAM_MAX2]))
+ ApiBase :: dieDebug(__METHOD__, "MAX1 or MAX2 are not defined for the limit $encParamName");
if ($multi)
- ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName");
+ ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName");
$min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : 0;
$value = intval($value);
- $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX1], $paramSettings[self :: PARAM_MAX2]);
+ $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX], $paramSettings[self :: PARAM_MAX2]);
break;
case 'boolean' :
if ($multi)
- ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName");
+ ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName");
break;
case 'timestamp' :
if ($multi)
- ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName");
+ ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName");
$value = wfTimestamp(TS_UNIX, $value);
if ($value === 0)
- $this->dieUsage("Invalid value '$value' for timestamp parameter $paramName", "badtimestamp_{$paramName}");
+ $this->dieUsage("Invalid value '$value' for timestamp parameter $encParamName", "badtimestamp_{$encParamName}");
$value = wfTimestamp(TS_MW, $value);
break;
+ case 'user' :
+ $title = Title::makeTitleSafe( NS_USER, $value );
+ if ( is_null( $title ) )
+ $this->dieUsage("Invalid value for user parameter $encParamName", "baduser_{$encParamName}");
+ $value = $title->getText();
+ break;
default :
- ApiBase :: dieDebug(__METHOD__, "Param $paramName's type is unknown - $type");
+ ApiBase :: dieDebug(__METHOD__, "Param $encParamName's type is unknown - $type");
}
}
@@ -405,19 +473,26 @@ abstract class ApiBase {
/**
* Validate the value against the minimum and user/bot maximum limits. Prints usage info on failure.
*/
- function validateLimit($varname, $value, $min, $max, $botMax) {
- if ($value < $min) {
- $this->dieUsage("$varname may not be less than $min (set to $value)", $varname);
+ function validateLimit($paramName, $value, $min, $max, $botMax = null) {
+ if (!is_null($min) && $value < $min) {
+ $this->dieUsage($this->encodeParamName($paramName) . " may not be less than $min (set to $value)", $paramName);
}
- if ($this->getMain()->isBot()) {
- if ($value > $botMax) {
- $this->dieUsage("$varname may not be over $botMax (set to $value) for bots", $varname);
+ // Minimum is always validated, whereas maximum is checked only if not running in internal call mode
+ if ($this->getMain()->isInternalMode())
+ return;
+
+ // Optimization: do not check user's bot status unless really needed -- skips db query
+ // assumes $botMax >= $max
+ if (!is_null($max) && $value > $max) {
+ if (!is_null($botMax) && ($this->getMain()->isBot() || $this->getMain()->isSysop())) {
+ if ($value > $botMax) {
+ $this->dieUsage($this->encodeParamName($paramName) . " may not be over $botMax (set to $value) for bots or sysops", $paramName);
+ }
+ } else {
+ $this->dieUsage($this->encodeParamName($paramName) . " may not be over $max (set to $value) for users", $paramName);
}
}
- elseif ($value > $max) {
- $this->dieUsage("$varname may not be over $max (set to $value) for users", $varname);
- }
}
/**
@@ -526,11 +601,19 @@ abstract class ApiBase {
ApiBase :: dieDebug(__METHOD__, 'called without calling profileDBOut() first');
return $this->mDBTime;
}
+
+ public static function debugPrint($value, $name = 'unknown', $backtrace = false) {
+ print "\n\n<pre><b>Debuging value '$name':</b>\n\n";
+ var_export($value);
+ if ($backtrace)
+ print "\n" . wfBacktrace();
+ print "\n</pre>\n";
+ }
public abstract function getVersion();
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiBase.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiBase.php 24934 2007-08-20 08:04:12Z nickj $';
}
}
-?>
+
diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php
index 7918ee0e..b2f6ceff 100644
--- a/includes/api/ApiFeedWatchlist.php
+++ b/includes/api/ApiFeedWatchlist.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,10 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * This action allows users to get their watchlist items in RSS/Atom formats.
+ * When executed, it performs a nested call to the API to get the needed data,
+ * and formats it in a proper format.
+ *
* @addtogroup API
*/
class ApiFeedWatchlist extends ApiBase {
@@ -37,47 +41,81 @@ class ApiFeedWatchlist extends ApiBase {
parent :: __construct($main, $action);
}
+ /**
+ * This module uses a custom feed wrapper printer.
+ */
public function getCustomPrinter() {
return new ApiFormatFeedWrapper($this->getMain());
}
+ /**
+ * Make a nested call to the API to request watchlist items in the last $hours.
+ * Wrap the result as an RSS/Atom feed.
+ */
public function execute() {
- $feedformat = null;
- extract($this->extractRequestParams());
-
- // limit to 1 day
- $startTime = wfTimestamp(TS_MW, time() - intval(1 * 86400));
-
- // Prepare nested request
- $params = new FauxRequest(array (
- 'action' => 'query',
- 'meta' => 'siteinfo',
- 'siprop' => 'general',
- 'list' => 'watchlist',
- 'wlprop' => 'user|comment|timestamp',
- 'wlstart' => $startTime,
- 'wllimit' => 50
- ));
-
- // Execute
- $module = new ApiMain($params);
- $module->execute();
-
- // Get data array
- $data = $module->getResultData();
-
- $feedItems = array ();
- foreach ($data['query']['watchlist'] as $info) {
- $feedItems[] = $this->createFeedItem($info);
- }
-
+
global $wgFeedClasses, $wgSitename, $wgContLanguageCode;
- $feedTitle = $wgSitename . ' - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']';
- $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl();
-
- $feed = new $wgFeedClasses[$feedformat] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl);
- ApiFormatFeedWrapper :: setResult($this->getResult(), $feed, $feedItems);
+ try {
+ $params = $this->extractRequestParams();
+
+ // limit to the number of hours going from now back
+ $endTime = wfTimestamp(TS_MW, time() - intval($params['hours'] * 60 * 60));
+
+ // Prepare nested request
+ $fauxReq = new FauxRequest(array (
+ 'action' => 'query',
+ 'meta' => 'siteinfo',
+ 'siprop' => 'general',
+ 'list' => 'watchlist',
+ 'wlprop' => 'title|user|comment|timestamp',
+ 'wldir' => 'older', // reverse order - from newest to oldest
+ 'wlend' => $endTime, // stop at this time
+ 'wllimit' => 50
+ ));
+
+ // Execute
+ $module = new ApiMain($fauxReq);
+ $module->execute();
+
+ // Get data array
+ $data = $module->getResultData();
+
+ $feedItems = array ();
+ foreach ($data['query']['watchlist'] as $info) {
+ $feedItems[] = $this->createFeedItem($info);
+ }
+
+ $feedTitle = $wgSitename . ' - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']';
+ $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl();
+
+ $feed = new $wgFeedClasses[$params['feedformat']] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl);
+
+ ApiFormatFeedWrapper :: setResult($this->getResult(), $feed, $feedItems);
+
+ } catch (Exception $e) {
+
+ // Error results should not be cached
+ $this->getMain()->setCacheMaxAge(0);
+
+ $feedTitle = $wgSitename . ' - Error - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']';
+ $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl();
+
+ $feedFormat = isset($params['feedformat']) ? $params['feedformat'] : 'rss';
+ $feed = new $wgFeedClasses[$feedFormat] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl);
+
+
+ if ($e instanceof UsageException) {
+ $errorCode = $e->getCodeString();
+ } else {
+ // Something is seriously wrong
+ $errorCode = 'internal_api_error';
+ }
+
+ $errorText = $e->getMessage();
+ $feedItems[] = new FeedItem("Error ($errorCode)", $errorText, "", "", "");
+ ApiFormatFeedWrapper :: setResult($this->getResult(), $feed, $feedItems);
+ }
}
private function createFeedItem($info) {
@@ -100,13 +138,20 @@ class ApiFeedWatchlist extends ApiBase {
'feedformat' => array (
ApiBase :: PARAM_DFLT => 'rss',
ApiBase :: PARAM_TYPE => $feedFormatNames
+ ),
+ 'hours' => array (
+ ApiBase :: PARAM_DFLT => 24,
+ ApiBase :: PARAM_TYPE => 'integer',
+ ApiBase :: PARAM_MIN => 1,
+ ApiBase :: PARAM_MAX => 72,
)
);
}
protected function getParamDescription() {
return array (
- 'feedformat' => 'The format of the feed'
+ 'feedformat' => 'The format of the feed',
+ 'hours' => 'List pages modified within this many hours from now'
);
}
@@ -121,7 +166,7 @@ class ApiFeedWatchlist extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFeedWatchlist.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiFeedWatchlist.php 23531 2007-06-29 01:19:14Z simetrical $';
}
}
-?>
+
diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php
index 782a4161..861310d2 100644
--- a/includes/api/ApiFormatBase.php
+++ b/includes/api/ApiFormatBase.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,8 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * This is the abstract base class for API formatters.
+ *
* @addtogroup API
*/
abstract class ApiFormatBase extends ApiBase {
@@ -36,7 +38,8 @@ abstract class ApiFormatBase extends ApiBase {
private $mIsHtml, $mFormat;
/**
- * Constructor
+ * Create a new instance of the formatter.
+ * If the format name ends with 'fm', wrap its output in the proper HTML.
*/
public function __construct($main, $format) {
parent :: __construct($main, $format);
@@ -56,6 +59,11 @@ abstract class ApiFormatBase extends ApiBase {
*/
public abstract function getMimeType();
+ /**
+ * If formatter outputs data results as is, the results must first be sanitized.
+ * An XML formatter on the other hand uses special tags, such as "_element" for special handling,
+ * and thus needs to override this function to return true.
+ */
public function getNeedsRawData() {
return false;
}
@@ -77,6 +85,7 @@ abstract class ApiFormatBase extends ApiBase {
function initPrinter($isError) {
$isHtml = $this->getIsHtml();
$mime = $isHtml ? 'text/html' : $this->getMimeType();
+ $script = wfScript( 'api' );
// Some printers (ex. Feed) do their own header settings,
// in which case $mime will be set to null
@@ -96,14 +105,14 @@ abstract class ApiFormatBase extends ApiBase {
<?php
- if (!$isError) {
+ if( !$isError ) {
?>
<br/>
<small>
-You are looking at the HTML representation of the <?=$this->mFormat?> format.<br/>
+You are looking at the HTML representation of the <?php echo( $this->mFormat ); ?> format.<br/>
HTML is good for debugging, but probably is not suitable for your application.<br/>
-Please see "format" parameter documentation at the <a href='api.php'>API help</a>
-for more information.
+See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
+<a href='<?php echo( $script ); ?>'>API help</a> for more information.
</small>
<?php
@@ -133,6 +142,10 @@ for more information.
}
}
+ /**
+ * The main format printing function. Call it to output the result string to the user.
+ * This function will automatically output HTML when format name ends in 'fm'.
+ */
public function printText($text) {
if ($this->getIsHtml())
echo $this->formatHTML($text);
@@ -152,9 +165,9 @@ for more information.
$text = preg_replace('/\&lt;(!--.*?--|.*?)\&gt;/', '<span style="color:blue;">&lt;\1&gt;</span>', $text);
// identify URLs
$protos = "http|https|ftp|gopher";
- $text = ereg_replace("($protos)://[^ '\"()<\n]+", '<a href="\\0">\\0</a>', $text);
+ $text = ereg_replace("($protos)://[^ \\'\"()<\n]+", '<a href="\\0">\\0</a>', $text);
// identify requests to api.php
- $text = ereg_replace("api\\.php\\?[^ ()<\n\t]+", '<a href="\\0">\\0</a>', $text);
+ $text = ereg_replace("api\\.php\\?[^ \\()<\n\t]+", '<a href="\\0">\\0</a>', $text);
// make strings inside * bold
$text = ereg_replace("\\*[^<>\n]+\\*", '<b>\\0</b>', $text);
// make strings inside $ italic
@@ -175,7 +188,7 @@ for more information.
}
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiFormatBase.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiFormatBase.php 25746 2007-09-10 21:36:51Z brion $';
}
}
@@ -190,7 +203,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
}
/**
- * Call this method to initialize output data
+ * Call this method to initialize output data. See self::execute()
*/
public static function setResult($result, $feed, $feedItems) {
// Store output in the Result data.
@@ -214,6 +227,11 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
return true;
}
+ /**
+ * This class expects the result data to be in a custom format set by self::setResult()
+ * $result['_feed'] - an instance of one of the $wgFeedClasses classes
+ * $result['_feeditems'] - an array of FeedItem instances
+ */
public function execute() {
$data = $this->getResultData();
if (isset ($data['_feed']) && isset ($data['_feeditems'])) {
@@ -232,7 +250,6 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatBase.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiFormatBase.php 25746 2007-09-10 21:36:51Z brion $';
}
}
-?>
diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php
index dd1847c4..ed9bd938 100644
--- a/includes/api/ApiFormatJson.php
+++ b/includes/api/ApiFormatJson.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -49,14 +49,35 @@ class ApiFormatJson extends ApiFormatBase {
}
public function execute() {
+ $prefix = $suffix = "";
+
+ $params = $this->extractRequestParams();
+ $callback = $params['callback'];
+ if(!is_null($callback)) {
+ $prefix = ereg_replace("[^_A-Za-z0-9]", "", $callback ) . "(";
+ $suffix = ")";
+ }
+
if (!function_exists('json_encode') || $this->getIsHtml()) {
$json = new Services_JSON();
- $this->printText($json->encode($this->getResultData(), $this->getIsHtml()));
+ $this->printText($prefix . $json->encode($this->getResultData(), $this->getIsHtml()) . $suffix);
} else {
- $this->printText(json_encode($this->getResultData()));
+ $this->printText($prefix . json_encode($this->getResultData()) . $suffix);
}
}
+ protected function getAllowedParams() {
+ return array (
+ 'callback' => null
+ );
+ }
+
+ protected function getParamDescription() {
+ return array (
+ 'callback' => 'If specified, wraps the output into a given function call',
+ );
+ }
+
protected function getDescription() {
if ($this->mIsRaw)
return 'Output data with the debuging elements in JSON format' . parent :: getDescription();
@@ -65,7 +86,7 @@ class ApiFormatJson extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatJson.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiFormatJson.php 23531 2007-06-29 01:19:14Z simetrical $';
}
}
-?>
+
diff --git a/includes/api/ApiFormatJson_json.php b/includes/api/ApiFormatJson_json.php
index 2cd87930..a8c649c3 100644
--- a/includes/api/ApiFormatJson_json.php
+++ b/includes/api/ApiFormatJson_json.php
@@ -843,4 +843,4 @@ if (class_exists('PEAR_Error')) {
}
-?>
+
diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php
index add63362..766d7041 100644
--- a/includes/api/ApiFormatPhp.php
+++ b/includes/api/ApiFormatPhp.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -50,7 +50,7 @@ class ApiFormatPhp extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatPhp.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiFormatPhp.php 23531 2007-06-29 01:19:14Z simetrical $';
}
}
-?>
+
diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php
index bc720490..0ddfac73 100644
--- a/includes/api/ApiFormatWddx.php
+++ b/includes/api/ApiFormatWddx.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -85,7 +85,7 @@ class ApiFormatWddx extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatWddx.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiFormatWddx.php 23531 2007-06-29 01:19:14Z simetrical $';
}
}
-?>
+
diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php
index 7d54b441..02647923 100644
--- a/includes/api/ApiFormatXml.php
+++ b/includes/api/ApiFormatXml.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -141,7 +141,7 @@ class ApiFormatXml extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatXml.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiFormatXml.php 23531 2007-06-29 01:19:14Z simetrical $';
}
}
-?>
+
diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php
index 0107eb2b..400c0a4b 100644
--- a/includes/api/ApiFormatYaml.php
+++ b/includes/api/ApiFormatYaml.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -50,7 +50,7 @@ class ApiFormatYaml extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatYaml.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiFormatYaml.php 23531 2007-06-29 01:19:14Z simetrical $';
}
}
-?>
+
diff --git a/includes/api/ApiFormatYaml_spyc.php b/includes/api/ApiFormatYaml_spyc.php
index a67bbb22..b3ccff0f 100644
--- a/includes/api/ApiFormatYaml_spyc.php
+++ b/includes/api/ApiFormatYaml_spyc.php
@@ -857,4 +857,4 @@
return $ret;
}
}
-?>
+
diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php
index 7c5144fd..9f1e88ea 100644
--- a/includes/api/ApiHelp.php
+++ b/includes/api/ApiHelp.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,8 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * This is a simple class to handle action=help
+ *
* @addtogroup API
*/
class ApiHelp extends ApiBase {
@@ -51,7 +53,7 @@ class ApiHelp extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiHelp.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiHelp.php 23531 2007-06-29 01:19:14Z simetrical $';
}
}
-?>
+
diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php
index 147d37a1..af68b29d 100644
--- a/includes/api/ApiLogin.php
+++ b/includes/api/ApiLogin.php
@@ -5,7 +5,8 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006-2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com,
+ * Daniel Cannon (cannon dot danielc at gmail dot com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,18 +30,60 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * Unit to authenticate log-in attempts to the current wiki.
+ *
* @addtogroup API
*/
class ApiLogin extends ApiBase {
-
+
+ /**
+ * Time (in seconds) a user must wait after submitting
+ * a bad login (will be multiplied by the THROTTLE_FACTOR for each bad attempt)
+ */
+ const THROTTLE_TIME = 1;
+
+ /**
+ * The factor by which the wait-time in between authentication
+ * attempts is increased every failed attempt.
+ */
+ const THROTTLE_FACTOR = 2;
+
+ /**
+ * The maximum number of failed logins after which the wait increase stops.
+ */
+ const THOTTLE_MAX_COUNT = 10;
+
public function __construct($main, $action) {
parent :: __construct($main, $action, 'lg');
}
+ /**
+ * Executes the log-in attempt using the parameters passed. If
+ * the log-in succeeeds, it attaches a cookie to the session
+ * and outputs the user id, username, and session token. If a
+ * log-in fails, as the result of a bad password, a nonexistant
+ * user, or any other reason, the host is cached with an expiry
+ * and no log-in attempts will be accepted until that expiry
+ * is reached. The expiry is $this->mLoginThrottle.
+ *
+ * @access public
+ */
public function execute() {
$name = $password = $domain = null;
extract($this->extractRequestParams());
+ $result = array ();
+
+ // Make sure noone is trying to guess the password brut-force
+ $nextLoginIn = $this->getNextLoginTimeout();
+ if ($nextLoginIn > 0) {
+ $result['result'] = 'NeedToWait';
+ $result['details'] = "Please wait $nextLoginIn seconds before next log-in attempt";
+ $result['wait'] = $nextLoginIn;
+ $this->getResult()->addValue(null, 'login', $result);
+ return;
+ }
+
$params = new FauxRequest(array (
'wpName' => $name,
'wpPassword' => $password,
@@ -48,8 +91,6 @@ class ApiLogin extends ApiBase {
'wpRemember' => ''
));
- $result = array ();
-
$loginForm = new LoginForm($params);
switch ($loginForm->authenticateUserData()) {
case LoginForm :: SUCCESS :
@@ -86,9 +127,89 @@ class ApiLogin extends ApiBase {
ApiBase :: dieDebug(__METHOD__, 'Unhandled case value');
}
+ if ($result['result'] != 'Success') {
+ $result['wait'] = $this->cacheBadLogin();
+ }
+ // if we were allowed to try to login, memcache is fine
+
$this->getResult()->addValue(null, 'login', $result);
}
+
+ /**
+ * Caches a bad-login attempt associated with the host and with an
+ * expiry of $this->mLoginThrottle. These are cached by a key
+ * separate from that used by the captcha system--as such, logging
+ * in through the standard interface will get you a legal session
+ * and cookies to prove it, but will not remove this entry.
+ *
+ * Returns the number of seconds until next login attempt will be allowed.
+ *
+ * @access private
+ */
+ private function cacheBadLogin() {
+ global $wgMemc;
+
+ $key = $this->getMemCacheKey();
+ $val = $wgMemc->get( $key );
+
+ $val['lastReqTime'] = time();
+ if (!isset($val['count'])) {
+ $val['count'] = 1;
+ } else {
+ $val['count'] = 1 + $val['count'];
+ }
+
+ $delay = ApiLogin::calculateDelay($val['count']);
+
+ $wgMemc->delete($key);
+ // Cache expiration should be the maximum timeout - to prevent a "try and wait" attack
+ $wgMemc->add( $key, $val, ApiLogin::calculateDelay(ApiLogin::THOTTLE_MAX_COUNT) );
+
+ return $delay;
+ }
+
+ /**
+ * How much time the client must wait before it will be
+ * allowed to try to log-in next.
+ * The return value is 0 if no wait is required.
+ */
+ private function getNextLoginTimeout() {
+ global $wgMemc;
+
+ $val = $wgMemc->get($this->getMemCacheKey());
+
+ $elapse = (time() - $val['lastReqTime']); // in seconds
+ $canRetryIn = ApiLogin::calculateDelay($val['count']) - $elapse;
+
+ return $canRetryIn < 0 ? 0 : $canRetryIn;
+ }
+
+ /**
+ * Based on the number of previously attempted logins, returns
+ * the delay (in seconds) when the next login attempt will be allowed.
+ */
+ private static function calculateDelay($count) {
+ // Defensive programming
+ $count = intval($count);
+ $count = $count < 1 ? 1 : $count;
+ $count = $count > self::THOTTLE_MAX_COUNT ? self::THOTTLE_MAX_COUNT : $count;
+
+ return self::THROTTLE_TIME + self::THROTTLE_TIME * ($count - 1) * self::THROTTLE_FACTOR;
+ }
+
+ /**
+ * Internal cache key for badlogin checks. Robbed from the
+ * ConfirmEdit extension and modified to use a key unique to the
+ * API login.3
+ *
+ * @return string
+ * @access private
+ */
+ private function getMemCacheKey() {
+ return wfMemcKey( 'apilogin', 'badlogin', 'ip', wfGetIP() );
+ }
+
protected function getAllowedParams() {
return array (
'name' => null,
@@ -107,7 +228,11 @@ class ApiLogin extends ApiBase {
protected function getDescription() {
return array (
- 'This module is used to login and get the authentication tokens.'
+ 'This module is used to login and get the authentication tokens. ',
+ 'In the event of a successful log-in, a cookie will be attached',
+ 'to your session. In the event of a failed log-in, you will not ',
+ 'be able to attempt another log-in through this method for 60 seconds.',
+ 'This is to prevent password guessing by automated password crackers.'
);
}
@@ -118,7 +243,7 @@ class ApiLogin extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiLogin.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiLogin.php 24695 2007-08-09 09:53:05Z yurik $';
}
}
-?>
+
diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php
index 9a6b0f83..31870449 100644
--- a/includes/api/ApiMain.php
+++ b/includes/api/ApiMain.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,7 +29,16 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * This is the main API class, used for both external and internal processing.
+ * This is the main API class, used for both external and internal processing.
+ * When executed, it will create the requested formatter object,
+ * instantiate and execute an object associated with the needed action,
+ * and use formatter to print results.
+ * In case of an exception, an error message will be printed using the same formatter.
+ *
+ * To use API from another application, run it using FauxRequest object, in which
+ * case any internal exceptions will not be handled but passed up to the caller.
+ * After successful execution, use getResult() for the resulting data.
+ *
* @addtogroup API
*/
class ApiMain extends ApiBase {
@@ -43,11 +52,11 @@ class ApiMain extends ApiBase {
* List of available modules: action name => module class
*/
private static $Modules = array (
- 'help' => 'ApiHelp',
'login' => 'ApiLogin',
+ 'query' => 'ApiQuery',
'opensearch' => 'ApiOpenSearch',
'feedwatchlist' => 'ApiFeedWatchlist',
- 'query' => 'ApiQuery'
+ 'help' => 'ApiHelp',
);
/**
@@ -68,10 +77,11 @@ class ApiMain extends ApiBase {
);
private $mPrinter, $mModules, $mModuleNames, $mFormats, $mFormatNames;
- private $mResult, $mShowVersions, $mEnableWrite, $mRequest, $mInternalMode, $mSquidMaxage;
+ private $mResult, $mAction, $mShowVersions, $mEnableWrite, $mRequest, $mInternalMode, $mSquidMaxage;
/**
- * Constructor
+ * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
+ *
* @param $request object - if this is an instance of FauxRequest, errors are thrown and no printing occurs
* @param $enableWrite bool should be set to true if the api may modify data
*/
@@ -82,7 +92,23 @@ class ApiMain extends ApiBase {
// Special handling for the main module: $parent === $this
parent :: __construct($this, $this->mInternalMode ? 'main_int' : 'main');
- $this->mModules = self :: $Modules;
+ if (!$this->mInternalMode) {
+
+ // Impose module restrictions.
+ // If the current user cannot read,
+ // Remove all modules other than login
+ global $wgUser;
+ if (!$wgUser->isAllowed('read')) {
+ self::$Modules = array(
+ 'login' => self::$Modules['login'],
+ 'help' => self::$Modules['help']
+ );
+ }
+ }
+
+ global $wgAPIModules; // extension modules
+ $this->mModules = $wgAPIModules + self :: $Modules;
+
$this->mModuleNames = array_keys($this->mModules); // todo: optimize
$this->mFormats = self :: $Formats;
$this->mFormatNames = array_keys($this->mFormats); // todo: optimize
@@ -96,28 +122,53 @@ class ApiMain extends ApiBase {
$this->mSquidMaxage = 0;
}
- public function & getRequest() {
+ /**
+ * Return true if the API was started by other PHP code using FauxRequest
+ */
+ public function isInternalMode() {
+ return $this->mInternalMode;
+ }
+
+ /**
+ * Return the request object that contains client's request
+ */
+ public function getRequest() {
return $this->mRequest;
}
+ /**
+ * Get the ApiResult object asscosiated with current request
+ */
public function getResult() {
return $this->mResult;
}
+ /**
+ * This method will simply cause an error if the write mode was disabled for this api.
+ */
public function requestWriteMode() {
if (!$this->mEnableWrite)
$this->dieUsage('Editing of this site is disabled. Make sure the $wgEnableWriteAPI=true; ' .
'statement is included in the site\'s LocalSettings.php file', 'readonly');
}
+ /**
+ * Set how long the response should be cached.
+ */
public function setCacheMaxAge($maxage) {
$this->mSquidMaxage = $maxage;
}
+ /**
+ * Create an instance of an output formatter by its name
+ */
public function createPrinterByName($format) {
return new $this->mFormats[$format] ($this, $format);
}
+ /**
+ * Execute api request. Any errors will be handled if the API was called by the remote client.
+ */
public function execute() {
$this->profileIn();
if ($this->mInternalMode)
@@ -127,10 +178,14 @@ class ApiMain extends ApiBase {
$this->profileOut();
}
+ /**
+ * Execute an action, and in case of an error, erase whatever partial results
+ * have been accumulated, and replace it with an error message and a help screen.
+ */
protected function executeActionWithErrorHandling() {
// In case an error occurs during data output,
- // this clear the output buffer and print just the error information
+ // clear the output buffer and print just the error information
ob_start();
try {
@@ -142,12 +197,51 @@ class ApiMain extends ApiBase {
// handler will process and log it.
//
+ $errCode = $this->substituteResultWithError($e);
+
// Error results should not be cached
$this->setCacheMaxAge(0);
+ $headerStr = 'MediaWiki-API-Error: ' . $errCode;
+ if ($e->getCode() === 0)
+ header($headerStr, true);
+ else
+ header($headerStr, true, $e->getCode());
+
+ // Reset and print just the error message
+ ob_clean();
+
+ // If the error occured during printing, do a printer->profileOut()
+ $this->mPrinter->safeProfileOut();
+ $this->printResult(true);
+ }
+
+ // Set the cache expiration at the last moment, as any errors may change the expiration.
+ // if $this->mSquidMaxage == 0, the expiry time is set to the first second of unix epoch
+ $expires = $this->mSquidMaxage == 0 ? 1 : time() + $this->mSquidMaxage;
+ header('Expires: ' . wfTimestamp(TS_RFC2822, $expires));
+ header('Cache-Control: s-maxage=' . $this->mSquidMaxage . ', must-revalidate, max-age=0');
+
+ if($this->mPrinter->getIsHtml())
+ echo wfReportTime();
+
+ ob_end_flush();
+ }
+
+ /**
+ * Replace the result data with the information about an exception.
+ * Returns the error code
+ */
+ protected function substituteResultWithError($e) {
+
// Printer may not be initialized if the extractRequestParams() fails for the main module
if (!isset ($this->mPrinter)) {
- $this->mPrinter = $this->createPrinterByName(self :: API_DEFAULT_FORMAT);
+ // The printer has not been created yet. Try to manually get formatter value.
+ $value = $this->getRequest()->getVal('format', self::API_DEFAULT_FORMAT);
+ if (!in_array($value, $this->mFormatNames))
+ $value = self::API_DEFAULT_FORMAT;
+
+ $this->mPrinter = $this->createPrinterByName($value);
if ($this->mPrinter->getNeedsRawData())
$this->getResult()->setRawMode();
}
@@ -157,8 +251,12 @@ class ApiMain extends ApiBase {
// User entered incorrect parameters - print usage screen
//
$errMessage = array (
- 'code' => $e->getCodeString(), 'info' => $e->getMessage());
- ApiResult :: setContent($errMessage, $this->makeHelpMsg());
+ 'code' => $e->getCodeString(),
+ 'info' => $e->getMessage());
+
+ // Only print the help message when this is for the developer, not runtime
+ if ($this->mPrinter->getIsHtml() || $this->mAction == 'help')
+ ApiResult :: setContent($errMessage, $this->makeHelpMsg());
} else {
//
@@ -171,41 +269,24 @@ class ApiMain extends ApiBase {
ApiResult :: setContent($errMessage, "\n\n{$e->getTraceAsString()}\n\n");
}
- $headerStr = 'MediaWiki-API-Error: ' . $errMessage['code'];
- if ($e->getCode() === 0)
- header($headerStr, true);
- else
- header($headerStr, true, $e->getCode());
-
- // Reset and print just the error message
- ob_clean();
$this->getResult()->reset();
$this->getResult()->addValue(null, 'error', $errMessage);
- // If the error occured during printing, do a printer->profileOut()
- $this->mPrinter->safeProfileOut();
- $this->printResult(true);
- }
-
- // Set the cache expiration at the last moment, as any errors may change the expiration.
- // if $this->mSquidMaxage == 0, the expiry time is set to the first second of unix epoch
- $expires = $this->mSquidMaxage == 0 ? 1 : time() + $this->mSquidMaxage;
- header('Expires: ' . wfTimestamp(TS_RFC2822, $expires));
- header('Cache-Control: s-maxage=' . $this->mSquidMaxage . ', must-revalidate, max-age=0');
-
- ob_end_flush();
+ return $errMessage['code'];
}
/**
* Execute the actual module, without any error handling
*/
protected function executeAction() {
- $action = $format = $version = null;
- extract($this->extractRequestParams());
- $this->mShowVersions = $version;
+
+ $params = $this->extractRequestParams();
+
+ $this->mShowVersions = $params['version'];
+ $this->mAction = $params['action'];
// Instantiate the module requested by the user
- $module = new $this->mModules[$action] ($this, $action);
+ $module = new $this->mModules[$this->mAction] ($this, $this->mAction);
if (!$this->mInternalMode) {
@@ -213,7 +294,7 @@ class ApiMain extends ApiBase {
$this->mPrinter = $module->getCustomPrinter();
if (is_null($this->mPrinter)) {
// Create an appropriate printer
- $this->mPrinter = $this->createPrinterByName($format);
+ $this->mPrinter = $this->createPrinterByName($params['format']);
}
if ($this->mPrinter->getNeedsRawData())
@@ -232,7 +313,7 @@ class ApiMain extends ApiBase {
}
/**
- * Internal printer
+ * Print results using the current printer
*/
protected function printResult($isError) {
$printer = $this->mPrinter;
@@ -243,6 +324,9 @@ class ApiMain extends ApiBase {
$printer->profileOut();
}
+ /**
+ * See ApiBase for description.
+ */
protected function getAllowedParams() {
return array (
'format' => array (
@@ -257,6 +341,9 @@ class ApiMain extends ApiBase {
);
}
+ /**
+ * See ApiBase for description.
+ */
protected function getParamDescription() {
return array (
'format' => 'The format of the output',
@@ -265,24 +352,44 @@ class ApiMain extends ApiBase {
);
}
+ /**
+ * See ApiBase for description.
+ */
protected function getDescription() {
return array (
'',
- 'This API allows programs to access various functions of MediaWiki software.',
- 'For more details see API Home Page @ http://meta.wikimedia.org/wiki/API',
'',
- 'Status: ALPHA -- all features shown on this page should be working,',
- ' but the API is still in active development, and may change at any time.',
- ' Make sure you monitor changes to this page, wikitech-l mailing list,',
- ' or the source code in the includes/api directory for any changes.',
- ''
+ '******************************************************************',
+ '** **',
+ '** This is an auto-generated MediaWiki API documentation page **',
+ '** **',
+ '** Documentation and Examples: **',
+ '** http://www.mediawiki.org/wiki/API **',
+ '** **',
+ '******************************************************************',
+ '',
+ 'Status: All features shown on this page should be working, but the API',
+ ' is still in active development, and may change at any time.',
+ ' Make sure to monitor our mailing list for any updates.',
+ '',
+ 'Documentation: http://www.mediawiki.org/wiki/API',
+ 'Mailing list: http://lists.wikimedia.org/mailman/listinfo/mediawiki-api',
+ 'Bugs & Requests: http://bugzilla.wikimedia.org/buglist.cgi?component=API&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts',
+ '',
+ '',
+ '',
+ '',
+ '',
);
}
+ /**
+ * Returns an array of strings with credits for the API
+ */
protected function getCredits() {
return array(
- 'This API is being implemented by Yuri Astrakhan [[User:Yurik]] / FirstnameLastname@gmail.com',
- 'Please leave your comments and suggestions at http://meta.wikimedia.org/wiki/API'
+ 'This API is being implemented by Yuri Astrakhan [[User:Yurik]] / <Firstname><Lastname>@gmail.com',
+ 'Please leave your comments and suggestions at http://www.mediawiki.org/wiki/API'
);
}
@@ -297,8 +404,8 @@ class ApiMain extends ApiBase {
$astriks = str_repeat('*** ', 10);
$msg .= "\n\n$astriks Modules $astriks\n\n";
foreach( $this->mModules as $moduleName => $unused ) {
- $msg .= "* action=$moduleName *";
$module = new $this->mModules[$moduleName] ($this, $moduleName);
+ $msg .= self::makeHelpMsgHeader($module, 'action');
$msg2 = $module->makeHelpMsg();
if ($msg2 !== false)
$msg .= $msg2;
@@ -307,8 +414,8 @@ class ApiMain extends ApiBase {
$msg .= "\n$astriks Formats $astriks\n\n";
foreach( $this->mFormats as $formatName => $unused ) {
- $msg .= "* format=$formatName *";
$module = $this->createPrinterByName($formatName);
+ $msg .= self::makeHelpMsgHeader($module, 'format');
$msg2 = $module->makeHelpMsg();
if ($msg2 !== false)
$msg .= $msg2;
@@ -321,7 +428,21 @@ class ApiMain extends ApiBase {
return $msg;
}
+ public static function makeHelpMsgHeader($module, $paramName) {
+ $modulePrefix = $module->getModulePrefix();
+ if (!empty($modulePrefix))
+ $modulePrefix = "($modulePrefix) ";
+
+ return "* $paramName={$module->getModuleName()} $modulePrefix*";
+ }
+
private $mIsBot = null;
+
+ private $mIsSysop = null;
+
+ /**
+ * Returns true if the currently logged in user is a bot, false otherwise
+ */
public function isBot() {
if (!isset ($this->mIsBot)) {
global $wgUser;
@@ -329,24 +450,69 @@ class ApiMain extends ApiBase {
}
return $this->mIsBot;
}
+
+ /**
+ * Similar to isBot(), this method returns true if the logged in user is
+ * a sysop, and false if not.
+ */
+ public function isSysop() {
+ if (!isset ($this->mIsSysop)) {
+ global $wgUser;
+ $this->mIsSysop = in_array( 'sysop', $wgUser->getGroups());
+ }
+
+ return $this->mIsSysop;
+ }
public function getShowVersions() {
return $this->mShowVersions;
}
+ /**
+ * Returns the version information of this file, plus it includes
+ * the versions for all files that are not callable proper API modules
+ */
public function getVersion() {
$vers = array ();
- $vers[] = __CLASS__ . ': $Id: ApiMain.php 21402 2007-04-20 08:55:14Z nickj $';
+ $vers[] = 'MediaWiki ' . SpecialVersion::getVersion();
+ $vers[] = __CLASS__ . ': $Id: ApiMain.php 25364 2007-08-31 15:23:48Z tstarling $';
$vers[] = ApiBase :: getBaseVersion();
$vers[] = ApiFormatBase :: getBaseVersion();
$vers[] = ApiQueryBase :: getBaseVersion();
$vers[] = ApiFormatFeedWrapper :: getVersion(); // not accessible with format=xxx
return $vers;
}
+
+ /**
+ * Add or overwrite a module in this ApiMain instance. Intended for use by extending
+ * classes who wish to add their own modules to their lexicon or override the
+ * behavior of inherent ones.
+ *
+ * @access protected
+ * @param $mdlName String The identifier for this module.
+ * @param $mdlClass String The class where this module is implemented.
+ */
+ protected function addModule( $mdlName, $mdlClass ) {
+ $this->mModules[$mdlName] = $mdlClass;
+ }
+
+ /**
+ * Add or overwrite an output format for this ApiMain. Intended for use by extending
+ * classes who wish to add to or modify current formatters.
+ *
+ * @access protected
+ * @param $fmtName The identifier for this format.
+ * @param $fmtClass The class implementing this format.
+ */
+ protected function addFormat( $fmtName, $fmtClass ) {
+ $this->mFormats[$fmtName] = $fmtClass;
+ }
}
/**
* This exception will be thrown when dieUsage is called to stop module execution.
+ * The exception handling code will print a help screen explaining how this API may be used.
+ *
* @addtogroup API
*/
class UsageException extends Exception {
@@ -364,4 +530,4 @@ class UsageException extends Exception {
return "{$this->getCodeString()}: {$this->getMessage()}";
}
}
-?>
+
diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php
index 77f8b889..8484b163 100644
--- a/includes/api/ApiOpenSearch.php
+++ b/includes/api/ApiOpenSearch.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -42,8 +42,8 @@ class ApiOpenSearch extends ApiBase {
}
public function execute() {
- $search = null;
- extract($this->ExtractRequestParams());
+ $params = $this->extractRequestParams();
+ $search = $params['search'];
// Open search results may be stored for a very long time
$this->getMain()->setCacheMaxAge(1200);
@@ -53,7 +53,7 @@ class ApiOpenSearch extends ApiBase {
return; // Return empty result
// Prepare nested request
- $params = new FauxRequest(array (
+ $req = new FauxRequest(array (
'action' => 'query',
'list' => 'allpages',
'apnamespace' => $title->getNamespace(),
@@ -62,7 +62,7 @@ class ApiOpenSearch extends ApiBase {
));
// Execute
- $module = new ApiMain($params);
+ $module = new ApiMain($req);
$module->execute();
// Get resulting data
@@ -105,7 +105,7 @@ class ApiOpenSearch extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiOpenSearch.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiOpenSearch.php 24099 2007-07-15 00:52:35Z yurik $';
}
}
-?>
+
diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php
index dea87b88..185c0c59 100644
--- a/includes/api/ApiPageSet.php
+++ b/includes/api/ApiPageSet.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,14 +29,25 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * This class contains a list of pages that the client has requested.
+ * Initially, when the client passes in titles=, pageids=, or revisions= parameter,
+ * an instance of the ApiPageSet class will normalize titles,
+ * determine if the pages/revisions exist, and prefetch any additional data page data requested.
+ *
+ * When generator is used, the result of the generator will become the input for the
+ * second instance of this class, and all subsequent actions will go use the second instance
+ * for all their work.
+ *
* @addtogroup API
*/
class ApiPageSet extends ApiQueryBase {
private $mAllPages; // [ns][dbkey] => page_id or 0 when missing
- private $mTitles, $mGoodTitles, $mMissingTitles, $mMissingPageIDs, $mRedirectTitles, $mNormalizedTitles;
+ private $mTitles, $mGoodTitles, $mMissingTitles, $mMissingPageIDs, $mRedirectTitles;
+ private $mNormalizedTitles, $mInterwikiTitles;
private $mResolveRedirects, $mPendingRedirectIDs;
private $mGoodRevIDs, $mMissingRevIDs;
+ private $mFakePageId;
private $mRequestedPageFields;
@@ -50,6 +61,7 @@ class ApiPageSet extends ApiQueryBase {
$this->mMissingPageIDs = array ();
$this->mRedirectTitles = array ();
$this->mNormalizedTitles = array ();
+ $this->mInterwikiTitles = array ();
$this->mGoodRevIDs = array();
$this->mMissingRevIDs = array();
@@ -57,6 +69,8 @@ class ApiPageSet extends ApiQueryBase {
$this->mResolveRedirects = $resolveRedirects;
if($resolveRedirects)
$this->mPendingRedirectIDs = array();
+
+ $this->mFakePageId = -1;
}
public function isResolvingRedirects() {
@@ -88,7 +102,16 @@ class ApiPageSet extends ApiQueryBase {
if ($this->mResolveRedirects)
$pageFlds['page_is_redirect'] = null;
- return array_keys(array_merge($pageFlds, $this->mRequestedPageFields));
+ $pageFlds = array_merge($pageFlds, $this->mRequestedPageFields);
+ return array_keys($pageFlds);
+ }
+
+ /**
+ * Returns an array [ns][dbkey] => page_id for all requested titles
+ * page_id is a unique negative number in case title was not found
+ */
+ public function getAllTitlesByNamespace() {
+ return $this->mAllPages;
}
/**
@@ -123,6 +146,7 @@ class ApiPageSet extends ApiQueryBase {
/**
* Title objects that were NOT found in the database.
+ * The array's index will be negative for each item
* @return array of Title objects
*/
public function getMissingTitles() {
@@ -155,6 +179,15 @@ class ApiPageSet extends ApiQueryBase {
}
/**
+ * Get a list of interwiki titles - maps the title given
+ * with to the interwiki prefix.
+ * @return array raw_prefixed_title (string) => interwiki_prefix (string)
+ */
+ public function getInterwikiTitles() {
+ return $this->mInterwikiTitles;
+ }
+
+ /**
* Get the list of revision IDs (requested with revids= parameter)
* @return array revID (int) => pageID (int)
*/
@@ -233,7 +266,6 @@ class ApiPageSet extends ApiQueryBase {
*/
public function populateFromPageIDs($pageIDs) {
$this->profileIn();
- $pageIDs = array_map('intval', $pageIDs); // paranoia
$this->initFromPageIds($pageIDs);
$this->profileOut();
}
@@ -265,22 +297,18 @@ class ApiPageSet extends ApiQueryBase {
// Store Title object in various data structures
$title = Title :: makeTitle($row->page_namespace, $row->page_title);
- // skip any pages that user has no rights to read
- if ($title->userCanRead()) {
-
- $pageId = intval($row->page_id);
- $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId;
- $this->mTitles[] = $title;
-
- if ($this->mResolveRedirects && $row->page_is_redirect == '1') {
- $this->mPendingRedirectIDs[$pageId] = $title;
- } else {
- $this->mGoodTitles[$pageId] = $title;
- }
-
- foreach ($this->mRequestedPageFields as $fieldName => & $fieldValues)
- $fieldValues[$pageId] = $row-> $fieldName;
+ $pageId = intval($row->page_id);
+ $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId;
+ $this->mTitles[] = $title;
+
+ if ($this->mResolveRedirects && $row->page_is_redirect == '1') {
+ $this->mPendingRedirectIDs[$pageId] = $title;
+ } else {
+ $this->mGoodTitles[$pageId] = $title;
}
+
+ foreach ($this->mRequestedPageFields as $fieldName => & $fieldValues)
+ $fieldValues[$pageId] = $row-> $fieldName;
}
public function finishPageSetGeneration() {
@@ -306,7 +334,7 @@ class ApiPageSet extends ApiQueryBase {
private function initFromTitles($titles) {
// Get validated and normalized title objects
- $linkBatch = $this->processTitlesStrArray($titles);
+ $linkBatch = $this->processTitlesArray($titles);
if($linkBatch->isEmpty())
return;
@@ -328,7 +356,8 @@ class ApiPageSet extends ApiQueryBase {
private function initFromPageIds($pageids) {
if(empty($pageids))
return;
-
+
+ $pageids = array_map('intval', $pageids); // paranoia
$set = array (
'page_id' => $pageids
);
@@ -386,8 +415,9 @@ class ApiPageSet extends ApiQueryBase {
foreach ($remaining as $ns => $dbkeys) {
foreach ( $dbkeys as $dbkey => $unused ) {
$title = Title :: makeTitle($ns, $dbkey);
- $this->mMissingTitles[] = $title;
- $this->mAllPages[$ns][$dbkey] = 0;
+ $this->mAllPages[$ns][$dbkey] = $this->mFakePageId;
+ $this->mMissingTitles[$this->mFakePageId] = $title;
+ $this->mFakePageId--;
$this->mTitles[] = $title;
}
}
@@ -536,39 +566,46 @@ class ApiPageSet extends ApiQueryBase {
/**
* Given an array of title strings, convert them into Title objects.
+ * Alternativelly, an array of Title objects may be given.
* This method validates access rights for the title,
* and appends normalization values to the output.
*
* @return LinkBatch of title objects.
*/
- private function processTitlesStrArray($titles) {
+ private function processTitlesArray($titles) {
$linkBatch = new LinkBatch();
- foreach ($titles as $titleString) {
- $titleObj = Title :: newFromText($titleString);
-
- // Validation
+ foreach ($titles as $title) {
+
+ $titleObj = is_string($title) ? Title :: newFromText($title) : $title;
if (!$titleObj)
- $this->dieUsage("bad title $titleString", 'invalidtitle');
- if ($titleObj->getNamespace() < 0)
- $this->dieUsage("No support for special page $titleString has been implemented", 'unsupportednamespace');
- if (!$titleObj->userCanRead())
- $this->dieUsage("No read permission for $titleString", 'titleaccessdenied');
+ $this->dieUsage("bad title", 'invalidtitle');
- $linkBatch->addObj($titleObj);
+ $iw = $titleObj->getInterwiki();
+ if (!empty($iw)) {
+ // This title is an interwiki link.
+ $this->mInterwikiTitles[$titleObj->getPrefixedText()] = $iw;
+ } else {
+ // Validation
+ if ($titleObj->getNamespace() < 0)
+ $this->dieUsage("No support for special pages has been implemented", 'unsupportednamespace');
+
+ $linkBatch->addObj($titleObj);
+ }
+
// Make sure we remember the original title that was given to us
// This way the caller can correlate new titles with the originally requested,
// i.e. namespace is localized or capitalization is different
- if ($titleString !== $titleObj->getPrefixedText()) {
- $this->mNormalizedTitles[$titleString] = $titleObj->getPrefixedText();
+ if (is_string($title) && $title !== $titleObj->getPrefixedText()) {
+ $this->mNormalizedTitles[$title] = $titleObj->getPrefixedText();
}
}
return $linkBatch;
}
-
+
protected function getAllowedParams() {
return array (
'titles' => array (
@@ -594,7 +631,7 @@ class ApiPageSet extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiPageSet.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiPageSet.php 24935 2007-08-20 08:13:16Z nickj $';
}
}
-?>
+
diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php
index 6ee05085..76dbb338 100644
--- a/includes/api/ApiQuery.php
+++ b/includes/api/ApiQuery.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,49 +29,67 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * This is the main query class. It behaves similar to ApiMain: based on the parameters given,
+ * it will create a list of titles to work on (an instance of the ApiPageSet object)
+ * instantiate and execute various property/list/meta modules,
+ * and assemble all resulting data into a single ApiResult object.
+ *
+ * In the generator mode, a generator will be first executed to populate a second ApiPageSet object,
+ * and that object will be used for all subsequent modules.
+ *
* @addtogroup API
*/
class ApiQuery extends ApiBase {
private $mPropModuleNames, $mListModuleNames, $mMetaModuleNames;
private $mPageSet;
+ private $params, $redirect;
private $mQueryPropModules = array (
'info' => 'ApiQueryInfo',
- 'revisions' => 'ApiQueryRevisions'
+ 'revisions' => 'ApiQueryRevisions',
+ 'links' => 'ApiQueryLinks',
+ 'langlinks' => 'ApiQueryLangLinks',
+ 'images' => 'ApiQueryImages',
+ 'imageinfo' => 'ApiQueryImageInfo',
+ 'templates' => 'ApiQueryLinks',
+ 'categories' => 'ApiQueryCategories',
+ 'extlinks' => 'ApiQueryExternalLinks',
);
- // 'categories' => 'ApiQueryCategories',
- // 'imageinfo' => 'ApiQueryImageinfo',
- // 'langlinks' => 'ApiQueryLanglinks',
- // 'links' => 'ApiQueryLinks',
- // 'templates' => 'ApiQueryTemplates',
private $mQueryListModules = array (
'allpages' => 'ApiQueryAllpages',
- 'logevents' => 'ApiQueryLogEvents',
- 'watchlist' => 'ApiQueryWatchlist',
- 'recentchanges' => 'ApiQueryRecentChanges',
+ 'alllinks' => 'ApiQueryAllLinks',
+ 'allusers' => 'ApiQueryAllUsers',
'backlinks' => 'ApiQueryBacklinks',
+ 'categorymembers' => 'ApiQueryCategoryMembers',
'embeddedin' => 'ApiQueryBacklinks',
- 'imagelinks' => 'ApiQueryBacklinks',
- 'usercontribs' => 'ApiQueryContributions'
+ 'imageusage' => 'ApiQueryBacklinks',
+ 'logevents' => 'ApiQueryLogEvents',
+ 'recentchanges' => 'ApiQueryRecentChanges',
+ 'search' => 'ApiQuerySearch',
+ 'usercontribs' => 'ApiQueryContributions',
+ 'watchlist' => 'ApiQueryWatchlist',
+ 'exturlusage' => 'ApiQueryExtLinksUsage',
);
- // 'categorymembers' => 'ApiQueryCategorymembers',
- // 'embeddedin' => 'ApiQueryEmbeddedin',
- // 'imagelinks' => 'ApiQueryImagelinks',
- // 'recentchanges' => 'ApiQueryRecentchanges',
- // 'users' => 'ApiQueryUsers',
- // 'watchlist' => 'ApiQueryWatchlist',
private $mQueryMetaModules = array (
- 'siteinfo' => 'ApiQuerySiteinfo'
+ 'siteinfo' => 'ApiQuerySiteinfo',
+ 'userinfo' => 'ApiQueryUserInfo',
);
- // 'userinfo' => 'ApiQueryUserinfo',
private $mSlaveDB = null;
+ private $mNamedDB = array();
public function __construct($main, $action) {
parent :: __construct($main, $action);
+
+ // Allow custom modules to be added in LocalSettings.php
+ global $wgApiQueryPropModules, $wgApiQueryListModules, $wgApiQueryMetaModules;
+ self :: appendUserModules($this->mQueryPropModules, $wgApiQueryPropModules);
+ self :: appendUserModules($this->mQueryListModules, $wgApiQueryListModules);
+ self :: appendUserModules($this->mQueryMetaModules, $wgApiQueryMetaModules);
+
$this->mPropModuleNames = array_keys($this->mQueryPropModules);
$this->mListModuleNames = array_keys($this->mQueryListModules);
$this->mMetaModuleNames = array_keys($this->mQueryMetaModules);
@@ -81,6 +99,20 @@ class ApiQuery extends ApiBase {
$this->mAllowedGenerators = array_merge($this->mListModuleNames, $this->mPropModuleNames);
}
+ /**
+ * Helper function to append any add-in modules to the list
+ */
+ private static function appendUserModules(&$modules, $newModules) {
+ if (is_array( $newModules )) {
+ foreach ( $newModules as $moduleName => $moduleClass) {
+ $modules[$moduleName] = $moduleClass;
+ }
+ }
+ }
+
+ /**
+ * Gets a default slave database connection object
+ */
public function getDB() {
if (!isset ($this->mSlaveDB)) {
$this->profileDBIn();
@@ -90,6 +122,24 @@ class ApiQuery extends ApiBase {
return $this->mSlaveDB;
}
+ /**
+ * Get the query database connection with the given name.
+ * If no such connection has been requested before, it will be created.
+ * Subsequent calls with the same $name will return the same connection
+ * as the first, regardless of $db or $groups new values.
+ */
+ public function getNamedDB($name, $db, $groups) {
+ if (!array_key_exists($name, $this->mNamedDB)) {
+ $this->profileDBIn();
+ $this->mNamedDB[$name] = wfGetDB($db, $groups);
+ $this->profileDBOut();
+ }
+ return $this->mNamedDB[$name];
+ }
+
+ /**
+ * Gets the set of pages the user has requested (or generated)
+ */
public function getPageSet() {
return $this->mPageSet;
}
@@ -105,42 +155,33 @@ class ApiQuery extends ApiBase {
* #5 Execute all requested modules
*/
public function execute() {
- $prop = $list = $meta = $generator = $redirects = null;
- extract($this->extractRequestParams());
-
+
+ $this->params = $this->extractRequestParams();
+ $this->redirects = $this->params['redirects'];
+
//
// Create PageSet
//
- $this->mPageSet = new ApiPageSet($this, $redirects);
-
- // Instantiate required modules
- $modules = array ();
- if (isset ($prop))
- foreach ($prop as $moduleName)
- $modules[] = new $this->mQueryPropModules[$moduleName] ($this, $moduleName);
- if (isset ($list))
- foreach ($list as $moduleName)
- $modules[] = new $this->mQueryListModules[$moduleName] ($this, $moduleName);
- if (isset ($meta))
- foreach ($meta as $moduleName)
- $modules[] = new $this->mQueryMetaModules[$moduleName] ($this, $moduleName);
-
- // Modules may optimize data requests through the $this->getPageSet() object
- // Execute all requested modules.
- foreach ($modules as $module) {
- $module->requestExtraData();
- }
+ $this->mPageSet = new ApiPageSet($this, $this->redirects);
//
- // If given, execute generator to substitute user supplied data with generated data.
+ // Instantiate requested modules
//
- if (isset ($generator))
- $this->executeGeneratorModule($generator, $redirects);
+ $modules = array ();
+ $this->InstantiateModules($modules, 'prop', $this->mQueryPropModules);
+ $this->InstantiateModules($modules, 'list', $this->mQueryListModules);
+ $this->InstantiateModules($modules, 'meta', $this->mQueryMetaModules);
//
- // Populate page information for the given pageSet
+ // If given, execute generator to substitute user supplied data with generated data.
//
- $this->mPageSet->execute();
+ if (isset ($this->params['generator'])) {
+ $this->executeGeneratorModule($this->params['generator'], $modules);
+ } else {
+ // Append custom fields and populate page/revision information
+ $this->addCustomFldsToPageSet($modules, $this->mPageSet);
+ $this->mPageSet->execute();
+ }
//
// Record page information (title, namespace, if exists, etc)
@@ -156,7 +197,33 @@ class ApiQuery extends ApiBase {
$module->profileOut();
}
}
+
+ /**
+ * Query modules may optimize data requests through the $this->getPageSet() object
+ * by adding extra fields from the page table.
+ * This function will gather all the extra request fields from the modules.
+ */
+ private function addCustomFldsToPageSet($modules, $pageSet) {
+ // Query all requested modules.
+ foreach ($modules as $module) {
+ $module->requestExtraData($pageSet);
+ }
+ }
+
+ /**
+ * Create instances of all modules requested by the client
+ */
+ private function InstantiateModules(&$modules, $param, $moduleList) {
+ $list = $this->params[$param];
+ if (isset ($list))
+ foreach ($list as $moduleName)
+ $modules[] = new $moduleList[$moduleName] ($this, $moduleName);
+ }
+ /**
+ * Appends an element for each page in the current pageSet with the most general
+ * information (id, title), plus any title normalizations and missing title/pageids/revids.
+ */
private function outputGeneralPageInfo() {
$pageSet = $this->getPageSet();
@@ -175,7 +242,21 @@ class ApiQuery extends ApiBase {
$result->setIndexedTagName($normValues, 'n');
$result->addValue('query', 'normalized', $normValues);
}
+
+ // Interwiki titles
+ $intrwValues = array ();
+ foreach ($pageSet->getInterwikiTitles() as $rawTitleStr => $interwikiStr) {
+ $intrwValues[] = array (
+ 'title' => $rawTitleStr,
+ 'iw' => $interwikiStr
+ );
+ }
+ if (!empty ($intrwValues)) {
+ $result->setIndexedTagName($intrwValues, 'i');
+ $result->addValue('query', 'interwiki', $intrwValues);
+ }
+
// Show redirect information
$redirValues = array ();
foreach ($pageSet->getRedirectTitles() as $titleStrFrom => $titleStrTo) {
@@ -211,10 +292,11 @@ class ApiQuery extends ApiBase {
$pages = array ();
// Report any missing titles
- $fakepageid = -1;
- foreach ($pageSet->getMissingTitles() as $title) {
- $pages[$fakepageid--] = array (
- 'ns' => $title->getNamespace(), 'title' => $title->getPrefixedText(), 'missing' => '');
+ foreach ($pageSet->getMissingTitles() as $fakeId => $title) {
+ $vals = array();
+ ApiQueryBase :: addTitleInfo($vals, $title);
+ $vals['missing'] = '';
+ $pages[$fakeId] = $vals;
}
// Report any missing page ids
@@ -227,32 +309,43 @@ class ApiQuery extends ApiBase {
// Output general page information for found titles
foreach ($pageSet->getGoodTitles() as $pageid => $title) {
- $pages[$pageid] = array (
- 'pageid' => $pageid,
- 'ns' => $title->getNamespace(), 'title' => $title->getPrefixedText());
+ $vals = array();
+ $vals['pageid'] = $pageid;
+ ApiQueryBase :: addTitleInfo($vals, $title);
+ $pages[$pageid] = $vals;
}
if (!empty ($pages)) {
+
+ if ($this->params['indexpageids']) {
+ $pageIDs = array_keys($pages);
+ // json treats all map keys as strings - converting to match
+ $pageIDs = array_map('strval', $pageIDs);
+ $result->setIndexedTagName($pageIDs, 'id');
+ $result->addValue('query', 'pageids', $pageIDs);
+ }
+
$result->setIndexedTagName($pages, 'page');
$result->addValue('query', 'pages', $pages);
}
}
- protected function executeGeneratorModule($generatorName, $redirects) {
+ /**
+ * For generator mode, execute generator, and use its output as new pageSet
+ */
+ protected function executeGeneratorModule($generatorName, $modules) {
// Find class that implements requested generator
if (isset ($this->mQueryListModules[$generatorName])) {
$className = $this->mQueryListModules[$generatorName];
- }
- elseif (isset ($this->mQueryPropModules[$generatorName])) {
+ } elseif (isset ($this->mQueryPropModules[$generatorName])) {
$className = $this->mQueryPropModules[$generatorName];
} else {
ApiBase :: dieDebug(__METHOD__, "Unknown generator=$generatorName");
}
- // Use current pageset as the result, and create a new one just for the generator
- $resultPageSet = $this->mPageSet;
- $this->mPageSet = new ApiPageSet($this, $redirects);
+ // Generator results
+ $resultPageSet = new ApiPageSet($this, $this->redirects);
// Create and execute the generator
$generator = new $className ($this, $generatorName);
@@ -260,9 +353,12 @@ class ApiQuery extends ApiBase {
$this->dieUsage("Module $generatorName cannot be used as a generator", "badgenerator");
$generator->setGeneratorMode();
- $generator->requestExtraData();
- // execute current pageSet to get the data for the generator module
+ // Add any additional fields modules may need
+ $generator->requestExtraData($this->mPageSet);
+ $this->addCustomFldsToPageSet($modules, $resultPageSet);
+
+ // Populate page information with the original user input
$this->mPageSet->execute();
// populate resultPageSet with the generator output
@@ -275,6 +371,10 @@ class ApiQuery extends ApiBase {
$this->mPageSet = $resultPageSet;
}
+ /**
+ * Returns the list of allowed parameters for this module.
+ * Qurey module also lists all ApiPageSet parameters as its own.
+ */
protected function getAllowedParams() {
return array (
'prop' => array (
@@ -292,7 +392,8 @@ class ApiQuery extends ApiBase {
'generator' => array (
ApiBase :: PARAM_TYPE => $this->mAllowedGenerators
),
- 'redirects' => false
+ 'redirects' => false,
+ 'indexpageids' => false,
);
}
@@ -301,12 +402,12 @@ class ApiQuery extends ApiBase {
*/
public function makeHelpMsg() {
- // Use parent to make default message for the query module
- $msg = parent :: makeHelpMsg();
+ $msg = '';
// Make sure the internal object is empty
// (just in case a sub-module decides to optimize during instantiation)
$this->mPageSet = null;
+ $this->mAllowedGenerators = array(); // Will be repopulated
$astriks = str_repeat('--- ', 8);
$msg .= "\n$astriks Query: Prop $astriks\n\n";
@@ -316,21 +417,32 @@ class ApiQuery extends ApiBase {
$msg .= "\n$astriks Query: Meta $astriks\n\n";
$msg .= $this->makeHelpMsgHelper($this->mQueryMetaModules, 'meta');
+ // Perform the base call last because the $this->mAllowedGenerators
+ // will be updated inside makeHelpMsgHelper()
+ // Use parent to make default message for the query module
+ $msg = parent :: makeHelpMsg() . $msg;
+
return $msg;
}
+ /**
+ * For all modules in $moduleList, generate help messages and join them together
+ */
private function makeHelpMsgHelper($moduleList, $paramName) {
$moduleDscriptions = array ();
foreach ($moduleList as $moduleName => $moduleClass) {
- $msg = "* $paramName=$moduleName *";
$module = new $moduleClass ($this, $moduleName, null);
+
+ $msg = ApiMain::makeHelpMsgHeader($module, $paramName);
$msg2 = $module->makeHelpMsg();
if ($msg2 !== false)
$msg .= $msg2;
- if ($module instanceof ApiQueryGeneratorBase)
+ if ($module instanceof ApiQueryGeneratorBase) {
+ $this->mAllowedGenerators[] = $moduleName;
$msg .= "Generator:\n This module may be used as a generator\n";
+ }
$moduleDscriptions[] = $msg;
}
@@ -351,7 +463,8 @@ class ApiQuery extends ApiBase {
'list' => 'Which lists to get',
'meta' => 'Which meta data to get about the site',
'generator' => 'Use the output of a list as the input for other prop/list/meta items',
- 'redirects' => 'Automatically resolve redirects'
+ 'redirects' => 'Automatically resolve redirects',
+ 'indexpageids' => 'Include an additional pageids section listing all returned page IDs.'
);
}
@@ -372,9 +485,9 @@ class ApiQuery extends ApiBase {
public function getVersion() {
$psModule = new ApiPageSet($this);
$vers = array ();
- $vers[] = __CLASS__ . ': $Id: ApiQuery.php 21402 2007-04-20 08:55:14Z nickj $';
+ $vers[] = __CLASS__ . ': $Id: ApiQuery.php 24494 2007-07-31 17:53:37Z yurik $';
$vers[] = $psModule->getVersion();
return $vers;
}
}
-?>
+
diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php
new file mode 100644
index 00000000..17f24b65
--- /dev/null
+++ b/includes/api/ApiQueryAllLinks.php
@@ -0,0 +1,179 @@
+<?php
+
+/*
+ * Created on July 7, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiQueryBase.php');
+}
+
+/**
+ * Query module to enumerate links from all pages together.
+ *
+ * @addtogroup API
+ */
+class ApiQueryAllLinks extends ApiQueryGeneratorBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'al');
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function executeGenerator($resultPageSet) {
+ $this->run($resultPageSet);
+ }
+
+ private function run($resultPageSet = null) {
+
+ $db = $this->getDB();
+ $params = $this->extractRequestParams();
+
+ $prop = array_flip($params['prop']);
+ $fld_ids = isset($prop['ids']);
+ $fld_title = isset($prop['title']);
+
+ if ($params['unique']) {
+ if (!is_null($resultPageSet))
+ $this->dieUsage($this->getModuleName() . ' cannot be used as a generator in unique links mode', 'params');
+ if ($fld_ids)
+ $this->dieUsage($this->getModuleName() . ' cannot return corresponding page ids in unique links mode', 'params');
+ $this->addOption('DISTINCT');
+ }
+
+ $this->addTables('pagelinks');
+ $this->addWhereFld('pl_namespace', $params['namespace']);
+
+ if (!is_null($params['from']))
+ $this->addWhere('pl_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from'])));
+ if (isset ($params['prefix']))
+ $this->addWhere("pl_title LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'");
+
+ if (is_null($resultPageSet)) {
+ $this->addFields(array (
+ 'pl_namespace',
+ 'pl_title'
+ ));
+ $this->addFieldsIf('pl_from', $fld_ids);
+ } else {
+ $this->addFields('pl_from');
+ $pageids = array();
+ }
+
+ $this->addOption('USE INDEX', 'pl_namespace');
+ $limit = $params['limit'];
+ $this->addOption('LIMIT', $limit+1);
+ $this->addOption('ORDER BY', 'pl_namespace, pl_title');
+
+ $res = $this->select(__METHOD__);
+
+ $data = array ();
+ $count = 0;
+ while ($row = $db->fetchObject($res)) {
+ if (++ $count > $limit) {
+ // We've reached the one extra which shows that there are additional pages to be had. Stop here...
+ // TODO: Security issue - if the user has no right to view next title, it will still be shown
+ $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->pl_title));
+ break;
+ }
+
+ if (is_null($resultPageSet)) {
+ $vals = array();
+ if ($fld_ids)
+ $vals['fromid'] = intval($row->pl_from);
+ if ($fld_title) {
+ $title = Title :: makeTitle($row->pl_namespace, $row->pl_title);
+ $vals['ns'] = intval($title->getNamespace());
+ $vals['title'] = $title->getPrefixedText();
+ }
+ $data[] = $vals;
+ } else {
+ $pageids[] = $row->pl_from;
+ }
+ }
+ $db->freeResult($res);
+
+ if (is_null($resultPageSet)) {
+ $result = $this->getResult();
+ $result->setIndexedTagName($data, 'l');
+ $result->addValue('query', $this->getModuleName(), $data);
+ } else {
+ $resultPageSet->populateFromPageIDs($pageids);
+ }
+ }
+
+ protected function getAllowedParams() {
+ return array (
+ 'from' => null,
+ 'prefix' => null,
+ 'unique' => false,
+ 'prop' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_DFLT => 'title',
+ ApiBase :: PARAM_TYPE => array (
+ 'ids',
+ 'title'
+ )
+ ),
+ 'namespace' => array (
+ ApiBase :: PARAM_DFLT => 0,
+ ApiBase :: PARAM_TYPE => 'namespace'
+ ),
+ 'limit' => array (
+ ApiBase :: PARAM_DFLT => 10,
+ ApiBase :: PARAM_TYPE => 'limit',
+ ApiBase :: PARAM_MIN => 1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
+ )
+ );
+ }
+
+ protected function getParamDescription() {
+ return array (
+ 'from' => 'The page title to start enumerating from.',
+ 'prefix' => 'Search for all page titles that begin with this value.',
+ 'unique' => 'Only show unique links. Cannot be used with generator or prop=ids',
+ 'prop' => 'What pieces of information to include',
+ 'namespace' => 'The namespace to enumerate.',
+ 'limit' => 'How many total links to return.'
+ );
+ }
+
+ protected function getDescription() {
+ return 'Enumerate all links that point to a given namespace';
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=query&list=alllinks&alunique&alfrom=B',
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryAllLinks.php 24453 2007-07-30 08:09:15Z yurik $';
+ }
+}
diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php
new file mode 100644
index 00000000..92bcc1a1
--- /dev/null
+++ b/includes/api/ApiQueryAllUsers.php
@@ -0,0 +1,204 @@
+<?php
+
+/*
+ * Created on July 7, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiQueryBase.php');
+}
+
+/**
+ * Query module to enumerate all registered users.
+ *
+ * @addtogroup API
+ */
+class ApiQueryAllUsers extends ApiQueryBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'au');
+ }
+
+ public function execute() {
+ $db = $this->getDB();
+ $params = $this->extractRequestParams();
+
+ $prop = $params['prop'];
+ if (!is_null($prop)) {
+ $prop = array_flip($prop);
+ $fld_editcount = isset($prop['editcount']);
+ $fld_groups = isset($prop['groups']);
+ } else {
+ $fld_editcount = $fld_groups = false;
+ }
+
+ $limit = $params['limit'];
+ $tables = $db->tableName('user');
+
+ if( !is_null( $params['from'] ) )
+ $this->addWhere( 'user_name >= ' . $db->addQuotes( self::keyToTitle( $params['from'] ) ) );
+
+ if( isset( $params['prefix'] ) )
+ $this->addWhere( 'user_name LIKE "' . $db->escapeLike( self::keyToTitle( $params['prefix'] ) ) . '%"' );
+
+ if (!is_null($params['group'])) {
+ // Filter only users that belong to a given group
+ $tblName = $db->tableName('user_groups');
+ $tables = "$tables INNER JOIN $tblName ug1 ON ug1.ug_user=user_id";
+ $this->addWhereFld('ug1.ug_group', $params['group']);
+ }
+
+ if ($fld_groups) {
+ // Show the groups the given users belong to
+ // request more than needed to avoid not getting all rows that belong to one user
+ $groupCount = count(User::getAllGroups());
+ $sqlLimit = $limit+$groupCount+1;
+
+ $tblName = $db->tableName('user_groups');
+ $tables = "$tables LEFT JOIN $tblName ug2 ON ug2.ug_user=user_id";
+ $this->addFields('ug2.ug_group ug_group2');
+ } else {
+ $sqlLimit = $limit+1;
+ }
+
+ $this->addOption('LIMIT', $sqlLimit);
+ $this->addTables($tables);
+
+ $this->addFields('user_name');
+ $this->addFieldsIf('user_editcount', $fld_editcount);
+
+ $this->addOption('ORDER BY', 'user_name');
+
+ $res = $this->select(__METHOD__);
+
+ $data = array ();
+ $count = 0;
+ $lastUserData = false;
+ $lastUser = false;
+ $result = $this->getResult();
+
+ //
+ // This loop keeps track of the last entry.
+ // For each new row, if the new row is for different user then the last, the last entry is added to results.
+ // Otherwise, the group of the new row is appended to the last entry.
+ // The setContinue... is more complex because of this, and takes into account the higher sql limit
+ // to make sure all rows that belong to the same user are received.
+ //
+ while (true) {
+
+ $row = $db->fetchObject($res);
+ $count++;
+
+ if (!$row || $lastUser != $row->user_name) {
+ // Save the last pass's user data
+ if (is_array($lastUserData))
+ $data[] = $lastUserData;
+
+ // No more rows left
+ if (!$row)
+ break;
+
+ if ($count > $limit) {
+ // We've reached the one extra which shows that there are additional pages to be had. Stop here...
+ $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->user_name));
+ break;
+ }
+
+ // Record new user's data
+ $lastUser = $row->user_name;
+ $lastUserData = array( 'name' => $lastUser );
+ if ($fld_editcount)
+ $lastUserData['editcount'] = intval($row->user_editcount);
+
+ }
+
+ if ($sqlLimit == $count) {
+ // BUG! database contains group name that User::getAllGroups() does not return
+ // TODO: should handle this more gracefully
+ ApiBase :: dieDebug(__METHOD__,
+ 'MediaWiki configuration error: the database contains more user groups than known to User::getAllGroups() function');
+ }
+
+ // Add user's group info
+ if ($fld_groups && !is_null($row->ug_group2)) {
+ $lastUserData['groups'][] = $row->ug_group2;
+ $result->setIndexedTagName($lastUserData['groups'], 'g');
+ }
+ }
+
+ $db->freeResult($res);
+
+ $result->setIndexedTagName($data, 'u');
+ $result->addValue('query', $this->getModuleName(), $data);
+ }
+
+ protected function getAllowedParams() {
+ return array (
+ 'from' => null,
+ 'prefix' => null,
+ 'group' => array(
+ ApiBase :: PARAM_TYPE => User::getAllGroups()
+ ),
+ 'prop' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array (
+ 'editcount',
+ 'groups',
+ )
+ ),
+ 'limit' => array (
+ ApiBase :: PARAM_DFLT => 10,
+ ApiBase :: PARAM_TYPE => 'limit',
+ ApiBase :: PARAM_MIN => 1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
+ )
+ );
+ }
+
+ protected function getParamDescription() {
+ return array (
+ 'from' => 'The user name to start enumerating from.',
+ 'prefix' => 'Search for all page titles that begin with this value.',
+ 'group' => 'Limit users to a given group name',
+ 'prop' => array(
+ 'What pieces of information to include.',
+ '`groups` property uses more server resources and may return fewer results than the limit.'),
+ 'limit' => 'How many total user names to return.',
+ );
+ }
+
+ protected function getDescription() {
+ return 'Enumerate all registered users';
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=query&list=allusers&aufrom=Y',
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryAllUsers.php 24870 2007-08-17 13:01:35Z robchurch $';
+ }
+}
diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php
index 494f7707..d9715b1a 100644
--- a/includes/api/ApiQueryAllpages.php
+++ b/includes/api/ApiQueryAllpages.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,8 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * Query module to enumerate all available pages.
+ *
* @addtogroup API
*/
class ApiQueryAllpages extends ApiQueryGeneratorBase {
@@ -50,22 +52,51 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
private function run($resultPageSet = null) {
- wfProfileIn($this->getModuleProfileName() . '-getDB');
$db = $this->getDB();
- wfProfileOut($this->getModuleProfileName() . '-getDB');
-
- wfProfileIn($this->getModuleProfileName() . '-parseParams');
- $limit = $from = $namespace = $filterredir = $prefix = null;
- extract($this->extractRequestParams());
+ $params = $this->extractRequestParams();
+
+ // Page filters
+ if (!$this->addWhereIf('page_is_redirect = 1', $params['filterredir'] === 'redirects'))
+ $this->addWhereIf('page_is_redirect = 0', $params['filterredir'] === 'nonredirects');
+ $this->addWhereFld('page_namespace', $params['namespace']);
+ if (!is_null($params['from']))
+ $this->addWhere('page_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from'])));
+ if (isset ($params['prefix']))
+ $this->addWhere("page_title LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'");
+
+ $forceNameTitleIndex = true;
+ if (isset ($params['minsize'])) {
+ $this->addWhere('page_len>=' . intval($params['minsize']));
+ $forceNameTitleIndex = false;
+ }
+
+ if (isset ($params['maxsize'])) {
+ $this->addWhere('page_len<=' . intval($params['maxsize']));
+ $forceNameTitleIndex = false;
+ }
+
+ // Page protection filtering
+ if (isset ($params['prtype'])) {
+ $this->addTables('page_restrictions');
+ $this->addWhere('page_id=pr_page');
+ $this->addWhere('pr_expiry>' . $db->addQuotes($db->timestamp()));
+ $this->addWhereFld('pr_type', $params['prtype']);
+
+ $prlevel = $params['prlevel'];
+ if (!is_null($prlevel) && $prlevel != '' && $prlevel != '*')
+ $this->addWhereFld('pr_level', $prlevel);
+
+ $forceNameTitleIndex = false;
+
+ } else if (isset ($params['prlevel'])) {
+ $this->dieUsage('prlevel may not be used without prtype', 'params');
+ }
+
$this->addTables('page');
- if (!$this->addWhereIf('page_is_redirect = 1', $filterredir === 'redirects'))
- $this->addWhereIf('page_is_redirect = 0', $filterredir === 'nonredirects');
- $this->addWhereFld('page_namespace', $namespace);
- if (isset ($from))
- $this->addWhere('page_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($from)));
- if (isset ($prefix))
- $this->addWhere("page_title LIKE '{$db->strencode(ApiQueryBase :: titleToKey($prefix))}%'");
+ if ($forceNameTitleIndex)
+ $this->addOption('USE INDEX', 'name_title');
+
if (is_null($resultPageSet)) {
$this->addFields(array (
@@ -77,29 +108,28 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
$this->addFields($resultPageSet->getPageTableFields());
}
- $this->addOption('USE INDEX', 'name_title');
- $this->addOption('LIMIT', $limit +1);
+ $limit = $params['limit'];
+ $this->addOption('LIMIT', $limit+1);
$this->addOption('ORDER BY', 'page_namespace, page_title');
- wfProfileOut($this->getModuleProfileName() . '-parseParams');
-
$res = $this->select(__METHOD__);
- wfProfileIn($this->getModuleProfileName() . '-saveResults');
-
$data = array ();
$count = 0;
while ($row = $db->fetchObject($res)) {
if (++ $count > $limit) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
+ // TODO: Security issue - if the user has no right to view next title, it will still be shown
$this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->page_title));
break;
}
if (is_null($resultPageSet)) {
- $vals = $this->addRowInfo('page', $row);
- if ($vals)
- $data[intval($row->page_id)] = $vals;
+ $title = Title :: makeTitle($row->page_namespace, $row->page_title);
+ $data[] = array(
+ 'pageid' => intval($row->page_id),
+ 'ns' => intval($title->getNamespace()),
+ 'title' => $title->getPrefixedText());
} else {
$resultPageSet->processDbRow($row);
}
@@ -111,17 +141,17 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
$result->setIndexedTagName($data, 'p');
$result->addValue('query', $this->getModuleName(), $data);
}
-
- wfProfileOut($this->getModuleProfileName() . '-saveResults');
}
protected function getAllowedParams() {
+ global $wgRestrictionTypes, $wgRestrictionLevels;
+
return array (
'from' => null,
'prefix' => null,
'namespace' => array (
ApiBase :: PARAM_DFLT => 0,
- ApiBase :: PARAM_TYPE => 'namespace'
+ ApiBase :: PARAM_TYPE => 'namespace',
),
'filterredir' => array (
ApiBase :: PARAM_DFLT => 'all',
@@ -131,11 +161,23 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
'nonredirects'
)
),
+ 'minsize' => array (
+ ApiBase :: PARAM_TYPE => 'integer',
+ ),
+ 'maxsize' => array (
+ ApiBase :: PARAM_TYPE => 'integer',
+ ),
+ 'prtype' => array (
+ ApiBase :: PARAM_TYPE => $wgRestrictionTypes,
+ ),
+ 'prlevel' => array (
+ ApiBase :: PARAM_TYPE => $wgRestrictionLevels,
+ ),
'limit' => array (
ApiBase :: PARAM_DFLT => 10,
ApiBase :: PARAM_TYPE => 'limit',
ApiBase :: PARAM_MIN => 1,
- ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
)
);
@@ -147,6 +189,10 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
'prefix' => 'Search for all page titles that begin with this value.',
'namespace' => 'The namespace to enumerate.',
'filterredir' => 'Which pages to list.',
+ 'minsize' => 'Limit to pages with at least this many bytes',
+ 'maxsize' => 'Limit to pages with at most this many bytes',
+ 'prtype' => 'Limit to protected pages only',
+ 'prlevel' => 'The protection level (must be used with apprtype= parameter)',
'limit' => 'How many total pages to return.'
);
}
@@ -169,7 +215,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllpages.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiQueryAllpages.php 24694 2007-08-09 08:41:58Z yurik $';
}
}
-?>
+
diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php
index 1a6783a9..a676b4bf 100644
--- a/includes/api/ApiQueryBacklinks.php
+++ b/includes/api/ApiQueryBacklinks.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,11 +29,16 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * This is three-in-one module to query:
+ * * backlinks - links pointing to the given page,
+ * * embeddedin - what pages transclude the given page within themselves,
+ * * imageusage - what pages use the given image
+ *
* @addtogroup API
*/
class ApiQueryBacklinks extends ApiQueryGeneratorBase {
- private $rootTitle, $contRedirs, $contLevel, $contTitle, $contID;
+ private $params, $rootTitle, $contRedirs, $contLevel, $contTitle, $contID;
// output element name, database column field prefix, database table
private $backlinksSettings = array (
@@ -47,8 +52,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
'prefix' => 'tl',
'linktbl' => 'templatelinks'
),
- 'imagelinks' => array (
- 'code' => 'il',
+ 'imageusage' => array (
+ 'code' => 'iu',
'prefix' => 'il',
'linktbl' => 'imagelinks'
)
@@ -67,7 +72,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
);
$this->bl_code = $code;
- $this->hasNS = $moduleName !== 'imagelinks';
+ $this->hasNS = $moduleName !== 'imageusage';
if ($this->hasNS) {
$this->bl_title = $prefix . '_title';
$this->bl_sort = "{$this->bl_ns}, {$this->bl_title}, {$this->bl_from}";
@@ -93,13 +98,13 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
}
private function run($resultPageSet = null) {
- $continue = $namespace = $redirect = $limit = null;
- extract($this->extractRequestParams());
-
+ $this->params = $this->extractRequestParams();
+
+ $redirect = $this->params['redirect'];
if ($redirect)
- ApiBase :: dieDebug(__METHOD__, 'Redirect is not yet been implemented', 'notimplemented');
+ $this->dieDebug('Redirect has not been implemented', 'notimplemented');
- $this->processContinue($continue, $redirect);
+ $this->processContinue();
$this->addFields($this->bl_fields);
if (is_null($resultPageSet))
@@ -117,15 +122,19 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
if ($this->hasNS)
$this->addWhereFld($this->bl_ns, $this->rootTitle->getNamespace());
$this->addWhereFld($this->bl_title, $this->rootTitle->getDBkey());
- $this->addWhereFld('page_namespace', $namespace);
- $this->addOption('LIMIT', $limit +1);
- $this->addOption('ORDER BY', $this->bl_sort);
+ $this->addWhereFld('page_namespace', $this->params['namespace']);
- if ($redirect)
+ if($this->params['filterredir'] == 'redirects')
+ $this->addWhereFld('page_is_redirect', 1);
+ if($this->params['filterredir'] == 'nonredirects')
$this->addWhereFld('page_is_redirect', 0);
+ $limit = $this->params['limit'];
+ $this->addOption('LIMIT', $limit +1);
+ $this->addOption('ORDER BY', $this->bl_sort);
+
$db = $this->getDB();
- if (!is_null($continue)) {
+ if (!is_null($this->params['continue'])) {
$plfrm = intval($this->contID);
if ($this->contLevel == 0) {
// For the first level, there is only one target title, so no need for complex filtering
@@ -150,48 +159,61 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
if (++ $count > $limit) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
if ($redirect) {
- $ns = $row-> {
- $this->bl_ns };
- $t = $row-> {
- $this->bl_title };
+ $ns = $row-> { $this->bl_ns };
+ $t = $row-> { $this->bl_title };
$continue = $this->getContinueRedirStr(false, 0, $ns, $t, $row->page_id);
} else
$continue = $this->getContinueStr($row->page_id);
+ // TODO: Security issue - if the user has no right to view next title, it will still be shown
$this->setContinueEnumParameter('continue', $continue);
break;
}
if (is_null($resultPageSet)) {
- $vals = $this->addRowInfo('page', $row);
+ $vals = $this->extractRowInfo($row);
if ($vals)
- $data[intval($row->page_id)] = $vals;
+ $data[] = $vals;
} else {
$resultPageSet->processDbRow($row);
}
}
$db->freeResult($res);
- if (is_null($resultPageSet)) {
+ if (is_null($resultPageSet) && !empty($data)) {
$result = $this->getResult();
$result->setIndexedTagName($data, $this->bl_code);
$result->addValue('query', $this->getModuleName(), $data);
}
}
- protected function processContinue($continue, $redirect) {
+ private function extractRowInfo($row) {
+
+ $vals = array();
+ $vals['pageid'] = intval($row->page_id);
+ ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->page_namespace, $row->page_title));
+
+ return $vals;
+ }
+
+ protected function processContinue() {
$pageSet = $this->getPageSet();
$count = $pageSet->getTitleCount();
- if (!is_null($continue)) {
- if ($count !== 0)
- $this->dieUsage("When continuing the {$this->getModuleName()} query, no other titles may be provided", 'titles_on_continue');
- $this->parseContinueParam($continue, $redirect);
+
+ if (!is_null($this->params['continue'])) {
+ $this->parseContinueParam();
// Skip all completed links
} else {
- if ($count !== 1)
- $this->dieUsage("The {$this->getModuleName()} query requires one title to start", 'bad_title_count');
- $this->rootTitle = current($pageSet->getTitles()); // only one title there
+ $title = $this->params['title'];
+ if (!is_null($title)) {
+ $this->rootTitle = Title :: newFromText($title);
+ } else { // This case is obsolete. Will support this for a while
+ if ($count !== 1)
+ $this->dieUsage("The {$this->getModuleName()} query requires one title to start", 'bad_title_count');
+ $this->rootTitle = current($pageSet->getTitles()); // only one title there
+ $this->setWarning('Using titles parameter is obsolete for this list. Use ' . $this->encodeParamName('title') . ' instead.');
+ }
}
// only image titles are allowed for the root
@@ -199,9 +221,9 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$this->dieUsage("The title for {$this->getModuleName()} query must be an image", 'bad_image_title');
}
- protected function parseContinueParam($continue, $redirect) {
- $continueList = explode('|', $continue);
- if ($redirect) {
+ protected function parseContinueParam() {
+ $continueList = explode('|', $this->params['continue']);
+ if ($this->params['redirect']) {
//
// expected redirect-mode parameter:
// ns|db_key|step|level|ns|db_key|id
@@ -215,7 +237,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$rootNs = intval($continueList[0]);
if (($rootNs !== 0 || $continueList[0] === '0') && !empty ($continueList[1])) {
$this->rootTitle = Title :: makeTitleSafe($rootNs, $continueList[1]);
- if ($this->rootTitle && $this->rootTitle->userCanRead()) {
+ if ($this->rootTitle) {
$step = intval($continueList[2]);
if ($step === 1 || $step === 2) {
@@ -263,7 +285,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$rootNs = intval($continueList[0]);
if (($rootNs !== 0 || $continueList[0] === '0') && !empty ($continueList[1])) {
$this->rootTitle = Title :: makeTitleSafe($rootNs, $continueList[1]);
- if ($this->rootTitle && $this->rootTitle->userCanRead()) {
+ if ($this->rootTitle) {
$contID = intval($continueList[2]);
if ($contID !== 0) {
@@ -296,17 +318,26 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
protected function getAllowedParams() {
return array (
+ 'title' => null,
'continue' => null,
'namespace' => array (
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => 'namespace'
),
+ 'filterredir' => array(
+ ApiBase :: PARAM_DFLT => 'all',
+ ApiBase :: PARAM_TYPE => array(
+ 'all',
+ 'redirects',
+ 'nonredirects'
+ )
+ ),
'redirect' => false,
'limit' => array (
ApiBase :: PARAM_DFLT => 10,
ApiBase :: PARAM_TYPE => 'limit',
ApiBase :: PARAM_MIN => 1,
- ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
)
);
@@ -314,8 +345,10 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
protected function getParamDescription() {
return array (
+ 'title' => 'Title to search. If null, titles= parameter will be used instead, but will be obsolete soon.',
'continue' => 'When more results are available, use this to continue.',
'namespace' => 'The namespace to enumerate.',
+ 'filterredir' => 'How to filter for redirects',
'redirect' => 'If linking page is a redirect, find all pages that link to that redirect (not implemented)',
'limit' => 'How many total pages to return.'
);
@@ -327,7 +360,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
return 'Find all pages that link to the given page';
case 'embeddedin' :
return 'Find all pages that embed (transclude) the given title';
- case 'imagelinks' :
+ case 'imageusage' :
return 'Find all pages that use the given image title.';
default :
ApiBase :: dieDebug(__METHOD__, 'Unknown module name');
@@ -337,16 +370,16 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
protected function getExamples() {
static $examples = array (
'backlinks' => array (
- "api.php?action=query&list=backlinks&titles=Main%20Page",
- "api.php?action=query&generator=backlinks&titles=Main%20Page&prop=info"
+ "api.php?action=query&list=backlinks&bltitle=Main%20Page",
+ "api.php?action=query&generator=backlinks&gbltitle=Main%20Page&prop=info"
),
'embeddedin' => array (
- "api.php?action=query&list=embeddedin&titles=Template:Stub",
- "api.php?action=query&generator=embeddedin&titles=Template:Stub&prop=info"
+ "api.php?action=query&list=embeddedin&eititle=Template:Stub",
+ "api.php?action=query&generator=embeddedin&geititle=Template:Stub&prop=info"
),
- 'imagelinks' => array (
- "api.php?action=query&list=imagelinks&titles=Image:Albert%20Einstein%20Head.jpg",
- "api.php?action=query&generator=imagelinks&titles=Image:Albert%20Einstein%20Head.jpg&prop=info"
+ 'imageusage' => array (
+ "api.php?action=query&list=imageusage&iutitle=Image:Albert%20Einstein%20Head.jpg",
+ "api.php?action=query&generator=imageusage&giutitle=Image:Albert%20Einstein%20Head.jpg&prop=info"
)
);
@@ -354,7 +387,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryBacklinks.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiQueryBacklinks.php 25476 2007-09-04 14:44:46Z catrope $';
}
}
-?>
+
diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php
index da07bb6c..28adb415 100644
--- a/includes/api/ApiQueryBase.php
+++ b/includes/api/ApiQueryBase.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,15 +29,19 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * This is a base class for all Query modules.
+ * It provides some common functionality such as constructing various SQL queries.
+ *
* @addtogroup API
*/
abstract class ApiQueryBase extends ApiBase {
- private $mQueryModule, $tables, $where, $fields, $options;
+ private $mQueryModule, $mDb, $tables, $where, $fields, $options;
public function __construct($query, $moduleName, $paramPrefix = '') {
parent :: __construct($query->getMain(), $moduleName, $paramPrefix);
$this->mQueryModule = $query;
+ $this->mDb = null;
$this->resetQueryParams();
}
@@ -48,11 +52,16 @@ abstract class ApiQueryBase extends ApiBase {
$this->options = array ();
}
- protected function addTables($value) {
- if (is_array($value))
- $this->tables = array_merge($this->tables, $value);
- else
- $this->tables[] = $value;
+ protected function addTables($tables, $alias = null) {
+ if (is_array($tables)) {
+ if (!is_null($alias))
+ ApiBase :: dieDebug(__METHOD__, 'Multiple table aliases not supported');
+ $this->tables = array_merge($this->tables, $tables);
+ } else {
+ if (!is_null($alias))
+ $tables = $this->getDB()->tableName($tables) . ' ' . $alias;
+ $this->tables[] = $tables;
+ }
}
protected function addFields($value) {
@@ -124,176 +133,16 @@ abstract class ApiQueryBase extends ApiBase {
return $res;
}
- protected function addRowInfo($prefix, $row) {
-
- $vals = array ();
-
- // ID
- if ( isset( $row-> { $prefix . '_id' } ) )
- $vals[$prefix . 'id'] = intval( $row-> { $prefix . '_id' } );
-
- // Title
- $title = ApiQueryBase :: addRowInfo_title($row, $prefix . '_namespace', $prefix . '_title');
- if ($title) {
- if (!$title->userCanRead())
- return false;
- $vals['ns'] = $title->getNamespace();
- $vals['title'] = $title->getPrefixedText();
- }
-
- switch ($prefix) {
-
- case 'page' :
- // page_is_redirect
- @ $tmp = $row->page_is_redirect;
- if ($tmp)
- $vals['redirect'] = '';
-
- break;
-
- case 'rc' :
- // PageId
- @ $tmp = $row->rc_cur_id;
- if (!is_null($tmp))
- $vals['pageid'] = intval($tmp);
-
- @ $tmp = $row->rc_this_oldid;
- if (!is_null($tmp))
- $vals['revid'] = intval($tmp);
-
- if ( isset( $row->rc_last_oldid ) )
- $vals['old_revid'] = intval( $row->rc_last_oldid );
-
- $title = ApiQueryBase :: addRowInfo_title($row, 'rc_moved_to_ns', 'rc_moved_to_title');
- if ($title) {
- if (!$title->userCanRead())
- return false;
- $vals['new_ns'] = $title->getNamespace();
- $vals['new_title'] = $title->getPrefixedText();
- }
-
- if ( isset( $row->rc_patrolled ) )
- $vals['patrolled'] = '';
-
- break;
-
- case 'log' :
- // PageId
- @ $tmp = $row->page_id;
- if (!is_null($tmp))
- $vals['pageid'] = intval($tmp);
-
- if ($row->log_params !== '') {
- $params = explode("\n", $row->log_params);
- if ($row->log_type == 'move' && isset ($params[0])) {
- $newTitle = Title :: newFromText($params[0]);
- if ($newTitle) {
- $vals['new_ns'] = $newTitle->getNamespace();
- $vals['new_title'] = $newTitle->getPrefixedText();
- $params = null;
- }
- }
-
- if (!empty ($params)) {
- $this->getResult()->setIndexedTagName($params, 'param');
- $vals = array_merge($vals, $params);
- }
- }
-
- break;
-
- case 'rev' :
- // PageID
- @ $tmp = $row->rev_page;
- if (!is_null($tmp))
- $vals['pageid'] = intval($tmp);
- }
-
- // Type
- @ $tmp = $row-> {
- $prefix . '_type' };
- if (!is_null($tmp))
- $vals['type'] = $tmp;
-
- // Action
- @ $tmp = $row-> {
- $prefix . '_action' };
- if (!is_null($tmp))
- $vals['action'] = $tmp;
-
- // Old ID
- @ $tmp = $row-> {
- $prefix . '_text_id' };
- if (!is_null($tmp))
- $vals['oldid'] = intval($tmp);
-
- // User Name / Anon IP
- @ $tmp = $row-> {
- $prefix . '_user_text' };
- if (is_null($tmp))
- @ $tmp = $row->user_name;
- if (!is_null($tmp)) {
- $vals['user'] = $tmp;
- @ $tmp = !$row-> {
- $prefix . '_user' };
- if (!is_null($tmp) && $tmp)
- $vals['anon'] = '';
- }
-
- // Bot Edit
- @ $tmp = $row-> {
- $prefix . '_bot' };
- if (!is_null($tmp) && $tmp)
- $vals['bot'] = '';
-
- // New Edit
- @ $tmp = $row-> {
- $prefix . '_new' };
- if (is_null($tmp))
- @ $tmp = $row-> {
- $prefix . '_is_new' };
- if (!is_null($tmp) && $tmp)
- $vals['new'] = '';
-
- // Minor Edit
- @ $tmp = $row-> {
- $prefix . '_minor_edit' };
- if (is_null($tmp))
- @ $tmp = $row-> {
- $prefix . '_minor' };
- if (!is_null($tmp) && $tmp)
- $vals['minor'] = '';
-
- // Timestamp
- @ $tmp = $row-> {
- $prefix . '_timestamp' };
- if (!is_null($tmp))
- $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $tmp);
-
- // Comment
- @ $tmp = $row-> {
- $prefix . '_comment' };
- if (!empty ($tmp)) // optimize bandwidth
- $vals['comment'] = $tmp;
-
- return $vals;
- }
-
- private static function addRowInfo_title($row, $nsfld, $titlefld) {
- if ( isset( $row-> $nsfld ) ) {
- $ns = $row-> $nsfld;
- @ $title = $row-> $titlefld;
- if (!empty ($title))
- return Title :: makeTitle($ns, $title);
- }
- return false;
+ public static function addTitleInfo(&$arr, $title, $prefix='') {
+ $arr[$prefix . 'ns'] = intval($title->getNamespace());
+ $arr[$prefix . 'title'] = $title->getPrefixedText();
}
-
+
/**
* Override this method to request extra fields from the pageSet
- * using $this->getPageSet()->requestField('fieldName')
+ * using $pageSet->requestField('fieldName')
*/
- public function requestExtraData() {
+ public function requestExtraData($pageSet) {
}
/**
@@ -303,10 +152,25 @@ abstract class ApiQueryBase extends ApiBase {
return $this->mQueryModule;
}
+ /**
+ * Add sub-element under the page element with the given pageId.
+ */
+ protected function addPageSubItems($pageId, $data) {
+ $result = $this->getResult();
+ $result->setIndexedTagName($data, $this->getModulePrefix());
+ $result->addValue(array ('query', 'pages', intval($pageId)),
+ $this->getModuleName(),
+ $data);
+ }
+
protected function setContinueEnumParameter($paramName, $paramValue) {
- $msg = array (
- $this->encodeParamName($paramName
- ) => $paramValue);
+
+ $paramName = $this->encodeParamName($paramName);
+ $msg = array( $paramName => $paramValue );
+
+// This is an alternative continue format as a part of the URL string
+// ApiResult :: setContent($msg, $paramName . '=' . urlencode($paramValue));
+
$this->getResult()->addValue('query-continue', $this->getModuleName(), $msg);
}
@@ -314,7 +178,19 @@ abstract class ApiQueryBase extends ApiBase {
* Get the Query database connection (readonly)
*/
protected function getDB() {
- return $this->getQuery()->getDB();
+ if (is_null($this->mDb))
+ $this->mDb = $this->getQuery()->getDB();
+ return $this->mDb;
+ }
+
+ /**
+ * Selects the query database connection with the given name.
+ * If no such connection has been requested before, it will be created.
+ * Subsequent calls with the same $name will return the same connection
+ * as the first, regardless of $db or $groups new values.
+ */
+ public function selectNamedDB($name, $db, $groups) {
+ $this->mDb = $this->getQuery()->getNamedDB($name, $db, $groups);
}
/**
@@ -322,7 +198,7 @@ abstract class ApiQueryBase extends ApiBase {
* @return ApiPageSet data
*/
protected function getPageSet() {
- return $this->mQueryModule->getPageSet();
+ return $this->getQuery()->getPageSet();
}
/**
@@ -338,8 +214,19 @@ abstract class ApiQueryBase extends ApiBase {
return str_replace('_', ' ', $key);
}
+ public function getTokenFlag($tokenArr, $action) {
+ if (in_array($action, $tokenArr)) {
+ global $wgUser;
+ if ($wgUser->isAllowed($action))
+ return true;
+ else
+ $this->dieUsage("Action '$action' is not allowed for the current user", 'permissiondenied');
+ }
+ return false;
+ }
+
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiQueryBase.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiQueryBase.php 24533 2007-08-01 22:46:22Z yurik $';
}
}
@@ -375,4 +262,4 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase {
*/
public abstract function executeGenerator($resultPageSet);
}
-?>
+
diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php
new file mode 100644
index 00000000..42bc1c38
--- /dev/null
+++ b/includes/api/ApiQueryCategories.php
@@ -0,0 +1,157 @@
+<?php
+
+/*
+ * Created on May 13, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiQueryBase.php");
+}
+
+/**
+ * A query module to enumerate categories the set of pages belong to.
+ *
+ * @addtogroup API
+ */
+class ApiQueryCategories extends ApiQueryGeneratorBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'cl');
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function executeGenerator($resultPageSet) {
+ $this->run($resultPageSet);
+ }
+
+ private function run($resultPageSet = null) {
+
+ if ($this->getPageSet()->getGoodTitleCount() == 0)
+ return; // nothing to do
+
+ $params = $this->extractRequestParams();
+ $prop = $params['prop'];
+
+ $this->addFields(array (
+ 'cl_from',
+ 'cl_to'
+ ));
+
+ $fld_sortkey = false;
+ if (!is_null($prop)) {
+ foreach($prop as $p) {
+ switch ($p) {
+ case 'sortkey':
+ $this->addFields('cl_sortkey');
+ $fld_sortkey = true;
+ break;
+ default :
+ ApiBase :: dieDebug(__METHOD__, "Unknown prop=$p");
+ }
+ }
+ }
+
+ $this->addTables('categorylinks');
+ $this->addWhereFld('cl_from', array_keys($this->getPageSet()->getGoodTitles()));
+ $this->addOption('ORDER BY', "cl_from, cl_to");
+
+ $db = $this->getDB();
+ $res = $this->select(__METHOD__);
+
+ if (is_null($resultPageSet)) {
+
+ $data = array();
+ $lastId = 0; // database has no ID 0
+ while ($row = $db->fetchObject($res)) {
+ if ($lastId != $row->cl_from) {
+ if($lastId != 0) {
+ $this->addPageSubItems($lastId, $data);
+ $data = array();
+ }
+ $lastId = $row->cl_from;
+ }
+
+ $title = Title :: makeTitle(NS_CATEGORY, $row->cl_to);
+
+ $vals = array();
+ ApiQueryBase :: addTitleInfo($vals, $title);
+ if ($fld_sortkey)
+ $vals['sortkey'] = $row->cl_sortkey;
+
+ $data[] = $vals;
+ }
+
+ if($lastId != 0) {
+ $this->addPageSubItems($lastId, $data);
+ }
+
+ } else {
+
+ $titles = array();
+ while ($row = $db->fetchObject($res)) {
+ $titles[] = Title :: makeTitle(NS_CATEGORY, $row->cl_to);
+ }
+ $resultPageSet->populateFromTitles($titles);
+ }
+
+ $db->freeResult($res);
+ }
+
+ protected function getAllowedParams() {
+ return array (
+ 'prop' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array (
+ 'sortkey',
+ )
+ )
+ );
+ }
+
+ protected function getParamDescription() {
+ return array (
+ 'prop' => 'Which additional properties to get for each category.',
+ );
+ }
+
+ protected function getDescription() {
+ return 'List all categories the page(s) belong to';
+ }
+
+ protected function getExamples() {
+ return array (
+ "Get a list of categories [[Albert Einstein]] belongs to:",
+ " api.php?action=query&prop=categories&titles=Albert%20Einstein",
+ "Get information about all categories used in the [[Albert Einstein]]:",
+ " api.php?action=query&generator=categories&titles=Albert%20Einstein&prop=info"
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryCategories.php 24092 2007-07-14 19:04:31Z yurik $';
+ }
+}
+
diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php
new file mode 100644
index 00000000..58a454a5
--- /dev/null
+++ b/includes/api/ApiQueryCategoryMembers.php
@@ -0,0 +1,238 @@
+<?php
+
+/*
+ * Created on June 14, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiQueryBase.php");
+}
+
+/**
+ * A query module to enumerate pages that belong to a category.
+ *
+ * @addtogroup API
+ */
+class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'cm');
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function executeGenerator($resultPageSet) {
+ $this->run($resultPageSet);
+ }
+
+ private function run($resultPageSet = null) {
+
+ $params = $this->extractRequestParams();
+
+ $category = $params['category'];
+ if (is_null($category))
+ $this->dieUsage("Category parameter is required", 'param_category');
+ $categoryTitle = Title::makeTitleSafe( NS_CATEGORY, $category );
+ if ( is_null( $categoryTitle ) )
+ $this->dieUsage("Category name $category is not valid", 'param_category');
+
+ $prop = array_flip($params['prop']);
+ $fld_ids = isset($prop['ids']);
+ $fld_title = isset($prop['title']);
+ $fld_sortkey = isset($prop['sortkey']);
+ $fld_timestamp = isset($prop['timestamp']);
+
+ if (is_null($resultPageSet)) {
+ $this->addFields(array('cl_from', 'cl_sortkey', 'page_namespace', 'page_title'));
+ $this->addFieldsIf('page_id', $fld_ids);
+ } else {
+ $this->addFields($resultPageSet->getPageTableFields()); // will include page_ id, ns, title
+ $this->addFields(array('cl_from', 'cl_sortkey'));
+ }
+
+ $this->addFieldsIf('cl_timestamp', $fld_timestamp);
+ $this->addTables(array('page','categorylinks')); // must be in this order for 'USE INDEX'
+ // Not needed after bug 10280 is applied to servers
+ if($params['sort'] == 'timestamp')
+ {
+ $this->addOption('USE INDEX', 'cl_timestamp');
+ $this->addOption('ORDER BY', 'cl_to, cl_timestamp');
+ }
+ else
+ {
+ $this->addOption('USE INDEX', 'cl_sortkey');
+ $this->addOption('ORDER BY', 'cl_to, cl_sortkey, cl_from');
+ }
+
+ $this->addWhere('cl_from=page_id');
+ $this->setContinuation($params['continue']);
+ $this->addWhereFld('cl_to', $categoryTitle->getDBkey());
+ $this->addWhereFld('page_namespace', $params['namespace']);
+
+ $limit = $params['limit'];
+ $this->addOption('LIMIT', $limit +1);
+
+ $db = $this->getDB();
+
+ $data = array ();
+ $count = 0;
+ $lastSortKey = null;
+ $res = $this->select(__METHOD__);
+ while ($row = $db->fetchObject($res)) {
+ if (++ $count > $limit) {
+ // We've reached the one extra which shows that there are additional pages to be had. Stop here...
+ // TODO: Security issue - if the user has no right to view next title, it will still be shown
+ $this->setContinueEnumParameter('continue', $this->getContinueStr($row, $lastSortKey));
+ break;
+ }
+
+ $lastSortKey = $row->cl_sortkey; // detect duplicate sortkeys
+
+ if (is_null($resultPageSet)) {
+ $vals = array();
+ if ($fld_ids)
+ $vals['pageid'] = intval($row->page_id);
+ if ($fld_title) {
+ $title = Title :: makeTitle($row->page_namespace, $row->page_title);
+ $vals['ns'] = intval($title->getNamespace());
+ $vals['title'] = $title->getPrefixedText();
+ }
+ if ($fld_sortkey)
+ $vals['sortkey'] = $row->cl_sortkey;
+ if ($fld_timestamp)
+ $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->cl_timestamp);
+ $data[] = $vals;
+ } else {
+ $resultPageSet->processDbRow($row);
+ }
+ }
+ $db->freeResult($res);
+
+ if (is_null($resultPageSet)) {
+ $this->getResult()->setIndexedTagName($data, 'cm');
+ $this->getResult()->addValue('query', $this->getModuleName(), $data);
+ }
+ }
+
+ private function getContinueStr($row, $lastSortKey) {
+ $ret = $row->cl_sortkey . '|';
+ if ($row->cl_sortkey == $lastSortKey) // duplicate sort key, add cl_from
+ $ret .= $row->cl_from;
+ return $ret;
+ }
+
+ /**
+ * Add DB WHERE clause to continue previous query based on 'continue' parameter
+ */
+ private function setContinuation($continue) {
+ if (is_null($continue))
+ return; // This is not a continuation request
+
+ $continueList = explode('|', $continue);
+ $hasError = count($continueList) != 2;
+ $from = 0;
+ if (!$hasError && strlen($continueList[1]) > 0) {
+ $from = intval($continueList[1]);
+ $hasError = ($from == 0);
+ }
+
+ if ($hasError)
+ $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "badcontinue");
+
+ $encSortKey = $this->getDB()->addQuotes($continueList[0]);
+ $encFrom = $this->getDB()->addQuotes($from);
+
+ if ($from != 0) {
+ // Duplicate sort key continue
+ $this->addWhere( "cl_sortkey>$encSortKey OR (cl_sortkey=$encSortKey AND cl_from>=$encFrom)" );
+ } else {
+ $this->addWhere( "cl_sortkey>=$encSortKey" );
+ }
+ }
+
+ protected function getAllowedParams() {
+ return array (
+ 'category' => null,
+ 'prop' => array (
+ ApiBase :: PARAM_DFLT => 'ids|title',
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array (
+ 'ids',
+ 'title',
+ 'sortkey',
+ 'timestamp',
+ )
+ ),
+ 'namespace' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => 'namespace',
+ ),
+ 'continue' => null,
+ 'limit' => array (
+ ApiBase :: PARAM_TYPE => 'limit',
+ ApiBase :: PARAM_DFLT => 10,
+ ApiBase :: PARAM_MIN => 1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
+ ),
+ 'sort' => array(
+ ApiBase :: PARAM_DFLT => 'sortkey',
+ ApiBase :: PARAM_TYPE => array(
+ 'sortkey',
+ 'timestamp'
+ )
+ )
+ );
+ }
+
+ protected function getParamDescription() {
+ return array (
+ 'category' => 'Which category to enumerate (required)',
+ 'prop' => 'What pieces of information to include',
+ 'namespace' => 'Only include pages in these namespaces',
+ 'sort' => 'Property to sort by',
+ 'continue' => 'For large categories, give the value retured from previous query',
+ 'limit' => 'The maximum number of pages to return.',
+ );
+ }
+
+ protected function getDescription() {
+ return 'List all pages in a given category';
+ }
+
+ protected function getExamples() {
+ return array (
+ "Get first 10 pages in the categories [[Physics]]:",
+ " api.php?action=query&list=categorymembers&cmcategory=Physics",
+ "Get page info about first 10 pages in the categories [[Physics]]:",
+ " api.php?action=query&generator=categorymembers&gcmcategory=Physics&prop=info",
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 25474 2007-09-04 14:30:31Z catrope $';
+ }
+}
+
diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php
new file mode 100644
index 00000000..385ae65b
--- /dev/null
+++ b/includes/api/ApiQueryExtLinksUsage.php
@@ -0,0 +1,200 @@
+<?php
+
+/*
+ * Created on July 7, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiQueryBase.php');
+}
+
+/**
+ * @addtogroup API
+ */
+class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'eu');
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function executeGenerator($resultPageSet) {
+ $this->run($resultPageSet);
+ }
+
+ private function run($resultPageSet = null) {
+
+ $params = $this->extractRequestParams();
+
+ $protocol = $params['protocol'];
+ $query = $params['query'];
+ if (is_null($query))
+ $this->dieUsage('Missing required query parameter', 'params');
+
+ // Find the right prefix
+ global $wgUrlProtocols;
+ foreach ($wgUrlProtocols as $p) {
+ if( substr( $p, 0, strlen( $protocol ) ) === $protocol ) {
+ $protocol = $p;
+ break;
+ }
+ }
+
+ $likeQuery = LinkFilter::makeLike($query , $protocol);
+ if (!$likeQuery)
+ $this->dieUsage('Invalid query', 'bad_query');
+ $likeQuery = substr($likeQuery, 0, strpos($likeQuery,'%')+1);
+
+ $this->addTables(array('page','externallinks')); // must be in this order for 'USE INDEX'
+ $this->addOption('USE INDEX', 'el_index');
+
+ $db = $this->getDB();
+ $this->addWhere('page_id=el_from');
+ $this->addWhere('el_index LIKE ' . $db->addQuotes( $likeQuery ));
+ $this->addWhereFld('page_namespace', $params['namespace']);
+
+ $prop = array_flip($params['prop']);
+ $fld_ids = isset($prop['ids']);
+ $fld_title = isset($prop['title']);
+ $fld_url = isset($prop['url']);
+
+ if (is_null($resultPageSet)) {
+ $this->addFields(array (
+ 'page_id',
+ 'page_namespace',
+ 'page_title'
+ ));
+ $this->addFieldsIf('el_to', $fld_url);
+ } else {
+ $this->addFields($resultPageSet->getPageTableFields());
+ }
+
+ $limit = $params['limit'];
+ $offset = $params['offset'];
+ $this->addOption('LIMIT', $limit +1);
+ if (isset ($offset))
+ $this->addOption('OFFSET', $offset);
+
+ $res = $this->select(__METHOD__);
+
+ $data = array ();
+ $count = 0;
+ while ($row = $db->fetchObject($res)) {
+ if (++ $count > $limit) {
+ // We've reached the one extra which shows that there are additional pages to be had. Stop here...
+ $this->setContinueEnumParameter('offset', $offset+$limit+1);
+ break;
+ }
+
+ if (is_null($resultPageSet)) {
+ $vals = array();
+ if ($fld_ids)
+ $vals['pageid'] = intval($row->page_id);
+ if ($fld_title) {
+ $title = Title :: makeTitle($row->page_namespace, $row->page_title);
+ $vals['ns'] = intval($title->getNamespace());
+ $vals['title'] = $title->getPrefixedText();
+ }
+ if ($fld_url)
+ $vals['url'] = $row->el_to;
+ $data[] = $vals;
+ } else {
+ $resultPageSet->processDbRow($row);
+ }
+ }
+ $db->freeResult($res);
+
+ if (is_null($resultPageSet)) {
+ $result = $this->getResult();
+ $result->setIndexedTagName($data, $this->getModulePrefix());
+ $result->addValue('query', $this->getModuleName(), $data);
+ }
+ }
+
+ protected function getAllowedParams() {
+ global $wgUrlProtocols;
+ $protocols = array();
+ foreach ($wgUrlProtocols as $p) {
+ $protocols[] = substr($p, 0, strpos($p,':'));
+ }
+
+ return array (
+ 'prop' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_DFLT => 'ids|title|url',
+ ApiBase :: PARAM_TYPE => array (
+ 'ids',
+ 'title',
+ 'url'
+ )
+ ),
+ 'offset' => array (
+ ApiBase :: PARAM_TYPE => 'integer'
+ ),
+ 'protocol' => array (
+ ApiBase :: PARAM_TYPE => $protocols,
+ ApiBase :: PARAM_DFLT => 'http',
+ ),
+ 'query' => null,
+ 'namespace' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => 'namespace'
+ ),
+ 'limit' => array (
+ ApiBase :: PARAM_DFLT => 10,
+ ApiBase :: PARAM_TYPE => 'limit',
+ ApiBase :: PARAM_MIN => 1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
+ )
+ );
+ }
+
+ protected function getParamDescription() {
+ return array (
+ 'prop' => 'What pieces of information to include',
+ 'offset' => 'Used for paging. Use the value returned for "continue"',
+ 'protocol' => 'Protocol of the url',
+ 'query' => 'Search string without protocol. See [[Special:LinkSearch]]',
+ 'namespace' => 'The page namespace(s) to enumerate.',
+ 'limit' => 'How many entries to return.'
+ );
+ }
+
+ protected function getDescription() {
+ return 'Enumerate pages that contain a given URL';
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=query&list=exturlusage&euquery=www.mediawiki.org'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 24694 2007-08-09 08:41:58Z yurik $';
+ }
+}
diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php
new file mode 100644
index 00000000..440b31d6
--- /dev/null
+++ b/includes/api/ApiQueryExternalLinks.php
@@ -0,0 +1,93 @@
+<?php
+
+/*
+ * Created on May 13, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiQueryBase.php");
+}
+
+/**
+ * A query module to list all external URLs found on a given set of pages.
+ *
+ * @addtogroup API
+ */
+class ApiQueryExternalLinks extends ApiQueryBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'el');
+ }
+
+ public function execute() {
+
+ $this->addFields(array (
+ 'el_from',
+ 'el_to'
+ ));
+
+ $this->addTables('externallinks');
+ $this->addWhereFld('el_from', array_keys($this->getPageSet()->getGoodTitles()));
+
+ $db = $this->getDB();
+ $res = $this->select(__METHOD__);
+
+ $data = array();
+ $lastId = 0; // database has no ID 0
+ while ($row = $db->fetchObject($res)) {
+ if ($lastId != $row->el_from) {
+ if($lastId != 0) {
+ $this->addPageSubItems($lastId, $data);
+ $data = array();
+ }
+ $lastId = $row->el_from;
+ }
+
+ $entry = array();
+ ApiResult :: setContent($entry, $row->el_to);
+ $data[] = $entry;
+ }
+
+ if($lastId != 0) {
+ $this->addPageSubItems($lastId, $data);
+ }
+
+ $db->freeResult($res);
+ }
+
+ protected function getDescription() {
+ return 'Returns all external urls (not interwikies) from the given page(s)';
+ }
+
+ protected function getExamples() {
+ return array (
+ "Get a list of external links on the [[Main Page]]:",
+ " api.php?action=query&prop=extlinks&titles=Main%20Page",
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 23819 2007-07-07 03:05:09Z yurik $';
+ }
+}
+
diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php
new file mode 100644
index 00000000..3d568ba1
--- /dev/null
+++ b/includes/api/ApiQueryImageInfo.php
@@ -0,0 +1,156 @@
+<?php
+
+/*
+ * Created on July 6, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiQueryBase.php');
+}
+
+/**
+ * A query action to get image information and upload history.
+ *
+ * @addtogroup API
+ */
+class ApiQueryImageInfo extends ApiQueryBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'ii');
+ }
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ $history = $params['history'];
+
+ $prop = array_flip($params['prop']);
+ $fld_timestamp = isset($prop['timestamp']);
+ $fld_user = isset($prop['user']);
+ $fld_comment = isset($prop['comment']);
+ $fld_url = isset($prop['url']);
+ $fld_size = isset($prop['size']);
+ $fld_sha1 = isset($prop['sha1']);
+
+ $pageIds = $this->getPageSet()->getAllTitlesByNamespace();
+ if (!empty($pageIds[NS_IMAGE])) {
+ foreach ($pageIds[NS_IMAGE] as $dbKey => $pageId) {
+
+ $title = Title :: makeTitle(NS_IMAGE, $dbKey);
+ $img = wfFindFile($title);
+
+ $data = array();
+ if ( !$img ) {
+ $repository = '';
+ } else {
+
+ $repository = $img->getRepoName();
+
+ $isCur = true;
+ while($line = $img->nextHistoryLine()) { // assignment
+ $row = get_object_vars( $line );
+ $vals = array();
+ $prefix = $isCur ? 'img' : 'oi';
+
+ if ($fld_timestamp)
+ $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row["${prefix}_timestamp"]);
+ if ($fld_user) {
+ $vals['user'] = $row["${prefix}_user_text"];
+ if(!$row["${prefix}_user"])
+ $vals['anon'] = '';
+ }
+ if ($fld_size) {
+ $vals['size'] = intval($row["{$prefix}_size"]);
+ $vals['width'] = intval($row["{$prefix}_width"]);
+ $vals['height'] = intval($row["{$prefix}_height"]);
+ }
+ if ($fld_url)
+ $vals['url'] = $isCur ? $img->getURL() : $img->getArchiveUrl($row["oi_archive_name"]);
+ if ($fld_comment)
+ $vals['comment'] = $row["{$prefix}_description"];
+
+ if ($fld_sha1)
+ $vals['sha1'] = wfBaseConvert($row["{$prefix}_sha1"], 36, 16, 40);
+
+ $data[] = $vals;
+
+ if (!$history) // Stop after the first line.
+ break;
+
+ $isCur = false;
+ }
+
+ $img->resetHistory();
+ }
+
+ $this->getResult()->addValue(array ('query', 'pages', intval($pageId)),
+ 'imagerepository',
+ $repository);
+ if (!empty($data))
+ $this->addPageSubItems($pageId, $data);
+ }
+ }
+ }
+
+ protected function getAllowedParams() {
+ return array (
+ 'prop' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_DFLT => 'timestamp|user',
+ ApiBase :: PARAM_TYPE => array (
+ 'timestamp',
+ 'user',
+ 'comment',
+ 'url',
+ 'size',
+ 'sha1'
+ )
+ ),
+ 'history' => false,
+ );
+ }
+
+ protected function getParamDescription() {
+ return array (
+ 'prop' => 'What image information to get.',
+ 'history' => 'Include upload history',
+ );
+ }
+
+ protected function getDescription() {
+ return array (
+ 'Returns image information and upload history'
+ );
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=query&titles=Image:Albert%20Einstein%20Head.jpg&prop=imageinfo',
+ 'api.php?action=query&titles=Image:Test.jpg&prop=imageinfo&iihistory&iiprop=timestamp|user|url',
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryImageInfo.php 25456 2007-09-03 19:58:05Z catrope $';
+ }
+}
diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php
new file mode 100644
index 00000000..d64a653b
--- /dev/null
+++ b/includes/api/ApiQueryImages.php
@@ -0,0 +1,118 @@
+<?php
+
+/*
+ * Created on May 13, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiQueryBase.php");
+}
+
+/**
+ * This query adds <images> subelement to all pages with the list of images embedded into those pages.
+ *
+ * @addtogroup API
+ */
+class ApiQueryImages extends ApiQueryGeneratorBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'im');
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function executeGenerator($resultPageSet) {
+ $this->run($resultPageSet);
+ }
+
+ private function run($resultPageSet = null) {
+
+ if ($this->getPageSet()->getGoodTitleCount() == 0)
+ return; // nothing to do
+
+ $this->addFields(array (
+ 'il_from',
+ 'il_to'
+ ));
+
+ $this->addTables('imagelinks');
+ $this->addWhereFld('il_from', array_keys($this->getPageSet()->getGoodTitles()));
+ $this->addOption('ORDER BY', "il_from, il_to");
+
+ $db = $this->getDB();
+ $res = $this->select(__METHOD__);
+
+ if (is_null($resultPageSet)) {
+
+ $data = array();
+ $lastId = 0; // database has no ID 0
+ while ($row = $db->fetchObject($res)) {
+ if ($lastId != $row->il_from) {
+ if($lastId != 0) {
+ $this->addPageSubItems($lastId, $data);
+ $data = array();
+ }
+ $lastId = $row->il_from;
+ }
+
+ $vals = array();
+ ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle(NS_IMAGE, $row->il_to));
+ $data[] = $vals;
+ }
+
+ if($lastId != 0) {
+ $this->addPageSubItems($lastId, $data);
+ }
+
+ } else {
+
+ $titles = array();
+ while ($row = $db->fetchObject($res)) {
+ $titles[] = Title :: makeTitle(NS_IMAGE, $row->il_to);
+ }
+ $resultPageSet->populateFromTitles($titles);
+ }
+
+ $db->freeResult($res);
+ }
+
+ protected function getDescription() {
+ return 'Returns all images contained on the given page(s)';
+ }
+
+ protected function getExamples() {
+ return array (
+ "Get a list of images used in the [[Main Page]]:",
+ " api.php?action=query&prop=images&titles=Main%20Page",
+ "Get information about all images used in the [[Main Page]]:",
+ " api.php?action=query&generator=images&titles=Main%20Page&prop=info"
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryImages.php 24092 2007-07-14 19:04:31Z yurik $';
+ }
+}
+
diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php
index 77489a5f..bebf4006 100644
--- a/includes/api/ApiQueryInfo.php
+++ b/includes/api/ApiQueryInfo.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,59 +29,167 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * A query module to show basic page information.
+ *
* @addtogroup API
*/
class ApiQueryInfo extends ApiQueryBase {
public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName);
+ parent :: __construct($query, $moduleName, 'in');
}
- public function requestExtraData() {
- $pageSet = $this->getPageSet();
+ public function requestExtraData($pageSet) {
$pageSet->requestField('page_is_redirect');
+ $pageSet->requestField('page_is_new');
+ $pageSet->requestField('page_counter');
$pageSet->requestField('page_touched');
$pageSet->requestField('page_latest');
+ $pageSet->requestField('page_len');
}
public function execute() {
+ global $wgUser;
+
+ $params = $this->extractRequestParams();
+ $fld_protection = false;
+ if(!is_null($params['prop'])) {
+ $prop = array_flip($params['prop']);
+ $fld_protection = isset($prop['protection']);
+ }
+ if(!is_null($params['token'])) {
+ $token = $params['token'];
+ $tok_edit = $this->getTokenFlag($token, 'edit');
+ $tok_delete = $this->getTokenFlag($token, 'delete');
+ $tok_protect = $this->getTokenFlag($token, 'protect');
+ $tok_move = $this->getTokenFlag($token, 'move');
+ }
+
$pageSet = $this->getPageSet();
$titles = $pageSet->getGoodTitles();
$result = $this->getResult();
$pageIsRedir = $pageSet->getCustomField('page_is_redirect');
+ $pageIsNew = $pageSet->getCustomField('page_is_new');
+ $pageCounter = $pageSet->getCustomField('page_counter');
$pageTouched = $pageSet->getCustomField('page_touched');
$pageLatest = $pageSet->getCustomField('page_latest');
+ $pageLength = $pageSet->getCustomField('page_len');
+
+ if ($fld_protection && count($titles) > 0) {
+ $this->addTables('page_restrictions');
+ $this->addFields(array('pr_page', 'pr_type', 'pr_level', 'pr_expiry'));
+ $this->addWhereFld('pr_page', array_keys($titles));
- foreach ( $titles as $pageid => $unused ) {
+ $db = $this->getDB();
+ $res = $this->select(__METHOD__);
+ while($row = $db->fetchObject($res)) {
+ $protections[$row->pr_page][] = array(
+ 'type' => $row->pr_type,
+ 'level' => $row->pr_level,
+ 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 )
+ );
+ }
+ $db->freeResult($res);
+ }
+
+ foreach ( $titles as $pageid => $title ) {
$pageInfo = array (
'touched' => wfTimestamp(TS_ISO_8601, $pageTouched[$pageid]),
- 'lastrevid' => intval($pageLatest[$pageid])
+ 'lastrevid' => intval($pageLatest[$pageid]),
+ 'counter' => intval($pageCounter[$pageid]),
+ 'length' => intval($pageLength[$pageid]),
);
if ($pageIsRedir[$pageid])
$pageInfo['redirect'] = '';
+ if ($pageIsNew[$pageid])
+ $pageInfo['new'] = '';
+
+ if (!is_null($token)) {
+ // Currently all tokens are generated the same way, but it might change
+ if ($tok_edit)
+ $pageInfo['edittoken'] = $wgUser->editToken();
+ if ($tok_delete)
+ $pageInfo['deletetoken'] = $wgUser->editToken();
+ if ($tok_protect)
+ $pageInfo['protecttoken'] = $wgUser->editToken();
+ if ($tok_move)
+ $pageInfo['movetoken'] = $wgUser->editToken();
+ }
+
+ if($fld_protection) {
+ if (isset($protections[$pageid])) {
+ $pageInfo['protection'] = $protections[$pageid];
+ $result->setIndexedTagName($pageInfo['protection'], 'pr');
+ } else {
+ $pageInfo['protection'] = array();
+ }
+ }
+
$result->addValue(array (
'query',
'pages'
), $pageid, $pageInfo);
}
+
+ // Get edit tokens for missing titles if requested
+ // Delete, protect and move tokens are N/A for missing titles anyway
+ if($tok_edit)
+ {
+ $missing = $pageSet->getMissingTitles();
+ $res = $result->getData();
+ foreach($missing as $pageid => $title)
+ $res['query']['pages'][$pageid]['edittoken'] = $wgUser->editToken();
+ }
}
+ protected function getAllowedParams() {
+ return array (
+ 'prop' => array (
+ ApiBase :: PARAM_DFLT => NULL,
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array (
+ 'protection'
+ )),
+ 'token' => array (
+ ApiBase :: PARAM_DFLT => NULL,
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array (
+ 'edit',
+ 'delete',
+ 'protect',
+ 'move',
+ )),
+ );
+ }
+
+ protected function getParamDescription() {
+ return array (
+ 'prop' => array (
+ 'Which additional properties to get:',
+ ' "protection" - List the protection level of each page'
+ ),
+ 'token' => 'Request a token to perform a data-modifying action on a page',
+ );
+ }
+
+
protected function getDescription() {
return 'Get basic page information such as namespace, title, last touched date, ...';
}
protected function getExamples() {
return array (
- 'api.php?action=query&prop=info&titles=Main%20Page'
+ 'api.php?action=query&prop=info&titles=Main%20Page',
+ 'api.php?action=query&prop=info&inprop=protection&titles=Main%20Page'
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryInfo.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiQueryInfo.php 25457 2007-09-03 20:17:53Z catrope $';
}
}
-?>
+
diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php
new file mode 100644
index 00000000..ae5ff790
--- /dev/null
+++ b/includes/api/ApiQueryLangLinks.php
@@ -0,0 +1,94 @@
+<?php
+
+/*
+ * Created on May 13, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiQueryBase.php");
+}
+
+/**
+ * A query module to list all langlinks (links to correspanding foreign language pages).
+ *
+ * @addtogroup API
+ */
+class ApiQueryLangLinks extends ApiQueryBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'll');
+ }
+
+ public function execute() {
+ $this->addFields(array (
+ 'll_from',
+ 'll_lang',
+ 'll_title'
+ ));
+
+ $this->addTables('langlinks');
+ $this->addWhereFld('ll_from', array_keys($this->getPageSet()->getGoodTitles()));
+ $this->addOption('ORDER BY', "ll_from, ll_lang");
+ $res = $this->select(__METHOD__);
+
+ $data = array();
+ $lastId = 0; // database has no ID 0
+ $db = $this->getDB();
+ while ($row = $db->fetchObject($res)) {
+
+ if ($lastId != $row->ll_from) {
+ if($lastId != 0) {
+ $this->addPageSubItems($lastId, $data);
+ $data = array();
+ }
+ $lastId = $row->ll_from;
+ }
+
+ $entry = array('lang'=>$row->ll_lang);
+ ApiResult :: setContent($entry, $row->ll_title);
+ $data[] = $entry;
+ }
+
+ if($lastId != 0) {
+ $this->addPageSubItems($lastId, $data);
+ }
+
+ $db->freeResult($res);
+ }
+
+ protected function getDescription() {
+ return 'Returns all interlanguage links from the given page(s)';
+ }
+
+ protected function getExamples() {
+ return array (
+ "Get interlanguage links from the [[Main Page]]:",
+ " api.php?action=query&prop=langlinks&titles=Main%20Page&redirects",
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryLangLinks.php 23819 2007-07-07 03:05:09Z yurik $';
+ }
+}
+
diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php
new file mode 100644
index 00000000..7ec20f44
--- /dev/null
+++ b/includes/api/ApiQueryLinks.php
@@ -0,0 +1,162 @@
+<?php
+
+/*
+ * Created on May 12, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiQueryBase.php");
+}
+
+/**
+ * A query module to list all wiki links on a given set of pages.
+ *
+ * @addtogroup API
+ */
+class ApiQueryLinks extends ApiQueryGeneratorBase {
+
+ const LINKS = 'links';
+ const TEMPLATES = 'templates';
+
+ private $table, $prefix, $description;
+
+ public function __construct($query, $moduleName) {
+
+ switch ($moduleName) {
+ case self::LINKS :
+ $this->table = 'pagelinks';
+ $this->prefix = 'pl';
+ $this->description = 'link';
+ break;
+ case self::TEMPLATES :
+ $this->table = 'templatelinks';
+ $this->prefix = 'tl';
+ $this->description = 'template';
+ break;
+ default :
+ ApiBase :: dieDebug(__METHOD__, 'Unknown module name');
+ }
+
+ parent :: __construct($query, $moduleName, $this->prefix);
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function executeGenerator($resultPageSet) {
+ $this->run($resultPageSet);
+ }
+
+ private function run($resultPageSet = null) {
+
+ if ($this->getPageSet()->getGoodTitleCount() == 0)
+ return; // nothing to do
+
+ $params = $this->extractRequestParams();
+
+ $this->addFields(array (
+ $this->prefix . '_from pl_from',
+ $this->prefix . '_namespace pl_namespace',
+ $this->prefix . '_title pl_title'
+ ));
+
+ $this->addTables($this->table);
+ $this->addWhereFld($this->prefix . '_from', array_keys($this->getPageSet()->getGoodTitles()));
+ $this->addWhereFld($this->prefix . '_namespace', $params['namespace']);
+ $this->addOption('ORDER BY', str_replace('pl_', $this->prefix . '_', 'pl_from, pl_namespace, pl_title'));
+
+ $db = $this->getDB();
+ $res = $this->select(__METHOD__);
+
+ if (is_null($resultPageSet)) {
+
+ $data = array();
+ $lastId = 0; // database has no ID 0
+ while ($row = $db->fetchObject($res)) {
+ if ($lastId != $row->pl_from) {
+ if($lastId != 0) {
+ $this->addPageSubItems($lastId, $data);
+ $data = array();
+ }
+ $lastId = $row->pl_from;
+ }
+
+ $vals = array();
+ ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->pl_namespace, $row->pl_title));
+ $data[] = $vals;
+ }
+
+ if($lastId != 0) {
+ $this->addPageSubItems($lastId, $data);
+ }
+
+ } else {
+
+ $titles = array();
+ while ($row = $db->fetchObject($res)) {
+ $titles[] = Title :: makeTitle($row->pl_namespace, $row->pl_title);
+ }
+ $resultPageSet->populateFromTitles($titles);
+ }
+
+ $db->freeResult($res);
+ }
+
+ protected function getAllowedParams()
+ {
+ return array(
+ 'namespace' => array(
+ ApiBase :: PARAM_TYPE => 'namespace',
+ ApiBase :: PARAM_ISMULTI => true
+ )
+ );
+ }
+
+ protected function getParamDescription()
+ {
+ return array(
+ 'namespace' => "Show {$this->description}s in this namespace(s) only"
+ );
+ }
+
+ protected function getDescription() {
+ return "Returns all {$this->description}s from the given page(s)";
+ }
+
+ protected function getExamples() {
+ return array (
+ "Get {$this->description}s from the [[Main Page]]:",
+ " api.php?action=query&prop={$this->getModuleName()}&titles=Main%20Page",
+ "Get information about the {$this->description} pages in the [[Main Page]]:",
+ " api.php?action=query&generator={$this->getModuleName()}&titles=Main%20Page&prop=info",
+ "Get {$this->description}s from the Main Page in the User and Template namespaces:",
+ " api.php?action=query&prop={$this->getModuleName()}&titles=Main%20Page&{$this->prefix}namespace=2|10"
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryLinks.php 24092 2007-07-14 19:04:31Z yurik $';
+ }
+}
+
diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php
index d9f23758..0f143658 100644
--- a/includes/api/ApiQueryLogEvents.php
+++ b/includes/api/ApiQueryLogEvents.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,8 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * Query action to List the log events, with optional filtering by various parameters.
+ *
* @addtogroup API
*/
class ApiQueryLogEvents extends ApiQueryBase {
@@ -38,11 +40,18 @@ class ApiQueryLogEvents extends ApiQueryBase {
}
public function execute() {
- $limit = $type = $start = $end = $dir = $user = $title = null;
- extract($this->extractRequestParams());
-
+ $params = $this->extractRequestParams();
$db = $this->getDB();
+ $prop = $params['prop'];
+ $this->fld_ids = in_array('ids', $prop);
+ $this->fld_title = in_array('title', $prop);
+ $this->fld_type = in_array('type', $prop);
+ $this->fld_user = in_array('user', $prop);
+ $this->fld_timestamp = in_array('timestamp', $prop);
+ $this->fld_comment = in_array('comment', $prop);
+ $this->fld_details = in_array('details', $prop);
+
list($tbl_logging, $tbl_page, $tbl_user) = $db->tableNamesN('logging', 'page', 'user');
$this->addOption('STRAIGHT_JOIN');
@@ -54,19 +63,27 @@ class ApiQueryLogEvents extends ApiQueryBase {
'log_type',
'log_action',
'log_timestamp',
- 'log_user',
- 'user_name',
- 'log_namespace',
- 'log_title',
- 'page_id',
- 'log_comment',
- 'log_params'
));
+
+ // FIXME: Fake out log_id for now until the column is live on Wikimedia
+ // $this->addFieldsIf('log_id', $this->fld_ids);
+ $this->addFieldsIf('page_id', $this->fld_ids);
+ $this->addFieldsIf('log_user', $this->fld_user);
+ $this->addFieldsIf('user_name', $this->fld_user);
+ $this->addFieldsIf('log_namespace', $this->fld_title);
+ $this->addFieldsIf('log_title', $this->fld_title);
+ $this->addFieldsIf('log_comment', $this->fld_comment);
+ $this->addFieldsIf('log_params', $this->fld_details);
+
+
+ $this->addWhereFld('log_deleted', 0);
+ $this->addWhereFld('log_type', $params['type']);
+ $this->addWhereRange('log_timestamp', $params['dir'], $params['start'], $params['end']);
- $this->addWhereFld('log_type', $type);
- $this->addWhereRange('log_timestamp', $dir, $start, $end);
+ $limit = $params['limit'];
$this->addOption('LIMIT', $limit +1);
+ $user = $params['user'];
if (!is_null($user)) {
$userid = $db->selectField('user', 'user_id', array (
'user_name' => $user
@@ -76,6 +93,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
$this->addWhereFld('log_user', $userid);
}
+ $title = $params['title'];
if (!is_null($title)) {
$titleObj = Title :: newFromText($title);
if (is_null($titleObj))
@@ -90,11 +108,11 @@ class ApiQueryLogEvents extends ApiQueryBase {
while ($row = $db->fetchObject($res)) {
if (++ $count > $limit) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter('start', $row->log_timestamp);
+ $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->log_timestamp));
break;
}
- $vals = $this->addRowInfo('log', $row);
+ $vals = $this->extractRowInfo($row);
if($vals)
$data[] = $vals;
}
@@ -104,23 +122,102 @@ class ApiQueryLogEvents extends ApiQueryBase {
$this->getResult()->addValue('query', $this->getModuleName(), $data);
}
+ private function extractRowInfo($row) {
+ $vals = array();
+
+ if ($this->fld_ids) {
+ // FIXME: Fake out log_id for now until the column is live on Wikimedia
+ // $vals['logid'] = intval($row->log_id);
+ $vals['logid'] = 0;
+ $vals['pageid'] = intval($row->page_id);
+ }
+
+ if ($this->fld_title) {
+ $title = Title :: makeTitle($row->log_namespace, $row->log_title);
+ ApiQueryBase :: addTitleInfo($vals, $title);
+ }
+
+ if ($this->fld_type) {
+ $vals['type'] = $row->log_type;
+ $vals['action'] = $row->log_action;
+ }
+
+ if ($this->fld_details && $row->log_params !== '') {
+ $params = explode("\n", $row->log_params);
+ switch ($row->log_type) {
+ case 'move':
+ if (isset ($params[0])) {
+ $title = Title :: newFromText($params[0]);
+ if ($title) {
+ $vals2 = array();
+ ApiQueryBase :: addTitleInfo($vals2, $title, "new_");
+ $vals[$row->log_type] = $vals2;
+ $params = null;
+ }
+ }
+ break;
+ case 'patrol':
+ $vals2 = array();
+ list( $vals2['cur'], $vals2['prev'], $vals2['auto'] ) = $params;
+ $vals[$row->log_type] = $vals2;
+ $params = null;
+ break;
+ case 'rights':
+ $vals2 = array();
+ list( $vals2['old'], $vals2['new'] ) = $params;
+ $vals[$row->log_type] = $vals2;
+ $params = null;
+ break;
+ case 'block':
+ $vals2 = array();
+ list( $vals2['duration'], $vals2['flags'] ) = $params;
+ $vals[$row->log_type] = $vals2;
+ $params = null;
+ break;
+ }
+
+ if (isset($params)) {
+ $this->getResult()->setIndexedTagName($params, 'param');
+ $vals = array_merge($vals, $params);
+ }
+ }
+
+ if ($this->fld_user) {
+ $vals['user'] = $row->user_name;
+ if(!$row->log_user)
+ $vals['anon'] = '';
+ }
+ if ($this->fld_timestamp) {
+ $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->log_timestamp);
+ }
+ if ($this->fld_comment && !empty ($row->log_comment)) {
+ $vals['comment'] = $row->log_comment;
+ }
+
+ return $vals;
+ }
+
+
protected function getAllowedParams() {
+ global $wgLogTypes;
return array (
- 'type' => array (
+ 'prop' => array (
ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_DFLT => 'ids|title|type|user|timestamp|comment|details',
ApiBase :: PARAM_TYPE => array (
- 'block',
- 'protect',
- 'rights',
- 'delete',
- 'upload',
- 'move',
- 'import',
- 'renameuser',
- 'newusers',
- 'makebot'
+ 'ids',
+ 'title',
+ 'type',
+ 'user',
+ 'timestamp',
+ 'comment',
+ 'details',
)
),
+ 'type' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => $wgLogTypes
+ ),
'start' => array (
ApiBase :: PARAM_TYPE => 'timestamp'
),
@@ -140,7 +237,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
ApiBase :: PARAM_DFLT => 10,
ApiBase :: PARAM_TYPE => 'limit',
ApiBase :: PARAM_MIN => 1,
- ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
)
);
@@ -169,7 +266,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryLogEvents.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiQueryLogEvents.php 24256 2007-07-18 21:47:09Z robchurch $';
}
}
-?>
+
diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php
index 25f7ff3e..309beaf9 100644
--- a/includes/api/ApiQueryRecentChanges.php
+++ b/includes/api/ApiQueryRecentChanges.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,9 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * A query action to enumerate the recent changes that were done to the wiki.
+ * Various filters are supported.
+ *
* @addtogroup API
*/
class ApiQueryRecentChanges extends ApiQueryBase {
@@ -37,6 +40,10 @@ class ApiQueryRecentChanges extends ApiQueryBase {
parent :: __construct($query, $moduleName, 'rc');
}
+ private $fld_comment = false, $fld_user = false, $fld_flags = false,
+ $fld_timestamp = false, $fld_title = false, $fld_ids = false,
+ $fld_sizes = false;
+
public function execute() {
$limit = $prop = $namespace = $show = $dir = $start = $end = null;
extract($this->extractRequestParams());
@@ -44,6 +51,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$this->addTables('recentchanges');
$this->addWhereRange('rc_timestamp', $dir, $start, $end);
$this->addWhereFld('rc_namespace', $namespace);
+ $this->addWhereFld('rc_deleted', 0);
if (!is_null($show)) {
$show = array_flip($show);
@@ -62,9 +70,6 @@ class ApiQueryRecentChanges extends ApiQueryBase {
'rc_timestamp',
'rc_namespace',
'rc_title',
- 'rc_cur_id',
- 'rc_this_oldid',
- 'rc_last_oldid',
'rc_type',
'rc_moved_to_ns',
'rc_moved_to_title'
@@ -72,16 +77,27 @@ class ApiQueryRecentChanges extends ApiQueryBase {
if (!is_null($prop)) {
$prop = array_flip($prop);
- $this->addFieldsIf('rc_comment', isset ($prop['comment']));
- if (isset ($prop['user'])) {
- $this->addFields('rc_user');
- $this->addFields('rc_user_text');
- }
- if (isset ($prop['flags'])) {
- $this->addFields('rc_minor');
- $this->addFields('rc_bot');
- $this->addFields('rc_new');
- }
+
+ $this->fld_comment = isset ($prop['comment']);
+ $this->fld_user = isset ($prop['user']);
+ $this->fld_flags = isset ($prop['flags']);
+ $this->fld_timestamp = isset ($prop['timestamp']);
+ $this->fld_title = isset ($prop['title']);
+ $this->fld_ids = isset ($prop['ids']);
+ $this->fld_sizes = isset ($prop['sizes']);
+
+ $this->addFieldsIf('rc_id', $this->fld_ids);
+ $this->addFieldsIf('rc_cur_id', $this->fld_ids);
+ $this->addFieldsIf('rc_this_oldid', $this->fld_ids);
+ $this->addFieldsIf('rc_last_oldid', $this->fld_ids);
+ $this->addFieldsIf('rc_comment', $this->fld_comment);
+ $this->addFieldsIf('rc_user', $this->fld_user);
+ $this->addFieldsIf('rc_user_text', $this->fld_user);
+ $this->addFieldsIf('rc_minor', $this->fld_flags);
+ $this->addFieldsIf('rc_bot', $this->fld_flags);
+ $this->addFieldsIf('rc_new', $this->fld_flags);
+ $this->addFieldsIf('rc_old_len', $this->fld_sizes);
+ $this->addFieldsIf('rc_new_len', $this->fld_sizes);
}
$this->addOption('LIMIT', $limit +1);
@@ -91,15 +107,16 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$count = 0;
$db = $this->getDB();
$res = $this->select(__METHOD__);
+
while ($row = $db->fetchObject($res)) {
if (++ $count > $limit) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter('start', $row->rc_timestamp);
+ $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rc_timestamp));
break;
}
- $vals = $this->addRowInfo('rc', $row);
- if ($vals)
+ $vals = $this->extractRowInfo($row);
+ if($vals)
$data[] = $vals;
}
$db->freeResult($res);
@@ -109,6 +126,59 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$result->addValue('query', $this->getModuleName(), $data);
}
+ private function extractRowInfo($row) {
+ $movedToTitle = false;
+ if (!empty($row->rc_moved_to_title))
+ $movedToTitle = Title :: makeTitle($row->rc_moved_to_ns, $row->rc_moved_to_title);
+
+ $title = Title :: makeTitle($row->rc_namespace, $row->rc_title);
+ $vals = array ();
+
+ $vals['type'] = intval($row->rc_type);
+
+ if ($this->fld_title) {
+ ApiQueryBase :: addTitleInfo($vals, $title);
+ if ($movedToTitle)
+ ApiQueryBase :: addTitleInfo($vals, $movedToTitle, "new_");
+ }
+
+ if ($this->fld_ids) {
+ $vals['rcid'] = intval($row->rc_id);
+ $vals['pageid'] = intval($row->rc_cur_id);
+ $vals['revid'] = intval($row->rc_this_oldid);
+ $vals['old_revid'] = intval( $row->rc_last_oldid );
+ }
+
+ if ($this->fld_user) {
+ $vals['user'] = $row->rc_user_text;
+ if(!$row->rc_user)
+ $vals['anon'] = '';
+ }
+
+ if ($this->fld_flags) {
+ if ($row->rc_bot)
+ $vals['bot'] = '';
+ if ($row->rc_new)
+ $vals['new'] = '';
+ if ($row->rc_minor)
+ $vals['minor'] = '';
+ }
+
+ if ($this->fld_sizes) {
+ $vals['oldlen'] = intval($row->rc_old_len);
+ $vals['newlen'] = intval($row->rc_new_len);
+ }
+
+ if ($this->fld_timestamp)
+ $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rc_timestamp);
+
+ if ($this->fld_comment && !empty ($row->rc_comment)) {
+ $vals['comment'] = $row->rc_comment;
+ }
+
+ return $vals;
+ }
+
protected function getAllowedParams() {
return array (
'start' => array (
@@ -130,10 +200,15 @@ class ApiQueryRecentChanges extends ApiQueryBase {
),
'prop' => array (
ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_DFLT => 'title|timestamp|ids',
ApiBase :: PARAM_TYPE => array (
'user',
'comment',
- 'flags'
+ 'flags',
+ 'timestamp',
+ 'title',
+ 'ids',
+ 'sizes'
)
),
'show' => array (
@@ -151,7 +226,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
ApiBase :: PARAM_DFLT => 10,
ApiBase :: PARAM_TYPE => 'limit',
ApiBase :: PARAM_MIN => 1,
- ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
)
);
@@ -183,7 +258,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 24100 2007-07-15 01:12:54Z yurik $';
}
}
-?>
+
diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php
index fc5f6241..2672478b 100644
--- a/includes/api/ApiQueryRevisions.php
+++ b/includes/api/ApiQueryRevisions.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,10 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * A query action to enumerate revisions of a given page, or show top revisions of multiple pages.
+ * Various pieces of information may be shown - flags, comments, and the actual wiki markup of the rev.
+ * In the enumeration mode, ranges of revisions may be requested and filtered.
+ *
* @addtogroup API
*/
class ApiQueryRevisions extends ApiQueryBase {
@@ -37,15 +41,18 @@ class ApiQueryRevisions extends ApiQueryBase {
parent :: __construct($query, $moduleName, 'rv');
}
+ private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false,
+ $fld_comment = false, $fld_user = false, $fld_content = false;
+
public function execute() {
- $limit = $startid = $endid = $start = $end = $dir = $prop = null;
+ $limit = $startid = $endid = $start = $end = $dir = $prop = $user = $excludeuser = null;
extract($this->extractRequestParams());
// If any of those parameters are used, work in 'enumeration' mode.
// Enum mode can only be used when exactly one page is provided.
// Enumerating revisions on multiple pages make it extremelly
// difficult to manage continuations and require additional sql indexes
- $enumRevMode = (!is_null($limit) || !is_null($startid) || !is_null($endid) || $dir === 'newer' || !is_null($start) || !is_null($end));
+ $enumRevMode = (!is_null($user) || !is_null($excludeuser) || !is_null($limit) || !is_null($startid) || !is_null($endid) || $dir === 'newer' || !is_null($start) || !is_null($end));
$pageSet = $this->getPageSet();
$pageCount = $pageSet->getGoodTitleCount();
@@ -59,39 +66,50 @@ class ApiQueryRevisions extends ApiQueryBase {
$this->dieUsage('The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).', 'revids');
if ($pageCount > 1 && $enumRevMode)
- $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, start, and end parameters may only be used on a single page.', 'multpages');
+ $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start, and end parameters may only be used on a single page.', 'multpages');
$this->addTables('revision');
- $this->addFields(array (
- 'rev_id',
- 'rev_page',
- 'rev_text_id',
- 'rev_minor_edit'
- ));
$this->addWhere('rev_deleted=0');
- $showContent = false;
+ $prop = array_flip($prop);
- if (!is_null($prop)) {
- $prop = array_flip($prop);
- $this->addFieldsIf('rev_timestamp', isset ($prop['timestamp']));
- $this->addFieldsIf('rev_comment', isset ($prop['comment']));
- if (isset ($prop['user'])) {
- $this->addFields('rev_user');
- $this->addFields('rev_user_text');
- }
- if (isset ($prop['content'])) {
- $this->addTables('text');
- $this->addWhere('rev_text_id=old_id');
- $this->addFields('old_id');
- $this->addFields('old_text');
- $this->addFields('old_flags');
- $showContent = true;
+ // These field are needed regardless of the client requesting them
+ $this->addFields('rev_id');
+ $this->addFields('rev_page');
+
+ // Optional fields
+ $this->fld_ids = isset ($prop['ids']);
+ // $this->addFieldsIf('rev_text_id', $this->fld_ids); // should this be exposed?
+ $this->fld_flags = $this->addFieldsIf('rev_minor_edit', isset ($prop['flags']));
+ $this->fld_timestamp = $this->addFieldsIf('rev_timestamp', isset ($prop['timestamp']));
+ $this->fld_comment = $this->addFieldsIf('rev_comment', isset ($prop['comment']));
+ $this->fld_size = $this->addFieldsIf('rev_len', isset ($prop['size']));
+
+ if (isset ($prop['user'])) {
+ $this->addFields('rev_user');
+ $this->addFields('rev_user_text');
+ $this->fld_user = true;
+ }
+ if (isset ($prop['content'])) {
+
+ // For each page we will request, the user must have read rights for that page
+ foreach ($pageSet->getGoodTitles() as $title) {
+ if( !$title->userCanRead() )
+ $this->dieUsage(
+ 'The current user is not allowed to read ' . $title->getPrefixedText(),
+ 'accessdenied');
}
+
+ $this->addTables('text');
+ $this->addWhere('rev_text_id=old_id');
+ $this->addFields('old_id');
+ $this->addFields('old_text');
+ $this->addFields('old_flags');
+ $this->fld_content = true;
}
- $userMax = ($showContent ? 50 : 500);
- $botMax = ($showContent ? 200 : 10000);
+ $userMax = ($this->fld_content ? 50 : 500);
+ $botMax = ($this->fld_content ? 200 : 10000);
if ($enumRevMode) {
@@ -102,6 +120,9 @@ class ApiQueryRevisions extends ApiQueryBase {
if (!is_null($endid) && !is_null($end))
$this->dieUsage('end and endid cannot be used together', 'badparams');
+ if(!is_null($user) && !is_null( $excludeuser))
+ $this->dieUsage('user and excludeuser cannot be used together', 'badparams');
+
// This code makes an assumption that sorting by rev_id and rev_timestamp produces
// the same result. This way users may request revisions starting at a given time,
// but to page through results use the rev_id returned after each page.
@@ -117,10 +138,25 @@ class ApiQueryRevisions extends ApiQueryBase {
// must manually initialize unset limit
if (is_null($limit))
$limit = 10;
- $this->validateLimit($this->encodeParamName('limit'), $limit, 1, $userMax, $botMax);
+ $this->validateLimit('limit', $limit, 1, $userMax, $botMax);
// There is only one ID, use it
$this->addWhereFld('rev_page', current(array_keys($pageSet->getGoodTitles())));
+
+ if(!is_null($user)) {
+ $this->addWhereFld('rev_user_text', $user);
+ } elseif (!is_null( $excludeuser)) {
+ $this->addWhere('rev_user_text != ' . $this->getDB()->addQuotes($excludeuser));
+ }
+ }
+ elseif ($revCount > 0) {
+ $this->validateLimit('rev_count', $revCount, 1, $userMax, $botMax);
+
+ // Get all revision IDs
+ $this->addWhereFld('rev_id', array_keys($pageSet->getRevisionIDs()));
+
+ // assumption testing -- we should never get more then $revCount rows.
+ $limit = $revCount;
}
elseif ($pageCount > 0) {
// When working in multi-page non-enumeration mode,
@@ -133,15 +169,8 @@ class ApiQueryRevisions extends ApiQueryBase {
// Get all page IDs
$this->addWhereFld('page_id', array_keys($pageSet->getGoodTitles()));
- $limit = $pageCount; // assumption testing -- we should never get more then $pageCount rows.
- }
- elseif ($revCount > 0) {
- $this->validateLimit('rev_count', $revCount, 1, $userMax, $botMax);
-
- // Get all revision IDs
- $this->addWhereFld('rev_id', array_keys($pageSet->getRevisionIDs()));
-
- $limit = $revCount; // assumption testing -- we should never get more then $revCount rows.
+ // assumption testing -- we should never get more then $pageCount rows.
+ $limit = $pageCount;
} else
ApiBase :: dieDebug(__METHOD__, 'param validation?');
@@ -158,21 +187,18 @@ class ApiQueryRevisions extends ApiQueryBase {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
if (!$enumRevMode)
ApiBase :: dieDebug(__METHOD__, 'Got more rows then expected'); // bug report
- $this->setContinueEnumParameter('startid', $row->rev_id);
+ $this->setContinueEnumParameter('startid', intval($row->rev_id));
break;
}
- $vals = $this->addRowInfo('rev', $row);
- if ($vals) {
- if ($showContent)
- ApiResult :: setContent($vals, Revision :: getRevisionText($row));
-
- $this->getResult()->addValue(array (
+ $this->getResult()->addValue(
+ array (
'query',
'pages',
- intval($row->rev_page
- ), 'revisions'), intval($row->rev_id), $vals);
- }
+ intval($row->rev_page),
+ 'revisions'),
+ null,
+ $this->extractRowInfo($row));
}
$db->freeResult($res);
@@ -188,21 +214,62 @@ class ApiQueryRevisions extends ApiQueryBase {
}
}
+ private function extractRowInfo($row) {
+
+ $vals = array ();
+
+ if ($this->fld_ids) {
+ $vals['revid'] = intval($row->rev_id);
+ // $vals['oldid'] = intval($row->rev_text_id); // todo: should this be exposed?
+ }
+
+ if ($this->fld_flags && $row->rev_minor_edit)
+ $vals['minor'] = '';
+
+ if ($this->fld_user) {
+ $vals['user'] = $row->rev_user_text;
+ if (!$row->rev_user)
+ $vals['anon'] = '';
+ }
+
+ if ($this->fld_timestamp) {
+ $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rev_timestamp);
+ }
+
+ if ($this->fld_size && !is_null($row->rev_len)) {
+ $vals['size'] = intval($row->rev_len);
+ }
+
+ if ($this->fld_comment && !empty ($row->rev_comment)) {
+ $vals['comment'] = $row->rev_comment;
+ }
+
+ if ($this->fld_content) {
+ ApiResult :: setContent($vals, Revision :: getRevisionText($row));
+ }
+
+ return $vals;
+ }
+
protected function getAllowedParams() {
return array (
'prop' => array (
ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_DFLT => 'ids|timestamp|flags|comment|user',
ApiBase :: PARAM_TYPE => array (
+ 'ids',
+ 'flags',
'timestamp',
'user',
+ 'size',
'comment',
- 'content'
+ 'content',
)
),
'limit' => array (
ApiBase :: PARAM_TYPE => 'limit',
ApiBase :: PARAM_MIN => 1,
- ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_SML1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_SML1,
ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_SML2
),
'startid' => array (
@@ -223,6 +290,12 @@ class ApiQueryRevisions extends ApiQueryBase {
'newer',
'older'
)
+ ),
+ 'user' => array(
+ ApiBase :: PARAM_TYPE => 'user'
+ ),
+ 'excludeuser' => array(
+ ApiBase :: PARAM_TYPE => 'user'
)
);
}
@@ -235,7 +308,9 @@ class ApiQueryRevisions extends ApiQueryBase {
'endid' => 'stop revision enumeration on this revid (enum)',
'start' => 'from which revision timestamp to start enumeration (enum)',
'end' => 'enumerate up to this timestamp (enum)',
- 'dir' => 'direction of enumeration - towards "newer" or "older" revisions (enum)'
+ 'dir' => 'direction of enumeration - towards "newer" or "older" revisions (enum)',
+ 'user' => 'only include revisions made by user',
+ 'excludeuser' => 'exclude revisions made by user',
);
}
@@ -259,12 +334,16 @@ class ApiQueryRevisions extends ApiQueryBase {
'Get first 5 revisions of the "Main Page":',
' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvdir=newer',
'Get first 5 revisions of the "Main Page" made after 2006-05-01:',
- ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvdir=newer&rvstart=20060501000000'
+ ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvdir=newer&rvstart=20060501000000',
+ 'Get first 5 revisions of the "Main Page" that were not made made by anonymous user "127.0.0.1"',
+ ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvexcludeuser=127.0.0.1',
+ 'Get first 5 revisions of the "Main Page" that were made by the user "MediaWiki default"',
+ ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvuser=MediaWiki%20default',
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryRevisions.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiQueryRevisions.php 25407 2007-09-02 14:00:11Z tstarling $';
}
}
-?>
+
diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php
new file mode 100644
index 00000000..268616b1
--- /dev/null
+++ b/includes/api/ApiQuerySearch.php
@@ -0,0 +1,151 @@
+<?php
+
+/*
+ * Created on July 30, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiQueryBase.php');
+}
+
+/**
+ * Query module to perform full text search within wiki titles and content
+ *
+ * @addtogroup API
+ */
+class ApiQuerySearch extends ApiQueryGeneratorBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'sr');
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function executeGenerator($resultPageSet) {
+ $this->run($resultPageSet);
+ }
+
+ private function run($resultPageSet = null) {
+
+ $params = $this->extractRequestParams();
+
+ $limit = $params['limit'];
+ $query = $params['search'];
+ if (is_null($query) || empty($query))
+ $this->dieUsage("empty search string is not allowed", 'param-search');
+
+ $search = SearchEngine::create();
+ $search->setLimitOffset( $limit+1, $params['offset'] );
+ $search->setNamespaces( $params['namespace'] );
+ $search->showRedirects = $params['redirects'];
+
+ if ($params['what'] == 'text')
+ $matches = $search->searchText( $query );
+ else
+ $matches = $search->searchTitle( $query );
+
+ $data = array ();
+ $count = 0;
+ while( $result = $matches->next() ) {
+ if (++ $count > $limit) {
+ // We've reached the one extra which shows that there are additional items to be had. Stop here...
+ $this->setContinueEnumParameter('offset', $params['offset'] + $params['limit']);
+ break;
+ }
+
+ $title = $result->getTitle();
+ if (is_null($resultPageSet)) {
+ $data[] = array(
+ 'ns' => intval($title->getNamespace()),
+ 'title' => $title->getPrefixedText());
+ } else {
+ $data[] = $title;
+ }
+ }
+
+ if (is_null($resultPageSet)) {
+ $result = $this->getResult();
+ $result->setIndexedTagName($data, 'p');
+ $result->addValue('query', $this->getModuleName(), $data);
+ } else {
+ $resultPageSet->populateFromTitles($data);
+ }
+ }
+
+ protected function getAllowedParams() {
+ return array (
+ 'search' => null,
+ 'namespace' => array (
+ ApiBase :: PARAM_DFLT => 0,
+ ApiBase :: PARAM_TYPE => 'namespace',
+ ApiBase :: PARAM_ISMULTI => true,
+ ),
+ 'what' => array (
+ ApiBase :: PARAM_DFLT => 'title',
+ ApiBase :: PARAM_TYPE => array (
+ 'title',
+ 'text',
+ )
+ ),
+ 'redirects' => false,
+ 'offset' => 0,
+ 'limit' => array (
+ ApiBase :: PARAM_DFLT => 10,
+ ApiBase :: PARAM_TYPE => 'limit',
+ ApiBase :: PARAM_MIN => 1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
+ )
+ );
+ }
+
+ protected function getParamDescription() {
+ return array (
+ 'search' => 'Search for all page titles (or content) that has this value.',
+ 'namespace' => 'The namespace(s) to enumerate.',
+ 'what' => 'Search inside the text or titles.',
+ 'redirects' => 'Include redirect pages in the search.',
+ 'offset' => 'Use this value to continue paging (return by query)',
+ 'limit' => 'How many total pages to return.'
+ );
+ }
+
+ protected function getDescription() {
+ return 'Perform a full text search';
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=query&list=search&srsearch=meaning',
+ 'api.php?action=query&list=search&srwhat=text&srsearch=meaning',
+ 'api.php?action=query&generator=search&gsrsearch=meaning&prop=info',
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQuerySearch.php 24453 2007-07-30 08:09:15Z yurik $';
+ }
+}
+
diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php
index fa185c97..1fa3d8fc 100644
--- a/includes/api/ApiQuerySiteinfo.php
+++ b/includes/api/ApiQuerySiteinfo.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,8 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * A query action to return meta information about the wiki site.
+ *
* @addtogroup API
*/
class ApiQuerySiteinfo extends ApiQueryBase {
@@ -38,58 +40,164 @@ class ApiQuerySiteinfo extends ApiQueryBase {
}
public function execute() {
- $prop = null;
- extract($this->extractRequestParams());
- foreach ($prop as $p) {
- switch ($p) {
+ $params = $this->extractRequestParams();
+ foreach ($params['prop'] as $p) {
+ switch ($p) {
+ default :
+ ApiBase :: dieDebug(__METHOD__, "Unknown prop=$p");
case 'general' :
-
- global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText;
- $data = array ();
- $mainPage = Title :: newFromText(wfMsgForContent('mainpage'));
- $data['mainpage'] = $mainPage->getText();
- $data['base'] = $mainPage->getFullUrl();
- $data['sitename'] = $wgSitename;
- $data['generator'] = "MediaWiki $wgVersion";
- $data['case'] = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; // 'case-insensitive' option is reserved for future
- if (isset($wgRightsCode))
- $data['rightscode'] = $wgRightsCode;
- $data['rights'] = $wgRightsText;
- $this->getResult()->addValue('query', $p, $data);
+ $this->appendGeneralInfo($p);
break;
-
case 'namespaces' :
-
- global $wgContLang;
- $data = array ();
- foreach ($wgContLang->getFormattedNamespaces() as $ns => $title) {
- $data[$ns] = array (
- 'id' => $ns
- );
- ApiResult :: setContent($data[$ns], $title);
- }
- $this->getResult()->setIndexedTagName($data, 'ns');
- $this->getResult()->addValue('query', $p, $data);
+ $this->appendNamespaces($p);
+ break;
+ case 'interwikimap' :
+ $filteriw = isset($params['filteriw']) ? $params['filteriw'] : false;
+ $this->appendInterwikiMap($p, $filteriw);
+ break;
+ case 'dbrepllag' :
+ $this->appendDbReplLagInfo($p, $params['showalldb']);
+ break;
+ case 'statistics' :
+ $this->appendStatistics($p);
break;
-
- default :
- ApiBase :: dieDebug(__METHOD__, "Unknown prop=$p");
}
}
}
+ protected function appendGeneralInfo($property) {
+ global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText, $wgLanguageCode;
+
+ $data = array ();
+ $mainPage = Title :: newFromText(wfMsgForContent('mainpage'));
+ $data['mainpage'] = $mainPage->getText();
+ $data['base'] = $mainPage->getFullUrl();
+ $data['sitename'] = $wgSitename;
+ $data['generator'] = "MediaWiki $wgVersion";
+ $data['case'] = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; // 'case-insensitive' option is reserved for future
+ if (isset($wgRightsCode))
+ $data['rightscode'] = $wgRightsCode;
+ $data['rights'] = $wgRightsText;
+ $data['lang'] = $wgLanguageCode;
+
+ $this->getResult()->addValue('query', $property, $data);
+ }
+
+ protected function appendNamespaces($property) {
+ global $wgContLang;
+
+ $data = array ();
+ foreach ($wgContLang->getFormattedNamespaces() as $ns => $title) {
+ $data[$ns] = array (
+ 'id' => $ns
+ );
+ ApiResult :: setContent($data[$ns], $title);
+ }
+
+ $this->getResult()->setIndexedTagName($data, 'ns');
+ $this->getResult()->addValue('query', $property, $data);
+ }
+
+ protected function appendInterwikiMap($property, $filter) {
+
+ $this->resetQueryParams();
+ $this->addTables('interwiki');
+ $this->addFields(array('iw_prefix', 'iw_local', 'iw_url'));
+
+ if($filter === 'local') {
+ $this->addWhere('iw_local = 1');
+ } elseif($filter === '!local') {
+ $this->addWhere('iw_local = 0');
+ } elseif($filter !== false) {
+ ApiBase :: dieDebug(__METHOD__, "Unknown filter=$filter");
+ }
+
+ $this->addOption('ORDER BY', 'iw_prefix');
+
+ $db = $this->getDB();
+ $res = $this->select(__METHOD__);
+
+ $data = array();
+ while($row = $db->fetchObject($res))
+ {
+ $val['prefix'] = $row->iw_prefix;
+ if ($row->iw_local == '1')
+ $val['local'] = '';
+// $val['trans'] = intval($row->iw_trans); // should this be exposed?
+ $val['url'] = $row->iw_url;
+
+ $data[] = $val;
+ }
+ $db->freeResult($res);
+
+ $this->getResult()->setIndexedTagName($data, 'iw');
+ $this->getResult()->addValue('query', $property, $data);
+ }
+
+ protected function appendDbReplLagInfo($property, $includeAll) {
+ global $wgLoadBalancer, $wgShowHostnames;
+
+ $data = array();
+
+ if ($includeAll) {
+ if (!$wgShowHostnames)
+ $this->dieUsage('Cannot view all servers info unless $wgShowHostnames is true', 'includeAllDenied');
+
+ global $wgDBservers;
+ $lags = $wgLoadBalancer->getLagTimes();
+ foreach( $lags as $i => $lag ) {
+ $data[] = array (
+ 'host' => $wgDBservers[$i]['host'],
+ 'lag' => $lag);
+ }
+ } else {
+ list( $host, $lag ) = $wgLoadBalancer->getMaxLag();
+ $data[] = array (
+ 'host' => $wgShowHostnames ? $host : '',
+ 'lag' => $lag);
+ }
+
+ $result = $this->getResult();
+ $result->setIndexedTagName($data, 'db');
+ $result->addValue('query', $property, $data);
+ }
+
+ protected function appendStatistics($property) {
+ $data = array ();
+ $data['pages'] = intval(SiteStats::pages());
+ $data['articles'] = intval(SiteStats::articles());
+ $data['views'] = intval(SiteStats::views());
+ $data['edits'] = intval(SiteStats::edits());
+ $data['images'] = intval(SiteStats::images());
+ $data['users'] = intval(SiteStats::users());
+ $data['admins'] = intval(SiteStats::admins());
+ $data['jobs'] = intval(SiteStats::jobs());
+ $this->getResult()->addValue('query', $property, $data);
+ }
+
protected function getAllowedParams() {
return array (
+
'prop' => array (
ApiBase :: PARAM_DFLT => 'general',
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => array (
'general',
- 'namespaces'
- )
- )
+ 'namespaces',
+ 'interwikimap',
+ 'dbrepllag',
+ 'statistics',
+ )),
+
+ 'filteriw' => array (
+ ApiBase :: PARAM_TYPE => array (
+ 'local',
+ '!local',
+ )),
+
+ 'showalldb' => false,
);
}
@@ -97,9 +205,14 @@ class ApiQuerySiteinfo extends ApiQueryBase {
return array (
'prop' => array (
'Which sysinfo properties to get:',
- ' "general" - Overall system information',
- ' "namespaces" - List of registered namespaces (localized)'
- )
+ ' "general" - Overall system information',
+ ' "namespaces" - List of registered namespaces (localized)',
+ ' "statistics" - Returns site statistics',
+ ' "interwikimap" - Returns interwiki map (optionally filtered)',
+ ' "dbrepllag" - Returns database server with the highest replication lag',
+ ),
+ 'filteriw' => 'Return only local or only nonlocal entries of the interwiki map',
+ 'showalldb' => 'List all database servers, not just the one lagging the most',
);
}
@@ -108,11 +221,14 @@ class ApiQuerySiteinfo extends ApiQueryBase {
}
protected function getExamples() {
- return 'api.php?action=query&meta=siteinfo&siprop=general|namespaces';
+ return array(
+ 'api.php?action=query&meta=siteinfo&siprop=general|namespaces|statistics',
+ 'api.php?action=query&meta=siteinfo&siprop=interwikimap&sifilteriw=local',
+ 'api.php?action=query&meta=siteinfo&siprop=dbrepllag&sishowalldb',
+ );
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 25238 2007-08-28 15:37:31Z robchurch $';
}
-}
-?>
+} \ No newline at end of file
diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php
index 05bfbb20..05c3d945 100644
--- a/includes/api/ApiQueryUserContributions.php
+++ b/includes/api/ApiQueryUserContributions.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,8 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * This query action adds a list of a specified user's contributions to the output.
+ *
* @addtogroup API
*/
class ApiQueryContributions extends ApiQueryBase {
@@ -37,83 +39,49 @@ class ApiQueryContributions extends ApiQueryBase {
parent :: __construct($query, $moduleName, 'uc');
}
+ private $params, $username;
+ private $fld_ids = false, $fld_title = false, $fld_timestamp = false,
+ $fld_comment = false, $fld_flags = false;
+
public function execute() {
- //Blank all our variables
- $limit = $user = $start = $end = $dir = null;
+ // Parse some parameters
+ $this->params = $this->extractRequestParams();
- //Get our parameters out
- extract($this->extractRequestParams());
+ $prop = array_flip($this->params['prop']);
+ $this->fld_ids = isset($prop['ids']);
+ $this->fld_title = isset($prop['title']);
+ $this->fld_comment = isset($prop['comment']);
+ $this->fld_flags = isset($prop['flags']);
+ $this->fld_timestamp = isset($prop['timestamp']);
- //Get a database instance
+ // TODO: if the query is going only against the revision table, should this be done?
+ $this->selectNamedDB('contributions', DB_SLAVE, 'contributions');
$db = $this->getDB();
- if (is_null($user))
- $this->dieUsage("User parameter may not be empty", 'param_user');
- $userid = $db->selectField('user', 'user_id', array (
- 'user_name' => $user
- ));
- if (!$userid)
- $this->dieUsage("User name $user not found", 'param_user');
-
- //Get the table names
- list ($tbl_page, $tbl_revision) = $db->tableNamesN('page', 'revision');
-
- //We're after the revision table, and the corresponding page row for
- //anything we retrieve.
- $this->addTables("$tbl_revision LEFT OUTER JOIN $tbl_page ON " .
- "page_id=rev_page");
-
- //We want to know the namespace, title, new-ness, and ID of a page,
- // and the id, text-id, timestamp, minor-status, summary and page
- // of a revision.
- $this->addFields(array('page_namespace', 'page_title', 'page_is_new',
- 'rev_id', 'rev_text_id', 'rev_timestamp', 'rev_minor_edit',
- 'rev_comment', 'rev_page'));
-
- // We only want pages by the specified user.
- $this->addWhereFld('rev_user_text', $user);
- // ... and in the specified timeframe.
- $this->addWhereRange('rev_timestamp', $dir, $start, $end );
+ // Prepare query
+ $this->prepareUsername();
+ $this->prepareQuery();
- $this->addOption('LIMIT', $limit + 1);
+ //Do the actual query.
+ $res = $this->select( __METHOD__ );
//Initialise some variables
$data = array ();
$count = 0;
-
- //Do the actual query.
- $res = $this->select( __METHOD__ );
+ $limit = $this->params['limit'];
//Fetch each row
while ( $row = $db->fetchObject( $res ) ) {
if (++ $count > $limit) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter('start', $row->rev_timestamp);
+ $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rev_timestamp));
break;
}
- //There's a fancy function in ApiQueryBase that does
- // most of the work for us. Use that for the page
- // and revision.
- $revvals = $this->addRowInfo('rev', $row);
- $pagevals = $this->addRowInfo('page', $row);
-
- //If we got data on the revision only, use only
- // that data.
- if($revvals && !$pagevals) {
- $data[] = $revvals;
- }
- //If we got data on the page only, use only
- // that data.
- else if($pagevals && !$revvals) {
- $data[] = $pagevals;
- }
- //... and if we got data on both the revision and
- // the page, merge the data and send it out.
- else if($pagevals && $revvals) {
- $data[] = array_merge($revvals, $pagevals);
- }
+ $vals = $this->extractRowInfo($row);
+ if ($vals)
+ $data[] = $vals;
}
//Free the database record so the connection can get on with other stuff
@@ -124,13 +92,117 @@ class ApiQueryContributions extends ApiQueryBase {
$this->getResult()->addValue('query', $this->getModuleName(), $data);
}
+ /**
+ * Validate the 'user' parameter and set the value to compare
+ * against `revision`.`rev_user_text`
+ */
+ private function prepareUsername() {
+ $user = $this->params['user'];
+ if( $user ) {
+ $name = User::isIP( $user )
+ ? $user
+ : User::getCanonicalName( $user, 'valid' );
+ if( $name === false ) {
+ $this->dieUsage( "User name {$user} is not valid", 'param_user' );
+ } else {
+ $this->username = $name;
+ }
+ } else {
+ $this->dieUsage( 'User parameter may not be empty', 'param_user' );
+ }
+ }
+
+ /**
+ * Prepares the query and returns the limit of rows requested
+ */
+ private function prepareQuery() {
+
+ //We're after the revision table, and the corresponding page row for
+ //anything we retrieve.
+ list ($tbl_page, $tbl_revision) = $this->getDB()->tableNamesN('page', 'revision');
+ $this->addTables("$tbl_revision LEFT OUTER JOIN $tbl_page ON page_id=rev_page");
+
+ $this->addWhereFld('rev_deleted', 0);
+
+ // We only want pages by the specified user.
+ $this->addWhereFld( 'rev_user_text', $this->username );
+
+ // ... and in the specified timeframe.
+ $this->addWhereRange('rev_timestamp',
+ $this->params['dir'], $this->params['start'], $this->params['end'] );
+
+ $this->addWhereFld('page_namespace', $this->params['namespace']);
+
+ $show = $this->params['show'];
+ if (!is_null($show)) {
+ $show = array_flip($show);
+ if (isset ($show['minor']) && isset ($show['!minor']))
+ $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show');
+
+ $this->addWhereIf('rev_minor_edit = 0', isset ($show['!minor']));
+ $this->addWhereIf('rev_minor_edit != 0', isset ($show['minor']));
+ }
+
+ $this->addOption('LIMIT', $this->params['limit'] + 1);
+
+ // Mandatory fields: timestamp allows request continuation
+ // ns+title checks if the user has access rights for this page
+ $this->addFields(array(
+ 'rev_timestamp',
+ 'page_namespace',
+ 'page_title',
+ ));
+
+ $this->addFieldsIf('rev_page', $this->fld_ids);
+ $this->addFieldsIf('rev_id', $this->fld_ids);
+ // $this->addFieldsIf('rev_text_id', $this->fld_ids); // Should this field be exposed?
+ $this->addFieldsIf('rev_comment', $this->fld_comment);
+ $this->addFieldsIf('rev_minor_edit', $this->fld_flags);
+
+ // These fields depend only work if the page table is joined
+ $this->addFieldsIf('page_is_new', $this->fld_flags);
+ }
+
+ /**
+ * Extract fields from the database row and append them to a result array
+ */
+ private function extractRowInfo($row) {
+
+ $vals = array();
+
+ if ($this->fld_ids) {
+ $vals['pageid'] = intval($row->rev_page);
+ $vals['revid'] = intval($row->rev_id);
+ // $vals['textid'] = intval($row->rev_text_id); // todo: Should this field be exposed?
+ }
+
+ if ($this->fld_title)
+ ApiQueryBase :: addTitleInfo($vals,
+ Title :: makeTitle($row->page_namespace, $row->page_title));
+
+ if ($this->fld_timestamp)
+ $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rev_timestamp);
+
+ if ($this->fld_flags) {
+ if ($row->page_is_new)
+ $vals['new'] = '';
+ if ($row->rev_minor_edit)
+ $vals['minor'] = '';
+ }
+
+ if ($this->fld_comment && !empty ($row->rev_comment))
+ $vals['comment'] = $row->rev_comment;
+
+ return $vals;
+ }
+
protected function getAllowedParams() {
return array (
'limit' => array (
ApiBase :: PARAM_DFLT => 10,
ApiBase :: PARAM_TYPE => 'limit',
ApiBase :: PARAM_MIN => 1,
- ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
),
'start' => array (
@@ -139,14 +211,38 @@ class ApiQueryContributions extends ApiQueryBase {
'end' => array (
ApiBase :: PARAM_TYPE => 'timestamp'
),
- 'user' => null,
+ 'user' => array (
+ ApiBase :: PARAM_TYPE => 'user'
+ ),
'dir' => array (
ApiBase :: PARAM_DFLT => 'older',
ApiBase :: PARAM_TYPE => array (
'newer',
'older'
)
- )
+ ),
+ 'namespace' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => 'namespace'
+ ),
+ 'prop' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_DFLT => 'ids|title|timestamp|flags|comment',
+ ApiBase :: PARAM_TYPE => array (
+ 'ids',
+ 'title',
+ 'timestamp',
+ 'comment',
+ 'flags'
+ )
+ ),
+ 'show' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array (
+ 'minor',
+ '!minor',
+ )
+ ),
);
}
@@ -156,12 +252,15 @@ class ApiQueryContributions extends ApiQueryBase {
'start' => 'The start timestamp to return from.',
'end' => 'The end timestamp to return to.',
'user' => 'The user to retrieve contributions for.',
- 'dir' => 'The direction to search (older or newer).'
+ 'dir' => 'The direction to search (older or newer).',
+ 'namespace' => 'Only list contributions in these namespaces',
+ 'prop' => 'Include additional pieces of information',
+ 'show' => 'Show only items that meet this criteria, e.g. non minor edits only: show=!minor',
);
}
protected function getDescription() {
- return 'Get edits by a user..';
+ return 'Get all edits by a user';
}
protected function getExamples() {
@@ -171,7 +270,7 @@ class ApiQueryContributions extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryUserContributions.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiQueryUserContributions.php 24754 2007-08-13 18:18:18Z robchurch $';
}
}
-?>
+
diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php
new file mode 100644
index 00000000..a41b8679
--- /dev/null
+++ b/includes/api/ApiQueryUserInfo.php
@@ -0,0 +1,133 @@
+<?php
+
+/*
+ * Created on July 30, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiQueryBase.php');
+}
+
+/**
+ * Query module to get information about the currently logged-in user
+ *
+ * @addtogroup API
+ */
+class ApiQueryUserInfo extends ApiQueryBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'ui');
+ }
+
+ public function execute() {
+
+ global $wgUser;
+
+ $params = $this->extractRequestParams();
+ $result = $this->getResult();
+
+ $vals = array();
+ $vals['name'] = $wgUser->getName();
+
+ if( $wgUser->isAnon() ) $vals['anon'] = '';
+
+ if (!is_null($params['prop'])) {
+ $prop = array_flip($params['prop']);
+ if (isset($prop['blockinfo'])) {
+ if ($wgUser->isBlocked()) {
+ $vals['blockedby'] = User::whoIs($wgUser->blockedBy());
+ $vals['blockreason'] = $wgUser->blockedFor();
+ }
+ }
+ if (isset($prop['hasmsg']) && $wgUser->getNewtalk()) {
+ $vals['messages'] = '';
+ }
+ if (isset($prop['groups'])) {
+ $vals['groups'] = $wgUser->getGroups();
+ $result->setIndexedTagName($vals['groups'], 'g'); // even if empty
+ }
+ if (isset($prop['rights'])) {
+ $vals['rights'] = $wgUser->getRights();
+ $result->setIndexedTagName($vals['rights'], 'r'); // even if empty
+ }
+ }
+
+ if (!empty($params['option'])) {
+ foreach( $params['option'] as $option ) {
+ if (empty($option))
+ $this->dieUsage('Empty value is not allowed for the option parameter', 'option');
+ $vals['options'][$option] = $wgUser->getOption($option);
+ }
+ }
+
+ $result->addValue(null, $this->getModuleName(), $vals);
+ }
+
+ protected function getAllowedParams() {
+ return array (
+ 'prop' => array (
+ ApiBase :: PARAM_DFLT => NULL,
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array (
+ 'blockinfo',
+ 'hasmsg',
+ 'groups',
+ 'rights',
+ )),
+ 'option' => array (
+ ApiBase :: PARAM_DFLT => NULL,
+ ApiBase :: PARAM_ISMULTI => true,
+ ),
+ );
+ }
+
+ protected function getParamDescription() {
+ return array (
+ 'prop' => array(
+ 'What pieces of information to include',
+ ' blockinfo - tags if the user is blocked, by whom, and for what reason',
+ ' hasmsg - adds a tag "message" if user has pending messages',
+ ' groups - lists all the groups the current user belongs to',
+ ' rights - lists of all rights the current user has',
+ ),
+ 'option' => 'A list of user preference options to get',
+ );
+ }
+
+ protected function getDescription() {
+ return 'Get information about the current user';
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=query&meta=userinfo',
+ 'api.php?action=query&meta=userinfo&uiprop=blockinfo|groups|rights|hasmsg',
+ 'api.php?action=query&meta=userinfo&uioption=rememberpassword',
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryUserInfo.php 24529 2007-08-01 20:11:29Z yurik $';
+ }
+}
+
diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php
index 73c31abb..16586a40 100644
--- a/includes/api/ApiQueryWatchlist.php
+++ b/includes/api/ApiQueryWatchlist.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,9 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * This query action allows clients to retrieve a list of recently modified pages
+ * that are part of the logged-in user's watchlist.
+ *
* @addtogroup API
*/
class ApiQueryWatchlist extends ApiQueryGeneratorBase {
@@ -45,8 +48,13 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$this->run($resultPageSet);
}
+ private $fld_ids = false, $fld_title = false, $fld_patrol = false, $fld_flags = false,
+ $fld_timestamp = false, $fld_user = false, $fld_comment = false, $fld_sizes = false;
+
private function run($resultPageSet = null) {
- global $wgUser;
+ global $wgUser, $wgDBtype;
+
+ $this->selectNamedDB('watchlist', DB_SLAVE, 'watchlist');
if (!$wgUser->isLoggedIn())
$this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin');
@@ -54,17 +62,20 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$allrev = $start = $end = $namespace = $dir = $limit = $prop = null;
extract($this->extractRequestParams());
- $patrol = $timestamp = $user = $comment = false;
- if (!is_null($prop)) {
- if (!is_null($resultPageSet))
- $this->dieUsage('prop parameter may not be used in a generator', 'params');
+ if (!is_null($prop) && is_null($resultPageSet)) {
+
+ $prop = array_flip($prop);
- $user = (false !== array_search('user', $prop));
- $comment = (false !== array_search('comment', $prop));
- $timestamp = (false !== array_search('timestamp', $prop)); // TODO: $timestamp not currently being used.
- $patrol = (false !== array_search('patrol', $prop));
+ $this->fld_ids = isset($prop['ids']);
+ $this->fld_title = isset($prop['title']);
+ $this->fld_flags = isset($prop['flags']);
+ $this->fld_user = isset($prop['user']);
+ $this->fld_comment = isset($prop['comment']);
+ $this->fld_timestamp = isset($prop['timestamp']);
+ $this->fld_sizes = isset($prop['sizes']);
+ $this->fld_patrol = isset($prop['patrol']);
- if ($patrol) {
+ if ($this->fld_patrol) {
global $wgUseRCPatrol, $wgUser;
if (!$wgUseRCPatrol || !$wgUser->isAllowed('patrol'))
$this->dieUsage('patrol property is not available', 'patrol');
@@ -77,15 +88,17 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'rc_this_oldid',
'rc_namespace',
'rc_title',
- 'rc_new',
- 'rc_minor',
'rc_timestamp'
));
- $this->addFieldsIf('rc_user', $user);
- $this->addFieldsIf('rc_user_text', $user);
- $this->addFieldsIf('rc_comment', $comment);
- $this->addFieldsIf('rc_patrolled', $patrol);
+ $this->addFieldsIf('rc_new', $this->fld_flags);
+ $this->addFieldsIf('rc_minor', $this->fld_flags);
+ $this->addFieldsIf('rc_user', $this->fld_user);
+ $this->addFieldsIf('rc_user_text', $this->fld_user);
+ $this->addFieldsIf('rc_comment', $this->fld_comment);
+ $this->addFieldsIf('rc_patrolled', $this->fld_patrol);
+ $this->addFieldsIf('rc_old_len', $this->fld_sizes);
+ $this->addFieldsIf('rc_new_len', $this->fld_sizes);
}
elseif ($allrev) {
$this->addFields(array (
@@ -114,12 +127,16 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'wl_namespace = rc_namespace',
'wl_title = rc_title',
'rc_cur_id = page_id',
- 'wl_user' => $userId
+ 'wl_user' => $userId,
+ 'rc_deleted' => 0,
));
+
$this->addWhereRange('rc_timestamp', $dir, $start, $end);
$this->addWhereFld('wl_namespace', $namespace);
$this->addWhereIf('rc_this_oldid=page_latest', !$allrev);
- $this->addWhereIf("rc_timestamp > ''", !isset ($start) && !isset ($end));
+
+ # This is a index optimization for mysql, as done in the Special:Watchlist page
+ $this->addWhereIf("rc_timestamp > ''", !isset ($start) && !isset ($end) && $wgDBtype == 'mysql');
$this->addOption('LIMIT', $limit +1);
@@ -131,23 +148,19 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
while ($row = $db->fetchObject($res)) {
if (++ $count > $limit) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter('start', $row->rc_timestamp);
+ $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rc_timestamp));
break;
}
if (is_null($resultPageSet)) {
- $vals = $this->addRowInfo('rc', $row);
- if($vals)
+ $vals = $this->extractRowInfo($row);
+ if ($vals)
$data[] = $vals;
} else {
- $title = Title :: makeTitle($row->rc_namespace, $row->rc_title);
- // skip any pages that user has no rights to read
- if ($title->userCanRead()) {
- if ($allrev) {
- $data[] = intval($row->rc_this_oldid);
- } else {
- $data[] = intval($row->rc_cur_id);
- }
+ if ($allrev) {
+ $data[] = intval($row->rc_this_oldid);
+ } else {
+ $data[] = intval($row->rc_cur_id);
}
}
}
@@ -165,6 +178,50 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
}
}
+ private function extractRowInfo($row) {
+
+ $vals = array ();
+
+ if ($this->fld_ids) {
+ $vals['pageid'] = intval($row->rc_cur_id);
+ $vals['revid'] = intval($row->rc_this_oldid);
+ }
+
+ if ($this->fld_title)
+ ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->rc_namespace, $row->rc_title));
+
+ if ($this->fld_user) {
+ $vals['user'] = $row->rc_user_text;
+ if (!$row->rc_user)
+ $vals['anon'] = '';
+ }
+
+ if ($this->fld_flags) {
+ if ($row->rc_new)
+ $vals['new'] = '';
+ if ($row->rc_minor)
+ $vals['minor'] = '';
+ }
+
+ if ($this->fld_patrol && isset($row->rc_patrolled))
+ $vals['patrolled'] = '';
+
+ if ($this->fld_timestamp)
+ $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rc_timestamp);
+
+ $this->addFieldsIf('rc_new_len', $this->fld_sizes);
+
+ if ($this->fld_sizes) {
+ $vals['oldlen'] = intval($row->rc_old_len);
+ $vals['newlen'] = intval($row->rc_new_len);
+ }
+
+ if ($this->fld_comment && !empty ($row->rc_comment))
+ $vals['comment'] = $row->rc_comment;
+
+ return $vals;
+ }
+
protected function getAllowedParams() {
return array (
'allrev' => false,
@@ -189,16 +246,21 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
ApiBase :: PARAM_DFLT => 10,
ApiBase :: PARAM_TYPE => 'limit',
ApiBase :: PARAM_MIN => 1,
- ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1,
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
),
'prop' => array (
APIBase :: PARAM_ISMULTI => true,
+ APIBase :: PARAM_DFLT => 'ids|title|flags',
APIBase :: PARAM_TYPE => array (
+ 'ids',
+ 'title',
+ 'flags',
'user',
'comment',
'timestamp',
- 'patrol'
+ 'patrol',
+ 'sizes',
)
)
);
@@ -223,14 +285,15 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
protected function getExamples() {
return array (
'api.php?action=query&list=watchlist',
- 'api.php?action=query&list=watchlist&wlallrev',
+ 'api.php?action=query&list=watchlist&wlprop=ids|title|timestamp|user|comment',
+ 'api.php?action=query&list=watchlist&wlallrev&wlprop=ids|title|timestamp|user|comment',
'api.php?action=query&generator=watchlist&prop=info',
'api.php?action=query&generator=watchlist&gwlallrev&prop=revisions&rvprop=timestamp|user'
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryWatchlist.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiQueryWatchlist.php 24092 2007-07-14 19:04:31Z yurik $';
}
}
-?>
+
diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php
index 79fd34a1..a318d808 100644
--- a/includes/api/ApiResult.php
+++ b/includes/api/ApiResult.php
@@ -5,7 +5,7 @@
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <FirstnameLastname@gmail.com>
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,20 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * This class represents the result of the API operations.
+ * It simply wraps a nested array() structure, adding some functions to simplify array's modifications.
+ * As various modules execute, they add different pieces of information to this result,
+ * structuring it as it will be given to the client.
+ *
+ * Each subarray may either be a dictionary - key-value pairs with unique keys,
+ * or lists, where the items are added using $data[] = $value notation.
+ *
+ * There are two special key values that change how XML output is generated:
+ * '_element' This key sets the tag name for the rest of the elements in the current array.
+ * It is only inserted if the formatter returned true for getNeedsRawData()
+ * '*' This key has special meaning only to the XML formatter, and is outputed as is
+ * for all others. In XML it becomes the content of the current element.
+ *
* @addtogroup API
*/
class ApiResult extends ApiBase {
@@ -44,6 +58,9 @@ class ApiResult extends ApiBase {
$this->reset();
}
+ /**
+ * Clear the current result data.
+ */
public function reset() {
$this->mData = array ();
}
@@ -56,10 +73,16 @@ class ApiResult extends ApiBase {
$this->mIsRawMode = true;
}
+ /**
+ * Returns true if the result is being created for the formatter that requested raw data.
+ */
public function getIsRawMode() {
return $this->mIsRawMode;
}
+ /**
+ * Get result's internal data array
+ */
public function & getData() {
return $this->mData;
}
@@ -103,11 +126,6 @@ class ApiResult extends ApiBase {
}
}
- // public static function makeContentElement($tag, $value) {
- // $result = array();
- // ApiResult::setContent($result, )
- // }
- //
/**
* In case the array contains indexed values (in addition to named),
* all indexed values will have the given tag name.
@@ -125,7 +143,8 @@ class ApiResult extends ApiBase {
/**
* Add value to the output data at the given path.
* Path is an indexed array, each element specifing the branch at which to add the new value
- * Setting $path to array('a','b','c') is equivalent to data['a']['b']['c'] = $value
+ * Setting $path to array('a','b','c') is equivalent to data['a']['b']['c'] = $value
+ * If $name is empty, the $value is added as a next list element data[] = $value
*/
public function addValue($path, $name, $value) {
@@ -145,7 +164,10 @@ class ApiResult extends ApiBase {
}
}
- ApiResult :: setElement($data, $name, $value);
+ if (empty($name))
+ $data[] = $value; // Add list element
+ else
+ ApiResult :: setElement($data, $name, $value); // Add named element
}
public function execute() {
@@ -153,7 +175,7 @@ class ApiResult extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiResult.php 21402 2007-04-20 08:55:14Z nickj $';
+ return __CLASS__ . ': $Id: ApiResult.php 23531 2007-06-29 01:19:14Z simetrical $';
}
}
-?>
+
diff --git a/includes/cbt/CBTCompiler.php b/includes/cbt/CBTCompiler.php
index 59088bed..17f43500 100644
--- a/includes/cbt/CBTCompiler.php
+++ b/includes/cbt/CBTCompiler.php
@@ -364,4 +364,4 @@ class CBTCompiler {
';
}
}
-?>
+
diff --git a/includes/cbt/CBTProcessor.php b/includes/cbt/CBTProcessor.php
index 0c34204e..31d1b60a 100644
--- a/includes/cbt/CBTProcessor.php
+++ b/includes/cbt/CBTProcessor.php
@@ -537,4 +537,4 @@ class CBTProcessor {
return new CBTValue( htmlspecialchars( $val->getText() ), $val->getDeps() );
}
}
-?>
+
diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php
new file mode 100644
index 00000000..bd9ff633
--- /dev/null
+++ b/includes/filerepo/ArchivedFile.php
@@ -0,0 +1,108 @@
+<?php
+
+/**
+ * @addtogroup Media
+ */
+class ArchivedFile
+{
+ /**
+ * Returns a file object from the filearchive table
+ * @param $title, the corresponding image page title
+ * @param $id, the image id, a unique key
+ * @param $key, optional storage key
+ * @return ResultWrapper
+ */
+ function ArchivedFile( $title, $id=0, $key='' ) {
+ if( !is_object( $title ) ) {
+ throw new MWException( 'ArchivedFile constructor given bogus title.' );
+ }
+ $conds = ($id) ? "fa_id = $id" : "fa_storage_key = '$key'";
+ if( $title->getNamespace() == NS_IMAGE ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'filearchive',
+ array(
+ 'fa_id',
+ 'fa_name',
+ 'fa_storage_key',
+ 'fa_storage_group',
+ 'fa_size',
+ 'fa_bits',
+ 'fa_width',
+ 'fa_height',
+ 'fa_metadata',
+ 'fa_media_type',
+ 'fa_major_mime',
+ 'fa_minor_mime',
+ 'fa_description',
+ 'fa_user',
+ 'fa_user_text',
+ 'fa_timestamp',
+ 'fa_deleted' ),
+ array(
+ 'fa_name' => $title->getDbKey(),
+ $conds ),
+ __METHOD__,
+ array( 'ORDER BY' => 'fa_timestamp DESC' ) );
+
+ if ( $dbr->numRows( $res ) == 0 ) {
+ // this revision does not exist?
+ return;
+ }
+ $ret = $dbr->resultObject( $res );
+ $row = $ret->fetchObject();
+
+ // initialize fields for filestore image object
+ $this->mId = intval($row->fa_id);
+ $this->mName = $row->fa_name;
+ $this->mGroup = $row->fa_storage_group;
+ $this->mKey = $row->fa_storage_key;
+ $this->mSize = $row->fa_size;
+ $this->mBits = $row->fa_bits;
+ $this->mWidth = $row->fa_width;
+ $this->mHeight = $row->fa_height;
+ $this->mMetaData = $row->fa_metadata;
+ $this->mMime = "$row->fa_major_mime/$row->fa_minor_mime";
+ $this->mType = $row->fa_media_type;
+ $this->mDescription = $row->fa_description;
+ $this->mUser = $row->fa_user;
+ $this->mUserText = $row->fa_user_text;
+ $this->mTimestamp = $row->fa_timestamp;
+ $this->mDeleted = $row->fa_deleted;
+ } else {
+ throw new MWException( 'This title does not correspond to an image page.' );
+ return;
+ }
+ return true;
+ }
+
+ /**
+ * int $field one of DELETED_* bitfield constants
+ * for file or revision rows
+ * @return bool
+ */
+ function isDeleted( $field ) {
+ return ($this->mDeleted & $field) == $field;
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this FileStore image file, if it's marked as deleted.
+ * @param int $field
+ * @return bool
+ */
+ function userCan( $field ) {
+ if( isset($this->mDeleted) && ($this->mDeleted & $field) == $field ) {
+ // images
+ global $wgUser;
+ $permission = ( $this->mDeleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED
+ ? 'hiderevision'
+ : 'deleterevision';
+ wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
+ return $wgUser->isAllowed( $permission );
+ } else {
+ return true;
+ }
+ }
+}
+
+
diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php
new file mode 100644
index 00000000..84ec9a27
--- /dev/null
+++ b/includes/filerepo/FSRepo.php
@@ -0,0 +1,530 @@
+<?php
+
+/**
+ * A repository for files accessible via the local filesystem. Does not support
+ * database access or registration.
+ */
+
+class FSRepo extends FileRepo {
+ var $directory, $deletedDir, $url, $hashLevels, $deletedHashLevels;
+ var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
+ var $oldFileFactory = false;
+ var $pathDisclosureProtection = 'simple';
+
+ function __construct( $info ) {
+ parent::__construct( $info );
+
+ // Required settings
+ $this->directory = $info['directory'];
+ $this->url = $info['url'];
+
+ // Optional settings
+ $this->hashLevels = isset( $info['hashLevels'] ) ? $info['hashLevels'] : 2;
+ $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ?
+ $info['deletedHashLevels'] : $this->hashLevels;
+ $this->deletedDir = isset( $info['deletedDir'] ) ? $info['deletedDir'] : false;
+ }
+
+ /**
+ * Get the public root directory of the repository.
+ */
+ function getRootDirectory() {
+ return $this->directory;
+ }
+
+ /**
+ * Get the public root URL of the repository
+ */
+ function getRootUrl() {
+ return $this->url;
+ }
+
+ /**
+ * Returns true if the repository uses a multi-level directory structure
+ */
+ function isHashed() {
+ return (bool)$this->hashLevels;
+ }
+
+ /**
+ * Get the local directory corresponding to one of the three basic zones
+ */
+ function getZonePath( $zone ) {
+ switch ( $zone ) {
+ case 'public':
+ return $this->directory;
+ case 'temp':
+ return "{$this->directory}/temp";
+ case 'deleted':
+ return $this->deletedDir;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Get the URL corresponding to one of the three basic zones
+ */
+ function getZoneUrl( $zone ) {
+ switch ( $zone ) {
+ case 'public':
+ return $this->url;
+ case 'temp':
+ return "{$this->url}/temp";
+ case 'deleted':
+ return false; // no public URL
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Get a URL referring to this repository, with the private mwrepo protocol.
+ * The suffix, if supplied, is considered to be unencoded, and will be
+ * URL-encoded before being returned.
+ */
+ function getVirtualUrl( $suffix = false ) {
+ $path = 'mwrepo://' . $this->name;
+ if ( $suffix !== false ) {
+ $path .= '/' . rawurlencode( $suffix );
+ }
+ return $path;
+ }
+
+ /**
+ * Get the local path corresponding to a virtual URL
+ */
+ function resolveVirtualUrl( $url ) {
+ if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
+ throw new MWException( __METHOD__.': unknown protoocl' );
+ }
+
+ $bits = explode( '/', substr( $url, 9 ), 3 );
+ if ( count( $bits ) != 3 ) {
+ throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
+ }
+ list( $repo, $zone, $rel ) = $bits;
+ if ( $repo !== $this->name ) {
+ throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
+ }
+ $base = $this->getZonePath( $zone );
+ if ( !$base ) {
+ throw new MWException( __METHOD__.": invalid zone: $zone" );
+ }
+ return $base . '/' . rawurldecode( $rel );
+ }
+
+ /**
+ * Store a batch of files
+ *
+ * @param array $triplets (src,zone,dest) triplets as per store()
+ * @param integer $flags Bitwise combination of the following flags:
+ * self::DELETE_SOURCE Delete the source file after upload
+ * self::OVERWRITE Overwrite an existing destination file instead of failing
+ * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
+ * same contents as the source
+ */
+ function storeBatch( $triplets, $flags = 0 ) {
+ if ( !is_writable( $this->directory ) ) {
+ return $this->newFatal( 'upload_directory_read_only', $this->directory );
+ }
+ $status = $this->newGood();
+ foreach ( $triplets as $i => $triplet ) {
+ list( $srcPath, $dstZone, $dstRel ) = $triplet;
+
+ $root = $this->getZonePath( $dstZone );
+ if ( !$root ) {
+ throw new MWException( "Invalid zone: $dstZone" );
+ }
+ if ( !$this->validateFilename( $dstRel ) ) {
+ throw new MWException( 'Validation error in $dstRel' );
+ }
+ $dstPath = "$root/$dstRel";
+ $dstDir = dirname( $dstPath );
+
+ if ( !is_dir( $dstDir ) ) {
+ if ( !wfMkdirParents( $dstDir ) ) {
+ return $this->newFatal( 'directorycreateerror', $dstDir );
+ }
+ // In the deleted zone, seed new directories with a blank
+ // index.html, to prevent crawling
+ if ( $dstZone == 'deleted' ) {
+ file_put_contents( "$dstDir/index.html", '' );
+ }
+ }
+
+ if ( self::isVirtualUrl( $srcPath ) ) {
+ $srcPath = $triplets[$i][0] = $this->resolveVirtualUrl( $srcPath );
+ }
+ if ( !is_file( $srcPath ) ) {
+ // Make a list of files that don't exist for return to the caller
+ $status->fatal( 'filenotfound', $srcPath );
+ continue;
+ }
+ if ( !( $flags & self::OVERWRITE ) && file_exists( $dstPath ) ) {
+ if ( $flags & self::OVERWRITE_SAME ) {
+ $hashSource = sha1_file( $srcPath );
+ $hashDest = sha1_file( $dstPath );
+ if ( $hashSource != $hashDest ) {
+ $status->fatal( 'fileexistserror', $dstPath );
+ }
+ } else {
+ $status->fatal( 'fileexistserror', $dstPath );
+ }
+ }
+ }
+
+ $deleteDest = wfIsWindows() && ( $flags & self::OVERWRITE );
+
+ // Abort now on failure
+ if ( !$status->ok ) {
+ return $status;
+ }
+
+ foreach ( $triplets as $triplet ) {
+ list( $srcPath, $dstZone, $dstRel ) = $triplet;
+ $root = $this->getZonePath( $dstZone );
+ $dstPath = "$root/$dstRel";
+ $good = true;
+
+ if ( $flags & self::DELETE_SOURCE ) {
+ if ( $deleteDest ) {
+ unlink( $dstPath );
+ }
+ if ( !rename( $srcPath, $dstPath ) ) {
+ $status->error( 'filerenameerror', $srcPath, $dstPath );
+ $good = false;
+ }
+ } else {
+ if ( !copy( $srcPath, $dstPath ) ) {
+ $status->error( 'filecopyerror', $srcPath, $dstPath );
+ $good = false;
+ }
+ }
+ if ( $good ) {
+ chmod( $dstPath, 0644 );
+ $status->successCount++;
+ } else {
+ $status->failCount++;
+ }
+ }
+ return $status;
+ }
+
+ /**
+ * Pick a random name in the temp zone and store a file to it.
+ * @param string $originalName The base name of the file as specified
+ * by the user. The file extension will be maintained.
+ * @param string $srcPath The current location of the file.
+ * @return FileRepoStatus object with the URL in the value.
+ */
+ function storeTemp( $originalName, $srcPath ) {
+ $date = gmdate( "YmdHis" );
+ $hashPath = $this->getHashPath( $originalName );
+ $dstRel = "$hashPath$date!$originalName";
+ $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
+
+ $result = $this->store( $srcPath, 'temp', $dstRel );
+ $result->value = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
+ return $result;
+ }
+
+ /**
+ * Remove a temporary file or mark it for garbage collection
+ * @param string $virtualUrl The virtual URL returned by storeTemp
+ * @return boolean True on success, false on failure
+ */
+ function freeTemp( $virtualUrl ) {
+ $temp = "mwrepo://{$this->name}/temp";
+ if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
+ wfDebug( __METHOD__.": Invalid virtual URL\n" );
+ return false;
+ }
+ $path = $this->resolveVirtualUrl( $virtualUrl );
+ wfSuppressWarnings();
+ $success = unlink( $path );
+ wfRestoreWarnings();
+ return $success;
+ }
+
+ /**
+ * Publish a batch of files
+ * @param array $triplets (source,dest,archive) triplets as per publish()
+ * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
+ * that the source files should be deleted if possible
+ */
+ function publishBatch( $triplets, $flags = 0 ) {
+ // Perform initial checks
+ if ( !is_writable( $this->directory ) ) {
+ return $this->newFatal( 'upload_directory_read_only', $this->directory );
+ }
+ $status = $this->newGood( array() );
+ foreach ( $triplets as $i => $triplet ) {
+ list( $srcPath, $dstRel, $archiveRel ) = $triplet;
+
+ if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
+ $triplets[$i][0] = $srcPath = $this->resolveVirtualUrl( $srcPath );
+ }
+ if ( !$this->validateFilename( $dstRel ) ) {
+ throw new MWException( 'Validation error in $dstRel' );
+ }
+ if ( !$this->validateFilename( $archiveRel ) ) {
+ throw new MWException( 'Validation error in $archiveRel' );
+ }
+ $dstPath = "{$this->directory}/$dstRel";
+ $archivePath = "{$this->directory}/$archiveRel";
+
+ $dstDir = dirname( $dstPath );
+ $archiveDir = dirname( $archivePath );
+ // Abort immediately on directory creation errors since they're likely to be repetitive
+ if ( !is_dir( $dstDir ) && !wfMkdirParents( $dstDir ) ) {
+ return $this->newFatal( 'directorycreateerror', $dstDir );
+ }
+ if ( !is_dir( $archiveDir ) && !wfMkdirParents( $archiveDir ) ) {
+ return $this->newFatal( 'directorycreateerror', $archiveDir );
+ }
+ if ( !is_file( $srcPath ) ) {
+ // Make a list of files that don't exist for return to the caller
+ $status->fatal( 'filenotfound', $srcPath );
+ }
+ }
+
+ if ( !$status->ok ) {
+ return $status;
+ }
+
+ foreach ( $triplets as $i => $triplet ) {
+ list( $srcPath, $dstRel, $archiveRel ) = $triplet;
+ $dstPath = "{$this->directory}/$dstRel";
+ $archivePath = "{$this->directory}/$archiveRel";
+
+ // Archive destination file if it exists
+ if( is_file( $dstPath ) ) {
+ // Check if the archive file exists
+ // This is a sanity check to avoid data loss. In UNIX, the rename primitive
+ // unlinks the destination file if it exists. DB-based synchronisation in
+ // publishBatch's caller should prevent races. In Windows there's no
+ // problem because the rename primitive fails if the destination exists.
+ if ( is_file( $archivePath ) ) {
+ $success = false;
+ } else {
+ wfSuppressWarnings();
+ $success = rename( $dstPath, $archivePath );
+ wfRestoreWarnings();
+ }
+
+ if( !$success ) {
+ $status->error( 'filerenameerror',$dstPath, $archivePath );
+ $status->failCount++;
+ continue;
+ } else {
+ wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n");
+ }
+ $status->value[$i] = 'archived';
+ } else {
+ $status->value[$i] = 'new';
+ }
+
+ $good = true;
+ wfSuppressWarnings();
+ if ( $flags & self::DELETE_SOURCE ) {
+ if ( !rename( $srcPath, $dstPath ) ) {
+ $status->error( 'filerenameerror', $srcPath, $dstPath );
+ $good = false;
+ }
+ } else {
+ if ( !copy( $srcPath, $dstPath ) ) {
+ $status->error( 'filecopyerror', $srcPath, $dstPath );
+ $good = false;
+ }
+ }
+ wfRestoreWarnings();
+
+ if ( $good ) {
+ $status->successCount++;
+ wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
+ // Thread-safe override for umask
+ chmod( $dstPath, 0644 );
+ } else {
+ $status->failCount++;
+ }
+ }
+ return $status;
+ }
+
+ /**
+ * Move a group of files to the deletion archive.
+ * If no valid deletion archive is configured, this may either delete the
+ * file or throw an exception, depending on the preference of the repository.
+ *
+ * @param array $sourceDestPairs Array of source/destination pairs. Each element
+ * is a two-element array containing the source file path relative to the
+ * public root in the first element, and the archive file path relative
+ * to the deleted zone root in the second element.
+ * @return FileRepoStatus
+ */
+ function deleteBatch( $sourceDestPairs ) {
+ $status = $this->newGood();
+ if ( !$this->deletedDir ) {
+ throw new MWException( __METHOD__.': no valid deletion archive directory' );
+ }
+
+ /**
+ * Validate filenames and create archive directories
+ */
+ foreach ( $sourceDestPairs as $pair ) {
+ list( $srcRel, $archiveRel ) = $pair;
+ if ( !$this->validateFilename( $srcRel ) ) {
+ throw new MWException( __METHOD__.':Validation error in $srcRel' );
+ }
+ if ( !$this->validateFilename( $archiveRel ) ) {
+ throw new MWException( __METHOD__.':Validation error in $archiveRel' );
+ }
+ $archivePath = "{$this->deletedDir}/$archiveRel";
+ $archiveDir = dirname( $archivePath );
+ if ( !is_dir( $archiveDir ) ) {
+ if ( !wfMkdirParents( $archiveDir ) ) {
+ $status->fatal( 'directorycreateerror', $archiveDir );
+ continue;
+ }
+ // Seed new directories with a blank index.html, to prevent crawling
+ file_put_contents( "$archiveDir/index.html", '' );
+ }
+ // Check if the archive directory is writable
+ // This doesn't appear to work on NTFS
+ if ( !is_writable( $archiveDir ) ) {
+ $status->fatal( 'filedelete-archive-read-only', $archiveDir );
+ }
+ }
+ if ( !$status->ok ) {
+ // Abort early
+ return $status;
+ }
+
+ /**
+ * Move the files
+ * We're now committed to returning an OK result, which will lead to
+ * the files being moved in the DB also.
+ */
+ foreach ( $sourceDestPairs as $pair ) {
+ list( $srcRel, $archiveRel ) = $pair;
+ $srcPath = "{$this->directory}/$srcRel";
+ $archivePath = "{$this->deletedDir}/$archiveRel";
+ $good = true;
+ if ( file_exists( $archivePath ) ) {
+ # A file with this content hash is already archived
+ if ( !@unlink( $srcPath ) ) {
+ $status->error( 'filedeleteerror', $srcPath );
+ $good = false;
+ }
+ } else{
+ if ( !@rename( $srcPath, $archivePath ) ) {
+ $status->error( 'filerenameerror', $srcPath, $archivePath );
+ $good = false;
+ } else {
+ chmod( $archivePath, 0644 );
+ }
+ }
+ if ( $good ) {
+ $status->successCount++;
+ } else {
+ $status->failCount++;
+ }
+ }
+ return $status;
+ }
+
+ /**
+ * Get a relative path including trailing slash, e.g. f/fa/
+ * If the repo is not hashed, returns an empty string
+ */
+ function getHashPath( $name ) {
+ return FileRepo::getHashPathForLevel( $name, $this->hashLevels );
+ }
+
+ /**
+ * Get a relative path for a deletion archive key,
+ * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg
+ */
+ function getDeletedHashPath( $key ) {
+ $path = '';
+ for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
+ $path .= $key[$i] . '/';
+ }
+ return $path;
+ }
+
+ /**
+ * Call a callback function for every file in the repository.
+ * Uses the filesystem even in child classes.
+ */
+ function enumFilesInFS( $callback ) {
+ $numDirs = 1 << ( $this->hashLevels * 4 );
+ for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) {
+ $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
+ $path = $this->directory;
+ for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) {
+ $path .= '/' . substr( $hexString, 0, $hexPos + 1 );
+ }
+ if ( !file_exists( $path ) || !is_dir( $path ) ) {
+ continue;
+ }
+ $dir = opendir( $path );
+ while ( false !== ( $name = readdir( $dir ) ) ) {
+ call_user_func( $callback, $path . '/' . $name );
+ }
+ }
+ }
+
+ /**
+ * Call a callback function for every file in the repository
+ * May use either the database or the filesystem
+ */
+ function enumFiles( $callback ) {
+ $this->enumFilesInFS( $callback );
+ }
+
+ /**
+ * Get properties of a file with a given virtual URL
+ * The virtual URL must refer to this repo
+ */
+ function getFileProps( $virtualUrl ) {
+ $path = $this->resolveVirtualUrl( $virtualUrl );
+ return File::getPropsFromPath( $path );
+ }
+
+ /**
+ * Path disclosure protection functions
+ *
+ * Get a callback function to use for cleaning error message parameters
+ */
+ function getErrorCleanupFunction() {
+ switch ( $this->pathDisclosureProtection ) {
+ case 'simple':
+ $callback = array( $this, 'simpleClean' );
+ break;
+ default:
+ $callback = parent::getErrorCleanupFunction();
+ }
+ return $callback;
+ }
+
+ function simpleClean( $param ) {
+ if ( !isset( $this->simpleCleanPairs ) ) {
+ global $IP;
+ $this->simpleCleanPairs = array(
+ $this->directory => 'public',
+ "{$this->directory}/temp" => 'temp',
+ $IP => '$IP',
+ dirname( __FILE__ ) => '$IP/extensions/WebStore',
+ );
+ if ( $this->deletedDir ) {
+ $this->simpleCleanPairs[$this->deletedDir] = 'deleted';
+ }
+ }
+ return strtr( $param, $this->simpleCleanPairs );
+ }
+
+}
+
+
diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php
new file mode 100644
index 00000000..21b7a865
--- /dev/null
+++ b/includes/filerepo/File.php
@@ -0,0 +1,1133 @@
+<?php
+
+/**
+ * Implements some public methods and some protected utility functions which
+ * are required by multiple child classes. Contains stub functionality for
+ * unimplemented public methods.
+ *
+ * Stub functions which should be overridden are marked with STUB. Some more
+ * concrete functions are also typically overridden by child classes.
+ *
+ * Note that only the repo object knows what its file class is called. You should
+ * never name a file class explictly outside of the repo class. Instead use the
+ * repo's factory functions to generate file objects, for example:
+ *
+ * RepoGroup::singleton()->getLocalRepo()->newFile($title);
+ *
+ * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
+ * in most cases.
+ *
+ * @addtogroup FileRepo
+ */
+abstract class File {
+ const DELETED_FILE = 1;
+ const DELETED_COMMENT = 2;
+ const DELETED_USER = 4;
+ const DELETED_RESTRICTED = 8;
+ const RENDER_NOW = 1;
+
+ const DELETE_SOURCE = 1;
+
+ /**
+ * Some member variables can be lazy-initialised using __get(). The
+ * initialisation function for these variables is always a function named
+ * like getVar(), where Var is the variable name with upper-case first
+ * letter.
+ *
+ * The following variables are initialised in this way in this base class:
+ * name, extension, handler, path, canRender, isSafeFile,
+ * transformScript, hashPath, pageCount, url
+ *
+ * Code within this class should generally use the accessor function
+ * directly, since __get() isn't re-entrant and therefore causes bugs that
+ * depend on initialisation order.
+ */
+
+ /**
+ * The following member variables are not lazy-initialised
+ */
+ var $repo, $title, $lastError;
+
+ /**
+ * Call this constructor from child classes
+ */
+ function __construct( $title, $repo ) {
+ $this->title = $title;
+ $this->repo = $repo;
+ }
+
+ function __get( $name ) {
+ $function = array( $this, 'get' . ucfirst( $name ) );
+ if ( !is_callable( $function ) ) {
+ return null;
+ } else {
+ $this->$name = call_user_func( $function );
+ return $this->$name;
+ }
+ }
+
+ /**
+ * Normalize a file extension to the common form, and ensure it's clean.
+ * Extensions with non-alphanumeric characters will be discarded.
+ *
+ * @param $ext string (without the .)
+ * @return string
+ */
+ static function normalizeExtension( $ext ) {
+ $lower = strtolower( $ext );
+ $squish = array(
+ 'htm' => 'html',
+ 'jpeg' => 'jpg',
+ 'mpeg' => 'mpg',
+ 'tiff' => 'tif' );
+ if( isset( $squish[$lower] ) ) {
+ return $squish[$lower];
+ } elseif( preg_match( '/^[0-9a-z]+$/', $lower ) ) {
+ return $lower;
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Upgrade the database row if there is one
+ * Called by ImagePage
+ * STUB
+ */
+ function upgradeRow() {}
+
+ /**
+ * Split an internet media type into its two components; if not
+ * a two-part name, set the minor type to 'unknown'.
+ *
+ * @param $mime "text/html" etc
+ * @return array ("text", "html") etc
+ */
+ static function splitMime( $mime ) {
+ if( strpos( $mime, '/' ) !== false ) {
+ return explode( '/', $mime, 2 );
+ } else {
+ return array( $mime, 'unknown' );
+ }
+ }
+
+ /**
+ * Return the name of this file
+ */
+ public function getName() {
+ if ( !isset( $this->name ) ) {
+ $this->name = $this->repo->getNameFromTitle( $this->title );
+ }
+ return $this->name;
+ }
+
+ /**
+ * Get the file extension, e.g. "svg"
+ */
+ function getExtension() {
+ if ( !isset( $this->extension ) ) {
+ $n = strrpos( $this->getName(), '.' );
+ $this->extension = self::normalizeExtension(
+ $n ? substr( $this->getName(), $n + 1 ) : '' );
+ }
+ return $this->extension;
+ }
+
+ /**
+ * Return the associated title object
+ * @public
+ */
+ function getTitle() { return $this->title; }
+
+ /**
+ * Return the URL of the file
+ * @public
+ */
+ function getUrl() {
+ if ( !isset( $this->url ) ) {
+ $this->url = $this->repo->getZoneUrl( 'public' ) . '/' . $this->getUrlRel();
+ }
+ return $this->url;
+ }
+
+ function getViewURL() {
+ if( $this->mustRender()) {
+ if( $this->canRender() ) {
+ return $this->createThumb( $this->getWidth() );
+ }
+ else {
+ wfDebug(__METHOD__.': supposed to render '.$this->getName().' ('.$this->getMimeType()."), but can't!\n");
+ return $this->getURL(); #hm... return NULL?
+ }
+ } else {
+ return $this->getURL();
+ }
+ }
+
+ /**
+ * Return the full filesystem path to the file. Note that this does
+ * not mean that a file actually exists under that location.
+ *
+ * This path depends on whether directory hashing is active or not,
+ * i.e. whether the files are all found in the same directory,
+ * or in hashed paths like /images/3/3c.
+ *
+ * May return false if the file is not locally accessible.
+ *
+ * @public
+ */
+ function getPath() {
+ if ( !isset( $this->path ) ) {
+ $this->path = $this->repo->getZonePath('public') . '/' . $this->getRel();
+ }
+ return $this->path;
+ }
+
+ /**
+ * Alias for getPath()
+ * @public
+ */
+ function getFullPath() {
+ return $this->getPath();
+ }
+
+ /**
+ * Return the width of the image. Returns false if the width is unknown
+ * or undefined.
+ *
+ * STUB
+ * Overridden by LocalFile, UnregisteredLocalFile
+ */
+ public function getWidth( $page = 1 ) { return false; }
+
+ /**
+ * Return the height of the image. Returns false if the height is unknown
+ * or undefined
+ *
+ * STUB
+ * Overridden by LocalFile, UnregisteredLocalFile
+ */
+ public function getHeight( $page = 1 ) { return false; }
+
+ /**
+ * Get the duration of a media file in seconds
+ */
+ public function getLength() {
+ $handler = $this->getHandler();
+ if ( $handler ) {
+ return $handler->getLength( $this );
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Get handler-specific metadata
+ * Overridden by LocalFile, UnregisteredLocalFile
+ * STUB
+ */
+ function getMetadata() { return false; }
+
+ /**
+ * Return the size of the image file, in bytes
+ * Overridden by LocalFile, UnregisteredLocalFile
+ * STUB
+ */
+ public function getSize() { return false; }
+
+ /**
+ * Returns the mime type of the file.
+ * Overridden by LocalFile, UnregisteredLocalFile
+ * STUB
+ */
+ function getMimeType() { return 'unknown/unknown'; }
+
+ /**
+ * Return the type of the media in the file.
+ * Use the value returned by this function with the MEDIATYPE_xxx constants.
+ * Overridden by LocalFile,
+ * STUB
+ */
+ function getMediaType() { return MEDIATYPE_UNKNOWN; }
+
+ /**
+ * Checks if the output of transform() for this file is likely
+ * to be valid. If this is false, various user elements will
+ * display a placeholder instead.
+ *
+ * Currently, this checks if the file is an image format
+ * that can be converted to a format
+ * supported by all browsers (namely GIF, PNG and JPEG),
+ * or if it is an SVG image and SVG conversion is enabled.
+ */
+ function canRender() {
+ if ( !isset( $this->canRender ) ) {
+ $this->canRender = $this->getHandler() && $this->handler->canRender( $this );
+ }
+ return $this->canRender;
+ }
+
+ /**
+ * Accessor for __get()
+ */
+ protected function getCanRender() {
+ return $this->canRender();
+ }
+
+ /**
+ * Return true if the file is of a type that can't be directly
+ * rendered by typical browsers and needs to be re-rasterized.
+ *
+ * This returns true for everything but the bitmap types
+ * supported by all browsers, i.e. JPEG; GIF and PNG. It will
+ * also return true for any non-image formats.
+ *
+ * @return bool
+ */
+ function mustRender() {
+ return $this->getHandler() && $this->handler->mustRender( $this );
+ }
+
+ /**
+ * Alias for canRender()
+ */
+ function allowInlineDisplay() {
+ return $this->canRender();
+ }
+
+ /**
+ * Determines if this media file is in a format that is unlikely to
+ * contain viruses or malicious content. It uses the global
+ * $wgTrustedMediaFormats list to determine if the file is safe.
+ *
+ * This is used to show a warning on the description page of non-safe files.
+ * It may also be used to disallow direct [[media:...]] links to such files.
+ *
+ * Note that this function will always return true if allowInlineDisplay()
+ * or isTrustedFile() is true for this file.
+ */
+ function isSafeFile() {
+ if ( !isset( $this->isSafeFile ) ) {
+ $this->isSafeFile = $this->_getIsSafeFile();
+ }
+ return $this->isSafeFile;
+ }
+
+ /** Accessor for __get() */
+ protected function getIsSafeFile() {
+ return $this->isSafeFile();
+ }
+
+ /** Uncached accessor */
+ protected function _getIsSafeFile() {
+ if ($this->allowInlineDisplay()) return true;
+ if ($this->isTrustedFile()) return true;
+
+ global $wgTrustedMediaFormats;
+
+ $type= $this->getMediaType();
+ $mime= $this->getMimeType();
+ #wfDebug("LocalFile::isSafeFile: type= $type, mime= $mime\n");
+
+ if (!$type || $type===MEDIATYPE_UNKNOWN) return false; #unknown type, not trusted
+ if ( in_array( $type, $wgTrustedMediaFormats) ) return true;
+
+ if ($mime==="unknown/unknown") return false; #unknown type, not trusted
+ if ( in_array( $mime, $wgTrustedMediaFormats) ) return true;
+
+ return false;
+ }
+
+ /** Returns true if the file is flagged as trusted. Files flagged that way
+ * can be linked to directly, even if that is not allowed for this type of
+ * file normally.
+ *
+ * This is a dummy function right now and always returns false. It could be
+ * implemented to extract a flag from the database. The trusted flag could be
+ * set on upload, if the user has sufficient privileges, to bypass script-
+ * and html-filters. It may even be coupled with cryptographics signatures
+ * or such.
+ */
+ function isTrustedFile() {
+ #this could be implemented to check a flag in the databas,
+ #look for signatures, etc
+ return false;
+ }
+
+ /**
+ * Returns true if file exists in the repository.
+ *
+ * Overridden by LocalFile to avoid unnecessary stat calls.
+ *
+ * @return boolean Whether file exists in the repository.
+ */
+ public function exists() {
+ return $this->getPath() && file_exists( $this->path );
+ }
+
+ function getTransformScript() {
+ if ( !isset( $this->transformScript ) ) {
+ $this->transformScript = false;
+ if ( $this->repo ) {
+ $script = $this->repo->getThumbScriptUrl();
+ if ( $script ) {
+ $this->transformScript = "$script?f=" . urlencode( $this->getName() );
+ }
+ }
+ }
+ return $this->transformScript;
+ }
+
+ /**
+ * Get a ThumbnailImage which is the same size as the source
+ */
+ function getUnscaledThumb( $page = false ) {
+ $width = $this->getWidth( $page );
+ if ( !$width ) {
+ return $this->iconThumb();
+ }
+ if ( $page ) {
+ $params = array(
+ 'page' => $page,
+ 'width' => $this->getWidth( $page )
+ );
+ } else {
+ $params = array( 'width' => $this->getWidth() );
+ }
+ return $this->transform( $params );
+ }
+
+ /**
+ * Return the file name of a thumbnail with the specified parameters
+ *
+ * @param array $params Handler-specific parameters
+ * @private -ish
+ */
+ function thumbName( $params ) {
+ if ( !$this->getHandler() ) {
+ return null;
+ }
+ $extension = $this->getExtension();
+ list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType() );
+ $thumbName = $this->handler->makeParamString( $params ) . '-' . $this->getName();
+ if ( $thumbExt != $extension ) {
+ $thumbName .= ".$thumbExt";
+ }
+ return $thumbName;
+ }
+
+ /**
+ * Create a thumbnail of the image having the specified width/height.
+ * The thumbnail will not be created if the width is larger than the
+ * image's width. Let the browser do the scaling in this case.
+ * The thumbnail is stored on disk and is only computed if the thumbnail
+ * file does not exist OR if it is older than the image.
+ * Returns the URL.
+ *
+ * Keeps aspect ratio of original image. If both width and height are
+ * specified, the generated image will be no bigger than width x height,
+ * and will also have correct aspect ratio.
+ *
+ * @param integer $width maximum width of the generated thumbnail
+ * @param integer $height maximum height of the image (optional)
+ */
+ public function createThumb( $width, $height = -1 ) {
+ $params = array( 'width' => $width );
+ if ( $height != -1 ) {
+ $params['height'] = $height;
+ }
+ $thumb = $this->transform( $params );
+ if( is_null( $thumb ) || $thumb->isError() ) return '';
+ return $thumb->getUrl();
+ }
+
+ /**
+ * As createThumb, but returns a ThumbnailImage object. This can
+ * provide access to the actual file, the real size of the thumb,
+ * and can produce a convenient <img> tag for you.
+ *
+ * For non-image formats, this may return a filetype-specific icon.
+ *
+ * @param integer $width maximum width of the generated thumbnail
+ * @param integer $height maximum height of the image (optional)
+ * @param boolean $render True to render the thumbnail if it doesn't exist,
+ * false to just return the URL
+ *
+ * @return ThumbnailImage or null on failure
+ *
+ * @deprecated use transform()
+ */
+ public function getThumbnail( $width, $height=-1, $render = true ) {
+ $params = array( 'width' => $width );
+ if ( $height != -1 ) {
+ $params['height'] = $height;
+ }
+ $flags = $render ? self::RENDER_NOW : 0;
+ return $this->transform( $params, $flags );
+ }
+
+ /**
+ * Transform a media file
+ *
+ * @param array $params An associative array of handler-specific parameters. Typical
+ * keys are width, height and page.
+ * @param integer $flags A bitfield, may contain self::RENDER_NOW to force rendering
+ * @return MediaTransformOutput
+ */
+ function transform( $params, $flags = 0 ) {
+ global $wgUseSquid, $wgIgnoreImageErrors;
+
+ wfProfileIn( __METHOD__ );
+ do {
+ if ( !$this->canRender() ) {
+ // not a bitmap or renderable image, don't try.
+ $thumb = $this->iconThumb();
+ break;
+ }
+
+ $script = $this->getTransformScript();
+ if ( $script && !($flags & self::RENDER_NOW) ) {
+ // Use a script to transform on client request
+ $thumb = $this->handler->getScriptedTransform( $this, $script, $params );
+ break;
+ }
+
+ $normalisedParams = $params;
+ $this->handler->normaliseParams( $this, $normalisedParams );
+ $thumbName = $this->thumbName( $normalisedParams );
+ $thumbPath = $this->getThumbPath( $thumbName );
+ $thumbUrl = $this->getThumbUrl( $thumbName );
+
+ if ( $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) {
+ $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
+ break;
+ }
+
+ wfDebug( __METHOD__.": Doing stat for $thumbPath\n" );
+ $this->migrateThumbFile( $thumbName );
+ if ( file_exists( $thumbPath ) ) {
+ $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
+ break;
+ }
+ $thumb = $this->handler->doTransform( $this, $thumbPath, $thumbUrl, $params );
+
+ // Ignore errors if requested
+ if ( !$thumb ) {
+ $thumb = null;
+ } elseif ( $thumb->isError() ) {
+ $this->lastError = $thumb->toText();
+ if ( $wgIgnoreImageErrors && !($flags & self::RENDER_NOW) ) {
+ $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
+ }
+ }
+
+ if ( $wgUseSquid ) {
+ wfPurgeSquidServers( array( $thumbUrl ) );
+ }
+ } while (false);
+
+ wfProfileOut( __METHOD__ );
+ return $thumb;
+ }
+
+ /**
+ * Hook into transform() to allow migration of thumbnail files
+ * STUB
+ * Overridden by LocalFile
+ */
+ function migrateThumbFile( $thumbName ) {}
+
+ /**
+ * Get a MediaHandler instance for this file
+ */
+ function getHandler() {
+ if ( !isset( $this->handler ) ) {
+ $this->handler = MediaHandler::getHandler( $this->getMimeType() );
+ }
+ return $this->handler;
+ }
+
+ /**
+ * Get a ThumbnailImage representing a file type icon
+ * @return ThumbnailImage
+ */
+ function iconThumb() {
+ global $wgStylePath, $wgStyleDirectory;
+
+ $try = array( 'fileicon-' . $this->getExtension() . '.png', 'fileicon.png' );
+ foreach( $try as $icon ) {
+ $path = '/common/images/icons/' . $icon;
+ $filepath = $wgStyleDirectory . $path;
+ if( file_exists( $filepath ) ) {
+ return new ThumbnailImage( $this, $wgStylePath . $path, 120, 120 );
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get last thumbnailing error.
+ * Largely obsolete.
+ */
+ function getLastError() {
+ return $this->lastError;
+ }
+
+ /**
+ * Get all thumbnail names previously generated for this file
+ * STUB
+ * Overridden by LocalFile
+ */
+ function getThumbnails() { return array(); }
+
+ /**
+ * Purge shared caches such as thumbnails and DB data caching
+ * STUB
+ * Overridden by LocalFile
+ */
+ function purgeCache( $archiveFiles = array() ) {}
+
+ /**
+ * Purge the file description page, but don't go after
+ * pages using the file. Use when modifying file history
+ * but not the current data.
+ */
+ function purgeDescription() {
+ $title = $this->getTitle();
+ if ( $title ) {
+ $title->invalidateCache();
+ $title->purgeSquid();
+ }
+ }
+
+ /**
+ * Purge metadata and all affected pages when the file is created,
+ * deleted, or majorly updated.
+ */
+ function purgeEverything() {
+ // Delete thumbnails and refresh file metadata cache
+ $this->purgeCache();
+ $this->purgeDescription();
+
+ // Purge cache of all pages using this file
+ $title = $this->getTitle();
+ if ( $title ) {
+ $update = new HTMLCacheUpdate( $title, 'imagelinks' );
+ $update->doUpdate();
+ }
+ }
+
+ /**
+ * Return the history of this file, line by line. Starts with current version,
+ * then old versions. Should return an object similar to an image/oldimage
+ * database row.
+ *
+ * STUB
+ * Overridden in LocalFile
+ */
+ public function nextHistoryLine() {
+ return false;
+ }
+
+ /**
+ * Reset the history pointer to the first element of the history.
+ * Always call this function after using nextHistoryLine() to free db resources
+ * STUB
+ * Overridden in LocalFile.
+ */
+ public function resetHistory() {}
+
+ /**
+ * Get the filename hash component of the directory including trailing slash,
+ * e.g. f/fa/
+ * If the repository is not hashed, returns an empty string.
+ */
+ function getHashPath() {
+ if ( !isset( $this->hashPath ) ) {
+ $this->hashPath = $this->repo->getHashPath( $this->getName() );
+ }
+ return $this->hashPath;
+ }
+
+ /**
+ * Get the path of the file relative to the public zone root
+ */
+ function getRel() {
+ return $this->getHashPath() . $this->getName();
+ }
+
+ /**
+ * Get urlencoded relative path of the file
+ */
+ function getUrlRel() {
+ return $this->getHashPath() . rawurlencode( $this->getName() );
+ }
+
+ /** Get the relative path for an archive file */
+ function getArchiveRel( $suffix = false ) {
+ $path = 'archive/' . $this->getHashPath();
+ if ( $suffix === false ) {
+ $path = substr( $path, 0, -1 );
+ } else {
+ $path .= $suffix;
+ }
+ return $path;
+ }
+
+ /** Get relative path for a thumbnail file */
+ function getThumbRel( $suffix = false ) {
+ $path = 'thumb/' . $this->getRel();
+ if ( $suffix !== false ) {
+ $path .= '/' . $suffix;
+ }
+ return $path;
+ }
+
+ /** Get the path of the archive directory, or a particular file if $suffix is specified */
+ function getArchivePath( $suffix = false ) {
+ return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel();
+ }
+
+ /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */
+ function getThumbPath( $suffix = false ) {
+ return $this->repo->getZonePath('public') . '/' . $this->getThumbRel( $suffix );
+ }
+
+ /** Get the URL of the archive directory, or a particular file if $suffix is specified */
+ function getArchiveUrl( $suffix = false ) {
+ $path = $this->repo->getZoneUrl('public') . '/archive/' . $this->getHashPath();
+ if ( $suffix === false ) {
+ $path = substr( $path, 0, -1 );
+ } else {
+ $path .= rawurlencode( $suffix );
+ }
+ return $path;
+ }
+
+ /** Get the URL of the thumbnail directory, or a particular file if $suffix is specified */
+ function getThumbUrl( $suffix = false ) {
+ $path = $this->repo->getZoneUrl('public') . '/thumb/' . $this->getUrlRel();
+ if ( $suffix !== false ) {
+ $path .= '/' . rawurlencode( $suffix );
+ }
+ return $path;
+ }
+
+ /** Get the virtual URL for an archive file or directory */
+ function getArchiveVirtualUrl( $suffix = false ) {
+ $path = $this->repo->getVirtualUrl() . '/public/archive/' . $this->getHashPath();
+ if ( $suffix === false ) {
+ $path = substr( $path, 0, -1 );
+ } else {
+ $path .= rawurlencode( $suffix );
+ }
+ return $path;
+ }
+
+ /** Get the virtual URL for a thumbnail file or directory */
+ function getThumbVirtualUrl( $suffix = false ) {
+ $path = $this->repo->getVirtualUrl() . '/public/thumb/' . $this->getUrlRel();
+ if ( $suffix !== false ) {
+ $path .= '/' . rawurlencode( $suffix );
+ }
+ return $path;
+ }
+
+ /** Get the virtual URL for the file itself */
+ function getVirtualUrl( $suffix = false ) {
+ $path = $this->repo->getVirtualUrl() . '/public/' . $this->getUrlRel();
+ if ( $suffix !== false ) {
+ $path .= '/' . rawurlencode( $suffix );
+ }
+ return $path;
+ }
+
+ /**
+ * @return bool
+ */
+ function isHashed() {
+ return $this->repo->isHashed();
+ }
+
+ function readOnlyError() {
+ throw new MWException( get_class($this) . ': write operations are not supported' );
+ }
+
+ /**
+ * Record a file upload in the upload log and the image table
+ * STUB
+ * Overridden by LocalFile
+ */
+ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) {
+ $this->readOnlyError();
+ }
+
+ /**
+ * Move or copy a file to its public location. If a file exists at the
+ * destination, move it to an archive. Returns the archive name on success
+ * or an empty string if it was a new file, and a wikitext-formatted
+ * WikiError object on failure.
+ *
+ * The archive name should be passed through to recordUpload for database
+ * registration.
+ *
+ * @param string $sourcePath Local filesystem path to the source image
+ * @param integer $flags A bitwise combination of:
+ * File::DELETE_SOURCE Delete the source file, i.e. move
+ * rather than copy
+ * @return The archive name on success or an empty string if it was a new
+ * file, and a wikitext-formatted WikiError object on failure.
+ *
+ * STUB
+ * Overridden by LocalFile
+ */
+ function publish( $srcPath, $flags = 0 ) {
+ $this->readOnlyError();
+ }
+
+ /**
+ * Get an array of Title objects which are articles which use this file
+ * Also adds their IDs to the link cache
+ *
+ * This is mostly copied from Title::getLinksTo()
+ *
+ * @deprecated Use HTMLCacheUpdate, this function uses too much memory
+ */
+ function getLinksTo( $options = '' ) {
+ wfProfileIn( __METHOD__ );
+
+ // Note: use local DB not repo DB, we want to know local links
+ if ( $options ) {
+ $db = wfGetDB( DB_MASTER );
+ } else {
+ $db = wfGetDB( DB_SLAVE );
+ }
+ $linkCache =& LinkCache::singleton();
+
+ list( $page, $imagelinks ) = $db->tableNamesN( 'page', 'imagelinks' );
+ $encName = $db->addQuotes( $this->getName() );
+ $sql = "SELECT page_namespace,page_title,page_id FROM $page,$imagelinks WHERE page_id=il_from AND il_to=$encName $options";
+ $res = $db->query( $sql, __METHOD__ );
+
+ $retVal = array();
+ if ( $db->numRows( $res ) ) {
+ while ( $row = $db->fetchObject( $res ) ) {
+ if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) {
+ $linkCache->addGoodLinkObj( $row->page_id, $titleObj );
+ $retVal[] = $titleObj;
+ }
+ }
+ }
+ $db->freeResult( $res );
+ wfProfileOut( __METHOD__ );
+ return $retVal;
+ }
+
+ function formatMetadata() {
+ if ( !$this->getHandler() ) {
+ return false;
+ }
+ return $this->getHandler()->formatMetadata( $this, $this->getMetadata() );
+ }
+
+ /**
+ * Returns true if the file comes from the local file repository.
+ *
+ * @return bool
+ */
+ function isLocal() {
+ return $this->getRepoName() == 'local';
+ }
+
+ /**
+ * Returns the name of the repository.
+ *
+ * @return string
+ */
+ function getRepoName() {
+ return $this->repo ? $this->repo->getName() : 'unknown';
+ }
+
+ /**
+ * Returns true if the image is an old version
+ * STUB
+ */
+ function isOld() {
+ return false;
+ }
+
+ /**
+ * Is this file a "deleted" file in a private archive?
+ * STUB
+ */
+ function isDeleted( $field ) {
+ return false;
+ }
+
+ /**
+ * Was this file ever deleted from the wiki?
+ *
+ * @return bool
+ */
+ function wasDeleted() {
+ $title = $this->getTitle();
+ return $title && $title->isDeleted() > 0;
+ }
+
+ /**
+ * Delete all versions of the file.
+ *
+ * Moves the files into an archive directory (or deletes them)
+ * and removes the database rows.
+ *
+ * Cache purging is done; logging is caller's responsibility.
+ *
+ * @param $reason
+ * @return true on success, false on some kind of failure
+ * STUB
+ * Overridden by LocalFile
+ */
+ function delete( $reason, $suppress=false ) {
+ $this->readOnlyError();
+ }
+
+ /**
+ * Restore all or specified deleted revisions to the given file.
+ * Permissions and logging are left to the caller.
+ *
+ * May throw database exceptions on error.
+ *
+ * @param $versions set of record ids of deleted items to restore,
+ * or empty to restore all revisions.
+ * @return the number of file revisions restored if successful,
+ * or false on failure
+ * STUB
+ * Overridden by LocalFile
+ */
+ function restore( $versions=array(), $Unsuppress=false ) {
+ $this->readOnlyError();
+ }
+
+ /**
+ * Returns 'true' if this image is a multipage document, e.g. a DJVU
+ * document.
+ *
+ * @return Bool
+ */
+ function isMultipage() {
+ return $this->getHandler() && $this->handler->isMultiPage( $this );
+ }
+
+ /**
+ * Returns the number of pages of a multipage document, or NULL for
+ * documents which aren't multipage documents
+ */
+ function pageCount() {
+ if ( !isset( $this->pageCount ) ) {
+ if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
+ $this->pageCount = $this->handler->pageCount( $this );
+ } else {
+ $this->pageCount = false;
+ }
+ }
+ return $this->pageCount;
+ }
+
+ /**
+ * Calculate the height of a thumbnail using the source and destination width
+ */
+ static function scaleHeight( $srcWidth, $srcHeight, $dstWidth ) {
+ // Exact integer multiply followed by division
+ if ( $srcWidth == 0 ) {
+ return 0;
+ } else {
+ return round( $srcHeight * $dstWidth / $srcWidth );
+ }
+ }
+
+ /**
+ * Get an image size array like that returned by getimagesize(), or false if it
+ * can't be determined.
+ *
+ * @param string $fileName The filename
+ * @return array
+ */
+ function getImageSize( $fileName ) {
+ if ( !$this->getHandler() ) {
+ return false;
+ }
+ return $this->handler->getImageSize( $this, $fileName );
+ }
+
+ /**
+ * Get the URL of the image description page. May return false if it is
+ * unknown or not applicable.
+ */
+ function getDescriptionUrl() {
+ return $this->repo->getDescriptionUrl( $this->getName() );
+ }
+
+ /**
+ * Get the HTML text of the description page, if available
+ */
+ function getDescriptionText() {
+ if ( !$this->repo->fetchDescription ) {
+ return false;
+ }
+ $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName() );
+ if ( $renderUrl ) {
+ wfDebug( "Fetching shared description from $renderUrl\n" );
+ return Http::get( $renderUrl );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get the 14-character timestamp of the file upload, or false if
+ * it doesn't exist
+ */
+ function getTimestamp() {
+ $path = $this->getPath();
+ if ( !file_exists( $path ) ) {
+ return false;
+ }
+ return wfTimestamp( filemtime( $path ) );
+ }
+
+ /**
+ * Get the SHA-1 base 36 hash of the file
+ */
+ function getSha1() {
+ return self::sha1Base36( $this->getPath() );
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this file, if it's marked as deleted.
+ * STUB
+ * @param int $field
+ * @return bool
+ */
+ function userCan( $field ) {
+ return true;
+ }
+
+ /**
+ * Get an associative array containing information about a file in the local filesystem\
+ *
+ * @param string $path Absolute local filesystem path
+ * @param mixed $ext The file extension, or true to extract it from the filename.
+ * Set it to false to ignore the extension.
+ */
+ static function getPropsFromPath( $path, $ext = true ) {
+ wfProfileIn( __METHOD__ );
+ wfDebug( __METHOD__.": Getting file info for $path\n" );
+ $info = array(
+ 'fileExists' => file_exists( $path ) && !is_dir( $path )
+ );
+ $gis = false;
+ if ( $info['fileExists'] ) {
+ $magic = MimeMagic::singleton();
+
+ $info['mime'] = $magic->guessMimeType( $path, $ext );
+ list( $info['major_mime'], $info['minor_mime'] ) = self::splitMime( $info['mime'] );
+ $info['media_type'] = $magic->getMediaType( $path, $info['mime'] );
+
+ # Get size in bytes
+ $info['size'] = filesize( $path );
+
+ # Height, width and metadata
+ $handler = MediaHandler::getHandler( $info['mime'] );
+ if ( $handler ) {
+ $tempImage = (object)array();
+ $info['metadata'] = $handler->getMetadata( $tempImage, $path );
+ $gis = $handler->getImageSize( $tempImage, $path, $info['metadata'] );
+ } else {
+ $gis = false;
+ $info['metadata'] = '';
+ }
+ $info['sha1'] = self::sha1Base36( $path );
+
+ wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n");
+ } else {
+ $info['mime'] = NULL;
+ $info['media_type'] = MEDIATYPE_UNKNOWN;
+ $info['metadata'] = '';
+ $info['sha1'] = '';
+ wfDebug(__METHOD__.": $path NOT FOUND!\n");
+ }
+ if( $gis ) {
+ # NOTE: $gis[2] contains a code for the image type. This is no longer used.
+ $info['width'] = $gis[0];
+ $info['height'] = $gis[1];
+ if ( isset( $gis['bits'] ) ) {
+ $info['bits'] = $gis['bits'];
+ } else {
+ $info['bits'] = 0;
+ }
+ } else {
+ $info['width'] = 0;
+ $info['height'] = 0;
+ $info['bits'] = 0;
+ }
+ wfProfileOut( __METHOD__ );
+ return $info;
+ }
+
+ /**
+ * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
+ * encoding, zero padded to 31 digits.
+ *
+ * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
+ * fairly neatly.
+ *
+ * Returns false on failure
+ */
+ static function sha1Base36( $path ) {
+ wfSuppressWarnings();
+ $hash = sha1_file( $path );
+ wfRestoreWarnings();
+ if ( $hash === false ) {
+ return false;
+ } else {
+ return wfBaseConvert( $hash, 16, 36, 31 );
+ }
+ }
+
+ function getLongDesc() {
+ $handler = $this->getHandler();
+ if ( $handler ) {
+ return $handler->getLongDesc( $this );
+ } else {
+ return MediaHandler::getLongDesc( $this );
+ }
+ }
+
+ function getShortDesc() {
+ $handler = $this->getHandler();
+ if ( $handler ) {
+ return $handler->getShortDesc( $this );
+ } else {
+ return MediaHandler::getShortDesc( $this );
+ }
+ }
+
+ function getDimensionsString() {
+ $handler = $this->getHandler();
+ if ( $handler ) {
+ return $handler->getDimensionsString( $this );
+ } else {
+ return '';
+ }
+ }
+}
+/**
+ * Aliases for backwards compatibility with 1.6
+ */
+define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE );
+define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT );
+define( 'MW_IMG_DELETED_USER', File::DELETED_USER );
+define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED );
+
+
diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php
new file mode 100644
index 00000000..cf6d65c2
--- /dev/null
+++ b/includes/filerepo/FileRepo.php
@@ -0,0 +1,404 @@
+<?php
+
+/**
+ * Base class for file repositories
+ * Do not instantiate, use a derived class.
+ */
+abstract class FileRepo {
+ const DELETE_SOURCE = 1;
+ const OVERWRITE = 2;
+ const OVERWRITE_SAME = 4;
+
+ var $thumbScriptUrl, $transformVia404;
+ var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital;
+ var $pathDisclosureProtection = 'paranoid';
+
+ /**
+ * Factory functions for creating new files
+ * Override these in the base class
+ */
+ var $fileFactory = false, $oldFileFactory = false;
+
+ function __construct( $info ) {
+ // Required settings
+ $this->name = $info['name'];
+
+ // Optional settings
+ $this->initialCapital = true; // by default
+ foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
+ 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection' ) as $var )
+ {
+ if ( isset( $info[$var] ) ) {
+ $this->$var = $info[$var];
+ }
+ }
+ $this->transformVia404 = !empty( $info['transformVia404'] );
+ }
+
+ /**
+ * Determine if a string is an mwrepo:// URL
+ */
+ static function isVirtualUrl( $url ) {
+ return substr( $url, 0, 9 ) == 'mwrepo://';
+ }
+
+ /**
+ * Create a new File object from the local repository
+ * @param mixed $title Title object or string
+ * @param mixed $time Time at which the image is supposed to have existed.
+ * If this is specified, the returned object will be an
+ * instance of the repository's old file class instead of
+ * a current file. Repositories not supporting version
+ * control should return false if this parameter is set.
+ */
+ function newFile( $title, $time = false ) {
+ if ( !($title instanceof Title) ) {
+ $title = Title::makeTitleSafe( NS_IMAGE, $title );
+ if ( !is_object( $title ) ) {
+ return null;
+ }
+ }
+ if ( $time ) {
+ if ( $this->oldFileFactory ) {
+ return call_user_func( $this->oldFileFactory, $title, $this, $time );
+ } else {
+ return false;
+ }
+ } else {
+ return call_user_func( $this->fileFactory, $title, $this );
+ }
+ }
+
+ /**
+ * Find an instance of the named file that existed at the specified time
+ * Returns false if the file did not exist. Repositories not supporting
+ * version control should return false if the time is specified.
+ *
+ * @param mixed $time 14-character timestamp, or false for the current version
+ */
+ function findFile( $title, $time = false ) {
+ # First try the current version of the file to see if it precedes the timestamp
+ $img = $this->newFile( $title );
+ if ( !$img ) {
+ return false;
+ }
+ if ( $img->exists() && ( !$time || $img->getTimestamp() <= $time ) ) {
+ return $img;
+ }
+ # Now try an old version of the file
+ $img = $this->newFile( $title, $time );
+ if ( $img->exists() ) {
+ return $img;
+ }
+ }
+
+ /**
+ * Get the URL of thumb.php
+ */
+ function getThumbScriptUrl() {
+ return $this->thumbScriptUrl;
+ }
+
+ /**
+ * Returns true if the repository can transform files via a 404 handler
+ */
+ function canTransformVia404() {
+ return $this->transformVia404;
+ }
+
+ /**
+ * Get the name of an image from its title object
+ */
+ function getNameFromTitle( $title ) {
+ global $wgCapitalLinks;
+ if ( $this->initialCapital != $wgCapitalLinks ) {
+ global $wgContLang;
+ $name = $title->getUserCaseDBKey();
+ if ( $this->initialCapital ) {
+ $name = $wgContLang->ucfirst( $name );
+ }
+ } else {
+ $name = $title->getDBkey();
+ }
+ return $name;
+ }
+
+ static function getHashPathForLevel( $name, $levels ) {
+ if ( $levels == 0 ) {
+ return '';
+ } else {
+ $hash = md5( $name );
+ $path = '';
+ for ( $i = 1; $i <= $levels; $i++ ) {
+ $path .= substr( $hash, 0, $i ) . '/';
+ }
+ return $path;
+ }
+ }
+
+ /**
+ * Get the name of this repository, as specified by $info['name]' to the constructor
+ */
+ function getName() {
+ return $this->name;
+ }
+
+ /**
+ * Get the file description page base URL, or false if there isn't one.
+ * @private
+ */
+ function getDescBaseUrl() {
+ if ( is_null( $this->descBaseUrl ) ) {
+ if ( !is_null( $this->articleUrl ) ) {
+ $this->descBaseUrl = str_replace( '$1',
+ wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':', $this->articleUrl );
+ } elseif ( !is_null( $this->scriptDirUrl ) ) {
+ $this->descBaseUrl = $this->scriptDirUrl . '/index.php?title=' .
+ wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':';
+ } else {
+ $this->descBaseUrl = false;
+ }
+ }
+ return $this->descBaseUrl;
+ }
+
+ /**
+ * Get the URL of an image description page. May return false if it is
+ * unknown or not applicable. In general this should only be called by the
+ * File class, since it may return invalid results for certain kinds of
+ * repositories. Use File::getDescriptionUrl() in user code.
+ *
+ * In particular, it uses the article paths as specified to the repository
+ * constructor, whereas local repositories use the local Title functions.
+ */
+ function getDescriptionUrl( $name ) {
+ $base = $this->getDescBaseUrl();
+ if ( $base ) {
+ return $base . wfUrlencode( $name );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get the URL of the content-only fragment of the description page. For
+ * MediaWiki this means action=render. This should only be called by the
+ * repository's file class, since it may return invalid results. User code
+ * should use File::getDescriptionText().
+ */
+ function getDescriptionRenderUrl( $name ) {
+ if ( isset( $this->scriptDirUrl ) ) {
+ return $this->scriptDirUrl . '/index.php?title=' .
+ wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) .
+ '&action=render';
+ } else {
+ $descBase = $this->getDescBaseUrl();
+ if ( $descBase ) {
+ return wfAppendQuery( $descBase . wfUrlencode( $name ), 'action=render' );
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Store a file to a given destination.
+ *
+ * @param string $srcPath Source path or virtual URL
+ * @param string $dstZone Destination zone
+ * @param string $dstRel Destination relative path
+ * @param integer $flags Bitwise combination of the following flags:
+ * self::DELETE_SOURCE Delete the source file after upload
+ * self::OVERWRITE Overwrite an existing destination file instead of failing
+ * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
+ * same contents as the source
+ * @return FileRepoStatus
+ */
+ function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
+ $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags );
+ if ( $status->successCount == 0 ) {
+ $status->ok = false;
+ }
+ return $status;
+ }
+
+ /**
+ * Store a batch of files
+ *
+ * @param array $triplets (src,zone,dest) triplets as per store()
+ * @param integer $flags Flags as per store
+ */
+ abstract function storeBatch( $triplets, $flags = 0 );
+
+ /**
+ * Pick a random name in the temp zone and store a file to it.
+ * Returns a FileRepoStatus object with the URL in the value.
+ *
+ * @param string $originalName The base name of the file as specified
+ * by the user. The file extension will be maintained.
+ * @param string $srcPath The current location of the file.
+ */
+ abstract function storeTemp( $originalName, $srcPath );
+
+ /**
+ * Remove a temporary file or mark it for garbage collection
+ * @param string $virtualUrl The virtual URL returned by storeTemp
+ * @return boolean True on success, false on failure
+ * STUB
+ */
+ function freeTemp( $virtualUrl ) {
+ return true;
+ }
+
+ /**
+ * Copy or move a file either from the local filesystem or from an mwrepo://
+ * virtual URL, into this repository at the specified destination location.
+ *
+ * Returns a FileRepoStatus object. On success, the value contains "new" or
+ * "archived", to indicate whether the file was new with that name.
+ *
+ * @param string $srcPath The source path or URL
+ * @param string $dstRel The destination relative path
+ * @param string $archiveRel The relative path where the existing file is to
+ * be archived, if there is one. Relative to the public zone root.
+ * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
+ * that the source file should be deleted if possible
+ */
+ function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
+ $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags );
+ if ( $status->successCount == 0 ) {
+ $status->ok = false;
+ }
+ if ( isset( $status->value[0] ) ) {
+ $status->value = $status->value[0];
+ } else {
+ $status->value = false;
+ }
+ return $status;
+ }
+
+ /**
+ * Publish a batch of files
+ * @param array $triplets (source,dest,archive) triplets as per publish()
+ * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
+ * that the source files should be deleted if possible
+ */
+ abstract function publishBatch( $triplets, $flags = 0 );
+
+ /**
+ * Move a group of files to the deletion archive.
+ *
+ * If no valid deletion archive is configured, this may either delete the
+ * file or throw an exception, depending on the preference of the repository.
+ *
+ * The overwrite policy is determined by the repository -- currently FSRepo
+ * assumes a naming scheme in the deleted zone based on content hash, as
+ * opposed to the public zone which is assumed to be unique.
+ *
+ * @param array $sourceDestPairs Array of source/destination pairs. Each element
+ * is a two-element array containing the source file path relative to the
+ * public root in the first element, and the archive file path relative
+ * to the deleted zone root in the second element.
+ * @return FileRepoStatus
+ */
+ abstract function deleteBatch( $sourceDestPairs );
+
+ /**
+ * Move a file to the deletion archive.
+ * If no valid deletion archive exists, this may either delete the file
+ * or throw an exception, depending on the preference of the repository
+ * @param mixed $srcRel Relative path for the file to be deleted
+ * @param mixed $archiveRel Relative path for the archive location.
+ * Relative to a private archive directory.
+ * @return WikiError object (wikitext-formatted), or true for success
+ */
+ function delete( $srcRel, $archiveRel ) {
+ return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) );
+ }
+
+ /**
+ * Get properties of a file with a given virtual URL
+ * The virtual URL must refer to this repo
+ * Properties should ultimately be obtained via File::getPropsFromPath()
+ */
+ abstract function getFileProps( $virtualUrl );
+
+ /**
+ * Call a callback function for every file in the repository
+ * May use either the database or the filesystem
+ * STUB
+ */
+ function enumFiles( $callback ) {
+ throw new MWException( 'enumFiles is not supported by ' . get_class( $this ) );
+ }
+
+ /**
+ * Determine if a relative path is valid, i.e. not blank or involving directory traveral
+ */
+ function validateFilename( $filename ) {
+ if ( strval( $filename ) == '' ) {
+ return false;
+ }
+ if ( wfIsWindows() ) {
+ $filename = strtr( $filename, '\\', '/' );
+ }
+ /**
+ * Use the same traversal protection as Title::secureAndSplit()
+ */
+ if ( strpos( $filename, '.' ) !== false &&
+ ( $filename === '.' || $filename === '..' ||
+ strpos( $filename, './' ) === 0 ||
+ strpos( $filename, '../' ) === 0 ||
+ strpos( $filename, '/./' ) !== false ||
+ strpos( $filename, '/../' ) !== false ) )
+ {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**#@+
+ * Path disclosure protection functions
+ */
+ function paranoidClean( $param ) { return '[hidden]'; }
+ function passThrough( $param ) { return $param; }
+
+ /**
+ * Get a callback function to use for cleaning error message parameters
+ */
+ function getErrorCleanupFunction() {
+ switch ( $this->pathDisclosureProtection ) {
+ case 'none':
+ $callback = array( $this, 'passThrough' );
+ break;
+ default: // 'paranoid'
+ $callback = array( $this, 'paranoidClean' );
+ }
+ return $callback;
+ }
+ /**#@-*/
+
+ /**
+ * Create a new fatal error
+ */
+ function newFatal( $message /*, parameters...*/ ) {
+ $params = func_get_args();
+ array_unshift( $params, $this );
+ return call_user_func_array( array( 'FileRepoStatus', 'newFatal' ), $params );
+ }
+
+ /**
+ * Create a new good result
+ */
+ function newGood( $value = null ) {
+ return FileRepoStatus::newGood( $this, $value );
+ }
+
+ /**
+ * Delete files in the deleted directory if they are not referenced in the filearchive table
+ * STUB
+ */
+ function cleanupDeletedBatch( $storageKeys ) {}
+}
+
diff --git a/includes/filerepo/FileRepoStatus.php b/includes/filerepo/FileRepoStatus.php
new file mode 100644
index 00000000..972b2e46
--- /dev/null
+++ b/includes/filerepo/FileRepoStatus.php
@@ -0,0 +1,171 @@
+<?php
+
+/**
+ * Generic operation result class
+ * Has warning/error list, boolean status and arbitrary value
+ */
+class FileRepoStatus {
+ var $ok = true;
+ var $value;
+
+ /** Counters for batch operations */
+ var $successCount = 0, $failCount = 0;
+
+ /*semi-private*/ var $errors = array();
+ /*semi-private*/ var $cleanCallback = false;
+
+ /**
+ * Factory function for fatal errors
+ */
+ static function newFatal( $repo, $message /*, parameters...*/ ) {
+ $params = array_slice( func_get_args(), 1 );
+ $result = new self( $repo );
+ call_user_func_array( array( &$result, 'error' ), $params );
+ $result->ok = false;
+ return $result;
+ }
+
+ static function newGood( $repo = false, $value = null ) {
+ $result = new self( $repo );
+ $result->value = $value;
+ return $result;
+ }
+
+ function __construct( $repo = false ) {
+ if ( $repo ) {
+ $this->cleanCallback = $repo->getErrorCleanupFunction();
+ }
+ }
+
+ function setResult( $ok, $value = null ) {
+ $this->ok = $ok;
+ $this->value = $value;
+ }
+
+ function isGood() {
+ return $this->ok && !$this->errors;
+ }
+
+ function isOK() {
+ return $this->ok;
+ }
+
+ function warning( $message /*, parameters... */ ) {
+ $params = array_slice( func_get_args(), 1 );
+ $this->errors[] = array(
+ 'type' => 'warning',
+ 'message' => $message,
+ 'params' => $params );
+ }
+
+ /**
+ * Add an error, do not set fatal flag
+ * This can be used for non-fatal errors
+ */
+ function error( $message /*, parameters... */ ) {
+ $params = array_slice( func_get_args(), 1 );
+ $this->errors[] = array(
+ 'type' => 'error',
+ 'message' => $message,
+ 'params' => $params );
+ }
+
+ /**
+ * Add an error and set OK to false, indicating that the operation as a whole was fatal
+ */
+ function fatal( $message /*, parameters... */ ) {
+ $params = array_slice( func_get_args(), 1 );
+ $this->errors[] = array(
+ 'type' => 'error',
+ 'message' => $message,
+ 'params' => $params );
+ $this->ok = false;
+ }
+
+ protected function cleanParams( $params ) {
+ if ( !$this->cleanCallback ) {
+ return $params;
+ }
+ $cleanParams = array();
+ foreach ( $params as $i => $param ) {
+ $cleanParams[$i] = call_user_func( $this->cleanCallback, $param );
+ }
+ return $cleanParams;
+ }
+
+ protected function getItemXML( $item ) {
+ $params = $this->cleanParams( $item['params'] );
+ $xml = "<{$item['type']}>\n" .
+ Xml::element( 'message', null, $item['message'] ) . "\n" .
+ Xml::element( 'text', null, wfMsgReal( $item['message'], $params ) ) ."\n";
+ foreach ( $params as $param ) {
+ $xml .= Xml::element( 'param', null, $param );
+ }
+ $xml .= "</{$this->type}>\n";
+ return $xml;
+ }
+
+ /**
+ * Get the error list as XML
+ */
+ function getXML() {
+ $xml = "<errors>\n";
+ foreach ( $this->errors as $error ) {
+ $xml .= $this->getItemXML( $error );
+ }
+ $xml .= "</errors>\n";
+ return $xml;
+ }
+
+ /**
+ * Get the error list as a wikitext formatted list
+ * @param string $shortContext A short enclosing context message name, to be used
+ * when there is a single error
+ * @param string $longContext A long enclosing context message name, for a list
+ */
+ function getWikiText( $shortContext = false, $longContext = false ) {
+ if ( count( $this->errors ) == 0 ) {
+ if ( $this->ok ) {
+ $this->fatal( 'internalerror_info',
+ __METHOD__." called for a good result, this is incorrect\n" );
+ } else {
+ $this->fatal( 'internalerror_info',
+ __METHOD__.": Invalid result object: no error text but not OK\n" );
+ }
+ }
+ if ( count( $this->errors ) == 1 ) {
+ $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $this->errors[0]['params'] ) );
+ $s = wfMsgReal( $this->errors[0]['message'], $params );
+ if ( $shortContext ) {
+ $s = wfMsg( $shortContext, $s );
+ } elseif ( $longContext ) {
+ $s = wfMsg( $longContext, "* $s\n" );
+ }
+ } else {
+ $s = '';
+ foreach ( $this->errors as $error ) {
+ $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) );
+ $s .= '* ' . wfMsgReal( $error['message'], $params ) . "\n";
+ }
+ if ( $longContext ) {
+ $s = wfMsg( $longContext, $s );
+ } elseif ( $shortContext ) {
+ $s = wfMsg( $shortContext, "\n* $s\n" );
+ }
+ }
+ return $s;
+ }
+
+ /**
+ * Merge another status object into this one
+ */
+ function merge( $other, $overwriteValue = false ) {
+ $this->errors = array_merge( $this->errors, $other->errors );
+ $this->ok = $this->ok && $other->ok;
+ if ( $overwriteValue ) {
+ $this->value = $other->value;
+ }
+ $this->successCount += $other->successCount;
+ $this->failCount += $other->failCount;
+ }
+}
diff --git a/includes/filerepo/ForeignDBFile.php b/includes/filerepo/ForeignDBFile.php
new file mode 100644
index 00000000..4d11640a
--- /dev/null
+++ b/includes/filerepo/ForeignDBFile.php
@@ -0,0 +1,42 @@
+<?php
+
+class ForeignDBFile extends LocalFile {
+ static function newFromTitle( $title, $repo ) {
+ return new self( $title, $repo );
+ }
+
+ function getCacheKey() {
+ if ( $this->repo->hasSharedCache ) {
+ $hashedName = md5($this->name);
+ return wfForeignMemcKey( $this->repo->dbName, $this->repo->tablePrefix,
+ 'file', $hashedName );
+ } else {
+ return false;
+ }
+ }
+
+ function publish( /*...*/ ) {
+ $this->readOnlyError();
+ }
+
+ function recordUpload( /*...*/ ) {
+ $this->readOnlyError();
+ }
+ function restore( /*...*/ ) {
+ $this->readOnlyError();
+ }
+ function delete( /*...*/ ) {
+ $this->readOnlyError();
+ }
+
+ function getDescriptionUrl() {
+ // Restore remote behaviour
+ return File::getDescriptionUrl();
+ }
+
+ function getDescriptionText() {
+ // Restore remote behaviour
+ return File::getDescriptionText();
+ }
+}
+
diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php
new file mode 100644
index 00000000..13dcd029
--- /dev/null
+++ b/includes/filerepo/ForeignDBRepo.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * A foreign repository with an accessible MediaWiki database
+ */
+
+class ForeignDBRepo extends LocalRepo {
+ # Settings
+ var $dbType, $dbServer, $dbUser, $dbPassword, $dbName, $dbFlags,
+ $tablePrefix, $hasSharedCache;
+
+ # Other stuff
+ var $dbConn;
+ var $fileFactory = array( 'ForeignDBFile', 'newFromTitle' );
+
+ function __construct( $info ) {
+ parent::__construct( $info );
+ $this->dbType = $info['dbType'];
+ $this->dbServer = $info['dbServer'];
+ $this->dbUser = $info['dbUser'];
+ $this->dbPassword = $info['dbPassword'];
+ $this->dbName = $info['dbName'];
+ $this->dbFlags = $info['dbFlags'];
+ $this->tablePrefix = $info['tablePrefix'];
+ $this->hasSharedCache = $info['hasSharedCache'];
+ }
+
+ function getMasterDB() {
+ if ( !isset( $this->dbConn ) ) {
+ $class = 'Database' . ucfirst( $this->dbType );
+ $this->dbConn = new $class( $this->dbServer, $this->dbUser,
+ $this->dbPassword, $this->dbName, false, $this->dbFlags,
+ $this->tablePrefix );
+ }
+ return $this->dbConn;
+ }
+
+ function getSlaveDB() {
+ return $this->getMasterDB();
+ }
+
+ function hasSharedCache() {
+ return $this->hasSharedCache;
+ }
+
+ function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
+ throw new MWException( get_class($this) . ': write operations are not supported' );
+ }
+ function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
+ throw new MWException( get_class($this) . ': write operations are not supported' );
+ }
+ function deleteBatch( $fileMap ) {
+ throw new MWException( get_class($this) . ': write operations are not supported' );
+ }
+}
+
+
diff --git a/includes/filerepo/ICRepo.php b/includes/filerepo/ICRepo.php
new file mode 100644
index 00000000..124fe2b6
--- /dev/null
+++ b/includes/filerepo/ICRepo.php
@@ -0,0 +1,313 @@
+<?php
+
+/**
+ * A repository for files accessible via InstantCommons.
+ */
+
+class ICRepo extends LocalRepo {
+ var $directory, $url, $hashLevels, $cache;
+ var $fileFactory = array( 'ICFile', 'newFromTitle' );
+ var $oldFileFactory = false;
+
+ function __construct( $info ) {
+ parent::__construct( $info );
+ // Required settings
+ $this->directory = $info['directory'];
+ $this->url = $info['url'];
+ $this->hashLevels = $info['hashLevels'];
+ if(isset($info['cache'])){
+ $this->cache = getcwd().'/images/'.$info['cache'];
+ }
+ }
+}
+
+/**
+ * A file loaded from InstantCommons
+ */
+class ICFile extends LocalFile{
+ static function newFromTitle($title,$repo){
+ return new self($title, $repo);
+ }
+
+ /**
+ * Returns true if the file comes from the local file repository.
+ *
+ * @return bool
+ */
+ function isLocal() {
+ return true;
+ }
+
+ function load(){
+ if (!$this->dataLoaded ) {
+ if ( !$this->loadFromCache() ) {
+ if(!$this->loadFromDB()){
+ $this->loadFromIC();
+ }
+ $this->saveToCache();
+ }
+ $this->dataLoaded = true;
+ }
+ }
+
+ /**
+ * Load file metadata from the DB
+ */
+ function loadFromDB() {
+ wfProfileIn( __METHOD__ );
+
+ # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
+ $this->dataLoaded = true;
+
+ $dbr = $this->repo->getSlaveDB();
+
+ $row = $dbr->selectRow( 'ic_image', $this->getCacheFields( 'img_' ),
+ array( 'img_name' => $this->getName() ), __METHOD__ );
+ if ( $row ) {
+ if (trim($row->img_media_type)==NULL) {
+ $this->upgradeRow();
+ $this->upgraded = true;
+ }
+ $this->loadFromRow( $row );
+ //This means that these files are local so the repository locations are local
+ $this->setUrlPathLocal();
+ $this->fileExists = true;
+ //var_dump($this); exit;
+ } else {
+ $this->fileExists = false;
+ }
+
+ wfProfileOut( __METHOD__ );
+
+ return $this->fileExists;
+ }
+
+ /**
+ * Fix assorted version-related problems with the image row by reloading it from the file
+ */
+ function upgradeRow() {
+ wfProfileIn( __METHOD__ );
+
+ $this->loadFromIC();
+
+ $dbw = $this->repo->getMasterDB();
+ list( $major, $minor ) = self::splitMime( $this->mime );
+
+ wfDebug(__METHOD__.': upgrading '.$this->getName()." to the current schema\n");
+
+ $dbw->update( 'ic_image',
+ array(
+ 'img_width' => $this->width,
+ 'img_height' => $this->height,
+ 'img_bits' => $this->bits,
+ 'img_media_type' => $this->type,
+ 'img_major_mime' => $major,
+ 'img_minor_mime' => $minor,
+ 'img_metadata' => $this->metadata,
+ ), array( 'img_name' => $this->getName() ),
+ __METHOD__
+ );
+ $this->saveToCache();
+ wfProfileOut( __METHOD__ );
+ }
+
+ function exists(){
+ $this->load();
+ return $this->fileExists;
+
+ }
+
+ /**
+ * Fetch the file from the repository. Check local ic_images table first. If not available, check remote server
+ */
+ function loadFromIC(){
+ # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
+ $this->dataLoaded = true;
+ $icUrl = $this->repo->directory.'&media='.$this->title->mDbkeyform;
+ if($h = @fopen($icUrl, 'rb')){
+ $contents = fread($h, 3000);
+ $image = $this->api_xml_to_array($contents);
+ if($image['fileExists']){
+ foreach($image as $property=>$value){
+ if($property=="url"){$value=$this->repo->url.$value; }
+ $this->$property = $value;
+ }
+ if($this->curl_file_get_contents($this->repo->url.$image['url'], $this->repo->cache.'/'.$image['name'])){
+ //Record the image
+ $this->recordDownload("Downloaded with InstantCommons");
+
+ //Then cache it
+ }else{//set fileExists back to false
+ $this->fileExists = false;
+ }
+ }
+ }
+ }
+
+
+ function setUrlPathLocal(){
+ global $wgScriptPath;
+ $path = $wgScriptPath.'/'.substr($this->repo->cache, strlen($wgScriptPath));
+ $this->repo->url = $path;//.'/'.rawurlencode($this->title->mDbkeyform);
+ $this->repo->directory = $this->repo->cache;//.'/'.rawurlencode($this->title->mDbkeyform);
+
+ }
+
+ function getThumbPath( $suffix=false ){
+ $path = $this->repo->cache;
+ if ( $suffix !== false ) {
+ $path .= '/thumb/' . rawurlencode( $suffix );
+ }
+ return $path;
+ }
+ function getThumbUrl( $suffix=false ){
+ global $wgScriptPath;
+ $path = $wgScriptPath.'/'.substr($this->repo->cache, strlen($wgScriptPath));
+ if ( $suffix !== false ) {
+ $path .= '/thumb/' . rawurlencode( $suffix );
+ }
+ return $path;
+ }
+
+ /**
+ * Convert the InstantCommons Server API XML Response to an associative array
+ */
+ function api_xml_to_array($xml){
+ preg_match("/<instantcommons><image(.*?)<\/instantcommons>/",$xml,$match);
+ preg_match_all("/(.*?=\".*?\")/",$match[1], $matches);
+ foreach($matches[1] as $match){
+ list($key,$value) = split("=",$match);
+ $image[trim($key,'<" ')]=trim($value,' "');
+ }
+ return $image;
+ }
+
+ /**
+ * Use cURL to read the content of a URL into a string
+ * ref: http://groups-beta.google.com/group/comp.lang.php/browse_thread/thread/8efbbaced3c45e3c/d63c7891cf8e380b?lnk=raot
+ * @param string $url - the URL to fetch
+ * @param resource $fp - filename to write file contents to
+ * @param boolean $bg - call cURL in the background (don't hang page until complete)
+ * @param int $timeout - cURL connect timeout
+ */
+ function curl_file_get_contents($url, $fp, $bg=TRUE, $timeout = 1) {
+ {
+ # Call curl in the background to download the file
+ $cmd = 'curl '.wfEscapeShellArg($url).' -o '.$fp.' &';
+ wfDebug('Curl download initiated='.$cmd );
+ $success = false;
+ $file_contents = array();
+ $file_contents['err'] = wfShellExec($cmd, $file_contents['return']);
+ if($file_contents['err']==0){//Success
+ $success = true;
+ }
+ }
+ return $success;
+ }
+
+ function getMasterDB() {
+ if ( !isset( $this->dbConn ) ) {
+ $class = 'Database' . ucfirst( $this->dbType );
+ $this->dbConn = new $class( $this->dbServer, $this->dbUser,
+ $this->dbPassword, $this->dbName, false, $this->dbFlags,
+ $this->tablePrefix );
+ }
+ return $this->dbConn;
+ }
+
+ /**
+ * Record a file upload in the upload log and the image table
+ */
+ private function recordDownload($comment='', $timestamp = false ){
+ global $wgUser;
+
+ $dbw = $this->repo->getMasterDB();
+
+ if ( $timestamp === false ) {
+ $timestamp = $dbw->timestamp();
+ }
+ list( $major, $minor ) = self::splitMime( $this->mime );
+
+ # Test to see if the row exists using INSERT IGNORE
+ # This avoids race conditions by locking the row until the commit, and also
+ # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
+ $dbw->insert( 'ic_image',
+ array(
+ 'img_name' => $this->getName(),
+ 'img_size'=> $this->size,
+ 'img_width' => intval( $this->width ),
+ 'img_height' => intval( $this->height ),
+ 'img_bits' => $this->bits,
+ 'img_media_type' => $this->type,
+ 'img_major_mime' => $major,
+ 'img_minor_mime' => $minor,
+ 'img_timestamp' => $timestamp,
+ 'img_description' => $comment,
+ 'img_user' => $wgUser->getID(),
+ 'img_user_text' => $wgUser->getName(),
+ 'img_metadata' => $this->metadata,
+ ),
+ __METHOD__,
+ 'IGNORE'
+ );
+
+ if( $dbw->affectedRows() == 0 ) {
+ # Collision, this is an update of a file
+ # Update the current image row
+ $dbw->update( 'ic_image',
+ array( /* SET */
+ 'img_size' => $this->size,
+ 'img_width' => intval( $this->width ),
+ 'img_height' => intval( $this->height ),
+ 'img_bits' => $this->bits,
+ 'img_media_type' => $this->media_type,
+ 'img_major_mime' => $this->major_mime,
+ 'img_minor_mime' => $this->minor_mime,
+ 'img_timestamp' => $timestamp,
+ 'img_description' => $comment,
+ 'img_user' => $wgUser->getID(),
+ 'img_user_text' => $wgUser->getName(),
+ 'img_metadata' => $this->metadata,
+ ), array( /* WHERE */
+ 'img_name' => $this->getName()
+ ), __METHOD__
+ );
+ } else {
+ # This is a new file
+ # Update the image count
+ $site_stats = $dbw->tableName( 'site_stats' );
+ $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
+ }
+
+ $descTitle = $this->getTitle();
+ $article = new Article( $descTitle );
+
+ # Add the log entry
+ $log = new LogPage( 'icdownload' );
+ $log->addEntry( 'InstantCommons download', $descTitle, $comment );
+
+ if( $descTitle->exists() ) {
+ # Create a null revision
+ $nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(), $log->getRcComment(), false );
+ $nullRevision->insertOn( $dbw );
+ $article->updateRevisionOn( $dbw, $nullRevision );
+
+ # Invalidate the cache for the description page
+ $descTitle->invalidateCache();
+ $descTitle->purgeSquid();
+ }
+
+
+ # Commit the transaction now, in case something goes wrong later
+ # The most important thing is that files don't get lost, especially archives
+ $dbw->immediateCommit();
+
+ # Invalidate cache for all pages using this file
+ $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
+ $update->doUpdate();
+
+ return true;
+ }
+
+}
+
diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php
new file mode 100644
index 00000000..1e5fc449
--- /dev/null
+++ b/includes/filerepo/LocalFile.php
@@ -0,0 +1,1573 @@
+<?php
+/**
+ */
+
+/**
+ * Bump this number when serialized cache records may be incompatible.
+ */
+define( 'MW_FILE_VERSION', 4 );
+
+/**
+ * Class to represent a local file in the wiki's own database
+ *
+ * Provides methods to retrieve paths (physical, logical, URL),
+ * to generate image thumbnails or for uploading.
+ *
+ * Note that only the repo object knows what its file class is called. You should
+ * never name a file class explictly outside of the repo class. Instead use the
+ * repo's factory functions to generate file objects, for example:
+ *
+ * RepoGroup::singleton()->getLocalRepo()->newFile($title);
+ *
+ * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
+ * in most cases.
+ *
+ * @addtogroup FileRepo
+ */
+class LocalFile extends File
+{
+ /**#@+
+ * @private
+ */
+ var $fileExists, # does the file file exist on disk? (loadFromXxx)
+ $historyLine, # Number of line to return by nextHistoryLine() (constructor)
+ $historyRes, # result of the query for the file's history (nextHistoryLine)
+ $width, # \
+ $height, # |
+ $bits, # --- returned by getimagesize (loadFromXxx)
+ $attr, # /
+ $media_type, # MEDIATYPE_xxx (bitmap, drawing, audio...)
+ $mime, # MIME type, determined by MimeMagic::guessMimeType
+ $major_mime, # Major mime type
+ $minor_mime, # Minor mime type
+ $size, # Size in bytes (loadFromXxx)
+ $metadata, # Handler-specific metadata
+ $timestamp, # Upload timestamp
+ $sha1, # SHA-1 base 36 content hash
+ $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
+ $upgraded, # Whether the row was upgraded on load
+ $locked; # True if the image row is locked
+
+ /**#@-*/
+
+ /**
+ * Create a LocalFile from a title
+ * Do not call this except from inside a repo class.
+ */
+ static function newFromTitle( $title, $repo ) {
+ return new self( $title, $repo );
+ }
+
+ /**
+ * Create a LocalFile from a title
+ * Do not call this except from inside a repo class.
+ */
+ static function newFromRow( $row, $repo ) {
+ $title = Title::makeTitle( NS_IMAGE, $row->img_name );
+ $file = new self( $title, $repo );
+ $file->loadFromRow( $row );
+ return $file;
+ }
+
+ /**
+ * Constructor.
+ * Do not call this except from inside a repo class.
+ */
+ function __construct( $title, $repo ) {
+ if( !is_object( $title ) ) {
+ throw new MWException( __CLASS__.' constructor given bogus title.' );
+ }
+ parent::__construct( $title, $repo );
+ $this->metadata = '';
+ $this->historyLine = 0;
+ $this->historyRes = null;
+ $this->dataLoaded = false;
+ }
+
+ /**
+ * Get the memcached key
+ */
+ function getCacheKey() {
+ $hashedName = md5($this->getName());
+ return wfMemcKey( 'file', $hashedName );
+ }
+
+ /**
+ * Try to load file metadata from memcached. Returns true on success.
+ */
+ function loadFromCache() {
+ global $wgMemc;
+ wfProfileIn( __METHOD__ );
+ $this->dataLoaded = false;
+ $key = $this->getCacheKey();
+ if ( !$key ) {
+ return false;
+ }
+ $cachedValues = $wgMemc->get( $key );
+
+ // Check if the key existed and belongs to this version of MediaWiki
+ if ( isset($cachedValues['version']) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) {
+ wfDebug( "Pulling file metadata from cache key $key\n" );
+ $this->fileExists = $cachedValues['fileExists'];
+ if ( $this->fileExists ) {
+ unset( $cachedValues['version'] );
+ unset( $cachedValues['fileExists'] );
+ foreach ( $cachedValues as $name => $value ) {
+ $this->$name = $value;
+ }
+ }
+ }
+ if ( $this->dataLoaded ) {
+ wfIncrStats( 'image_cache_hit' );
+ } else {
+ wfIncrStats( 'image_cache_miss' );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $this->dataLoaded;
+ }
+
+ /**
+ * Save the file metadata to memcached
+ */
+ function saveToCache() {
+ global $wgMemc;
+ $this->load();
+ $key = $this->getCacheKey();
+ if ( !$key ) {
+ return;
+ }
+ $fields = $this->getCacheFields( '' );
+ $cache = array( 'version' => MW_FILE_VERSION );
+ $cache['fileExists'] = $this->fileExists;
+ if ( $this->fileExists ) {
+ foreach ( $fields as $field ) {
+ $cache[$field] = $this->$field;
+ }
+ }
+
+ $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week
+ }
+
+ /**
+ * Load metadata from the file itself
+ */
+ function loadFromFile() {
+ $this->setProps( self::getPropsFromPath( $this->getPath() ) );
+ }
+
+ function getCacheFields( $prefix = 'img_' ) {
+ static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
+ 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1' );
+ static $results = array();
+ if ( $prefix == '' ) {
+ return $fields;
+ }
+ if ( !isset( $results[$prefix] ) ) {
+ $prefixedFields = array();
+ foreach ( $fields as $field ) {
+ $prefixedFields[] = $prefix . $field;
+ }
+ $results[$prefix] = $prefixedFields;
+ }
+ return $results[$prefix];
+ }
+
+ /**
+ * Load file metadata from the DB
+ */
+ function loadFromDB() {
+ # Polymorphic function name to distinguish foreign and local fetches
+ $fname = get_class( $this ) . '::' . __FUNCTION__;
+ wfProfileIn( $fname );
+
+ # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
+ $this->dataLoaded = true;
+
+ $dbr = $this->repo->getSlaveDB();
+
+ $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
+ array( 'img_name' => $this->getName() ), $fname );
+ if ( $row ) {
+ $this->loadFromRow( $row );
+ } else {
+ $this->fileExists = false;
+ }
+
+ wfProfileOut( $fname );
+ }
+
+ /**
+ * Decode a row from the database (either object or array) to an array
+ * with timestamps and MIME types decoded, and the field prefix removed.
+ */
+ function decodeRow( $row, $prefix = 'img_' ) {
+ $array = (array)$row;
+ $prefixLength = strlen( $prefix );
+ // Sanity check prefix once
+ if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
+ throw new MWException( __METHOD__. ': incorrect $prefix parameter' );
+ }
+ $decoded = array();
+ foreach ( $array as $name => $value ) {
+ $decoded[substr( $name, $prefixLength )] = $value;
+ }
+ $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
+ if ( empty( $decoded['major_mime'] ) ) {
+ $decoded['mime'] = "unknown/unknown";
+ } else {
+ if (!$decoded['minor_mime']) {
+ $decoded['minor_mime'] = "unknown";
+ }
+ $decoded['mime'] = $decoded['major_mime'].'/'.$decoded['minor_mime'];
+ }
+ # Trim zero padding from char/binary field
+ $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
+ return $decoded;
+ }
+
+ /*
+ * Load file metadata from a DB result row
+ */
+ function loadFromRow( $row, $prefix = 'img_' ) {
+ $this->dataLoaded = true;
+ $array = $this->decodeRow( $row, $prefix );
+ foreach ( $array as $name => $value ) {
+ $this->$name = $value;
+ }
+ $this->fileExists = true;
+ // Check for rows from a previous schema, quietly upgrade them
+ $this->maybeUpgradeRow();
+ }
+
+ /**
+ * Load file metadata from cache or DB, unless already loaded
+ */
+ function load() {
+ if ( !$this->dataLoaded ) {
+ if ( !$this->loadFromCache() ) {
+ $this->loadFromDB();
+ $this->saveToCache();
+ }
+ $this->dataLoaded = true;
+ }
+ }
+
+ /**
+ * Upgrade a row if it needs it
+ */
+ function maybeUpgradeRow() {
+ if ( wfReadOnly() ) {
+ return;
+ }
+ if ( is_null($this->media_type) ||
+ $this->mime == 'image/svg'
+ ) {
+ $this->upgradeRow();
+ $this->upgraded = true;
+ } else {
+ $handler = $this->getHandler();
+ if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) {
+ $this->upgradeRow();
+ $this->upgraded = true;
+ }
+ }
+ }
+
+ function getUpgraded() {
+ return $this->upgraded;
+ }
+
+ /**
+ * Fix assorted version-related problems with the image row by reloading it from the file
+ */
+ function upgradeRow() {
+ wfProfileIn( __METHOD__ );
+
+ $this->loadFromFile();
+
+ # Don't destroy file info of missing files
+ if ( !$this->fileExists ) {
+ wfDebug( __METHOD__.": file does not exist, aborting\n" );
+ return;
+ }
+ $dbw = $this->repo->getMasterDB();
+ list( $major, $minor ) = self::splitMime( $this->mime );
+
+ wfDebug(__METHOD__.': upgrading '.$this->getName()." to the current schema\n");
+
+ $dbw->update( 'image',
+ array(
+ 'img_width' => $this->width,
+ 'img_height' => $this->height,
+ 'img_bits' => $this->bits,
+ 'img_media_type' => $this->media_type,
+ 'img_major_mime' => $major,
+ 'img_minor_mime' => $minor,
+ 'img_metadata' => $this->metadata,
+ 'img_sha1' => $this->sha1,
+ ), array( 'img_name' => $this->getName() ),
+ __METHOD__
+ );
+ $this->saveToCache();
+ wfProfileOut( __METHOD__ );
+ }
+
+ function setProps( $info ) {
+ $this->dataLoaded = true;
+ $fields = $this->getCacheFields( '' );
+ $fields[] = 'fileExists';
+ foreach ( $fields as $field ) {
+ if ( isset( $info[$field] ) ) {
+ $this->$field = $info[$field];
+ }
+ }
+ // Fix up mime fields
+ if ( isset( $info['major_mime'] ) ) {
+ $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
+ } elseif ( isset( $info['mime'] ) ) {
+ list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
+ }
+ }
+
+ /** splitMime inherited */
+ /** getName inherited */
+ /** getTitle inherited */
+ /** getURL inherited */
+ /** getViewURL inherited */
+ /** getPath inherited */
+
+ /**
+ * Return the width of the image
+ *
+ * Returns false on error
+ * @public
+ */
+ function getWidth( $page = 1 ) {
+ $this->load();
+ if ( $this->isMultipage() ) {
+ $dim = $this->getHandler()->getPageDimensions( $this, $page );
+ if ( $dim ) {
+ return $dim['width'];
+ } else {
+ return false;
+ }
+ } else {
+ return $this->width;
+ }
+ }
+
+ /**
+ * Return the height of the image
+ *
+ * Returns false on error
+ * @public
+ */
+ function getHeight( $page = 1 ) {
+ $this->load();
+ if ( $this->isMultipage() ) {
+ $dim = $this->getHandler()->getPageDimensions( $this, $page );
+ if ( $dim ) {
+ return $dim['height'];
+ } else {
+ return false;
+ }
+ } else {
+ return $this->height;
+ }
+ }
+
+ /**
+ * Get handler-specific metadata
+ */
+ function getMetadata() {
+ $this->load();
+ return $this->metadata;
+ }
+
+ /**
+ * Return the size of the image file, in bytes
+ * @public
+ */
+ function getSize() {
+ $this->load();
+ return $this->size;
+ }
+
+ /**
+ * Returns the mime type of the file.
+ */
+ function getMimeType() {
+ $this->load();
+ return $this->mime;
+ }
+
+ /**
+ * Return the type of the media in the file.
+ * Use the value returned by this function with the MEDIATYPE_xxx constants.
+ */
+ function getMediaType() {
+ $this->load();
+ return $this->media_type;
+ }
+
+ /** canRender inherited */
+ /** mustRender inherited */
+ /** allowInlineDisplay inherited */
+ /** isSafeFile inherited */
+ /** isTrustedFile inherited */
+
+ /**
+ * Returns true if the file file exists on disk.
+ * @return boolean Whether file file exist on disk.
+ * @public
+ */
+ function exists() {
+ $this->load();
+ return $this->fileExists;
+ }
+
+ /** getTransformScript inherited */
+ /** getUnscaledThumb inherited */
+ /** thumbName inherited */
+ /** createThumb inherited */
+ /** getThumbnail inherited */
+ /** transform inherited */
+
+ /**
+ * Fix thumbnail files from 1.4 or before, with extreme prejudice
+ */
+ function migrateThumbFile( $thumbName ) {
+ $thumbDir = $this->getThumbPath();
+ $thumbPath = "$thumbDir/$thumbName";
+ if ( is_dir( $thumbPath ) ) {
+ // Directory where file should be
+ // This happened occasionally due to broken migration code in 1.5
+ // Rename to broken-*
+ for ( $i = 0; $i < 100 ; $i++ ) {
+ $broken = $this->repo->getZonePath('public') . "/broken-$i-$thumbName";
+ if ( !file_exists( $broken ) ) {
+ rename( $thumbPath, $broken );
+ break;
+ }
+ }
+ // Doesn't exist anymore
+ clearstatcache();
+ }
+ if ( is_file( $thumbDir ) ) {
+ // File where directory should be
+ unlink( $thumbDir );
+ // Doesn't exist anymore
+ clearstatcache();
+ }
+ }
+
+ /** getHandler inherited */
+ /** iconThumb inherited */
+ /** getLastError inherited */
+
+ /**
+ * Get all thumbnail names previously generated for this file
+ */
+ function getThumbnails() {
+ if ( $this->isHashed() ) {
+ $this->load();
+ $files = array();
+ $dir = $this->getThumbPath();
+
+ if ( is_dir( $dir ) ) {
+ $handle = opendir( $dir );
+
+ if ( $handle ) {
+ while ( false !== ( $file = readdir($handle) ) ) {
+ if ( $file{0} != '.' ) {
+ $files[] = $file;
+ }
+ }
+ closedir( $handle );
+ }
+ }
+ } else {
+ $files = array();
+ }
+
+ return $files;
+ }
+
+ /**
+ * Refresh metadata in memcached, but don't touch thumbnails or squid
+ */
+ function purgeMetadataCache() {
+ $this->loadFromDB();
+ $this->saveToCache();
+ $this->purgeHistory();
+ }
+
+ /**
+ * Purge the shared history (OldLocalFile) cache
+ */
+ function purgeHistory() {
+ global $wgMemc;
+ $hashedName = md5($this->getName());
+ $oldKey = wfMemcKey( 'oldfile', $hashedName );
+ $wgMemc->delete( $oldKey );
+ }
+
+ /**
+ * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
+ */
+ function purgeCache() {
+ // Refresh metadata cache
+ $this->purgeMetadataCache();
+
+ // Delete thumbnails
+ $this->purgeThumbnails();
+
+ // Purge squid cache for this file
+ wfPurgeSquidServers( array( $this->getURL() ) );
+ }
+
+ /**
+ * Delete cached transformed files
+ */
+ function purgeThumbnails() {
+ global $wgUseSquid;
+ // Delete thumbnails
+ $files = $this->getThumbnails();
+ $dir = $this->getThumbPath();
+ $urls = array();
+ foreach ( $files as $file ) {
+ # Check that the base file name is part of the thumb name
+ # This is a basic sanity check to avoid erasing unrelated directories
+ if ( strpos( $file, $this->getName() ) !== false ) {
+ $url = $this->getThumbUrl( $file );
+ $urls[] = $url;
+ @unlink( "$dir/$file" );
+ }
+ }
+
+ // Purge the squid
+ if ( $wgUseSquid ) {
+ wfPurgeSquidServers( $urls );
+ }
+ }
+
+ /** purgeDescription inherited */
+ /** purgeEverything inherited */
+
+ /**
+ * Return the history of this file, line by line.
+ * starts with current version, then old versions.
+ * uses $this->historyLine to check which line to return:
+ * 0 return line for current version
+ * 1 query for old versions, return first one
+ * 2, ... return next old version from above query
+ *
+ * @public
+ */
+ function nextHistoryLine() {
+ $dbr = $this->repo->getSlaveDB();
+
+ if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
+ $this->historyRes = $dbr->select( 'image',
+ array(
+ '*',
+ "'' AS oi_archive_name"
+ ),
+ array( 'img_name' => $this->title->getDBkey() ),
+ __METHOD__
+ );
+ if ( 0 == $dbr->numRows( $this->historyRes ) ) {
+ $dbr->freeResult($this->historyRes);
+ $this->historyRes = null;
+ return FALSE;
+ }
+ } else if ( $this->historyLine == 1 ) {
+ $dbr->freeResult($this->historyRes);
+ $this->historyRes = $dbr->select( 'oldimage', '*',
+ array( 'oi_name' => $this->title->getDBkey() ),
+ __METHOD__,
+ array( 'ORDER BY' => 'oi_timestamp DESC' )
+ );
+ }
+ $this->historyLine ++;
+
+ return $dbr->fetchObject( $this->historyRes );
+ }
+
+ /**
+ * Reset the history pointer to the first element of the history
+ * @public
+ */
+ function resetHistory() {
+ $this->historyLine = 0;
+ if (!is_null($this->historyRes)) {
+ $this->repo->getSlaveDB()->freeResult($this->historyRes);
+ $this->historyRes = null;
+ }
+ }
+
+ /** getFullPath inherited */
+ /** getHashPath inherited */
+ /** getRel inherited */
+ /** getUrlRel inherited */
+ /** getArchiveRel inherited */
+ /** getThumbRel inherited */
+ /** getArchivePath inherited */
+ /** getThumbPath inherited */
+ /** getArchiveUrl inherited */
+ /** getThumbUrl inherited */
+ /** getArchiveVirtualUrl inherited */
+ /** getThumbVirtualUrl inherited */
+ /** isHashed inherited */
+
+ /**
+ * Upload a file and record it in the DB
+ * @param string $srcPath Source path or virtual URL
+ * @param string $comment Upload description
+ * @param string $pageText Text to use for the new description page, if a new description page is created
+ * @param integer $flags Flags for publish()
+ * @param array $props File properties, if known. This can be used to reduce the
+ * upload time when uploading virtual URLs for which the file info
+ * is already known
+ * @param string $timestamp Timestamp for img_timestamp, or false to use the current time
+ *
+ * @return FileRepoStatus object. On success, the value member contains the
+ * archive name, or an empty string if it was a new file.
+ */
+ function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false ) {
+ $this->lock();
+ $status = $this->publish( $srcPath, $flags );
+ if ( $status->ok ) {
+ if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp ) ) {
+ $status->fatal( 'filenotfound', $srcPath );
+ }
+ }
+ $this->unlock();
+ return $status;
+ }
+
+ /**
+ * Record a file upload in the upload log and the image table
+ * @deprecated use upload()
+ */
+ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
+ $watch = false, $timestamp = false )
+ {
+ $pageText = UploadForm::getInitialPageText( $desc, $license, $copyStatus, $source );
+ if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
+ return false;
+ }
+ if ( $watch ) {
+ global $wgUser;
+ $wgUser->addWatch( $this->getTitle() );
+ }
+ return true;
+
+ }
+
+ /**
+ * Record a file upload in the upload log and the image table
+ */
+ function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false )
+ {
+ global $wgUser;
+
+ $dbw = $this->repo->getMasterDB();
+
+ if ( !$props ) {
+ $props = $this->repo->getFileProps( $this->getVirtualUrl() );
+ }
+ $this->setProps( $props );
+
+ // Delete thumbnails and refresh the metadata cache
+ $this->purgeThumbnails();
+ $this->saveToCache();
+ wfPurgeSquidServers( array( $this->getURL() ) );
+
+ // Fail now if the file isn't there
+ if ( !$this->fileExists ) {
+ wfDebug( __METHOD__.": File ".$this->getPath()." went missing!\n" );
+ return false;
+ }
+
+ $reupload = false;
+ if ( $timestamp === false ) {
+ $timestamp = $dbw->timestamp();
+ }
+
+ # Test to see if the row exists using INSERT IGNORE
+ # This avoids race conditions by locking the row until the commit, and also
+ # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
+ $dbw->insert( 'image',
+ array(
+ 'img_name' => $this->getName(),
+ 'img_size'=> $this->size,
+ 'img_width' => intval( $this->width ),
+ 'img_height' => intval( $this->height ),
+ 'img_bits' => $this->bits,
+ 'img_media_type' => $this->media_type,
+ 'img_major_mime' => $this->major_mime,
+ 'img_minor_mime' => $this->minor_mime,
+ 'img_timestamp' => $timestamp,
+ 'img_description' => $comment,
+ 'img_user' => $wgUser->getID(),
+ 'img_user_text' => $wgUser->getName(),
+ 'img_metadata' => $this->metadata,
+ 'img_sha1' => $this->sha1
+ ),
+ __METHOD__,
+ 'IGNORE'
+ );
+
+ if( $dbw->affectedRows() == 0 ) {
+ $reupload = true;
+
+ # Collision, this is an update of a file
+ # Insert previous contents into oldimage
+ $dbw->insertSelect( 'oldimage', 'image',
+ array(
+ 'oi_name' => 'img_name',
+ 'oi_archive_name' => $dbw->addQuotes( $oldver ),
+ 'oi_size' => 'img_size',
+ 'oi_width' => 'img_width',
+ 'oi_height' => 'img_height',
+ 'oi_bits' => 'img_bits',
+ 'oi_timestamp' => 'img_timestamp',
+ 'oi_description' => 'img_description',
+ 'oi_user' => 'img_user',
+ 'oi_user_text' => 'img_user_text',
+ 'oi_metadata' => 'img_metadata',
+ 'oi_media_type' => 'img_media_type',
+ 'oi_major_mime' => 'img_major_mime',
+ 'oi_minor_mime' => 'img_minor_mime',
+ 'oi_sha1' => 'img_sha1',
+ ), array( 'img_name' => $this->getName() ), __METHOD__
+ );
+
+ # Update the current image row
+ $dbw->update( 'image',
+ array( /* SET */
+ 'img_size' => $this->size,
+ 'img_width' => intval( $this->width ),
+ 'img_height' => intval( $this->height ),
+ 'img_bits' => $this->bits,
+ 'img_media_type' => $this->media_type,
+ 'img_major_mime' => $this->major_mime,
+ 'img_minor_mime' => $this->minor_mime,
+ 'img_timestamp' => $timestamp,
+ 'img_description' => $comment,
+ 'img_user' => $wgUser->getID(),
+ 'img_user_text' => $wgUser->getName(),
+ 'img_metadata' => $this->metadata,
+ 'img_sha1' => $this->sha1
+ ), array( /* WHERE */
+ 'img_name' => $this->getName()
+ ), __METHOD__
+ );
+ } else {
+ # This is a new file
+ # Update the image count
+ $site_stats = $dbw->tableName( 'site_stats' );
+ $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
+ }
+
+ $descTitle = $this->getTitle();
+ $article = new Article( $descTitle );
+
+ # Add the log entry
+ $log = new LogPage( 'upload' );
+ $action = $reupload ? 'overwrite' : 'upload';
+ $log->addEntry( $action, $descTitle, $comment );
+
+ if( $descTitle->exists() ) {
+ # Create a null revision
+ $nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(), $log->getRcComment(), false );
+ $nullRevision->insertOn( $dbw );
+ $article->updateRevisionOn( $dbw, $nullRevision );
+
+ # Invalidate the cache for the description page
+ $descTitle->invalidateCache();
+ $descTitle->purgeSquid();
+ } else {
+ // New file; create the description page.
+ // There's already a log entry, so don't make a second RC entry
+ $article->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC );
+ }
+
+ # Hooks, hooks, the magic of hooks...
+ wfRunHooks( 'FileUpload', array( $this ) );
+
+ # Commit the transaction now, in case something goes wrong later
+ # The most important thing is that files don't get lost, especially archives
+ $dbw->immediateCommit();
+
+ # Invalidate cache for all pages using this file
+ $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
+ $update->doUpdate();
+
+ return true;
+ }
+
+ /**
+ * Move or copy a file to its public location. If a file exists at the
+ * destination, move it to an archive. Returns the archive name on success
+ * or an empty string if it was a new file, and a wikitext-formatted
+ * WikiError object on failure.
+ *
+ * The archive name should be passed through to recordUpload for database
+ * registration.
+ *
+ * @param string $sourcePath Local filesystem path to the source image
+ * @param integer $flags A bitwise combination of:
+ * File::DELETE_SOURCE Delete the source file, i.e. move
+ * rather than copy
+ * @return FileRepoStatus object. On success, the value member contains the
+ * archive name, or an empty string if it was a new file.
+ */
+ function publish( $srcPath, $flags = 0 ) {
+ $this->lock();
+ $dstRel = $this->getRel();
+ $archiveName = gmdate( 'YmdHis' ) . '!'. $this->getName();
+ $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
+ $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
+ $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
+ if ( $status->value == 'new' ) {
+ $status->value = '';
+ } else {
+ $status->value = $archiveName;
+ }
+ $this->unlock();
+ return $status;
+ }
+
+ /** getLinksTo inherited */
+ /** getExifData inherited */
+ /** isLocal inherited */
+ /** wasDeleted inherited */
+
+ /**
+ * Delete all versions of the file.
+ *
+ * Moves the files into an archive directory (or deletes them)
+ * and removes the database rows.
+ *
+ * Cache purging is done; logging is caller's responsibility.
+ *
+ * @param $reason
+ * @return FileRepoStatus object.
+ */
+ function delete( $reason ) {
+ $this->lock();
+ $batch = new LocalFileDeleteBatch( $this, $reason );
+ $batch->addCurrent();
+
+ # Get old version relative paths
+ $dbw = $this->repo->getMasterDB();
+ $result = $dbw->select( 'oldimage',
+ array( 'oi_archive_name' ),
+ array( 'oi_name' => $this->getName() ) );
+ while ( $row = $dbw->fetchObject( $result ) ) {
+ $batch->addOld( $row->oi_archive_name );
+ }
+ $status = $batch->execute();
+
+ if ( $status->ok ) {
+ // Update site_stats
+ $site_stats = $dbw->tableName( 'site_stats' );
+ $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
+ $this->purgeEverything();
+ }
+
+ $this->unlock();
+ return $status;
+ }
+
+ /**
+ * Delete an old version of the file.
+ *
+ * Moves the file into an archive directory (or deletes it)
+ * and removes the database row.
+ *
+ * Cache purging is done; logging is caller's responsibility.
+ *
+ * @param $reason
+ * @throws MWException or FSException on database or filestore failure
+ * @return FileRepoStatus object.
+ */
+ function deleteOld( $archiveName, $reason ) {
+ $this->lock();
+ $batch = new LocalFileDeleteBatch( $this, $reason );
+ $batch->addOld( $archiveName );
+ $status = $batch->execute();
+ $this->unlock();
+ if ( $status->ok ) {
+ $this->purgeDescription();
+ $this->purgeHistory();
+ }
+ return $status;
+ }
+
+ /**
+ * Restore all or specified deleted revisions to the given file.
+ * Permissions and logging are left to the caller.
+ *
+ * May throw database exceptions on error.
+ *
+ * @param $versions set of record ids of deleted items to restore,
+ * or empty to restore all revisions.
+ * @return FileRepoStatus
+ */
+ function restore( $versions = array(), $unsuppress = false ) {
+ $batch = new LocalFileRestoreBatch( $this );
+ if ( !$versions ) {
+ $batch->addAll();
+ } else {
+ $batch->addIds( $versions );
+ }
+ $status = $batch->execute();
+ if ( !$status->ok ) {
+ return $status;
+ }
+
+ $cleanupStatus = $batch->cleanup();
+ $cleanupStatus->successCount = 0;
+ $cleanupStatus->failCount = 0;
+ $status->merge( $cleanupStatus );
+ return $status;
+ }
+
+ /** isMultipage inherited */
+ /** pageCount inherited */
+ /** scaleHeight inherited */
+ /** getImageSize inherited */
+
+ /**
+ * Get the URL of the file description page.
+ */
+ function getDescriptionUrl() {
+ return $this->title->getLocalUrl();
+ }
+
+ /**
+ * Get the HTML text of the description page
+ * This is not used by ImagePage for local files, since (among other things)
+ * it skips the parser cache.
+ */
+ function getDescriptionText() {
+ global $wgParser;
+ $revision = Revision::newFromTitle( $this->title );
+ if ( !$revision ) return false;
+ $text = $revision->getText();
+ if ( !$text ) return false;
+ $html = $wgParser->parse( $text, new ParserOptions );
+ return $html;
+ }
+
+ function getTimestamp() {
+ $this->load();
+ return $this->timestamp;
+ }
+
+ function getSha1() {
+ $this->load();
+ // Initialise now if necessary
+ if ( $this->sha1 == '' && $this->fileExists ) {
+ $this->sha1 = File::sha1Base36( $this->getPath() );
+ if ( strval( $this->sha1 ) != '' ) {
+ $dbw = $this->repo->getMasterDB();
+ $dbw->update( 'image',
+ array( 'img_sha1' => $this->sha1 ),
+ array( 'img_name' => $this->getName() ),
+ __METHOD__ );
+ $this->saveToCache();
+ }
+ }
+
+ return $this->sha1;
+ }
+
+ /**
+ * Start a transaction and lock the image for update
+ * Increments a reference counter if the lock is already held
+ * @return boolean True if the image exists, false otherwise
+ */
+ function lock() {
+ $dbw = $this->repo->getMasterDB();
+ if ( !$this->locked ) {
+ $dbw->begin();
+ $this->locked++;
+ }
+ return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ );
+ }
+
+ /**
+ * Decrement the lock reference count. If the reference count is reduced to zero, commits
+ * the transaction and thereby releases the image lock.
+ */
+ function unlock() {
+ if ( $this->locked ) {
+ --$this->locked;
+ if ( !$this->locked ) {
+ $dbw = $this->repo->getMasterDB();
+ $dbw->commit();
+ }
+ }
+ }
+
+ /**
+ * Roll back the DB transaction and mark the image unlocked
+ */
+ function unlockAndRollback() {
+ $this->locked = false;
+ $dbw = $this->repo->getMasterDB();
+ $dbw->rollback();
+ }
+} // LocalFile class
+
+#------------------------------------------------------------------------------
+
+/**
+ * Backwards compatibility class
+ */
+class Image extends LocalFile {
+ function __construct( $title ) {
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ parent::__construct( $title, $repo );
+ }
+
+ /**
+ * Wrapper for wfFindFile(), for backwards-compatibility only
+ * Do not use in core code.
+ * @deprecated
+ */
+ static function newFromTitle( $title, $time = false ) {
+ $img = wfFindFile( $title, $time );
+ if ( !$img ) {
+ $img = wfLocalFile( $title );
+ }
+ return $img;
+ }
+
+ /**
+ * Wrapper for wfFindFile(), for backwards-compatibility only.
+ * Do not use in core code.
+ *
+ * @param string $name name of the image, used to create a title object using Title::makeTitleSafe
+ * @return image object or null if invalid title
+ * @deprecated
+ */
+ static function newFromName( $name ) {
+ $title = Title::makeTitleSafe( NS_IMAGE, $name );
+ if ( is_object( $title ) ) {
+ $img = wfFindFile( $title );
+ if ( !$img ) {
+ $img = wfLocalFile( $title );
+ }
+ return $img;
+ } else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Return the URL of an image, provided its name.
+ *
+ * Backwards-compatibility for extensions.
+ * Note that fromSharedDirectory will only use the shared path for files
+ * that actually exist there now, and will return local paths otherwise.
+ *
+ * @param string $name Name of the image, without the leading "Image:"
+ * @param boolean $fromSharedDirectory Should this be in $wgSharedUploadPath?
+ * @return string URL of $name image
+ * @deprecated
+ */
+ static function imageUrl( $name, $fromSharedDirectory = false ) {
+ $image = null;
+ if( $fromSharedDirectory ) {
+ $image = wfFindFile( $name );
+ }
+ if( !$image ) {
+ $image = wfLocalFile( $name );
+ }
+ return $image->getUrl();
+ }
+}
+
+#------------------------------------------------------------------------------
+
+/**
+ * Helper class for file deletion
+ */
+class LocalFileDeleteBatch {
+ var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch;
+ var $status;
+
+ function __construct( File $file, $reason = '' ) {
+ $this->file = $file;
+ $this->reason = $reason;
+ $this->status = $file->repo->newGood();
+ }
+
+ function addCurrent() {
+ $this->srcRels['.'] = $this->file->getRel();
+ }
+
+ function addOld( $oldName ) {
+ $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
+ $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
+ }
+
+ function getOldRels() {
+ if ( !isset( $this->srcRels['.'] ) ) {
+ $oldRels =& $this->srcRels;
+ $deleteCurrent = false;
+ } else {
+ $oldRels = $this->srcRels;
+ unset( $oldRels['.'] );
+ $deleteCurrent = true;
+ }
+ return array( $oldRels, $deleteCurrent );
+ }
+
+ /*protected*/ function getHashes() {
+ $hashes = array();
+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
+ if ( $deleteCurrent ) {
+ $hashes['.'] = $this->file->getSha1();
+ }
+ if ( count( $oldRels ) ) {
+ $dbw = $this->file->repo->getMasterDB();
+ $res = $dbw->select( 'oldimage', array( 'oi_archive_name', 'oi_sha1' ),
+ 'oi_archive_name IN(' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
+ __METHOD__ );
+ while ( $row = $dbw->fetchObject( $res ) ) {
+ if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
+ // Get the hash from the file
+ $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
+ $props = $this->file->repo->getFileProps( $oldUrl );
+ if ( $props['fileExists'] ) {
+ // Upgrade the oldimage row
+ $dbw->update( 'oldimage',
+ array( 'oi_sha1' => $props['sha1'] ),
+ array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
+ __METHOD__ );
+ $hashes[$row->oi_archive_name] = $props['sha1'];
+ } else {
+ $hashes[$row->oi_archive_name] = false;
+ }
+ } else {
+ $hashes[$row->oi_archive_name] = $row->oi_sha1;
+ }
+ }
+ }
+ $missing = array_diff_key( $this->srcRels, $hashes );
+ foreach ( $missing as $name => $rel ) {
+ $this->status->error( 'filedelete-old-unregistered', $name );
+ }
+ foreach ( $hashes as $name => $hash ) {
+ if ( !$hash ) {
+ $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
+ unset( $hashes[$name] );
+ }
+ }
+
+ return $hashes;
+ }
+
+ function doDBInserts() {
+ global $wgUser;
+ $dbw = $this->file->repo->getMasterDB();
+ $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
+ $encUserId = $dbw->addQuotes( $wgUser->getId() );
+ $encReason = $dbw->addQuotes( $this->reason );
+ $encGroup = $dbw->addQuotes( 'deleted' );
+ $ext = $this->file->getExtension();
+ $dotExt = $ext === '' ? '' : ".$ext";
+ $encExt = $dbw->addQuotes( $dotExt );
+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
+
+ if ( $deleteCurrent ) {
+ $where = array( 'img_name' => $this->file->getName() );
+ $dbw->insertSelect( 'filearchive', 'image',
+ array(
+ 'fa_storage_group' => $encGroup,
+ 'fa_storage_key' => "IF(img_sha1='', '', CONCAT(img_sha1,$encExt))",
+
+ 'fa_deleted_user' => $encUserId,
+ 'fa_deleted_timestamp' => $encTimestamp,
+ 'fa_deleted_reason' => $encReason,
+ 'fa_deleted' => 0,
+
+ 'fa_name' => 'img_name',
+ 'fa_archive_name' => 'NULL',
+ 'fa_size' => 'img_size',
+ 'fa_width' => 'img_width',
+ 'fa_height' => 'img_height',
+ 'fa_metadata' => 'img_metadata',
+ 'fa_bits' => 'img_bits',
+ 'fa_media_type' => 'img_media_type',
+ 'fa_major_mime' => 'img_major_mime',
+ 'fa_minor_mime' => 'img_minor_mime',
+ 'fa_description' => 'img_description',
+ 'fa_user' => 'img_user',
+ 'fa_user_text' => 'img_user_text',
+ 'fa_timestamp' => 'img_timestamp'
+ ), $where, __METHOD__ );
+ }
+
+ if ( count( $oldRels ) ) {
+ $where = array(
+ 'oi_name' => $this->file->getName(),
+ 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
+
+ $dbw->insertSelect( 'filearchive', 'oldimage',
+ array(
+ 'fa_storage_group' => $encGroup,
+ 'fa_storage_key' => "IF(oi_sha1='', '', CONCAT(oi_sha1,$encExt))",
+
+ 'fa_deleted_user' => $encUserId,
+ 'fa_deleted_timestamp' => $encTimestamp,
+ 'fa_deleted_reason' => $encReason,
+ 'fa_deleted' => 0,
+
+ 'fa_name' => 'oi_name',
+ 'fa_archive_name' => 'oi_archive_name',
+ 'fa_size' => 'oi_size',
+ 'fa_width' => 'oi_width',
+ 'fa_height' => 'oi_height',
+ 'fa_metadata' => 'oi_metadata',
+ 'fa_bits' => 'oi_bits',
+ 'fa_media_type' => 'oi_media_type',
+ 'fa_major_mime' => 'oi_major_mime',
+ 'fa_minor_mime' => 'oi_minor_mime',
+ 'fa_description' => 'oi_description',
+ 'fa_user' => 'oi_user',
+ 'fa_user_text' => 'oi_user_text',
+ 'fa_timestamp' => 'oi_timestamp'
+ ), $where, __METHOD__ );
+ }
+ }
+
+ function doDBDeletes() {
+ $dbw = $this->file->repo->getMasterDB();
+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
+ if ( $deleteCurrent ) {
+ $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
+ }
+ if ( count( $oldRels ) ) {
+ $dbw->delete( 'oldimage',
+ array(
+ 'oi_name' => $this->file->getName(),
+ 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')'
+ ), __METHOD__ );
+ }
+ }
+
+ /**
+ * Run the transaction
+ */
+ function execute() {
+ global $wgUser, $wgUseSquid;
+ wfProfileIn( __METHOD__ );
+
+ $this->file->lock();
+
+ // Prepare deletion batch
+ $hashes = $this->getHashes();
+ $this->deletionBatch = array();
+ $ext = $this->file->getExtension();
+ $dotExt = $ext === '' ? '' : ".$ext";
+ foreach ( $this->srcRels as $name => $srcRel ) {
+ // Skip files that have no hash (missing source)
+ if ( isset( $hashes[$name] ) ) {
+ $hash = $hashes[$name];
+ $key = $hash . $dotExt;
+ $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
+ $this->deletionBatch[$name] = array( $srcRel, $dstRel );
+ }
+ }
+
+ // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
+ // We acquire this lock by running the inserts now, before the file operations.
+ //
+ // This potentially has poor lock contention characteristics -- an alternative
+ // scheme would be to insert stub filearchive entries with no fa_name and commit
+ // them in a separate transaction, then run the file ops, then update the fa_name fields.
+ $this->doDBInserts();
+
+ // Execute the file deletion batch
+ $status = $this->file->repo->deleteBatch( $this->deletionBatch );
+ if ( !$status->isGood() ) {
+ $this->status->merge( $status );
+ }
+
+ if ( !$this->status->ok ) {
+ // Critical file deletion error
+ // Roll back inserts, release lock and abort
+ // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
+ $this->file->unlockAndRollback();
+ return $this->status;
+ }
+
+ // Purge squid
+ if ( $wgUseSquid ) {
+ $urls = array();
+ foreach ( $this->srcRels as $srcRel ) {
+ $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) );
+ $urls[] = $this->file->repo->getZoneUrl( 'public' ) . '/' . $urlRel;
+ }
+ SquidUpdate::purge( $urls );
+ }
+
+ // Delete image/oldimage rows
+ $this->doDBDeletes();
+
+ // Commit and return
+ $this->file->unlock();
+ wfProfileOut( __METHOD__ );
+ return $this->status;
+ }
+}
+
+#------------------------------------------------------------------------------
+
+/**
+ * Helper class for file undeletion
+ */
+class LocalFileRestoreBatch {
+ var $file, $cleanupBatch, $ids, $all, $unsuppress = false;
+
+ function __construct( File $file ) {
+ $this->file = $file;
+ $this->cleanupBatch = $this->ids = array();
+ $this->ids = array();
+ }
+
+ /**
+ * Add a file by ID
+ */
+ function addId( $fa_id ) {
+ $this->ids[] = $fa_id;
+ }
+
+ /**
+ * Add a whole lot of files by ID
+ */
+ function addIds( $ids ) {
+ $this->ids = array_merge( $this->ids, $ids );
+ }
+
+ /**
+ * Add all revisions of the file
+ */
+ function addAll() {
+ $this->all = true;
+ }
+
+ /**
+ * Run the transaction, except the cleanup batch.
+ * The cleanup batch should be run in a separate transaction, because it locks different
+ * rows and there's no need to keep the image row locked while it's acquiring those locks
+ * The caller may have its own transaction open.
+ * So we save the batch and let the caller call cleanup()
+ */
+ function execute() {
+ global $wgUser, $wgLang;
+ if ( !$this->all && !$this->ids ) {
+ // Do nothing
+ return $this->file->repo->newGood();
+ }
+
+ $exists = $this->file->lock();
+ $dbw = $this->file->repo->getMasterDB();
+ $status = $this->file->repo->newGood();
+
+ // Fetch all or selected archived revisions for the file,
+ // sorted from the most recent to the oldest.
+ $conditions = array( 'fa_name' => $this->file->getName() );
+ if( !$this->all ) {
+ $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
+ }
+
+ $result = $dbw->select( 'filearchive', '*',
+ $conditions,
+ __METHOD__,
+ array( 'ORDER BY' => 'fa_timestamp DESC' ) );
+
+ $idsPresent = array();
+ $storeBatch = array();
+ $insertBatch = array();
+ $insertCurrent = false;
+ $deleteIds = array();
+ $first = true;
+ $archiveNames = array();
+ while( $row = $dbw->fetchObject( $result ) ) {
+ $idsPresent[] = $row->fa_id;
+ if ( $this->unsuppress ) {
+ // Currently, fa_deleted flags fall off upon restore, lets be careful about this
+ } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) {
+ // Skip restoring file revisions that the user cannot restore
+ continue;
+ }
+ if ( $row->fa_name != $this->file->getName() ) {
+ $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
+ $status->failCount++;
+ continue;
+ }
+ if ( $row->fa_storage_key == '' ) {
+ // Revision was missing pre-deletion
+ $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
+ $status->failCount++;
+ continue;
+ }
+
+ $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
+ $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
+
+ $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) );
+ # Fix leading zero
+ if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
+ $sha1 = substr( $sha1, 1 );
+ }
+
+ if( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
+ || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
+ || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
+ || is_null( $row->fa_metadata ) ) {
+ // Refresh our metadata
+ // Required for a new current revision; nice for older ones too. :)
+ $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
+ } else {
+ $props = array(
+ 'minor_mime' => $row->fa_minor_mime,
+ 'major_mime' => $row->fa_major_mime,
+ 'media_type' => $row->fa_media_type,
+ 'metadata' => $row->fa_metadata );
+ }
+
+ if ( $first && !$exists ) {
+ // This revision will be published as the new current version
+ $destRel = $this->file->getRel();
+ $insertCurrent = array(
+ 'img_name' => $row->fa_name,
+ 'img_size' => $row->fa_size,
+ 'img_width' => $row->fa_width,
+ 'img_height' => $row->fa_height,
+ 'img_metadata' => $props['metadata'],
+ 'img_bits' => $row->fa_bits,
+ 'img_media_type' => $props['media_type'],
+ 'img_major_mime' => $props['major_mime'],
+ 'img_minor_mime' => $props['minor_mime'],
+ 'img_description' => $row->fa_description,
+ 'img_user' => $row->fa_user,
+ 'img_user_text' => $row->fa_user_text,
+ 'img_timestamp' => $row->fa_timestamp,
+ 'img_sha1' => $sha1);
+ } else {
+ $archiveName = $row->fa_archive_name;
+ if( $archiveName == '' ) {
+ // This was originally a current version; we
+ // have to devise a new archive name for it.
+ // Format is <timestamp of archiving>!<name>
+ $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
+ do {
+ $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
+ $timestamp++;
+ } while ( isset( $archiveNames[$archiveName] ) );
+ }
+ $archiveNames[$archiveName] = true;
+ $destRel = $this->file->getArchiveRel( $archiveName );
+ $insertBatch[] = array(
+ 'oi_name' => $row->fa_name,
+ 'oi_archive_name' => $archiveName,
+ 'oi_size' => $row->fa_size,
+ 'oi_width' => $row->fa_width,
+ 'oi_height' => $row->fa_height,
+ 'oi_bits' => $row->fa_bits,
+ 'oi_description' => $row->fa_description,
+ 'oi_user' => $row->fa_user,
+ 'oi_user_text' => $row->fa_user_text,
+ 'oi_timestamp' => $row->fa_timestamp,
+ 'oi_metadata' => $props['metadata'],
+ 'oi_media_type' => $props['media_type'],
+ 'oi_major_mime' => $props['major_mime'],
+ 'oi_minor_mime' => $props['minor_mime'],
+ 'oi_deleted' => $row->fa_deleted,
+ 'oi_sha1' => $sha1 );
+ }
+
+ $deleteIds[] = $row->fa_id;
+ $storeBatch[] = array( $deletedUrl, 'public', $destRel );
+ $this->cleanupBatch[] = $row->fa_storage_key;
+ $first = false;
+ }
+ unset( $result );
+
+ // Add a warning to the status object for missing IDs
+ $missingIds = array_diff( $this->ids, $idsPresent );
+ foreach ( $missingIds as $id ) {
+ $status->error( 'undelete-missing-filearchive', $id );
+ }
+
+ // Run the store batch
+ // Use the OVERWRITE_SAME flag to smooth over a common error
+ $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
+ $status->merge( $storeStatus );
+
+ if ( !$status->ok ) {
+ // Store batch returned a critical error -- this usually means nothing was stored
+ // Stop now and return an error
+ $this->file->unlock();
+ return $status;
+ }
+
+ // Run the DB updates
+ // Because we have locked the image row, key conflicts should be rare.
+ // If they do occur, we can roll back the transaction at this time with
+ // no data loss, but leaving unregistered files scattered throughout the
+ // public zone.
+ // This is not ideal, which is why it's important to lock the image row.
+ if ( $insertCurrent ) {
+ $dbw->insert( 'image', $insertCurrent, __METHOD__ );
+ }
+ if ( $insertBatch ) {
+ $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
+ }
+ if ( $deleteIds ) {
+ $dbw->delete( 'filearchive',
+ array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
+ __METHOD__ );
+ }
+
+ if( $status->successCount > 0 ) {
+ if( !$exists ) {
+ wfDebug( __METHOD__." restored {$status->successCount} items, creating a new current\n" );
+
+ // Update site_stats
+ $site_stats = $dbw->tableName( 'site_stats' );
+ $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
+
+ $this->file->purgeEverything();
+ } else {
+ wfDebug( __METHOD__." restored {$status->successCount} as archived versions\n" );
+ $this->file->purgeDescription();
+ $this->file->purgeHistory();
+ }
+ }
+ $this->file->unlock();
+ return $status;
+ }
+
+ /**
+ * Delete unused files in the deleted zone.
+ * This should be called from outside the transaction in which execute() was called.
+ */
+ function cleanup() {
+ if ( !$this->cleanupBatch ) {
+ return $this->file->repo->newGood();
+ }
+ $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
+ return $status;
+ }
+}
diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php
new file mode 100644
index 00000000..72f9e9a6
--- /dev/null
+++ b/includes/filerepo/LocalRepo.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * A repository that stores files in the local filesystem and registers them
+ * in the wiki's own database. This is the most commonly used repository class.
+ */
+class LocalRepo extends FSRepo {
+ var $fileFactory = array( 'LocalFile', 'newFromTitle' );
+ var $oldFileFactory = array( 'OldLocalFile', 'newFromTitle' );
+
+ function getSlaveDB() {
+ return wfGetDB( DB_SLAVE );
+ }
+
+ function getMasterDB() {
+ return wfGetDB( DB_MASTER );
+ }
+
+ function newFileFromRow( $row ) {
+ if ( isset( $row->img_name ) ) {
+ return LocalFile::newFromRow( $row, $this );
+ } elseif ( isset( $row->oi_name ) ) {
+ return OldLocalFile::newFromRow( $row, $this );
+ } else {
+ throw new MWException( __METHOD__.': invalid row' );
+ }
+ }
+
+ function newFromArchiveName( $title, $archiveName ) {
+ return OldLocalFile::newFromArchiveName( $title, $this, $archiveName );
+ }
+
+ /**
+ * Delete files in the deleted directory if they are not referenced in the
+ * filearchive table. This needs to be done in the repo because it needs to
+ * interleave database locks with file operations, which is potentially a
+ * remote operation.
+ * @return FileRepoStatus
+ */
+ function cleanupDeletedBatch( $storageKeys ) {
+ $root = $this->getZonePath( 'deleted' );
+ $dbw = $this->getMasterDB();
+ $status = $this->newGood();
+ $storageKeys = array_unique($storageKeys);
+ foreach ( $storageKeys as $key ) {
+ $hashPath = $this->getDeletedHashPath( $key );
+ $path = "$root/$hashPath$key";
+ $dbw->begin();
+ $inuse = $dbw->selectField( 'filearchive', '1',
+ array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ),
+ __METHOD__, array( 'FOR UPDATE' ) );
+ if ( !$inuse ) {
+ wfDebug( __METHOD__ . ": deleting $key\n" );
+ if ( !@unlink( $path ) ) {
+ $status->error( 'undelete-cleanup-error', $path );
+ $status->failCount++;
+ }
+ } else {
+ wfDebug( __METHOD__ . ": $key still in use\n" );
+ $status->successCount++;
+ }
+ $dbw->commit();
+ }
+ return $status;
+ }
+}
diff --git a/includes/filerepo/OldLocalFile.php b/includes/filerepo/OldLocalFile.php
new file mode 100644
index 00000000..850a8d8a
--- /dev/null
+++ b/includes/filerepo/OldLocalFile.php
@@ -0,0 +1,232 @@
+<?php
+
+/**
+ * Class to represent a file in the oldimage table
+ *
+ * @addtogroup FileRepo
+ */
+class OldLocalFile extends LocalFile {
+ var $requestedTime, $archive_name;
+
+ const CACHE_VERSION = 1;
+ const MAX_CACHE_ROWS = 20;
+
+ static function newFromTitle( $title, $repo, $time ) {
+ return new self( $title, $repo, $time, null );
+ }
+
+ static function newFromArchiveName( $title, $repo, $archiveName ) {
+ return new self( $title, $repo, null, $archiveName );
+ }
+
+ static function newFromRow( $row, $repo ) {
+ $title = Title::makeTitle( NS_IMAGE, $row->oi_name );
+ $file = new self( $title, $repo, null, $row->oi_archive_name );
+ $file->loadFromRow( $row, 'oi_' );
+ return $file;
+ }
+
+ /**
+ * @param Title $title
+ * @param FileRepo $repo
+ * @param string $time Timestamp or null to load by archive name
+ * @param string $archiveName Archive name or null to load by timestamp
+ */
+ function __construct( $title, $repo, $time, $archiveName ) {
+ parent::__construct( $title, $repo );
+ $this->requestedTime = $time;
+ $this->archive_name = $archiveName;
+ if ( is_null( $time ) && is_null( $archiveName ) ) {
+ throw new MWException( __METHOD__.': must specify at least one of $time or $archiveName' );
+ }
+ }
+
+ function getCacheKey() {
+ $hashedName = md5($this->getName());
+ return wfMemcKey( 'oldfile', $hashedName );
+ }
+
+ function getArchiveName() {
+ if ( !isset( $this->archive_name ) ) {
+ $this->load();
+ }
+ return $this->archive_name;
+ }
+
+ function isOld() {
+ return true;
+ }
+
+ /**
+ * Try to load file metadata from memcached. Returns true on success.
+ */
+ function loadFromCache() {
+ global $wgMemc;
+ wfProfileIn( __METHOD__ );
+ $this->dataLoaded = false;
+ $key = $this->getCacheKey();
+ if ( !$key ) {
+ return false;
+ }
+ $oldImages = $wgMemc->get( $key );
+
+ if ( isset( $oldImages['version'] ) && $oldImages['version'] == self::CACHE_VERSION ) {
+ unset( $oldImages['version'] );
+ $more = isset( $oldImages['more'] );
+ unset( $oldImages['more'] );
+ $found = false;
+ if ( is_null( $this->requestedTime ) ) {
+ foreach ( $oldImages as $timestamp => $info ) {
+ if ( $info['archive_name'] == $this->archive_name ) {
+ $found = true;
+ break;
+ }
+ }
+ } else {
+ krsort( $oldImages );
+ foreach ( $oldImages as $timestamp => $info ) {
+ if ( $timestamp <= $this->requestedTime ) {
+ $found = true;
+ break;
+ }
+ }
+ }
+ if ( $found ) {
+ wfDebug( "Pulling file metadata from cache key {$key}[{$timestamp}]\n" );
+ $this->dataLoaded = true;
+ $this->fileExists = true;
+ foreach ( $info as $name => $value ) {
+ $this->$name = $value;
+ }
+ } elseif ( $more ) {
+ wfDebug( "Cache key was truncated, oldimage row might be found in the database\n" );
+ } else {
+ wfDebug( "Image did not exist at the specified time.\n" );
+ $this->fileExists = false;
+ $this->dataLoaded = true;
+ }
+ }
+
+ if ( $this->dataLoaded ) {
+ wfIncrStats( 'image_cache_hit' );
+ } else {
+ wfIncrStats( 'image_cache_miss' );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $this->dataLoaded;
+ }
+
+ function saveToCache() {
+ // If a timestamp was specified, cache the entire history of the image (up to MAX_CACHE_ROWS).
+ if ( is_null( $this->requestedTime ) ) {
+ return;
+ }
+ // This is expensive, so we only do it if $wgMemc is real
+ global $wgMemc;
+ if ( $wgMemc instanceof FakeMemcachedClient ) {
+ return;
+ }
+ $key = $this->getCacheKey();
+ if ( !$key ) {
+ return;
+ }
+ wfProfileIn( __METHOD__ );
+
+ $dbr = $this->repo->getSlaveDB();
+ $res = $dbr->select( 'oldimage', $this->getCacheFields( 'oi_' ),
+ array( 'oi_name' => $this->getName() ), __METHOD__,
+ array(
+ 'LIMIT' => self::MAX_CACHE_ROWS + 1,
+ 'ORDER BY' => 'oi_timestamp DESC',
+ ));
+ $cache = array( 'version' => self::CACHE_VERSION );
+ $numRows = $dbr->numRows( $res );
+ if ( $numRows > self::MAX_CACHE_ROWS ) {
+ $cache['more'] = true;
+ $numRows--;
+ }
+ for ( $i = 0; $i < $numRows; $i++ ) {
+ $row = $dbr->fetchObject( $res );
+ $decoded = $this->decodeRow( $row, 'oi_' );
+ $cache[$row->oi_timestamp] = $decoded;
+ }
+ $dbr->freeResult( $res );
+ $wgMemc->set( $key, $cache, 7*86400 /* 1 week */ );
+ wfProfileOut( __METHOD__ );
+ }
+
+ function loadFromDB() {
+ wfProfileIn( __METHOD__ );
+ $this->dataLoaded = true;
+ $dbr = $this->repo->getSlaveDB();
+ $conds = array( 'oi_name' => $this->getName() );
+ if ( is_null( $this->requestedTime ) ) {
+ $conds['oi_archive_name'] = $this->archive_name;
+ } else {
+ $conds[] = 'oi_timestamp <= ' . $dbr->addQuotes( $this->requestedTime );
+ }
+ $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ),
+ $conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) );
+ if ( $row ) {
+ $this->loadFromRow( $row, 'oi_' );
+ } else {
+ $this->fileExists = false;
+ }
+ wfProfileOut( __METHOD__ );
+ }
+
+ function getCacheFields( $prefix = 'img_' ) {
+ $fields = parent::getCacheFields( $prefix );
+ $fields[] = $prefix . 'archive_name';
+
+ // XXX: Temporary hack before schema update
+ //$fields = array_diff( $fields, array(
+ // 'oi_media_type', 'oi_major_mime', 'oi_minor_mime', 'oi_metadata' ) );
+ return $fields;
+ }
+
+ function getRel() {
+ return 'archive/' . $this->getHashPath() . $this->getArchiveName();
+ }
+
+ function getUrlRel() {
+ return 'archive/' . $this->getHashPath() . urlencode( $this->getArchiveName() );
+ }
+
+ function upgradeRow() {
+ wfProfileIn( __METHOD__ );
+ $this->loadFromFile();
+
+ # Don't destroy file info of missing files
+ if ( !$this->fileExists ) {
+ wfDebug( __METHOD__.": file does not exist, aborting\n" );
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ $dbw = $this->repo->getMasterDB();
+ list( $major, $minor ) = self::splitMime( $this->mime );
+
+ wfDebug(__METHOD__.': upgrading '.$this->archive_name." to the current schema\n");
+ $dbw->update( 'oldimage',
+ array(
+ 'oi_width' => $this->width,
+ 'oi_height' => $this->height,
+ 'oi_bits' => $this->bits,
+ 'oi_media_type' => $this->media_type,
+ 'oi_major_mime' => $major,
+ 'oi_minor_mime' => $minor,
+ 'oi_metadata' => $this->metadata,
+ 'oi_sha1' => $this->sha1,
+ ), array(
+ 'oi_name' => $this->getName(),
+ 'oi_archive_name' => $this->archive_name ),
+ __METHOD__
+ );
+ wfProfileOut( __METHOD__ );
+ }
+}
+
+
+
diff --git a/includes/filerepo/README b/includes/filerepo/README
new file mode 100644
index 00000000..03cb8b3b
--- /dev/null
+++ b/includes/filerepo/README
@@ -0,0 +1,41 @@
+Some quick notes on the file/repository architecture.
+
+Functionality is, as always, driven by data model.
+
+* The repository object stores configuration information about a file storage
+ method.
+
+* The file object is a process-local cache of information about a particular
+ file.
+
+Thus the file object is the primary public entry point for obtaining information
+about files, since access via the file object can be cached, whereas access via
+the repository should not be cached.
+
+Functions which can act on any file specified in their parameters typically find
+their place either in the repository object, where reference to
+repository-specific configuration is needed, or in static members of File or
+FileRepo, where no such configuration is needed.
+
+File objects are generated by a factory function from the repository. The
+repository thus has full control over the behaviour of its subsidiary file
+class, since it can subclass the file class and override functionality at its
+whim. Thus there is no need for the File subclass to query its parent repository
+for information about repository-class-dependent behaviour -- the file subclass
+is generally fully aware of the static preferences of its repository. Limited
+exceptions can be made to this rule to permit sharing of functions, or perhaps
+even entire classes, between repositories.
+
+These rules alone still do lead to some ambiguity -- it may not be clear whether
+to implement some functionality in a repository function with a filename
+parameter, or in the file object itself.
+
+So we introduce the following rule: the file subclass is smarter than the
+repository subclass. The repository should in general provide a minimal API
+needed to access the storage backend efficiently.
+
+In particular, note that I have not implemented any database access in
+LocalRepo.php. LocalRepo provides only file access, and LocalFile provides
+database access and higher-level functions such as cache management.
+
+Tim Starling, June 2007
diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php
new file mode 100644
index 00000000..23d222af
--- /dev/null
+++ b/includes/filerepo/RepoGroup.php
@@ -0,0 +1,150 @@
+<?php
+
+/**
+ * Prioritized list of file repositories
+ * @addtogroup filerepo
+ */
+class RepoGroup {
+ var $localRepo, $foreignRepos, $reposInitialised = false;
+ var $localInfo, $foreignInfo;
+
+ protected static $instance;
+
+ /**
+ * Get a RepoGroup instance. At present only one instance of RepoGroup is
+ * needed in a MediaWiki invocation, this may change in the future.
+ */
+ static function singleton() {
+ if ( self::$instance ) {
+ return self::$instance;
+ }
+ global $wgLocalFileRepo, $wgForeignFileRepos;
+ self::$instance = new RepoGroup( $wgLocalFileRepo, $wgForeignFileRepos );
+ return self::$instance;
+ }
+
+ /**
+ * Destroy the singleton instance, so that a new one will be created next
+ * time singleton() is called.
+ */
+ static function destroySingleton() {
+ self::$instance = null;
+ }
+
+ /**
+ * Construct a group of file repositories.
+ * @param array $data Array of repository info arrays.
+ * Each info array is an associative array with the 'class' member
+ * giving the class name. The entire array is passed to the repository
+ * constructor as the first parameter.
+ */
+ function __construct( $localInfo, $foreignInfo ) {
+ $this->localInfo = $localInfo;
+ $this->foreignInfo = $foreignInfo;
+ }
+
+ /**
+ * Search repositories for an image.
+ * You can also use wfGetFile() to do this.
+ * @param mixed $title Title object or string
+ * @param mixed $time The 14-char timestamp before which the file should
+ * have been uploaded, or false for the current version
+ * @return File object or false if it is not found
+ */
+ function findFile( $title, $time = false ) {
+ if ( !$this->reposInitialised ) {
+ $this->initialiseRepos();
+ }
+
+ $image = $this->localRepo->findFile( $title, $time );
+ if ( $image ) {
+ return $image;
+ }
+ foreach ( $this->foreignRepos as $repo ) {
+ $image = $repo->findFile( $title, $time );
+ if ( $image ) {
+ return $image;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get the repo instance with a given key.
+ */
+ function getRepo( $index ) {
+ if ( !$this->reposInitialised ) {
+ $this->initialiseRepos();
+ }
+ if ( $index == 'local' ) {
+ return $this->localRepo;
+ } elseif ( isset( $this->foreignRepos[$index] ) ) {
+ return $this->foreignRepos[$index];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get the local repository, i.e. the one corresponding to the local image
+ * table. Files are typically uploaded to the local repository.
+ */
+ function getLocalRepo() {
+ return $this->getRepo( 'local' );
+ }
+
+ /**
+ * Initialise the $repos array
+ */
+ function initialiseRepos() {
+ if ( $this->reposInitialised ) {
+ return;
+ }
+ $this->reposInitialised = true;
+
+ $this->localRepo = $this->newRepo( $this->localInfo );
+ $this->foreignRepos = array();
+ foreach ( $this->foreignInfo as $key => $info ) {
+ $this->foreignRepos[$key] = $this->newRepo( $info );
+ }
+ }
+
+ /**
+ * Create a repo class based on an info structure
+ */
+ protected function newRepo( $info ) {
+ $class = $info['class'];
+ return new $class( $info );
+ }
+
+ /**
+ * Split a virtual URL into repo, zone and rel parts
+ * @return an array containing repo, zone and rel
+ */
+ function splitVirtualUrl( $url ) {
+ if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
+ throw new MWException( __METHOD__.': unknown protoocl' );
+ }
+
+ $bits = explode( '/', substr( $url, 9 ), 3 );
+ if ( count( $bits ) != 3 ) {
+ throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
+ }
+ return $bits;
+ }
+
+ function getFileProps( $fileName ) {
+ if ( FileRepo::isVirtualUrl( $fileName ) ) {
+ list( $repoName, /* $zone */, /* $rel */ ) = $this->splitVirtualUrl( $fileName );
+ if ( $repoName === '' ) {
+ $repoName = 'local';
+ }
+ $repo = $this->getRepo( $repoName );
+ return $repo->getFileProps( $fileName );
+ } else {
+ return File::getPropsFromPath( $fileName );
+ }
+ }
+}
+
+
diff --git a/includes/filerepo/UnregisteredLocalFile.php b/includes/filerepo/UnregisteredLocalFile.php
new file mode 100644
index 00000000..419c61f6
--- /dev/null
+++ b/includes/filerepo/UnregisteredLocalFile.php
@@ -0,0 +1,109 @@
+<?php
+
+/**
+ * A file object referring to either a standalone local file, or a file in a
+ * local repository with no database, for example an FSRepo repository.
+ *
+ * Read-only.
+ *
+ * TODO: Currently it doesn't really work in the repository role, there are
+ * lots of functions missing. It is used by the WebStore extension in the
+ * standalone role.
+ */
+class UnregisteredLocalFile extends File {
+ var $title, $path, $mime, $handler, $dims;
+
+ static function newFromPath( $path, $mime ) {
+ return new UnregisteredLocalFile( false, false, $path, $mime );
+ }
+
+ static function newFromTitle( $title, $repo ) {
+ return new UnregisteredLocalFile( $title, $repo, false, false );
+ }
+
+ function __construct( $title = false, $repo = false, $path = false, $mime = false ) {
+ if ( !( $title && $repo ) && !$path ) {
+ throw new MWException( __METHOD__.': not enough parameters, must specify title and repo, or a full path' );
+ }
+ if ( $title ) {
+ $this->title = $title;
+ $this->name = $repo->getNameFromTitle( $title );
+ } else {
+ $this->name = basename( $path );
+ $this->title = Title::makeTitleSafe( NS_IMAGE, $this->name );
+ }
+ $this->repo = $repo;
+ if ( $path ) {
+ $this->path = $path;
+ } else {
+ $this->path = $repo->getRootDirectory() . '/' . $repo->getHashPath( $this->name ) . $this->name;
+ }
+ if ( $mime ) {
+ $this->mime = $mime;
+ }
+ $this->dims = array();
+ }
+
+ function getPageDimensions( $page = 1 ) {
+ if ( !isset( $this->dims[$page] ) ) {
+ if ( !$this->getHandler() ) {
+ return false;
+ }
+ $this->dims[$page] = $this->handler->getPageDimensions( $this, $page );
+ }
+ return $this->dims[$page];
+ }
+
+ function getWidth( $page = 1 ) {
+ $dim = $this->getPageDimensions( $page );
+ return $dim['width'];
+ }
+
+ function getHeight( $page = 1 ) {
+ $dim = $this->getPageDimensions( $page );
+ return $dim['height'];
+ }
+
+ function getMimeType() {
+ if ( !isset( $this->mime ) ) {
+ $magic = MimeMagic::singleton();
+ $this->mime = $magic->guessMimeType( $this->path );
+ }
+ return $this->mime;
+ }
+
+ function getImageSize( $filename ) {
+ if ( !$this->getHandler() ) {
+ return false;
+ }
+ return $this->handler->getImageSize( $this, $this->getPath() );
+ }
+
+ function getMetadata() {
+ if ( !isset( $this->metadata ) ) {
+ if ( !$this->getHandler() ) {
+ $this->metadata = false;
+ } else {
+ $this->metadata = $this->handler->getMetadata( $this, $this->getPath() );
+ }
+ }
+ return $this->metadata;
+ }
+
+ function getURL() {
+ if ( $this->repo ) {
+ return $this->repo->getZoneUrl( 'public' ) . '/' . $this->repo->getHashPath( $this->name ) . urlencode( $this->name );
+ } else {
+ return false;
+ }
+ }
+
+ function getSize() {
+ if ( file_exists( $this->path ) ) {
+ return filesize( $this->path );
+ } else {
+ return false;
+ }
+ }
+}
+
diff --git a/includes/media/BMP.php b/includes/media/BMP.php
index 9917856a..2f451b0a 100644
--- a/includes/media/BMP.php
+++ b/includes/media/BMP.php
@@ -1,8 +1,8 @@
<?php
+
/**
- * Handler for Microsoft bitmap format (bmp). It inherits most of the methods
- * from ImageHandler, some of them had to be overriden cause gd does not
- * support this format.
+ * Handler for Microsoft's bitmap format; getimagesize() doesn't
+ * support these files
*
* @addtogroup Media
*/
@@ -26,6 +26,4 @@ class BmpHandler extends BitmapHandler {
$h = unpack( 'V' , $h );
return array( $w[1], $h[1] );
}
-}
-
-?>
+} \ No newline at end of file
diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php
index 3f3aabbf..ca82aab0 100644
--- a/includes/media/Bitmap.php
+++ b/includes/media/Bitmap.php
@@ -51,17 +51,20 @@ class BitmapHandler extends ImageHandler {
$srcWidth = $image->getWidth();
$srcHeight = $image->getHeight();
$mimeType = $image->getMimeType();
- $srcPath = $image->getImagePath();
+ $srcPath = $image->getPath();
$retval = 0;
wfDebug( __METHOD__.": creating {$physicalWidth}x{$physicalHeight} thumbnail at $dstPath\n" );
if ( $physicalWidth == $srcWidth && $physicalHeight == $srcHeight ) {
# normaliseParams (or the user) wants us to return the unscaled image
wfDebug( __METHOD__.": returning unscaled image\n" );
- return new ThumbnailImage( $image->getURL(), $clientWidth, $clientHeight, $srcPath );
+ return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath );
}
- if ( $wgUseImageMagick ) {
+ if ( !$dstPath ) {
+ // No output path available, client side scaling only
+ $scaler = 'client';
+ } elseif ( $wgUseImageMagick ) {
$scaler = 'im';
} elseif ( $wgCustomConvertCommand ) {
$scaler = 'custom';
@@ -74,11 +77,11 @@ class BitmapHandler extends ImageHandler {
if ( $scaler == 'client' ) {
# Client-side image scaling, use the source URL
# Using the destination URL in a TRANSFORM_LATER request would be incorrect
- return new ThumbnailImage( $image->getURL(), $clientWidth, $clientHeight, $srcPath );
+ return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath );
}
if ( $flags & self::TRANSFORM_LATER ) {
- return new ThumbnailImage( $dstUrl, $clientWidth, $clientHeight, $dstPath );
+ return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
}
if ( !wfMkdirParents( dirname( $dstPath ) ) ) {
@@ -164,9 +167,27 @@ class BitmapHandler extends ImageHandler {
$src_image = call_user_func( $loader, $srcPath );
$dst_image = imagecreatetruecolor( $physicalWidth, $physicalHeight );
- imagecopyresampled( $dst_image, $src_image,
- 0,0,0,0,
- $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) );
+
+ // Initialise the destination image to transparent instead of
+ // the default solid black, to support PNG and GIF transparency nicely
+ $background = imagecolorallocate( $dst_image, 0, 0, 0 );
+ imagecolortransparent( $dst_image, $background );
+ imagealphablending( $dst_image, false );
+
+ if( $colorStyle == 'palette' ) {
+ // Don't resample for paletted GIF images.
+ // It may just uglify them, and completely breaks transparency.
+ imagecopyresized( $dst_image, $src_image,
+ 0,0,0,0,
+ $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) );
+ } else {
+ imagecopyresampled( $dst_image, $src_image,
+ 0,0,0,0,
+ $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) );
+ }
+
+ imagesavealpha( $dst_image, true );
+
call_user_func( $saveType, $dst_image, $dstPath );
imagedestroy( $dst_image );
imagedestroy( $src_image );
@@ -180,7 +201,7 @@ class BitmapHandler extends ImageHandler {
wfHostname(), $retval, trim($err), $cmd ) );
return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
} else {
- return new ThumbnailImage( $dstUrl, $clientWidth, $clientHeight, $dstPath );
+ return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
}
}
@@ -231,6 +252,56 @@ class BitmapHandler extends ImageHandler {
return true;
}
+ /**
+ * Get a list of EXIF metadata items which should be displayed when
+ * the metadata table is collapsed.
+ *
+ * @return array of strings
+ * @access private
+ */
+ function visibleMetadataFields() {
+ $fields = array();
+ $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) );
+ foreach( $lines as $line ) {
+ $matches = array();
+ if( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
+ $fields[] = $matches[1];
+ }
+ }
+ $fields = array_map( 'strtolower', $fields );
+ return $fields;
+ }
+
+ function formatMetadata( $image ) {
+ $result = array(
+ 'visible' => array(),
+ 'collapsed' => array()
+ );
+ $metadata = $image->getMetadata();
+ if ( !$metadata ) {
+ return false;
+ }
+ $exif = unserialize( $metadata );
+ if ( !$exif ) {
+ return false;
+ }
+ unset( $exif['MEDIAWIKI_EXIF_VERSION'] );
+ $format = new FormatExif( $exif );
+
+ $formatted = $format->getFormattedData();
+ // Sort fields into visible and collapsed
+ $visibleFields = $this->visibleMetadataFields();
+ foreach ( $formatted as $name => $value ) {
+ $tag = strtolower( $name );
+ self::addMeta( $result,
+ in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
+ 'exif',
+ $tag,
+ $value
+ );
+ }
+ return $result;
+ }
}
-?>
+
diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php
index 3c053a0c..20e59d18 100644
--- a/includes/media/DjVu.php
+++ b/includes/media/DjVu.php
@@ -17,6 +17,13 @@ class DjVuHandler extends ImageHandler {
function mustRender() { return true; }
function isMultiPage() { return true; }
+ function getParamMap() {
+ return array(
+ 'img_width' => 'width',
+ 'img_page' => 'page',
+ );
+ }
+
function validateParam( $name, $value ) {
if ( in_array( $name, array( 'width', 'height', 'page' ) ) ) {
if ( $value <= 0 ) {
@@ -69,15 +76,14 @@ class DjVuHandler extends ImageHandler {
}
$width = $params['width'];
$height = $params['height'];
- $srcPath = $image->getImagePath();
+ $srcPath = $image->getPath();
$page = $params['page'];
- $pageCount = $this->pageCount( $image );
if ( $page > $this->pageCount( $image ) ) {
return new MediaTransformError( 'thumbnail_error', $width, $height, wfMsg( 'djvu_page_error' ) );
}
if ( $flags & self::TRANSFORM_LATER ) {
- return new ThumbnailImage( $dstUrl, $width, $height, $dstPath );
+ return new ThumbnailImage( $image, $dstUrl, $width, $height, $dstPath, $page );
}
if ( !wfMkdirParents( dirname( $dstPath ) ) ) {
@@ -104,7 +110,7 @@ class DjVuHandler extends ImageHandler {
wfHostname(), $retval, trim($err), $cmd ) );
return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
} else {
- return new ThumbnailImage( $dstUrl, $width, $height, $dstPath );
+ return new ThumbnailImage( $image, $dstUrl, $width, $height, $dstPath, $page );
}
}
@@ -203,4 +209,4 @@ class DjVuHandler extends ImageHandler {
}
}
-?>
+
diff --git a/includes/media/Generic.php b/includes/media/Generic.php
index 5254e0ea..c7ab7d81 100644
--- a/includes/media/Generic.php
+++ b/includes/media/Generic.php
@@ -36,6 +36,12 @@ abstract class MediaHandler {
return self::$handlers[$class];
}
+ /**
+ * Get an associative array mapping magic word IDs to parameter names.
+ * Will be used by the parser to identify parameters.
+ */
+ abstract function getParamMap();
+
/*
* Validate a thumbnail parameter at parse time.
* Return true to accept the parameter, and false to reject it.
@@ -126,20 +132,20 @@ abstract class MediaHandler {
/**
* True if the handled types can be transformed
*/
- function canRender() { return true; }
+ function canRender( $file ) { return true; }
/**
* True if handled types cannot be displayed directly in a browser
* but can be rendered
*/
- function mustRender() { return false; }
+ function mustRender( $file ) { return false; }
/**
* True if the type has multi-page capabilities
*/
- function isMultiPage() { return false; }
+ function isMultiPage( $file ) { return false; }
/**
* Page count for a multi-page document, false if unsupported or unknown
*/
- function pageCount() { return false; }
+ function pageCount( $file ) { return false; }
/**
* False if the handler is disabled for all files
*/
@@ -152,12 +158,102 @@ abstract class MediaHandler {
* Returns false if unknown or if the document is not multi-page.
*/
function getPageDimensions( $image, $page ) {
- $gis = $this->getImageSize( $image, $image->getImagePath() );
+ $gis = $this->getImageSize( $image, $image->getPath() );
return array(
'width' => $gis[0],
'height' => $gis[1]
);
}
+
+ /**
+ * Get an array structure that looks like this:
+ *
+ * array(
+ * 'visible' => array(
+ * 'Human-readable name' => 'Human readable value',
+ * ...
+ * ),
+ * 'collapsed' => array(
+ * 'Human-readable name' => 'Human readable value',
+ * ...
+ * )
+ * )
+ * The UI will format this into a table where the visible fields are always
+ * visible, and the collapsed fields are optionally visible.
+ *
+ * The function should return false if there is no metadata to display.
+ */
+
+ /**
+ * FIXME: I don't really like this interface, it's not very flexible
+ * I think the media handler should generate HTML instead. It can do
+ * all the formatting according to some standard. That makes it possible
+ * to do things like visual indication of grouped and chained streams
+ * in ogg container files.
+ */
+ function formatMetadata( $image, $metadata ) {
+ return false;
+ }
+
+ /**
+ * @fixme document this!
+ * 'value' thingy goes into a wikitext table; it used to be escaped but
+ * that was incompatible with previous practice of customized display
+ * with wikitext formatting via messages such as 'exif-model-value'.
+ * So the escaping is taken back out, but generally this seems a confusing
+ * interface.
+ */
+ protected static function addMeta( &$array, $visibility, $type, $id, $value, $param = false ) {
+ $array[$visibility][] = array(
+ 'id' => "$type-$id",
+ 'name' => wfMsg( "$type-$id", $param ),
+ 'value' => $value
+ );
+ }
+
+ function getShortDesc( $file ) {
+ global $wgLang;
+ $nbytes = '(' . wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $file->getSize() ) ) . ')';
+ return "$nbytes";
+ }
+
+ function getLongDesc( $file ) {
+ global $wgUser;
+ $sk = $wgUser->getSkin();
+ return wfMsg( 'file-info', $sk->formatSize( $file->getSize() ), $file->getMimeType() );
+ }
+
+ function getDimensionsString() {
+ return '';
+ }
+
+ /**
+ * Modify the parser object post-transform
+ */
+ function parserTransformHook( $parser, $file ) {}
+
+ /**
+ * Check for zero-sized thumbnails. These can be generated when
+ * no disk space is available or some other error occurs
+ *
+ * @param $dstPath The location of the suspect file
+ * @param $retval Return value of some shell process, file will be deleted if this is non-zero
+ * @return true if removed, false otherwise
+ */
+ function removeBadFile( $dstPath, $retval = 0 ) {
+ if( file_exists( $dstPath ) ) {
+ $thumbstat = stat( $dstPath );
+ if( $thumbstat['size'] == 0 || $retval != 0 ) {
+ wfDebugLog( 'thumbnail',
+ sprintf( 'Removing bad %d-byte thumbnail "%s"',
+ $thumbstat['size'], $dstPath ) );
+ unlink( $dstPath );
+ return true;
+ }
+ }
+ return false;
+ }
}
/**
@@ -166,6 +262,18 @@ abstract class MediaHandler {
* @addtogroup Media
*/
abstract class ImageHandler extends MediaHandler {
+ function canRender( $file ) {
+ if ( $file->getWidth() && $file->getHeight() ) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ function getParamMap() {
+ return array( 'img_width' => 'width' );
+ }
+
function validateParam( $name, $value ) {
if ( in_array( $name, array( 'width', 'height' ) ) ) {
if ( $value <= 0 ) {
@@ -181,8 +289,10 @@ abstract class ImageHandler extends MediaHandler {
function makeParamString( $params ) {
if ( isset( $params['physicalWidth'] ) ) {
$width = $params['physicalWidth'];
- } else {
+ } elseif ( isset( $params['width'] ) ) {
$width = $params['width'];
+ } else {
+ throw new MWException( 'No width specified to '.__METHOD__ );
}
# Removed for ProofreadPage
#$width = intval( $width );
@@ -218,7 +328,7 @@ abstract class ImageHandler extends MediaHandler {
$params['width'] = wfFitBoxWidth( $srcWidth, $srcHeight, $params['height'] );
}
}
- $params['height'] = Image::scaleHeight( $srcWidth, $srcHeight, $params['width'] );
+ $params['height'] = File::scaleHeight( $srcWidth, $srcHeight, $params['width'] );
if ( !$this->validateThumbParams( $params['width'], $params['height'], $srcWidth, $srcHeight, $mimeType ) ) {
return false;
}
@@ -252,7 +362,7 @@ abstract class ImageHandler extends MediaHandler {
return false;
}
- $height = Image::scaleHeight( $srcWidth, $srcHeight, $width );
+ $height = File::scaleHeight( $srcWidth, $srcHeight, $width );
return true;
}
@@ -261,30 +371,8 @@ abstract class ImageHandler extends MediaHandler {
return false;
}
$url = $script . '&' . wfArrayToCGI( $this->getScriptParams( $params ) );
- return new ThumbnailImage( $url, $params['width'], $params['height'] );
- }
-
- /**
- * Check for zero-sized thumbnails. These can be generated when
- * no disk space is available or some other error occurs
- *
- * @param $dstPath The location of the suspect file
- * @param $retval Return value of some shell process, file will be deleted if this is non-zero
- * @return true if removed, false otherwise
- */
- function removeBadFile( $dstPath, $retval = 0 ) {
- $removed = false;
- if( file_exists( $dstPath ) ) {
- $thumbstat = stat( $dstPath );
- if( $thumbstat['size'] == 0 || $retval != 0 ) {
- wfDebugLog( 'thumbnail',
- sprintf( 'Removing bad %d-byte thumbnail "%s"',
- $thumbstat['size'], $dstPath ) );
- unlink( $dstPath );
- return true;
- }
- }
- return false;
+ $page = isset( $params['page'] ) ? $params['page'] : false;
+ return new ThumbnailImage( $image, $url, $params['width'], $params['height'], $page );
}
function getImageSize( $image, $path ) {
@@ -293,6 +381,31 @@ abstract class ImageHandler extends MediaHandler {
wfRestoreWarnings();
return $gis;
}
+
+ function getShortDesc( $file ) {
+ global $wgLang;
+ $nbytes = '(' . wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $file->getSize() ) ) . ')';
+ $widthheight = wfMsgHtml( 'widthheight', $file->getWidth(), $file->getHeight() );
+
+ return "$widthheight ($nbytes)";
+ }
+
+ function getLongDesc( $file ) {
+ global $wgLang;
+ return wfMsgHtml('file-info-size', $file->getWidth(), $file->getHeight(),
+ $wgLang->formatSize( $file->getSize() ), $file->getMimeType() );
+ }
+
+ function getDimensionsString( $file ) {
+ $pages = $file->pageCount();
+ if ( $pages > 1 ) {
+ return wfMsg( 'widthheightpage', $file->getWidth(), $file->getHeight(), $pages );
+ } else {
+ return wfMsg( 'widthheight', $file->getWidth(), $file->getHeight() );
+ }
+ }
}
-?>
+
+
diff --git a/includes/media/SVG.php b/includes/media/SVG.php
index 5307e269..75d0ad3d 100644
--- a/includes/media/SVG.php
+++ b/includes/media/SVG.php
@@ -14,7 +14,7 @@ class SvgHandler extends ImageHandler {
}
}
- function mustRender() {
+ function mustRender( $file ) {
return true;
}
@@ -31,7 +31,7 @@ class SvgHandler extends ImageHandler {
$srcWidth = $image->getWidth( $params['page'] );
$srcHeight = $image->getHeight( $params['page'] );
$params['physicalWidth'] = $wgSVGMaxSize;
- $params['physicalHeight'] = Image::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize );
+ $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize );
}
return true;
}
@@ -46,12 +46,10 @@ class SvgHandler extends ImageHandler {
$clientHeight = $params['height'];
$physicalWidth = $params['physicalWidth'];
$physicalHeight = $params['physicalHeight'];
- $srcWidth = $image->getWidth();
- $srcHeight = $image->getHeight();
- $srcPath = $image->getImagePath();
+ $srcPath = $image->getPath();
if ( $flags & self::TRANSFORM_LATER ) {
- return new ThumbnailImage( $dstUrl, $clientWidth, $clientHeight, $dstPath );
+ return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
}
if ( !wfMkdirParents( dirname( $dstPath ) ) ) {
@@ -82,7 +80,7 @@ class SvgHandler extends ImageHandler {
wfHostname(), $retval, trim($err), $cmd ) );
return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
} else {
- return new ThumbnailImage( $dstUrl, $clientWidth, $clientHeight, $dstPath );
+ return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
}
}
@@ -93,5 +91,12 @@ class SvgHandler extends ImageHandler {
function getThumbType( $ext, $mime ) {
return array( 'png', 'image/png' );
}
+
+ function getLongDesc( $file ) {
+ global $wgLang;
+ return wfMsg( 'svg-long-desc', $file->getWidth(), $file->getHeight(),
+ $wgLang->formatSize( $file->getSize() ) );
+ }
}
-?>
+
+
diff --git a/includes/memcached-client.php b/includes/memcached-client.php
index 1f4bac00..2eddb908 100644
--- a/includes/memcached-client.php
+++ b/includes/memcached-client.php
@@ -152,7 +152,7 @@ class memcached
/**
* At how many bytes should we compress?
*
- * @var interger
+ * @var integer
* @access private
*/
var $_compress_threshold;
@@ -192,7 +192,7 @@ class memcached
/**
* Total # of bit buckets we have
*
- * @var interger
+ * @var integer
* @access private
*/
var $_bucketcount;
@@ -200,7 +200,7 @@ class memcached
/**
* # of total servers we have
*
- * @var interger
+ * @var integer
* @access private
*/
var $_active;
@@ -272,9 +272,9 @@ class memcached
* Adds a key/value to the memcache server if one isn't already set with
* that key
*
- * @param string $key Key to set with data
- * @param mixed $val Value to store
- * @param interger $exp (optional) Time to expire data at
+ * @param string $key Key to set with data
+ * @param mixed $val Value to store
+ * @param integer $exp (optional) Time to expire data at
*
* @return boolean
* @access public
@@ -291,7 +291,7 @@ class memcached
* Decriment a value stored on the memcache server
*
* @param string $key Key to decriment
- * @param interger $amt (optional) Amount to decriment
+ * @param integer $amt (optional) Amount to decriment
*
* @return mixed FALSE on failure, value on success
* @access public
@@ -308,7 +308,7 @@ class memcached
* Deletes a key from the server, optionally after $time
*
* @param string $key Key to delete
- * @param interger $time (optional) How long to wait before deleting
+ * @param integer $time (optional) How long to wait before deleting
*
* @return boolean TRUE on success, FALSE on failure
* @access public
@@ -506,9 +506,9 @@ class memcached
* Increments $key (optionally) by $amt
*
* @param string $key Key to increment
- * @param interger $amt (optional) amount to increment
+ * @param integer $amt (optional) amount to increment
*
- * @return interger New key value?
+ * @return integer New key value?
* @access public
*/
function incr ($key, $amt=1)
@@ -524,7 +524,7 @@ class memcached
*
* @param string $key Key to set value as
* @param mixed $value Value to store
- * @param interger $exp (optional) Experiation time
+ * @param integer $exp (optional) Experiation time
*
* @return boolean
* @access public
@@ -582,7 +582,7 @@ class memcached
*
* @param string $key Key to set value as
* @param mixed $value Value to set
- * @param interger $exp (optional) Experiation time
+ * @param integer $exp (optional) Experiation time
*
* @return boolean TRUE on success
* @access public
@@ -598,7 +598,7 @@ class memcached
/**
* Sets the compression threshold
*
- * @param interger $thresh Threshold to compress if larger than
+ * @param integer $thresh Threshold to compress if larger than
*
* @access public
*/
@@ -687,7 +687,7 @@ class memcached
/**
* Connects $sock to $host, timing out after $timeout
*
- * @param interger $sock Socket to connect
+ * @param integer $sock Socket to connect
* @param string $host Host:IP to connect to
*
* @return boolean
@@ -807,11 +807,11 @@ class memcached
// {{{ _hashfunc()
/**
- * Creates a hash interger based on the $key
+ * Creates a hash integer based on the $key
*
* @param string $key Key to hash
*
- * @return interger Hash value
+ * @return integer Hash value
* @access private
*/
function _hashfunc ($key)
@@ -830,9 +830,9 @@ class memcached
*
* @param string $cmd Command to perform
* @param string $key Key to perform it on
- * @param interger $amt Amount to adjust
+ * @param integer $amt Amount to adjust
*
- * @return interger New value of $key
+ * @return integer New value of $key
* @access private
*/
function _incrdecr ($cmd, $key, $amt=1)
@@ -929,7 +929,7 @@ class memcached
* @param string $cmd Command to perform
* @param string $key Key to act on
* @param mixed $val What we need to store
- * @param interger $exp When it should expire
+ * @param integer $exp When it should expire
*
* @return boolean
* @access private
@@ -1085,4 +1085,4 @@ class memcached
// vim: sts=3 sw=3 et
// }}}
-?>
+
diff --git a/includes/normal/CleanUpTest.php b/includes/normal/CleanUpTest.php
index cc6f0737..0ca45b3c 100644
--- a/includes/normal/CleanUpTest.php
+++ b/includes/normal/CleanUpTest.php
@@ -410,4 +410,4 @@ if( !$result->wasSuccessful() ) {
exit( -1 );
}
exit( 0 );
-?>
+
diff --git a/includes/normal/RandomTest.php b/includes/normal/RandomTest.php
index 9ccbc01d..aa491dbb 100644
--- a/includes/normal/RandomTest.php
+++ b/includes/normal/RandomTest.php
@@ -105,4 +105,4 @@ while( true ) {
$norm = '';
}
-?>
+
diff --git a/includes/normal/Utf8Test.php b/includes/normal/Utf8Test.php
index fc2e7776..8600d49d 100644
--- a/includes/normal/Utf8Test.php
+++ b/includes/normal/Utf8Test.php
@@ -150,4 +150,4 @@ function testLine( $test, $line, &$total, &$success, &$failed ) {
}
}
-?>
+
diff --git a/includes/normal/UtfNormal.php b/includes/normal/UtfNormal.php
index 43bbafd8..557b8e5e 100644
--- a/includes/normal/UtfNormal.php
+++ b/includes/normal/UtfNormal.php
@@ -29,59 +29,6 @@ $utfCanonicalDecomp = NULL;
global $utfCompatibilityDecomp;
$utfCompatibilityDecomp = NULL;
-define( 'UNICODE_HANGUL_FIRST', 0xac00 );
-define( 'UNICODE_HANGUL_LAST', 0xd7a3 );
-
-define( 'UNICODE_HANGUL_LBASE', 0x1100 );
-define( 'UNICODE_HANGUL_VBASE', 0x1161 );
-define( 'UNICODE_HANGUL_TBASE', 0x11a7 );
-
-define( 'UNICODE_HANGUL_LCOUNT', 19 );
-define( 'UNICODE_HANGUL_VCOUNT', 21 );
-define( 'UNICODE_HANGUL_TCOUNT', 28 );
-define( 'UNICODE_HANGUL_NCOUNT', UNICODE_HANGUL_VCOUNT * UNICODE_HANGUL_TCOUNT );
-
-define( 'UNICODE_HANGUL_LEND', UNICODE_HANGUL_LBASE + UNICODE_HANGUL_LCOUNT - 1 );
-define( 'UNICODE_HANGUL_VEND', UNICODE_HANGUL_VBASE + UNICODE_HANGUL_VCOUNT - 1 );
-define( 'UNICODE_HANGUL_TEND', UNICODE_HANGUL_TBASE + UNICODE_HANGUL_TCOUNT - 1 );
-
-define( 'UNICODE_SURROGATE_FIRST', 0xd800 );
-define( 'UNICODE_SURROGATE_LAST', 0xdfff );
-define( 'UNICODE_MAX', 0x10ffff );
-define( 'UNICODE_REPLACEMENT', 0xfffd );
-
-
-define( 'UTF8_HANGUL_FIRST', "\xea\xb0\x80" /*codepointToUtf8( UNICODE_HANGUL_FIRST )*/ );
-define( 'UTF8_HANGUL_LAST', "\xed\x9e\xa3" /*codepointToUtf8( UNICODE_HANGUL_LAST )*/ );
-
-define( 'UTF8_HANGUL_LBASE', "\xe1\x84\x80" /*codepointToUtf8( UNICODE_HANGUL_LBASE )*/ );
-define( 'UTF8_HANGUL_VBASE', "\xe1\x85\xa1" /*codepointToUtf8( UNICODE_HANGUL_VBASE )*/ );
-define( 'UTF8_HANGUL_TBASE', "\xe1\x86\xa7" /*codepointToUtf8( UNICODE_HANGUL_TBASE )*/ );
-
-define( 'UTF8_HANGUL_LEND', "\xe1\x84\x92" /*codepointToUtf8( UNICODE_HANGUL_LEND )*/ );
-define( 'UTF8_HANGUL_VEND', "\xe1\x85\xb5" /*codepointToUtf8( UNICODE_HANGUL_VEND )*/ );
-define( 'UTF8_HANGUL_TEND', "\xe1\x87\x82" /*codepointToUtf8( UNICODE_HANGUL_TEND )*/ );
-
-define( 'UTF8_SURROGATE_FIRST', "\xed\xa0\x80" /*codepointToUtf8( UNICODE_SURROGATE_FIRST )*/ );
-define( 'UTF8_SURROGATE_LAST', "\xed\xbf\xbf" /*codepointToUtf8( UNICODE_SURROGATE_LAST )*/ );
-define( 'UTF8_MAX', "\xf4\x8f\xbf\xbf" /*codepointToUtf8( UNICODE_MAX )*/ );
-define( 'UTF8_REPLACEMENT', "\xef\xbf\xbd" /*codepointToUtf8( UNICODE_REPLACEMENT )*/ );
-#define( 'UTF8_REPLACEMENT', '!' );
-
-define( 'UTF8_OVERLONG_A', "\xc1\xbf" );
-define( 'UTF8_OVERLONG_B', "\xe0\x9f\xbf" );
-define( 'UTF8_OVERLONG_C', "\xf0\x8f\xbf\xbf" );
-
-# These two ranges are illegal
-define( 'UTF8_FDD0', "\xef\xb7\x90" /*codepointToUtf8( 0xfdd0 )*/ );
-define( 'UTF8_FDEF', "\xef\xb7\xaf" /*codepointToUtf8( 0xfdef )*/ );
-define( 'UTF8_FFFE', "\xef\xbf\xbe" /*codepointToUtf8( 0xfffe )*/ );
-define( 'UTF8_FFFF', "\xef\xbf\xbf" /*codepointToUtf8( 0xffff )*/ );
-
-define( 'UTF8_HEAD', false );
-define( 'UTF8_TAIL', true );
-
-
/**
* For using the ICU wrapper
*/
@@ -804,4 +751,4 @@ class UtfNormal {
}
}
-?>
+
diff --git a/includes/normal/UtfNormalBench.php b/includes/normal/UtfNormalBench.php
index c394f4d8..d89b0eb5 100644
--- a/includes/normal/UtfNormalBench.php
+++ b/includes/normal/UtfNormalBench.php
@@ -108,4 +108,4 @@ function benchmarkForm( &$u, &$data, $form ) {
return $out;
}
-?>
+
diff --git a/includes/normal/UtfNormalGenerate.php b/includes/normal/UtfNormalGenerate.php
index 30f18675..83f3085e 100644
--- a/includes/normal/UtfNormalGenerate.php
+++ b/includes/normal/UtfNormalGenerate.php
@@ -231,4 +231,4 @@ function callbackCompat( $matches ) {
return $matches[1];
}
-?>
+
diff --git a/includes/normal/UtfNormalTest.php b/includes/normal/UtfNormalTest.php
index 6d0dce25..556cf11a 100644
--- a/includes/normal/UtfNormalTest.php
+++ b/includes/normal/UtfNormalTest.php
@@ -246,4 +246,4 @@ function testInvariant( &$u, $char, $desc, $reportFailure = false ) {
return $result;
}
-?>
+
diff --git a/includes/normal/UtfNormalUtil.php b/includes/normal/UtfNormalUtil.php
index 4ba05693..e68c6ec5 100644
--- a/includes/normal/UtfNormalUtil.php
+++ b/includes/normal/UtfNormalUtil.php
@@ -139,4 +139,4 @@ function escapeSingleString( $string ) {
));
}
-?>
+
diff --git a/includes/proxy_check.php b/includes/proxy_check.php
index 4c672760..a878a257 100644
--- a/includes/proxy_check.php
+++ b/includes/proxy_check.php
@@ -51,4 +51,4 @@ $output = escapeshellarg( $output );
#`echo $output >> /home/tstarling/open/proxy.log`;
-?>
+
diff --git a/includes/templates/NoLocalSettings.php b/includes/templates/NoLocalSettings.php
index e71dd396..9020c46e 100644
--- a/includes/templates/NoLocalSettings.php
+++ b/includes/templates/NoLocalSettings.php
@@ -7,9 +7,10 @@ if ( isset( $wgVersion ) ) {
}
# Set the path in case we hit a page such as /index.php/Main_Page
# Could use <base href> but then we have to worry about http[s]/port #/etc.
+$ext = strpos( $_SERVER['SCRIPT_NAME'], 'index.php5' ) === false ? 'php' : 'php5';
$path = '';
if( isset( $_SERVER['SCRIPT_NAME'] )) {
- $path = htmlspecialchars( preg_replace('/index.php/', '', $_SERVER['SCRIPT_NAME']) );
+ $path = htmlspecialchars( preg_replace('/index.php5?/', '', $_SERVER['SCRIPT_NAME']) );
}
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
@@ -39,7 +40,7 @@ if( isset( $_SERVER['SCRIPT_NAME'] )) {
if ( file_exists( 'config/LocalSettings.php' ) ) {
echo( 'To complete the installation, move <tt>config/LocalSettings.php</tt> to the parent directory.' );
} else {
- echo( "Please <a href=\"${path}config/index.php\" title='setup'> set up the wiki</a> first." );
+ echo( "Please <a href=\"${path}config/index.{$ext}\" title='setup'> set up the wiki</a> first." );
}
?>
diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php
index ccddfa66..127c30a0 100644
--- a/includes/templates/Userlogin.php
+++ b/includes/templates/Userlogin.php
@@ -28,11 +28,12 @@ class UserloginTemplate extends QuickTemplate {
<form name="userlogin" method="post" action="<?php $this->text('action') ?>">
<h2><?php $this->msg('login') ?></h2>
<p id="userloginlink"><?php $this->html('link') ?></p>
+ <?php $this->html('header'); /* pre-table point for form plugins... */ ?>
<div id="userloginprompt"><?php $this->msgWiki('loginprompt') ?></div>
<?php if( @$this->haveData( 'languages' ) ) { ?><div id="languagelinks"><p><?php $this->html( 'languages' ); ?></p></div><?php } ?>
<table>
<tr>
- <td align='right'><label for='wpName1'><?php $this->msg('yourname') ?>:</label></td>
+ <td align='right'><label for='wpName1'><?php $this->msg('yourname') ?></label></td>
<td align='left'>
<input type='text' class='loginText' name="wpName" id="wpName1"
tabindex="1"
@@ -40,11 +41,11 @@ class UserloginTemplate extends QuickTemplate {
</td>
</tr>
<tr>
- <td align='right'><label for='wpPassword1'><?php $this->msg('yourpassword') ?>:</label></td>
+ <td align='right'><label for='wpPassword1'><?php $this->msg('yourpassword') ?></label></td>
<td align='left'>
<input type='password' class='loginPassword' name="wpPassword" id="wpPassword1"
tabindex="2"
- value="<?php $this->text('password') ?>" size='20' />
+ value="" size='20' />
</td>
</tr>
<?php if( $this->data['usedomain'] ) {
@@ -54,7 +55,7 @@ class UserloginTemplate extends QuickTemplate {
}
?>
<tr>
- <td align='right'><?php $this->msg( 'yourdomainname' ) ?>:</td>
+ <td align='right'><?php $this->msg( 'yourdomainname' ) ?></td>
<td align='left'>
<select name="wpDomain" value="<?php $this->text( 'domain' ) ?>"
tabindex="3">
@@ -116,7 +117,7 @@ class UsercreateTemplate extends QuickTemplate {
<?php if( @$this->haveData( 'languages' ) ) { ?><div id="languagelinks"><p><?php $this->html( 'languages' ); ?></p></div><?php } ?>
<table>
<tr>
- <td align='right'><label for='wpName2'><?php $this->msg('yourname') ?>:</label></td>
+ <td align='right'><label for='wpName2'><?php $this->msg('yourname') ?></label></td>
<td align='left'>
<input type='text' class='loginText' name="wpName" id="wpName2"
tabindex="1"
@@ -124,11 +125,11 @@ class UsercreateTemplate extends QuickTemplate {
</td>
</tr>
<tr>
- <td align='right'><label for='wpPassword2'><?php $this->msg('yourpassword') ?>:</label></td>
+ <td align='right'><label for='wpPassword2'><?php $this->msg('yourpassword') ?></label></td>
<td align='left'>
<input type='password' class='loginPassword' name="wpPassword" id="wpPassword2"
tabindex="2"
- value="<?php $this->text('password') ?>" size='20' />
+ value="" size='20' />
</td>
</tr>
<?php if( $this->data['usedomain'] ) {
@@ -138,7 +139,7 @@ class UsercreateTemplate extends QuickTemplate {
}
?>
<tr>
- <td align='right'><?php $this->msg( 'yourdomainname' ) ?>:</td>
+ <td align='right'><?php $this->msg( 'yourdomainname' ) ?></td>
<td align='left'>
<select name="wpDomain" value="<?php $this->text( 'domain' ) ?>"
tabindex="3">
@@ -148,31 +149,37 @@ class UsercreateTemplate extends QuickTemplate {
</tr>
<?php } ?>
<tr>
- <td align='right'><label for='wpRetype'><?php $this->msg('yourpasswordagain') ?>:</label></td>
+ <td align='right'><label for='wpRetype'><?php $this->msg('yourpasswordagain') ?></label></td>
<td align='left'>
<input type='password' class='loginPassword' name="wpRetype" id="wpRetype"
tabindex="4"
- value="<?php $this->text('retype') ?>"
+ value=""
size='20' />
</td>
</tr>
<tr>
<?php if( $this->data['useemail'] ) { ?>
- <td align='right'><label for='wpEmail'><?php $this->msg('youremail') ?></label></td>
+ <td align='right' style='vertical-align: top'><label for='wpEmail'><?php $this->msg('youremail') ?></label></td>
<td align='left'>
<input type='text' class='loginText' name="wpEmail" id="wpEmail"
tabindex="5"
value="<?php $this->text('email') ?>" size='20' />
+ <div class="prefsectiontip">
+ <?php $this->msgWiki('prefs-help-email'); ?>
+ </div>
</td>
<?php } ?>
<?php if( $this->data['userealname'] ) { ?>
</tr>
<tr>
- <td align='right'><label for='wpRealName'><?php $this->msg('yourrealname') ?></label></td>
+ <td align='right' style='vertical-align: top'><label for='wpRealName'><?php $this->msg('yourrealname') ?></label></td>
<td align='left'>
<input type='text' class='loginText' name="wpRealName" id="wpRealName"
tabindex="6"
value="<?php $this->text('realname') ?>" size='20' />
+ <div class="prefsectiontip">
+ <?php $this->msgWiki('prefs-help-realname'); ?>
+ </div>
</td>
<?php } ?>
</tr>
@@ -200,24 +207,6 @@ class UsercreateTemplate extends QuickTemplate {
</td>
</tr>
</table>
- <?php
-
- if ($this->data['userealname'] || $this->data['useemail']) {
- echo '<div id="login-sectiontip">';
- if ( $this->data['useemail'] ) {
- echo '<div>';
- $this->msgHtml('prefs-help-email');
- echo '</div>';
- }
- if ( $this->data['userealname'] ) {
- echo '<div>';
- $this->msgHtml('prefs-help-realname');
- echo '</div>';
- }
- echo '</div>';
- }
-
- ?>
<?php if( @$this->haveData( 'uselang' ) ) { ?><input type="hidden" name="uselang" value="<?php $this->text( 'uselang' ); ?>" /><?php } ?>
</form>
</div>