summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2009-02-22 13:37:51 +0100
committerPierre Schmitz <pierre@archlinux.de>2009-02-22 13:37:51 +0100
commitb9b85843572bf283f48285001e276ba7e61b63f6 (patch)
tree4c6f4571552ada9ccfb4030481dcf77308f8b254 /includes
parentd9a20acc4e789cca747ad360d87ee3f3e7aa58c1 (diff)
updated to MediaWiki 1.14.0
Diffstat (limited to 'includes')
-rw-r--r--includes/AjaxFunctions.php126
-rw-r--r--includes/AjaxResponse.php6
-rw-r--r--includes/Article.php1829
-rw-r--r--includes/AuthPlugin.php82
-rw-r--r--includes/AutoLoader.php982
-rw-r--r--includes/Autopromote.php9
-rw-r--r--includes/BagOStuff.php13
-rw-r--r--includes/Block.php480
-rw-r--r--includes/Category.php41
-rw-r--r--includes/CategoryPage.php82
-rw-r--r--includes/Categoryfinder.php10
-rw-r--r--includes/ChangesFeed.php7
-rw-r--r--includes/ChangesList.php381
-rw-r--r--includes/Credits.php300
-rw-r--r--includes/DatabaseFunctions.php26
-rw-r--r--includes/DefaultSettings.php563
-rw-r--r--includes/Defines.php20
-rw-r--r--includes/EditPage.php958
-rw-r--r--includes/Exception.php34
-rw-r--r--includes/Exif.php25
-rw-r--r--includes/Export.php288
-rw-r--r--includes/ExternalStore.php47
-rw-r--r--includes/ExternalStoreDB.php11
-rw-r--r--includes/FakeTitle.php23
-rw-r--r--includes/Feed.php44
-rw-r--r--includes/FeedUtils.php17
-rw-r--r--includes/FileDeleteForm.php66
-rw-r--r--includes/FileRevertForm.php6
-rw-r--r--includes/FileStore.php27
-rw-r--r--includes/FormOptions.php4
-rw-r--r--includes/GlobalFunctions.php445
-rw-r--r--includes/HTMLCacheUpdate.php28
-rw-r--r--includes/HTMLFileCache.php116
-rw-r--r--includes/HistoryBlob.php444
-rw-r--r--includes/HttpFunctions.php86
-rw-r--r--includes/IEContentAnalyzer.php7
-rw-r--r--includes/IP.php53
-rw-r--r--includes/ImageFunctions.php100
-rw-r--r--includes/ImageGallery.php2
-rw-r--r--includes/ImagePage.php395
-rw-r--r--includes/ImageQueryPage.php6
-rw-r--r--includes/Import.php1133
-rw-r--r--includes/Interwiki.php207
-rw-r--r--includes/JobQueue.php2
-rw-r--r--includes/Licenses.php2
-rw-r--r--includes/LinkBatch.php2
-rw-r--r--includes/LinkCache.php70
-rw-r--r--includes/Linker.php783
-rw-r--r--includes/LinksUpdate.php73
-rw-r--r--includes/LogEventsList.php468
-rw-r--r--includes/LogPage.php140
-rw-r--r--includes/MagicWord.php8
-rw-r--r--includes/Math.php48
-rw-r--r--includes/MediaTransformOutput.php20
-rw-r--r--includes/MessageCache.php93
-rw-r--r--includes/Metadata.php528
-rw-r--r--includes/MimeMagic.php90
-rw-r--r--includes/Namespace.php14
-rw-r--r--includes/ObjectCache.php23
-rw-r--r--includes/OutputPage.php575
-rw-r--r--includes/PageHistory.php374
-rw-r--r--includes/PageQueryPage.php6
-rw-r--r--includes/Pager.php82
-rw-r--r--includes/PrefixSearch.php19
-rw-r--r--includes/Profiler.php5
-rw-r--r--includes/ProtectionForm.php409
-rw-r--r--includes/ProxyTools.php58
-rw-r--r--includes/QueryPage.php23
-rw-r--r--includes/RawPage.php62
-rw-r--r--includes/RecentChange.php327
-rw-r--r--includes/RefreshLinksJob.php84
-rw-r--r--includes/Revision.php174
-rw-r--r--includes/Sanitizer.php121
-rw-r--r--includes/SearchEngine.php138
-rw-r--r--includes/SearchMySQL.php16
-rw-r--r--includes/SearchOracle.php14
-rw-r--r--includes/SearchPostgres.php10
-rw-r--r--includes/Setup.php42
-rw-r--r--includes/SiteConfiguration.php341
-rw-r--r--includes/SiteStats.php88
-rw-r--r--includes/Skin.php483
-rw-r--r--includes/SkinTemplate.php242
-rw-r--r--includes/SpecialPage.php33
-rw-r--r--includes/SquidUpdate.php2
-rw-r--r--includes/StringUtils.php99
-rw-r--r--includes/StubObject.php2
-rw-r--r--includes/Title.php1366
-rw-r--r--includes/TitleArray.php81
-rw-r--r--includes/UploadBase.php867
-rw-r--r--includes/UploadFromStash.php58
-rw-r--r--includes/UploadFromUpload.php20
-rw-r--r--includes/UploadFromUrl.php92
-rw-r--r--includes/User.php1059
-rw-r--r--includes/UserArray.php4
-rw-r--r--includes/UserMailer.php57
-rw-r--r--includes/WatchedItem.php37
-rw-r--r--includes/WatchlistEditor.php76
-rw-r--r--includes/WebRequest.php52
-rw-r--r--includes/WebResponse.php54
-rw-r--r--includes/WebStart.php55
-rw-r--r--includes/Wiki.php139
-rw-r--r--includes/WikiError.php3
-rw-r--r--includes/Xml.php70
-rw-r--r--includes/XmlFunctions.php24
-rw-r--r--includes/XmlTypeCheck.php7
-rw-r--r--includes/ZhConversion.php196
-rw-r--r--includes/api/ApiBase.php148
-rw-r--r--includes/api/ApiBlock.php22
-rw-r--r--includes/api/ApiDelete.php46
-rw-r--r--includes/api/ApiDisabled.php72
-rw-r--r--includes/api/ApiEditPage.php39
-rw-r--r--includes/api/ApiEmailUser.php14
-rw-r--r--includes/api/ApiExpandTemplates.php17
-rw-r--r--includes/api/ApiFormatBase.php10
-rw-r--r--includes/api/ApiFormatJson.php7
-rw-r--r--includes/api/ApiFormatJson_json.php76
-rw-r--r--includes/api/ApiFormatWddx.php58
-rw-r--r--includes/api/ApiFormatXml.php31
-rw-r--r--includes/api/ApiFormatYaml_spyc.php1115
-rw-r--r--includes/api/ApiLogin.php135
-rw-r--r--includes/api/ApiLogout.php5
-rw-r--r--includes/api/ApiMain.php72
-rw-r--r--includes/api/ApiMove.php51
-rw-r--r--includes/api/ApiPageSet.php54
-rw-r--r--includes/api/ApiParamInfo.php16
-rw-r--r--includes/api/ApiParse.php60
-rw-r--r--includes/api/ApiPatrol.php99
-rw-r--r--includes/api/ApiProtect.php83
-rw-r--r--includes/api/ApiPurge.php106
-rw-r--r--includes/api/ApiQuery.php29
-rw-r--r--includes/api/ApiQueryAllCategories.php23
-rw-r--r--includes/api/ApiQueryAllLinks.php29
-rw-r--r--includes/api/ApiQueryAllUsers.php4
-rw-r--r--includes/api/ApiQueryAllimages.php18
-rw-r--r--includes/api/ApiQueryAllpages.php50
-rw-r--r--includes/api/ApiQueryBacklinks.php56
-rw-r--r--includes/api/ApiQueryBase.php48
-rw-r--r--includes/api/ApiQueryBlocks.php42
-rw-r--r--includes/api/ApiQueryCategories.php35
-rw-r--r--includes/api/ApiQueryCategoryInfo.php21
-rw-r--r--includes/api/ApiQueryCategoryMembers.php36
-rw-r--r--includes/api/ApiQueryDeletedrevs.php4
-rw-r--r--includes/api/ApiQueryDisabled.php72
-rw-r--r--includes/api/ApiQueryDuplicateFiles.php164
-rw-r--r--includes/api/ApiQueryExtLinksUsage.php6
-rw-r--r--includes/api/ApiQueryImageInfo.php30
-rw-r--r--includes/api/ApiQueryImages.php8
-rw-r--r--includes/api/ApiQueryInfo.php149
-rw-r--r--includes/api/ApiQueryLangLinks.php4
-rw-r--r--includes/api/ApiQueryLinks.php10
-rw-r--r--includes/api/ApiQueryLogEvents.php103
-rw-r--r--includes/api/ApiQueryRandom.php13
-rw-r--r--includes/api/ApiQueryRecentChanges.php129
-rw-r--r--includes/api/ApiQueryRevisions.php90
-rw-r--r--includes/api/ApiQuerySearch.php37
-rw-r--r--includes/api/ApiQuerySiteinfo.php82
-rw-r--r--includes/api/ApiQueryUserContributions.php39
-rw-r--r--includes/api/ApiQueryUserInfo.php10
-rw-r--r--includes/api/ApiQueryUsers.php39
-rw-r--r--includes/api/ApiQueryWatchlist.php51
-rw-r--r--includes/api/ApiQueryWatchlistRaw.php179
-rw-r--r--includes/api/ApiResult.php17
-rw-r--r--includes/api/ApiRollback.php27
-rw-r--r--includes/api/ApiUnblock.php8
-rw-r--r--includes/api/ApiUndelete.php6
-rw-r--r--includes/api/ApiWatch.php99
-rw-r--r--includes/db/Database.php308
-rw-r--r--includes/db/DatabaseMssql.php59
-rw-r--r--includes/db/DatabaseOracle.php14
-rw-r--r--includes/db/DatabasePostgres.php89
-rw-r--r--includes/db/DatabaseSqlite.php43
-rw-r--r--includes/db/LBFactory_Multi.php14
-rw-r--r--includes/db/LoadBalancer.php77
-rw-r--r--includes/db/LoadMonitor.php3
-rw-r--r--includes/diff/Diff.php580
-rw-r--r--includes/diff/DifferenceEngine.php (renamed from includes/DifferenceEngine.php)666
-rw-r--r--includes/diff/HTMLDiff.php1005
-rw-r--r--includes/diff/Nodes.php439
-rw-r--r--includes/filerepo/ArchivedFile.php44
-rw-r--r--includes/filerepo/FSRepo.php10
-rw-r--r--includes/filerepo/File.php26
-rw-r--r--includes/filerepo/FileCache.php156
-rw-r--r--includes/filerepo/FileRepo.php80
-rw-r--r--includes/filerepo/ForeignAPIFile.php83
-rw-r--r--includes/filerepo/ForeignAPIRepo.php77
-rw-r--r--includes/filerepo/ForeignDBFile.php2
-rw-r--r--includes/filerepo/Image.php2
-rw-r--r--includes/filerepo/LocalFile.php99
-rw-r--r--includes/filerepo/LocalRepo.php7
-rw-r--r--includes/filerepo/OldLocalFile.php3
-rw-r--r--includes/filerepo/RepoGroup.php24
-rw-r--r--includes/filerepo/UnregisteredLocalFile.php2
-rw-r--r--includes/media/BMP.php9
-rw-r--r--includes/media/Bitmap.php36
-rw-r--r--includes/media/Bitmap_ClientOnly.php15
-rw-r--r--includes/media/Generic.php15
-rw-r--r--includes/media/SVG.php40
-rw-r--r--includes/memcached-client.php6
-rw-r--r--includes/mime.types1
-rw-r--r--includes/parser/CoreLinkFunctions.php47
-rw-r--r--includes/parser/CoreParserFunctions.php75
-rw-r--r--includes/parser/LinkHolderArray.php438
-rw-r--r--includes/parser/Parser.php1471
-rw-r--r--includes/parser/ParserCache.php4
-rw-r--r--includes/parser/ParserOptions.php16
-rw-r--r--includes/parser/ParserOutput.php63
-rw-r--r--includes/parser/Parser_DiffTest.php34
-rw-r--r--includes/parser/Parser_LinkHooks.php315
-rw-r--r--includes/parser/Parser_OldPP.php4944
-rw-r--r--includes/parser/Preprocessor_DOM.php41
-rw-r--r--includes/parser/Preprocessor_Hash.php37
-rw-r--r--includes/specials/SpecialAllmessages.php85
-rw-r--r--includes/specials/SpecialAllpages.php717
-rw-r--r--includes/specials/SpecialBlockip.php195
-rw-r--r--includes/specials/SpecialBooksources.php56
-rw-r--r--includes/specials/SpecialCategories.php4
-rw-r--r--includes/specials/SpecialConfirmemail.php22
-rw-r--r--includes/specials/SpecialContributions.php682
-rw-r--r--includes/specials/SpecialDeletedContributions.php369
-rw-r--r--includes/specials/SpecialDisambiguations.php8
-rw-r--r--includes/specials/SpecialEmailuser.php142
-rw-r--r--includes/specials/SpecialExport.php38
-rw-r--r--includes/specials/SpecialFileDuplicateSearch.php6
-rw-r--r--includes/specials/SpecialFilepath.php4
-rw-r--r--includes/specials/SpecialImport.php1202
-rw-r--r--includes/specials/SpecialIpblocklist.php118
-rw-r--r--includes/specials/SpecialLinkSearch.php185
-rw-r--r--includes/specials/SpecialListUserRestrictions.php161
-rw-r--r--includes/specials/SpecialListfiles.php (renamed from includes/specials/SpecialImagelist.php)51
-rw-r--r--includes/specials/SpecialListgrouprights.php25
-rw-r--r--includes/specials/SpecialListredirects.php3
-rw-r--r--includes/specials/SpecialListusers.php68
-rw-r--r--includes/specials/SpecialLockdb.php2
-rw-r--r--includes/specials/SpecialLog.php22
-rw-r--r--includes/specials/SpecialLonelypages.php7
-rw-r--r--includes/specials/SpecialMIMEsearch.php2
-rw-r--r--includes/specials/SpecialMergeHistory.php22
-rw-r--r--includes/specials/SpecialMostcategories.php4
-rw-r--r--includes/specials/SpecialMostimages.php2
-rw-r--r--includes/specials/SpecialMostlinkedtemplates.php19
-rw-r--r--includes/specials/SpecialMovepage.php136
-rw-r--r--includes/specials/SpecialNewimages.php127
-rw-r--r--includes/specials/SpecialNewpages.php57
-rw-r--r--includes/specials/SpecialPreferences.php461
-rw-r--r--includes/specials/SpecialPrefixindex.php139
-rw-r--r--includes/specials/SpecialProtectedpages.php32
-rw-r--r--includes/specials/SpecialProtectedtitles.php13
-rw-r--r--includes/specials/SpecialRandompage.php22
-rw-r--r--includes/specials/SpecialRecentchanges.php263
-rw-r--r--includes/specials/SpecialRecentchangeslinked.php30
-rw-r--r--includes/specials/SpecialRemoveRestrictions.php60
-rw-r--r--includes/specials/SpecialResetpass.php178
-rw-r--r--includes/specials/SpecialRestrictUser.php189
-rw-r--r--includes/specials/SpecialRevisiondelete.php72
-rw-r--r--includes/specials/SpecialSearch.php984
-rw-r--r--includes/specials/SpecialSpecialpages.php2
-rw-r--r--includes/specials/SpecialStatistics.php277
-rw-r--r--includes/specials/SpecialUncategorizedimages.php2
-rw-r--r--includes/specials/SpecialUndelete.php197
-rw-r--r--includes/specials/SpecialUnusedimages.php2
-rw-r--r--includes/specials/SpecialUpload.php69
-rw-r--r--includes/specials/SpecialUserlogin.php249
-rw-r--r--includes/specials/SpecialUserlogout.php2
-rw-r--r--includes/specials/SpecialUserrights.php88
-rw-r--r--includes/specials/SpecialVersion.php74
-rw-r--r--includes/specials/SpecialWantedfiles.php90
-rw-r--r--includes/specials/SpecialWantedtemplates.php110
-rw-r--r--includes/specials/SpecialWatchlist.php266
-rw-r--r--includes/specials/SpecialWhatlinkshere.php20
-rw-r--r--includes/templates/NoLocalSettings.php29
-rw-r--r--includes/templates/PHP4.php100
-rw-r--r--includes/templates/Userlogin.php14
-rw-r--r--includes/zhtable/simpphrases.manual18
-rw-r--r--includes/zhtable/toCN.manual73
-rw-r--r--includes/zhtable/toHK.manual80
-rw-r--r--includes/zhtable/toSG.manual2
-rw-r--r--includes/zhtable/toTW.manual15
-rw-r--r--includes/zhtable/tradphrases.manual7
-rw-r--r--includes/zhtable/tradphrases_exclude.manual2
279 files changed, 26138 insertions, 18295 deletions
diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php
index 9daca9e5..1a9adbca 100644
--- a/includes/AjaxFunctions.php
+++ b/includes/AjaxFunctions.php
@@ -14,7 +14,8 @@ if( !defined( 'MEDIAWIKI' ) ) {
* Modified function from http://pure-essence.net/stuff/code/utf8RawUrlDecode.phps
*
* @param $source String escaped with Javascript's escape() function
- * @param $iconv_to String destination character set will be used as second paramether in the iconv function. Default is UTF-8.
+ * @param $iconv_to String destination character set will be used as second parameter
+ * in the iconv function. Default is UTF-8.
* @return string
*/
function js_unescape($source, $iconv_to = 'UTF-8') {
@@ -72,91 +73,6 @@ function code2utf($num){
return '';
}
-define( 'AJAX_SEARCH_VERSION', 2 ); //AJAX search cache version
-
-function wfSajaxSearch( $term ) {
- global $wgContLang, $wgUser, $wgCapitalLinks, $wgMemc;
- $limit = 16;
- $sk = $wgUser->getSkin();
- $output = '';
-
- $term = trim( $term );
- $term = $wgContLang->checkTitleEncoding( $wgContLang->recodeInput( js_unescape( $term ) ) );
- if ( $wgCapitalLinks )
- $term = $wgContLang->ucfirst( $term );
- $term_title = Title::newFromText( $term );
-
- $memckey = $term_title ? wfMemcKey( 'ajaxsearch', md5( $term_title->getFullText() ) ) : wfMemcKey( 'ajaxsearch', md5( $term ) );
- $cached = $wgMemc->get($memckey);
- if( is_array( $cached ) && $cached['version'] == AJAX_SEARCH_VERSION ) {
- $response = new AjaxResponse( $cached['html'] );
- $response->setCacheDuration( 30*60 );
- return $response;
- }
-
- $r = $more = '';
- $canSearch = true;
-
- $results = PrefixSearch::titleSearch( $term, $limit + 1 );
- foreach( array_slice( $results, 0, $limit ) as $titleText ) {
- $r .= '<li>' . $sk->makeKnownLink( $titleText ) . "</li>\n";
- }
-
- // Hack to check for specials
- if( $results ) {
- $t = Title::newFromText( $results[0] );
- if( $t && $t->getNamespace() == NS_SPECIAL ) {
- $canSearch = false;
- if( count( $results ) > $limit ) {
- $more = '<i>' .
- $sk->makeKnownLinkObj(
- SpecialPage::getTitleFor( 'Specialpages' ),
- wfMsgHtml( 'moredotdotdot' ) ) .
- '</i>';
- }
- } else {
- if( count( $results ) > $limit ) {
- $more = '<i>' .
- $sk->makeKnownLinkObj(
- SpecialPage::getTitleFor( "Allpages", $term ),
- wfMsgHtml( 'moredotdotdot' ) ) .
- '</i>';
- }
- }
- }
-
- $valid = (bool) $term_title;
- $term_url = urlencode( $term );
- $term_normalized = $valid ? $term_title->getFullText() : $term;
- $term_display = htmlspecialchars( $term );
- $subtitlemsg = ( $valid ? 'searchsubtitle' : 'searchsubtitleinvalid' );
- $subtitle = wfMsgExt( $subtitlemsg, array( 'parse' ), wfEscapeWikiText( $term_normalized ) );
- $html = '<div id="searchTargetHide"><a onclick="Searching_Hide_Results();">'
- . wfMsgHtml( 'hideresults' ) . '</a></div>'
- . '<h1 class="firstHeading">'.wfMsgHtml('search')
- . '</h1><div id="contentSub">'. $subtitle . '</div>';
- if( $canSearch ) {
- $html .= '<ul><li>'
- . $sk->makeKnownLink( $wgContLang->specialPage( 'Search' ),
- wfMsgHtml( 'searchcontaining', $term_display ),
- "search={$term_url}&fulltext=Search" )
- . '</li><li>' . $sk->makeKnownLink( $wgContLang->specialPage( 'Search' ),
- wfMsgHtml( 'searchnamed', $term_display ) ,
- "search={$term_url}&go=Go" )
- . "</li></ul>";
- }
- if( $r ) {
- $html .= "<h2>" . wfMsgHtml( 'articletitles', $term_display ) . "</h2>"
- . '<ul>' .$r .'</ul>' . $more;
- }
-
- $wgMemc->set( $memckey, array( 'version' => AJAX_SEARCH_VERSION, 'html' => $html ), 30 * 60 );
-
- $response = new AjaxResponse( $html );
- $response->setCacheDuration( 30*60 );
- return $response;
-}
-
/**
* Called for AJAX watch/unwatch requests.
* @param $pagename Prefixed title string for page to watch/unwatch
@@ -189,20 +105,54 @@ function wfAjaxWatch($pagename = "", $watch = "") {
if(!$watching) {
$dbw = wfGetDB(DB_MASTER);
$dbw->begin();
- $article->doWatch();
+ $ok = $article->doWatch();
$dbw->commit();
}
} else {
if($watching) {
$dbw = wfGetDB(DB_MASTER);
$dbw->begin();
- $article->doUnwatch();
+ $ok = $article->doUnwatch();
$dbw->commit();
}
}
+ // Something stopped the change
+ if( isset($ok) && !$ok ) {
+ return '<err#>';
+ }
if( $watch ) {
return '<w#>'.wfMsgExt( 'addedwatchtext', array( 'parse' ), $title->getPrefixedText() );
} else {
return '<u#>'.wfMsgExt( 'removedwatchtext', array( 'parse' ), $title->getPrefixedText() );
}
}
+
+/**
+ * Called in some places (currently just extensions)
+ * to get the thumbnail URL for a given file at a given resolution.
+ */
+function wfAjaxGetThumbnailUrl( $file, $width, $height ) {
+ $file = wfFindFile( $file );
+
+ if ( !$file || !$file->exists() )
+ return null;
+
+ $url = $file->getThumbnail( $width, $height )->url;
+
+ return $url;
+}
+
+/**
+ * Called in some places (currently just extensions)
+ * to get the URL for a given file.
+ */
+function wfAjaxGetFileUrl( $file ) {
+ $file = wfFindFile( $file );
+
+ if ( !$file || !$file->exists() )
+ return null;
+
+ $url = $file->getUrl();
+
+ return $url;
+} \ No newline at end of file
diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php
index c79e928b..63468a14 100644
--- a/includes/AjaxResponse.php
+++ b/includes/AjaxResponse.php
@@ -9,7 +9,9 @@ if( !defined( 'MEDIAWIKI' ) ) {
}
/**
- * @todo document
+ * Handle responses for Ajax requests (send headers, print
+ * content, that sort of thing)
+ *
* @ingroup Ajax
*/
class AjaxResponse {
@@ -20,7 +22,7 @@ class AjaxResponse {
/** HTTP header Content-Type */
private $mContentType;
- /** @todo document */
+ /** Disables output. Can be set by calling $AjaxResponse->disable() */
private $mDisabled;
/** Date for the HTTP header Last-modified */
diff --git a/includes/Article.php b/includes/Article.php
index 4d8277bb..3d9c2147 100644
--- a/includes/Article.php
+++ b/includes/Article.php
@@ -16,27 +16,30 @@ class Article {
/**@{{
* @private
*/
- var $mComment; //!<
- var $mContent; //!<
- var $mContentLoaded; //!<
- var $mCounter; //!<
- var $mForUpdate; //!<
- var $mGoodAdjustment; //!<
- var $mLatest; //!<
- var $mMinorEdit; //!<
- var $mOldId; //!<
- var $mRedirectedFrom; //!<
- var $mRedirectUrl; //!<
- var $mRevIdFetched; //!<
- var $mRevision; //!<
- var $mTimestamp; //!<
- var $mTitle; //!<
- var $mTotalAdjustment; //!<
- var $mTouched; //!<
- var $mUser; //!<
- var $mUserText; //!<
- var $mRedirectTarget; //!<
- var $mIsRedirect;
+ var $mComment = ''; //!<
+ var $mContent; //!<
+ var $mContentLoaded = false; //!<
+ var $mCounter = -1; //!< Not loaded
+ var $mCurID = -1; //!< Not loaded
+ var $mDataLoaded = false; //!<
+ var $mForUpdate = false; //!<
+ var $mGoodAdjustment = 0; //!<
+ var $mIsRedirect = false; //!<
+ var $mLatest = false; //!<
+ var $mMinorEdit; //!<
+ var $mOldId; //!<
+ var $mPreparedEdit = false; //!< Title object if set
+ var $mRedirectedFrom = null; //!< Title object if set
+ var $mRedirectTarget = null; //!< Title object if set
+ var $mRedirectUrl = false; //!<
+ var $mRevIdFetched = 0; //!<
+ var $mRevision; //!<
+ var $mTimestamp = ''; //!<
+ var $mTitle; //!<
+ var $mTotalAdjustment = 0; //!<
+ var $mTouched = '19700101000000'; //!<
+ var $mUser = -1; //!< Not loaded
+ var $mUserText = ''; //!<
/**@}}*/
/**
@@ -44,10 +47,18 @@ class Article {
* @param $title Reference to a Title object.
* @param $oldId Integer revision ID, null to fetch from request, zero for current
*/
- function __construct( Title $title, $oldId = null ) {
+ public function __construct( Title $title, $oldId = null ) {
$this->mTitle =& $title;
$this->mOldId = $oldId;
- $this->clear();
+ }
+
+ /**
+ * Constructor from an article article
+ * @param $id The article ID to load
+ */
+ public static function newFromID( $id ) {
+ $t = Title::newFromID( $id );
+ return $t == null ? null : new Article( $t );
}
/**
@@ -55,7 +66,7 @@ class Article {
* from another page on the wiki.
* @param $from Title object.
*/
- function setRedirectedFrom( $from ) {
+ public function setRedirectedFrom( $from ) {
$this->mRedirectedFrom = $from;
}
@@ -67,22 +78,20 @@ class Article {
* @return mixed Title object, or null if this page is not a redirect
*/
public function getRedirectTarget() {
- if(!$this->mTitle || !$this->mTitle->isRedirect())
+ if( !$this->mTitle || !$this->mTitle->isRedirect() )
return null;
- if(!is_null($this->mRedirectTarget))
+ if( !is_null($this->mRedirectTarget) )
return $this->mRedirectTarget;
-
# Query the redirect table
- $dbr = wfGetDB(DB_SLAVE);
- $res = $dbr->select('redirect',
- array('rd_namespace', 'rd_title'),
- array('rd_from' => $this->getID()),
- __METHOD__
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'redirect',
+ array('rd_namespace', 'rd_title'),
+ array('rd_from' => $this->getID()),
+ __METHOD__
);
- $row = $dbr->fetchObject($res);
- if($row)
+ if( $row = $dbr->fetchObject($res) ) {
return $this->mRedirectTarget = Title::makeTitle($row->rd_namespace, $row->rd_title);
-
+ }
# This page doesn't have an entry in the redirect table
return $this->mRedirectTarget = $this->insertRedirect();
}
@@ -94,15 +103,19 @@ class Article {
* @return Title object
*/
public function insertRedirect() {
- $retval = Title::newFromRedirect($this->getContent());
- if(!$retval)
+ $retval = Title::newFromRedirect( $this->getContent() );
+ if( !$retval ) {
return null;
- $dbw = wfGetDB(DB_MASTER);
- $dbw->replace('redirect', array('rd_from'), array(
+ }
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->replace( 'redirect', array('rd_from'),
+ array(
'rd_from' => $this->getID(),
'rd_namespace' => $retval->getNamespace(),
'rd_title' => $retval->getDBKey()
- ), __METHOD__);
+ ),
+ __METHOD__
+ );
return $retval;
}
@@ -113,9 +126,9 @@ class Article {
*/
public function followRedirect() {
$text = $this->getContent();
- return self::followRedirectText( $text );
+ return $this->followRedirectText( $text );
}
-
+
/**
* Get the Title object this text redirects to
*
@@ -131,7 +144,6 @@ class Article {
//
// This can be hard to reverse and may produce loops,
// so they may be disabled in the site configuration.
-
$source = $this->mTitle->getFullURL( 'redirect=no' );
return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) );
}
@@ -142,7 +154,6 @@ class Article {
// the rest of the page we're on.
//
// This can be hard to reverse, so they may be disabled.
-
if( $rt->isSpecial( 'Userlogout' ) ) {
// rolleyes
} else {
@@ -159,15 +170,15 @@ class Article {
/**
* get the title object of the article
*/
- function getTitle() {
+ public function getTitle() {
return $this->mTitle;
}
/**
- * Clear the object
- * @private
- */
- function clear() {
+ * Clear the object
+ * @private
+ */
+ public function clear() {
$this->mDataLoaded = false;
$this->mContentLoaded = false;
@@ -190,30 +201,27 @@ class Article {
* Note that getContent/loadContent do not follow redirects anymore.
* If you need to fetch redirectable content easily, try
* the shortcut in Article::followContent()
- * FIXME
- * @todo There are still side-effects in this!
- * In general, you should use the Revision class, not Article,
- * to fetch text for purposes other than page views.
*
* @return Return the text of this revision
*/
- function getContent() {
- global $wgUser, $wgOut, $wgMessageCache;
-
+ public function getContent() {
+ global $wgUser, $wgContLang, $wgOut, $wgMessageCache;
wfProfileIn( __METHOD__ );
-
- if ( 0 == $this->getID() ) {
- wfProfileOut( __METHOD__ );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
-
- if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
- $wgMessageCache->loadAllMessages();
- $ret = wfMsgWeirdKey ( $this->mTitle->getText() ) ;
+ if( $this->getID() === 0 ) {
+ # If this is a MediaWiki:x message, then load the messages
+ # and return the message value for x.
+ if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ # If this is a system message, get the default text.
+ list( $message, $lang ) = $wgMessageCache->figureMessage( $wgContLang->lcfirst( $this->mTitle->getText() ) );
+ $wgMessageCache->loadAllMessages( $lang );
+ $text = wfMsgGetKey( $message, false, $lang, false );
+ if( wfEmptyMsg( $message, $text ) )
+ $text = '';
} else {
- $ret = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' );
+ $text = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' );
}
-
- return "<div class='noarticletext'>\n$ret\n</div>";
+ wfProfileOut( __METHOD__ );
+ return $text;
} else {
$this->loadContent();
wfProfileOut( __METHOD__ );
@@ -233,7 +241,7 @@ class Article {
* @return string text of the requested section
* @deprecated
*/
- function getSection($text,$section) {
+ public function getSection( $text, $section ) {
global $wgParser;
return $wgParser->getSection( $text, $section );
}
@@ -242,8 +250,8 @@ class Article {
* @return int The oldid of the article that is to be shown, 0 for the
* current revision
*/
- function getOldID() {
- if ( is_null( $this->mOldId ) ) {
+ public function getOldID() {
+ if( is_null( $this->mOldId ) ) {
$this->mOldId = $this->getOldIDFromRequest();
}
return $this->mOldId;
@@ -254,32 +262,27 @@ class Article {
*
* @return int The old id for the request
*/
- function getOldIDFromRequest() {
+ public function getOldIDFromRequest() {
global $wgRequest;
$this->mRedirectUrl = false;
$oldid = $wgRequest->getVal( 'oldid' );
- if ( isset( $oldid ) ) {
+ if( isset( $oldid ) ) {
$oldid = intval( $oldid );
- if ( $wgRequest->getVal( 'direction' ) == 'next' ) {
+ if( $wgRequest->getVal( 'direction' ) == 'next' ) {
$nextid = $this->mTitle->getNextRevisionID( $oldid );
- if ( $nextid ) {
+ if( $nextid ) {
$oldid = $nextid;
} else {
$this->mRedirectUrl = $this->mTitle->getFullURL( 'redirect=no' );
}
- } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) {
+ } elseif( $wgRequest->getVal( 'direction' ) == 'prev' ) {
$previd = $this->mTitle->getPreviousRevisionID( $oldid );
- if ( $previd ) {
+ if( $previd ) {
$oldid = $previd;
- } else {
- # TODO
}
}
- # unused:
- # $lastid = $oldid;
}
-
- if ( !$oldid ) {
+ if( !$oldid ) {
$oldid = 0;
}
return $oldid;
@@ -289,25 +292,24 @@ class Article {
* Load the revision (including text) into this object
*/
function loadContent() {
- if ( $this->mContentLoaded ) return;
-
+ if( $this->mContentLoaded ) return;
+ wfProfileIn( __METHOD__ );
# Query variables :P
$oldid = $this->getOldID();
-
# Pre-fill content with error message so that if something
# fails we'll have something telling us what we intended.
$this->mOldId = $oldid;
$this->fetchContent( $oldid );
+ wfProfileOut( __METHOD__ );
}
/**
* Fetch a page record with the given conditions
- * @param Database $dbr
- * @param array $conditions
- * @private
+ * @param $dbr Database object
+ * @param $conditions Array
*/
- function pageData( $dbr, $conditions ) {
+ protected function pageData( $dbr, $conditions ) {
$fields = array(
'page_id',
'page_namespace',
@@ -333,20 +335,20 @@ class Article {
}
/**
- * @param Database $dbr
- * @param Title $title
+ * @param $dbr Database object
+ * @param $title Title object
*/
- function pageDataFromTitle( $dbr, $title ) {
+ public function pageDataFromTitle( $dbr, $title ) {
return $this->pageData( $dbr, array(
'page_namespace' => $title->getNamespace(),
'page_title' => $title->getDBkey() ) );
}
/**
- * @param Database $dbr
- * @param int $id
+ * @param $dbr Database
+ * @param $id Integer
*/
- function pageDataFromId( $dbr, $id ) {
+ protected function pageDataFromId( $dbr, $id ) {
return $this->pageData( $dbr, array( 'page_id' => $id ) );
}
@@ -354,22 +356,21 @@ class Article {
* Set the general counter, title etc data loaded from
* some source.
*
- * @param object $data
- * @private
+ * @param $data Database row object or "fromdb"
*/
- function loadPageData( $data = 'fromdb' ) {
- if ( $data === 'fromdb' ) {
+ public function loadPageData( $data = 'fromdb' ) {
+ if( $data === 'fromdb' ) {
$dbr = wfGetDB( DB_MASTER );
$data = $this->pageDataFromId( $dbr, $this->getId() );
}
$lc = LinkCache::singleton();
- if ( $data ) {
+ if( $data ) {
$lc->addGoodLinkObj( $data->page_id, $this->mTitle, $data->page_len, $data->page_is_redirect );
$this->mTitle->mArticleID = $data->page_id;
- # Old-fashioned restrictions.
+ # Old-fashioned restrictions
$this->mTitle->loadRestrictions( $data->page_restrictions );
$this->mCounter = $data->page_counter;
@@ -377,7 +378,7 @@ class Article {
$this->mIsRedirect = $data->page_is_redirect;
$this->mLatest = $data->page_latest;
} else {
- if ( is_object( $this->mTitle ) ) {
+ if( is_object( $this->mTitle ) ) {
$lc->addBadLinkObj( $this->mTitle );
}
$this->mTitle->mArticleID = 0;
@@ -389,11 +390,11 @@ class Article {
/**
* Get text of an article from database
* Does *NOT* follow redirects.
- * @param int $oldid 0 for whatever the latest revision is
+ * @param $oldid Int: 0 for whatever the latest revision is
* @return string
*/
function fetchContent( $oldid = 0 ) {
- if ( $this->mContentLoaded ) {
+ if( $this->mContentLoaded ) {
return $this->mContent;
}
@@ -429,14 +430,14 @@ class Article {
}
$revision = Revision::newFromId( $this->mLatest );
if( is_null( $revision ) ) {
- wfDebug( __METHOD__." failed to retrieve current page, rev_id {$data->page_latest}\n" );
+ wfDebug( __METHOD__." failed to retrieve current page, rev_id {$this->mLatest}\n" );
return false;
}
}
// FIXME: Horrible, horrible! This content-loading interface just plain sucks.
// We should instead work with the Revision object when we need it...
- $this->mContent = $revision->revText(); // Loads if user is allowed
+ $this->mContent = $revision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed
$this->mUser = $revision->getUser();
$this->mUserText = $revision->getUserText();
@@ -457,7 +458,7 @@ class Article {
*
* @param $x Mixed: FIXME
*/
- function forUpdate( $x = NULL ) {
+ public function forUpdate( $x = NULL ) {
return wfSetVar( $this->mForUpdate, $x );
}
@@ -479,9 +480,9 @@ class Article {
* the default
* @return Array: options
*/
- function getSelectOptions( $options = '' ) {
- if ( $this->mForUpdate ) {
- if ( is_array( $options ) ) {
+ protected function getSelectOptions( $options = '' ) {
+ if( $this->mForUpdate ) {
+ if( is_array( $options ) ) {
$options[] = 'FOR UPDATE';
} else {
$options = 'FOR UPDATE';
@@ -493,7 +494,7 @@ class Article {
/**
* @return int Page ID
*/
- function getID() {
+ public function getID() {
if( $this->mTitle ) {
return $this->mTitle->getArticleID();
} else {
@@ -504,22 +505,38 @@ class Article {
/**
* @return bool Whether or not the page exists in the database
*/
- function exists() {
- return $this->getId() != 0;
+ public function exists() {
+ return $this->getId() > 0;
+ }
+
+ /**
+ * Check if this page is something we're going to be showing
+ * some sort of sensible content for. If we return false, page
+ * views (plain action=view) will return an HTTP 404 response,
+ * so spiders and robots can know they're following a bad link.
+ *
+ * @return bool
+ */
+ public function hasViewableContent() {
+ return $this->exists() || $this->mTitle->isAlwaysKnown();
}
/**
* @return int The view count for the page
*/
- function getCount() {
- if ( -1 == $this->mCounter ) {
+ public function getCount() {
+ if( -1 == $this->mCounter ) {
$id = $this->getID();
- if ( $id == 0 ) {
+ if( $id == 0 ) {
$this->mCounter = 0;
} else {
$dbr = wfGetDB( DB_SLAVE );
- $this->mCounter = $dbr->selectField( 'page', 'page_counter', array( 'page_id' => $id ),
- 'Article::getCount', $this->getSelectOptions() );
+ $this->mCounter = $dbr->selectField( 'page',
+ 'page_counter',
+ array( 'page_id' => $id ),
+ __METHOD__,
+ $this->getSelectOptions()
+ );
}
}
return $this->mCounter;
@@ -532,14 +549,11 @@ class Article {
* @param $text String: text to analyze
* @return bool
*/
- function isCountable( $text ) {
+ public function isCountable( $text ) {
global $wgUseCommaCount;
$token = $wgUseCommaCount ? ',' : '[[';
- return
- $this->mTitle->isContentPage()
- && !$this->isRedirect( $text )
- && in_string( $token, $text );
+ return $this->mTitle->isContentPage() && !$this->isRedirect($text) && in_string($token,$text);
}
/**
@@ -548,11 +562,11 @@ class Article {
* @param $text String: FIXME
* @return bool
*/
- function isRedirect( $text = false ) {
- if ( $text === false ) {
- if ( $this->mDataLoaded )
+ public function isRedirect( $text = false ) {
+ if( $text === false ) {
+ if( $this->mDataLoaded ) {
return $this->mIsRedirect;
-
+ }
// Apparently loadPageData was never called
$this->loadContent();
$titleObj = Title::newFromRedirect( $this->fetchContent() );
@@ -567,28 +581,25 @@ class Article {
* to this page (and it exists).
* @return bool
*/
- function isCurrent() {
+ public function isCurrent() {
# If no oldid, this is the current version.
- if ($this->getOldID() == 0)
+ if( $this->getOldID() == 0 ) {
return true;
-
- return $this->exists() &&
- isset( $this->mRevision ) &&
- $this->mRevision->isCurrent();
+ }
+ return $this->exists() && isset($this->mRevision) && $this->mRevision->isCurrent();
}
/**
* Loads everything except the text
* This isn't necessary for all uses, so it's only done if needed.
- * @private
*/
- function loadLastEdit() {
- if ( -1 != $this->mUser )
+ protected function loadLastEdit() {
+ if( -1 != $this->mUser )
return;
# New or non-existent articles have no user information
$id = $this->getID();
- if ( 0 == $id ) return;
+ if( 0 == $id ) return;
$this->mLastRevision = Revision::loadFromPageId( wfGetDB( DB_MASTER ), $id );
if( !is_null( $this->mLastRevision ) ) {
@@ -601,35 +612,36 @@ class Article {
}
}
- function getTimestamp() {
+ public function getTimestamp() {
// Check if the field has been filled by ParserCache::get()
- if ( !$this->mTimestamp ) {
+ if( !$this->mTimestamp ) {
$this->loadLastEdit();
}
return wfTimestamp(TS_MW, $this->mTimestamp);
}
- function getUser() {
+ public function getUser() {
$this->loadLastEdit();
return $this->mUser;
}
- function getUserText() {
+ public function getUserText() {
$this->loadLastEdit();
return $this->mUserText;
}
- function getComment() {
+ public function getComment() {
$this->loadLastEdit();
return $this->mComment;
}
- function getMinorEdit() {
+ public function getMinorEdit() {
$this->loadLastEdit();
return $this->mMinorEdit;
}
- function getRevIdFetched() {
+ /* Use this to fetch the rev ID used on page views */
+ public function getRevIdFetched() {
$this->loadLastEdit();
return $this->mRevIdFetched;
}
@@ -638,7 +650,7 @@ class Article {
* @param $limit Integer: default 0.
* @param $offset Integer: default 0.
*/
- function getContributors($limit = 0, $offset = 0) {
+ public function getContributors($limit = 0, $offset = 0) {
# XXX: this is expensive; cache this info somewhere.
$contribs = array();
@@ -648,49 +660,62 @@ class Article {
$user = $this->getUser();
$pageId = $this->getId();
- $sql = "SELECT rev_user, rev_user_text, user_real_name, MAX(rev_timestamp) as timestamp
+ $sql = "SELECT {$userTable}.*, MAX(rev_timestamp) as timestamp
FROM $revTable LEFT JOIN $userTable ON rev_user = user_id
WHERE rev_page = $pageId
AND rev_user != $user
GROUP BY rev_user, rev_user_text, user_real_name
ORDER BY timestamp DESC";
- if ($limit > 0) { $sql .= ' LIMIT '.$limit; }
- if ($offset > 0) { $sql .= ' OFFSET '.$offset; }
-
- $sql .= ' '. $this->getSelectOptions();
+ if($limit > 0) { $sql .= ' LIMIT '.$limit; }
+ if($offset > 0) { $sql .= ' OFFSET '.$offset; }
- $res = $dbr->query($sql, __METHOD__);
+ $sql .= ' '. $this->getSelectOptions();
- while ( $line = $dbr->fetchObject( $res ) ) {
- $contribs[] = array($line->rev_user, $line->rev_user_text, $line->user_real_name);
- }
+ $res = $dbr->query($sql, __METHOD__ );
- $dbr->freeResult($res);
- return $contribs;
+ return new UserArrayFromResult( $res );
}
/**
* This is the default action of the script: just view the page of
* the given title.
*/
- function view() {
+ public function view() {
global $wgUser, $wgOut, $wgRequest, $wgContLang;
global $wgEnableParserCache, $wgStylePath, $wgParser;
global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies;
global $wgDefaultRobotPolicy;
- $sk = $wgUser->getSkin();
wfProfileIn( __METHOD__ );
- $parserCache = ParserCache::singleton();
- $ns = $this->mTitle->getNamespace(); # shortcut
-
# Get variables from query string
$oldid = $this->getOldID();
+ # Try file cache
+ if( $oldid === 0 && $this->checkTouched() ) {
+ global $wgUseETag;
+ if( $wgUseETag ) {
+ $parserCache = ParserCache::singleton();
+ $wgOut->setETag( $parserCache->getETag($this,$wgUser) );
+ }
+ if( $wgOut->checkLastModified( $this->getTouched() ) ) {
+ wfProfileOut( __METHOD__ );
+ return;
+ } else if( $this->tryFileCache() ) {
+ # tell wgOut that output is taken care of
+ $wgOut->disable();
+ $this->viewUpdates();
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+ }
+
+ $ns = $this->mTitle->getNamespace(); # shortcut
+ $sk = $wgUser->getSkin();
+
# getOldID may want us to redirect somewhere else
- if ( $this->mRedirectUrl ) {
+ if( $this->mRedirectUrl ) {
$wgOut->redirect( $this->mRedirectUrl );
wfProfileOut( __METHOD__ );
return;
@@ -701,13 +726,14 @@ class Article {
$rdfrom = $wgRequest->getVal( 'rdfrom' );
$diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
$purge = $wgRequest->getVal( 'action' ) == 'purge';
+ $return404 = false;
$wgOut->setArticleFlag( true );
# Discourage indexing of printable versions, but encourage following
if( $wgOut->isPrintable() ) {
$policy = 'noindex,follow';
- } elseif ( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) {
+ } elseif( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) {
$policy = $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()];
} elseif( isset( $wgNamespaceRobotPolicies[$ns] ) ) {
# Honour customised robot policies for this namespace
@@ -720,10 +746,12 @@ class Article {
# If we got diff and oldid in the query, we want to see a
# diff page instead of the article.
- if ( !is_null( $diff ) ) {
+ if( !is_null( $diff ) ) {
$wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
- $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge );
+ $diff = $wgRequest->getVal( 'diff' );
+ $htmldiff = $wgRequest->getVal( 'htmldiff' , false);
+ $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $htmldiff);
// DifferenceEngine directly fetched the revision:
$this->mRevIdFetched = $de->mNewid;
$de->showDiffPage( $diffOnly );
@@ -738,51 +766,36 @@ class Article {
return;
}
- if ( empty( $oldid ) && $this->checkTouched() ) {
- $wgOut->setETag($parserCache->getETag($this, $wgUser));
-
- if( $wgOut->checkLastModified( $this->mTouched ) ){
- wfProfileOut( __METHOD__ );
- return;
- } else if ( $this->tryFileCache() ) {
- # tell wgOut that output is taken care of
- $wgOut->disable();
- $this->viewUpdates();
- wfProfileOut( __METHOD__ );
- return;
- }
- }
-
# Should the parser cache be used?
$pcache = $this->useParserCache( $oldid );
wfDebug( 'Article::view using parser cache: ' . ($pcache ? 'yes' : 'no' ) . "\n" );
- if ( $wgUser->getOption( 'stubthreshold' ) ) {
+ if( $wgUser->getOption( 'stubthreshold' ) ) {
wfIncrStats( 'pcache_miss_stub' );
}
$wasRedirected = false;
- if ( isset( $this->mRedirectedFrom ) ) {
+ if( isset( $this->mRedirectedFrom ) ) {
// This is an internally redirected page view.
// We'll need a backlink to the source page for navigation.
- if ( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) {
+ if( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) {
$redir = $sk->makeKnownLinkObj( $this->mRedirectedFrom, '', 'redirect=no' );
- $s = wfMsg( 'redirectedfrom', $redir );
+ $s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir );
$wgOut->setSubtitle( $s );
// Set the fragment if one was specified in the redirect
- if ( strval( $this->mTitle->getFragment() ) != '' ) {
+ if( strval( $this->mTitle->getFragment() ) != '' ) {
$fragment = Xml::escapeJsString( $this->mTitle->getFragmentForURL() );
$wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" );
}
$wasRedirected = true;
}
- } elseif ( !empty( $rdfrom ) ) {
+ } elseif( !empty( $rdfrom ) ) {
// This is an externally redirected view, from some other wiki.
// If it was reported from a trusted site, supply a backlink.
global $wgRedirectSources;
if( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) {
$redir = $sk->makeExternalLink( $rdfrom, $rdfrom );
- $s = wfMsg( 'redirectedfrom', $redir );
+ $s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir );
$wgOut->setSubtitle( $s );
$wasRedirected = true;
}
@@ -790,18 +803,20 @@ class Article {
$outputDone = false;
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;
- }
+ if( $pcache && $wgOut->tryParserCache( $this, $wgUser ) ) {
+ // Ensure that UI elements requiring revision ID have
+ // the correct version information.
+ $wgOut->setRevisionId( $this->mLatest );
+ $outputDone = true;
}
# Fetch content and check for errors
- if ( !$outputDone ) {
+ if( !$outputDone ) {
+ # If the article does not exist and was deleted, show the log
+ if( $this->getID() == 0 ) {
+ $this->showDeletionLog();
+ }
$text = $this->getContent();
- if ( $text === false ) {
+ if( $text === false ) {
# Failed to load, replace text with error message
$t = $this->mTitle->getPrefixedText();
if( $oldid ) {
@@ -811,18 +826,38 @@ class Article {
$text = wfMsg( 'noarticletext' );
}
}
+
+ # Non-existent pages
+ if( $this->getID() === 0 ) {
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ $text = "<div class='noarticletext'>\n$text\n</div>";
+ if( !$this->hasViewableContent() ) {
+ // If there's no backing content, send a 404 Not Found
+ // for better machine handling of broken links.
+ $return404 = true;
+ }
+ }
+
+ if( $return404 ) {
+ $wgRequest->response()->header( "HTTP/1.x 404 Not Found" );
+ }
# Another whitelist check in case oldid is altering the title
- if ( !$this->mTitle->userCanRead() ) {
+ if( !$this->mTitle->userCanRead() ) {
$wgOut->loginToUse();
$wgOut->output();
+ $wgOut->disable();
wfProfileOut( __METHOD__ );
- exit;
+ return;
}
+
+ # For ?curid=x urls, disallow indexing
+ if( $wgRequest->getInt('curid') )
+ $wgOut->setRobotPolicy( 'noindex,follow' );
# We're looking at an old revision
- if ( !empty( $oldid ) ) {
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ if( !empty( $oldid ) ) {
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
if( is_null( $this->mRevision ) ) {
// FIXME: This would be a nice place to load the 'no such page' text.
} else {
@@ -840,27 +875,27 @@ class Article {
}
}
}
-
+
$wgOut->setRevisionId( $this->getRevIdFetched() );
// Pages containing custom CSS or JavaScript get special treatment
if( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) {
- $wgOut->addHtml( wfMsgExt( 'clearyourcache', 'parse' ) );
+ $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" );
+ $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
+ $wgOut->addHTML( htmlspecialchars( $this->mContent ) );
+ $wgOut->addHTML( "\n</pre>\n" );
}
- } else if ( $rt = Title::newFromRedirect( $text ) ) {
+ } else if( $rt = Title::newFromRedirect( $text ) ) {
# Don't append the subtitle if this was an old revision
- $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() );
+ $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) );
$parseout = $wgParser->parse($text, $this->mTitle, ParserOptions::newFromUser($wgUser));
$wgOut->addParserOutputNoText( $parseout );
- } else if ( $pcache ) {
+ } else if( $pcache ) {
# Display content and save to parser cache
$this->outputWikiText( $text );
} else {
@@ -876,7 +911,7 @@ class Article {
$time += wfTime();
# Timing hack
- if ( $time > 3 ) {
+ if( $time > 3 ) {
wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time,
$this->mTitle->getPrefixedDBkey()));
}
@@ -890,6 +925,14 @@ class Article {
$t = $wgOut->getPageTitle();
if( empty( $t ) ) {
$wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
+
+ # For the main page, overwrite the <title> element with the con-
+ # tents of 'pagetitle-view-mainpage' instead of the default (if
+ # that's not empty).
+ if( $this->mTitle->equals( Title::newMainPage() ) &&
+ wfMsgForContent( 'pagetitle-view-mainpage' ) !== '' ) {
+ $wgOut->setHTMLTitle( wfMsgForContent( 'pagetitle-view-mainpage' ) );
+ }
}
# check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
@@ -899,7 +942,7 @@ class Article {
# If we have been passed an &rcid= parameter, we want to give the user a
# chance to mark this new article as patrolled.
- if( !is_null( $rcid ) && $rcid != 0 && $wgUser->isAllowed( 'patrol' ) && $this->mTitle->exists() ) {
+ if( !empty($rcid) && $this->mTitle->exists() && $this->mTitle->userCan('patrol') ) {
$wgOut->addHTML(
"<div class='patrollink'>" .
wfMsgHtml( 'markaspatrolledlink',
@@ -911,19 +954,45 @@ class Article {
}
# Trackbacks
- if ($wgUseTrackbacks)
+ if( $wgUseTrackbacks ) {
$this->addTrackbacks();
+ }
$this->viewUpdates();
wfProfileOut( __METHOD__ );
}
- /*
+ protected function showDeletionLog() {
+ global $wgUser, $wgOut;
+ $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut );
+ $pager = new LogPager( $loglist, 'delete', false, $this->mTitle->getPrefixedText() );
+ if( $pager->getNumRows() > 0 ) {
+ $pager->mLimit = 10;
+ $wgOut->addHTML( '<div class="mw-warning-with-logexcerpt">' );
+ $wgOut->addWikiMsg( 'deleted-notice' );
+ $wgOut->addHTML(
+ $loglist->beginLogEventsList() .
+ $pager->getBody() .
+ $loglist->endLogEventsList()
+ );
+ if( $pager->getNumRows() > 10 ) {
+ $wgOut->addHTML( $wgUser->getSkin()->link(
+ SpecialPage::getTitleFor( 'Log' ),
+ wfMsgHtml( 'deletelog-fulllog' ),
+ array(),
+ array( 'type' => 'delete', 'page' => $this->mTitle->getPrefixedText() )
+ ) );
+ }
+ $wgOut->addHTML( '</div>' );
+ }
+ }
+
+ /*
* Should the parser cache be used?
*/
protected function useParserCache( $oldid ) {
global $wgUser, $wgEnableParserCache;
-
+
return $wgEnableParserCache
&& intval( $wgUser->getOption( 'stubthreshold' ) ) == 0
&& $this->exists()
@@ -931,47 +1000,48 @@ class Article {
&& !$this->mTitle->isCssOrJsPage()
&& !$this->mTitle->isCssJsSubpage();
}
-
- protected function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) {
+
+ /**
+ * View redirect
+ * @param $target Title object of destination to redirect
+ * @param $appendSubtitle Boolean [optional]
+ * @param $forceKnown Boolean: should the image be shown as a bluelink regardless of existence?
+ */
+ public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) {
global $wgParser, $wgOut, $wgContLang, $wgStylePath, $wgUser;
-
# Display redirect
$imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr';
$imageUrl = $wgStylePath.'/common/images/redirect' . $imageDir . '.png';
-
+
if( $appendSubtitle ) {
$wgOut->appendSubtitle( wfMsgHtml( 'redirectpagesub' ) );
}
$sk = $wgUser->getSkin();
- if ( $forceKnown )
+ if( $forceKnown ) {
$link = $sk->makeKnownLinkObj( $target, htmlspecialchars( $target->getFullText() ) );
- else
+ } else {
$link = $sk->makeLinkObj( $target, htmlspecialchars( $target->getFullText() ) );
+ }
+ return '<img src="'.$imageUrl.'" alt="#REDIRECT " />' .
+ '<span class="redirectText">'.$link.'</span>';
- $wgOut->addHTML( '<img src="'.$imageUrl.'" alt="#REDIRECT " />' .
- '<span class="redirectText">'.$link.'</span>' );
-
}
- function addTrackbacks() {
+ public function addTrackbacks() {
global $wgOut, $wgUser;
-
- $dbr = wfGetDB(DB_SLAVE);
- $tbs = $dbr->select(
- /* FROM */ 'trackbacks',
- /* SELECT */ array('tb_id', 'tb_title', 'tb_url', 'tb_ex', 'tb_name'),
- /* WHERE */ array('tb_page' => $this->getID())
+ $dbr = wfGetDB( DB_SLAVE );
+ $tbs = $dbr->select( 'trackbacks',
+ array('tb_id', 'tb_title', 'tb_url', 'tb_ex', 'tb_name'),
+ array('tb_page' => $this->getID() )
);
-
- if (!$dbr->numrows($tbs))
- return;
+ if( !$dbr->numRows($tbs) ) return;
$tbtext = "";
- while ($o = $dbr->fetchObject($tbs)) {
+ while( $o = $dbr->fetchObject($tbs) ) {
$rmvtxt = "";
- if ($wgUser->isAllowed( 'trackback' )) {
- $delurl = $this->mTitle->getFullURL("action=deletetrackback&tbid="
- . $o->tb_id . "&token=" . urlencode( $wgUser->editToken() ) );
+ if( $wgUser->isAllowed( 'trackback' ) ) {
+ $delurl = $this->mTitle->getFullURL("action=deletetrackback&tbid=" .
+ $o->tb_id . "&token=" . urlencode( $wgUser->editToken() ) );
$rmvtxt = wfMsg( 'trackbackremove', htmlspecialchars( $delurl ) );
}
$tbtext .= "\n";
@@ -983,33 +1053,31 @@ class Article {
$rmvtxt);
}
$wgOut->addWikiMsg( 'trackbackbox', $tbtext );
+ $this->mTitle->invalidateCache();
}
- function deletetrackback() {
+ public function deletetrackback() {
global $wgUser, $wgRequest, $wgOut, $wgTitle;
-
- if (!$wgUser->matchEditToken($wgRequest->getVal('token'))) {
+ if( !$wgUser->matchEditToken($wgRequest->getVal('token')) ) {
$wgOut->addWikiMsg( 'sessionfailure' );
return;
}
$permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser );
-
- if (count($permission_errors)>0)
- {
+ if( count($permission_errors) ) {
$wgOut->showPermissionsErrorPage( $permission_errors );
return;
}
- $db = wfGetDB(DB_MASTER);
- $db->delete('trackbacks', array('tb_id' => $wgRequest->getInt('tbid')));
- $wgTitle->invalidateCache();
- $wgOut->addWikiMsg('trackbackdeleteok');
+ $db = wfGetDB( DB_MASTER );
+ $db->delete( 'trackbacks', array('tb_id' => $wgRequest->getInt('tbid')) );
+
+ $wgOut->addWikiMsg( 'trackbackdeleteok' );
+ $this->mTitle->invalidateCache();
}
- function render() {
+ public function render() {
global $wgOut;
-
$wgOut->setArticleBodyOnly(true);
$this->view();
}
@@ -1017,37 +1085,36 @@ class Article {
/**
* Handle action=purge
*/
- function purge() {
+ public function purge() {
global $wgUser, $wgRequest, $wgOut;
-
- if ( $wgUser->isAllowed( 'purge' ) || $wgRequest->wasPosted() ) {
+ if( $wgUser->isAllowed( 'purge' ) || $wgRequest->wasPosted() ) {
if( wfRunHooks( 'ArticlePurge', array( &$this ) ) ) {
$this->doPurge();
+ $this->view();
}
} else {
- $msg = $wgOut->parse( wfMsg( 'confirm_purge' ) );
- $action = htmlspecialchars( $_SERVER['REQUEST_URI'] );
- $button = htmlspecialchars( wfMsg( 'confirm_purge_button' ) );
- $msg = str_replace( '$1',
- "<form method=\"post\" action=\"$action\">\n" .
- "<input type=\"submit\" name=\"submit\" value=\"$button\" />\n" .
- "</form>\n", $msg );
-
+ $action = htmlspecialchars( $wgRequest->getRequestURL() );
+ $button = wfMsgExt( 'confirm_purge_button', array('escapenoentities') );
+ $form = "<form method=\"post\" action=\"$action\">\n" .
+ "<input type=\"submit\" name=\"submit\" value=\"$button\" />\n" .
+ "</form>\n";
+ $top = wfMsgExt( 'confirm-purge-top', array('parse') );
+ $bottom = wfMsgExt( 'confirm-purge-bottom', array('parse') );
$wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
- $wgOut->addHTML( $msg );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ $wgOut->addHTML( $top . $form . $bottom );
}
}
/**
* Perform the actions of a page purging
*/
- function doPurge() {
+ public function doPurge() {
global $wgUseSquid;
// Invalidate the cache
$this->mTitle->invalidateCache();
- if ( $wgUseSquid ) {
+ if( $wgUseSquid ) {
// Commit the transaction before the purge is sent
$dbw = wfGetDB( DB_MASTER );
$dbw->immediateCommit();
@@ -1056,16 +1123,15 @@ class Article {
$update = SquidUpdate::newSimplePurge( $this->mTitle );
$update->doUpdate();
}
- if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
global $wgMessageCache;
- if ( $this->getID() == 0 ) {
+ if( $this->getID() == 0 ) {
$text = false;
} else {
$text = $this->getContent();
}
$wgMessageCache->replace( $this->mTitle->getDBkey(), $text );
}
- $this->view();
}
/**
@@ -1075,11 +1141,11 @@ class Article {
* or else the record will be left in a funky state.
* Best if all done inside a transaction.
*
- * @param Database $dbw
- * @return int The newly created page_id key
+ * @param $dbw Database
+ * @return int The newly created page_id key, or false if the title already existed
* @private
*/
- function insertOn( $dbw ) {
+ public function insertOn( $dbw ) {
wfProfileIn( __METHOD__ );
$page_id = $dbw->nextSequenceValue( 'page_page_id_seq' );
@@ -1095,31 +1161,33 @@ class Article {
'page_touched' => $dbw->timestamp(),
'page_latest' => 0, # Fill this in shortly...
'page_len' => 0, # Fill this in shortly...
- ), __METHOD__ );
- $newid = $dbw->insertId();
-
- $this->mTitle->resetArticleId( $newid );
+ ), __METHOD__, 'IGNORE' );
+ $affected = $dbw->affectedRows();
+ if( $affected ) {
+ $newid = $dbw->insertId();
+ $this->mTitle->resetArticleId( $newid );
+ }
wfProfileOut( __METHOD__ );
- return $newid;
+ return $affected ? $newid : false;
}
/**
* Update the page record to point to a newly saved revision.
*
- * @param Database $dbw
- * @param Revision $revision For ID number, and text used to set
- length and redirect status fields
- * @param int $lastRevision If given, will not overwrite the page field
- * when different from the currently set value.
- * Giving 0 indicates the new page flag should
- * be set on.
- * @param bool $lastRevIsRedirect If given, will optimize adding and
- * removing rows in redirect table.
+ * @param $dbw Database object
+ * @param $revision Revision: For ID number, and text used to set
+ length and redirect status fields
+ * @param $lastRevision Integer: if given, will not overwrite the page field
+ * when different from the currently set value.
+ * Giving 0 indicates the new page flag should be set
+ * on.
+ * @param $lastRevIsRedirect Boolean: if given, will optimize adding and
+ * removing rows in redirect table.
* @return bool true on success, false on failure
* @private
*/
- function updateRevisionOn( &$dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) {
+ public function updateRevisionOn( &$dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) {
wfProfileIn( __METHOD__ );
$text = $revision->getText();
@@ -1143,8 +1211,7 @@ class Article {
__METHOD__ );
$result = $dbw->affectedRows() != 0;
-
- if ($result) {
+ if( $result ) {
$this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
}
@@ -1155,46 +1222,40 @@ class Article {
/**
* Add row to the redirect table if this is a redirect, remove otherwise.
*
- * @param Database $dbw
+ * @param $dbw Database
* @param $redirectTitle a title object pointing to the redirect target,
- * or NULL if this is not a redirect
- * @param bool $lastRevIsRedirect If given, will optimize adding and
- * removing rows in redirect table.
+ * or NULL if this is not a redirect
+ * @param $lastRevIsRedirect If given, will optimize adding and
+ * removing rows in redirect table.
* @return bool true on success, false on failure
* @private
*/
- function updateRedirectOn( &$dbw, $redirectTitle, $lastRevIsRedirect = null ) {
-
+ public function updateRedirectOn( &$dbw, $redirectTitle, $lastRevIsRedirect = null ) {
// Always update redirects (target link might have changed)
// Update/Insert if we don't know if the last revision was a redirect or not
// Delete if changing from redirect to non-redirect
$isRedirect = !is_null($redirectTitle);
- if ($isRedirect || is_null($lastRevIsRedirect) || $lastRevIsRedirect !== $isRedirect) {
-
+ if($isRedirect || is_null($lastRevIsRedirect) || $lastRevIsRedirect !== $isRedirect) {
wfProfileIn( __METHOD__ );
-
- if ($isRedirect) {
-
+ if( $isRedirect ) {
// This title is a redirect, Add/Update row in the redirect table
$set = array( /* SET */
'rd_namespace' => $redirectTitle->getNamespace(),
'rd_title' => $redirectTitle->getDBkey(),
'rd_from' => $this->getId(),
);
-
$dbw->replace( 'redirect', array( 'rd_from' ), $set, __METHOD__ );
} else {
// This is not a redirect, remove row from redirect table
$where = array( 'rd_from' => $this->getId() );
$dbw->delete( 'redirect', $where, __METHOD__);
}
-
- if( $this->getTitle()->getNamespace() == NS_IMAGE )
+ if( $this->getTitle()->getNamespace() == NS_FILE ) {
RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
+ }
wfProfileOut( __METHOD__ );
return ( $dbw->affectedRows() != 0 );
}
-
return true;
}
@@ -1202,12 +1263,11 @@ class Article {
* If the given revision is newer than the currently set page_latest,
* update the page record. Otherwise, do nothing.
*
- * @param Database $dbw
- * @param Revision $revision
+ * @param $dbw Database object
+ * @param $revision Revision object
*/
- function updateIfNewerOn( &$dbw, $revision ) {
+ public function updateIfNewerOn( &$dbw, $revision ) {
wfProfileIn( __METHOD__ );
-
$row = $dbw->selectRow(
array( 'revision', 'page' ),
array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ),
@@ -1227,28 +1287,27 @@ class Article {
$prev = 0;
$lastRevIsRedirect = null;
}
-
$ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
wfProfileOut( __METHOD__ );
return $ret;
}
/**
+ * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...)
* @return string Complete article text, or null if error
*/
- function replaceSection($section, $text, $summary = '', $edittime = NULL) {
+ public function replaceSection( $section, $text, $summary = '', $edittime = NULL ) {
wfProfileIn( __METHOD__ );
-
- if( $section == '' ) {
- // Whole-page edit; let the text through unmolested.
+ if( strval( $section ) == '' ) {
+ // Whole-page edit; let the whole text through
} else {
- if( is_null( $edittime ) ) {
+ if( is_null($edittime) ) {
$rev = Revision::newFromTitle( $this->mTitle );
} else {
$dbw = wfGetDB( DB_MASTER );
$rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
}
- if( is_null( $rev ) ) {
+ if( !$rev ) {
wfDebug( "Article::replaceSection asked for bogus section (page: " .
$this->getId() . "; section: $section; edittime: $edittime)\n" );
return null;
@@ -1266,9 +1325,7 @@ class Article {
global $wgParser;
$text = $wgParser->replaceSection( $oldtext, $section, $text );
}
-
}
-
wfProfileOut( __METHOD__ );
return $text;
}
@@ -1277,27 +1334,28 @@ class Article {
* @deprecated use Article::doEdit()
*/
function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false, $bot=false ) {
+ wfDeprecated( __METHOD__ );
$flags = EDIT_NEW | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
( $isminor ? EDIT_MINOR : 0 ) |
( $suppressRC ? EDIT_SUPPRESS_RC : 0 ) |
( $bot ? EDIT_FORCE_BOT : 0 );
# If this is a comment, add the summary as headline
- if ( $comment && $summary != "" ) {
+ if( $comment && $summary != "" ) {
$text = wfMsgForContent('newsectionheaderdefaultlevel',$summary) . "\n\n".$text;
}
$this->doEdit( $text, $summary, $flags );
$dbw = wfGetDB( DB_MASTER );
- if ($watchthis) {
- if (!$this->mTitle->userIsWatching()) {
+ if($watchthis) {
+ if(!$this->mTitle->userIsWatching()) {
$dbw->begin();
$this->doWatch();
$dbw->commit();
}
} else {
- if ( $this->mTitle->userIsWatching() ) {
+ if( $this->mTitle->userIsWatching() ) {
$dbw->begin();
$this->doUnwatch();
$dbw->commit();
@@ -1310,33 +1368,36 @@ class Article {
* @deprecated use Article::doEdit()
*/
function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' ) {
+ wfDeprecated( __METHOD__ );
$flags = EDIT_UPDATE | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
( $minor ? EDIT_MINOR : 0 ) |
( $forceBot ? EDIT_FORCE_BOT : 0 );
- $good = $this->doEdit( $text, $summary, $flags );
- if ( $good ) {
- $dbw = wfGetDB( DB_MASTER );
- if ($watchthis) {
- if (!$this->mTitle->userIsWatching()) {
- $dbw->begin();
- $this->doWatch();
- $dbw->commit();
- }
- } else {
- if ( $this->mTitle->userIsWatching() ) {
- $dbw->begin();
- $this->doUnwatch();
- $dbw->commit();
- }
+ $status = $this->doEdit( $text, $summary, $flags );
+ if( !$status->isOK() ) {
+ return false;
+ }
+
+ $dbw = wfGetDB( DB_MASTER );
+ if( $watchthis ) {
+ if(!$this->mTitle->userIsWatching()) {
+ $dbw->begin();
+ $this->doWatch();
+ $dbw->commit();
}
+ } else {
+ if( $this->mTitle->userIsWatching() ) {
+ $dbw->begin();
+ $this->doUnwatch();
+ $dbw->commit();
+ }
+ }
- $extraQuery = ''; // Give extensions a chance to modify URL query on update
- wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraQuery ) );
+ $extraQuery = ''; // Give extensions a chance to modify URL query on update
+ wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraQuery ) );
- $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraQuery );
- }
- return $good;
+ $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraQuery );
+ return true;
}
/**
@@ -1347,9 +1408,9 @@ class Article {
*
* $wgUser must be set before calling this function.
*
- * @param string $text New text
- * @param string $summary Edit summary
- * @param integer $flags bitfield:
+ * @param $text String: new text
+ * @param $summary String: edit summary
+ * @param $flags Integer bitfield:
* EDIT_NEW
* Article is known or assumed to be non-existent, create a new one
* EDIT_UPDATE
@@ -1366,40 +1427,67 @@ class Article {
* Fill in blank summaries with generated text where possible
*
* If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected.
- * If EDIT_UPDATE is specified and the article doesn't exist, the function will return false. If
- * EDIT_NEW is specified and the article does exist, a duplicate key error will cause an exception
- * to be thrown from the Database. These two conditions are also possible with auto-detection due
- * to MediaWiki's performance-optimised locking strategy.
- * @param $baseRevId, the revision ID this edit was based off, if any
+ * If EDIT_UPDATE is specified and the article doesn't exist, the function will an
+ * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an
+ * edit-already-exists error will be returned. These two conditions are also possible with
+ * auto-detection due to MediaWiki's performance-optimised locking strategy.
+ *
+ * @param $baseRevId the revision ID this edit was based off, if any
+ * @param $user Optional user object, $wgUser will be used if not passed
*
- * @return bool success
+ * @return Status object. Possible errors:
+ * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't set the fatal flag of $status
+ * edit-gone-missing: In update mode, but the article didn't exist
+ * edit-conflict: In update mode, the article changed unexpectedly
+ * edit-no-change: Warning that the text was the same as before
+ * edit-already-exists: In creation mode, but the article already exists
+ *
+ * Extensions may define additional errors.
+ *
+ * $return->value will contain an associative array with members as follows:
+ * new: Boolean indicating if the function attempted to create a new article
+ * revision: The revision object for the inserted revision, or null
+ *
+ * Compatibility note: this function previously returned a boolean value indicating success/failure
*/
- function doEdit( $text, $summary, $flags = 0, $baseRevId = false ) {
+ public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
+ # Low-level sanity check
+ if( $this->mTitle->getText() == '' ) {
+ throw new MWException( 'Something is trying to edit an article with an empty title' );
+ }
+
wfProfileIn( __METHOD__ );
- $good = true;
- if ( !($flags & EDIT_NEW) && !($flags & EDIT_UPDATE) ) {
- $aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE );
- if ( $aid ) {
+ $user = is_null($user) ? $wgUser : $user;
+ $status = Status::newGood( array() );
+
+ # Load $this->mTitle->getArticleID() and $this->mLatest if it's not already
+ $this->loadPageData();
+
+ if( !($flags & EDIT_NEW) && !($flags & EDIT_UPDATE) ) {
+ $aid = $this->mTitle->getArticleID();
+ if( $aid ) {
$flags |= EDIT_UPDATE;
} else {
$flags |= EDIT_NEW;
}
}
- if( !wfRunHooks( 'ArticleSave', array( &$this, &$wgUser, &$text,
- &$summary, $flags & EDIT_MINOR,
- null, null, &$flags ) ) )
+ if( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
+ $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
{
wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
wfProfileOut( __METHOD__ );
- return false;
+ if( $status->isOK() ) {
+ $status->fatal( 'edit-hook-aborted');
+ }
+ return $status;
}
# Silently ignore EDIT_MINOR if not allowed
- $isminor = ( $flags & EDIT_MINOR ) && $wgUser->isAllowed('minoredit');
+ $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed('minoredit');
$bot = $flags & EDIT_FORCE_BOT;
$oldtext = $this->getContent();
@@ -1417,32 +1505,29 @@ class Article {
$dbw = wfGetDB( DB_MASTER );
$now = wfTimestampNow();
- if ( $flags & EDIT_UPDATE ) {
+ if( $flags & EDIT_UPDATE ) {
# Update article, but only if changed.
-
+ $status->value['new'] = false;
# Make sure the revision is either completely inserted or not inserted at all
if( !$wgDBtransactions ) {
$userAbort = ignore_user_abort( true );
}
- $lastRevision = 0;
$revisionId = 0;
$changed = ( strcmp( $text, $oldtext ) != 0 );
- if ( $changed ) {
+ if( $changed ) {
$this->mGoodAdjustment = (int)$this->isCountable( $text )
- (int)$this->isCountable( $oldtext );
$this->mTotalAdjustment = 0;
- $lastRevision = $dbw->selectField(
- 'page', 'page_latest', array( 'page_id' => $this->getId() ) );
-
- if ( !$lastRevision ) {
+ if( !$this->mLatest ) {
# Article gone missing
wfDebug( __METHOD__.": EDIT_UPDATE specified but article doesn't exist\n" );
+ $status->fatal( 'edit-gone-missing' );
wfProfileOut( __METHOD__ );
- return false;
+ return $status;
}
$revision = new Revision( array(
@@ -1450,38 +1535,54 @@ class Article {
'comment' => $summary,
'minor_edit' => $isminor,
'text' => $text,
- 'parent_id' => $lastRevision
+ 'parent_id' => $this->mLatest,
+ 'user' => $user->getId(),
+ 'user_text' => $user->getName(),
) );
$dbw->begin();
$revisionId = $revision->insertOn( $dbw );
# Update page
- $ok = $this->updateRevisionOn( $dbw, $revision, $lastRevision );
+ #
+ # Note that we use $this->mLatest instead of fetching a value from the master DB
+ # during the course of this function. This makes sure that EditPage can detect
+ # edit conflicts reliably, either by $ok here, or by $article->getTimestamp()
+ # before this function is called. A previous function used a separate query, this
+ # creates a window where concurrent edits can cause an ignored edit conflict.
+ $ok = $this->updateRevisionOn( $dbw, $revision, $this->mLatest );
if( !$ok ) {
/* Belated edit conflict! Run away!! */
- $good = false;
+ $status->fatal( 'edit-conflict' );
+ # Delete the invalid revision if the DB is not transactional
+ if( !$wgDBtransactions ) {
+ $dbw->delete( 'revision', array( 'rev_id' => $revisionId ), __METHOD__ );
+ }
+ $revisionId = 0;
$dbw->rollback();
} else {
- wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId ) );
-
+ global $wgUseRCPatrol;
+ wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, $baseRevId, $user) );
# Update recentchanges
if( !( $flags & EDIT_SUPPRESS_RC ) ) {
- $rcid = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $wgUser, $summary,
- $lastRevision, $this->getTimestamp(), $bot, '', $oldsize, $newsize,
- $revisionId );
-
# Mark as patrolled if the user can do so
- if( $GLOBALS['wgUseRCPatrol'] && $wgUser->isAllowed( 'autopatrol' ) ) {
- RecentChange::markPatrolled( $rcid );
- PatrolLog::record( $rcid, true );
+ $patrolled = $wgUseRCPatrol && $this->mTitle->userCan('autopatrol');
+ # Add RC row to the DB
+ $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary,
+ $this->mLatest, $this->getTimestamp(), $bot, '', $oldsize, $newsize,
+ $revisionId, $patrolled
+ );
+ # Log auto-patrolled edits
+ if( $patrolled ) {
+ PatrolLog::record( $rc, true );
}
}
- $wgUser->incEditCount();
+ $user->incEditCount();
$dbw->commit();
}
} else {
+ $status->warning( 'edit-no-change' );
$revision = null;
// Keep the same revision ID, but do some updates on it
$revisionId = $this->getRevIdFetched();
@@ -1493,17 +1594,20 @@ class Article {
if( !$wgDBtransactions ) {
ignore_user_abort( $userAbort );
}
-
- if ( $good ) {
- # Invalidate cache of this article and all pages using this article
- # as a template. Partly deferred.
- Article::onArticleEdit( $this->mTitle );
-
- # Update links tables, site stats, etc.
- $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed );
+ // Now that ignore_user_abort is restored, we can respond to fatal errors
+ if( !$status->isOK() ) {
+ wfProfileOut( __METHOD__ );
+ return $status;
}
+
+ # Invalidate cache of this article and all pages using this article
+ # as a template. Partly deferred. Leave templatelinks for editUpdates().
+ Article::onArticleEdit( $this->mTitle, 'skiptransclusions' );
+ # Update links tables, site stats, etc.
+ $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed );
} else {
# Create new article
+ $status->value['new'] = true;
# Set statistics members
# We work out if it's countable after PST to avoid counter drift
@@ -1514,15 +1618,24 @@ class Article {
$dbw->begin();
# Add the page record; stake our claim on this title!
- # This will fail with a database query exception if the article already exists
+ # This will return false if the article already exists
$newid = $this->insertOn( $dbw );
+ if( $newid === false ) {
+ $dbw->rollback();
+ $status->fatal( 'edit-already-exists' );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
# Save the revision text...
$revision = new Revision( array(
'page' => $newid,
'comment' => $summary,
'minor_edit' => $isminor,
- 'text' => $text
+ 'text' => $text,
+ 'user' => $user->getId(),
+ 'user_text' => $user->getName(),
) );
$revisionId = $revision->insertOn( $dbw );
@@ -1530,19 +1643,22 @@ class Article {
# Update the page record with revision data
$this->updateRevisionOn( $dbw, $revision, 0 );
-
- wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false, $user) );
+ # Update recentchanges
if( !( $flags & EDIT_SUPPRESS_RC ) ) {
- $rcid = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, $bot,
- '', strlen( $text ), $revisionId );
- # Mark as patrolled if the user can
- if( ($GLOBALS['wgUseRCPatrol'] || $GLOBALS['wgUseNPPatrol']) && $wgUser->isAllowed( 'autopatrol' ) ) {
- RecentChange::markPatrolled( $rcid );
- PatrolLog::record( $rcid, true );
+ global $wgUseRCPatrol, $wgUseNPPatrol;
+ # Mark as patrolled if the user can do so
+ $patrolled = ($wgUseRCPatrol || $wgUseNPPatrol) && $this->mTitle->userCan('autopatrol');
+ # Add RC row to the DB
+ $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
+ '', strlen($text), $revisionId, $patrolled );
+ # Log auto-patrolled edits
+ if( $patrolled ) {
+ PatrolLog::record( $rc, true );
}
}
- $wgUser->incEditCount();
+ $user->incEditCount();
$dbw->commit();
# Update links, etc.
@@ -1551,27 +1667,30 @@ class Article {
# Clear caches
Article::onArticleCreate( $this->mTitle );
- wfRunHooks( 'ArticleInsertComplete', array( &$this, &$wgUser, $text, $summary,
- $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
+ wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
+ $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
}
- if ( $good && !( $flags & EDIT_DEFER_UPDATES ) ) {
+ # Do updates right now unless deferral was requested
+ if( !( $flags & EDIT_DEFER_UPDATES ) ) {
wfDoUpdates();
}
- if ( $good ) {
- wfRunHooks( 'ArticleSaveComplete', array( &$this, &$wgUser, $text, $summary,
- $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
- }
+ // Return the new revision (or null) to the caller
+ $status->value['revision'] = $revision;
+
+ wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
+ $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status ) );
wfProfileOut( __METHOD__ );
- return $good;
+ return $status;
}
/**
* @deprecated wrapper for doRedirect
*/
- function showArticle( $text, $subtitle , $sectionanchor = '', $me2, $now, $summary, $oldid ) {
+ public function showArticle( $text, $subtitle , $sectionanchor = '', $me2, $now, $summary, $oldid ) {
+ wfDeprecated( __METHOD__ );
$this->doRedirect( $this->isRedirect( $text ), $sectionanchor );
}
@@ -1579,13 +1698,13 @@ class Article {
* Output a redirect back to the article.
* This is typically used after an edit.
*
- * @param boolean $noRedir Add redirect=no
- * @param string $sectionAnchor section to redirect to, including "#"
- * @param string $extraQuery, extra query params
+ * @param $noRedir Boolean: add redirect=no
+ * @param $sectionAnchor String: section to redirect to, including "#"
+ * @param $extraQuery String: extra query params
*/
- function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) {
+ public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) {
global $wgOut;
- if ( $noRedir ) {
+ if( $noRedir ) {
$query = 'redirect=no';
if( $extraQuery )
$query .= "&$query";
@@ -1598,77 +1717,45 @@ class Article {
/**
* Mark this particular edit/page as patrolled
*/
- function markpatrolled() {
+ public function markpatrolled() {
global $wgOut, $wgRequest, $wgUseRCPatrol, $wgUseNPPatrol, $wgUser;
$wgOut->setRobotPolicy( 'noindex,nofollow' );
- # Check patrol config options
-
- if ( !($wgUseNPPatrol || $wgUseRCPatrol)) {
- $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
- return;
- }
-
# If we haven't been given an rc_id value, we can't do anything
$rcid = (int) $wgRequest->getVal('rcid');
- $rc = $rcid ? RecentChange::newFromId($rcid) : null;
- if ( is_null ( $rc ) )
- {
+ $rc = RecentChange::newFromId($rcid);
+ if( is_null($rc) ) {
$wgOut->showErrorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' );
return;
}
- if ( !$wgUseRCPatrol && $rc->getAttribute( 'rc_type' ) != RC_NEW) {
- // Only new pages can be patrolled if the general patrolling is off....???
- // @fixme -- is this necessary? Shouldn't we only bother controlling the
- // front end here?
- $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
- return;
- }
+ #It would be nice to see where the user had actually come from, but for now just guess
+ $returnto = $rc->getAttribute( 'rc_type' ) == RC_NEW ? 'Newpages' : 'Recentchanges';
+ $return = Title::makeTitle( NS_SPECIAL, $returnto );
- # Check permissions
- $permission_errors = $this->mTitle->getUserPermissionsErrors( 'patrol', $wgUser );
+ $dbw = wfGetDB( DB_MASTER );
+ $errors = $rc->doMarkPatrolled();
- if (count($permission_errors)>0)
- {
- $wgOut->showPermissionsErrorPage( $permission_errors );
+ if( in_array(array('rcpatroldisabled'), $errors) ) {
+ $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
return;
}
-
- # Handle the 'MarkPatrolled' hook
- if( !wfRunHooks( 'MarkPatrolled', array( $rcid, &$wgUser, false ) ) ) {
+
+ if( in_array(array('hookaborted'), $errors) ) {
+ // The hook itself has handled any output
return;
}
-
- #It would be nice to see where the user had actually come from, but for now just guess
- $returnto = $rc->getAttribute( 'rc_type' ) == RC_NEW ? 'Newpages' : 'Recentchanges';
- $return = Title::makeTitle( NS_SPECIAL, $returnto );
-
- # If it's left up to us, check that the user is allowed to patrol this edit
- # If the user has the "autopatrol" right, then we'll assume there are no
- # other conditions stopping them doing so
- if( !$wgUser->isAllowed( 'autopatrol' ) ) {
- $rc = RecentChange::newFromId( $rcid );
- # Graceful error handling, as we've done before here...
- # (If the recent change doesn't exist, then it doesn't matter whether
- # the user is allowed to patrol it or not; nothing is going to happen
- if( is_object( $rc ) && $wgUser->getName() == $rc->getAttribute( 'rc_user_text' ) ) {
- # The user made this edit, and can't patrol it
- # Tell them so, and then back off
- $wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) );
- $wgOut->addWikiMsg( 'markedaspatrollederror-noautopatrol' );
- $wgOut->returnToMain( false, $return );
- return;
- }
+
+ if( in_array(array('markedaspatrollederror-noautopatrol'), $errors) ) {
+ $wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) );
+ $wgOut->addWikiMsg( 'markedaspatrollederror-noautopatrol' );
+ $wgOut->returnToMain( false, $return );
+ return;
}
- # Check that the revision isn't patrolled already
- # Prevents duplicate log entries
- if( !$rc->getAttribute( 'rc_patrolled' ) ) {
- # Mark the edit as patrolled
- RecentChange::markPatrolled( $rcid );
- PatrolLog::record( $rcid );
- wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) );
+ if( !empty($errors) ) {
+ $wgOut->showPermissionsErrorPage( $errors );
+ return;
}
# Inform the user
@@ -1681,26 +1768,21 @@ class Article {
* User-interface handler for the "watch" action
*/
- function watch() {
-
+ public function watch() {
global $wgUser, $wgOut;
-
- if ( $wgUser->isAnon() ) {
+ if( $wgUser->isAnon() ) {
$wgOut->showErrorPage( 'watchnologin', 'watchnologintext' );
return;
}
- if ( wfReadOnly() ) {
+ if( wfReadOnly() ) {
$wgOut->readOnlyPage();
return;
}
-
if( $this->doWatch() ) {
$wgOut->setPagetitle( wfMsg( 'addedwatch' ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
-
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->addWikiMsg( 'addedwatchtext', $this->mTitle->getPrefixedText() );
}
-
$wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
}
@@ -1708,44 +1790,36 @@ class Article {
* Add this page to $wgUser's watchlist
* @return bool true on successful watch operation
*/
- function doWatch() {
+ public function doWatch() {
global $wgUser;
if( $wgUser->isAnon() ) {
return false;
}
-
- if (wfRunHooks('WatchArticle', array(&$wgUser, &$this))) {
+ if( wfRunHooks('WatchArticle', array(&$wgUser, &$this)) ) {
$wgUser->addWatch( $this->mTitle );
-
return wfRunHooks('WatchArticleComplete', array(&$wgUser, &$this));
}
-
return false;
}
/**
* User interface handler for the "unwatch" action.
*/
- function unwatch() {
-
+ public function unwatch() {
global $wgUser, $wgOut;
-
- if ( $wgUser->isAnon() ) {
+ if( $wgUser->isAnon() ) {
$wgOut->showErrorPage( 'watchnologin', 'watchnologintext' );
return;
}
- if ( wfReadOnly() ) {
+ if( wfReadOnly() ) {
$wgOut->readOnlyPage();
return;
}
-
if( $this->doUnwatch() ) {
$wgOut->setPagetitle( wfMsg( 'removedwatch' ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
-
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->addWikiMsg( 'removedwatchtext', $this->mTitle->getPrefixedText() );
}
-
$wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
}
@@ -1753,25 +1827,22 @@ class Article {
* Stop watching a page
* @return bool true on successful unwatch
*/
- function doUnwatch() {
+ public function doUnwatch() {
global $wgUser;
if( $wgUser->isAnon() ) {
return false;
}
-
- if (wfRunHooks('UnwatchArticle', array(&$wgUser, &$this))) {
+ if( wfRunHooks('UnwatchArticle', array(&$wgUser, &$this)) ) {
$wgUser->removeWatch( $this->mTitle );
-
return wfRunHooks('UnwatchArticleComplete', array(&$wgUser, &$this));
}
-
return false;
}
/**
* action=protect handler
*/
- function protect() {
+ public function protect() {
$form = new ProtectionForm( $this );
$form->execute();
}
@@ -1779,26 +1850,28 @@ class Article {
/**
* action=unprotect handler (alias)
*/
- function unprotect() {
+ public function unprotect() {
$this->protect();
}
/**
* Update the article's restriction field, and leave a log entry.
*
- * @param array $limit set of restriction keys
- * @param string $reason
+ * @param $limit Array: set of restriction keys
+ * @param $reason String
+ * @param &$cascade Integer. Set to false if cascading protection isn't allowed.
+ * @param $expiry Array: per restriction type expiration
* @return bool true on success
*/
- function updateRestrictions( $limit = array(), $reason = '', $cascade = 0, $expiry = null ) {
+ public function updateRestrictions( $limit = array(), $reason = '', &$cascade = 0, $expiry = array() ) {
global $wgUser, $wgRestrictionTypes, $wgContLang;
$id = $this->mTitle->getArticleID();
- if( array() != $this->mTitle->getUserPermissionsErrors( 'protect', $wgUser ) || wfReadOnly() || $id == 0 ) {
+ if( $id <= 0 || wfReadOnly() || !$this->mTitle->userCan('protect') ) {
return false;
}
- if (!$cascade) {
+ if( !$cascade ) {
$cascade = false;
}
@@ -1808,34 +1881,39 @@ class Article {
# FIXME: Same limitations as described in ProtectionForm.php (line 37);
# we expect a single selection, but the schema allows otherwise.
$current = array();
- foreach( $wgRestrictionTypes as $action )
- $current[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
+ $updated = Article::flattenRestrictions( $limit );
+ $changed = false;
+ foreach( $wgRestrictionTypes as $action ) {
+ if( isset( $expiry[$action] ) ) {
+ # Get current restrictions on $action
+ $aLimits = $this->mTitle->getRestrictions( $action );
+ $current[$action] = implode( '', $aLimits );
+ # Are any actual restrictions being dealt with here?
+ $aRChanged = count($aLimits) || !empty($limit[$action]);
+ # If something changed, we need to log it. Checking $aRChanged
+ # assures that "unprotecting" a page that is not protected does
+ # not log just because the expiry was "changed".
+ if( $aRChanged && $this->mTitle->mRestrictionsExpiry[$action] != $expiry[$action] ) {
+ $changed = true;
+ }
+ }
+ }
$current = Article::flattenRestrictions( $current );
- $updated = Article::flattenRestrictions( $limit );
- $changed = ( $current != $updated );
+ $changed = ($changed || $current != $updated );
$changed = $changed || ($updated && $this->mTitle->areRestrictionsCascading() != $cascade);
- $changed = $changed || ($updated && $this->mTitle->mRestrictionsExpiry != $expiry);
$protect = ( $updated != '' );
# If nothing's changed, do nothing
if( $changed ) {
- global $wgGroupPermissions;
if( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) {
$dbw = wfGetDB( DB_MASTER );
-
- $encodedExpiry = Block::encodeExpiry($expiry, $dbw );
-
- $expiry_description = '';
- if ( $encodedExpiry != 'infinity' ) {
- $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry, false, false ) ).')';
- }
-
+
# Prepare a null revision to be added to the history
$modified = $current != '' && $protect;
- if ( $protect ) {
+ if( $protect ) {
$comment_type = $modified ? 'modifiedarticleprotection' : 'protectedarticle';
} else {
$comment_type = 'unprotectedarticle';
@@ -1844,35 +1922,51 @@ class Article {
# Only restrictions with the 'protect' right can cascade...
# Otherwise, people who cannot normally protect can "protect" pages via transclusion
- foreach( $limit as $action => $restriction ) {
- # FIXME: can $restriction be an array or what? (same as fixme above)
- if( $restriction != 'protect' && $restriction != 'sysop' ) {
- $cascade = false;
- break;
- }
- }
-
- $cascade_description = '';
- if ($cascade) {
- $cascade_description = ' ['.wfMsg('protect-summary-cascade').']';
+ $editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' );
+ # The schema allows multiple restrictions
+ if(!in_array('protect', $editrestriction) && !in_array('sysop', $editrestriction))
+ $cascade = false;
+ $cascade_description = '';
+ if( $cascade ) {
+ $cascade_description = ' ['.wfMsgForContent('protect-summary-cascade').']';
}
if( $reason )
$comment .= ": $reason";
- if( $protect )
- $comment .= " [$updated]";
- if ( $expiry_description && $protect )
- $comment .= "$expiry_description";
- if ( $cascade )
- $comment .= "$cascade_description";
+ $editComment = $comment;
+ $encodedExpiry = array();
+ $protect_description = '';
+ foreach( $limit as $action => $restrictions ) {
+ $encodedExpiry[$action] = Block::encodeExpiry($expiry[$action], $dbw );
+ if( $restrictions != '' ) {
+ $protect_description .= "[$action=$restrictions] (";
+ if( $encodedExpiry[$action] != 'infinity' ) {
+ $protect_description .= wfMsgForContent( 'protect-expiring',
+ $wgContLang->timeanddate( $expiry[$action], false, false ) ,
+ $wgContLang->date( $expiry[$action], false, false ) ,
+ $wgContLang->time( $expiry[$action], false, false ) );
+ } else {
+ $protect_description .= wfMsgForContent( 'protect-expiry-indefinite' );
+ }
+ $protect_description .= ') ';
+ }
+ }
+ $protect_description = trim($protect_description);
+
+ if( $protect_description && $protect )
+ $editComment .= " ($protect_description)";
+ if( $cascade )
+ $editComment .= "$cascade_description";
# Update restrictions table
foreach( $limit as $action => $restrictions ) {
- if ($restrictions != '' ) {
+ if($restrictions != '' ) {
$dbw->replace( 'page_restrictions', array(array('pr_page', 'pr_type')),
- array( 'pr_page' => $id, 'pr_type' => $action
- , 'pr_level' => $restrictions, 'pr_cascade' => $cascade ? 1 : 0
- , 'pr_expiry' => $encodedExpiry ), __METHOD__ );
+ array( 'pr_page' => $id,
+ 'pr_type' => $action,
+ 'pr_level' => $restrictions,
+ 'pr_cascade' => ($cascade && $action == 'edit') ? 1 : 0,
+ 'pr_expiry' => $encodedExpiry[$action] ), __METHOD__ );
} else {
$dbw->delete( 'page_restrictions', array( 'pr_page' => $id,
'pr_type' => $action ), __METHOD__ );
@@ -1880,9 +1974,10 @@ class Article {
}
# Insert a null revision
- $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true );
+ $nullRevision = Revision::newNullRevision( $dbw, $id, $editComment, true );
$nullRevId = $nullRevision->insertOn( $dbw );
+ $latest = $this->getLatest();
# Update page record
$dbw->update( 'page',
array( /* SET */
@@ -1893,15 +1988,15 @@ class Article {
'page_id' => $id
), 'Article::protect'
);
-
- wfRunHooks( 'NewRevisionFromEditComplete', array($this, $nullRevision, false) );
+
+ wfRunHooks( 'NewRevisionFromEditComplete', array($this, $nullRevision, $latest, $wgUser) );
wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) );
# Update the protection log
$log = new LogPage( 'protect' );
if( $protect ) {
- $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle,
- trim( $reason . " [$updated]$cascade_description$expiry_description" ) );
+ $params = array($protect_description,$cascade ? 'cascade' : '');
+ $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason), $params );
} else {
$log->addEntry( 'unprotect', $this->mTitle, $reason );
}
@@ -1915,11 +2010,10 @@ class Article {
/**
* Take an array of page restrictions and flatten it to a string
* suitable for insertion into the page_restrictions field.
- * @param array $limit
- * @return string
- * @private
+ * @param $limit Array
+ * @return String
*/
- function flattenRestrictions( $limit ) {
+ protected static function flattenRestrictions( $limit ) {
if( !is_array( $limit ) ) {
throw new MWException( 'Article::flattenRestrictions given non-array restriction set' );
}
@@ -1935,26 +2029,24 @@ class Article {
/**
* Auto-generates a deletion reason
- * @param bool &$hasHistory Whether the page has a history
+ * @param &$hasHistory Boolean: whether the page has a history
*/
- public function generateReason(&$hasHistory)
- {
+ public function generateReason( &$hasHistory ) {
global $wgContLang;
- $dbw = wfGetDB(DB_MASTER);
+ $dbw = wfGetDB( DB_MASTER );
// Get the last revision
- $rev = Revision::newFromTitle($this->mTitle);
- if(is_null($rev))
+ $rev = Revision::newFromTitle( $this->mTitle );
+ if( is_null( $rev ) )
return false;
+
// Get the article's contents
$contents = $rev->getText();
$blank = false;
// If the page is blank, use the text from the previous revision,
// which can only be blank if there's a move/import/protect dummy revision involved
- if($contents == '')
- {
+ if( $contents == '' ) {
$prev = $rev->getPrevious();
- if($prev)
- {
+ if( $prev ) {
$contents = $prev->getText();
$blank = true;
}
@@ -1963,44 +2055,51 @@ class Article {
// Find out if there was only one contributor
// Only scan the last 20 revisions
$limit = 20;
- $res = $dbw->select('revision', 'rev_user_text', array('rev_page' => $this->getID()), __METHOD__,
- array('LIMIT' => $limit));
- if($res === false)
+ $res = $dbw->select( 'revision', 'rev_user_text',
+ array( 'rev_page' => $this->getID() ), __METHOD__,
+ array( 'LIMIT' => $limit )
+ );
+ if( $res === false )
// This page has no revisions, which is very weird
return false;
- if($res->numRows() > 1)
+ if( $res->numRows() > 1 )
$hasHistory = true;
else
$hasHistory = false;
- $row = $dbw->fetchObject($res);
+ $row = $dbw->fetchObject( $res );
$onlyAuthor = $row->rev_user_text;
// Try to find a second contributor
- while( $row = $dbw->fetchObject($res) ) {
- if($row->rev_user_text != $onlyAuthor) {
+ foreach( $res as $row ) {
+ if( $row->rev_user_text != $onlyAuthor ) {
$onlyAuthor = false;
break;
}
}
- $dbw->freeResult($res);
+ $dbw->freeResult( $res );
// Generate the summary with a '$1' placeholder
- if($blank) {
+ if( $blank ) {
// The current revision is blank and the one before is also
// blank. It's just not our lucky day
- $reason = wfMsgForContent('exbeforeblank', '$1');
+ $reason = wfMsgForContent( 'exbeforeblank', '$1' );
} else {
- if($onlyAuthor)
- $reason = wfMsgForContent('excontentauthor', '$1', $onlyAuthor);
+ if( $onlyAuthor )
+ $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor );
else
- $reason = wfMsgForContent('excontent', '$1');
+ $reason = wfMsgForContent( 'excontent', '$1' );
+ }
+
+ if( $reason == '-' ) {
+ // Allow these UI messages to be blanked out cleanly
+ return '';
}
// Replace newlines with spaces to prevent uglyness
- $contents = preg_replace("/[\n\r]/", ' ', $contents);
+ $contents = preg_replace( "/[\n\r]/", ' ', $contents );
// Calculate the maximum amount of chars to get
// Max content length = max comment length - length of the comment (excl. $1) - '...'
- $maxLength = 255 - (strlen($reason) - 2) - 3;
- $contents = $wgContLang->truncate($contents, $maxLength, '...');
+ $maxLength = 255 - (strlen( $reason ) - 2) - 3;
+ $contents = $wgContLang->truncate( $contents, $maxLength, '...' );
// Remove possible unfinished links
$contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
// Now replace the '$1' placeholder
@@ -2012,7 +2111,7 @@ class Article {
/*
* UI entry point for page deletion
*/
- function delete() {
+ public function delete() {
global $wgUser, $wgOut, $wgRequest;
$confirm = $wgRequest->wasPosted() &&
@@ -2023,19 +2122,19 @@ class Article {
$reason = $this->DeleteReasonList;
- if ( $reason != 'other' && $this->DeleteReason != '') {
+ if( $reason != 'other' && $this->DeleteReason != '' ) {
// Entry from drop down menu + additional comment
$reason .= ': ' . $this->DeleteReason;
- } elseif ( $reason == 'other' ) {
+ } elseif( $reason == 'other' ) {
$reason = $this->DeleteReason;
}
# Flag to hide all contents of the archived revisions
- $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed('suppressrevision');
+ $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed( 'suppressrevision' );
# This code desperately needs to be totally rewritten
# Read-only check...
- if ( wfReadOnly() ) {
+ if( wfReadOnly() ) {
$wgOut->readOnlyPage();
return;
}
@@ -2043,7 +2142,7 @@ class Article {
# Check permissions
$permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser );
- if (count($permission_errors)>0) {
+ if( count( $permission_errors ) > 0 ) {
$wgOut->showPermissionsErrorPage( $permission_errors );
return;
}
@@ -2054,8 +2153,10 @@ class Article {
$dbw = wfGetDB( DB_MASTER );
$conds = $this->mTitle->pageCond();
$latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
- if ( $latest === false ) {
- $wgOut->showFatalError( wfMsg( 'cannotdelete' ) );
+ if( $latest === false ) {
+ $wgOut->showFatalError( wfMsgExt( 'cannotdelete', array( 'parse' ) ) );
+ $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
+ LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTitle->getPrefixedText() );
return;
}
@@ -2080,12 +2181,12 @@ class Article {
// Generate deletion reason
$hasHistory = false;
- if ( !$reason ) $reason = $this->generateReason($hasHistory);
+ if( !$reason ) $reason = $this->generateReason($hasHistory);
// If the page has a history, insert a warning
if( $hasHistory && !$confirm ) {
- $skin=$wgUser->getSkin();
- $wgOut->addHTML( '<strong>' . wfMsg( 'historywarning' ) . ' ' . $skin->historyLink() . '</strong>' );
+ $skin = $wgUser->getSkin();
+ $wgOut->addHTML( '<strong>' . wfMsgExt( 'historywarning', array( 'parseinline' ) ) . ' ' . $skin->historyLink() . '</strong>' );
if( $bigHistory ) {
global $wgLang, $wgDeleteRevisionsLimit;
$wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n",
@@ -2099,7 +2200,7 @@ class Article {
/**
* @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions
*/
- function isBigDeletion() {
+ public function isBigDeletion() {
global $wgDeleteRevisionsLimit;
if( $wgDeleteRevisionsLimit ) {
$revCount = $this->estimateRevisionCount();
@@ -2111,8 +2212,8 @@ class Article {
/**
* @return int approximate revision count
*/
- function estimateRevisionCount() {
- $dbr = wfGetDB();
+ public function estimateRevisionCount() {
+ $dbr = wfGetDB( DB_SLAVE );
// For an exact count...
//return $dbr->selectField( 'revision', 'COUNT(*)',
// array( 'rev_page' => $this->getId() ), __METHOD__ );
@@ -2122,13 +2223,12 @@ class Article {
/**
* Get the last N authors
- * @param int $num Number of revisions to get
- * @param string $revLatest The latest rev_id, selected from the master (optional)
+ * @param $num Integer: number of revisions to get
+ * @param $revLatest String: the latest rev_id, selected from the master (optional)
* @return array Array of authors, duplicates not removed
*/
- function getLastNAuthors( $num, $revLatest = 0 ) {
+ public function getLastNAuthors( $num, $revLatest = 0 ) {
wfProfileIn( __METHOD__ );
-
// First try the slave
// If that doesn't have the latest revision, try the master
$continue = 2;
@@ -2145,12 +2245,12 @@ class Article {
'LIMIT' => $num
) )
);
- if ( !$res ) {
+ if( !$res ) {
wfProfileOut( __METHOD__ );
return array();
}
$row = $db->fetchObject( $res );
- if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) {
+ if( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) {
$db = wfGetDB( DB_MASTER );
$continue--;
} else {
@@ -2168,59 +2268,67 @@ class Article {
/**
* Output deletion confirmation dialog
- * @param $reason string Prefilled reason
+ * @param $reason String: prefilled reason
*/
- function confirmDelete( $reason ) {
- global $wgOut, $wgUser, $wgContLang;
- $align = $wgContLang->isRtl() ? 'left' : 'right';
+ public function confirmDelete( $reason ) {
+ global $wgOut, $wgUser;
wfDebug( "Article::confirmDelete\n" );
- $wgOut->setSubtitle( wfMsg( 'delete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->mTitle ) ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setSubtitle( wfMsgHtml( 'delete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->mTitle ) ) );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->addWikiMsg( 'confirmdeletetext' );
if( $wgUser->isAllowed( 'suppressrevision' ) ) {
- $suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\"><td></td><td>";
- $suppress .= Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '2' ) );
- $suppress .= "</td></tr>";
+ $suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\">
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'revdelete-suppress' ),
+ 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) .
+ "</td>
+ </tr>";
} else {
$suppress = '';
}
+ $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching();
- $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->mTitle->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) .
+ $form = Xml::openElement( 'form', array( 'method' => 'post',
+ 'action' => $this->mTitle->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) .
Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) .
Xml::tags( 'legend', null, wfMsgExt( 'delete-legend', array( 'parsemag', 'escapenoentities' ) ) ) .
- Xml::openElement( 'table' ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-deleteconfirm-table' ) ) .
"<tr id=\"wpDeleteReasonListRow\">
- <td align='$align'>" .
+ <td class='mw-label'>" .
Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' ) .
"</td>
- <td>" .
+ <td class='mw-input'>" .
Xml::listDropDown( 'wpDeleteReasonList',
wfMsgForContent( 'deletereason-dropdown' ),
wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) .
"</td>
</tr>
<tr id=\"wpDeleteReasonRow\">
- <td align='$align'>" .
+ <td class='mw-label'>" .
Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) .
"</td>
- <td>" .
- Xml::input( 'wpReason', 60, $reason, array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) .
+ <td class='mw-input'>" .
+ Xml::input( 'wpReason', 60, $reason, array( 'type' => 'text', 'maxlength' => '255',
+ 'tabindex' => '2', 'id' => 'wpReason' ) ) .
"</td>
</tr>
<tr>
<td></td>
- <td>" .
- Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '3' ) ) .
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'watchthis' ),
+ 'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) .
"</td>
</tr>
$suppress
<tr>
<td></td>
- <td>" .
- Xml::submitButton( wfMsg( 'deletepage' ), array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '4' ) ) .
+ <td class='mw-submit'>" .
+ Xml::submitButton( wfMsg( 'deletepage' ),
+ array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '5' ) ) .
"</td>
</tr>" .
Xml::closeElement( 'table' ) .
@@ -2228,43 +2336,30 @@ class Article {
Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
Xml::closeElement( 'form' );
- if ( $wgUser->isAllowed( 'editinterface' ) ) {
+ if( $wgUser->isAllowed( 'editinterface' ) ) {
$skin = $wgUser->getSkin();
$link = $skin->makeLink ( 'MediaWiki:Deletereason-dropdown', wfMsgHtml( 'delete-edit-reasonlist' ) );
$form .= '<p class="mw-delete-editreasons">' . $link . '</p>';
}
$wgOut->addHTML( $form );
- $this->showLogExtract( $wgOut );
- }
-
-
- /**
- * Show relevant lines from the deletion log
- */
- function showLogExtract( $out ) {
- $out->addHtml( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
- LogEventsList::showLogExtract( $out, 'delete', $this->mTitle->getPrefixedText() );
+ LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTitle->getPrefixedText() );
}
-
/**
* Perform a deletion and output success or failure messages
*/
- function doDelete( $reason, $suppress = false ) {
+ public function doDelete( $reason, $suppress = false ) {
global $wgOut, $wgUser;
- wfDebug( __METHOD__."\n" );
-
- $id = $this->getId();
-
- $error = '';
+ $id = $this->mTitle->getArticleID( GAID_FOR_UPDATE );
- if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason, &$error))) {
- if ( $this->doDeleteArticle( $reason, $suppress ) ) {
+ $error = '';
+ if( wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason, &$error)) ) {
+ if( $this->doDeleteArticle( $reason, $suppress, $id ) ) {
$deleted = $this->mTitle->getPrefixedText();
$wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]';
@@ -2272,10 +2367,13 @@ class Article {
$wgOut->returnToMain( false );
wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason, $id));
} else {
- if ($error = '')
- $wgOut->showFatalError( wfMsg( 'cannotdelete' ) );
- else
+ if( $error == '' ) {
+ $wgOut->showFatalError( wfMsgExt( 'cannotdelete', array( 'parse' ) ) );
+ $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
+ LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTitle->getPrefixedText() );
+ } else {
$wgOut->showFatalError( $error );
+ }
}
}
}
@@ -2285,7 +2383,7 @@ class Article {
* Deletes the article with database consistency, writes logs, purges caches
* Returns success
*/
- function doDeleteArticle( $reason, $suppress = false ) {
+ public function doDeleteArticle( $reason, $suppress = false, $id = 0 ) {
global $wgUseSquid, $wgDeferredUpdateList;
global $wgUseTrackbacks;
@@ -2294,9 +2392,9 @@ class Article {
$dbw = wfGetDB( DB_MASTER );
$ns = $this->mTitle->getNamespace();
$t = $this->mTitle->getDBkey();
- $id = $this->mTitle->getArticleID();
+ $id = $id ? $id : $this->mTitle->getArticleID( GAID_FOR_UPDATE );
- if ( $t == '' || $id == 0 ) {
+ if( $t == '' || $id == 0 ) {
return false;
}
@@ -2304,7 +2402,7 @@ class Article {
array_push( $wgDeferredUpdateList, $u );
// Bitfields to further suppress the content
- if ( $suppress ) {
+ if( $suppress ) {
$bitfield = 0;
// This should be 15...
$bitfield |= Revision::DELETED_TEXT;
@@ -2351,15 +2449,6 @@ class Article {
# Delete restrictions for it
$dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
- # Fix category table counts
- $cats = array();
- $res = $dbw->select( 'categorylinks', 'cl_to',
- array( 'cl_from' => $id ), __METHOD__ );
- foreach( $res as $row ) {
- $cats []= $row->cl_to;
- }
- $this->updateCategoryCounts( array(), $cats );
-
# Now that it's safely backed up, delete it
$dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__);
$ok = ( $dbw->affectedRows() > 0 ); // getArticleId() uses slave, could be laggy
@@ -2367,12 +2456,20 @@ class Article {
$dbw->rollback();
return false;
}
+
+ # Fix category table counts
+ $cats = array();
+ $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
+ foreach( $res as $row ) {
+ $cats []= $row->cl_to;
+ }
+ $this->updateCategoryCounts( array(), $cats );
# If using cascading deletes, we can skip some explicit deletes
- if ( !$dbw->cascadingDeletes() ) {
+ if( !$dbw->cascadingDeletes() ) {
$dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
- if ($wgUseTrackbacks)
+ if($wgUseTrackbacks)
$dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ );
# Delete outgoing links
@@ -2386,14 +2483,17 @@ class Article {
}
# If using cleanup triggers, we can skip some manual deletes
- if ( !$dbw->cleanupTriggers() ) {
-
+ if( !$dbw->cleanupTriggers() ) {
# Clean up recentchanges entries...
$dbw->delete( 'recentchanges',
- array( 'rc_namespace' => $ns, 'rc_title' => $t, 'rc_type != '.RC_LOG ),
+ array( 'rc_type != '.RC_LOG,
+ 'rc_namespace' => $this->mTitle->getNamespace(),
+ 'rc_title' => $this->mTitle->getDBKey() ),
+ __METHOD__ );
+ $dbw->delete( 'recentchanges',
+ array( 'rc_type != '.RC_LOG, 'rc_cur_id' => $id ),
__METHOD__ );
}
- $dbw->commit();
# Clear caches
Article::onArticleDelete( $this->mTitle );
@@ -2409,6 +2509,8 @@ class Article {
# Make sure logging got through
$log->addEntry( 'delete', $this->mTitle, $reason, array() );
+ $dbw->commit();
+
return true;
}
@@ -2419,12 +2521,12 @@ class Article {
* performs permissions checks on $wgUser, then calls commitRollback()
* to do the dirty work
*
- * @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 $fromP String: Name of the user whose edits to rollback.
+ * @param $summary String: Custom summary. Set to default summary if empty.
+ * @param $token String: Rollback token.
+ * @param $bot Boolean: If true, mark all reverted edits as bot.
*
- * @param array $resultDetails contains result-specific array of additional values
+ * @param $resultDetails Array: contains result-specific array of additional values
* 'alreadyrolled' : 'current' (rev)
* success : 'summary' (str), 'current' (rev), 'target' (rev)
*
@@ -2438,16 +2540,18 @@ class Article {
$resultDetails = null;
# Check permissions
- $errors = array_merge( $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ),
- $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ) );
+ $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser );
+ $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser );
+ $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
+
if( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) )
$errors[] = array( 'sessionfailure' );
- if ( $wgUser->pingLimiter('rollback') || $wgUser->pingLimiter() ) {
+ if( $wgUser->pingLimiter( 'rollback' ) || $wgUser->pingLimiter() ) {
$errors[] = array( 'actionthrottledtext' );
}
# If there were errors, bail out now
- if(!empty($errors))
+ if( !empty( $errors ) )
return $errors;
return $this->commitRollback($fromP, $summary, $bot, $resultDetails);
@@ -2493,7 +2597,7 @@ class Article {
$s = $dbw->selectRow( 'revision',
array( 'rev_id', 'rev_timestamp', 'rev_deleted' ),
array( 'rev_page' => $current->getPage(),
- "rev_user <> {$user} OR rev_user_text <> {$user_text}"
+ "rev_user != {$user} OR rev_user_text != {$user_text}"
), __METHOD__,
array( 'USE INDEX' => 'page_timestamp',
'ORDER BY' => 'rev_timestamp DESC' )
@@ -2507,16 +2611,16 @@ class Article {
}
$set = array();
- if ( $bot && $wgUser->isAllowed('markbotedits') ) {
+ if( $bot && $wgUser->isAllowed('markbotedits') ) {
# Mark all reverted edits as bot
$set['rc_bot'] = 1;
}
- if ( $wgUseRCPatrol ) {
+ if( $wgUseRCPatrol ) {
# Mark all reverted edits as patrolled
$set['rc_patrolled'] = 1;
}
- if ( $set ) {
+ if( $set ) {
$dbw->update( 'recentchanges', $set,
array( /* WHERE */
'rc_cur_id' => $current->getPage(),
@@ -2531,31 +2635,38 @@ class Article {
if( empty( $summary ) ){
$summary = wfMsgForContent( 'revertpage' );
}
-
+
# Allow the custom summary to use the same args as the default message
$args = array(
$target->getUserText(), $from, $s->rev_id,
$wgLang->timeanddate(wfTimestamp(TS_MW, $s->rev_timestamp), true),
$current->getId(), $wgLang->timeanddate($current->getTimestamp())
);
- $summary = wfMsgReplaceArgs( $summary, $args );
+ $summary = wfMsgReplaceArgs( $summary, $args );
# Save
$flags = EDIT_UPDATE;
- if ($wgUser->isAllowed('minoredit'))
+ if( $wgUser->isAllowed('minoredit') )
$flags |= EDIT_MINOR;
if( $bot && ($wgUser->isAllowed('markbotedits') || $wgUser->isAllowed('bot')) )
$flags |= EDIT_FORCE_BOT;
- $this->doEdit( $target->getText(), $summary, $flags, $target->getId() );
+ # Actually store the edit
+ $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId() );
+ if( !empty( $status->value['revision'] ) ) {
+ $revId = $status->value['revision']->getId();
+ } else {
+ $revId = false;
+ }
- wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target ) );
+ wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target, $current ) );
$resultDetails = array(
'summary' => $summary,
'current' => $current,
'target' => $target,
+ 'newid' => $revId
);
return array();
}
@@ -2563,7 +2674,7 @@ class Article {
/**
* User interface for rollback operations
*/
- function rollback() {
+ public function rollback() {
global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol;
$details = null;
@@ -2575,15 +2686,11 @@ class Article {
$details
);
- if( in_array( array( 'blocked' ), $result ) ) {
- $wgOut->blockedPage();
- return;
- }
if( in_array( array( 'actionthrottledtext' ), $result ) ) {
$wgOut->rateLimited();
return;
}
- if( isset( $result[0][0] ) && ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) ){
+ if( isset( $result[0][0] ) && ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) ) {
$wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
$errArray = $result[0];
$errMsg = array_shift( $errArray );
@@ -2591,7 +2698,8 @@ class Article {
if( isset( $details['current'] ) ){
$current = $details['current'];
if( $current->getComment() != '' ) {
- $wgOut->addWikiMsgArray( 'editcomment', array( $wgUser->getSkin()->formatComment( $current->getComment() ) ), array( 'replaceafter' ) );
+ $wgOut->addWikiMsgArray( 'editcomment', array(
+ $wgUser->getSkin()->formatComment( $current->getComment() ) ), array( 'replaceafter' ) );
}
}
return;
@@ -2599,7 +2707,7 @@ class Article {
# Display permissions errors before read-only message -- there's no
# point in misleading the user into thinking the inability to rollback
# is only temporary.
- if( !empty($result) && $result !== array( array('readonlytext') ) ) {
+ if( !empty( $result ) && $result !== array( array( 'readonlytext' ) ) ) {
# array_diff is completely broken for arrays of arrays, sigh. Re-
# move any 'readonlytext' error manually.
$out = array();
@@ -2611,24 +2719,25 @@ class Article {
$wgOut->showPermissionsErrorPage( $out );
return;
}
- if( $result == array( array('readonlytext') ) ) {
+ if( $result == array( array( 'readonlytext' ) ) ) {
$wgOut->readOnlyPage();
return;
}
$current = $details['current'];
$target = $details['target'];
+ $newId = $details['newid'];
$wgOut->setPageTitle( wfMsg( 'actioncomplete' ) );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() )
. $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() );
$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->addHTML( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) );
$wgOut->returnToMain( false, $this->mTitle );
-
- if( !$wgRequest->getBool( 'hidediff', false ) ) {
- $de = new DifferenceEngine( $this->mTitle, $current->getId(), 'next', false, true );
+
+ if( !$wgRequest->getBool( 'hidediff', false ) && !$wgUser->getBoolOption( 'norollbackdiff', false ) ) {
+ $de = new DifferenceEngine( $this->mTitle, $current->getId(), $newId, false, true );
$de->showDiff( '', '' );
}
}
@@ -2636,21 +2745,15 @@ class Article {
/**
* Do standard deferred updates after page view
- * @private
*/
- function viewUpdates() {
- global $wgDeferredUpdateList, $wgUser;
-
- if ( 0 != $this->getID() ) {
- # Don't update page view counters on views from bot users (bug 14044)
- global $wgDisableCounters;
- if( !$wgDisableCounters && !$wgUser->isAllowed( 'bot' ) ) {
- Article::incViewCount( $this->getID() );
- $u = new SiteStatsUpdate( 1, 0, 0 );
- array_push( $wgDeferredUpdateList, $u );
- }
+ public function viewUpdates() {
+ global $wgDeferredUpdateList, $wgDisableCounters, $wgUser;
+ # Don't update page view counters on views from bot users (bug 14044)
+ if( !$wgDisableCounters && !$wgUser->isAllowed('bot') && $this->getID() ) {
+ Article::incViewCount( $this->getID() );
+ $u = new SiteStatsUpdate( 1, 0, 0 );
+ array_push( $wgDeferredUpdateList, $u );
}
-
# Update newtalk / watchlist notification status
$wgUser->clearNotification( $this->mTitle );
}
@@ -2659,8 +2762,8 @@ class Article {
* Prepare text which is about to be saved.
* Returns a stdclass with source, pst and output members
*/
- function prepareTextForEdit( $text, $revid=null ) {
- if ( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid) {
+ public function prepareTextForEdit( $text, $revid=null ) {
+ if( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid) {
// Already prepared
return $this->mPreparedEdit;
}
@@ -2681,6 +2784,7 @@ class Article {
/**
* Do standard deferred updates after page edit.
* Update links tables, site stats, search index and message cache.
+ * Purges pages that include this page if the text was changed here.
* Every 100th edit, prune the recent changes table.
*
* @private
@@ -2691,14 +2795,14 @@ class Article {
* @param $newid rev_id value of the new revision
* @param $changed Whether or not the content actually changed
*/
- function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true ) {
+ public function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true ) {
global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser, $wgEnableParserCache;
wfProfileIn( __METHOD__ );
# Parse the text
# Be careful not to double-PST: $text is usually already PST-ed once
- if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
+ if( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
$editInfo = $this->prepareTextForEdit( $text, $newid );
} else {
@@ -2707,17 +2811,20 @@ class Article {
}
# Save it to the parser cache
- if ( $wgEnableParserCache ) {
+ if( $wgEnableParserCache ) {
$parserCache = ParserCache::singleton();
$parserCache->save( $editInfo->output, $this, $wgUser );
}
# Update the links tables
- $u = new LinksUpdate( $this->mTitle, $editInfo->output );
+ $u = new LinksUpdate( $this->mTitle, $editInfo->output, false );
+ $u->setRecursiveTouch( $changed ); // refresh/invalidate including pages too
$u->doUpdate();
+
+ wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $changed ) );
if( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
- if ( 0 == mt_rand( 0, 99 ) ) {
+ 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;
@@ -2733,7 +2840,7 @@ class Article {
$title = $this->mTitle->getPrefixedDBkey();
$shortTitle = $this->mTitle->getDBkey();
- if ( 0 == $id ) {
+ if( 0 == $id ) {
wfProfileOut( __METHOD__ );
return;
}
@@ -2748,21 +2855,23 @@ class Article {
# load of user talk pages and piss people off, nor if it's a minor edit
# by a properly-flagged bot.
if( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed
- && !($minoredit && $wgUser->isAllowed('nominornewtalk') ) ) {
- if (wfRunHooks('ArticleEditUpdateNewTalk', array(&$this)) ) {
- $other = User::newFromName( $shortTitle );
- if( is_null( $other ) && User::isIP( $shortTitle ) ) {
+ && !( $minoredit && $wgUser->isAllowed( 'nominornewtalk' ) ) ) {
+ if( wfRunHooks('ArticleEditUpdateNewTalk', array( &$this ) ) ) {
+ $other = User::newFromName( $shortTitle, false );
+ if( !$other ) {
+ wfDebug( __METHOD__.": invalid username\n" );
+ } elseif( User::isIP( $shortTitle ) ) {
// An anonymous user
- $other = new User();
- $other->setName( $shortTitle );
- }
- if( $other ) {
$other->setNewtalk( true );
+ } elseif( $other->isLoggedIn() ) {
+ $other->setNewtalk( true );
+ } else {
+ wfDebug( __METHOD__. ": don't need to notify a nonexistent user\n" );
}
}
}
- if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
$wgMessageCache->replace( $shortTitle, $text );
}
@@ -2772,13 +2881,13 @@ class Article {
/**
* Perform article updates on a special page creation.
*
- * @param Revision $rev
+ * @param $rev Revision object
*
* @todo This is a shitty interface function. Kill it and replace the
* other shitty functions like editUpdates and such so it's not needed
* anymore.
*/
- function createUpdates( $rev ) {
+ public function createUpdates( $rev ) {
$this->mGoodAdjustment = $this->isCountable( $rev->getText() );
$this->mTotalAdjustment = 1;
$this->editUpdates( $rev->getText(), $rev->getComment(),
@@ -2791,14 +2900,13 @@ class Article {
* Revision as of \<date\>; view current revision
* \<- Previous version | Next Version -\>
*
- * @private
- * @param string $oldid Revision ID of this article revision
+ * @param $oldid String: revision ID of this article revision
*/
- function setOldSubtitle( $oldid=0 ) {
+ public function setOldSubtitle( $oldid = 0 ) {
global $wgLang, $wgOut, $wgUser;
- if ( !wfRunHooks( 'DisplayOldSubtitle', array(&$this, &$oldid) ) ) {
- return;
+ if( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) {
+ return;
}
$revision = Revision::newFromId( $oldid );
@@ -2807,34 +2915,34 @@ class Article {
$td = $wgLang->timeanddate( $this->mTimestamp, true );
$sk = $wgUser->getSkin();
$lnk = $current
- ? wfMsg( 'currentrevisionlink' )
- : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) );
+ ? wfMsgHtml( 'currentrevisionlink' )
+ : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'currentrevisionlink' ) );
$curdiff = $current
- ? wfMsg( 'diff' )
- : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=cur&oldid='.$oldid );
+ ? wfMsgHtml( 'diff' )
+ : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'diff' ), 'diff=cur&oldid='.$oldid );
$prev = $this->mTitle->getPreviousRevisionID( $oldid ) ;
$prevlink = $prev
- ? $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'previousrevision' ), 'direction=prev&oldid='.$oldid )
- : wfMsg( 'previousrevision' );
+ ? $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousrevision' ), 'direction=prev&oldid='.$oldid )
+ : wfMsgHtml( 'previousrevision' );
$prevdiff = $prev
- ? $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=prev&oldid='.$oldid )
- : wfMsg( 'diff' );
+ ? $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'diff' ), 'diff=prev&oldid='.$oldid )
+ : wfMsgHtml( 'diff' );
$nextlink = $current
- ? wfMsg( 'nextrevision' )
- : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'nextrevision' ), 'direction=next&oldid='.$oldid );
+ ? wfMsgHtml( 'nextrevision' )
+ : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextrevision' ), 'direction=next&oldid='.$oldid );
$nextdiff = $current
- ? wfMsg( 'diff' )
- : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=next&oldid='.$oldid );
+ ? wfMsgHtml( 'diff' )
+ : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'diff' ), 'diff=next&oldid='.$oldid );
$cdel='';
if( $wgUser->isAllowed( 'deleterevision' ) ) {
$revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
if( $revision->isCurrent() ) {
// We don't handle top deleted edits too well
- $cdel = wfMsgHtml('rev-delundel');
+ $cdel = wfMsgHtml( 'rev-delundel' );
} else if( !$revision->userCan( Revision::DELETED_RESTRICTED ) ) {
// If revision was hidden from sysops
- $cdel = wfMsgHtml('rev-delundel');
+ $cdel = wfMsgHtml( 'rev-delundel' );
} else {
$cdel = $sk->makeKnownLinkObj( $revdel,
wfMsgHtml('rev-delundel'),
@@ -2855,9 +2963,10 @@ class Article {
? 'revision-info-current'
: 'revision-info';
- $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . wfMsg( $infomsg, $td, $userlinks ) . "</div>\n" .
+ $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . wfMsgExt( $infomsg, array( 'parseinline', 'replaceafter' ), $td, $userlinks, $revision->getID() ) . "</div>\n" .
- "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t";
+ "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsgExt( 'revision-nav', array( 'escapenoentities', 'parsemag', 'replaceafter' ),
+ $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t";
$wgOut->setSubtitle( $r );
}
@@ -2865,9 +2974,9 @@ class Article {
* This function is called right before saving the wikitext,
* so we can do things like signatures and links-in-context.
*
- * @param string $text
+ * @param $text String
*/
- function preSaveTransform( $text ) {
+ public function preSaveTransform( $text ) {
global $wgParser, $wgUser;
return $wgParser->preSaveTransform( $text, $this->mTitle, $wgUser, ParserOptions::newFromUser( $wgUser ) );
}
@@ -2879,17 +2988,16 @@ class Article {
* output to the client that is necessary for this request.
* (that is, it has sent a cached version of the page)
*/
- function tryFileCache() {
+ protected function tryFileCache() {
static $called = false;
if( $called ) {
wfDebug( "Article::tryFileCache(): called twice!?\n" );
- return;
+ return false;
}
$called = true;
- if($this->isFileCacheable()) {
- $touched = $this->mTouched;
+ if( $this->isFileCacheable() ) {
$cache = new HTMLFileCache( $this->mTitle );
- if($cache->isFileCacheGood( $touched )) {
+ if( $cache->isFileCacheGood( $this->mTouched ) ) {
wfDebug( "Article::tryFileCache(): about to load file\n" );
$cache->loadFromFileCache();
return true;
@@ -2900,46 +3008,22 @@ class Article {
} else {
wfDebug( "Article::tryFileCache(): not cacheable\n" );
}
+ return false;
}
/**
* Check if the page can be cached
* @return bool
*/
- function isFileCacheable() {
- global $wgUser, $wgUseFileCache, $wgShowIPinHeader, $wgRequest, $wgLang, $wgContLang;
- $action = $wgRequest->getVal( 'action' );
- $oldid = $wgRequest->getVal( 'oldid' );
- $diff = $wgRequest->getVal( 'diff' );
- $redirect = $wgRequest->getVal( 'redirect' );
- $printable = $wgRequest->getVal( 'printable' );
- $page = $wgRequest->getVal( 'page' );
-
- //check for non-standard user language; this covers uselang,
- //and extensions for auto-detecting user language.
- $ulang = $wgLang->getCode();
- $clang = $wgContLang->getCode();
-
- $cacheable = $wgUseFileCache
- && (!$wgShowIPinHeader)
- && ($this->getID() != 0)
- && ($wgUser->isAnon())
- && (!$wgUser->getNewtalk())
- && ($this->mTitle->getNamespace() != NS_SPECIAL )
- && (empty( $action ) || $action == 'view')
- && (!isset($oldid))
- && (!isset($diff))
- && (!isset($redirect))
- && (!isset($printable))
- && !isset($page)
- && (!$this->mRedirectedFrom)
- && ($ulang === $clang);
-
- if ( $cacheable ) {
- //extension may have reason to disable file caching on some pages.
- $cacheable = wfRunHooks( 'IsFileCacheable', array( $this ) );
+ public function isFileCacheable() {
+ $cacheable = false;
+ if( HTMLFileCache::useFileCache() ) {
+ $cacheable = $this->getID() && !$this->mRedirectedFrom;
+ // Extension may have reason to disable file caching on some pages.
+ if( $cacheable ) {
+ $cacheable = wfRunHooks( 'IsFileCacheable', array( &$this ) );
+ }
}
-
return $cacheable;
}
@@ -2947,7 +3031,7 @@ class Article {
* Loads page_touched and returns a value indicating if it should be used
*
*/
- function checkTouched() {
+ public function checkTouched() {
if( !$this->mDataLoaded ) {
$this->loadPageData();
}
@@ -2957,7 +3041,7 @@ class Article {
/**
* Get the page_touched field
*/
- function getTouched() {
+ public function getTouched() {
# Ensure that page data has been loaded
if( !$this->mDataLoaded ) {
$this->loadPageData();
@@ -2968,8 +3052,8 @@ class Article {
/**
* Get the page_latest field
*/
- function getLatest() {
- if ( !$this->mDataLoaded ) {
+ public function getLatest() {
+ if( !$this->mDataLoaded ) {
$this->loadPageData();
}
return $this->mLatest;
@@ -2980,15 +3064,14 @@ class Article {
* The article must already exist; link tables etc
* are not updated, caches are not flushed.
*
- * @param string $text text submitted
- * @param string $comment comment submitted
- * @param bool $minor whereas it's a minor modification
+ * @param $text String: text submitted
+ * @param $comment String: comment submitted
+ * @param $minor Boolean: whereas it's a minor modification
*/
- function quickEdit( $text, $comment = '', $minor = 0 ) {
+ public function quickEdit( $text, $comment = '', $minor = 0 ) {
wfProfileIn( __METHOD__ );
$dbw = wfGetDB( DB_MASTER );
- $dbw->begin();
$revision = new Revision( array(
'page' => $this->getId(),
'text' => $text,
@@ -2997,9 +3080,8 @@ class Article {
) );
$revision->insertOn( $dbw );
$this->updateRevisionOn( $dbw, $revision );
- $dbw->commit();
-
- wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false) );
+
+ wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false, $wgUser) );
wfProfileOut( __METHOD__ );
}
@@ -3007,10 +3089,9 @@ class Article {
/**
* Used to increment the view counter
*
- * @static
- * @param integer $id article id
+ * @param $id Integer: article id
*/
- function incViewCount( $id ) {
+ public static function incViewCount( $id ) {
$id = intval( $id );
global $wgHitcounterUpdateFreq, $wgDBtype;
@@ -3043,14 +3124,14 @@ class Article {
wfProfileIn( 'Article::incViewCount-collect' );
$old_user_abort = ignore_user_abort( true );
- if ($wgDBtype == 'mysql')
+ if($wgDBtype == 'mysql')
$dbw->query("LOCK TABLES $hitcounterTable WRITE");
$tabletype = $wgDBtype == 'mysql' ? "ENGINE=HEAP " : '';
$dbw->query("CREATE TEMPORARY TABLE $acchitsTable $tabletype AS ".
"SELECT hc_id,COUNT(*) AS hc_n FROM $hitcounterTable ".
'GROUP BY hc_id');
$dbw->query("DELETE FROM $hitcounterTable");
- if ($wgDBtype == 'mysql') {
+ if($wgDBtype == 'mysql') {
$dbw->query('UNLOCK TABLES');
$dbw->query("UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n ".
'WHERE page_id = hc_id');
@@ -3075,13 +3156,13 @@ class Article {
* This is a good place to put code to clear caches, for instance.
*
* This is called on page move and undelete, as well as edit
- * @static
- * @param $title_obj a title object
+ *
+ * @param $title a title object
*/
- static function onArticleCreate($title) {
- # The talk page isn't in the regular link tables, so we need to update manually:
- if ( $title->isTalkPage() ) {
+ public static function onArticleCreate( $title ) {
+ # Update existence markers on article/talk tabs...
+ if( $title->isTalkPage() ) {
$other = $title->getSubjectPage();
} else {
$other = $title->getTalkPage();
@@ -3094,10 +3175,9 @@ class Article {
$title->deleteTitleProtection();
}
- static function onArticleDelete( $title ) {
- global $wgUseFileCache, $wgMessageCache;
-
- // Update existence markers on article/talk tabs...
+ public static function onArticleDelete( $title ) {
+ global $wgMessageCache;
+ # Update existence markers on article/talk tabs...
if( $title->isTalkPage() ) {
$other = $title->getSubjectPage();
} else {
@@ -3110,17 +3190,14 @@ class Article {
$title->purgeSquid();
# File cache
- if ( $wgUseFileCache ) {
- $cm = new HTMLFileCache( $title );
- @unlink( $cm->fileCacheName() );
- }
+ HTMLFileCache::clearFileCache( $title );
# Messages
if( $title->getNamespace() == NS_MEDIAWIKI ) {
$wgMessageCache->replace( $title->getDBkey(), false );
}
# Images
- if( $title->getNamespace() == NS_IMAGE ) {
+ if( $title->getNamespace() == NS_FILE ) {
$update = new HTMLCacheUpdate( $title, 'imagelinks' );
$update->doUpdate();
}
@@ -3134,11 +3211,12 @@ class Article {
/**
* Purge caches on page update etc
*/
- static function onArticleEdit( $title ) {
- global $wgDeferredUpdateList, $wgUseFileCache;
+ public static function onArticleEdit( $title, $transclusions = 'transclusions' ) {
+ global $wgDeferredUpdateList;
// Invalidate caches of articles which include this page
- $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' );
+ if( $transclusions !== 'skiptransclusions' )
+ $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' );
// Invalidate the caches of all pages which redirect here
$wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'redirect' );
@@ -3146,11 +3224,8 @@ class Article {
# Purge squid for this page only
$title->purgeSquid();
- # Clear file cache
- if ( $wgUseFileCache ) {
- $cm = new HTMLFileCache( $title );
- @unlink( $cm->fileCacheName() );
- }
+ # Clear file cache for this page only
+ HTMLFileCache::clearFileCache( $title );
}
/**#@-*/
@@ -3159,7 +3234,7 @@ class Article {
* Overriden by ImagePage class, only present here to avoid a fatal error
* Called for ?action=revert
*/
- public function revert(){
+ public function revert() {
global $wgOut;
$wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
}
@@ -3167,13 +3242,11 @@ class Article {
/**
* Info about this page
* Called for ?action=info when $wgAllowPageInfo is on.
- *
- * @public
*/
- function info() {
+ public function info() {
global $wgLang, $wgOut, $wgAllowPageInfo, $wgUser;
- if ( !$wgAllowPageInfo ) {
+ if( !$wgAllowPageInfo ) {
$wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
return;
}
@@ -3182,21 +3255,21 @@ class Article {
$wgOut->setPagetitle( $page->getPrefixedText() );
$wgOut->setPageTitleActionText( wfMsg( 'info_short' ) );
- $wgOut->setSubtitle( wfMsg( 'infosubtitle' ) );
+ $wgOut->setSubtitle( wfMsgHtml( 'infosubtitle' ) );
if( !$this->mTitle->exists() ) {
- $wgOut->addHtml( '<div class="noarticletext">' );
+ $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() ) ) );
+ $wgOut->addHTML( htmlspecialchars( wfMsgWeirdKey( $this->mTitle->getText() ) ) );
} else {
$msg = $wgUser->isLoggedIn()
? 'noarticletext'
: 'noarticletextanon';
- $wgOut->addHtml( wfMsgExt( $msg, 'parse' ) );
+ $wgOut->addHTML( wfMsgExt( $msg, 'parse' ) );
}
- $wgOut->addHtml( '</div>' );
+ $wgOut->addHTML( '</div>' );
} else {
$dbr = wfGetDB( DB_SLAVE );
$wl_clause = array(
@@ -3222,7 +3295,6 @@ class Article {
$wgOut->addHTML( '<li>' . wfMsg('numtalkauthors', $wgLang->formatNum( $talkInfo['authors'] ) ) . '</li>' );
}
$wgOut->addHTML( '</ul>' );
-
}
}
@@ -3230,34 +3302,30 @@ class Article {
* Return the total number of edits and number of unique editors
* on a given page. If page does not exist, returns false.
*
- * @param Title $title
+ * @param $title Title object
* @return array
- * @private
*/
- function pageCountInfo( $title ) {
+ protected function pageCountInfo( $title ) {
$id = $title->getArticleId();
if( $id == 0 ) {
return false;
}
-
$dbr = wfGetDB( DB_SLAVE );
-
$rev_clause = array( 'rev_page' => $id );
-
$edits = $dbr->selectField(
'revision',
'COUNT(rev_page)',
$rev_clause,
__METHOD__,
- $this->getSelectOptions() );
-
+ $this->getSelectOptions()
+ );
$authors = $dbr->selectField(
'revision',
'COUNT(DISTINCT rev_user_text)',
$rev_clause,
__METHOD__,
- $this->getSelectOptions() );
-
+ $this->getSelectOptions()
+ );
return array( 'edits' => $edits, 'authors' => $authors );
}
@@ -3265,25 +3333,22 @@ class Article {
* Return a list of templates used by this article.
* Uses the templatelinks table
*
- * @return array Array of Title objects
+ * @return Array of Title objects
*/
- function getUsedTemplates() {
+ public function getUsedTemplates() {
$result = array();
$id = $this->mTitle->getArticleID();
if( $id == 0 ) {
return array();
}
-
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( array( 'templatelinks' ),
array( 'tl_namespace', 'tl_title' ),
array( 'tl_from' => $id ),
- 'Article:getUsedTemplates' );
- if ( false !== $res ) {
- if ( $dbr->numRows( $res ) ) {
- while ( $row = $dbr->fetchObject( $res ) ) {
- $result[] = Title::makeTitle( $row->tl_namespace, $row->tl_title );
- }
+ __METHOD__ );
+ if( $res !== false ) {
+ foreach( $res as $row ) {
+ $result[] = Title::makeTitle( $row->tl_namespace, $row->tl_title );
}
}
$dbr->freeResult( $res );
@@ -3294,26 +3359,23 @@ class Article {
* Returns a list of hidden categories this page is a member of.
* Uses the page_props and categorylinks tables.
*
- * @return array Array of Title objects
+ * @return Array of Title objects
*/
- function getHiddenCategories() {
+ public function getHiddenCategories() {
$result = array();
$id = $this->mTitle->getArticleID();
if( $id == 0 ) {
return array();
}
-
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ),
array( 'cl_to' ),
array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
'page_namespace' => NS_CATEGORY, 'page_title=cl_to'),
- 'Article:getHiddenCategories' );
- if ( false !== $res ) {
- if ( $dbr->numRows( $res ) ) {
- while ( $row = $dbr->fetchObject( $res ) ) {
- $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
- }
+ __METHOD__ );
+ if( $res !== false ) {
+ foreach( $res as $row ) {
+ $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
}
}
$dbr->freeResult( $res );
@@ -3322,17 +3384,18 @@ class Article {
/**
* Return an applicable autosummary if one exists for the given edit.
- * @param string $oldtext The previous text of the page.
- * @param string $newtext The submitted text of the page.
- * @param bitmask $flags A bitmask of flags submitted for the edit.
+ * @param $oldtext String: the previous text of the page.
+ * @param $newtext String: The submitted text of the page.
+ * @param $flags Bitmask: a bitmask of flags submitted for the edit.
* @return string An appropriate autosummary, or an empty string.
*/
public static function getAutosummary( $oldtext, $newtext, $flags ) {
# Decide what kind of autosummary is needed.
# Redirect autosummaries
+ $ot = Title::newFromRedirect( $oldtext );
$rt = Title::newFromRedirect( $newtext );
- if( is_object( $rt ) ) {
+ if( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
return wfMsgForContent( 'autoredircomment', $rt->getFullText() );
}
@@ -3342,14 +3405,14 @@ class Article {
global $wgContLang;
$truncatedtext = $wgContLang->truncate(
str_replace("\n", ' ', $newtext),
- max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new') ) ),
+ max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ),
'...' );
return wfMsgForContent( 'autosumm-new', $truncatedtext );
}
# Blanking autosummaries
if( $oldtext != '' && $newtext == '' ) {
- return wfMsgForContent('autosumm-blank');
+ return wfMsgForContent( 'autosumm-blank' );
} elseif( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500) {
# Removing more than 90% of the article
global $wgContLang;
@@ -3371,11 +3434,11 @@ class Article {
* Saves the text into the parser cache if possible.
* Updates templatelinks if it is out of date.
*
- * @param string $text
- * @param bool $cache
+ * @param $text String
+ * @param $cache Boolean
*/
public function outputWikiText( $text, $cache = true ) {
- global $wgParser, $wgUser, $wgOut, $wgEnableParserCache;
+ global $wgParser, $wgUser, $wgOut, $wgEnableParserCache, $wgUseFileCache;
$popts = $wgOut->parserOptions();
$popts->setTidy(true);
@@ -3384,12 +3447,18 @@ class Article {
$popts, true, true, $this->getRevIdFetched() );
$popts->setTidy(false);
$popts->enableLimitReport( false );
- if ( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) {
+ if( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) {
$parserCache = ParserCache::singleton();
$parserCache->save( $parserOutput, $this, $wgUser );
}
+ // Make sure file cache is not used on uncacheable content.
+ // Output that has magic words in it can still use the parser cache
+ // (if enabled), though it will generally expire sooner.
+ if( $parserOutput->getCacheTime() == -1 || $parserOutput->containsOldMagic() ) {
+ $wgUseFileCache = false;
+ }
- if ( !wfReadOnly() && $this->mTitle->areRestrictionsCascading() ) {
+ if( $this->isCurrent() && !wfReadOnly() && $this->mTitle->areRestrictionsCascading() ) {
// templatelinks table may have become out of sync,
// especially if using variable-based transclusions.
// For paranoia, check if things have changed and if
@@ -3406,15 +3475,13 @@ class Article {
$res = $dbr->select( array( 'templatelinks' ),
array( 'tl_namespace', 'tl_title' ),
array( 'tl_from' => $id ),
- 'Article:getUsedTemplates' );
+ __METHOD__ );
global $wgContLang;
- if ( false !== $res ) {
- if ( $dbr->numRows( $res ) ) {
- while ( $row = $dbr->fetchObject( $res ) ) {
- $tlTemplates[] = $wgContLang->getNsText( $row->tl_namespace ) . ':' . $row->tl_title ;
- }
+ if( $res !== false ) {
+ foreach( $res as $row ) {
+ $tlTemplates[] = $wgContLang->getNsText( $row->tl_namespace ) . ':' . $row->tl_title ;
}
}
@@ -3429,16 +3496,10 @@ class Article {
# Get the diff
$templates_diff = array_diff( $poTemplates, $tlTemplates );
- if ( count( $templates_diff ) > 0 ) {
+ if( count( $templates_diff ) > 0 ) {
# Whee, link updates time.
$u = new LinksUpdate( $this->mTitle, $parserOutput );
-
- $dbw = wfGetDb( DB_MASTER );
- $dbw->begin();
-
$u->doUpdate();
-
- $dbw->commit();
}
}
@@ -3479,12 +3540,12 @@ class Article {
if( $ns == NS_CATEGORY ) {
$addFields[] = 'cat_subcats = cat_subcats + 1';
$removeFields[] = 'cat_subcats = cat_subcats - 1';
- } elseif( $ns == NS_IMAGE ) {
+ } elseif( $ns == NS_FILE ) {
$addFields[] = 'cat_files = cat_files + 1';
$removeFields[] = 'cat_files = cat_files - 1';
}
- if ( $added ) {
+ if( $added ) {
$dbw->update(
'category',
$addFields,
@@ -3492,7 +3553,7 @@ class Article {
__METHOD__
);
}
- if ( $deleted ) {
+ if( $deleted ) {
$dbw->update(
'category',
$removeFields,
diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php
index 7717e001..b29e13f2 100644
--- a/includes/AuthPlugin.php
+++ b/includes/AuthPlugin.php
@@ -38,9 +38,8 @@ class AuthPlugin {
*
* @param $username String: username.
* @return bool
- * @public
*/
- function userExists( $username ) {
+ public function userExists( $username ) {
# Override this!
return false;
}
@@ -54,9 +53,8 @@ class AuthPlugin {
* @param $username String: username.
* @param $password String: user password.
* @return bool
- * @public
*/
- function authenticate( $username, $password ) {
+ public function authenticate( $username, $password ) {
# Override this!
return false;
}
@@ -65,9 +63,8 @@ class AuthPlugin {
* Modify options in the login template.
*
* @param $template UserLoginTemplate object.
- * @public
*/
- function modifyUITemplate( &$template ) {
+ public function modifyUITemplate( &$template ) {
# Override this!
$template->set( 'usedomain', false );
}
@@ -76,9 +73,8 @@ class AuthPlugin {
* Set the domain this plugin is supposed to use when authenticating.
*
* @param $domain String: authentication domain.
- * @public
*/
- function setDomain( $domain ) {
+ public function setDomain( $domain ) {
$this->domain = $domain;
}
@@ -87,9 +83,8 @@ class AuthPlugin {
*
* @param $domain String: authentication domain.
* @return bool
- * @public
*/
- function validDomain( $domain ) {
+ public function validDomain( $domain ) {
# Override this!
return true;
}
@@ -103,9 +98,8 @@ class AuthPlugin {
* forget the & on your function declaration.
*
* @param User $user
- * @public
*/
- function updateUser( &$user ) {
+ public function updateUser( &$user ) {
# Override this and do something
return true;
}
@@ -123,9 +117,8 @@ class AuthPlugin {
* This is just a question, and shouldn't perform any actions.
*
* @return bool
- * @public
*/
- function autoCreate() {
+ public function autoCreate() {
return false;
}
@@ -134,7 +127,7 @@ class AuthPlugin {
*
* @return bool
*/
- function allowPasswordChange() {
+ public function allowPasswordChange() {
return true;
}
@@ -149,9 +142,8 @@ class AuthPlugin {
* @param $user User object.
* @param $password String: password.
* @return bool
- * @public
*/
- function setPassword( $user, $password ) {
+ public function setPassword( $user, $password ) {
return true;
}
@@ -161,9 +153,8 @@ class AuthPlugin {
*
* @param $user User object.
* @return bool
- * @public
*/
- function updateExternalDB( $user ) {
+ public function updateExternalDB( $user ) {
return true;
}
@@ -171,9 +162,8 @@ class AuthPlugin {
* Check to see if external accounts can be created.
* Return true if external accounts can be created.
* @return bool
- * @public
*/
- function canCreateAccounts() {
+ public function canCreateAccounts() {
return false;
}
@@ -186,9 +176,8 @@ class AuthPlugin {
* @param string $email
* @param string $realname
* @return bool
- * @public
*/
- function addUser( $user, $password, $email='', $realname='' ) {
+ public function addUser( $user, $password, $email='', $realname='' ) {
return true;
}
@@ -200,9 +189,8 @@ class AuthPlugin {
* This is just a question, and shouldn't perform any actions.
*
* @return bool
- * @public
*/
- function strict() {
+ public function strict() {
return false;
}
@@ -212,9 +200,8 @@ class AuthPlugin {
*
* @param $username String: username.
* @return bool
- * @public
*/
- function strictUserAuth( $username ) {
+ public function strictUserAuth( $username ) {
return false;
}
@@ -228,9 +215,8 @@ class AuthPlugin {
*
* @param $user User object.
* @param $autocreate bool True if user is being autocreated on login
- * @public
*/
- function initUser( &$user, $autocreate=false ) {
+ public function initUser( &$user, $autocreate=false ) {
# Override this to do something.
}
@@ -238,7 +224,43 @@ class AuthPlugin {
* If you want to munge the case of an account name before the final
* check, now is your chance.
*/
- function getCanonicalName( $username ) {
+ public function getCanonicalName( $username ) {
return $username;
}
+
+ /**
+ * Get an instance of a User object
+ *
+ * @param $user User
+ * @public
+ */
+ public function getUserInstance( User &$user ) {
+ return new AuthPluginUser( $user );
+ }
+}
+
+class AuthPluginUser {
+ function __construct( $user ) {
+ # Override this!
+ }
+
+ public function getId() {
+ # Override this!
+ return -1;
+ }
+
+ public function isLocked() {
+ # Override this!
+ return false;
+ }
+
+ public function isHidden() {
+ # Override this!
+ return false;
+ }
+
+ public function resetAuthToken() {
+ # Override this!
+ return true;
+ }
}
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php
index de75b41d..ce1912ea 100644
--- a/includes/AutoLoader.php
+++ b/includes/AutoLoader.php
@@ -4,483 +4,544 @@
ini_set('unserialize_callback_func', '__autoload' );
-class AutoLoader {
- # Locations of core classes
- # Extension classes are specified with $wgAutoloadClasses
- static $localClasses = array(
- # Includes
- 'AjaxDispatcher' => 'includes/AjaxDispatcher.php',
- 'AjaxResponse' => 'includes/AjaxResponse.php',
- 'AlphabeticPager' => 'includes/Pager.php',
- 'APCBagOStuff' => 'includes/BagOStuff.php',
- 'ArrayDiffFormatter' => 'includes/DifferenceEngine.php',
- 'Article' => 'includes/Article.php',
- 'AtomFeed' => 'includes/Feed.php',
- 'AuthPlugin' => 'includes/AuthPlugin.php',
- 'Autopromote' => 'includes/Autopromote.php',
- 'BagOStuff' => 'includes/BagOStuff.php',
- 'Block' => 'includes/Block.php',
- 'CacheDependency' => 'includes/CacheDependency.php',
- 'Category' => 'includes/Category.php',
- 'Categoryfinder' => 'includes/Categoryfinder.php',
- 'CategoryPage' => 'includes/CategoryPage.php',
- 'CategoryViewer' => 'includes/CategoryPage.php',
- 'ChangesList' => 'includes/ChangesList.php',
- 'ChangesFeed' => 'includes/ChangesFeed.php',
- 'ChannelFeed' => 'includes/Feed.php',
- 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php',
- 'ConstantDependency' => 'includes/CacheDependency.php',
- 'DBABagOStuff' => 'includes/BagOStuff.php',
- 'DependencyWrapper' => 'includes/CacheDependency.php',
- '_DiffEngine' => 'includes/DifferenceEngine.php',
- 'DifferenceEngine' => 'includes/DifferenceEngine.php',
- 'DiffFormatter' => 'includes/DifferenceEngine.php',
- 'Diff' => 'includes/DifferenceEngine.php',
- '_DiffOp_Add' => 'includes/DifferenceEngine.php',
- '_DiffOp_Change' => 'includes/DifferenceEngine.php',
- '_DiffOp_Copy' => 'includes/DifferenceEngine.php',
- '_DiffOp_Delete' => 'includes/DifferenceEngine.php',
- '_DiffOp' => 'includes/DifferenceEngine.php',
- 'DjVuImage' => 'includes/DjVuImage.php',
- 'DoubleReplacer' => 'includes/StringUtils.php',
- 'DoubleRedirectJob' => 'includes/DoubleRedirectJob.php',
- 'Dump7ZipOutput' => 'includes/Export.php',
- 'DumpBZip2Output' => 'includes/Export.php',
- 'DumpFileOutput' => 'includes/Export.php',
- 'DumpFilter' => 'includes/Export.php',
- 'DumpGZipOutput' => 'includes/Export.php',
- 'DumpLatestFilter' => 'includes/Export.php',
- 'DumpMultiWriter' => 'includes/Export.php',
- 'DumpNamespaceFilter' => 'includes/Export.php',
- 'DumpNotalkFilter' => 'includes/Export.php',
- 'DumpOutput' => 'includes/Export.php',
- 'DumpPipeOutput' => 'includes/Export.php',
- 'eAccelBagOStuff' => 'includes/BagOStuff.php',
- 'EditPage' => 'includes/EditPage.php',
- 'EmaillingJob' => 'includes/EmaillingJob.php',
- 'EmailNotification' => 'includes/UserMailer.php',
- 'EnhancedChangesList' => 'includes/ChangesList.php',
- 'EnotifNotifyJob' => 'includes/EnotifNotifyJob.php',
- 'ErrorPageError' => 'includes/Exception.php',
- 'Exif' => 'includes/Exif.php',
- 'ExternalEdit' => 'includes/ExternalEdit.php',
- 'ExternalStoreDB' => 'includes/ExternalStoreDB.php',
- 'ExternalStoreHttp' => 'includes/ExternalStoreHttp.php',
- 'ExternalStore' => 'includes/ExternalStore.php',
- 'FatalError' => 'includes/Exception.php',
- 'FakeTitle' => 'includes/FakeTitle.php',
- 'FauxRequest' => 'includes/WebRequest.php',
- 'FeedItem' => 'includes/Feed.php',
- 'FeedUtils' => 'includes/FeedUtils.php',
- 'FileDeleteForm' => 'includes/FileDeleteForm.php',
- 'FileDependency' => 'includes/CacheDependency.php',
- 'FileRevertForm' => 'includes/FileRevertForm.php',
- 'FileStore' => 'includes/FileStore.php',
- 'FormatExif' => 'includes/Exif.php',
- 'FormOptions' => 'includes/FormOptions.php',
- 'FSException' => 'includes/FileStore.php',
- 'FSTransaction' => 'includes/FileStore.php',
- 'GlobalDependency' => 'includes/CacheDependency.php',
- 'HashBagOStuff' => 'includes/BagOStuff.php',
- 'HashtableReplacer' => 'includes/StringUtils.php',
- 'HistoryBlobCurStub' => 'includes/HistoryBlob.php',
- 'HistoryBlob' => 'includes/HistoryBlob.php',
- 'HistoryBlobStub' => 'includes/HistoryBlob.php',
- 'HTMLCacheUpdate' => 'includes/HTMLCacheUpdate.php',
- 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php',
- 'HTMLFileCache' => 'includes/HTMLFileCache.php',
- 'Http' => 'includes/HttpFunctions.php',
- '_HWLDF_WordAccumulator' => 'includes/DifferenceEngine.php',
- 'IEContentAnalyzer' => 'includes/IEContentAnalyzer.php',
- 'ImageGallery' => 'includes/ImageGallery.php',
- 'ImageHistoryList' => 'includes/ImagePage.php',
- 'ImagePage' => 'includes/ImagePage.php',
- 'ImageQueryPage' => 'includes/ImageQueryPage.php',
- 'IncludableSpecialPage' => 'includes/SpecialPage.php',
- 'IndexPager' => 'includes/Pager.php',
- 'IP' => 'includes/IP.php',
- 'Job' => 'includes/JobQueue.php',
- 'License' => 'includes/Licenses.php',
- 'Licenses' => 'includes/Licenses.php',
- 'LinkBatch' => 'includes/LinkBatch.php',
- 'LinkCache' => 'includes/LinkCache.php',
- 'Linker' => 'includes/Linker.php',
- 'LinkFilter' => 'includes/LinkFilter.php',
- 'LinksUpdate' => 'includes/LinksUpdate.php',
- 'LogPage' => 'includes/LogPage.php',
- 'LogPager' => 'includes/LogEventsList.php',
- 'LogEventsList' => 'includes/LogEventsList.php',
- 'LogReader' => 'includes/LogEventsList.php',
- 'LogViewer' => 'includes/LogEventsList.php',
- 'MacBinary' => 'includes/MacBinary.php',
- 'MagicWordArray' => 'includes/MagicWord.php',
- 'MagicWord' => 'includes/MagicWord.php',
- 'MailAddress' => 'includes/UserMailer.php',
- 'MappedDiff' => 'includes/DifferenceEngine.php',
- 'MathRenderer' => 'includes/Math.php',
- 'MediaTransformError' => 'includes/MediaTransformOutput.php',
- 'MediaTransformOutput' => 'includes/MediaTransformOutput.php',
- 'MediaWikiBagOStuff' => 'includes/BagOStuff.php',
- 'MediaWiki_I18N' => 'includes/SkinTemplate.php',
- 'MediaWiki' => 'includes/Wiki.php',
- 'memcached' => 'includes/memcached-client.php',
- 'MessageCache' => 'includes/MessageCache.php',
- 'MimeMagic' => 'includes/MimeMagic.php',
- 'MWException' => 'includes/Exception.php',
- 'MWNamespace' => 'includes/Namespace.php',
- 'MySQLSearchResultSet' => 'includes/SearchMySQL.php',
- 'Namespace' => 'includes/NamespaceCompat.php', // Compat
- 'OldChangesList' => 'includes/ChangesList.php',
- 'OracleSearchResultSet' => 'includes/SearchOracle.php',
- 'OutputPage' => 'includes/OutputPage.php',
- 'PageHistory' => 'includes/PageHistory.php',
- 'PageHistoryPager' => 'includes/PageHistory.php',
- 'PageQueryPage' => 'includes/PageQueryPage.php',
- 'Pager' => 'includes/Pager.php',
- 'PasswordError' => 'includes/User.php',
- 'PatrolLog' => 'includes/PatrolLog.php',
- 'PostgresSearchResult' => 'includes/SearchPostgres.php',
- 'PostgresSearchResultSet' => 'includes/SearchPostgres.php',
- 'PrefixSearch' => 'includes/PrefixSearch.php',
- 'Profiler' => 'includes/Profiler.php',
- 'ProfilerSimple' => 'includes/ProfilerSimple.php',
- 'ProfilerSimpleText' => 'includes/ProfilerSimpleText.php',
- 'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php',
- 'ProtectionForm' => 'includes/ProtectionForm.php',
- 'QueryPage' => 'includes/QueryPage.php',
- 'QuickTemplate' => 'includes/SkinTemplate.php',
- 'RawPage' => 'includes/RawPage.php',
- 'RCCacheEntry' => 'includes/ChangesList.php',
- 'RecentChange' => 'includes/RecentChange.php',
- 'RefreshLinksJob' => 'includes/RefreshLinksJob.php',
- 'RegexlikeReplacer' => 'includes/StringUtils.php',
- 'ReplacementArray' => 'includes/StringUtils.php',
- 'Replacer' => 'includes/StringUtils.php',
- 'ReverseChronologicalPager' => 'includes/Pager.php',
- 'Revision' => 'includes/Revision.php',
- 'RSSFeed' => 'includes/Feed.php',
- 'Sanitizer' => 'includes/Sanitizer.php',
- 'SearchEngineDummy' => 'includes/SearchEngine.php',
- 'SearchEngine' => 'includes/SearchEngine.php',
- 'SearchHighlighter' => 'includes/SearchEngine.php',
- 'SearchMySQL4' => 'includes/SearchMySQL4.php',
- 'SearchMySQL' => 'includes/SearchMySQL.php',
- 'SearchOracle' => 'includes/SearchOracle.php',
- 'SearchPostgres' => 'includes/SearchPostgres.php',
- 'SearchResult' => 'includes/SearchEngine.php',
- 'SearchResultSet' => 'includes/SearchEngine.php',
- 'SearchResultTooMany' => 'includes/SearchEngine.php',
- 'SearchUpdate' => 'includes/SearchUpdate.php',
- 'SearchUpdateMyISAM' => 'includes/SearchUpdate.php',
- 'SiteConfiguration' => 'includes/SiteConfiguration.php',
- 'SiteStats' => 'includes/SiteStats.php',
- 'SiteStatsUpdate' => 'includes/SiteStats.php',
- 'Skin' => 'includes/Skin.php',
- 'SkinTemplate' => 'includes/SkinTemplate.php',
- 'SpecialMycontributions' => 'includes/SpecialPage.php',
- 'SpecialMypage' => 'includes/SpecialPage.php',
- 'SpecialMytalk' => 'includes/SpecialPage.php',
- 'SpecialPage' => 'includes/SpecialPage.php',
- 'SpecialRedirectToSpecial' => 'includes/SpecialPage.php',
- 'SqlBagOStuff' => 'includes/BagOStuff.php',
- 'SquidUpdate' => 'includes/SquidUpdate.php',
- 'Status' => 'includes/Status.php',
- 'StringUtils' => 'includes/StringUtils.php',
- 'TableDiffFormatter' => 'includes/DifferenceEngine.php',
- 'TablePager' => 'includes/Pager.php',
- 'ThumbnailImage' => 'includes/MediaTransformOutput.php',
- 'TitleDependency' => 'includes/CacheDependency.php',
- 'Title' => 'includes/Title.php',
- 'TitleListDependency' => 'includes/CacheDependency.php',
- 'TransformParameterError' => 'includes/MediaTransformOutput.php',
- 'TurckBagOStuff' => 'includes/BagOStuff.php',
- 'UnifiedDiffFormatter' => 'includes/DifferenceEngine.php',
- 'UnlistedSpecialPage' => 'includes/SpecialPage.php',
- 'User' => 'includes/User.php',
- 'UserArray' => 'includes/UserArray.php',
- 'UserArrayFromResult' => 'includes/UserArray.php',
- 'UserMailer' => 'includes/UserMailer.php',
- 'UserRightsProxy' => 'includes/UserRightsProxy.php',
- 'WatchedItem' => 'includes/WatchedItem.php',
- 'WatchlistEditor' => 'includes/WatchlistEditor.php',
- 'WebRequest' => 'includes/WebRequest.php',
- 'WebResponse' => 'includes/WebResponse.php',
- 'WikiError' => 'includes/WikiError.php',
- 'WikiErrorMsg' => 'includes/WikiError.php',
- 'WikiExporter' => 'includes/Export.php',
- 'WikiXmlError' => 'includes/WikiError.php',
- 'WordLevelDiff' => 'includes/DifferenceEngine.php',
- 'XCacheBagOStuff' => 'includes/BagOStuff.php',
- 'XmlDumpWriter' => 'includes/Export.php',
- 'Xml' => 'includes/Xml.php',
- 'XmlSelect' => 'includes/Xml.php',
- 'XmlTypeCheck' => 'includes/XmlTypeCheck.php',
- 'ZhClient' => 'includes/ZhClient.php',
+# Locations of core classes
+# Extension classes are specified with $wgAutoloadClasses
+# This array is a global instead of a static member of AutoLoader to work around a bug in APC
+global $wgAutoloadLocalClasses;
+$wgAutoloadLocalClasses = array(
+ # Includes
+ 'AjaxDispatcher' => 'includes/AjaxDispatcher.php',
+ 'AjaxResponse' => 'includes/AjaxResponse.php',
+ 'AlphabeticPager' => 'includes/Pager.php',
+ 'APCBagOStuff' => 'includes/BagOStuff.php',
+ 'Article' => 'includes/Article.php',
+ 'AtomFeed' => 'includes/Feed.php',
+ 'AuthPlugin' => 'includes/AuthPlugin.php',
+ 'AuthPluginUser' => 'includes/AuthPlugin.php',
+ 'Autopromote' => 'includes/Autopromote.php',
+ 'BagOStuff' => 'includes/BagOStuff.php',
+ 'Block' => 'includes/Block.php',
+ 'CacheDependency' => 'includes/CacheDependency.php',
+ 'Category' => 'includes/Category.php',
+ 'Categoryfinder' => 'includes/Categoryfinder.php',
+ 'CategoryPage' => 'includes/CategoryPage.php',
+ 'CategoryViewer' => 'includes/CategoryPage.php',
+ 'ChangesList' => 'includes/ChangesList.php',
+ 'ChangesFeed' => 'includes/ChangesFeed.php',
+ 'ChannelFeed' => 'includes/Feed.php',
+ 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php',
+ 'ConstantDependency' => 'includes/CacheDependency.php',
+ 'CreativeCommonsRdf' => 'includes/Metadata.php',
+ 'Credits' => 'includes/Credits.php',
+ 'DBABagOStuff' => 'includes/BagOStuff.php',
+ 'DependencyWrapper' => 'includes/CacheDependency.php',
+ 'DiffHistoryBlob' => 'includes/HistoryBlob.php',
+ 'DjVuImage' => 'includes/DjVuImage.php',
+ 'DoubleReplacer' => 'includes/StringUtils.php',
+ 'DoubleRedirectJob' => 'includes/DoubleRedirectJob.php',
+ 'DublinCoreRdf' => 'includes/Metadata.php',
+ 'Dump7ZipOutput' => 'includes/Export.php',
+ 'DumpBZip2Output' => 'includes/Export.php',
+ 'DumpFileOutput' => 'includes/Export.php',
+ 'DumpFilter' => 'includes/Export.php',
+ 'DumpGZipOutput' => 'includes/Export.php',
+ 'DumpLatestFilter' => 'includes/Export.php',
+ 'DumpMultiWriter' => 'includes/Export.php',
+ 'DumpNamespaceFilter' => 'includes/Export.php',
+ 'DumpNotalkFilter' => 'includes/Export.php',
+ 'DumpOutput' => 'includes/Export.php',
+ 'DumpPipeOutput' => 'includes/Export.php',
+ 'eAccelBagOStuff' => 'includes/BagOStuff.php',
+ 'EditPage' => 'includes/EditPage.php',
+ 'EmaillingJob' => 'includes/EmaillingJob.php',
+ 'EmailNotification' => 'includes/UserMailer.php',
+ 'EnhancedChangesList' => 'includes/ChangesList.php',
+ 'EnotifNotifyJob' => 'includes/EnotifNotifyJob.php',
+ 'ErrorPageError' => 'includes/Exception.php',
+ 'Exif' => 'includes/Exif.php',
+ 'ExplodeIterator' => 'includes/StringUtils.php',
+ 'ExternalEdit' => 'includes/ExternalEdit.php',
+ 'ExternalStoreDB' => 'includes/ExternalStoreDB.php',
+ 'ExternalStoreHttp' => 'includes/ExternalStoreHttp.php',
+ 'ExternalStore' => 'includes/ExternalStore.php',
+ 'FatalError' => 'includes/Exception.php',
+ 'FakeTitle' => 'includes/FakeTitle.php',
+ 'FauxRequest' => 'includes/WebRequest.php',
+ 'FeedItem' => 'includes/Feed.php',
+ 'FeedUtils' => 'includes/FeedUtils.php',
+ 'FileDeleteForm' => 'includes/FileDeleteForm.php',
+ 'FileDependency' => 'includes/CacheDependency.php',
+ 'FileRevertForm' => 'includes/FileRevertForm.php',
+ 'FileStore' => 'includes/FileStore.php',
+ 'FormatExif' => 'includes/Exif.php',
+ 'FormOptions' => 'includes/FormOptions.php',
+ 'FSException' => 'includes/FileStore.php',
+ 'FSTransaction' => 'includes/FileStore.php',
+ 'GlobalDependency' => 'includes/CacheDependency.php',
+ 'HashBagOStuff' => 'includes/BagOStuff.php',
+ 'HashtableReplacer' => 'includes/StringUtils.php',
+ 'HistoryBlobCurStub' => 'includes/HistoryBlob.php',
+ 'HistoryBlob' => 'includes/HistoryBlob.php',
+ 'HistoryBlobStub' => 'includes/HistoryBlob.php',
+ 'HTMLCacheUpdate' => 'includes/HTMLCacheUpdate.php',
+ 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php',
+ 'HTMLFileCache' => 'includes/HTMLFileCache.php',
+ 'Http' => 'includes/HttpFunctions.php',
+ 'IEContentAnalyzer' => 'includes/IEContentAnalyzer.php',
+ 'ImageGallery' => 'includes/ImageGallery.php',
+ 'ImageHistoryList' => 'includes/ImagePage.php',
+ 'ImagePage' => 'includes/ImagePage.php',
+ 'ImageQueryPage' => 'includes/ImageQueryPage.php',
+ 'IncludableSpecialPage' => 'includes/SpecialPage.php',
+ 'IndexPager' => 'includes/Pager.php',
+ 'Interwiki' => 'includes/Interwiki.php',
+ 'IP' => 'includes/IP.php',
+ 'Job' => 'includes/JobQueue.php',
+ 'License' => 'includes/Licenses.php',
+ 'Licenses' => 'includes/Licenses.php',
+ 'LinkBatch' => 'includes/LinkBatch.php',
+ 'LinkCache' => 'includes/LinkCache.php',
+ 'Linker' => 'includes/Linker.php',
+ 'LinkFilter' => 'includes/LinkFilter.php',
+ 'LinksUpdate' => 'includes/LinksUpdate.php',
+ 'LogPage' => 'includes/LogPage.php',
+ 'LogPager' => 'includes/LogEventsList.php',
+ 'LogEventsList' => 'includes/LogEventsList.php',
+ 'LogReader' => 'includes/LogEventsList.php',
+ 'LogViewer' => 'includes/LogEventsList.php',
+ 'MacBinary' => 'includes/MacBinary.php',
+ 'MagicWordArray' => 'includes/MagicWord.php',
+ 'MagicWord' => 'includes/MagicWord.php',
+ 'MailAddress' => 'includes/UserMailer.php',
+ 'MathRenderer' => 'includes/Math.php',
+ 'MediaTransformError' => 'includes/MediaTransformOutput.php',
+ 'MediaTransformOutput' => 'includes/MediaTransformOutput.php',
+ 'MediaWikiBagOStuff' => 'includes/BagOStuff.php',
+ 'MediaWiki_I18N' => 'includes/SkinTemplate.php',
+ 'MediaWiki' => 'includes/Wiki.php',
+ 'memcached' => 'includes/memcached-client.php',
+ 'MessageCache' => 'includes/MessageCache.php',
+ 'MimeMagic' => 'includes/MimeMagic.php',
+ 'MWException' => 'includes/Exception.php',
+ 'MWNamespace' => 'includes/Namespace.php',
+ 'MySQLSearchResultSet' => 'includes/SearchMySQL.php',
+ 'Namespace' => 'includes/NamespaceCompat.php', // Compat
+ 'OldChangesList' => 'includes/ChangesList.php',
+ 'OracleSearchResultSet' => 'includes/SearchOracle.php',
+ 'OutputPage' => 'includes/OutputPage.php',
+ 'PageHistory' => 'includes/PageHistory.php',
+ 'PageHistoryPager' => 'includes/PageHistory.php',
+ 'PageQueryPage' => 'includes/PageQueryPage.php',
+ 'Pager' => 'includes/Pager.php',
+ 'PasswordError' => 'includes/User.php',
+ 'PatrolLog' => 'includes/PatrolLog.php',
+ 'PostgresSearchResult' => 'includes/SearchPostgres.php',
+ 'PostgresSearchResultSet' => 'includes/SearchPostgres.php',
+ 'PrefixSearch' => 'includes/PrefixSearch.php',
+ 'Profiler' => 'includes/Profiler.php',
+ 'ProfilerSimple' => 'includes/ProfilerSimple.php',
+ 'ProfilerSimpleText' => 'includes/ProfilerSimpleText.php',
+ 'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php',
+ 'ProtectionForm' => 'includes/ProtectionForm.php',
+ 'QueryPage' => 'includes/QueryPage.php',
+ 'QuickTemplate' => 'includes/SkinTemplate.php',
+ 'RawPage' => 'includes/RawPage.php',
+ 'RCCacheEntry' => 'includes/ChangesList.php',
+ 'RdfMetaData' => 'includes/Metadata.php',
+ 'RecentChange' => 'includes/RecentChange.php',
+ 'RefreshLinksJob' => 'includes/RefreshLinksJob.php',
+ 'RefreshLinksJob2' => 'includes/RefreshLinksJob.php',
+ 'RegexlikeReplacer' => 'includes/StringUtils.php',
+ 'ReplacementArray' => 'includes/StringUtils.php',
+ 'Replacer' => 'includes/StringUtils.php',
+ 'ReverseChronologicalPager' => 'includes/Pager.php',
+ 'Revision' => 'includes/Revision.php',
+ 'RSSFeed' => 'includes/Feed.php',
+ 'Sanitizer' => 'includes/Sanitizer.php',
+ 'SearchEngineDummy' => 'includes/SearchEngine.php',
+ 'SearchEngine' => 'includes/SearchEngine.php',
+ 'SearchHighlighter' => 'includes/SearchEngine.php',
+ 'SearchMySQL4' => 'includes/SearchMySQL4.php',
+ 'SearchMySQL' => 'includes/SearchMySQL.php',
+ 'SearchOracle' => 'includes/SearchOracle.php',
+ 'SearchPostgres' => 'includes/SearchPostgres.php',
+ 'SearchResult' => 'includes/SearchEngine.php',
+ 'SearchResultSet' => 'includes/SearchEngine.php',
+ 'SearchResultTooMany' => 'includes/SearchEngine.php',
+ 'SearchUpdate' => 'includes/SearchUpdate.php',
+ 'SearchUpdateMyISAM' => 'includes/SearchUpdate.php',
+ 'SiteConfiguration' => 'includes/SiteConfiguration.php',
+ 'SiteStats' => 'includes/SiteStats.php',
+ 'SiteStatsUpdate' => 'includes/SiteStats.php',
+ 'Skin' => 'includes/Skin.php',
+ 'SkinTemplate' => 'includes/SkinTemplate.php',
+ 'SpecialMycontributions' => 'includes/SpecialPage.php',
+ 'SpecialMypage' => 'includes/SpecialPage.php',
+ 'SpecialMytalk' => 'includes/SpecialPage.php',
+ 'SpecialPage' => 'includes/SpecialPage.php',
+ 'SpecialRedirectToSpecial' => 'includes/SpecialPage.php',
+ 'SqlBagOStuff' => 'includes/BagOStuff.php',
+ 'SquidUpdate' => 'includes/SquidUpdate.php',
+ 'Status' => 'includes/Status.php',
+ 'StringUtils' => 'includes/StringUtils.php',
+ 'TablePager' => 'includes/Pager.php',
+ 'ThumbnailImage' => 'includes/MediaTransformOutput.php',
+ 'TitleDependency' => 'includes/CacheDependency.php',
+ 'Title' => 'includes/Title.php',
+ 'TitleArray' => 'includes/TitleArray.php',
+ 'TitleListDependency' => 'includes/CacheDependency.php',
+ 'TransformParameterError' => 'includes/MediaTransformOutput.php',
+ 'TurckBagOStuff' => 'includes/BagOStuff.php',
+ 'UnlistedSpecialPage' => 'includes/SpecialPage.php',
+ 'UploadBase' => 'includes/UploadBase.php',
+ 'UploadFromStash' => 'includes/UploadFromStash.php',
+ 'UploadFromUpload' => 'includes/UploadFromUpload.php',
+ 'UploadFromUrl' => 'includes/UploadFromUrl.php',
+ 'User' => 'includes/User.php',
+ 'UserArray' => 'includes/UserArray.php',
+ 'UserArrayFromResult' => 'includes/UserArray.php',
+ 'UserMailer' => 'includes/UserMailer.php',
+ 'UserRightsProxy' => 'includes/UserRightsProxy.php',
+ 'WatchedItem' => 'includes/WatchedItem.php',
+ 'WatchlistEditor' => 'includes/WatchlistEditor.php',
+ 'WebRequest' => 'includes/WebRequest.php',
+ 'WebResponse' => 'includes/WebResponse.php',
+ 'WikiError' => 'includes/WikiError.php',
+ 'WikiErrorMsg' => 'includes/WikiError.php',
+ 'WikiExporter' => 'includes/Export.php',
+ 'WikiXmlError' => 'includes/WikiError.php',
+ 'XCacheBagOStuff' => 'includes/BagOStuff.php',
+ 'XmlDumpWriter' => 'includes/Export.php',
+ 'Xml' => 'includes/Xml.php',
+ 'XmlSelect' => 'includes/Xml.php',
+ 'XmlTypeCheck' => 'includes/XmlTypeCheck.php',
+ 'ZhClient' => 'includes/ZhClient.php',
+
+ # includes/api
+ 'ApiBase' => 'includes/api/ApiBase.php',
+ 'ApiBlock' => 'includes/api/ApiBlock.php',
+ 'ApiDelete' => 'includes/api/ApiDelete.php',
+ 'ApiDisabled' => 'includes/api/ApiDisabled.php',
+ 'ApiEditPage' => 'includes/api/ApiEditPage.php',
+ 'ApiEmailUser' => 'includes/api/ApiEmailUser.php',
+ 'ApiExpandTemplates' => 'includes/api/ApiExpandTemplates.php',
+ 'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php',
+ 'ApiFormatBase' => 'includes/api/ApiFormatBase.php',
+ 'ApiFormatDbg' => 'includes/api/ApiFormatDbg.php',
+ 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php',
+ 'ApiFormatJson' => 'includes/api/ApiFormatJson.php',
+ 'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php',
+ 'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php',
+ 'ApiFormatWddx' => 'includes/api/ApiFormatWddx.php',
+ 'ApiFormatXml' => 'includes/api/ApiFormatXml.php',
+ 'ApiFormatYaml' => 'includes/api/ApiFormatYaml.php',
+ 'ApiHelp' => 'includes/api/ApiHelp.php',
+ 'ApiLogin' => 'includes/api/ApiLogin.php',
+ 'ApiLogout' => 'includes/api/ApiLogout.php',
+ 'ApiMain' => 'includes/api/ApiMain.php',
+ 'ApiMove' => 'includes/api/ApiMove.php',
+ 'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php',
+ 'ApiPageSet' => 'includes/api/ApiPageSet.php',
+ 'ApiParamInfo' => 'includes/api/ApiParamInfo.php',
+ 'ApiParse' => 'includes/api/ApiParse.php',
+ 'ApiPatrol' => 'includes/api/ApiPatrol.php',
+ 'ApiProtect' => 'includes/api/ApiProtect.php',
+ 'ApiPurge' => 'includes/api/ApiPurge.php',
+ 'ApiQuery' => 'includes/api/ApiQuery.php',
+ 'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php',
+ 'ApiQueryAllimages' => 'includes/api/ApiQueryAllimages.php',
+ 'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php',
+ 'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php',
+ 'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php',
+ 'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php',
+ 'ApiQueryBacklinks' => 'includes/api/ApiQueryBacklinks.php',
+ 'ApiQueryBase' => 'includes/api/ApiQueryBase.php',
+ 'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php',
+ 'ApiQueryCategories' => 'includes/api/ApiQueryCategories.php',
+ 'ApiQueryCategoryInfo' => 'includes/api/ApiQueryCategoryInfo.php',
+ 'ApiQueryCategoryMembers' => 'includes/api/ApiQueryCategoryMembers.php',
+ 'ApiQueryContributions' => 'includes/api/ApiQueryUserContributions.php',
+ 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php',
+ 'ApiQueryDisabled' => 'includes/api/ApiQueryDisabled.php',
+ 'ApiQueryDuplicateFiles' => 'includes/api/ApiQueryDuplicateFiles.php',
+ 'ApiQueryExtLinksUsage' => 'includes/api/ApiQueryExtLinksUsage.php',
+ 'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php',
+ 'ApiQueryGeneratorBase' => 'includes/api/ApiQueryBase.php',
+ 'ApiQueryImageInfo' => 'includes/api/ApiQueryImageInfo.php',
+ 'ApiQueryImages' => 'includes/api/ApiQueryImages.php',
+ 'ApiQueryInfo' => 'includes/api/ApiQueryInfo.php',
+ 'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php',
+ 'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php',
+ 'ApiQueryLogEvents' => 'includes/api/ApiQueryLogEvents.php',
+ 'ApiQueryRandom' => 'includes/api/ApiQueryRandom.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',
+ 'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php',
+ 'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php',
+ 'ApiQueryWatchlistRaw' => 'includes/api/ApiQueryWatchlistRaw.php',
+ 'ApiResult' => 'includes/api/ApiResult.php',
+ 'ApiRollback' => 'includes/api/ApiRollback.php',
+ 'ApiUnblock' => 'includes/api/ApiUnblock.php',
+ 'ApiUndelete' => 'includes/api/ApiUndelete.php',
+ 'ApiWatch' => 'includes/api/ApiWatch.php',
+ 'Services_JSON' => 'includes/api/ApiFormatJson_json.php',
+ 'Services_JSON_Error' => 'includes/api/ApiFormatJson_json.php',
+ 'Spyc' => 'includes/api/ApiFormatYaml_spyc.php',
+ 'UsageException' => 'includes/api/ApiMain.php',
- # includes/api
- 'ApiBase' => 'includes/api/ApiBase.php',
- 'ApiBlock' => 'includes/api/ApiBlock.php',
- 'ApiDelete' => 'includes/api/ApiDelete.php',
- 'ApiEditPage' => 'includes/api/ApiEditPage.php',
- 'ApiEmailUser' => 'includes/api/ApiEmailUser.php',
- 'ApiExpandTemplates' => 'includes/api/ApiExpandTemplates.php',
- 'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php',
- 'ApiFormatBase' => 'includes/api/ApiFormatBase.php',
- 'ApiFormatDbg' => 'includes/api/ApiFormatDbg.php',
- 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php',
- 'ApiFormatJson' => 'includes/api/ApiFormatJson.php',
- 'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php',
- 'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php',
- 'ApiFormatWddx' => 'includes/api/ApiFormatWddx.php',
- 'ApiFormatXml' => 'includes/api/ApiFormatXml.php',
- 'ApiFormatYaml' => 'includes/api/ApiFormatYaml.php',
- 'ApiHelp' => 'includes/api/ApiHelp.php',
- 'ApiLogin' => 'includes/api/ApiLogin.php',
- 'ApiLogout' => 'includes/api/ApiLogout.php',
- 'ApiMain' => 'includes/api/ApiMain.php',
- 'ApiMove' => 'includes/api/ApiMove.php',
- 'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php',
- 'ApiPageSet' => 'includes/api/ApiPageSet.php',
- 'ApiParamInfo' => 'includes/api/ApiParamInfo.php',
- 'ApiParse' => 'includes/api/ApiParse.php',
- 'ApiProtect' => 'includes/api/ApiProtect.php',
- 'ApiQuery' => 'includes/api/ApiQuery.php',
- 'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php',
- 'ApiQueryAllimages' => 'includes/api/ApiQueryAllimages.php',
- 'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php',
- 'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php',
- 'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php',
- 'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php',
- 'ApiQueryBacklinks' => 'includes/api/ApiQueryBacklinks.php',
- 'ApiQueryBase' => 'includes/api/ApiQueryBase.php',
- 'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php',
- 'ApiQueryCategories' => 'includes/api/ApiQueryCategories.php',
- 'ApiQueryCategoryInfo' => 'includes/api/ApiQueryCategoryInfo.php',
- 'ApiQueryCategoryMembers' => 'includes/api/ApiQueryCategoryMembers.php',
- 'ApiQueryContributions' => 'includes/api/ApiQueryUserContributions.php',
- 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php',
- 'ApiQueryExtLinksUsage' => 'includes/api/ApiQueryExtLinksUsage.php',
- 'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php',
- 'ApiQueryGeneratorBase' => 'includes/api/ApiQueryBase.php',
- 'ApiQueryImageInfo' => 'includes/api/ApiQueryImageInfo.php',
- 'ApiQueryImages' => 'includes/api/ApiQueryImages.php',
- 'ApiQueryInfo' => 'includes/api/ApiQueryInfo.php',
- 'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php',
- 'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php',
- 'ApiQueryLogEvents' => 'includes/api/ApiQueryLogEvents.php',
- 'ApiQueryRandom' => 'includes/api/ApiQueryRandom.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',
- 'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php',
- 'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php',
- 'ApiResult' => 'includes/api/ApiResult.php',
- 'ApiRollback' => 'includes/api/ApiRollback.php',
- 'ApiUnblock' => 'includes/api/ApiUnblock.php',
- 'ApiUndelete' => 'includes/api/ApiUndelete.php',
- 'Services_JSON' => 'includes/api/ApiFormatJson_json.php',
- 'Services_JSON_Error' => 'includes/api/ApiFormatJson_json.php',
- 'Spyc' => 'includes/api/ApiFormatYaml_spyc.php',
- 'UsageException' => 'includes/api/ApiMain.php',
- 'YAMLNode' => 'includes/api/ApiFormatYaml_spyc.php',
+ # includes/db
+ 'Blob' => 'includes/db/Database.php',
+ 'ChronologyProtector' => 'includes/db/LBFactory.php',
+ 'Database' => 'includes/db/Database.php',
+ 'DatabaseMssql' => 'includes/db/DatabaseMssql.php',
+ 'DatabaseMysql' => 'includes/db/Database.php',
+ 'DatabaseOracle' => 'includes/db/DatabaseOracle.php',
+ 'DatabasePostgres' => 'includes/db/DatabasePostgres.php',
+ 'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php',
+ 'DBConnectionError' => 'includes/db/Database.php',
+ 'DBError' => 'includes/db/Database.php',
+ 'DBObject' => 'includes/db/Database.php',
+ 'DBQueryError' => 'includes/db/Database.php',
+ 'DBUnexpectedError' => 'includes/db/Database.php',
+ 'LBFactory' => 'includes/db/LBFactory.php',
+ 'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php',
+ 'LBFactory_Simple' => 'includes/db/LBFactory.php',
+ 'LoadBalancer' => 'includes/db/LoadBalancer.php',
+ 'LoadMonitor' => 'includes/db/LoadMonitor.php',
+ 'LoadMonitor_MySQL' => 'includes/db/LoadMonitor.php',
+ 'MSSQLField' => 'includes/db/DatabaseMssql.php',
+ 'MySQLField' => 'includes/db/Database.php',
+ 'MySQLMasterPos' => 'includes/db/Database.php',
+ 'ORABlob' => 'includes/db/DatabaseOracle.php',
+ 'ORAResult' => 'includes/db/DatabaseOracle.php',
+ 'PostgresField' => 'includes/db/DatabasePostgres.php',
+ 'ResultWrapper' => 'includes/db/Database.php',
+ 'SQLiteField' => 'includes/db/DatabaseSqlite.php',
- # includes/db
- 'Blob' => 'includes/db/Database.php',
- 'ChronologyProtector' => 'includes/db/LBFactory.php',
- 'Database' => 'includes/db/Database.php',
- 'DatabaseMssql' => 'includes/db/DatabaseMssql.php',
- 'DatabaseMysql' => 'includes/db/Database.php',
- 'DatabaseOracle' => 'includes/db/DatabaseOracle.php',
- 'DatabasePostgres' => 'includes/db/DatabasePostgres.php',
- 'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php',
- 'DBConnectionError' => 'includes/db/Database.php',
- 'DBError' => 'includes/db/Database.php',
- 'DBObject' => 'includes/db/Database.php',
- 'DBQueryError' => 'includes/db/Database.php',
- 'DBUnexpectedError' => 'includes/db/Database.php',
- 'LBFactory' => 'includes/db/LBFactory.php',
- 'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php',
- 'LBFactory_Simple' => 'includes/db/LBFactory.php',
- 'LoadBalancer' => 'includes/db/LoadBalancer.php',
- 'LoadMonitor' => 'includes/db/LoadMonitor.php',
- 'LoadMonitor_MySQL' => 'includes/db/LoadMonitor.php',
- 'MSSQLField' => 'includes/db/DatabaseMssql.php',
- 'MySQLField' => 'includes/db/Database.php',
- 'MySQLMasterPos' => 'includes/db/Database.php',
- 'ORABlob' => 'includes/db/DatabaseOracle.php',
- 'ORAResult' => 'includes/db/DatabaseOracle.php',
- 'PostgresField' => 'includes/db/DatabasePostgres.php',
- 'ResultWrapper' => 'includes/db/Database.php',
- 'SQLiteField' => 'includes/db/DatabaseSqlite.php',
+ # includes/diff
+ 'AncestorComparator' => 'includes/diff/HTMLDiff.php',
+ 'AnchorToString' => 'includes/diff/HTMLDiff.php',
+ 'ArrayDiffFormatter' => 'includes/diff/DifferenceEngine.php',
+ 'BodyNode' => 'includes/diff/Nodes.php',
+ 'ChangeText' => 'includes/diff/HTMLDiff.php',
+ 'ChangeTextGenerator' => 'includes/diff/HTMLDiff.php',
+ 'DelegatingContentHandler' => 'includes/diff/HTMLDiff.php',
+ '_DiffEngine' => 'includes/diff/DifferenceEngine.php',
+ 'DifferenceEngine' => 'includes/diff/DifferenceEngine.php',
+ 'DiffFormatter' => 'includes/diff/DifferenceEngine.php',
+ 'Diff' => 'includes/diff/DifferenceEngine.php',
+ '_DiffOp_Add' => 'includes/diff/DifferenceEngine.php',
+ '_DiffOp_Change' => 'includes/diff/DifferenceEngine.php',
+ '_DiffOp_Copy' => 'includes/diff/DifferenceEngine.php',
+ '_DiffOp_Delete' => 'includes/diff/DifferenceEngine.php',
+ '_DiffOp' => 'includes/diff/DifferenceEngine.php',
+ 'DomTreeBuilder' => 'includes/diff/HTMLDiff.php',
+ 'DummyNode' => 'includes/diff/Nodes.php',
+ 'HTMLDiffer' => 'includes/diff/HTMLDiff.php',
+ 'HTMLOutput' => 'includes/diff/HTMLDiff.php',
+ '_HWLDF_WordAccumulator' => 'includes/diff/DifferenceEngine.php',
+ 'ImageNode' => 'includes/diff/Nodes.php',
+ 'LastCommonParentResult' => 'includes/diff/HTMLDiff.php',
+ 'MappedDiff' => 'includes/diff/DifferenceEngine.php',
+ 'Modification' => 'includes/diff/HTMLDiff.php',
+ 'NoContentTagToString' => 'includes/diff/HTMLDiff.php',
+ 'Node' => 'includes/diff/Nodes.php',
+ 'RangeDifference' => 'includes/diff/Diff.php',
+ 'TableDiffFormatter' => 'includes/diff/DifferenceEngine.php',
+ 'TagNode' => 'includes/diff/Nodes.php',
+ 'TagToString' => 'includes/diff/HTMLDiff.php',
+ 'TagToStringFactory' => 'includes/diff/HTMLDiff.php',
+ 'TextNode' => 'includes/diff/Nodes.php',
+ 'TextNodeDiffer' => 'includes/diff/HTMLDiff.php',
+ 'TextOnlyComparator' => 'includes/diff/HTMLDiff.php',
+ 'UnifiedDiffFormatter' => 'includes/diff/DifferenceEngine.php',
+ 'WhiteSpaceNode' => 'includes/diff/Nodes.php',
+ 'WikiDiff3' => 'includes/diff/Diff.php',
+ 'WordLevelDiff' => 'includes/diff/DifferenceEngine.php',
- # includes/filerepo
- 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
- 'File' => 'includes/filerepo/File.php',
- 'FileRepo' => 'includes/filerepo/FileRepo.php',
- 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
- 'ForeignAPIFile' => 'includes/filerepo/ForeignAPIFile.php',
- 'ForeignAPIRepo' => 'includes/filerepo/ForeignAPIRepo.php',
- 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php',
- 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php',
- 'ForeignDBViaLBRepo' => 'includes/filerepo/ForeignDBViaLBRepo.php',
- 'FSRepo' => 'includes/filerepo/FSRepo.php',
- 'Image' => 'includes/filerepo/Image.php',
- 'LocalFile' => 'includes/filerepo/LocalFile.php',
- 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php',
- 'LocalFileMoveBatch' => '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',
+ # includes/filerepo
+ 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
+ 'File' => 'includes/filerepo/File.php',
+ 'FileCache' => 'includes/filerepo/FileCache.php',
+ 'FileRepo' => 'includes/filerepo/FileRepo.php',
+ 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
+ 'ForeignAPIFile' => 'includes/filerepo/ForeignAPIFile.php',
+ 'ForeignAPIRepo' => 'includes/filerepo/ForeignAPIRepo.php',
+ 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php',
+ 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php',
+ 'ForeignDBViaLBRepo' => 'includes/filerepo/ForeignDBViaLBRepo.php',
+ 'FSRepo' => 'includes/filerepo/FSRepo.php',
+ 'Image' => 'includes/filerepo/Image.php',
+ 'LocalFile' => 'includes/filerepo/LocalFile.php',
+ 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php',
+ 'LocalFileMoveBatch' => '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',
- # includes/media
- 'BitmapHandler' => 'includes/media/Bitmap.php',
- 'BmpHandler' => 'includes/media/BMP.php',
- 'DjVuHandler' => 'includes/media/DjVu.php',
- 'ImageHandler' => 'includes/media/Generic.php',
- 'MediaHandler' => 'includes/media/Generic.php',
- 'SvgHandler' => 'includes/media/SVG.php',
+ # includes/media
+ 'BitmapHandler' => 'includes/media/Bitmap.php',
+ 'BitmapHandler_ClientOnly' => 'includes/media/Bitmap_ClientOnly.php',
+ 'BmpHandler' => 'includes/media/BMP.php',
+ 'DjVuHandler' => 'includes/media/DjVu.php',
+ 'ImageHandler' => 'includes/media/Generic.php',
+ 'MediaHandler' => 'includes/media/Generic.php',
+ 'SvgHandler' => 'includes/media/SVG.php',
- # includes/normal
- 'UtfNormal' => 'includes/normal/UtfNormal.php',
+ # includes/normal
+ 'UtfNormal' => 'includes/normal/UtfNormal.php',
- # includes/parser
- 'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php',
- 'DateFormatter' => 'includes/parser/DateFormatter.php',
- 'OnlyIncludeReplacer' => 'includes/parser/Parser.php',
- 'PPDAccum_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'PPDPart' => 'includes/parser/Preprocessor_DOM.php',
- 'PPDPart_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'PPDStack' => 'includes/parser/Preprocessor_DOM.php',
- 'PPDStackElement' => 'includes/parser/Preprocessor_DOM.php',
- 'PPDStackElement_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'PPDStack_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'PPFrame' => 'includes/parser/Preprocessor.php',
- 'PPFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
- 'PPFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'PPNode' => 'includes/parser/Preprocessor.php',
- 'PPNode_DOM' => 'includes/parser/Preprocessor_DOM.php',
- 'PPNode_Hash_Array' => 'includes/parser/Preprocessor_Hash.php',
- 'PPNode_Hash_Attr' => 'includes/parser/Preprocessor_Hash.php',
- 'PPNode_Hash_Text' => 'includes/parser/Preprocessor_Hash.php',
- 'PPNode_Hash_Tree' => 'includes/parser/Preprocessor_Hash.php',
- 'PPTemplateFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
- 'PPTemplateFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'Parser' => 'includes/parser/Parser.php',
- 'ParserCache' => 'includes/parser/ParserCache.php',
- 'ParserOptions' => 'includes/parser/ParserOptions.php',
- 'ParserOutput' => 'includes/parser/ParserOutput.php',
- 'Parser_DiffTest' => 'includes/parser/Parser_DiffTest.php',
- 'Parser_OldPP' => 'includes/parser/Parser_OldPP.php',
- 'Preprocessor' => 'includes/parser/Preprocessor.php',
- 'Preprocessor_DOM' => 'includes/parser/Preprocessor_DOM.php',
- 'Preprocessor_Hash' => 'includes/parser/Preprocessor_Hash.php',
- 'StripState' => 'includes/parser/Parser.php',
+ # includes/parser
+ 'CoreLinkFunctions' => 'includes/parser/CoreLinkFunctions.php',
+ 'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php',
+ 'DateFormatter' => 'includes/parser/DateFormatter.php',
+ 'LinkHolderArray' => 'includes/parser/LinkHolderArray.php',
+ 'LinkMarkerReplacer' => 'includes/parser/LinkMarkerReplacer.php',
+ 'OnlyIncludeReplacer' => 'includes/parser/Parser.php',
+ 'PPDAccum_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPDPart' => 'includes/parser/Preprocessor_DOM.php',
+ 'PPDPart_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPDStack' => 'includes/parser/Preprocessor_DOM.php',
+ 'PPDStackElement' => 'includes/parser/Preprocessor_DOM.php',
+ 'PPDStackElement_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPDStack_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPFrame' => 'includes/parser/Preprocessor.php',
+ 'PPFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
+ 'PPFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPNode' => 'includes/parser/Preprocessor.php',
+ 'PPNode_DOM' => 'includes/parser/Preprocessor_DOM.php',
+ 'PPNode_Hash_Array' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPNode_Hash_Attr' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPNode_Hash_Text' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPNode_Hash_Tree' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPTemplateFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
+ 'PPTemplateFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'Parser' => 'includes/parser/Parser.php',
+ 'ParserCache' => 'includes/parser/ParserCache.php',
+ 'ParserOptions' => 'includes/parser/ParserOptions.php',
+ 'ParserOutput' => 'includes/parser/ParserOutput.php',
+ 'Parser_DiffTest' => 'includes/parser/Parser_DiffTest.php',
+ 'Parser_LinkHooks' => 'includes/parser/Parser_LinkHooks.php',
+ 'Preprocessor' => 'includes/parser/Preprocessor.php',
+ 'Preprocessor_DOM' => 'includes/parser/Preprocessor_DOM.php',
+ 'Preprocessor_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'StripState' => 'includes/parser/Parser.php',
- # includes/specials
- 'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php',
- 'BrokenRedirectsPage' => 'includes/specials/SpecialBrokenRedirects.php',
- 'ContribsPager' => 'includes/specials/SpecialContributions.php',
- 'DBLockForm' => 'includes/specials/SpecialLockdb.php',
- 'DBUnlockForm' => 'includes/specials/SpecialUnlockdb.php',
- 'DeadendPagesPage' => 'includes/specials/SpecialDeadendpages.php',
- 'DisambiguationsPage' => 'includes/specials/SpecialDisambiguations.php',
- 'DoubleRedirectsPage' => 'includes/specials/SpecialDoubleRedirects.php',
- 'EmailConfirmation' => 'includes/specials/SpecialConfirmemail.php',
- 'EmailInvalidation' => 'includes/specials/SpecialConfirmemail.php',
- 'EmailUserForm' => 'includes/specials/SpecialEmailuser.php',
- 'FewestrevisionsPage' => 'includes/specials/SpecialFewestrevisions.php',
- 'FileDuplicateSearchPage' => 'includes/specials/SpecialFileDuplicateSearch.php',
- 'IPBlockForm' => 'includes/specials/SpecialBlockip.php',
- 'IPBlocklistPager' => 'includes/specials/SpecialIpblocklist.php',
- 'IPUnblockForm' => 'includes/specials/SpecialIpblocklist.php',
- 'ImportReporter' => 'includes/specials/SpecialImport.php',
- 'ImportStreamSource' => 'includes/specials/SpecialImport.php',
- 'ImportStringSource' => 'includes/specials/SpecialImport.php',
- 'ListredirectsPage' => 'includes/specials/SpecialListredirects.php',
- 'LoginForm' => 'includes/specials/SpecialUserlogin.php',
- 'LonelyPagesPage' => 'includes/specials/SpecialLonelypages.php',
- 'LongPagesPage' => 'includes/specials/SpecialLongpages.php',
- 'MIMEsearchPage' => 'includes/specials/SpecialMIMEsearch.php',
- 'MostcategoriesPage' => 'includes/specials/SpecialMostcategories.php',
- 'MostimagesPage' => 'includes/specials/SpecialMostimages.php',
- 'MostlinkedCategoriesPage' => 'includes/specials/SpecialMostlinkedcategories.php',
- 'MostlinkedPage' => 'includes/specials/SpecialMostlinked.php',
- 'MostrevisionsPage' => 'includes/specials/SpecialMostrevisions.php',
- 'MovePageForm' => 'includes/specials/SpecialMovepage.php',
- 'SpecialNewpages' => 'includes/specials/SpecialNewpages.php',
- 'NewPagesPager' => 'includes/specials/SpecialNewpages.php',
- 'PageArchive' => 'includes/specials/SpecialUndelete.php',
- 'PasswordResetForm' => 'includes/specials/SpecialResetpass.php',
- 'PopularPagesPage' => 'includes/specials/SpecialPopularpages.php',
- 'PreferencesForm' => 'includes/specials/SpecialPreferences.php',
- 'RandomPage' => 'includes/specials/SpecialRandompage.php',
- 'RevisionDeleteForm' => 'includes/specials/SpecialRevisiondelete.php',
- 'RevisionDeleter' => 'includes/specials/SpecialRevisiondelete.php',
- 'ShortPagesPage' => 'includes/specials/SpecialShortpages.php',
- 'SpecialAllpages' => 'includes/specials/SpecialAllpages.php',
- 'SpecialBookSources' => 'includes/specials/SpecialBooksources.php',
- 'SpecialListGroupRights' => 'includes/specials/SpecialListgrouprights.php',
- 'SpecialMostlinkedtemplates' => 'includes/specials/SpecialMostlinkedtemplates.php',
- 'SpecialPrefixindex' => 'includes/specials/SpecialPrefixindex.php',
- 'SpecialRandomredirect' => 'includes/specials/SpecialRandomredirect.php',
- 'SpecialRecentchanges' => 'includes/specials/SpecialRecentchanges.php',
- 'SpecialRecentchangeslinked' => 'includes/specials/SpecialRecentchangeslinked.php',
- 'SpecialSearch' => 'includes/specials/SpecialSearch.php',
- 'SpecialVersion' => 'includes/specials/SpecialVersion.php',
- 'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php',
- 'UncategorizedPagesPage' => 'includes/specials/SpecialUncategorizedpages.php',
- 'UncategorizedTemplatesPage' => 'includes/specials/SpecialUncategorizedtemplates.php',
- 'UndeleteForm' => 'includes/specials/SpecialUndelete.php',
- 'UnusedCategoriesPage' => 'includes/specials/SpecialUnusedcategories.php',
- 'UnusedimagesPage' => 'includes/specials/SpecialUnusedimages.php',
- 'UnusedtemplatesPage' => 'includes/specials/SpecialUnusedtemplates.php',
- 'UnwatchedpagesPage' => 'includes/specials/SpecialUnwatchedpages.php',
- 'UploadForm' => 'includes/specials/SpecialUpload.php',
- 'UploadFormMogile' => 'includes/specials/SpecialUploadMogile.php',
- 'UserrightsPage' => 'includes/specials/SpecialUserrights.php',
- 'UsersPager' => 'includes/specials/SpecialListusers.php',
- 'WantedCategoriesPage' => 'includes/specials/SpecialWantedcategories.php',
- 'WantedPagesPage' => 'includes/specials/SpecialWantedpages.php',
- 'WhatLinksHerePage' => 'includes/specials/SpecialWhatlinkshere.php',
- 'WikiImporter' => 'includes/specials/SpecialImport.php',
- 'WikiRevision' => 'includes/specials/SpecialImport.php',
- 'WithoutInterwikiPage' => 'includes/specials/SpecialWithoutinterwiki.php',
+ # includes/specials
+ 'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php',
+ 'BrokenRedirectsPage' => 'includes/specials/SpecialBrokenRedirects.php',
+ 'ContribsPager' => 'includes/specials/SpecialContributions.php',
+ 'DBLockForm' => 'includes/specials/SpecialLockdb.php',
+ 'DBUnlockForm' => 'includes/specials/SpecialUnlockdb.php',
+ 'DeadendPagesPage' => 'includes/specials/SpecialDeadendpages.php',
+ 'DeletedContributionsPage' => 'includes/specials/SpecialDeletedContributions.php',
+ 'DeletedContribsPager' => 'includes/specials/SpecialDeletedContributions.php',
+ 'DisambiguationsPage' => 'includes/specials/SpecialDisambiguations.php',
+ 'DoubleRedirectsPage' => 'includes/specials/SpecialDoubleRedirects.php',
+ 'EmailConfirmation' => 'includes/specials/SpecialConfirmemail.php',
+ 'EmailInvalidation' => 'includes/specials/SpecialConfirmemail.php',
+ 'EmailUserForm' => 'includes/specials/SpecialEmailuser.php',
+ 'FewestrevisionsPage' => 'includes/specials/SpecialFewestrevisions.php',
+ 'FileDuplicateSearchPage' => 'includes/specials/SpecialFileDuplicateSearch.php',
+ 'IPBlockForm' => 'includes/specials/SpecialBlockip.php',
+ 'IPBlocklistPager' => 'includes/specials/SpecialIpblocklist.php',
+ 'IPUnblockForm' => 'includes/specials/SpecialIpblocklist.php',
+ 'ImportReporter' => 'includes/specials/SpecialImport.php',
+ 'ImportStreamSource' => 'includes/Import.php',
+ 'ImportStringSource' => 'includes/Import.php',
+ 'LinkSearchPage' => 'includes/specials/SpecialLinkSearch.php',
+ 'ListredirectsPage' => 'includes/specials/SpecialListredirects.php',
+ 'LoginForm' => 'includes/specials/SpecialUserlogin.php',
+ 'LonelyPagesPage' => 'includes/specials/SpecialLonelypages.php',
+ 'LongPagesPage' => 'includes/specials/SpecialLongpages.php',
+ 'MIMEsearchPage' => 'includes/specials/SpecialMIMEsearch.php',
+ 'MostcategoriesPage' => 'includes/specials/SpecialMostcategories.php',
+ 'MostimagesPage' => 'includes/specials/SpecialMostimages.php',
+ 'MostlinkedCategoriesPage' => 'includes/specials/SpecialMostlinkedcategories.php',
+ 'MostlinkedPage' => 'includes/specials/SpecialMostlinked.php',
+ 'MostrevisionsPage' => 'includes/specials/SpecialMostrevisions.php',
+ 'MovePageForm' => 'includes/specials/SpecialMovepage.php',
+ 'SpecialNewpages' => 'includes/specials/SpecialNewpages.php',
+ 'SpecialContributions' => 'includes/specials/SpecialContributions.php',
+ 'NewPagesPager' => 'includes/specials/SpecialNewpages.php',
+ 'PageArchive' => 'includes/specials/SpecialUndelete.php',
+ 'SpecialResetpass' => 'includes/specials/SpecialResetpass.php',
+ 'PopularPagesPage' => 'includes/specials/SpecialPopularpages.php',
+ 'PreferencesForm' => 'includes/specials/SpecialPreferences.php',
+ 'RandomPage' => 'includes/specials/SpecialRandompage.php',
+ 'RevisionDeleteForm' => 'includes/specials/SpecialRevisiondelete.php',
+ 'RevisionDeleter' => 'includes/specials/SpecialRevisiondelete.php',
+ 'ShortPagesPage' => 'includes/specials/SpecialShortpages.php',
+ 'SpecialAllpages' => 'includes/specials/SpecialAllpages.php',
+ 'SpecialBookSources' => 'includes/specials/SpecialBooksources.php',
+ 'SpecialImport' => 'includes/specials/SpecialImport.php',
+ 'SpecialListGroupRights' => 'includes/specials/SpecialListgrouprights.php',
+ 'SpecialMostlinkedtemplates' => 'includes/specials/SpecialMostlinkedtemplates.php',
+ 'SpecialPrefixindex' => 'includes/specials/SpecialPrefixindex.php',
+ 'SpecialRandomredirect' => 'includes/specials/SpecialRandomredirect.php',
+ 'SpecialRecentchanges' => 'includes/specials/SpecialRecentchanges.php',
+ 'SpecialRecentchangeslinked' => 'includes/specials/SpecialRecentchangeslinked.php',
+ 'SpecialSearch' => 'includes/specials/SpecialSearch.php',
+ 'SpecialSearchOld' => 'includes/specials/SpecialSearch.php',
+ 'SpecialStatistics' => 'includes/specials/SpecialStatistics.php',
+ 'SpecialVersion' => 'includes/specials/SpecialVersion.php',
+ 'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php',
+ 'UncategorizedPagesPage' => 'includes/specials/SpecialUncategorizedpages.php',
+ 'UncategorizedTemplatesPage' => 'includes/specials/SpecialUncategorizedtemplates.php',
+ 'UndeleteForm' => 'includes/specials/SpecialUndelete.php',
+ 'UnusedCategoriesPage' => 'includes/specials/SpecialUnusedcategories.php',
+ 'UnusedimagesPage' => 'includes/specials/SpecialUnusedimages.php',
+ 'UnusedtemplatesPage' => 'includes/specials/SpecialUnusedtemplates.php',
+ 'UnwatchedpagesPage' => 'includes/specials/SpecialUnwatchedpages.php',
+ 'UploadForm' => 'includes/specials/SpecialUpload.php',
+ 'UploadFormMogile' => 'includes/specials/SpecialUploadMogile.php',
+ 'UserrightsPage' => 'includes/specials/SpecialUserrights.php',
+ 'UsersPager' => 'includes/specials/SpecialListusers.php',
+ 'WantedCategoriesPage' => 'includes/specials/SpecialWantedcategories.php',
+ 'WantedFilesPage' => 'includes/specials/SpecialWantedfiles.php',
+ 'WantedPagesPage' => 'includes/specials/SpecialWantedpages.php',
+ 'WantedTemplatesPage' => 'includes/specials/SpecialWantedtemplates.php',
+ 'WhatLinksHerePage' => 'includes/specials/SpecialWhatlinkshere.php',
+ 'WikiImporter' => 'includes/Import.php',
+ 'WikiRevision' => 'includes/Import.php',
+ 'WithoutInterwikiPage' => 'includes/specials/SpecialWithoutinterwiki.php',
- # includes/templates
- 'UsercreateTemplate' => 'includes/templates/Userlogin.php',
- 'UserloginTemplate' => 'includes/templates/Userlogin.php',
+ # includes/templates
+ 'UsercreateTemplate' => 'includes/templates/Userlogin.php',
+ 'UserloginTemplate' => 'includes/templates/Userlogin.php',
- # languages
- 'Language' => 'languages/Language.php',
- 'FakeConverter' => 'languages/Language.php',
+ # languages
+ 'Language' => 'languages/Language.php',
+ 'FakeConverter' => 'languages/Language.php',
- # maintenance/language
- 'statsOutput' => 'maintenance/language/StatOutputs.php',
- 'wikiStatsOutput' => 'maintenance/language/StatOutputs.php',
- 'metawikiStatsOutput' => 'maintenance/language/StatOutputs.php',
- 'textStatsOutput' => 'maintenance/language/StatOutputs.php',
- 'csvStatsOutput' => 'maintenance/language/StatOutputs.php',
+ # maintenance/language
+ 'statsOutput' => 'maintenance/language/StatOutputs.php',
+ 'wikiStatsOutput' => 'maintenance/language/StatOutputs.php',
+ 'metawikiStatsOutput' => 'maintenance/language/StatOutputs.php',
+ 'textStatsOutput' => 'maintenance/language/StatOutputs.php',
+ 'csvStatsOutput' => 'maintenance/language/StatOutputs.php',
- );
+);
+class AutoLoader {
/**
* autoload - take a class name and attempt to load it
- *
+ *
* @param string $className Name of class we're looking for.
* @return bool Returning false is important on failure as
* it allows Zend to try and look in other registered autoloaders
- * as well.
+ * as well.
*/
static function autoload( $className ) {
- global $wgAutoloadClasses;
+ global $wgAutoloadClasses, $wgAutoloadLocalClasses;
- wfProfileIn( __METHOD__ );
- if ( isset( self::$localClasses[$className] ) ) {
- $filename = self::$localClasses[$className];
+ if ( isset( $wgAutoloadLocalClasses[$className] ) ) {
+ $filename = $wgAutoloadLocalClasses[$className];
} elseif ( isset( $wgAutoloadClasses[$className] ) ) {
$filename = $wgAutoloadClasses[$className];
} else {
@@ -488,14 +549,15 @@ class AutoLoader {
# The case can sometimes be wrong when unserializing PHP 4 objects
$filename = false;
$lowerClass = strtolower( $className );
- foreach ( self::$localClasses as $class2 => $file2 ) {
+ foreach ( $wgAutoloadLocalClasses as $class2 => $file2 ) {
if ( strtolower( $class2 ) == $lowerClass ) {
$filename = $file2;
}
}
if ( !$filename ) {
+ if( function_exists( 'wfDebug' ) )
+ wfDebug( "Class {$className} not found; skipped loading" );
# Give up
- wfProfileOut( __METHOD__ );
return false;
}
}
@@ -506,7 +568,6 @@ class AutoLoader {
$filename = "$IP/$filename";
}
require( $filename );
- wfProfileOut( __METHOD__ );
return true;
}
@@ -532,4 +593,3 @@ if ( function_exists( 'spl_autoload_register' ) ) {
AutoLoader::autoload( $class );
}
}
-
diff --git a/includes/Autopromote.php b/includes/Autopromote.php
index 68fe6636..c8a4c03b 100644
--- a/includes/Autopromote.php
+++ b/includes/Autopromote.php
@@ -19,7 +19,7 @@ class Autopromote {
$promote[] = $group;
}
- wfRunHooks( 'GetAutoPromoteGroups', array($user, &$promote) );
+ wfRunHooks( 'GetAutoPromoteGroups', array( $user, &$promote ) );
return $promote;
}
@@ -106,9 +106,16 @@ class Autopromote {
case APCOND_AGE:
$age = time() - wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
return $age >= $cond[1];
+ case APCOND_AGE_FROM_EDIT:
+ $age = time() - wfTimestampOrNull( TS_UNIX, $user->getFirstEditTimestamp() );
+ return $age >= $cond[1];
case APCOND_INGROUPS:
$groups = array_slice( $cond, 1 );
return count( array_intersect( $groups, $user->getGroups() ) ) == count( $groups );
+ case APCOND_ISIP:
+ return $cond[1] == wfGetIP();
+ case APCOND_IPINRANGE:
+ return IP::isInRange( wfGetIP(), $cond[1] );
default:
$result = null;
wfRunHooks( 'AutopromoteCondition', array( $cond[0], array_slice( $cond, 1 ), $user, &$result ) );
diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php
index 92311329..572dca6c 100644
--- a/includes/BagOStuff.php
+++ b/includes/BagOStuff.php
@@ -475,8 +475,19 @@ class MediaWikiBagOStuff extends SqlBagOStuff {
function _fromunixtime($ts) {
return $this->_getDB()->timestamp($ts);
}
+ /***
+ * Note -- this should *not* check wfReadOnly().
+ * Read-only mode has been repurposed from the original
+ * "nothing must write to the database" to "users should not
+ * be able to edit or alter anything user-visible".
+ *
+ * Backend bits like the object cache should continue
+ * to work in this mode, otherwise things will blow up
+ * like the message cache failing to save its state,
+ * causing long delays (bug 11533).
+ */
function _readonly(){
- return wfReadOnly();
+ return false;
}
function _strencode($s) {
return $this->_getDB()->strencode($s);
diff --git a/includes/Block.php b/includes/Block.php
index b208fa8a..2c2227e2 100644
--- a/includes/Block.php
+++ b/includes/Block.php
@@ -13,11 +13,10 @@
*
* @todo This could be used everywhere, but it isn't.
*/
-class Block
-{
+class Block {
/* public*/ var $mAddress, $mUser, $mBy, $mReason, $mTimestamp, $mAuto, $mId, $mExpiry,
$mRangeStart, $mRangeEnd, $mAnonOnly, $mEnableAutoblock, $mHideName,
- $mBlockEmail, $mByName, $mAngryAutoblock;
+ $mBlockEmail, $mByName, $mAngryAutoblock, $mAllowUsertalk;
/* private */ var $mNetworkBits, $mIntegerAddr, $mForUpdate, $mFromMaster;
const EB_KEEP_EXPIRED = 1;
@@ -26,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, $blockEmail = 0 )
+ $hideName = 0, $blockEmail = 0, $allowUsertalk = 0 )
{
$this->mId = 0;
# Expand valid IPv6 addresses
@@ -43,6 +42,7 @@ class Block
$this->mEnableAutoblock = $enableAutoblock;
$this->mHideName = $hideName;
$this->mBlockEmail = $blockEmail;
+ $this->mAllowUsertalk = $allowUsertalk;
$this->mForUpdate = false;
$this->mFromMaster = false;
$this->mByName = false;
@@ -50,9 +50,18 @@ class Block
$this->initialiseRange();
}
- static function newFromDB( $address, $user = 0, $killExpired = true )
- {
- $block = new Block();
+ /**
+ * Load a block from the database, using either the IP address or
+ * user ID. Tries the user ID first, and if that doesn't work, tries
+ * the address.
+ *
+ * @param $address String: IP address of user/anon
+ * @param $user Integer: user id of user
+ * @param $killExpired Boolean: delete expired blocks on load
+ * @return Block Object
+ */
+ public static function newFromDB( $address, $user = 0, $killExpired = true ) {
+ $block = new Block;
$block->load( $address, $user, $killExpired );
if ( $block->isValid() ) {
return $block;
@@ -61,8 +70,13 @@ class Block
}
}
- static function newFromID( $id )
- {
+ /**
+ * Load a blocked user from their block id.
+ *
+ * @param $id Integer: Block id to search for
+ * @return Block object
+ */
+ public static function newFromID( $id ) {
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->resultObject( $dbr->select( 'ipblocks', '*',
array( 'ipb_id' => $id ), __METHOD__ ) );
@@ -73,21 +87,47 @@ class Block
return null;
}
}
+
+ /**
+ * Check if two blocks are effectively equal
+ *
+ * @return Boolean
+ */
+ public function equals( Block $block ) {
+ return (
+ $this->mAddress == $block->mAddress
+ && $this->mUser == $block->mUser
+ && $this->mAuto == $block->mAuto
+ && $this->mAnonOnly == $block->mAnonOnly
+ && $this->mCreateAccount == $block->mCreateAccount
+ && $this->mExpiry == $block->mExpiry
+ && $this->mEnableAutoblock == $block->mEnableAutoblock
+ && $this->mHideName == $block->mHideName
+ && $this->mBlockEmail == $block->mBlockEmail
+ && $this->mAllowUsertalk == $block->mAllowUsertalk
+ );
+ }
- function clear()
- {
+ /**
+ * Clear all member variables in the current object. Does not clear
+ * the block from the DB.
+ */
+ public function clear() {
$this->mAddress = $this->mReason = $this->mTimestamp = '';
$this->mId = $this->mAnonOnly = $this->mCreateAccount =
$this->mEnableAutoblock = $this->mAuto = $this->mUser =
- $this->mBy = $this->mHideName = $this->mBlockEmail = 0;
+ $this->mBy = $this->mHideName = $this->mBlockEmail = $this->mAllowUsertalk = 0;
$this->mByName = false;
}
/**
- * Get the DB object and set the reference parameter to the query options
+ * Get the DB object and set the reference parameter to the select options.
+ * The options array will contain FOR UPDATE if appropriate.
+ *
+ * @param $options Array
+ * @return Database
*/
- function &getDBOptions( &$options )
- {
+ protected function &getDBOptions( &$options ) {
global $wgAntiLockFlags;
if ( $this->mForUpdate || $this->mFromMaster ) {
$db = wfGetDB( DB_MASTER );
@@ -104,15 +144,15 @@ class Block
}
/**
- * Get a ban from the DB, with either the given address or the given username
+ * Get a block from the DB, with either the given address or the given username
*
- * @param string $address The IP address of the user, or blank to skip IP blocks
- * @param integer $user The user ID, or zero for anonymous users
- * @param bool $killExpired Whether to delete expired rows while loading
+ * @param $address string The IP address of the user, or blank to skip IP blocks
+ * @param $user int The user ID, or zero for anonymous users
+ * @param $killExpired bool Whether to delete expired rows while loading
+ * @return Boolean: the user is blocked from editing
*
*/
- function load( $address = '', $user = 0, $killExpired = true )
- {
+ public function load( $address = '', $user = 0, $killExpired = true ) {
wfDebug( "Block::load: '$address', '$user', $killExpired\n" );
$options = array();
@@ -143,7 +183,10 @@ class Block
if ( $user && $this->mAnonOnly ) {
# Block is marked anon-only
# Whitelist this IP address against autoblocks and range blocks
- $this->clear();
+ # (but not account creation blocks -- bug 13611)
+ if( !$this->mCreateAccount ) {
+ $this->clear();
+ }
return false;
} else {
return true;
@@ -154,7 +197,10 @@ class Block
# Try range block
if ( $this->loadRange( $address, $killExpired, $user ) ) {
if ( $user && $this->mAnonOnly ) {
- $this->clear();
+ # Respect account creation blocks on logged-in users -- bug 13611
+ if( !$this->mCreateAccount ) {
+ $this->clear();
+ }
return false;
} else {
return true;
@@ -180,9 +226,12 @@ class Block
/**
* Fill in member variables from a result wrapper
+ *
+ * @param $res ResultWrapper: row from the ipblocks table
+ * @param $killExpired Boolean: whether to delete expired rows while loading
+ * @return Boolean
*/
- function loadFromResult( ResultWrapper $res, $killExpired = true )
- {
+ protected function loadFromResult( ResultWrapper $res, $killExpired = true ) {
$ret = false;
if ( 0 != $res->numRows() ) {
# Get first block
@@ -216,9 +265,13 @@ class Block
/**
* Search the database for any range blocks matching the given address, and
* load the row if one is found.
+ *
+ * @param $address String: IP address range
+ * @param $killExpired Boolean: whether to delete expired rows while loading
+ * @param $userid Integer: if not 0, then sets ipb_anon_only
+ * @return Boolean
*/
- function loadRange( $address, $killExpired = true, $user = 0 )
- {
+ public function loadRange( $address, $killExpired = true, $user = 0 ) {
$iaddr = IP::toHex( $address );
if ( $iaddr === false ) {
# Invalid address
@@ -247,15 +300,12 @@ class Block
}
/**
- * Determine if a given integer IPv4 address is in a given CIDR network
- * @deprecated Use IP::isInRange
+ * Given a database row from the ipblocks table, initialize
+ * member variables
+ *
+ * @param $row ResultWrapper: a row from the ipblocks table
*/
- function isAddressInRange( $addr, $range ) {
- return IP::isInRange( $addr, $range );
- }
-
- function initFromRow( $row )
- {
+ public function initFromRow( $row ) {
$this->mAddress = $row->ipb_address;
$this->mReason = $row->ipb_reason;
$this->mTimestamp = wfTimestamp(TS_MW,$row->ipb_timestamp);
@@ -266,6 +316,7 @@ class Block
$this->mCreateAccount = $row->ipb_create_account;
$this->mEnableAutoblock = $row->ipb_enable_autoblock;
$this->mBlockEmail = $row->ipb_block_email;
+ $this->mAllowUsertalk = $row->ipb_allow_usertalk;
$this->mHideName = $row->ipb_deleted;
$this->mId = $row->ipb_id;
$this->mExpiry = self::decodeExpiry( $row->ipb_expiry );
@@ -278,8 +329,11 @@ class Block
$this->mRangeEnd = $row->ipb_range_end;
}
- function initialiseRange()
- {
+ /**
+ * Once $mAddress has been set, get the range they came from.
+ * Wrapper for IP::parseRange
+ */
+ protected function initialiseRange() {
$this->mRangeStart = '';
$this->mRangeEnd = '';
@@ -289,64 +343,12 @@ class Block
}
/**
- * Callback with a Block object for every block
- * @return integer number of blocks;
+ * Delete the row from the IP blocks table.
+ *
+ * @return Boolean
*/
- /*static*/ function enumBlocks( $callback, $tag, $flags = 0 )
- {
- global $wgAntiLockFlags;
-
- $block = new Block();
- if ( $flags & Block::EB_FOR_UPDATE ) {
- $db = wfGetDB( DB_MASTER );
- if ( $wgAntiLockFlags & ALF_NO_BLOCK_LOCK ) {
- $options = '';
- } else {
- $options = 'FOR UPDATE';
- }
- $block->forUpdate( true );
- } else {
- $db = wfGetDB( DB_SLAVE );
- $options = '';
- }
- if ( $flags & Block::EB_RANGE_ONLY ) {
- $cond = " AND ipb_range_start <> ''";
- } else {
- $cond = '';
- }
-
- $now = wfTimestampNow();
-
- list( $ipblocks, $user ) = $db->tableNamesN( 'ipblocks', 'user' );
-
- $sql = "SELECT $ipblocks.*,user_name FROM $ipblocks,$user " .
- "WHERE user_id=ipb_by $cond ORDER BY ipb_timestamp DESC $options";
- $res = $db->query( $sql, 'Block::enumBlocks' );
- $num_rows = $db->numRows( $res );
-
- while ( $row = $db->fetchObject( $res ) ) {
- $block->initFromRow( $row );
- if ( ( $flags & Block::EB_RANGE_ONLY ) && $block->mRangeStart == '' ) {
- continue;
- }
-
- if ( !( $flags & Block::EB_KEEP_EXPIRED ) ) {
- if ( $block->mExpiry && $now > $block->mExpiry ) {
- $block->delete();
- } else {
- call_user_func( $callback, $block, $tag );
- }
- } else {
- call_user_func( $callback, $block, $tag );
- }
- }
- $db->freeResult( $res );
- return $num_rows;
- }
-
- function delete()
- {
- if (wfReadOnly()) {
+ public function delete() {
+ if ( wfReadOnly() ) {
return false;
}
if ( !$this->mId ) {
@@ -359,33 +361,17 @@ class Block
}
/**
- * Insert a block into the block table.
- * @return Whether or not the insertion was successful.
- */
- function insert()
- {
+ * Insert a block into the block table. Will fail if there is a conflicting
+ * block (same name and options) already in the database.
+ *
+ * @return Boolean: whether or not the insertion was successful.
+ */
+ public function insert() {
wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" );
$dbw = wfGetDB( DB_MASTER );
- # Unset ipb_anon_only for user blocks, makes no sense
- if ( $this->mUser ) {
- $this->mAnonOnly = 0;
- }
-
- # Unset ipb_enable_autoblock for IP blocks, makes no sense
- if ( !$this->mUser ) {
- $this->mEnableAutoblock = 0;
- $this->mBlockEmail = 0; //Same goes for email...
- }
-
- if( !$this->mByName ) {
- if( $this->mBy ) {
- $this->mByName = User::whoIs( $this->mBy );
- } else {
- global $wgUser;
- $this->mByName = $wgUser->getName();
- }
- }
+ $this->validateBlockParams();
+ $this->initialiseRange();
# Don't collide with expired blocks
Block::purgeExpired();
@@ -408,7 +394,8 @@ class Block
'ipb_range_start' => $this->mRangeStart,
'ipb_range_end' => $this->mRangeEnd,
'ipb_deleted' => $this->mHideName,
- 'ipb_block_email' => $this->mBlockEmail
+ 'ipb_block_email' => $this->mBlockEmail,
+ 'ipb_allow_usertalk' => $this->mAllowUsertalk
), 'Block::insert', array( 'IGNORE' )
);
$affected = $dbw->affectedRows();
@@ -416,15 +403,76 @@ class Block
if ($affected)
$this->doRetroactiveAutoblock();
- return $affected;
+ return (bool)$affected;
+ }
+
+ /**
+ * Update a block in the DB with new parameters.
+ * The ID field needs to be loaded first.
+ */
+ public function update() {
+ wfDebug( "Block::update; timestamp {$this->mTimestamp}\n" );
+ $dbw = wfGetDB( DB_MASTER );
+
+ $this->validateBlockParams();
+
+ $dbw->update( 'ipblocks',
+ array(
+ 'ipb_user' => $this->mUser,
+ 'ipb_by' => $this->mBy,
+ 'ipb_by_text' => $this->mByName,
+ 'ipb_reason' => $this->mReason,
+ 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp),
+ 'ipb_auto' => $this->mAuto,
+ 'ipb_anon_only' => $this->mAnonOnly,
+ 'ipb_create_account' => $this->mCreateAccount,
+ 'ipb_enable_autoblock' => $this->mEnableAutoblock,
+ 'ipb_expiry' => self::encodeExpiry( $this->mExpiry, $dbw ),
+ 'ipb_range_start' => $this->mRangeStart,
+ 'ipb_range_end' => $this->mRangeEnd,
+ 'ipb_deleted' => $this->mHideName,
+ 'ipb_block_email' => $this->mBlockEmail,
+ 'ipb_allow_usertalk' => $this->mAllowUsertalk ),
+ array( 'ipb_id' => $this->mId ),
+ 'Block::update' );
+
+ return $dbw->affectedRows();
}
+
+ /**
+ * Make sure all the proper members are set to sane values
+ * before adding/updating a block
+ */
+ protected function validateBlockParams() {
+ # Unset ipb_anon_only for user blocks, makes no sense
+ if ( $this->mUser ) {
+ $this->mAnonOnly = 0;
+ }
+
+ # Unset ipb_enable_autoblock for IP blocks, makes no sense
+ if ( !$this->mUser ) {
+ $this->mEnableAutoblock = 0;
+ $this->mBlockEmail = 0; //Same goes for email...
+ }
+ if( !$this->mByName ) {
+ if( $this->mBy ) {
+ $this->mByName = User::whoIs( $this->mBy );
+ } else {
+ global $wgUser;
+ $this->mByName = $wgUser->getName();
+ }
+ }
+ }
+
+
/**
* Retroactively autoblocks the last IP used by the user (if it is a user)
* blocked by this Block.
- *@return Whether or not a retroactive autoblock was made.
+ *
+ * @return Boolean: whether or not a retroactive autoblock was made.
*/
- function doRetroactiveAutoblock() {
+ public function doRetroactiveAutoblock() {
$dbr = wfGetDB( DB_SLAVE );
#If autoblock is enabled, autoblock the LAST IP used
# - stolen shamelessly from CheckUser_body.php
@@ -458,25 +506,25 @@ class Block
}
}
}
-
+
/**
- * Autoblocks the given IP, referring to this Block.
- * @param string $autoblockip The IP to autoblock.
- * @param bool $justInserted The main block was just inserted
- * @return bool Whether or not an autoblock was inserted.
- */
- function doAutoblock( $autoblockip, $justInserted = false ) {
- # If autoblocks are disabled, go away.
- if ( !$this->mEnableAutoblock ) {
- return;
+ * Checks whether a given IP is on the autoblock whitelist.
+ *
+ * @param $ip String: The IP to check
+ * @return Boolean
+ */
+ public static function isWhitelistedFromAutoblocks( $ip ) {
+ global $wgMemc;
+
+ // Try to get the autoblock_whitelist from the cache, as it's faster
+ // than getting the msg raw and explode()'ing it.
+ $key = wfMemcKey( 'ipb', 'autoblock', 'whitelist' );
+ $lines = $wgMemc->get( $key );
+ if ( !$lines ) {
+ $lines = explode( "\n", wfMsgForContentNoTrans( 'autoblock_whitelist' ) );
+ $wgMemc->set( $key, $lines, 3600 * 24 );
}
- # Check for presence on the autoblock whitelist
- # TODO cache this?
- $lines = explode( "\n", wfMsgForContentNoTrans( 'autoblock_whitelist' ) );
-
- $ip = $autoblockip;
-
wfDebug("Checking the autoblock whitelist..\n");
foreach( $lines as $line ) {
@@ -493,23 +541,42 @@ class Block
# Is the IP in this range?
if (IP::isInRange( $ip, $wlEntry )) {
wfDebug(" IP $ip matches $wlEntry, not autoblocking\n");
- #$autoblockip = null; # Don't autoblock a whitelisted IP.
- return; #This /SHOULD/ introduce a dummy block - but
- # I don't know a safe way to do so. -werdna
+ return true;
} else {
wfDebug( " No match\n" );
}
}
+ return false;
+ }
+
+ /**
+ * Autoblocks the given IP, referring to this Block.
+ *
+ * @param $autoblockIP String: the IP to autoblock.
+ * @param $justInserted Boolean: the main block was just inserted
+ * @return Boolean: whether or not an autoblock was inserted.
+ */
+ public function doAutoblock( $autoblockIP, $justInserted = false ) {
+ # If autoblocks are disabled, go away.
+ if ( !$this->mEnableAutoblock ) {
+ return;
+ }
+
+ # Check for presence on the autoblock whitelist
+ if (Block::isWhitelistedFromAutoblocks($autoblockIP)) {
+ return;
+ }
+
## Allow hooks to cancel the autoblock.
- if (!wfRunHooks( 'AbortAutoblock', array( $autoblockip, &$this ) )) {
+ if (!wfRunHooks( 'AbortAutoblock', array( $autoblockIP, &$this ) )) {
wfDebug( "Autoblock aborted by hook." );
return false;
}
# It's okay to autoblock. Go ahead and create/insert the block.
- $ipblock = Block::newFromDB( $autoblockip );
+ $ipblock = Block::newFromDB( $autoblockIP );
if ( $ipblock ) {
# If the user is already blocked. Then check if the autoblock would
# exceed the user block. If it would exceed, then do nothing, else
@@ -528,8 +595,8 @@ class Block
}
# Make a new block object with the desired properties
- wfDebug( "Autoblocking {$this->mAddress}@" . $autoblockip . "\n" );
- $ipblock->mAddress = $autoblockip;
+ wfDebug( "Autoblocking {$this->mAddress}@" . $autoblockIP . "\n" );
+ $ipblock->mAddress = $autoblockIP;
$ipblock->mUser = 0;
$ipblock->mBy = $this->mBy;
$ipblock->mByName = $this->mByName;
@@ -539,7 +606,7 @@ class Block
$ipblock->mCreateAccount = $this->mCreateAccount;
# Continue suppressing the name if needed
$ipblock->mHideName = $this->mHideName;
-
+ $ipblock->mAllowUsertalk = $this->mAllowUsertalk;
# If the user is already blocked with an expiry date, we don't
# want to pile on top of that!
if($this->mExpiry) {
@@ -551,8 +618,11 @@ class Block
return $ipblock->insert();
}
- function deleteIfExpired()
- {
+ /**
+ * Check if a block has expired. Delete it if it is.
+ * @return Boolean
+ */
+ public function deleteIfExpired() {
$fname = 'Block::deleteIfExpired';
wfProfileIn( $fname );
if ( $this->isExpired() ) {
@@ -567,8 +637,11 @@ class Block
return $retVal;
}
- function isExpired()
- {
+ /**
+ * Has the block expired?
+ * @return Boolean
+ */
+ public function isExpired() {
wfDebug( "Block::isExpired() checking current " . wfTimestampNow() . " vs $this->mExpiry\n" );
if ( !$this->mExpiry ) {
return false;
@@ -577,13 +650,18 @@ class Block
}
}
- function isValid()
- {
+ /**
+ * Is the block address valid (i.e. not a null string?)
+ * @return Boolean
+ */
+ public function isValid() {
return $this->mAddress != '';
}
- function updateTimestamp()
- {
+ /**
+ * Update the timestamp on autoblocks.
+ */
+ public function updateTimestamp() {
if ( $this->mAuto ) {
$this->mTimestamp = wfTimestamp();
$this->mExpiry = Block::getAutoblockExpiry( $this->mTimestamp );
@@ -600,41 +678,43 @@ class Block
}
}
- /*
- function getIntegerAddr()
- {
- return $this->mIntegerAddr;
- }
-
- function getNetworkBits()
- {
- return $this->mNetworkBits;
- }*/
-
/**
- * @return The blocker user ID.
+ * Get the user id of the blocking sysop
+ *
+ * @return Integer
*/
public function getBy() {
return $this->mBy;
}
/**
- * @return The blocker user name.
+ * Get the username of the blocking sysop
+ *
+ * @return String
*/
- function getByName()
- {
+ public function getByName() {
return $this->mByName;
}
- function forUpdate( $x = NULL ) {
+ /**
+ * Get/set the SELECT ... FOR UPDATE flag
+ */
+ public function forUpdate( $x = NULL ) {
return wfSetVar( $this->mForUpdate, $x );
}
- function fromMaster( $x = NULL ) {
+ /**
+ * Get/set a flag determining whether the master is used for reads
+ */
+ public function fromMaster( $x = NULL ) {
return wfSetVar( $this->mFromMaster, $x );
}
- function getRedactedName() {
+ /**
+ * Get the block name, but with autoblocked IPs hidden as per standard privacy policy
+ * @return String
+ */
+ public function getRedactedName() {
if ( $this->mAuto ) {
return '#' . $this->mId;
} else {
@@ -644,8 +724,12 @@ class Block
/**
* Encode expiry for DB
+ *
+ * @param $expiry String: timestamp for expiry, or
+ * @param $db Database object
+ * @return String
*/
- static function encodeExpiry( $expiry, $db ) {
+ public static function encodeExpiry( $expiry, $db ) {
if ( $expiry == '' || $expiry == Block::infinity() ) {
return Block::infinity();
} else {
@@ -655,8 +739,12 @@ class Block
/**
* Decode expiry which has come from the DB
+ *
+ * @param $expiry String: Database expiry format
+ * @param $timestampType Requested timestamp format
+ * @return String
*/
- static function decodeExpiry( $expiry, $timestampType = TS_MW ) {
+ public static function decodeExpiry( $expiry, $timestampType = TS_MW ) {
if ( $expiry == '' || $expiry == Block::infinity() ) {
return Block::infinity();
} else {
@@ -664,8 +752,12 @@ class Block
}
}
- static function getAutoblockExpiry( $timestamp )
- {
+ /**
+ * Get a timestamp of the expiry for autoblocks
+ *
+ * @return String
+ */
+ public static function getAutoblockExpiry( $timestamp ) {
global $wgAutoblockExpiry;
return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry );
}
@@ -673,8 +765,10 @@ class Block
/**
* Gets rid of uneeded numbers in quad-dotted/octet IP strings
* For example, 127.111.113.151/24 -> 127.111.113.0/24
+ * @param $range String: IP address to normalize
+ * @return string
*/
- static function normaliseRange( $range ) {
+ public static function normaliseRange( $range ) {
$parts = explode( '/', $range );
if ( count( $parts ) == 2 ) {
// IPv6
@@ -706,31 +800,31 @@ class Block
/**
* Purge expired blocks from the ipblocks table
*/
- static function purgeExpired() {
+ public static function purgeExpired() {
$dbw = wfGetDB( DB_MASTER );
$dbw->delete( 'ipblocks', array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), __METHOD__ );
}
- static function infinity() {
+ /**
+ * Get a value to insert into expiry field of the database when infinite expiry
+ * is desired. In principle this could be DBMS-dependant, but currently all
+ * supported DBMS's support the string "infinity", so we just use that.
+ *
+ * @return String
+ */
+ public static function infinity() {
# This is a special keyword for timestamps in PostgreSQL, and
# works with CHAR(14) as well because "i" sorts after all numbers.
return 'infinity';
-
- /*
- static $infinity;
- if ( !isset( $infinity ) ) {
- $dbr = wfGetDB( DB_SLAVE );
- $infinity = $dbr->bigTimestamp();
- }
- return $infinity;
- */
}
/**
* Convert a DB-encoded expiry into a real string that humans can read.
+ *
+ * @param $encoded_expiry String: Database encoded expiry time
+ * @return String
*/
- static function formatExpiry( $encoded_expiry ) {
-
+ public static function formatExpiry( $encoded_expiry ) {
static $msg = null;
if( is_null( $msg ) ) {
@@ -749,14 +843,15 @@ class Block
$expiretimestr = $wgLang->timeanddate( $expiry, true );
$expirystr = wfMsgReplaceArgs( $msg['expiringblock'], array($expiretimestr) );
}
-
return $expirystr;
}
/**
* Convert a typed-in expiry time into something we can put into the database.
+ * @param $expiry_input String: whatever was typed into the form
+ * @return String: more database friendly
*/
- static function parseExpiryInput( $expiry_input ) {
+ public static function parseExpiryInput( $expiry_input ) {
if ( $expiry_input == 'infinite' || $expiry_input == 'indefinite' ) {
$expiry = 'infinity';
} else {
@@ -765,7 +860,6 @@ class Block
return false;
}
}
-
return $expiry;
}
diff --git a/includes/Category.php b/includes/Category.php
index acafc47a..78567add 100644
--- a/includes/Category.php
+++ b/includes/Category.php
@@ -1,6 +1,8 @@
<?php
/**
- * Category objects are immutable, strictly speaking. If you call methods that change the database, like to refresh link counts, the objects will be appropriately reinitialized. Member variables are lazy-initialized.
+ * Category objects are immutable, strictly speaking. If you call methods that change the database,
+ * like to refresh link counts, the objects will be appropriately reinitialized.
+ * Member variables are lazy-initialized.
*
* TODO: Move some stuff from CategoryPage.php to here, and use that.
*
@@ -79,7 +81,7 @@ class Category {
/**
* Factory function.
*
- * @param array $name A category name (no "Category:" prefix). It need
+ * @param $name Array: A category name (no "Category:" prefix). It need
* not be normalized, with spaces replaced by underscores.
* @return mixed Category, or false on a totally invalid name
*/
@@ -99,8 +101,8 @@ class Category {
/**
* Factory function.
*
- * @param array $title Title for the category page
- * @return mixed Category, or false on a totally invalid name
+ * @param $title Title for the category page
+ * @return Mixed: category, or false on a totally invalid name
*/
public static function newFromTitle( $title ) {
$cat = new self();
@@ -114,7 +116,7 @@ class Category {
/**
* Factory function.
*
- * @param array $id A category id
+ * @param $id Integer: a category id
* @return Category
*/
public static function newFromID( $id ) {
@@ -192,6 +194,33 @@ class Category {
return $this->mTitle;
}
+ /**
+ * Fetch a TitleArray of up to $limit category members, beginning after the
+ * category sort key $offset.
+ * @param $limit integer
+ * @param $offset string
+ * @return TitleArray object for category members.
+ */
+ public function getMembers( $limit = false, $offset = '' ) {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $conds = array( 'cl_to' => $this->getName(), 'cl_from = page_id' );
+ $options = array( 'ORDER BY' => 'cl_sortkey' );
+ if( $limit ) $options[ 'LIMIT' ] = $limit;
+ if( $offset !== '' ) $conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset );
+
+ return TitleArray::newFromResult(
+ $dbr->select(
+ array( 'page', 'categorylinks' ),
+ array( 'page_id', 'page_namespace','page_title', 'page_len',
+ 'page_is_redirect', 'page_latest' ),
+ $conds,
+ __METHOD__,
+ $options
+ )
+ );
+ }
+
/** Generic accessor */
private function getX( $key ) {
if( !$this->initialize() ) {
@@ -228,7 +257,7 @@ class Category {
}
$cond1 = $dbw->conditional( 'page_namespace='.NS_CATEGORY, 1, 'NULL' );
- $cond2 = $dbw->conditional( 'page_namespace='.NS_IMAGE, 1, 'NULL' );
+ $cond2 = $dbw->conditional( 'page_namespace='.NS_FILE, 1, 'NULL' );
$result = $dbw->selectRow(
array( 'categorylinks', 'page' ),
array( 'COUNT(*) AS pages',
diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php
index 92e4e279..4ac24b5f 100644
--- a/includes/CategoryPage.php
+++ b/includes/CategoryPage.php
@@ -36,18 +36,18 @@ class CategoryPage extends Article {
$this->closeShowCategory();
}
}
-
+
/**
- * This page should not be cached if 'from' or 'until' has been used
- * @return bool
+ * Don't return a 404 for categories in use.
*/
- function isFileCacheable() {
- global $wgRequest;
-
- return ( ! Article::isFileCacheable()
- || $wgRequest->getVal( 'from' )
- || $wgRequest->getVal( 'until' )
- ) ? false : true;
+ function hasViewableContent() {
+ if( parent::hasViewableContent() ) {
+ return true;
+ } else {
+ $cat = Category::newFromTitle( $this->mTitle );
+ return $cat->getId() != 0;
+ }
+
}
function openShowCategory() {
@@ -85,8 +85,6 @@ class CategoryViewer {
/**
* Format the category data list.
*
- * @param string $from -- return only sort keys from this item on
- * @param string $until -- don't return keys after this point.
* @return string HTML output
* @private
*/
@@ -144,7 +142,7 @@ class CategoryViewer {
/**
* Add a subcategory to the internal lists, using a title object
- * @deprectated kept for compatibility, please use addSubcategoryObject instead
+ * @deprecated kept for compatibility, please use addSubcategoryObject instead
*/
function addSubcategory( $title, $sortkey, $pageLength ) {
global $wgContLang;
@@ -225,14 +223,14 @@ class CategoryViewer {
array( 'page', 'categorylinks', 'category' ),
array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey',
'cat_id', 'cat_title', 'cat_subcats', 'cat_pages', 'cat_files' ),
- array( $pageCondition,
- 'cl_to' => $this->title->getDBkey() ),
+ array( $pageCondition, 'cl_to' => $this->title->getDBkey() ),
__METHOD__,
array( 'ORDER BY' => $this->flip ? 'cl_sortkey DESC' : 'cl_sortkey',
- 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ),
- 'LIMIT' => $this->limit + 1 ),
+ 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ),
+ 'LIMIT' => $this->limit + 1 ),
array( 'categorylinks' => array( 'INNER JOIN', 'cl_from = page_id' ),
- 'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY ) ) );
+ 'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY ) )
+ );
$count = 0;
$this->nextPage = null;
@@ -249,7 +247,7 @@ class CategoryViewer {
if( $title->getNamespace() == NS_CATEGORY ) {
$cat = Category::newFromRow( $x, $title );
$this->addSubcategoryObject( $cat, $x->cl_sortkey, $x->page_len );
- } elseif( $this->showGallery && $title->getNamespace() == NS_IMAGE ) {
+ } elseif( $this->showGallery && $title->getNamespace() == NS_FILE ) {
$this->addImage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect );
} else {
$this->addPage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect );
@@ -339,10 +337,10 @@ class CategoryViewer {
* Format a list of articles chunked by letter, either as a
* bullet list or a columnar format, depending on the length.
*
- * @param array $articles
- * @param array $articles_start_char
- * @param int $cutoff
- * @return string
+ * @param $articles Array
+ * @param $articles_start_char Array
+ * @param $cutoff Int
+ * @return String
* @private
*/
function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
@@ -359,9 +357,9 @@ class CategoryViewer {
* Format a list of articles chunked by letter in a three-column
* list, ordered vertically.
*
- * @param array $articles
- * @param array $articles_start_char
- * @return string
+ * @param $articles Array
+ * @param $articles_start_char Array
+ * @return String
* @private
*/
function columnList( $articles, $articles_start_char ) {
@@ -418,9 +416,9 @@ class CategoryViewer {
/**
* Format a list of articles chunked by letter in a bullet list.
- * @param array $articles
- * @param array $articles_start_char
- * @return string
+ * @param $articles Array
+ * @param $articles_start_char Array
+ * @return String
* @private
*/
function shortList( $articles, $articles_start_char ) {
@@ -440,12 +438,12 @@ class CategoryViewer {
}
/**
- * @param Title $title
- * @param string $first
- * @param string $last
- * @param int $limit
- * @param array $query - additional query options to pass
- * @return string
+ * @param $title Title object
+ * @param $first String
+ * @param $last String
+ * @param $limit Int
+ * @param $query Array: additional query options to pass
+ * @return String
* @private
*/
function pagingLinks( $title, $first, $last, $limit, $query = array() ) {
@@ -477,10 +475,10 @@ class CategoryViewer {
* category-subcat-count-limited, category-file-count,
* category-file-count-limited.
*
- * @param int $rescnt The number of items returned by our database query.
- * @param int $dbcnt The number of items according to the category table.
- * @param string $type 'subcat', 'article', or 'file'
- * @return string A message giving the number of items, to output to HTML.
+ * @param $rescnt Int: The number of items returned by our database query.
+ * @param $dbcnt Int: The number of items according to the category table.
+ * @param $type String: 'subcat', 'article', or 'file'
+ * @return String: A message giving the number of items, to output to HTML.
*/
private function getCountMessage( $rescnt, $dbcnt, $type ) {
global $wgLang;
@@ -500,8 +498,12 @@ class CategoryViewer {
# Case 1: seems sane.
$totalcnt = $dbcnt;
} elseif($totalrescnt < $this->limit && !$this->from && !$this->until){
- # Case 2: not sane, but salvageable.
+ # Case 2: not sane, but salvageable. Use the number of results.
+ # Since there are fewer than 200, we can also take this opportunity
+ # to refresh the incorrect category table entry -- which should be
+ # quick due to the small number of entries.
$totalcnt = $rescnt;
+ $this->cat->refreshCounts();
} else {
# Case 3: hopeless. Don't give a total count at all.
return wfMsgExt("category-$type-count-limited", 'parse',
diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php
index d28f2eeb..4413bd1a 100644
--- a/includes/Categoryfinder.php
+++ b/includes/Categoryfinder.php
@@ -86,9 +86,15 @@ class Categoryfinder {
* This functions recurses through the parent representation, trying to match the conditions
* @param $id The article/category to check
* @param $conds The array of categories to match
+ * @param $path used to check for recursion loops
* @return bool Does this match the conditions?
*/
- function check ( $id , &$conds ) {
+ function check ( $id , &$conds, $path=array() ) {
+ // Check for loops and stop!
+ if( in_array( $id, $path ) )
+ return false;
+ $path[] = $id;
+
# Shortcut (runtime paranoia): No contitions=all matched
if ( count ( $conds ) == 0 ) return true ;
@@ -120,7 +126,7 @@ class Categoryfinder {
# No sub-parent
continue ;
}
- $done = $this->check ( $this->name2id[$pname] , $conds ) ;
+ $done = $this->check ( $this->name2id[$pname] , $conds, $path );
if ( $done OR count ( $conds ) == 0 ) {
# Subparents have done it!
return true ;
diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php
index 9bee1790..f3c3e429 100644
--- a/includes/ChangesFeed.php
+++ b/includes/ChangesFeed.php
@@ -12,14 +12,15 @@ class ChangesFeed {
public function getFeedObject( $title, $description ) {
global $wgSitename, $wgContLanguageCode, $wgFeedClasses, $wgTitle;
$feedTitle = "$wgSitename - {$title} [$wgContLanguageCode]";
-
+ if( !isset($wgFeedClasses[$this->format] ) )
+ return false;
return new $wgFeedClasses[$this->format](
$feedTitle, htmlspecialchars( $description ), $wgTitle->getFullUrl() );
}
public function execute( $feed, $rows, $limit = 0 , $hideminor = false, $lastmod = false ) {
global $messageMemc, $wgFeedCacheTimeout;
- global $wgFeedClasses, $wgTitle, $wgSitename, $wgContLanguageCode;
+ global $wgFeedClasses, $wgSitename, $wgContLanguageCode;
if ( !FeedUtils::checkFeedOutput( $this->format ) ) {
return;
@@ -85,7 +86,7 @@ class ChangesFeed {
}
/**
- * @todo document
+ * Generate the feed items given a row from the database.
* @param $rows Database resource with recentchanges rows
* @param $feed Feed object
*/
diff --git a/includes/ChangesList.php b/includes/ChangesList.php
index 436f006e..a8f5fff0 100644
--- a/includes/ChangesList.php
+++ b/includes/ChangesList.php
@@ -3,10 +3,9 @@
/**
* @todo document
*/
-class RCCacheEntry extends RecentChange
-{
+class RCCacheEntry extends RecentChange {
var $secureName, $link;
- var $curlink , $difflink, $lastlink , $usertalklink , $versionlink ;
+ var $curlink , $difflink, $lastlink, $usertalklink, $versionlink;
var $userlink, $timestamp, $watched;
static function newFromParent( $rc ) {
@@ -15,7 +14,7 @@ class RCCacheEntry extends RecentChange
$rc2->mExtra = $rc->mExtra;
return $rc2;
}
-} ;
+}
/**
* Class to show various lists of changes:
@@ -25,13 +24,13 @@ class RCCacheEntry extends RecentChange
*/
class ChangesList {
# Called by history lists and recent changes
- #
+ public $skin;
/**
* Changeslist contructor
* @param Skin $skin
*/
- function __construct( &$skin ) {
+ public function __construct( &$skin ) {
$this->skin =& $skin;
$this->preCacheMessages();
}
@@ -47,7 +46,8 @@ class ChangesList {
$sk = $user->getSkin();
$list = NULL;
if( wfRunHooks( 'FetchChangesList', array( &$user, &$sk, &$list ) ) ) {
- return $user->getOption( 'usenewrc' ) ? new EnhancedChangesList( $sk ) : new OldChangesList( $sk );
+ return $user->getOption( 'usenewrc' ) ?
+ new EnhancedChangesList( $sk ) : new OldChangesList( $sk );
} else {
return $list;
}
@@ -58,7 +58,6 @@ class ChangesList {
* they are called often, we call them once and save them in $this->message
*/
private function preCacheMessages() {
- // Precache various messages
if( !isset( $this->message ) ) {
foreach( explode(' ', 'cur diff hist minoreditletter newpageletter last '.
'blocklink history boteditletter semicolon-separator' ) as $msg ) {
@@ -78,10 +77,10 @@ class ChangesList {
* @return string
*/
protected function recentChangesFlags( $new, $minor, $patrolled, $nothing = '&nbsp;', $bot = false ) {
- $f = $new ? '<span class="newpage">' . $this->message['newpageletter'] . '</span>'
- : $nothing;
- $f .= $minor ? '<span class="minor">' . $this->message['minoreditletter'] . '</span>'
- : $nothing;
+ $f = $new ?
+ '<span class="newpage">' . $this->message['newpageletter'] . '</span>' : $nothing;
+ $f .= $minor ?
+ '<span class="minor">' . $this->message['minoreditletter'] . '</span>' : $nothing;
$f .= $bot ? '<span class="bot">' . $this->message['boteditletter'] . '</span>' : $nothing;
$f .= $patrolled ? '<span class="unpatrolled">!</span>' : $nothing;
return $f;
@@ -99,6 +98,30 @@ class ChangesList {
$this->rclistOpen = false;
return '';
}
+
+ /**
+ * Show formatted char difference
+ * @param int $old bytes
+ * @param int $new bytes
+ * @returns string
+ */
+ public static function showCharacterDifference( $old, $new ) {
+ global $wgRCChangedSizeThreshold, $wgLang;
+ $szdiff = $new - $old;
+ $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape'), $wgLang->formatNum($szdiff) );
+ if( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) {
+ $tag = 'strong';
+ } else {
+ $tag = 'span';
+ }
+ if( $szdiff === 0 ) {
+ return "<$tag class='mw-plusminus-null'>($formatedSize)</$tag>";
+ } elseif( $szdiff > 0 ) {
+ return "<$tag class='mw-plusminus-pos'>(+$formatedSize)</$tag>";
+ } else {
+ return "<$tag class='mw-plusminus-neg'>($formatedSize)</$tag>";
+ }
+ }
/**
* Returns text for the end of RC
@@ -116,21 +139,18 @@ class ChangesList {
# Diff
$s .= '(' . $this->message['diff'] . ') (';
# Hist
- $s .= $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), $this->message['hist'], 'action=history' ) .
- ') . . ';
-
+ $s .= $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), $this->message['hist'],
+ 'action=history' ) . ') . . ';
# "[[x]] moved to [[y]]"
$msg = ( $rc->mAttribs['rc_type'] == RC_MOVE ) ? '1movedto2' : '1movedto2_redir';
$s .= wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ),
$this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) );
}
- protected function insertDateHeader(&$s, $rc_timestamp) {
+ protected function insertDateHeader( &$s, $rc_timestamp ) {
global $wgLang;
-
# Make date header if necessary
$date = $wgLang->date( $rc_timestamp, true, true );
- $s = '';
if( $date != $this->lastdate ) {
if( '' != $this->lastdate ) {
$s .= "</ul>\n";
@@ -141,21 +161,19 @@ class ChangesList {
}
}
- protected function insertLog(&$s, $title, $logtype) {
+ protected function insertLog( &$s, $title, $logtype ) {
$logname = LogPage::logName( $logtype );
$s .= '(' . $this->skin->makeKnownLinkObj($title, $logname ) . ')';
}
- protected function insertDiffHist(&$s, &$rc, $unpatrolled) {
+ protected function insertDiffHist( &$s, &$rc, $unpatrolled ) {
# Diff link
- if( !$this->userCan($rc,Revision::DELETED_TEXT) ) {
+ if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) {
$diffLink = $this->message['diff'];
- } else if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) {
+ } else if( !$this->userCan($rc,Revision::DELETED_TEXT) ) {
$diffLink = $this->message['diff'];
} else {
- $rcidparam = $unpatrolled
- ? array( 'rcid' => $rc->mAttribs['rc_id'] )
- : array();
+ $rcidparam = $unpatrolled ? array( 'rcid' => $rc->mAttribs['rc_id'] ) : array();
$diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'],
wfArrayToCGI( array(
'curid' => $rc->mAttribs['rc_cur_id'],
@@ -165,7 +183,6 @@ class ChangesList {
'', '', ' tabindex="'.$rc->counter.'"');
}
$s .= '('.$diffLink.') (';
-
# History link
$s .= $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['hist'],
wfArrayToCGI( array(
@@ -174,39 +191,40 @@ class ChangesList {
$s .= ') . . ';
}
- protected function insertArticleLink(&$s, &$rc, $unpatrolled, $watched) {
- # Article link
+ protected function insertArticleLink( &$s, &$rc, $unpatrolled, $watched ) {
+ global $wgContLang;
# If it's a new article, there is no diff link, but if it hasn't been
# patrolled yet, we need to give users a way to do so
- $params = ( $unpatrolled && $rc->mAttribs['rc_type'] == RC_NEW )
- ? 'rcid='.$rc->mAttribs['rc_id']
- : '';
+ $params = ( $unpatrolled && $rc->mAttribs['rc_type'] == RC_NEW ) ?
+ 'rcid='.$rc->mAttribs['rc_id'] : '';
if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) {
$articlelink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params );
$articlelink = '<span class="history-deleted">'.$articlelink.'</span>';
} else {
$articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params );
}
- if( $watched )
+ # Bolden pages watched by this user
+ if( $watched ) {
$articlelink = "<strong class=\"mw-watched\">{$articlelink}</strong>";
- global $wgContLang;
+ }
+ # RTL/LTR marker
$articlelink .= $wgContLang->getDirMark();
- wfRunHooks('ChangesListInsertArticleLink',
- array(&$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched));
+ wfRunHooks( 'ChangesListInsertArticleLink',
+ array(&$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched) );
- $s .= ' '.$articlelink;
+ $s .= " $articlelink";
}
- protected function insertTimestamp(&$s, $rc) {
+ protected function insertTimestamp( &$s, $rc ) {
global $wgLang;
- # Timestamp
- $s .= $this->message['semicolon-separator'] . ' ' . $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . ';
+ $s .= $this->message['semicolon-separator'] .
+ $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . ';
}
/** Insert links to user page, user talk page and eventually a blocking link */
- protected function insertUserRelatedLinks(&$s, &$rc) {
- if ( $this->isDeleted($rc,Revision::DELETED_USER) ) {
+ public function insertUserRelatedLinks(&$s, &$rc) {
+ if( $this->isDeleted($rc,Revision::DELETED_USER) ) {
$s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-user') . '</span>';
} else {
$s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
@@ -216,13 +234,11 @@ class ChangesList {
/** insert a formatted action */
protected function insertAction(&$s, &$rc) {
- # Add action
if( $rc->mAttribs['rc_type'] == RC_LOG ) {
- // log action
- if ( $this->isDeleted($rc,LogPage::DELETED_ACTION) ) {
+ if( $this->isDeleted($rc,LogPage::DELETED_ACTION) ) {
$s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
} else {
- $s .= ' ' . LogPage::actionText( $rc->mAttribs['rc_log_type'], $rc->mAttribs['rc_log_action'],
+ $s .= ' '.LogPage::actionText( $rc->mAttribs['rc_log_type'], $rc->mAttribs['rc_log_action'],
$rc->getTitle(), $this->skin, LogPage::extractParams($rc->mAttribs['rc_params']), true, true );
}
}
@@ -230,10 +246,8 @@ class ChangesList {
/** insert a formatted comment */
protected function insertComment(&$s, &$rc) {
- # Add comment
if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) {
- // log comment
- if ( $this->isDeleted($rc,Revision::DELETED_COMMENT) ) {
+ if( $this->isDeleted($rc,Revision::DELETED_COMMENT) ) {
$s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>';
} else {
$s .= $this->skin->commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() );
@@ -256,8 +270,8 @@ class ChangesList {
protected function numberofWatchingusers( $count ) {
global $wgLang;
static $cache = array();
- if ( $count > 0 ) {
- if ( !isset( $cache[$count] ) ) {
+ if( $count > 0 ) {
+ if( !isset( $cache[$count] ) ) {
$cache[$count] = wfMsgExt('number_of_watching_users_RCview',
array('parsemag', 'escape'), $wgLang->formatNum($count));
}
@@ -290,12 +304,20 @@ class ChangesList {
$permission = ( $rc->mAttribs['rc_deleted'] & Revision::DELETED_RESTRICTED ) == Revision::DELETED_RESTRICTED
? 'suppressrevision'
: 'deleterevision';
- wfDebug( "Checking for $permission due to $field match on $rc->mAttribs['rc_deleted']\n" );
+ wfDebug( "Checking for $permission due to $field match on {$rc->mAttribs['rc_deleted']}\n" );
return $wgUser->isAllowed( $permission );
} else {
return true;
}
}
+
+ protected function maybeWatchedLink( $link, $watched=false ) {
+ if( $watched ) {
+ return '<strong class="mw-watched">' . $link . '</strong>';
+ } else {
+ return '<span class="mw-rc-unwatched">' . $link . '</span>';
+ }
+ }
}
@@ -308,55 +330,43 @@ class OldChangesList extends ChangesList {
*/
public function recentChangesLine( &$rc, $watched = false ) {
global $wgContLang, $wgRCShowChangedSize, $wgUser;
-
- $fname = 'ChangesList::recentChangesLineOld';
- wfProfileIn( $fname );
-
- # Extract DB fields into local scope
- // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables.
- extract( $rc->mAttribs );
-
+ wfProfileIn( __METHOD__ );
# Should patrol-related stuff be shown?
- $unpatrolled = $wgUser->useRCPatrol() && $rc_patrolled == 0;
+ $unpatrolled = $wgUser->useRCPatrol() && !$rc->mAttribs['rc_patrolled'];
- $this->insertDateHeader($s,$rc_timestamp);
-
- $s .= '<li>';
+ $dateheader = ''; // $s now contains only <li>...</li>, for hooks' convenience.
+ $this->insertDateHeader( $dateheader, $rc->mAttribs['rc_timestamp'] );
+ $s = '';
// Moved pages
- if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
+ if( $rc->mAttribs['rc_type'] == RC_MOVE || $rc->mAttribs['rc_type'] == RC_MOVE_OVER_REDIRECT ) {
$this->insertMove( $s, $rc );
// Log entries
- } elseif( $rc_log_type ) {
- $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL );
- $this->insertLog( $s, $logtitle, $rc_log_type );
+ } elseif( $rc->mAttribs['rc_log_type'] ) {
+ $logtitle = Title::newFromText( 'Log/'.$rc->mAttribs['rc_log_type'], NS_SPECIAL );
+ $this->insertLog( $s, $logtitle, $rc->mAttribs['rc_log_type'] );
// Log entries (old format) or log targets, and special pages
- } elseif( $rc_namespace == NS_SPECIAL ) {
- list( $specialName, $specialSubpage ) = SpecialPage::resolveAliasWithSubpage( $rc_title );
- if ( $specialName == 'Log' ) {
- $this->insertLog( $s, $rc->getTitle(), $specialSubpage );
- } else {
- wfDebug( "Unexpected special page in recentchanges\n" );
+ } elseif( $rc->mAttribs['rc_namespace'] == NS_SPECIAL ) {
+ list( $name, $subpage ) = SpecialPage::resolveAliasWithSubpage( $rc->mAttribs['rc_title'] );
+ if( $name == 'Log' ) {
+ $this->insertLog( $s, $rc->getTitle(), $subpage );
}
// Regular entries
} else {
- wfProfileIn($fname.'-page');
-
- $this->insertDiffHist($s, $rc, $unpatrolled);
-
+ $this->insertDiffHist( $s, $rc, $unpatrolled );
# M, N, b and ! (minor, new, bot and unpatrolled)
- $s .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $unpatrolled, '', $rc_bot );
- $this->insertArticleLink($s, $rc, $unpatrolled, $watched);
-
- wfProfileOut($fname.'-page');
+ $s .= $this->recentChangesFlags( $rc->mAttribs['rc_new'], $rc->mAttribs['rc_minor'],
+ $unpatrolled, '', $rc->mAttribs['rc_bot'] );
+ $this->insertArticleLink( $s, $rc, $unpatrolled, $watched );
}
-
- wfProfileIn( $fname.'-rest' );
-
- $this->insertTimestamp($s,$rc);
-
+ # Edit/log timestamp
+ $this->insertTimestamp( $s, $rc );
+ # Bytes added or removed
if( $wgRCShowChangedSize ) {
- $s .= ( $rc->getCharacterDifference() == '' ? '' : $rc->getCharacterDifference() . ' . . ' );
+ $cd = $rc->getCharacterDifference();
+ if( $cd != '' ) {
+ $s .= "$cd . . ";
+ }
}
# User tool links
$this->insertUserRelatedLinks($s,$rc);
@@ -364,29 +374,45 @@ class OldChangesList extends ChangesList {
$this->insertAction($s, $rc);
# Edit or log comment
$this->insertComment($s, $rc);
-
# Mark revision as deleted if so
- if ( !$rc_log_type && $this->isDeleted($rc,Revision::DELETED_TEXT) )
+ if( !$rc->mAttribs['rc_log_type'] && $this->isDeleted($rc,Revision::DELETED_TEXT) ) {
$s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
- if($rc->numberofWatchingusers > 0) {
- $s .= ' ' . wfMsg('number_of_watching_users_RCview', $wgContLang->formatNum($rc->numberofWatchingusers));
+ }
+ # How many users watch this page
+ if( $rc->numberofWatchingusers > 0 ) {
+ $s .= ' ' . wfMsg( 'number_of_watching_users_RCview',
+ $wgContLang->formatNum($rc->numberofWatchingusers) );
}
- $s .= "</li>\n";
-
- wfProfileOut( $fname.'-rest' );
+ wfRunHooks( 'OldChangesListRecentChangesLine', array(&$this, &$s, $rc) );
- wfProfileOut( $fname );
- return $s;
+ wfProfileOut( __METHOD__ );
+ return "$dateheader<li>$s</li>\n";
}
}
/**
- * Generate a list of changes using an Enhanced system (use javascript).
+ * Generate a list of changes using an Enhanced system (uses javascript).
*/
class EnhancedChangesList extends ChangesList {
/**
+ * Add the JavaScript file for enhanced changeslist
+ * @ return string
+ */
+ public function beginRecentChangesList() {
+ global $wgStylePath, $wgJsMimeType, $wgStyleVersion;
+ $this->rc_cache = array();
+ $this->rcMoveIndex = 0;
+ $this->rcCacheIndex = 0;
+ $this->lastdate = '';
+ $this->rclistOpen = false;
+ $script = Xml::tags( 'script', array(
+ 'type' => $wgJsMimeType,
+ 'src' => $wgStylePath . "/common/enhancedchanges.js?$wgStyleVersion" ), '' );
+ return $script;
+ }
+ /**
* Format a line for enhanced recentchange (aka with javascript and block of lines).
*/
public function recentChangesLine( &$baseRC, $watched = false ) {
@@ -396,12 +422,13 @@ class EnhancedChangesList extends ChangesList {
$rc = RCCacheEntry::newFromParent( $baseRC );
# Extract fields from DB into the function scope (rc_xxxx variables)
- // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables.
+ // FIXME: Would be good to replace this extract() call with something
+ // that explicitly initializes variables.
extract( $rc->mAttribs );
$curIdEq = 'curid=' . $rc_cur_id;
# If it's a new day, add the headline and flush the cache
- $date = $wgLang->date( $rc_timestamp, true);
+ $date = $wgLang->date( $rc_timestamp, true );
$ret = '';
if( $date != $this->lastdate ) {
# Process current cache
@@ -425,17 +452,6 @@ class EnhancedChangesList extends ChangesList {
$msg = ( $rc_type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir";
$clink = wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ),
$this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) );
- // Log entries (old format) and special pages
- } elseif( $rc_namespace == NS_SPECIAL ) {
- list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $rc_title );
- if ( $specialName == 'Log' ) {
- # Log updates, etc
- $logname = LogPage::logName( $logtype );
- $clink = '(' . $this->skin->makeKnownLinkObj( $rc->getTitle(), $logname ) . ')';
- } else {
- wfDebug( "Unexpected special page in recentchanges\n" );
- $clink = '';
- }
// New unpatrolled pages
} else if( $rc->unpatrolled && $rc_type == RC_NEW ) {
$clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', "rcid={$rc_id}" );
@@ -443,11 +459,23 @@ class EnhancedChangesList extends ChangesList {
} else if( $rc_type == RC_LOG ) {
if( $rc_log_type ) {
$logtitle = SpecialPage::getTitleFor( 'Log', $rc_log_type );
- $clink = '(' . $this->skin->makeKnownLinkObj( $logtitle, LogPage::logName($rc_log_type) ) . ')';
+ $clink = '(' . $this->skin->makeKnownLinkObj( $logtitle,
+ LogPage::logName($rc_log_type) ) . ')';
} else {
$clink = $this->skin->makeLinkObj( $rc->getTitle(), '' );
}
$watched = false;
+ // Log entries (old format) and special pages
+ } elseif( $rc_namespace == NS_SPECIAL ) {
+ list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $rc_title );
+ if ( $specialName == 'Log' ) {
+ # Log updates, etc
+ $logname = LogPage::logName( $logtype );
+ $clink = '(' . $this->skin->makeKnownLinkObj( $rc->getTitle(), $logname ) . ')';
+ } else {
+ wfDebug( "Unexpected special page in recentchanges\n" );
+ $clink = '';
+ }
// Edits
} else {
$clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '' );
@@ -473,7 +501,8 @@ class EnhancedChangesList extends ChangesList {
$querycur = $curIdEq."&diff=0&oldid=$rc_this_oldid";
$querydiff = $curIdEq."&diff=$rc_this_oldid&oldid=$rc_last_oldid$rcIdQuery";
$aprops = ' tabindex="'.$baseRC->counter.'"';
- $curLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['cur'], $querycur, '' ,'', $aprops );
+ $curLink = $this->skin->makeKnownLinkObj( $rc->getTitle(),
+ $this->message['cur'], $querycur, '' ,'', $aprops );
# Make "diff" an "cur" links
if( !$showdifflinks ) {
@@ -485,7 +514,8 @@ class EnhancedChangesList extends ChangesList {
}
$diffLink = $this->message['diff'];
} else {
- $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'], $querydiff, '' ,'', $aprops );
+ $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'],
+ $querydiff, '' ,'', $aprops );
}
# Make "last" link
@@ -545,7 +575,7 @@ class EnhancedChangesList extends ChangesList {
$curId = $currentRevision = 0;
# Some catalyst variables...
$namehidden = true;
- $alllogs = true;
+ $allLogs = true;
foreach( $block as $rcObj ) {
$oldid = $rcObj->mAttribs['rc_last_oldid'];
if( $rcObj->mAttribs['rc_new'] ) {
@@ -564,7 +594,7 @@ class EnhancedChangesList extends ChangesList {
$unpatrolled = true;
}
if( $rcObj->mAttribs['rc_type'] != RC_LOG ) {
- $alllogs = false;
+ $allLogs = false;
}
# Get the latest entry with a page_id and oldid
# since logs may not have these.
@@ -587,20 +617,24 @@ class EnhancedChangesList extends ChangesList {
$text = $userlink;
$text .= $wgContLang->getDirMark();
if( $count > 1 ) {
- $text .= ' ('.$count.'&times;)';
+ $text .= ' (' . $wgLang->formatNum( $count ) . '×)';
}
array_push( $users, $text );
}
- $users = ' <span class="changedby">[' . implode( $this->message['semicolon-separator'] . ' ', $users ) . ']</span>';
+ $users = ' <span class="changedby">[' .
+ implode( $this->message['semicolon-separator'], $users ) . ']</span>';
- # Arrow
- $rci = 'RCI'.$this->rcCacheIndex;
- $rcl = 'RCL'.$this->rcCacheIndex;
- $rcm = 'RCM'.$this->rcCacheIndex;
- $toggleLink = "javascript:toggleVisibility('$rci','$rcm','$rcl')";
- $tl = '<span id="'.$rcm.'"><a href="'.$toggleLink.'">' . $this->sideArrow() . '</a></span>';
- $tl .= '<span id="'.$rcl.'" style="display:none"><a href="'.$toggleLink.'">' . $this->downArrow() . '</a></span>';
+ # ID for JS visibility toggle
+ $jsid = $this->rcCacheIndex;
+ # onclick handler to toggle hidden/expanded
+ $toggleLink = "onclick='toggleVisibility($jsid); return false'";
+ # Title for <a> tags
+ $expandTitle = htmlspecialchars( wfMsg('rc-enhanced-expand') );
+ $closeTitle = htmlspecialchars( wfMsg('rc-enhanced-hide') );
+
+ $tl = "<span id='mw-rc-openarrow-$jsid' class='mw-changeslist-expanded' style='visibility:hidden'><a href='#' $toggleLink title='$expandTitle'>" . $this->sideArrow() . "</a></span>";
+ $tl .= "<span id='mw-rc-closearrow-$jsid' class='mw-changeslist-hidden' style='display:none'><a href='#' $toggleLink title='$closeTitle'>" . $this->downArrow() . "</a></span>";
$r .= '<td valign="top" style="white-space: nowrap"><tt>'.$tl.'&nbsp;';
# Main line
@@ -612,8 +646,10 @@ class EnhancedChangesList extends ChangesList {
# Article link
if( $namehidden ) {
$r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
- } else {
+ } else if( $allLogs ) {
$r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched );
+ } else {
+ $this->insertArticleLink( $r, $block[0], $block[0]->unpatrolled, $block[0]->watched );
}
$r .= $wgContLang->getDirMark();
@@ -627,7 +663,7 @@ class EnhancedChangesList extends ChangesList {
}
# Total change link
$r .= ' ';
- if( !$alllogs ) {
+ if( !$allLogs ) {
$r .= '(';
if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) {
$r .= $nchanges[$n];
@@ -637,11 +673,21 @@ class EnhancedChangesList extends ChangesList {
$r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(),
$nchanges[$n], $curIdEq."&diff=$currentRevision&oldid=$oldid" );
}
- $r .= ') . . ';
}
+ # History
+ if( $allLogs ) {
+ // don't show history link for logs
+ } else if( $namehidden || !$block[0]->getTitle()->exists() ) {
+ $r .= $this->message['semicolon-separator'] . $this->message['hist'] . ')';
+ } else {
+ $r .= $this->message['semicolon-separator'] . $this->skin->makeKnownLinkObj( $block[0]->getTitle(),
+ $this->message['hist'], $curIdEq . '&action=history' ) . ')';
+ }
+ $r .= ' . . ';
+
# Character difference (does not apply if only log items)
- if( $wgRCShowChangedSize && !$alllogs ) {
+ if( $wgRCShowChangedSize && !$allLogs ) {
$last = 0;
$first = count($block) - 1;
# Some events (like logs) have an "empty" size, so we need to skip those...
@@ -662,26 +708,18 @@ class EnhancedChangesList extends ChangesList {
}
}
- # History
- if( $alllogs ) {
- // don't show history link for logs
- } else if( $namehidden || !$block[0]->getTitle()->exists() ) {
- $r .= '(' . $this->message['history'] . ')';
- } else {
- $r .= '(' . $this->skin->makeKnownLinkObj( $block[0]->getTitle(),
- $this->message['history'], $curIdEq.'&action=history' ) . ')';
- }
-
$r .= $users;
$r .= $this->numberofWatchingusers($block[0]->numberofWatchingusers);
$r .= "</td></tr></table>\n";
# Sub-entries
- $r .= '<div id="'.$rci.'" style="display:none;"><table cellpadding="0" cellspacing="0" border="0" style="background: none">';
+ $r .= '<div id="mw-rc-subentries-'.$jsid.'" class="mw-changeslist-hidden">';
+ $r .= '<table cellpadding="0" cellspacing="0" border="0" style="background: none">';
foreach( $block as $rcObj ) {
- # Get rc_xxxx variables
- // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables.
+ # Extract fields from DB into the function scope (rc_xxxx variables)
+ // FIXME: Would be good to replace this extract() call with something
+ // that explicitly initializes variables.
extract( $rcObj->mAttribs );
#$r .= '<tr><td valign="top">'.$this->spacerArrow();
@@ -701,9 +739,10 @@ class EnhancedChangesList extends ChangesList {
} else if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) {
$link = '<span class="history-deleted"><tt>'.$rcObj->timestamp.'</tt></span> ';
} else {
- $rcIdEq = ($rcObj->unpatrolled && $rc_type == RC_NEW) ? '&rcid='.$rcObj->mAttribs['rc_id'] : '';
-
- $link = '<tt>'.$this->skin->makeKnownLinkObj( $rcObj->getTitle(), $rcObj->timestamp, $curIdEq.'&'.$o.$rcIdEq ).'</tt>';
+ $rcIdEq = ($rcObj->unpatrolled && $rc_type == RC_NEW) ?
+ '&rcid='.$rcObj->mAttribs['rc_id'] : '';
+ $link = '<tt>'.$this->skin->makeKnownLinkObj( $rcObj->getTitle(),
+ $rcObj->timestamp, $curIdEq.'&'.$o.$rcIdEq ).'</tt>';
if( $this->isDeleted($rcObj,Revision::DELETED_TEXT) )
$link = '<span class="history-deleted">'.$link.'</span> ';
}
@@ -712,7 +751,7 @@ class EnhancedChangesList extends ChangesList {
if ( !$rc_type == RC_LOG || $rc_type == RC_NEW ) {
$r .= ' (';
$r .= $rcObj->curlink;
- $r .= $this->message['semicolon-separator'] . ' ';
+ $r .= $this->message['semicolon-separator'];
$r .= $rcObj->lastlink;
$r .= ')';
}
@@ -742,26 +781,19 @@ class EnhancedChangesList extends ChangesList {
return $r;
}
- protected function maybeWatchedLink( $link, $watched=false ) {
- if( $watched ) {
- // FIXME: css style might be more appropriate
- return '<strong class="mw-watched">' . $link . '</strong>';
- } else {
- return $link;
- }
- }
-
/**
* Generate HTML for an arrow or placeholder graphic
* @param string $dir one of '', 'd', 'l', 'r'
* @param string $alt text
+ * @param string $title text
* @return string HTML <img> tag
*/
- protected function arrow( $dir, $alt='' ) {
+ protected function arrow( $dir, $alt='', $title='' ) {
global $wgStylePath;
$encUrl = htmlspecialchars( $wgStylePath . '/common/images/Arr_' . $dir . '.png' );
$encAlt = htmlspecialchars( $alt );
- return "<img src=\"$encUrl\" width=\"12\" height=\"12\" alt=\"$encAlt\" />";
+ $encTitle = htmlspecialchars( $title );
+ return "<img src=\"$encUrl\" width=\"12\" height=\"12\" alt=\"$encAlt\" title=\"$encTitle\" />";
}
/**
@@ -772,7 +804,7 @@ class EnhancedChangesList extends ChangesList {
protected function sideArrow() {
global $wgContLang;
$dir = $wgContLang->isRTL() ? 'l' : 'r';
- return $this->arrow( $dir, '+' );
+ return $this->arrow( $dir, '+', wfMsg('rc-enhanced-expand') );
}
/**
@@ -781,7 +813,7 @@ class EnhancedChangesList extends ChangesList {
* @return string HTML <img> tag
*/
protected function downArrow() {
- return $this->arrow( 'd', '-' );
+ return $this->arrow( 'd', '-', wfMsg('rc-enhanced-hide') );
}
/**
@@ -789,7 +821,7 @@ class EnhancedChangesList extends ChangesList {
* @return string HTML <img> tag
*/
protected function spacerArrow() {
- return $this->arrow( '', ' ' );
+ return $this->arrow( '', codepointToUtf8( 0xa0 ) ); // non-breaking space
}
/**
@@ -806,16 +838,14 @@ class EnhancedChangesList extends ChangesList {
*/
protected function recentChangesBlockLine( $rcObj ) {
global $wgContLang, $wgRCShowChangedSize;
-
- # Get rc_xxxx variables
- // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables.
+ # Extract fields from DB into the function scope (rc_xxxx variables)
+ // FIXME: Would be good to replace this extract() call with something
+ // that explicitly initializes variables.
extract( $rcObj->mAttribs );
- $curIdEq = 'curid='.$rc_cur_id;
+ $curIdEq = "curid={$rc_cur_id}";
$r = '<table cellspacing="0" cellpadding="0" border="0" style="background: none"><tr>';
-
$r .= '<td valign="top" style="white-space: nowrap"><tt>' . $this->spacerArrow() . '&nbsp;';
-
# Flag and Timestamp
if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
$r .= '&nbsp;&nbsp;&nbsp;&nbsp;'; // 4 flags -> 4 spaces
@@ -823,33 +853,27 @@ class EnhancedChangesList extends ChangesList {
$r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
}
$r .= '&nbsp;'.$rcObj->timestamp.'&nbsp;</tt></td><td>';
-
# Article or log link
if( $rc_log_type ) {
$logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL );
$logname = LogPage::logName( $rc_log_type );
$r .= '(' . $this->skin->makeKnownLinkObj($logtitle, $logname ) . ')';
- } else if( !$this->userCan($rcObj,Revision::DELETED_TEXT) ) {
- $r .= '<span class="history-deleted">' . $rcObj->link . '</span>';
} else {
- $r .= $this->maybeWatchedLink( $rcObj->link, $rcObj->watched );
+ $this->insertArticleLink( $r, $rcObj, $rcObj->unpatrolled, $rcObj->watched );
}
-
# Diff and hist links
if ( $rc_type != RC_LOG ) {
- $r .= ' ('. $rcObj->difflink . $this->message['semicolon-separator'] . ' ';
- $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ) . ')';
+ $r .= ' ('. $rcObj->difflink . $this->message['semicolon-separator'];
+ $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ),
+ $curIdEq.'&action=history' ) . ')';
}
$r .= ' . . ';
-
# Character diff
- if( $wgRCShowChangedSize ) {
- $r .= ( $rcObj->getCharacterDifference() == '' ? '' : '&nbsp;' . $rcObj->getCharacterDifference() . ' . . ' ) ;
+ if( $wgRCShowChangedSize && ($cd = $rcObj->getCharacterDifference()) ) {
+ $r .= "$cd . . ";
}
-
# User/talk
$r .= ' '.$rcObj->userlink . $rcObj->usertalklink;
-
# Log action (if any)
if( $rc_log_type ) {
if( $this->isDeleted($rcObj,LogPage::DELETED_ACTION) ) {
@@ -859,7 +883,6 @@ class EnhancedChangesList extends ChangesList {
$this->skin, LogPage::extractParams($rc_params), true, true );
}
}
-
# Edit or log comment
if( $rc_type != RC_MOVE && $rc_type != RC_MOVE_OVER_REDIRECT ) {
// log comment
@@ -869,7 +892,6 @@ class EnhancedChangesList extends ChangesList {
$r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() );
}
}
-
# Show how many people are watching this if enabled
$r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers);
@@ -893,7 +915,6 @@ class EnhancedChangesList extends ChangesList {
$blockOut .= $this->recentChangesBlockGroup( $block );
}
}
-
return '<div>'.$blockOut.'</div>';
}
diff --git a/includes/Credits.php b/includes/Credits.php
index 6326e3a2..ae9377f2 100644
--- a/includes/Credits.php
+++ b/includes/Credits.php
@@ -20,167 +20,187 @@
* @author <evan@wikitravel.org>
*/
-/**
- * This is largely cadged from PageHistory::history
- */
-function showCreditsPage($article) {
- global $wgOut;
-
- $fname = 'showCreditsPage';
-
- wfProfileIn( $fname );
-
- $wgOut->setPageTitle( $article->mTitle->getPrefixedText() );
- $wgOut->setSubtitle( wfMsg( 'creditspage' ) );
- $wgOut->setArticleFlag( false );
- $wgOut->setArticleRelated( true );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
-
- if( $article->mTitle->getArticleID() == 0 ) {
- $s = wfMsg( 'nocredits' );
- } else {
- $s = getCredits($article, -1);
- }
-
- $wgOut->addHTML( $s );
-
- wfProfileOut( $fname );
-}
-
-function getCredits($article, $cnt, $showIfMax=true) {
- $fname = 'getCredits';
- wfProfileIn( $fname );
- $s = '';
-
- if (isset($cnt) && $cnt != 0) {
- $s = getAuthorCredits($article);
- if ($cnt > 1 || $cnt < 0) {
- $s .= ' ' . getContributorCredits($article, $cnt - 1, $showIfMax);
+class Credits {
+
+ /**
+ * This is largely cadged from PageHistory::history
+ * @param $article Article object
+ */
+ public static function showPage( Article $article ) {
+ global $wgOut;
+
+ wfProfileIn( __METHOD__ );
+
+ $wgOut->setPageTitle( $article->mTitle->getPrefixedText() );
+ $wgOut->setSubtitle( wfMsg( 'creditspage' ) );
+ $wgOut->setArticleFlag( false );
+ $wgOut->setArticleRelated( true );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+
+ if( $article->mTitle->getArticleID() == 0 ) {
+ $s = wfMsg( 'nocredits' );
+ } else {
+ $s = self::getCredits($article, -1 );
}
+
+ $wgOut->addHTML( $s );
+
+ wfProfileOut( __METHOD__ );
}
- wfProfileOut( $fname );
- return $s;
-}
-
-/**
- *
- */
-function getAuthorCredits($article) {
- global $wgLang, $wgAllowRealName;
-
- $last_author = $article->getUser();
-
- if ($last_author == 0) {
- $author_credit = wfMsg('anonymous');
- } else {
- if($wgAllowRealName) { $real_name = User::whoIsReal($last_author); }
- $user_name = User::whoIs($last_author);
-
- if (!empty($real_name)) {
- $author_credit = creditLink($user_name, $real_name);
- } else {
- $author_credit = wfMsg('siteuser', creditLink($user_name));
+ /**
+ * Get a list of contributors of $article
+ * @param $article Article object
+ * @param $cnt Int: maximum list of contributors to show
+ * @param $showIfMax Bool: whether to contributors if there more than $cnt
+ * @return String: html
+ */
+ public static function getCredits($article, $cnt, $showIfMax=true) {
+ wfProfileIn( __METHOD__ );
+ $s = '';
+
+ if( isset( $cnt ) && $cnt != 0 ){
+ $s = self::getAuthor( $article );
+ if ($cnt > 1 || $cnt < 0) {
+ $s .= ' ' . self::getContributors( $article, $cnt - 1, $showIfMax );
+ }
}
- }
- $timestamp = $article->getTimestamp();
- if ($timestamp) {
- $d = $wgLang->date($article->getTimestamp(), true);
- $t = $wgLang->time($article->getTimestamp(), true);
- } else {
- $d = '';
- $t = '';
+ wfProfileOut( __METHOD__ );
+ return $s;
}
- return wfMsg('lastmodifiedatby', $d, $t, $author_credit);
-}
-
-/**
- *
- */
-function getContributorCredits($article, $cnt, $showIfMax) {
-
- global $wgLang, $wgAllowRealName;
- $contributors = $article->getContributors();
+ /**
+ * Get the last author with the last modification time
+ * @param $article Article object
+ */
+ protected static function getAuthor( Article $article ){
+ global $wgLang, $wgAllowRealName;
- $others_link = '';
+ $user = User::newFromId( $article->getUser() );
- # Hmm... too many to fit!
-
- if ($cnt > 0 && count($contributors) > $cnt) {
- $others_link = creditOthersLink($article);
- if (!$showIfMax) {
- return wfMsg('othercontribs', $others_link);
+ $timestamp = $article->getTimestamp();
+ if( $timestamp ){
+ $d = $wgLang->date( $article->getTimestamp(), true );
+ $t = $wgLang->time( $article->getTimestamp(), true );
} else {
- $contributors = array_slice($contributors, 0, $cnt);
+ $d = '';
+ $t = '';
}
+ return wfMsg( 'lastmodifiedatby', $d, $t, self::userLink( $user ) );
}
- $real_names = array();
- $user_names = array();
-
- $anon = '';
-
- # Sift for real versus user names
-
- foreach ($contributors as $user_parts) {
- if ($user_parts[0] != 0) {
- if ($wgAllowRealName && !empty($user_parts[2])) {
- $real_names[] = creditLink($user_parts[1], $user_parts[2]);
+ /**
+ * Get a list of contributors of $article
+ * @param $article Article object
+ * @param $cnt Int: maximum list of contributors to show
+ * @param $showIfMax Bool: whether to contributors if there more than $cnt
+ * @return String: html
+ */
+ protected static function getContributors( Article $article, $cnt, $showIfMax ) {
+ global $wgLang, $wgAllowRealName;
+
+ $contributors = $article->getContributors();
+
+ $others_link = '';
+
+ # Hmm... too many to fit!
+ if( $cnt > 0 && $contributors->count() > $cnt ){
+ $others_link = self::othersLink( $article );
+ if( !$showIfMax )
+ return wfMsg( 'othercontribs', $others_link );
+ }
+
+ $real_names = array();
+ $user_names = array();
+ $anon = 0;
+
+ # Sift for real versus user names
+ foreach( $contributors as $user ) {
+ $cnt--;
+ if( $user->isLoggedIn() ){
+ $link = self::link( $user );
+ if( $wgAllowRealName && $user->getRealName() )
+ $real_names[] = $link;
+ else
+ $user_names[] = $link;
} else {
- $user_names[] = creditLink($user_parts[1]);
+ $anon++;
+ }
+ if( $cnt == 0 ) break;
+ }
+
+ # Two strings: real names, and user names
+ $real = $wgLang->listToText( $real_names );
+ $user = $wgLang->listToText( $user_names );
+ if( $anon )
+ $anon = wfMsgExt( 'anonymous', array( 'parseinline' ), $anon );
+
+ # "ThisSite user(s) A, B and C"
+ if( !empty( $user ) ){
+ $user = wfMsgExt( 'siteusers', array( 'parsemag' ), $user, count( $user_names ) );
+ }
+
+ # This is the big list, all mooshed together. We sift for blank strings
+ $fulllist = array();
+ foreach( array( $real, $user, $anon, $others_link ) as $s ){
+ if( !empty( $s ) ){
+ array_push( $fulllist, $s );
}
- } else {
- $anon = wfMsg('anonymous');
}
- }
-
- # Two strings: real names, and user names
-
- $real = $wgLang->listToText($real_names);
- $user = $wgLang->listToText($user_names);
- # "ThisSite user(s) A, B and C"
+ # Make the list into text...
+ $creds = $wgLang->listToText( $fulllist );
- if (!empty($user)) {
- $user = wfMsg('siteusers', $user);
+ # "Based on work by ..."
+ return empty( $creds ) ? '' : wfMsg( 'othercontribs', $creds );
}
- # This is the big list, all mooshed together. We sift for blank strings
-
- $fulllist = array();
+ /**
+ * Get a link to $user_name page
+ * @param $user User object
+ * @return String: html
+ */
+ protected static function link( User $user ) {
+ global $wgUser, $wgAllowRealName;
+ if( $wgAllowRealName )
+ $real = $user->getRealName();
+ else
+ $real = false;
+
+ $skin = $wgUser->getSkin();
+ $page = $user->getUserPage();
+
+ return $skin->link( $page, htmlspecialchars( $real ? $real : $user->getName() ) );
+ }
- foreach (array($real, $user, $anon, $others_link) as $s) {
- if (!empty($s)) {
- array_push($fulllist, $s);
+ /**
+ * Get a link to $user_name page
+ * @param $user_name String: user name
+ * @param $linkText String: optional display
+ * @return String: html
+ */
+ protected static function userLink( User $user ) {
+ global $wgUser, $wgAllowRealName;
+ if( $user->isAnon() ){
+ return wfMsgExt( 'anonymous', array( 'parseinline' ), 1 );
+ } else {
+ $link = self::link( $user );
+ if( $wgAllowRealName && $user->getRealName() )
+ return $link;
+ else
+ return wfMsgExt( 'siteuser', array( 'parseinline', 'replaceafter' ), $link );
}
}
- # Make the list into text...
-
- $creds = $wgLang->listToText($fulllist);
-
- # "Based on work by ..."
-
- return (empty($creds)) ? '' : wfMsg('othercontribs', $creds);
-}
-
-/**
- *
- */
-function creditLink($user_name, $link_text = '') {
- global $wgUser, $wgContLang;
- $skin = $wgUser->getSkin();
- return $skin->makeLink($wgContLang->getNsText(NS_USER) . ':' . $user_name,
- htmlspecialchars( (empty($link_text)) ? $user_name : $link_text ));
-}
-
-/**
- *
- */
-function creditOthersLink($article) {
- global $wgUser;
- $skin = $wgUser->getSkin();
- return $skin->makeKnownLink($article->mTitle->getPrefixedText(), wfMsg('others'), 'action=credits');
-}
+ /**
+ * Get a link to action=credits of $article page
+ * @param $article Article object
+ * @return String: html
+ */
+ protected static function othersLink( Article $article ) {
+ global $wgUser;
+ $skin = $wgUser->getSkin();
+ return $skin->link( $article->getTitle(), wfMsgHtml( 'others' ), array(), array( 'action' => 'credits' ), array( 'known' ) );
+ }
+} \ No newline at end of file
diff --git a/includes/DatabaseFunctions.php b/includes/DatabaseFunctions.php
index ad6e7f6c..52e9a8c8 100644
--- a/includes/DatabaseFunctions.php
+++ b/includes/DatabaseFunctions.php
@@ -154,6 +154,7 @@ function wfFieldName( $res, $n, $dbi = DB_LAST )
/**
* @todo document function
+ * @see Database::insertId()
*/
function wfInsertId( $dbi = DB_LAST ) {
$db = wfGetDB( $dbi );
@@ -166,6 +167,7 @@ function wfInsertId( $dbi = DB_LAST ) {
/**
* @todo document function
+ * @see Database::dataSeek()
*/
function wfDataSeek( $res, $row, $dbi = DB_LAST ) {
$db = wfGetDB( $dbi );
@@ -177,7 +179,8 @@ function wfDataSeek( $res, $row, $dbi = DB_LAST ) {
}
/**
- * @todo document function
+ * Get the last error number
+ * @see Database::lastErrno()
*/
function wfLastErrno( $dbi = DB_LAST ) {
$db = wfGetDB( $dbi );
@@ -189,7 +192,8 @@ function wfLastErrno( $dbi = DB_LAST ) {
}
/**
- * @todo document function
+ * Get the last error
+ * @see Database::lastError()
*/
function wfLastError( $dbi = DB_LAST ) {
$db = wfGetDB( $dbi );
@@ -201,7 +205,8 @@ function wfLastError( $dbi = DB_LAST ) {
}
/**
- * @todo document function
+ * Get the number of affected rows
+ * @see Database::affectedRows()
*/
function wfAffectedRows( $dbi = DB_LAST ) {
$db = wfGetDB( $dbi );
@@ -213,7 +218,8 @@ function wfAffectedRows( $dbi = DB_LAST ) {
}
/**
- * @todo document function
+ * Get the last query ran
+ * @see Database::lastQuery
*/
function wfLastDBquery( $dbi = DB_LAST ) {
$db = wfGetDB( $dbi );
@@ -245,8 +251,8 @@ function wfSetSQL( $table, $var, $value, $cond, $dbi = DB_MASTER )
/**
+ * Simple select wrapper, return one field
* @see Database::selectField()
- * @todo document function
* @param $table
* @param $var
* @param $cond Default ''
@@ -263,8 +269,8 @@ function wfGetSQL( $table, $var, $cond='', $dbi = DB_LAST )
}
/**
+ * Does a given field exist on the specified table?
* @see Database::fieldExists()
- * @todo document function
* @param $table
* @param $field
* @param $dbi Default DB_LAST
@@ -280,8 +286,8 @@ function wfFieldExists( $table, $field, $dbi = DB_LAST ) {
}
/**
+ * Does the requested index exist on the specified table?
* @see Database::indexExists()
- * @todo document function
* @param $table String
* @param $index
* @param $dbi Default DB_LAST
@@ -354,7 +360,8 @@ function wfUpdateArray( $table, $values, $conds, $fname = 'wfUpdateArray', $dbi
}
/**
- * @todo document function
+ * Get fully usable table name
+ * @see Database::tableName()
*/
function wfTableName( $name, $dbi = DB_LAST ) {
$db = wfGetDB( $dbi );
@@ -367,6 +374,7 @@ function wfTableName( $name, $dbi = DB_LAST ) {
/**
* @todo document function
+ * @see Database::strencode()
*/
function wfStrencode( $s, $dbi = DB_LAST ) {
$db = wfGetDB( $dbi );
@@ -379,6 +387,7 @@ function wfStrencode( $s, $dbi = DB_LAST ) {
/**
* @todo document function
+ * @see Database::nextSequenceValue()
*/
function wfNextSequenceValue( $seqName, $dbi = DB_MASTER ) {
$db = wfGetDB( $dbi );
@@ -391,6 +400,7 @@ function wfNextSequenceValue( $seqName, $dbi = DB_MASTER ) {
/**
* @todo document function
+ * @see Database::useIndexClause()
*/
function wfUseIndexClause( $index, $dbi = DB_SLAVE ) {
$db = wfGetDB( $dbi );
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index aaf934f5..ed68fe7a 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -27,11 +27,13 @@ if( !defined( 'MEDIAWIKI' ) ) {
* Create a site configuration object
* Not used for much in a default install
*/
-require_once( "$IP/includes/SiteConfiguration.php" );
-$wgConf = new SiteConfiguration;
+if ( !defined( 'MW_PHP4' ) ) {
+ require_once( "$IP/includes/SiteConfiguration.php" );
+ $wgConf = new SiteConfiguration;
+}
/** MediaWiki version number */
-$wgVersion = '1.13.4';
+$wgVersion = '1.14.0';
/** Name of the site. It must be changed in LocalSettings.php */
$wgSitename = 'MediaWiki';
@@ -539,10 +541,10 @@ $wgSMTP = false;
*/
/** database host name or ip address */
$wgDBserver = 'localhost';
-/** database port number */
-$wgDBport = '';
+/** database port number (for PostgreSQL) */
+$wgDBport = 5432;
/** name of the database */
-$wgDBname = 'wikidb';
+$wgDBname = 'my_wiki';
/** */
$wgDBconnection = '';
/** Database username */
@@ -572,6 +574,12 @@ $wgDBts2schema = 'public';
/** To override default SQLite data directory ($docroot/../data) */
$wgSQLiteDataDir = '';
+/** Default directory mode for SQLite data directory on creation.
+ * Note that this is different from the default directory mode used
+ * elsewhere.
+ */
+$wgSQLiteDataDirMode = 0700;
+
/**
* Make all database connections secretly go to localhost. Fool the load balancer
* thinking there is an arbitrarily large cluster of servers to connect to.
@@ -672,14 +680,6 @@ $wgDBClusterTimeout = 10;
*/
$wgDBAvgStatusPoll = 2000;
-/**
- * wgDBminWordLen :
- * MySQL 3.x : used to discard words that MySQL will not return any results for
- * shorter values configure mysql directly.
- * MySQL 4.x : ignore it and configure mySQL
- * See: http://dev.mysql.com/doc/mysql/en/Fulltext_Fine-tuning.html
- */
-$wgDBminWordLen = 4;
/** Set to true if using InnoDB tables */
$wgDBtransactions = false;
/** Set to true for compatibility with extensions that might be checking.
@@ -745,12 +745,6 @@ $wgLocalMessageCache = false;
*/
$wgLocalMessageCacheSerialized = true;
-/**
- * Directory for compiled constant message array databases
- * WARNING: turning anything on will just break things, aaaaaah!!!!
- */
-$wgCachedMessageArrays = false;
-
# Language settings
#
/** Site language code, should be one of ./languages/Language(.*).php */
@@ -844,7 +838,6 @@ $wgTranslateNumerals = true;
/**
* Translation using MediaWiki: namespace.
- * This will increase load times by 25-60% unless memcached is installed.
* Interface messages will be loaded from the database.
*/
$wgUseDatabaseMessages = true;
@@ -860,6 +853,14 @@ $wgMsgCacheExpiry = 86400;
$wgMaxMsgCacheEntrySize = 10000;
/**
+ * If true, serialized versions of the messages arrays will be
+ * read from the 'serialized' subdirectory if they are present.
+ * Set to false to always use the Messages files, regardless of
+ * whether they are up to date or not.
+ */
+$wgEnableSerializedMessages = true;
+
+/**
* Set to false if you are thorough system admin who always remembers to keep
* serialized files up to date to save few mtime calls.
*/
@@ -868,6 +869,9 @@ $wgCheckSerialized = true;
/** Whether to enable language variant conversion. */
$wgDisableLangConversion = false;
+/** Whether to enable language variant conversion for links. */
+$wgDisableTitleConversion = false;
+
/** Default variant code, if false, the default will be the language code */
$wgDefaultLanguageVariant = false;
@@ -947,26 +951,68 @@ $wgMaxPPNodeCount = 1000000; # A complexity limit on template expansion
$wgMaxTemplateDepth = 40;
$wgMaxPPExpandDepth = 40;
+/**
+ * If true, removes (substitutes) templates in "~~~~" signatures.
+ */
+$wgCleanSignatures = true;
+
$wgExtraSubtitle = '';
$wgSiteSupportPage = ''; # A page where you users can receive donations
+/**
+ * Set this to a string to put the wiki into read-only mode. The text will be
+ * used as an explanation to users.
+ *
+ * This prevents most write operations via the web interface. Cache updates may
+ * still be possible. To prevent database writes completely, use the read_only
+ * option in MySQL.
+ */
+$wgReadOnly = null;
+
/***
- * If this lock file exists, the wiki will be forced into read-only mode.
+ * If this lock file exists (size > 0), the wiki will be forced into read-only mode.
* Its contents will be shown to users as part of the read-only warning
* message.
*/
$wgReadOnlyFile = false; ///< defaults to "{$wgUploadDirectory}/lock_yBgMBwiR";
/**
+ * Filename for debug logging.
* The debug log file should be not be publicly accessible if it is used, as it
- * may contain private data. */
+ * may contain private data.
+ */
$wgDebugLogFile = '';
+/**
+ * Prefix for debug log lines
+ */
+$wgDebugLogPrefix = '';
+
+/**
+ * If true, instead of redirecting, show a page with a link to the redirect
+ * destination. This allows for the inspection of PHP error messages, and easy
+ * resubmission of form data. For developer use only.
+ */
$wgDebugRedirects = false;
-$wgDebugRawPage = false; # Avoid overlapping debug entries by leaving out CSS
+/**
+ * If true, log debugging data from action=raw.
+ * This is normally false to avoid overlapping debug entries due to gen=css and
+ * gen=js requests.
+ */
+$wgDebugRawPage = false;
+
+/**
+ * Send debug data to an HTML comment in the output.
+ *
+ * This may occasionally be useful when supporting a non-technical end-user. It's
+ * more secure than exposing the debug log file to the web, since the output only
+ * contains private data for the current user. But it's not ideal for development
+ * use since data is lost on fatal errors and redirects.
+ */
$wgDebugComments = false;
-$wgReadOnly = null;
+
+/** Does nothing. Obsolete? */
$wgLogQueries = false;
/**
@@ -1025,11 +1071,18 @@ $wgUseCategoryBrowser = false;
* same options.
*
* This can provide a significant speedup for medium to large pages,
- * so you probably want to keep it on.
+ * so you probably want to keep it on. Extensions that conflict with the
+ * parser cache should disable the cache on a per-page basis instead.
*/
$wgEnableParserCache = true;
/**
+ * Append a configured value to the parser cache and the sitenotice key so
+ * that they can be kept separate for some class of activity.
+ */
+$wgRenderHashAppend = '';
+
+/**
* If on, the sidebar navigation links are cached for users with the
* current language set. This can save a touch of load on a busy site
* by shaving off extra message lookups.
@@ -1070,7 +1123,7 @@ $wgHitcounterUpdateFreq = 1;
$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
-$wgBlockAllowsUTEdit = false; # Blocks allow users to edit their own user talk page
+$wgBlockAllowsUTEdit = false; # Default setting for option on block form to allow self talkpage editing whilst blocked
$wgSysopEmailBans = true; # Allow sysops to ban users from accessing Emailuser
# Pages anonymous user may see as an array, e.g.:
@@ -1110,40 +1163,42 @@ $wgEmailConfirmToEdit=false;
$wgGroupPermissions = array();
// Implicit group for all visitors
-$wgGroupPermissions['*' ]['createaccount'] = true;
-$wgGroupPermissions['*' ]['read'] = true;
-$wgGroupPermissions['*' ]['edit'] = true;
-$wgGroupPermissions['*' ]['createpage'] = true;
-$wgGroupPermissions['*' ]['createtalk'] = true;
-$wgGroupPermissions['*' ]['writeapi'] = true;
+$wgGroupPermissions['*']['createaccount'] = true;
+$wgGroupPermissions['*']['read'] = true;
+$wgGroupPermissions['*']['edit'] = true;
+$wgGroupPermissions['*']['createpage'] = true;
+$wgGroupPermissions['*']['createtalk'] = true;
+$wgGroupPermissions['*']['writeapi'] = true;
// Implicit group for all logged-in accounts
-$wgGroupPermissions['user' ]['move'] = true;
-$wgGroupPermissions['user' ]['move-subpages'] = true;
-$wgGroupPermissions['user' ]['read'] = true;
-$wgGroupPermissions['user' ]['edit'] = true;
-$wgGroupPermissions['user' ]['createpage'] = true;
-$wgGroupPermissions['user' ]['createtalk'] = true;
-$wgGroupPermissions['user' ]['writeapi'] = true;
-$wgGroupPermissions['user' ]['upload'] = true;
-$wgGroupPermissions['user' ]['reupload'] = true;
-$wgGroupPermissions['user' ]['reupload-shared'] = true;
-$wgGroupPermissions['user' ]['minoredit'] = true;
-$wgGroupPermissions['user' ]['purge'] = true; // can use ?action=purge without clicking "ok"
+$wgGroupPermissions['user']['move'] = true;
+$wgGroupPermissions['user']['move-subpages'] = true;
+$wgGroupPermissions['user']['move-rootuserpages'] = true; // can move root userpages
+//$wgGroupPermissions['user']['movefile'] = true; // Disabled for now due to possible bugs and security concerns
+$wgGroupPermissions['user']['read'] = true;
+$wgGroupPermissions['user']['edit'] = true;
+$wgGroupPermissions['user']['createpage'] = true;
+$wgGroupPermissions['user']['createtalk'] = true;
+$wgGroupPermissions['user']['writeapi'] = true;
+$wgGroupPermissions['user']['upload'] = true;
+$wgGroupPermissions['user']['reupload'] = true;
+$wgGroupPermissions['user']['reupload-shared'] = true;
+$wgGroupPermissions['user']['minoredit'] = true;
+$wgGroupPermissions['user']['purge'] = true; // can use ?action=purge without clicking "ok"
// Implicit group for accounts that pass $wgAutoConfirmAge
$wgGroupPermissions['autoconfirmed']['autoconfirmed'] = true;
// Users with bot privilege can have their edits hidden
// from various log pages by default
-$wgGroupPermissions['bot' ]['bot'] = true;
-$wgGroupPermissions['bot' ]['autoconfirmed'] = true;
-$wgGroupPermissions['bot' ]['nominornewtalk'] = true;
-$wgGroupPermissions['bot' ]['autopatrol'] = true;
-$wgGroupPermissions['bot' ]['suppressredirect'] = true;
-$wgGroupPermissions['bot' ]['apihighlimits'] = true;
-$wgGroupPermissions['bot' ]['writeapi'] = true;
-#$wgGroupPermissions['bot' ]['editprotected'] = true; // can edit all protected pages without cascade protection enabled
+$wgGroupPermissions['bot']['bot'] = true;
+$wgGroupPermissions['bot']['autoconfirmed'] = true;
+$wgGroupPermissions['bot']['nominornewtalk'] = true;
+$wgGroupPermissions['bot']['autopatrol'] = true;
+$wgGroupPermissions['bot']['suppressredirect'] = true;
+$wgGroupPermissions['bot']['apihighlimits'] = true;
+$wgGroupPermissions['bot']['writeapi'] = true;
+#$wgGroupPermissions['bot']['editprotected'] = true; // can edit all protected pages without cascade protection enabled
// Most extra permission abilities go to this group
$wgGroupPermissions['sysop']['block'] = true;
@@ -1158,6 +1213,7 @@ $wgGroupPermissions['sysop']['import'] = true;
$wgGroupPermissions['sysop']['importupload'] = true;
$wgGroupPermissions['sysop']['move'] = true;
$wgGroupPermissions['sysop']['move-subpages'] = true;
+$wgGroupPermissions['sysop']['move-rootuserpages'] = true;
$wgGroupPermissions['sysop']['patrol'] = true;
$wgGroupPermissions['sysop']['autopatrol'] = true;
$wgGroupPermissions['sysop']['protect'] = true;
@@ -1173,10 +1229,10 @@ $wgGroupPermissions['sysop']['upload_by_url'] = true;
$wgGroupPermissions['sysop']['ipblock-exempt'] = true;
$wgGroupPermissions['sysop']['blockemail'] = true;
$wgGroupPermissions['sysop']['markbotedits'] = true;
-$wgGroupPermissions['sysop']['suppressredirect'] = true;
$wgGroupPermissions['sysop']['apihighlimits'] = true;
$wgGroupPermissions['sysop']['browsearchive'] = true;
$wgGroupPermissions['sysop']['noratelimit'] = true;
+$wgGroupPermissions['sysop']['movefile'] = true;
#$wgGroupPermissions['sysop']['mergehistory'] = true;
// Permission to change users' group assignments
@@ -1208,8 +1264,22 @@ $wgGroupPermissions['bureaucrat']['noratelimit'] = true;
$wgImplicitGroups = array( '*', 'user', 'autoconfirmed' );
/**
- * These are the groups that users are allowed to add to or remove from
- * their own account via Special:Userrights.
+ * A map of group names that the user is in, to group names that those users
+ * are allowed to add or revoke.
+ *
+ * Setting the list of groups to add or revoke to true is equivalent to "any group".
+ *
+ * For example, to allow sysops to add themselves to the "bot" group:
+ *
+ * $wgGroupsAddToSelf = array( 'sysop' => array( 'bot' ) );
+ *
+ * Implicit groups may be used for the source group, for instance:
+ *
+ * $wgGroupsRemoveFromSelf = array( '*' => true );
+ *
+ * This allows users in the '*' group (i.e. any user) to remove themselves from
+ * any group that they happen to be in.
+ *
*/
$wgGroupsAddToSelf = array();
$wgGroupsRemoveFromSelf = array();
@@ -1237,9 +1307,10 @@ $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' );
* Set the minimum permissions required to edit pages in each
* namespace. If you list more than one permission, a user must
* have all of them to edit pages in that namespace.
+ *
+ * Note: NS_MEDIAWIKI is implicitly restricted to editinterface.
*/
$wgNamespaceProtection = array();
-$wgNamespaceProtection[ NS_MEDIAWIKI ] = array( 'editinterface' );
/**
* Pages in namespaces in this array can not be used as templates.
@@ -1303,8 +1374,8 @@ $wgAutopromote = array(
* // Sysops can disable other sysops in an emergency, and disable bots
* $wgRemoveGroups['sysop'] = array( 'sysop', 'bot' );
*/
-$wgAddGroups = $wgRemoveGroups = array();
-
+$wgAddGroups = array();
+$wgRemoveGroups = array();
/**
* A list of available rights, in addition to the ones defined by the core.
@@ -1375,7 +1446,7 @@ $wgCacheEpoch = '20030516000000';
* to ensure that client-side caches don't keep obsolete copies of global
* styles.
*/
-$wgStyleVersion = '164';
+$wgStyleVersion = '195';
# Server-side caching:
@@ -1439,6 +1510,9 @@ $wgEnotifMaxRecips = 500;
# Send mails via the job queue.
$wgEnotifUseJobQ = false;
+# Use real name instead of username in e-mail "from" field
+$wgEnotifUseRealName = false;
+
/**
* Array of usernames who will be sent a notification email for every change which occurs on a wiki
*/
@@ -1456,14 +1530,17 @@ $wgRCShowChangedSize = true;
* before and after the edit is below that value, the value will be
* highlighted on the RC page.
*/
-$wgRCChangedSizeThreshold = -500;
+$wgRCChangedSizeThreshold = 500;
/**
* Show "Updated (since my last visit)" marker in RC view, watchlist and history
* view for watched pages with new changes */
$wgShowUpdatedMarker = true;
-$wgCookieExpiration = 2592000;
+/**
+ * Default cookie expiration time. Setting to 0 makes all cookies session-only.
+ */
+$wgCookieExpiration = 30*86400;
/** Clock skew or the one-second resolution of time() can occasionally cause cache
* problems when the user requests two pages within a short period of time. This
@@ -1523,6 +1600,9 @@ $wgHTCPMulticastTTL = 1;
# $wgHTCPMulticastAddress = "224.0.0.85";
$wgHTCPMulticastAddress = false;
+/** Should forwarded Private IPs be accepted? */
+$wgUsePrivateIPs = false;
+
# Cookie settings:
#
/**
@@ -1572,14 +1652,26 @@ $wgAllowExternalImages = false;
/** If the above is false, you can specify an exception here. Image URLs
* that start with this string are then rendered, while all others are not.
* You can use this to set up a trusted, simple repository of images.
+ * You may also specify an array of strings to allow multiple sites
*
- * Example:
+ * Examples:
* $wgAllowExternalImagesFrom = 'http://127.0.0.1/';
+ * $wgAllowExternalImagesFrom = array( 'http://127.0.0.1/', 'http://example.com' );
*/
$wgAllowExternalImagesFrom = '';
-/** Allows to move images and other media files. Experemintal, not sure if it always works */
-$wgAllowImageMoving = false;
+/** If $wgAllowExternalImages is false, you can allow an on-wiki
+ * whitelist of regular expression fragments to match the image URL
+ * against. If the image matches one of the regular expression fragments,
+ * The image will be displayed.
+ *
+ * Set this to true to enable the on-wiki whitelist (MediaWiki:External image whitelist)
+ * Or false to disable it
+ */
+$wgEnableImageWhitelist = true;
+
+/** Allows to move images and other media files */
+$wgAllowImageMoving = true;
/** Disable database-intensive features */
$wgMiserMode = false;
@@ -1598,6 +1690,7 @@ $wgAllowSlowParserFunctions = false;
*/
$wgJobClasses = array(
'refreshLinks' => 'RefreshLinksJob',
+ 'refreshLinks2' => 'RefreshLinksJob2',
'htmlCacheUpdate' => 'HTMLCacheUpdateJob',
'html_cache_update' => 'HTMLCacheUpdateJob', // backwards-compatible
'sendMail' => 'EmaillingJob',
@@ -1606,6 +1699,14 @@ $wgJobClasses = array(
);
/**
+ * Additional functions to be performed with updateSpecialPages.
+ * Expensive Querypages are already updated.
+ */
+$wgSpecialPageCacheUpdates = array(
+ 'Statistics' => array('SiteStatsUpdate','cacheUpdate')
+);
+
+/**
* 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.
@@ -1797,7 +1898,10 @@ $wgMimeTypeBlacklist= array(
# Client-side hazards on Internet Explorer
'text/scriptlet', 'application/x-msdownload',
# Windows metafile, client-side vulnerability on some systems
- 'application/x-msmetafile'
+ 'application/x-msmetafile',
+ # A ZIP file may be a valid Java archive containing an applet which exploits the
+ # same-origin policy to steal cookies
+ 'application/zip',
);
/** This is a flag to determine whether or not to check file extensions on upload. */
@@ -1823,7 +1927,7 @@ $wgNamespacesWithSubpages = array(
NS_USER => true,
NS_USER_TALK => true,
NS_PROJECT_TALK => true,
- NS_IMAGE_TALK => true,
+ NS_FILE_TALK => true,
NS_MEDIAWIKI_TALK => true,
NS_TEMPLATE_TALK => true,
NS_HELP_TALK => true,
@@ -1835,6 +1939,21 @@ $wgNamespacesToBeSearchedDefault = array(
);
/**
+ * Additional namespaces to those in $wgNamespacesToBeSearchedDefault that
+ * will be added to default search for "project" page inclusive searches
+ *
+ * Same format as $wgNamespacesToBeSearchedDefault
+ */
+$wgNamespacesToBeSearchedProject = array(
+ NS_USER => true,
+ NS_PROJECT => true,
+ NS_HELP => true,
+ NS_CATEGORY => true,
+);
+
+$wgUseOldSearchUI = true; // temp testing variable
+
+/**
* Site notice shown at the top of each page
*
* This message can contain wiki text, and can also be set through the
@@ -1883,6 +2002,12 @@ $wgSharpenParameter = '0x0.4';
/** Reduction in linear dimensions below which sharpening will be enabled */
$wgSharpenReductionThreshold = 0.85;
+/**
+ * Temporary directory used for ImageMagick. The directory must exist. Leave
+ * this set to false to let ImageMagick decide for itself.
+ */
+$wgImageMagickTempDir = false;
+
/**
* Use another resizing converter, e.g. GraphicMagick
* %s will be replaced with the source path, %d with the destination
@@ -1900,7 +2025,7 @@ $wgCustomConvertCommand = false;
#
# An external program is required to perform this conversion:
$wgSVGConverters = array(
- 'ImageMagick' => '$path/convert -background white -geometry $width $input PNG:$output',
+ 'ImageMagick' => '$path/convert -background white -thumbnail $widthx$height\! $input PNG:$output',
'sodipodi' => '$path/sodipodi -z -w $width -f $input -e $output',
'inkscape' => '$path/inkscape -z -w $width -f $input -e $output',
'batik' => 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input',
@@ -1920,6 +2045,13 @@ $wgSVGMaxSize = 2048;
*/
$wgMaxImageArea = 1.25e7;
/**
+ * Force thumbnailing of animated GIFs above this size to a single
+ * frame instead of an animated thumbnail. ImageMagick seems to
+ * get real unhappy and doesn't play well with resource limits. :P
+ * Defaulting to 1 megapixel (1000x1000)
+ */
+$wgMaxAnimatedGifArea = 1.0e6;
+/**
* If rendered thumbnail files are older than this timestamp, they
* will be rerendered on demand as if the file didn't already exist.
* Update if there is some need to force thumbs and SVG rasterizations
@@ -1988,17 +2120,54 @@ $wgRCFilterByAge = false;
$wgRCLinkLimits = array( 50, 100, 250, 500 );
$wgRCLinkDays = array( 1, 3, 7, 14, 30 );
-# Send RC updates via UDP
+/**
+ * Send recent changes updates via UDP. The updates will be formatted for IRC.
+ * Set this to the IP address of the receiver.
+ */
$wgRC2UDPAddress = false;
+
+/**
+ * Port number for RC updates
+ */
$wgRC2UDPPort = false;
+
+/**
+ * Prefix to prepend to each UDP packet.
+ * This can be used to identify the wiki. A script is available called
+ * mxircecho.py which listens on a UDP port, and uses a prefix ending in a
+ * tab to identify the IRC channel to send the log line to.
+ */
$wgRC2UDPPrefix = '';
+
+/**
+ * If this is set to true, $wgLocalInterwiki will be prepended to links in the
+ * IRC feed. If this is set to a string, that string will be used as the prefix.
+ */
+$wgRC2UDPInterwikiPrefix = false;
+
+/**
+ * Set to true to omit "bot" edits (by users with the bot permission) from the
+ * UDP feed.
+ */
$wgRC2UDPOmitBots = false;
-# Enable user search in Special:Newpages
-# This is really a temporary hack around an index install bug on some Wikipedias.
-# Kill it once fixed.
+/**
+ * Enable user search in Special:Newpages
+ * This is really a temporary hack around an index install bug on some Wikipedias.
+ * Kill it once fixed.
+ */
$wgEnableNewpagesUserFilter = true;
+/**
+ * Whether to use metadata edition
+ * This will put categories, language links and allowed templates in a separate text box
+ * while editing pages
+ * EXPERIMENTAL
+ */
+$wgUseMetadataEdit = false;
+/** Full name (including namespace) of the page containing templates names that will be allowed as metadata */
+$wgMetadataWhitelist = '';
+
#
# Copyright and credits settings
#
@@ -2084,9 +2253,17 @@ $wgExportMaxHistory = 0;
$wgExportAllowListContributors = false ;
-/** Text matching this regular expression will be recognised as spam
- * See http://en.wikipedia.org/wiki/Regular_expression */
-$wgSpamRegex = false;
+/**
+ * Edits matching these regular expressions in body text or edit summary
+ * will be recognised as spam and rejected automatically.
+ *
+ * There's no administrator override on-wiki, so be careful what you set. :)
+ * May be an array of regexes or a single string for backwards compatibility.
+ *
+ * See http://en.wikipedia.org/wiki/Regular_expression
+ */
+$wgSpamRegex = array();
+
/** Similarly you can get a function to do the job. The function will be given
* the following args:
* - a Title object for the article the edit is made on
@@ -2145,6 +2322,35 @@ $wgValidateAllHtml = false;
/** See list of skins and their symbolic names in languages/Language.php */
$wgDefaultSkin = 'monobook';
+/** Should we allow the user's to select their own skin that will override the default? */
+$wgAllowUserSkin = true;
+
+/**
+ * Optionally, we can specify a stylesheet to use for media="handheld".
+ * This is recognized by some, but not all, handheld/mobile/PDA browsers.
+ * If left empty, compliant handheld browsers won't pick up the skin
+ * stylesheet, which is specified for 'screen' media.
+ *
+ * Can be a complete URL, base-relative path, or $wgStylePath-relative path.
+ * Try 'chick/main.css' to apply the Chick styles to the MonoBook HTML.
+ *
+ * Will also be switched in when 'handheld=yes' is added to the URL, like
+ * the 'printable=yes' mode for print media.
+ */
+$wgHandheldStyle = false;
+
+/**
+ * If set, 'screen' and 'handheld' media specifiers for stylesheets are
+ * transformed such that they apply to the iPhone/iPod Touch Mobile Safari,
+ * which doesn't recognize 'handheld' but does support media queries on its
+ * screen size.
+ *
+ * Consider only using this if you have a *really good* handheld stylesheet,
+ * as iPhone users won't have any way to disable it and use the "grown-up"
+ * styles instead.
+ */
+$wgHandheldForIPhone = false;
+
/**
* Settings added to this array will override the default globals for the user
* preferences used by anonymous visitors and newly created accounts.
@@ -2161,7 +2367,6 @@ $wgDefaultUserOptions = array(
'contextlines' => 5,
'contextchars' => 50,
'disablesuggest' => 0,
- 'ajaxsearch' => 0,
'skin' => false,
'math' => 1,
'usenewrc' => 0,
@@ -2184,6 +2389,10 @@ $wgDefaultUserOptions = array(
'imagesize' => 2,
'thumbsize' => 2,
'rememberpassword' => 0,
+ 'nocache' => 0,
+ 'diffonly' => 0,
+ 'showhiddencats' => 0,
+ 'norollbackdiff' => 0,
'enotifwatchlistpages' => 0,
'enotifusertalkpages' => 1,
'enotifminoredits' => 0,
@@ -2192,7 +2401,9 @@ $wgDefaultUserOptions = array(
'fancysig' => 0,
'externaleditor' => 0,
'externaldiff' => 0,
+ 'forceeditsummary' => 0,
'showjumplinks' => 1,
+ 'justify' => 0,
'numberheadings' => 0,
'uselivepreview' => 0,
'watchlistdays' => 3.0,
@@ -2200,10 +2411,13 @@ $wgDefaultUserOptions = array(
'watchlisthideminor' => 0,
'watchlisthidebots' => 0,
'watchlisthideown' => 0,
+ 'watchlisthideanons' => 0,
+ 'watchlisthideliu' => 0,
'watchcreations' => 0,
'watchdefault' => 0,
'watchmoves' => 0,
'watchdeletion' => 0,
+ 'noconvertlink' => 0,
);
/** Whether or not to allow and use real name fields. Defaults to true. */
@@ -2290,7 +2504,7 @@ $wgAutoloadClasses = array();
* $wgExtensionCredits[$type][] = array(
* 'name' => 'Example extension',
* 'version' => 1.9,
- * 'svn-revision' => '$LastChangedRevision: 46957 $',
+ * 'svn-revision' => '$LastChangedRevision: 47653 $',
* 'author' => 'Foo Barstein',
* 'url' => 'http://wwww.example.com/Example%20Extension/',
* 'description' => 'An example extension',
@@ -2337,6 +2551,9 @@ $wgMaxTocLevel = 999;
/** Name of the external diff engine to use */
$wgExternalDiffEngine = false;
+/** Whether to use inline diff */
+$wgEnableHtmlDiff = false;
+
/** Use RC Patrolling to check for vandalism */
$wgUseRCPatrol = true;
@@ -2363,6 +2580,13 @@ $wgFeedCacheTimeout = 60;
* pages larger than this size. */
$wgFeedDiffCutoff = 32768;
+/** Override the site's default RSS/ATOM feed for recentchanges that appears on
+ * every page. Some sites might have a different feed they'd like to promote
+ * instead of the RC feed (maybe like a "Recent New Articles" or "Breaking news" one).
+ * Ex: $wgSiteFeed['format'] = "http://example.com/somefeed.xml"; Format can be one
+ * of either 'rss' or 'atom'.
+ */
+$wgOverrideSiteFeed = array();
/**
* Additional namespaces. If the namespaces defined in Language.php and
@@ -2448,6 +2672,12 @@ $wgCategoryMagicGallery = true;
$wgCategoryPagingLimit = 200;
/**
+ * Should the default category sortkey be the prefixed title?
+ * Run maintenance/refreshLinks.php after changing this.
+ */
+$wgCategoryPrefixedDefaultSortkey = true;
+
+/**
* Browser Blacklist for unicode non compliant browsers
* Contains a list of regexps : "/regexp/" matching problematic browsers
*/
@@ -2587,6 +2817,30 @@ $wgLogRestrictions = array(
);
/**
+ * Show/hide links on Special:Log will be shown for these log types.
+ *
+ * This is associative array of log type => boolean "hide by default"
+ *
+ * See $wgLogTypes for a list of available log types.
+ *
+ * For example:
+ * $wgFilterLogTypes => array(
+ * 'move' => true,
+ * 'import' => false,
+ * );
+ *
+ * Will display show/hide links for the move and import logs. Move logs will be
+ * hidden by default unless the link is clicked. Import logs will be shown by
+ * default, and hidden when the link is clicked.
+ *
+ * A message of the form log-show-hide-<type> should be added, and will be used
+ * for the link text.
+ */
+$wgFilterLogTypes = array(
+ 'patrol' => true
+);
+
+/**
* Lists the message key string for each log type. The localized messages
* will be listed in the user interface.
*
@@ -2635,9 +2889,11 @@ $wgLogHeaders = array(
$wgLogActions = array(
'block/block' => 'blocklogentry',
'block/unblock' => 'unblocklogentry',
+ 'block/reblock' => 'reblock-logentry',
'protect/protect' => 'protectedarticle',
'protect/modify' => 'modifiedarticleprotection',
'protect/unprotect' => 'unprotectedarticle',
+ 'protect/move_prot' => 'movedarticleprotection',
'rights/rights' => 'rightslogentry',
'delete/delete' => 'deletedarticle',
'delete/restore' => 'undeletedarticle',
@@ -2656,6 +2912,7 @@ $wgLogActions = array(
'suppress/event' => 'logdelete-logentry',
'suppress/delete' => 'suppressedarticle',
'suppress/block' => 'blocklogentry',
+ 'suppress/reblock' => 'reblock-logentry',
);
/**
@@ -2665,6 +2922,11 @@ $wgLogActions = array(
$wgLogActionsHandlers = array();
/**
+ * Maintain a log of newusers at Log/newusers?
+ */
+$wgNewUserLog = true;
+
+/**
* List of special pages, followed by what subtitle they should go under
* at Special:SpecialPages
*/
@@ -2688,6 +2950,8 @@ $wgSpecialPageGroups = array(
'Deadendpages' => 'maintenance',
'Wantedpages' => 'maintenance',
'Wantedcategories' => 'maintenance',
+ 'Wantedfiles' => 'maintenance',
+ 'Wantedtemplates' => 'maintenance',
'Unwatchedpages' => 'maintenance',
'Fewestrevisions' => 'maintenance',
@@ -2703,7 +2967,7 @@ $wgSpecialPageGroups = array(
'Log' => 'changes',
'Upload' => 'media',
- 'Imagelist' => 'media',
+ 'Listfiles' => 'media',
'MIMEsearch' => 'media',
'FileDuplicateSearch' => 'media',
'Filepath' => 'media',
@@ -2719,6 +2983,7 @@ $wgSpecialPageGroups = array(
'Blockip' => 'users',
'Preferences' => 'users',
'Resetpass' => 'users',
+ 'DeletedContributions' => 'users',
'Mostlinked' => 'highuse',
'Mostlinkedcategories' => 'highuse',
@@ -2739,6 +3004,7 @@ $wgSpecialPageGroups = array(
'Mytalk' => 'redirects',
'Mycontributions' => 'redirects',
'Search' => 'redirects',
+ 'LinkSearch' => 'redirects',
'Movepage' => 'pagetools',
'MergeHistory' => 'pagetools',
@@ -2788,6 +3054,11 @@ $wgDisableInternalSearch = false;
$wgSearchForwardUrl = null;
/**
+ * Set a default target for external links, e.g. _blank to pop up a new window
+ */
+$wgExternalLinkTarget = false;
+
+/**
* If true, external URL links in wiki text will be given the
* rel="nofollow" attribute as a hint to search engines that
* they should not be followed for ranking purposes as they
@@ -2802,34 +3073,57 @@ $wgNoFollowLinks = true;
$wgNoFollowNsExceptions = array();
/**
- * Default robot policy.
- * The default policy is to encourage indexing and following of links.
- * It may be overridden on a per-namespace and/or per-page basis.
+ * Default robot policy. The default policy is to encourage indexing and fol-
+ * lowing of links. It may be overridden on a per-namespace and/or per-page
+ * basis.
*/
$wgDefaultRobotPolicy = 'index,follow';
/**
- * Robot policies per namespaces.
- * The default policy is given above, the array is made of namespace
- * constants as defined in includes/Defines.php
+ * Robot policies per namespaces. The default policy is given above, the array
+ * is made of namespace constants as defined in includes/Defines.php. You can-
+ * not specify a different default policy for NS_SPECIAL: it is always noindex,
+ * nofollow. This is because a number of special pages (e.g., ListPages) have
+ * many permutations of options that display the same data under redundant
+ * URLs, so search engine spiders risk getting lost in a maze of twisty special
+ * pages, all alike, and never reaching your actual content.
+ *
* Example:
* $wgNamespaceRobotPolicies = array( NS_TALK => 'noindex' );
*/
$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.
+ * 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 canonical-
+ * ised text form title and the value is a robot policy.
* Example:
- * $wgArticleRobotPolicies = array( 'Main Page' => 'noindex' );
+ * $wgArticleRobotPolicies = array( 'Main Page' => 'noindex,follow',
+ * 'User:Bob' => 'index,follow' );
+ * Example that DOES NOT WORK because the names are not canonical text forms:
+ * $wgArticleRobotPolicies = array(
+ * # Underscore, not space!
+ * 'Main_Page' => 'noindex,follow',
+ * # "Project", not the actual project name!
+ * 'Project:X' => 'index,follow',
+ * # Needs to be "Abc", not "abc" (unless $wgCapitalLinks is false)!
+ * 'abc' => 'noindex,nofollow'
+ * );
*/
$wgArticleRobotPolicies = array();
/**
- * Specifies the minimal length of a user password. If set to
- * 0, empty passwords are allowed.
+ * An array of namespace keys in which the __INDEX__/__NOINDEX__ magic words
+ * will not function, so users can't decide whether pages in that namespace are
+ * indexed by search engines. If set to null, default to $wgContentNamespaces.
+ * Example:
+ * $wgExemptFromUserRobotsControl = array( NS_MAIN, NS_TALK, NS_PROJECT );
+ */
+$wgExemptFromUserRobotsControl = null;
+
+/**
+ * Specifies the minimal length of a user password. If set to 0, empty pass-
+ * words are allowed.
*/
$wgMinimalPasswordLength = 0;
@@ -2844,9 +3138,8 @@ $wgUseExternalEditor = true;
$wgSortSpecialPages = true;
/**
- * Specify the name of a skin that should not be presented in the
- * list of available skins.
- * Use for blacklisting a skin which you do not want to remove
+ * Specify the name of a skin that should not be presented in the list of a-
+ * vailable skins. Use for blacklisting a skin which you do not want to remove
* from the .../skins/ directory
*/
$wgSkipSkin = '';
@@ -2858,7 +3151,8 @@ $wgSkipSkins = array(); # More of the same
$wgDisabledActions = array();
/**
- * Disable redirects to special pages and interwiki redirects, which use a 302 and have no "redirected from" link
+ * Disable redirects to special pages and interwiki redirects, which use a 302
+ * and have no "redirected from" link.
*/
$wgDisableHardRedirects = false;
@@ -2869,21 +3163,19 @@ $wgEnableSorbs = false;
$wgSorbsUrl = 'http.dnsbl.sorbs.net.';
/**
- * Proxy whitelist, list of addresses that are assumed to be non-proxy despite what the other
- * methods might say
+ * Proxy whitelist, list of addresses that are assumed to be non-proxy despite
+ * what the other methods might say.
*/
$wgProxyWhitelist = array();
/**
- * Simple rate limiter options to brake edit floods.
- * Maximum number actions allowed in the given number of seconds;
- * after that the violating client receives HTTP 500 error pages
- * until the period elapses.
+ * Simple rate limiter options to brake edit floods. Maximum number actions
+ * allowed in the given number of seconds; after that the violating client re-
+ * ceives HTTP 500 error pages until the period elapses.
*
* array( 4, 60 ) for a maximum of 4 hits in 60 seconds.
*
- * This option set is experimental and likely to change.
- * Requires memcached.
+ * This option set is experimental and likely to change. Requires memcached.
*/
$wgRateLimits = array(
'edit' => array(
@@ -3045,17 +3337,10 @@ $wgUpdateRowsPerQuery = 10;
$wgUseAjax = true;
/**
- * Enable auto suggestion for the search bar
- * Requires $wgUseAjax to be true too.
- * Causes wfSajaxSearch to be added to $wgAjaxExportList
- */
-$wgAjaxSearch = false;
-
-/**
* List of Ajax-callable functions.
* Extensions acting as Ajax callbacks must register here
*/
-$wgAjaxExportList = array( );
+$wgAjaxExportList = array( 'wfAjaxGetThumbnailUrl', 'wfAjaxGetFileUrl' );
/**
* Enable watching/unwatching pages using AJAX.
@@ -3080,6 +3365,11 @@ $wgAjaxLicensePreview = true;
$wgAllowDisplayTitle = true;
/**
+ * for consistency, restrict DISPLAYTITLE to titles that normalize to the same canonical DB key
+ */
+$wgRestrictDisplayTitle = true;
+
+/**
* Array of usernames which may not be registered or logged in from
* Maintenance scripts can still use these
*/
@@ -3120,6 +3410,16 @@ $wgMaxShellMemory = 102400;
$wgMaxShellFileSize = 102400;
/**
+ * Maximum CPU time in seconds for shell processes under linux
+ */
+$wgMaxShellTime = 180;
+
+/**
+* Executable name of PHP cli client (php/php5)
+*/
+$wgPhpCli = 'php';
+
+/**
* DJVU settings
* Path of the djvudump executable
* Enable this and $wgDjvuRenderer to enable djvu rendering
@@ -3171,7 +3471,7 @@ $wgEnableAPI = true;
* (page edits, rollback, etc.) when an authorised user
* accesses it
*/
-$wgEnableWriteAPI = false;
+$wgEnableWriteAPI = true;
/**
* API module extensions
@@ -3241,8 +3541,6 @@ $wgSlaveLagCritical = 30;
* If this parameter is not given, it uses Preprocessor_DOM if the
* DOM module is available, otherwise it uses Preprocessor_Hash.
*
- * Has no effect on Parser_OldPP.
- *
* The entire associative array will be passed through to the constructor as
* the first parameter. Note that only Setup.php can use this variable --
* the configuration will change at runtime via $wgParser member functions, so
@@ -3256,6 +3554,12 @@ $wgParserConf = array(
);
/**
+ * LinkHolderArray batch size
+ * For debugging
+ */
+$wgLinkHolderBatchSize = 1000;
+
+/**
* Hooks that are used for outputting exceptions. Format is:
* $wgExceptionHooks[] = $funcname
* or:
@@ -3290,6 +3594,12 @@ $wgExpensiveParserFunctionLimit = 100;
$wgMaximumMovedPages = 100;
/**
+ * Fix double redirects after a page move.
+ * Tends to conflict with page move vandalism, use only on a private wiki.
+ */
+$wgFixDoubleRedirects = false;
+
+/**
* Array of namespaces to generate a sitemap for when the
* maintenance/generateSitemap.php script is run, or false if one is to be ge-
* nerated for all namespaces.
@@ -3303,3 +3613,28 @@ $wgSitemapNamespaces = false;
* ting this variable false.
*/
$wgUseAutomaticEditSummaries = true;
+
+/**
+ * Limit password attempts to X attempts per Y seconds per IP per account.
+ * Requires memcached.
+ */
+$wgPasswordAttemptThrottle = array( 'count' => 5, 'seconds' => 300 );
+
+/**
+ * Display user edit counts in various prominent places.
+ */
+$wgEdititis = false;
+
+/**
+* Enable the UniversalEditButton for browsers that support it
+* (currently only Firefox with an extension)
+* See http://universaleditbutton.org for more background information
+*/
+$wgUniversalEditButton = true;
+
+/**
+ * Allow id's that don't conform to HTML4 backward compatibility requirements.
+ * This is currently for testing; if all goes well, this option will be removed
+ * and the functionality will be enabled universally.
+ */
+$wgEnforceHtmlIds = true;
diff --git a/includes/Defines.php b/includes/Defines.php
index 98cee57d..8de6c5a1 100644
--- a/includes/Defines.php
+++ b/includes/Defines.php
@@ -52,8 +52,8 @@ define('NS_USER', 2);
define('NS_USER_TALK', 3);
define('NS_PROJECT', 4);
define('NS_PROJECT_TALK', 5);
-define('NS_IMAGE', 6);
-define('NS_IMAGE_TALK', 7);
+define('NS_FILE', 6);
+define('NS_FILE_TALK', 7);
define('NS_MEDIAWIKI', 8);
define('NS_MEDIAWIKI_TALK', 9);
define('NS_TEMPLATE', 10);
@@ -62,6 +62,16 @@ define('NS_HELP', 12);
define('NS_HELP_TALK', 13);
define('NS_CATEGORY', 14);
define('NS_CATEGORY_TALK', 15);
+/**
+ * NS_IMAGE and NS_IMAGE_TALK are the pre-v1.14 names for NS_FILE and
+ * NS_FILE_TALK respectively, and are kept for compatibility.
+ *
+ * When writing code that should be compatible with older MediaWiki
+ * versions, either stick to the old names or define the new constants
+ * yourself, if they're not defined already.
+ */
+define('NS_IMAGE', NS_FILE);
+define('NS_IMAGE_TALK', NS_FILE_TALK);
/**#@-*/
/**
@@ -202,6 +212,9 @@ define( 'OT_MSG' , 3 ); // b/c alias for OT_PREPROCESS
define( 'SFH_NO_HASH', 1 );
define( 'SFH_OBJECT_ARGS', 2 );
+# Flags for Parser::setLinkHook
+define( 'SLH_PATTERN', 1 );
+
# Flags for Parser::replaceLinkHolders
define( 'RLH_FOR_UPDATE', 1 );
@@ -211,3 +224,6 @@ define( 'APCOND_EDITCOUNT', 1 );
define( 'APCOND_AGE', 2 );
define( 'APCOND_EMAILCONFIRMED', 3 );
define( 'APCOND_INGROUPS', 4 );
+define( 'APCOND_ISIP', 5 );
+define( 'APCOND_IPINRANGE', 6 );
+define( 'APCOND_AGE_FROM_EDIT', 7 );
diff --git a/includes/EditPage.php b/includes/EditPage.php
index a34964bc..0193dc38 100644
--- a/includes/EditPage.php
+++ b/includes/EditPage.php
@@ -44,6 +44,7 @@ class EditPage {
var $mArticle;
var $mTitle;
+ var $action;
var $mMetaData = '';
var $isConflict = false;
var $isCssJsSubpage = false;
@@ -61,7 +62,8 @@ class EditPage {
var $allowBlankSummary = false;
var $autoSumm = '';
var $hookError = '';
- var $mPreviewTemplates;
+ #var $mPreviewTemplates;
+ var $mParserOutput;
var $mBaseRevision = false;
# Form values
@@ -92,6 +94,7 @@ class EditPage {
function EditPage( $article ) {
$this->mArticle =& $article;
$this->mTitle = $article->getTitle();
+ $this->action = 'submit';
# Placeholders for text injection by hooks (empty per default)
$this->editFormPageTop =
@@ -101,49 +104,47 @@ class EditPage {
$this->editFormTextAfterTools =
$this->editFormTextBottom = "";
}
+
+ function getArticle() {
+ return $this->mArticle;
+ }
/**
* Fetch initial editing page content.
* @private
*/
function getContent( $def_text = '' ) {
- global $wgOut, $wgRequest, $wgParser, $wgMessageCache;
+ global $wgOut, $wgRequest, $wgParser, $wgContLang, $wgMessageCache;
+ wfProfileIn( __METHOD__ );
# Get variables from query string :P
$section = $wgRequest->getVal( 'section' );
$preload = $wgRequest->getVal( 'preload' );
$undoafter = $wgRequest->getVal( 'undoafter' );
$undo = $wgRequest->getVal( 'undo' );
- wfProfileIn( __METHOD__ );
-
$text = '';
- if( !$this->mTitle->exists() ) {
+ // For message page not locally set, use the i18n message.
+ // For other non-existent articles, use preload text if any.
+ if ( !$this->mTitle->exists() ) {
if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
- $wgMessageCache->loadAllMessages();
# If this is a system message, get the default text.
- $text = wfMsgWeirdKey ( $this->mTitle->getText() ) ;
+ list( $message, $lang ) = $wgMessageCache->figureMessage( $wgContLang->lcfirst( $this->mTitle->getText() ) );
+ $wgMessageCache->loadAllMessages( $lang );
+ $text = wfMsgGetKey( $message, false, $lang, false );
+ if( wfEmptyMsg( $message, $text ) )
+ $text = '';
} else {
# If requested, preload some text.
$text = $this->getPreloadedText( $preload );
}
- # We used to put MediaWiki:Newarticletext here if
- # $text was empty at this point.
- # This is now shown above the edit box instead.
+ // For existing pages, get text based on "undo" or section parameters.
} else {
- // FIXME: may be better to use Revision class directly
- // But don't mess with it just yet. Article knows how to
- // fetch the page record from the high-priority server,
- // which is needed to guarantee we don't pick up lagged
- // information.
-
$text = $this->mArticle->getContent();
-
- if ($undo > 0 && $undoafter > 0 && $undo < $undoafter) {
+ 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.
@@ -158,7 +159,7 @@ class EditPage {
# Sanity check, make sure it's the right page,
# the revisions exist and they were not deleted.
# Otherwise, $text will be left as-is.
- if( !is_null( $undorev ) && !is_null( $oldrev ) &&
+ if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
$undorev->getPage() == $oldrev->getPage() &&
$undorev->getPage() == $this->mArticle->getID() &&
!$undorev->isDeleted( Revision::DELETED_TEXT ) &&
@@ -174,12 +175,12 @@ class EditPage {
$text = $oldrev_text;
$result = true;
}
- if( $result ) {
+ if ( $result ) {
# Inform the user of our success and set an automatic edit summary
$this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-success' ) );
$firstrev = $oldrev->getNext();
# If we just undid one rev, use an autosummary
- if( $firstrev->mId == $undo ) {
+ if ( $firstrev->mId == $undo ) {
$this->summary = wfMsgForContent('undo-summary', $undo, $undorev->getUserText());
}
$this->formtype = 'diff';
@@ -193,8 +194,8 @@ class EditPage {
// was created, or we may simply have got bogus input.
$this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-norev' ) );
}
- } else if( $section != '' ) {
- if( $section == 'new' ) {
+ } else if ( $section != '' ) {
+ if ( $section == 'new' ) {
$text = $this->getPreloadedText( $preload );
} else {
$text = $wgParser->getSection( $text, $section, $def_text );
@@ -212,13 +213,13 @@ class EditPage {
* @param $preload String: the title of the page.
* @return string The contents of the page.
*/
- protected function getPreloadedText($preload) {
- if ( $preload === '' )
+ protected function getPreloadedText( $preload ) {
+ if ( $preload === '' ) {
return '';
- else {
+ } else {
$preloadTitle = Title::newFromText( $preload );
if ( isset( $preloadTitle ) && $preloadTitle->userCanRead() ) {
- $rev=Revision::newFromTitle($preloadTitle);
+ $rev = Revision::newFromTitle($preloadTitle);
if ( is_object( $rev ) ) {
$text = $rev->getText();
// TODO FIXME: AAAAAAAAAAA, this shouldn't be implementing
@@ -237,107 +238,103 @@ class EditPage {
* and set $wgMetadataWhitelist to the *full* title of the template whitelist
*/
function extractMetaDataFromArticle () {
- global $wgUseMetadataEdit , $wgMetadataWhitelist , $wgLang ;
- $this->mMetaData = '' ;
- if ( !$wgUseMetadataEdit ) return ;
- if ( $wgMetadataWhitelist == '' ) return ;
- $s = '' ;
+ global $wgUseMetadataEdit, $wgMetadataWhitelist, $wgContLang;
+ $this->mMetaData = '';
+ if ( !$wgUseMetadataEdit ) return;
+ if ( $wgMetadataWhitelist == '' ) return;
+ $s = '';
$t = $this->getContent();
# MISSING : <nowiki> filtering
# Categories and language links
- $t = explode ( "\n" , $t ) ;
- $catlow = strtolower ( $wgLang->getNsText ( NS_CATEGORY ) ) ;
- $cat = $ll = array() ;
- foreach ( $t AS $key => $x )
- {
- $y = trim ( strtolower ( $x ) ) ;
- while ( substr ( $y , 0 , 2 ) == '[[' )
- {
- $y = explode ( ']]' , trim ( $x ) ) ;
- $first = array_shift ( $y ) ;
- $first = explode ( ':' , $first ) ;
- $ns = array_shift ( $first ) ;
- $ns = trim ( str_replace ( '[' , '' , $ns ) ) ;
- if ( strlen ( $ns ) == 2 OR strtolower ( $ns ) == $catlow )
- {
- $add = '[[' . $ns . ':' . implode ( ':' , $first ) . ']]' ;
- if ( strtolower ( $ns ) == $catlow ) $cat[] = $add ;
- else $ll[] = $add ;
- $x = implode ( ']]' , $y ) ;
- $t[$key] = $x ;
- $y = trim ( strtolower ( $x ) ) ;
+ $t = explode ( "\n" , $t );
+ $catlow = strtolower ( $wgContLang->getNsText( NS_CATEGORY ) );
+ $cat = $ll = array();
+ foreach ( $t AS $key => $x ) {
+ $y = trim ( strtolower ( $x ) );
+ while ( substr ( $y , 0 , 2 ) == '[[' ) {
+ $y = explode ( ']]' , trim ( $x ) );
+ $first = array_shift ( $y );
+ $first = explode ( ':' , $first );
+ $ns = array_shift ( $first );
+ $ns = trim ( str_replace ( '[' , '' , $ns ) );
+ if ( $wgContLang->getLanguageName( $ns ) || strtolower ( $ns ) == $catlow ) {
+ $add = '[[' . $ns . ':' . implode ( ':' , $first ) . ']]';
+ if ( strtolower ( $ns ) == $catlow ) $cat[] = $add;
+ else $ll[] = $add;
+ $x = implode ( ']]' , $y );
+ $t[$key] = $x;
+ $y = trim ( strtolower ( $x ) );
+ } else {
+ $x = implode ( ']]' , $y );
+ $y = trim ( strtolower ( $x ) );
}
}
}
- if ( count ( $cat ) ) $s .= implode ( ' ' , $cat ) . "\n" ;
- if ( count ( $ll ) ) $s .= implode ( ' ' , $ll ) . "\n" ;
- $t = implode ( "\n" , $t ) ;
+ if ( count ( $cat ) ) $s .= implode ( ' ' , $cat ) . "\n";
+ if ( count ( $ll ) ) $s .= implode ( ' ' , $ll ) . "\n";
+ $t = implode ( "\n" , $t );
# Load whitelist
$sat = array () ; # stand-alone-templates; must be lowercase
- $wl_title = Title::newFromText ( $wgMetadataWhitelist ) ;
- $wl_article = new Article ( $wl_title ) ;
- $wl = explode ( "\n" , $wl_article->getContent() ) ;
- foreach ( $wl AS $x )
- {
- $isentry = false ;
- $x = trim ( $x ) ;
- while ( substr ( $x , 0 , 1 ) == '*' )
- {
- $isentry = true ;
- $x = trim ( substr ( $x , 1 ) ) ;
+ $wl_title = Title::newFromText ( $wgMetadataWhitelist );
+ $wl_article = new Article ( $wl_title );
+ $wl = explode ( "\n" , $wl_article->getContent() );
+ foreach ( $wl AS $x ) {
+ $isentry = false;
+ $x = trim ( $x );
+ while ( substr ( $x , 0 , 1 ) == '*' ) {
+ $isentry = true;
+ $x = trim ( substr ( $x , 1 ) );
}
- if ( $isentry )
- {
- $sat[] = strtolower ( $x ) ;
+ if ( $isentry ) {
+ $sat[] = strtolower ( $x );
}
}
# Templates, but only some
- $t = explode ( '{{' , $t ) ;
+ $t = explode ( '{{' , $t );
$tl = array () ;
- foreach ( $t AS $key => $x )
- {
- $y = explode ( '}}' , $x , 2 ) ;
- if ( count ( $y ) == 2 )
- {
- $z = $y[0] ;
- $z = explode ( '|' , $z ) ;
- $tn = array_shift ( $z ) ;
- if ( in_array ( strtolower ( $tn ) , $sat ) )
- {
- $tl[] = '{{' . $y[0] . '}}' ;
- $t[$key] = $y[1] ;
- $y = explode ( '}}' , $y[1] , 2 ) ;
+ foreach ( $t AS $key => $x ) {
+ $y = explode ( '}}' , $x , 2 );
+ if ( count ( $y ) == 2 ) {
+ $z = $y[0];
+ $z = explode ( '|' , $z );
+ $tn = array_shift ( $z );
+ if ( in_array ( strtolower ( $tn ) , $sat ) ) {
+ $tl[] = '{{' . $y[0] . '}}';
+ $t[$key] = $y[1];
+ $y = explode ( '}}' , $y[1] , 2 );
}
- else $t[$key] = '{{' . $x ;
+ else $t[$key] = '{{' . $x;
}
- else if ( $key != 0 ) $t[$key] = '{{' . $x ;
- else $t[$key] = $x ;
+ else if ( $key != 0 ) $t[$key] = '{{' . $x;
+ else $t[$key] = $x;
}
- if ( count ( $tl ) ) $s .= implode ( ' ' , $tl ) ;
- $t = implode ( '' , $t ) ;
+ if ( count ( $tl ) ) $s .= implode ( ' ' , $tl );
+ $t = implode ( '' , $t );
- $t = str_replace ( "\n\n\n" , "\n" , $t ) ;
- $this->mArticle->mContent = $t ;
- $this->mMetaData = $s ;
+ $t = str_replace ( "\n\n\n" , "\n" , $t );
+ $this->mArticle->mContent = $t;
+ $this->mMetaData = $s;
}
+ /*
+ * Check if a page was deleted while the user was editing it, before submit.
+ * Note that we rely on the logging table, which hasn't been always there,
+ * but that doesn't matter, because this only applies to brand new
+ * deletes.
+ */
protected function wasDeletedSinceLastEdit() {
- /* Note that we rely on the logging table, which hasn't been always there,
- * but that doesn't matter, because this only applies to brand new
- * deletes.
- */
if ( $this->deletedSinceEdit )
return true;
if ( $this->mTitle->isDeleted() ) {
$this->lastDelete = $this->getLastDelete();
- if ( !is_null($this->lastDelete) ) {
- $deletetime = $this->lastDelete->log_timestamp;
- if ( ($deletetime - $this->starttime) > 0 ) {
+ if ( $this->lastDelete ) {
+ $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
+ if ( $deleteTime > $this->starttime ) {
$this->deletedSinceEdit = true;
}
}
@@ -362,61 +359,34 @@ class EditPage {
*/
function edit() {
global $wgOut, $wgUser, $wgRequest;
-
- if ( !wfRunHooks( 'AlternateEdit', array( &$this ) ) )
+ // Allow extensions to modify/prevent this form or submission
+ if ( !wfRunHooks( 'AlternateEdit', array( &$this ) ) ) {
return;
+ }
wfProfileIn( __METHOD__ );
wfDebug( __METHOD__.": enter\n" );
- // this is not an article
- $wgOut->setArticleFlag(false);
+ // This is not an article
+ $wgOut->setArticleFlag( false );
$this->importFormData( $wgRequest );
$this->firsttime = false;
- if( $this->live ) {
+ if ( $this->live ) {
$this->livePreview();
wfProfileOut( __METHOD__ );
return;
}
-
- $wgOut->addScriptFile( 'edit.js' );
-
- if( wfReadOnly() ) {
- $this->readOnlyPage( $this->getContent() );
- wfProfileOut( __METHOD__ );
- return;
- }
- $permErrors = $this->mTitle->getUserPermissionsErrors('edit', $wgUser);
-
- if( !$this->mTitle->exists() ) {
- $permErrors = array_merge( $permErrors,
- wfArrayDiff2( $this->mTitle->getUserPermissionsErrors('create', $wgUser), $permErrors ) );
+ if ( wfReadOnly() && $this->save ) {
+ // Force preview
+ $this->save = false;
+ $this->preview = true;
}
- # 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;
- }
- }
- }
- $permErrors = wfArrayDiff2( $permErrors, $remove );
-
+ $wgOut->addScriptFile( 'edit.js' );
+ $permErrors = $this->getEditPermissionErrors();
if ( $permErrors ) {
wfDebug( __METHOD__.": User can't edit\n" );
$this->readOnlyPage( $this->getContent(), true, $permErrors, 'edit' );
@@ -431,7 +401,7 @@ class EditPage {
$this->formtype = 'diff';
} else { # First time through
$this->firsttime = true;
- if( $this->previewOnOpen() ) {
+ if ( $this->previewOnOpen() ) {
$this->formtype = 'preview';
} else {
$this->extractMetaDataFromArticle () ;
@@ -448,13 +418,32 @@ class EditPage {
$this->isValidCssJsSubpage = $this->mTitle->isValidCssJsSubpage();
# Show applicable editing introductions
- if( $this->formtype == 'initial' || $this->firsttime )
+ if ( $this->formtype == 'initial' || $this->firsttime )
$this->showIntro();
- if( $this->mTitle->isTalkPage() ) {
+ if ( $this->mTitle->isTalkPage() ) {
$wgOut->addWikiMsg( 'talkpagetext' );
}
+ # Optional notices on a per-namespace and per-page basis
+ $editnotice_ns = 'editnotice-'.$this->mTitle->getNamespace();
+ $editnotice_page = $editnotice_ns.'-'.$this->mTitle->getDBkey();
+ if ( !wfEmptyMsg( $editnotice_ns, wfMsgForContent( $editnotice_ns ) ) ) {
+ $wgOut->addWikiText( wfMsgForContent( $editnotice_ns ) );
+ }
+ if ( MWNamespace::hasSubpages( $this->mTitle->getNamespace() ) ) {
+ $parts = explode( '/', $this->mTitle->getDBkey() );
+ $editnotice_base = $editnotice_ns;
+ while ( count( $parts ) > 0 ) {
+ $editnotice_base .= '-'.array_shift( $parts );
+ if ( !wfEmptyMsg( $editnotice_base, wfMsgForContent( $editnotice_base ) ) ) {
+ $wgOut->addWikiText( wfMsgForContent( $editnotice_base ) );
+ }
+ }
+ } else if ( !wfEmptyMsg( $editnotice_page, wfMsgForContent( $editnotice_page ) ) ) {
+ $wgOut->addWikiText( wfMsgForContent( $editnotice_page ) );
+ }
+
# Attempt submission here. This will check for edit conflicts,
# and redundantly check for locked database, blocked IPs, etc.
# that edit() already checked just in case someone tries to sneak
@@ -471,13 +460,13 @@ class EditPage {
# First time through: get contents, set time for conflict
# checking, etc.
if ( 'initial' == $this->formtype || $this->firsttime ) {
- if ($this->initialiseForm() === false) {
+ if ( $this->initialiseForm() === false) {
$this->noSuchSectionPage();
wfProfileOut( __METHOD__."-business-end" );
wfProfileOut( __METHOD__ );
return;
}
- if( !$this->mTitle->getArticleId() )
+ if ( !$this->mTitle->getArticleId() )
wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) );
}
@@ -485,6 +474,27 @@ class EditPage {
wfProfileOut( __METHOD__."-business-end" );
wfProfileOut( __METHOD__ );
}
+
+ protected function getEditPermissionErrors() {
+ global $wgUser;
+ $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser );
+ # Can this title be created?
+ if ( !$this->mTitle->exists() ) {
+ $permErrors = array_merge( $permErrors,
+ wfArrayDiff2( $this->mTitle->getUserPermissionsErrors( 'create', $wgUser ), $permErrors ) );
+ }
+ # Ignore some permissions errors when a user is just previewing/viewing diffs
+ $remove = array();
+ foreach( $permErrors as $error ) {
+ if ( ($this->preview || $this->diff) &&
+ ($error[0] == 'blockedtext' || $error[0] == 'autoblockedtext') )
+ {
+ $remove[] = $error;
+ }
+ }
+ $permErrors = wfArrayDiff2( $permErrors, $remove );
+ return $permErrors;
+ }
/**
* Show a read-only error
@@ -510,19 +520,19 @@ class EditPage {
*/
protected function previewOnOpen() {
global $wgRequest, $wgUser;
- if( $wgRequest->getVal( 'preview' ) == 'yes' ) {
+ if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
// Explicit override from request
return true;
- } elseif( $wgRequest->getVal( 'preview' ) == 'no' ) {
+ } elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) {
// Explicit override from request
return false;
- } elseif( $this->section == 'new' ) {
+ } elseif ( $this->section == 'new' ) {
// Nothing *to* preview for new sections
return false;
- } elseif( ( $wgRequest->getVal( 'preload' ) !== '' || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) {
+ } elseif ( ( $wgRequest->getVal( 'preload' ) !== '' || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) {
// Standard preference behaviour
return true;
- } elseif( !$this->mTitle->exists() && $this->mTitle->getNamespace() == NS_CATEGORY ) {
+ } elseif ( !$this->mTitle->exists() && $this->mTitle->getNamespace() == NS_CATEGORY ) {
// Categories are special
return true;
} else {
@@ -542,7 +552,7 @@ class EditPage {
# Section edit can come from either the form or a link
$this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
- if( $request->wasPosted() ) {
+ if ( $request->wasPosted() ) {
# These fields need to be checked for encoding.
# Also remove trailing whitespace, but don't remove _initial_
# whitespace from the text boxes. This may be significant formatting.
@@ -550,7 +560,7 @@ class EditPage {
$this->textbox2 = $this->safeUnicodeInput( $request, 'wpTextbox2' );
$this->mMetaData = rtrim( $request->getText( 'metadata' ) );
# Truncate for whole multibyte characters. +5 bytes for ellipsis
- $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 );
+ $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 );
# Remove extra headings from summaries and new sections.
$this->summary = preg_replace('/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary);
@@ -560,7 +570,7 @@ class EditPage {
$this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
- if( is_null( $this->edittime ) ) {
+ if ( is_null( $this->edittime ) ) {
# If the form is incomplete, force to preview.
wfDebug( "$fname: Form data appears to be incomplete\n" );
wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" );
@@ -591,11 +601,11 @@ class EditPage {
}
}
$this->save = !$this->preview && !$this->diff;
- if( !preg_match( '/^\d{14}$/', $this->edittime )) {
+ if ( !preg_match( '/^\d{14}$/', $this->edittime )) {
$this->edittime = null;
}
- if( !preg_match( '/^\d{14}$/', $this->starttime )) {
+ if ( !preg_match( '/^\d{14}$/', $this->starttime )) {
$this->starttime = null;
}
@@ -605,10 +615,12 @@ class EditPage {
$this->watchthis = $request->getCheck( 'wpWatchthis' );
# Don't force edit summaries when a user is editing their own user or talk page
- if( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) && $this->mTitle->getText() == $wgUser->getName() ) {
+ if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) &&
+ $this->mTitle->getText() == $wgUser->getName() )
+ {
$this->allowBlankSummary = true;
} else {
- $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' );
+ $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ) || !$wgUser->getOption( 'forceeditsummary');
}
$this->autoSumm = $request->getText( 'wpAutoSummary' );
@@ -662,28 +674,30 @@ class EditPage {
*/
protected function showIntro() {
global $wgOut, $wgUser;
- if( $this->suppressIntro )
+ if ( $this->suppressIntro ) {
return;
-
+ }
# Show a warning message when someone creates/edits a user (talk) page but the user does not exists
- if( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) {
+ if ( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) {
$parts = explode( '/', $this->mTitle->getText(), 2 );
$username = $parts[0];
$id = User::idFromName( $username );
$ip = User::isIP( $username );
-
if ( $id == 0 && !$ip ) {
$wgOut->wrapWikiMsg( '<div class="mw-userpage-userdoesnotexist error">$1</div>',
array( 'userpage-userdoesnotexist', $username ) );
}
}
-
- if( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
- if( $wgUser->isLoggedIn() ) {
+ # Try to add a custom edit intro, or use the standard one if this is not possible.
+ if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
+ if ( $wgUser->isLoggedIn() ) {
$wgOut->wrapWikiMsg( '<div class="mw-newarticletext">$1</div>', 'newarticletext' );
} else {
$wgOut->wrapWikiMsg( '<div class="mw-newarticletextanon">$1</div>', 'newarticletextanon' );
}
+ }
+ # Give a notice if the user is editing a deleted page...
+ if ( !$this->mTitle->exists() ) {
$this->showDeletionLog( $wgOut );
}
}
@@ -694,9 +708,9 @@ class EditPage {
* @return bool
*/
protected function showCustomIntro() {
- if( $this->editintro ) {
+ if ( $this->editintro ) {
$title = Title::newFromText( $this->editintro );
- if( $title instanceof Title && $title->exists() && $title->userCanRead() ) {
+ if ( $title instanceof Title && $title->exists() && $title->userCanRead() ) {
global $wgOut;
$revision = Revision::newFromTitle( $title );
$wgOut->addWikiTextTitleTidy( $revision->getText(), $this->mTitle );
@@ -714,24 +728,24 @@ class EditPage {
* @return one of the constants describing the result
*/
function internalAttemptSave( &$result, $bot = false ) {
- global $wgSpamRegex, $wgFilterCallback, $wgUser, $wgOut, $wgParser;
+ global $wgFilterCallback, $wgUser, $wgOut, $wgParser;
global $wgMaxArticleSize;
$fname = 'EditPage::attemptSave';
wfProfileIn( $fname );
wfProfileIn( "$fname-checks" );
- if( !wfRunHooks( 'EditPage::attemptSave', array( &$this ) ) )
+ if ( !wfRunHooks( 'EditPage::attemptSave', array( &$this ) ) )
{
wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" );
return self::AS_HOOK_ERROR;
}
# Check image redirect
- if ( $this->mTitle->getNamespace() == NS_IMAGE &&
+ if ( $this->mTitle->getNamespace() == NS_FILE &&
Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
!$wgUser->isAllowed( 'upload' ) ) {
- if( $wgUser->isAnon() ) {
+ if ( $wgUser->isAnon() ) {
return self::AS_IMAGE_REDIRECT_ANON;
} else {
return self::AS_IMAGE_REDIRECT_LOGGED;
@@ -743,12 +757,15 @@ class EditPage {
$this->mMetaData = '' ;
# Check for spam
- $matches = array();
- if ( $wgSpamRegex && preg_match( $wgSpamRegex, $this->textbox1, $matches ) ) {
- $result['spam'] = $matches[0];
+ $match = self::matchSpamRegex( $this->summary );
+ if ( $match === false ) {
+ $match = self::matchSpamRegex( $this->textbox1 );
+ }
+ if ( $match !== false ) {
+ $result['spam'] = $match;
$ip = wfGetIP();
$pdbk = $this->mTitle->getPrefixedDBkey();
- $match = str_replace( "\n", '', $matches[0] );
+ $match = str_replace( "\n", '', $match );
wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
wfProfileOut( "$fname-checks" );
wfProfileOut( $fname );
@@ -765,7 +782,7 @@ class EditPage {
wfProfileOut( "$fname-checks" );
wfProfileOut( $fname );
return self::AS_HOOK_ERROR;
- } elseif( $this->hookError != '' ) {
+ } elseif ( $this->hookError != '' ) {
# ...or the hook could be expecting us to produce an error
wfProfileOut( "$fname-checks" );
wfProfileOut( $fname );
@@ -823,7 +840,6 @@ class EditPage {
# If article is new, insert it.
$aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE );
if ( 0 == $aid ) {
-
// Late check for create permission, just in case *PARANOIA*
if ( !$this->mTitle->userCan( 'create' ) ) {
wfDebug( "$fname: no create permission\n" );
@@ -833,8 +849,8 @@ class EditPage {
# Don't save a new article if it's blank.
if ( '' == $this->textbox1 ) {
- wfProfileOut( $fname );
- return self::AS_BLANK_ARTICLE;
+ wfProfileOut( $fname );
+ return self::AS_BLANK_ARTICLE;
}
// Run post-section-merge edit filter
@@ -860,10 +876,10 @@ class EditPage {
wfDebug("timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n");
- if( $this->mArticle->getTimestamp() != $this->edittime ) {
+ if ( $this->mArticle->getTimestamp() != $this->edittime ) {
$this->isConflict = true;
- if( $this->section == 'new' ) {
- if( $this->mArticle->getUserText() == $wgUser->getName() &&
+ if ( $this->section == 'new' ) {
+ if ( $this->mArticle->getUserText() == $wgUser->getName() &&
$this->mArticle->getComment() == $this->summary ) {
// Probably a duplicate submission of a new comment.
// This can happen when squid resends a request after
@@ -878,7 +894,7 @@ class EditPage {
}
$userid = $wgUser->getId();
- if ( $this->isConflict) {
+ if ( $this->isConflict ) {
wfDebug( "EditPage::editForm conflict! getting section '$this->section' for time '$this->edittime' (article time '" .
$this->mArticle->getTimestamp() . "')\n" );
$text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime);
@@ -887,21 +903,21 @@ class EditPage {
wfDebug( "EditPage::editForm getting section '$this->section'\n" );
$text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary);
}
- if( is_null( $text ) ) {
+ if ( is_null( $text ) ) {
wfDebug( "EditPage::editForm activating conflict; section replace failed.\n" );
$this->isConflict = true;
$text = $this->textbox1;
}
# Suppress edit conflict with self, except for section edits where merging is required.
- if ( ( $this->section == '' ) && ( 0 != $userid ) && ( $this->mArticle->getUser() == $userid ) ) {
+ if ( $this->section == '' && $userid && $this->userWasLastToEdit($userid,$this->edittime) ) {
wfDebug( "EditPage::editForm Suppressing edit conflict, same user.\n" );
$this->isConflict = false;
} else {
# switch from section editing to normal editing in edit conflict
- if($this->isConflict) {
+ if ( $this->isConflict ) {
# Attempt merge
- if( $this->mergeChangesInto( $text ) ){
+ if ( $this->mergeChangesInto( $text ) ) {
// Successful merge! Maybe we should tell the user the good news?
$this->isConflict = false;
wfDebug( "EditPage::editForm Suppressing edit conflict, successful merge.\n" );
@@ -928,12 +944,10 @@ class EditPage {
}
# Handle the user preference to force summaries here, but not for null edits
- if( $this->section != 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary') &&
- 0 != strcmp($oldtext, $text) &&
+ if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp($oldtext, $text) &&
!is_object( Title::newFromRedirect( $text ) ) # check if it's not a redirect
) {
-
- if( md5( $this->summary ) == $this->autoSumm ) {
+ if ( md5( $this->summary ) == $this->autoSumm ) {
$this->missingSummary = true;
wfProfileOut( $fname );
return self::AS_SUMMARY_NEEDED;
@@ -941,7 +955,7 @@ class EditPage {
}
# And a similar thing for new sections
- if( $this->section == 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary' ) ) {
+ if ( $this->section == 'new' && !$this->allowBlankSummary ) {
if (trim($this->summary) == '') {
$this->missingSummary = true;
wfProfileOut( $fname );
@@ -952,26 +966,26 @@ class EditPage {
# All's well
wfProfileIn( "$fname-sectionanchor" );
$sectionanchor = '';
- if( $this->section == 'new' ) {
+ if ( $this->section == 'new' ) {
if ( $this->textbox1 == '' ) {
$this->missingComment = true;
return self::AS_TEXTBOX_EMPTY;
}
- if( $this->summary != '' ) {
+ if ( $this->summary != '' ) {
$sectionanchor = $wgParser->guessSectionNameFromWikiText( $this->summary );
# This is a new section, so create a link to the new section
# in the revision summary.
$cleanSummary = $wgParser->stripSectionName( $this->summary );
$this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
}
- } elseif( $this->section != '' ) {
+ } elseif ( $this->section != '' ) {
# Try to get a section anchor from the section source, redirect to edited section if header found
# XXX: might be better to integrate this into Article::replaceSection
# for duplicate heading checking and maybe parsing
$hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
# we can't deal with anchors, includes, html etc in the header for now,
# headline would need to be parsed to improve this
- if($hasmatch and strlen($matches[2]) > 0) {
+ if ( $hasmatch and strlen($matches[2]) > 0 ) {
$sectionanchor = $wgParser->guessSectionNameFromWikiText( $matches[2] );
}
}
@@ -993,7 +1007,7 @@ class EditPage {
}
# update the article here
- if( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit,
+ if ( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit,
$this->watchthis, $bot, $sectionanchor ) ) {
wfProfileOut( $fname );
return self::AS_SUCCESS_UPDATE;
@@ -1003,6 +1017,48 @@ class EditPage {
wfProfileOut( $fname );
return self::AS_END;
}
+
+ /**
+ * Check if no edits were made by other users since
+ * the time a user started editing the page. Limit to
+ * 50 revisions for the sake of performance.
+ */
+ protected function userWasLastToEdit( $id, $edittime ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $res = $dbw->select( 'revision',
+ 'rev_user',
+ array(
+ 'rev_page' => $this->mArticle->getId(),
+ 'rev_timestamp > '.$dbw->addQuotes( $dbw->timestamp($edittime) )
+ ),
+ __METHOD__,
+ array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) );
+ while( $row = $res->fetchObject() ) {
+ if( $row->rev_user != $id ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Check given input text against $wgSpamRegex, and return the text of the first match.
+ * @return mixed -- matching string or false
+ */
+ public static function matchSpamRegex( $text ) {
+ global $wgSpamRegex;
+ if ( $wgSpamRegex ) {
+ // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
+ $regexes = (array)$wgSpamRegex;
+ foreach( $regexes as $regex ) {
+ $matches = array();
+ if ( preg_match( $regex, $text, $matches ) ) {
+ return $matches[0];
+ }
+ }
+ }
+ return false;
+ }
/**
* Initialise form fields in the object
@@ -1010,15 +1066,35 @@ class EditPage {
*/
function initialiseForm() {
$this->edittime = $this->mArticle->getTimestamp();
- $this->textbox1 = $this->getContent(false);
- if ($this->textbox1 === false) return false;
-
- if ( !$this->mArticle->exists() && $this->mTitle->getNamespace() == NS_MEDIAWIKI )
- $this->textbox1 = wfMsgWeirdKey( $this->mTitle->getText() );
+ $this->textbox1 = $this->getContent( false );
+ if ( $this->textbox1 === false ) return false;
wfProxyCheck();
return true;
}
+ function setHeaders() {
+ global $wgOut, $wgTitle;
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ if ( $this->formtype == 'preview' ) {
+ $wgOut->setPageTitleActionText( wfMsg( 'preview' ) );
+ }
+ if ( $this->isConflict ) {
+ $wgOut->setPageTitle( wfMsg( 'editconflict', $wgTitle->getPrefixedText() ) );
+ } elseif ( $this->section != '' ) {
+ $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
+ $wgOut->setPageTitle( wfMsg( $msg, $wgTitle->getPrefixedText() ) );
+ } else {
+ # Use the title defined by DISPLAYTITLE magic word when present
+ if ( isset($this->mParserOutput)
+ && ( $dt = $this->mParserOutput->getDisplayTitle() ) !== false ) {
+ $title = $dt;
+ } else {
+ $title = $wgTitle->getPrefixedText();
+ }
+ $wgOut->setPageTitle( wfMsg( 'editing', $title ) );
+ }
+ }
+
/**
* Send the edit form and related headers to $wgOut
* @param $formCallback Optional callable that takes an OutputPage
@@ -1026,13 +1102,13 @@ class EditPage {
* near the top, for captchas and the like.
*/
function showEditForm( $formCallback=null ) {
- global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize, $wgTitle;
+ global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize, $wgTitle, $wgRequest;
# If $wgTitle is null, that means we're in API mode.
# Some hook probably called this function without checking
# for is_null($wgTitle) first. Bail out right here so we don't
# do lots of work just to discard it right after.
- if(is_null($wgTitle))
+ if (is_null($wgTitle))
return;
$fname = 'EditPage::showEditForm';
@@ -1042,60 +1118,55 @@ class EditPage {
wfRunHooks( 'EditPage::showEditForm:initial', array( &$this ) ) ;
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ #need to parse the preview early so that we know which templates are used,
+ #otherwise users with "show preview after edit box" will get a blank list
+ #we parse this near the beginning so that setHeaders can do the title
+ #setting work instead of leaving it in getPreviewText
+ $previewOutput = '';
+ if ( $this->formtype == 'preview' ) {
+ $previewOutput = $this->getPreviewText();
+ }
+
+ $this->setHeaders();
# Enabled article-related sidebar, toplinks, etc.
$wgOut->setArticleRelated( true );
- if ( $this->formtype == 'preview' ) {
- $wgOut->setPageTitleActionText( wfMsg( 'preview' ) );
- }
-
if ( $this->isConflict ) {
- $s = wfMsg( 'editconflict', $wgTitle->getPrefixedText() );
- $wgOut->setPageTitle( $s );
$wgOut->addWikiMsg( 'explainconflict' );
$this->textbox2 = $this->textbox1;
$this->textbox1 = $this->getContent();
$this->edittime = $this->mArticle->getTimestamp();
} else {
- if( $this->section != '' ) {
- if( $this->section == 'new' ) {
- $s = wfMsg('editingcomment', $wgTitle->getPrefixedText() );
- } else {
- $s = wfMsg('editingsection', $wgTitle->getPrefixedText() );
- $matches = array();
- if( !$this->summary && !$this->preview && !$this->diff ) {
- preg_match( "/^(=+)(.+)\\1/mi",
- $this->textbox1,
- $matches );
- if( !empty( $matches[2] ) ) {
- global $wgParser;
- $this->summary = "/* " .
- $wgParser->stripSectionName(trim($matches[2])) .
- " */ ";
- }
+ if ( $this->section != '' && $this->section != 'new' ) {
+ $matches = array();
+ if ( !$this->summary && !$this->preview && !$this->diff ) {
+ preg_match( "/^(=+)(.+)\\1/mi",
+ $this->textbox1,
+ $matches );
+ if ( !empty( $matches[2] ) ) {
+ global $wgParser;
+ $this->summary = "/* " .
+ $wgParser->stripSectionName(trim($matches[2])) .
+ " */ ";
}
}
- } else {
- $s = wfMsg( 'editing', $wgTitle->getPrefixedText() );
}
- $wgOut->setPageTitle( $s );
if ( $this->missingComment ) {
$wgOut->wrapWikiMsg( '<div id="mw-missingcommenttext">$1</div>', 'missingcommenttext' );
}
- if( $this->missingSummary && $this->section != 'new' ) {
+ if ( $this->missingSummary && $this->section != 'new' ) {
$wgOut->wrapWikiMsg( '<div id="mw-missingsummary">$1</div>', 'missingsummary' );
}
- if( $this->missingSummary && $this->section == 'new' ) {
+ if ( $this->missingSummary && $this->section == 'new' ) {
$wgOut->wrapWikiMsg( '<div id="mw-missingcommentheader">$1</div>', 'missingcommentheader' );
}
- if( $this->hookError !== '' ) {
+ if ( $this->hookError !== '' ) {
$wgOut->addWikiText( $this->hookError );
}
@@ -1105,46 +1176,54 @@ class EditPage {
if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) {
// Let sysop know that this will make private content public if saved
- if( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) {
+ if ( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) {
$wgOut->addWikiMsg( 'rev-deleted-text-permission' );
- } else if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
+ } else if ( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
$wgOut->addWikiMsg( 'rev-deleted-text-view' );
}
- if( !$this->mArticle->mRevision->isCurrent() ) {
+ if ( !$this->mArticle->mRevision->isCurrent() ) {
$this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() );
$wgOut->addWikiMsg( 'editingold' );
}
}
}
- if( wfReadOnly() ) {
- $wgOut->addHTML( '<div id="mw-read-only-warning">'.wfMsgWikiHTML( 'readonlywarning' ).'</div>' );
- } elseif( $wgUser->isAnon() && $this->formtype != 'preview' ) {
- $wgOut->addHTML( '<div id="mw-anon-edit-warning">'.wfMsgWikiHTML( 'anoneditwarning' ).'</div>' );
+ if ( wfReadOnly() ) {
+ $wgOut->wrapWikiMsg( "<div id=\"mw-read-only-warning\">\n$1\n</div>", array( 'readonlywarning', wfReadOnlyReason() ) );
+ } elseif ( $wgUser->isAnon() && $this->formtype != 'preview' ) {
+ $wgOut->wrapWikiMsg( '<div id="mw-anon-edit-warning">$1</div>', 'anoneditwarning' );
} else {
- if( $this->isCssJsSubpage && $this->formtype != 'preview' ) {
+ if ( $this->isCssJsSubpage ) {
# Check the skin exists
- if( $this->isValidCssJsSubpage ) {
- $wgOut->addWikiMsg( 'usercssjsyoucanpreview' );
+ if ( $this->isValidCssJsSubpage ) {
+ if ( $this->formtype !== 'preview' ) {
+ $wgOut->addWikiMsg( 'usercssjsyoucanpreview' );
+ }
} else {
$wgOut->addWikiMsg( 'userinvalidcssjstitle', $wgTitle->getSkinFromCssJsSubpage() );
}
}
}
- if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ $classes = array(); // Textarea CSS
+ if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
# Show a warning if editing an interface message
$wgOut->addWikiMsg( 'editinginterface' );
- } elseif( $this->mTitle->isProtected( 'edit' ) ) {
+ } elseif ( $this->mTitle->isProtected( 'edit' ) ) {
# Is the title semi-protected?
- if( $this->mTitle->isSemiProtected() ) {
+ if ( $this->mTitle->isSemiProtected() ) {
$noticeMsg = 'semiprotectedpagewarning';
+ $classes[] = 'mw-textarea-sprotected';
} else {
# Then it must be protected based on static groups (regular)
$noticeMsg = 'protectedpagewarning';
+ $classes[] = 'mw-textarea-protected';
}
+ $wgOut->addHTML( "<div class='mw-warning-with-logexcerpt'>\n" );
$wgOut->addWikiMsg( $noticeMsg );
+ LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle->getPrefixedText(), '', 1 );
+ $wgOut->addHTML( "</div>\n" );
}
if ( $this->mTitle->isCascadeProtected() ) {
# Is this page under cascading protection from some source pages?
@@ -1158,7 +1237,7 @@ class EditPage {
}
$wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', count($cascadeSources) ) );
}
- if( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) != array() ){
+ if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
$wgOut->addWikiMsg( 'titleprotectedwarning' );
}
@@ -1166,30 +1245,21 @@ class EditPage {
$this->kblength = (int)(strlen( $this->textbox1 ) / 1024);
}
if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) {
- $wgOut->addWikiMsg( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgMaxArticleSize );
- } elseif( $this->kblength > 29 ) {
+ $wgOut->addHTML( "<div class='error' id='mw-edit-longpageerror'>\n" );
+ $wgOut->addWikiMsg( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgLang->formatNum( $wgMaxArticleSize ) );
+ $wgOut->addHTML( "</div>\n" );
+ } elseif ( $this->kblength > 29 ) {
+ $wgOut->addHTML( "<div id='mw-edit-longpagewarning'>\n" );
$wgOut->addWikiMsg( 'longpagewarning', $wgLang->formatNum( $this->kblength ) );
+ $wgOut->addHTML( "</div>\n" );
}
- #need to parse the preview early so that we know which templates are used,
- #otherwise users with "show preview after edit box" will get a blank list
- if ( $this->formtype == 'preview' ) {
- $previewOutput = $this->getPreviewText();
- }
-
- $rows = $wgUser->getIntOption( 'rows' );
- $cols = $wgUser->getIntOption( 'cols' );
-
- $ew = $wgUser->getOption( 'editwidth' );
- if ( $ew ) $ew = " style=\"width:100%\"";
- else $ew = '';
-
- $q = 'action=submit';
+ $q = 'action='.$this->action;
#if ( "no" == $redirect ) { $q .= "&redirect=no"; }
$action = $wgTitle->escapeLocalURL( $q );
- $summary = wfMsg('summary');
- $subject = wfMsg('subject');
+ $summary = wfMsg( 'summary' );
+ $subject = wfMsg( 'subject' );
$cancel = $sk->makeKnownLink( $wgTitle->getPrefixedText(),
wfMsgExt('cancel', array('parseinline')) );
@@ -1208,7 +1278,7 @@ class EditPage {
'[[' . wfMsgForContent( 'copyrightpage' ) . ']]' );
}
- if( $wgUser->getOption('showtoolbar') and !$this->isCssJsSubpage ) {
+ if ( $wgUser->getOption('showtoolbar') and !$this->isCssJsSubpage ) {
# prepare toolbar for edit buttons
$toolbar = EditPage::getEditToolbar();
} else {
@@ -1216,35 +1286,31 @@ class EditPage {
}
// activate checkboxes if user wants them to be always active
- if( !$this->preview && !$this->diff ) {
+ if ( !$this->preview && !$this->diff ) {
# Sort out the "watch" checkbox
- if( $wgUser->getOption( 'watchdefault' ) ) {
+ if ( $wgUser->getOption( 'watchdefault' ) ) {
# Watch all edits
$this->watchthis = true;
- } elseif( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
+ } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
# Watch creations
$this->watchthis = true;
- } elseif( $this->mTitle->userIsWatching() ) {
+ } elseif ( $this->mTitle->userIsWatching() ) {
# Already watched
$this->watchthis = true;
}
+
+ # May be overriden by request parameters
+ if( $wgRequest->getBool( 'watchthis' ) ) {
+ $this->watchthis = true;
+ }
- if( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true;
+ if ( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true;
}
$wgOut->addHTML( $this->editFormPageTop );
if ( $wgUser->getOption( 'previewontop' ) ) {
-
- if ( 'preview' == $this->formtype ) {
- $this->showPreview( $previewOutput );
- } else {
- $wgOut->addHTML( '<div id="wikiPreview"></div>' );
- }
-
- if ( 'diff' == $this->formtype ) {
- $this->showDiff();
- }
+ $this->displayPreviewArea( $previewOutput, true );
}
@@ -1262,28 +1328,28 @@ class EditPage {
# For a bit more sophisticated detection of blank summaries, hash the
# automatic one and pass that in the hidden field wpAutoSummary.
$summaryhiddens = '';
- if( $this->missingSummary ) $summaryhiddens .= Xml::hidden( 'wpIgnoreBlankSummary', true );
+ if ( $this->missingSummary ) $summaryhiddens .= Xml::hidden( 'wpIgnoreBlankSummary', true );
$autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
$summaryhiddens .= Xml::hidden( 'wpAutoSummary', $autosumm );
- if( $this->section == 'new' ) {
- $commentsubject="<span id='wpSummaryLabel'><label for='wpSummary'>{$subject}:</label></span>\n<input tabindex='1' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' />{$summaryhiddens}<br />";
+ if ( $this->section == 'new' ) {
+ $commentsubject="<span id='wpSummaryLabel'><label for='wpSummary'>{$subject}</label></span>\n<input tabindex='1' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' />{$summaryhiddens}<br />";
$editsummary = "<div class='editOptions'>\n";
global $wgParser;
$formattedSummary = wfMsgForContent( 'newsectionsummary', $wgParser->stripSectionName( $this->summary ) );
- $subjectpreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">".wfMsg('subject-preview').':'.$sk->commentBlock( $formattedSummary, $this->mTitle, true )."</div>\n" : '';
+ $subjectpreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">". wfMsg('subject-preview') . $sk->commentBlock( $formattedSummary, $this->mTitle, true )."</div>\n" : '';
$summarypreview = '';
} else {
$commentsubject = '';
- $editsummary="<div class='editOptions'>\n<span id='wpSummaryLabel'><label for='wpSummary'>{$summary}:</label></span>\n<input tabindex='2' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' />{$summaryhiddens}<br />";
- $summarypreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">".wfMsg('summary-preview').':'.$sk->commentBlock( $this->summary, $this->mTitle )."</div>\n" : '';
+ $editsummary="<div class='editOptions'>\n<span id='wpSummaryLabel'><label for='wpSummary'>{$summary}</label></span>\n<input tabindex='2' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' />{$summaryhiddens}<br />";
+ $summarypreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">". wfMsg('summary-preview') .$sk->commentBlock( $this->summary, $this->mTitle )."</div>\n" : '';
$subjectpreview = '';
}
# Set focus to the edit box on load, except on preview or diff, where it would interfere with the display
- if( !$this->preview && !$this->diff ) {
+ if ( !$this->preview && !$this->diff ) {
$wgOut->setOnloadHandler( 'document.editform.wpTextbox1.focus()' );
}
- $templates = ($this->preview || $this->section != '') ? $this->mPreviewTemplates : $this->mArticle->getUsedTemplates();
+ $templates = $this->getTemplates();
$formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != '');
$hiddencats = $this->mArticle->getHiddenCategories();
@@ -1294,20 +1360,24 @@ class EditPage {
$metadata = $this->mMetaData ;
$metadata = htmlspecialchars( $wgContLang->recodeForEdit( $metadata ) ) ;
$top = wfMsgWikiHtml( 'metadata_help' );
+ /* ToDo: Replace with clean code */
+ $ew = $wgUser->getOption( 'editwidth' );
+ if ( $ew ) $ew = " style=\"width:100%\"";
+ else $ew = '';
+ $cols = $wgUser->getIntOption( 'cols' );
+ /* /ToDo */
$metadata = $top . "<textarea name='metadata' rows='3' cols='{$cols}'{$ew}>{$metadata}</textarea>" ;
}
else $metadata = "" ;
- $hidden = '';
$recreate = '';
- if ($this->wasDeletedSinceLastEdit()) {
+ if ( $this->wasDeletedSinceLastEdit() ) {
if ( 'save' != $this->formtype ) {
$wgOut->addWikiMsg('deletedwhileediting');
} else {
// Hide the toolbar and edit area, use can click preview to get it back
// Add an confirmation checkbox and explanation.
$toolbar = '';
- $hidden = 'type="hidden" style="display:none;"';
$recreate = $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment ));
$recreate .=
"<br /><input tabindex='1' type='checkbox' value='1' name='wpRecreate' id='wpRecreate' />".
@@ -1317,7 +1387,7 @@ class EditPage {
$tabindex = 2;
- $checkboxes = self::getCheckboxes( $tabindex, $sk,
+ $checkboxes = $this->getCheckboxes( $tabindex, $sk,
array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) );
$checkboxhtml = implode( $checkboxes, "\n" );
@@ -1334,47 +1404,34 @@ class EditPage {
END
);
- if( is_callable( $formCallback ) ) {
+ if ( is_callable( $formCallback ) ) {
call_user_func_array( $formCallback, array( &$wgOut ) );
}
wfRunHooks( 'EditPage::showEditForm:fields', array( &$this, &$wgOut ) );
// Put these up at the top to ensure they aren't lost on early form submission
- $wgOut->addHTML( "
-<input type='hidden' value=\"" . htmlspecialchars( $this->section ) . "\" name=\"wpSection\" />
-<input type='hidden' value=\"{$this->starttime}\" name=\"wpStarttime\" />\n
-<input type='hidden' value=\"{$this->edittime}\" name=\"wpEdittime\" />\n
-<input type='hidden' value=\"{$this->scrolltop}\" name=\"wpScrolltop\" id=\"wpScrolltop\" />\n" );
-
- $encodedtext = htmlspecialchars( $this->safeUnicodeOutput( $this->textbox1 ) );
- if( $encodedtext !== '' ) {
- // Ensure there's a newline at the end, otherwise adding lines
- // is awkward.
- // But don't add a newline if the ext is empty, or Firefox in XHTML
- // mode will show an extra newline. A bit annoying.
- $encodedtext .= "\n";
- }
+ $this->showFormBeforeText();
$wgOut->addHTML( <<<END
-$recreate
+{$recreate}
{$commentsubject}
{$subjectpreview}
{$this->editFormTextBeforeContent}
-<textarea tabindex='1' accesskey="," name="wpTextbox1" id="wpTextbox1" rows='{$rows}'
-cols='{$cols}'{$ew} $hidden>{$encodedtext}</textarea>
END
);
+ $this->showTextbox1( $classes );
$wgOut->wrapWikiMsg( "<div id=\"editpage-copywarn\">\n$1\n</div>", $copywarnMsg );
- $wgOut->addHTML( $this->editFormTextAfterWarn );
- $wgOut->addHTML( "
+ $wgOut->addHTML( <<<END
+{$this->editFormTextAfterWarn}
{$metadata}
{$editsummary}
{$summarypreview}
{$checkboxhtml}
{$safemodehtml}
-");
+END
+);
$wgOut->addHTML(
"<div class='editButtons'>
@@ -1398,20 +1455,18 @@ END
$token = htmlspecialchars( $wgUser->editToken() );
$wgOut->addHTML( "\n<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" );
- $wgOut->addHtml( '<div class="mw-editTools">' );
- $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) );
- $wgOut->addHtml( '</div>' );
-
- $wgOut->addHTML( $this->editFormTextAfterTools );
+ $this->showEditTools();
- $wgOut->addHTML( "
+ $wgOut->addHTML( <<<END
+{$this->editFormTextAfterTools}
<div class='templatesUsed'>
{$formattedtemplates}
</div>
<div class='hiddencats'>
{$formattedhiddencats}
</div>
-");
+END
+);
if ( $this->isConflict && wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
$wgOut->wrapWikiMsg( '==$1==', "yourdiff" );
@@ -1421,26 +1476,88 @@ END
$de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) );
$wgOut->wrapWikiMsg( '==$1==', "yourtext" );
- $wgOut->addHTML( "<textarea tabindex='6' id='wpTextbox2' name=\"wpTextbox2\" rows='{$rows}' cols='{$cols}'>"
- . htmlspecialchars( $this->safeUnicodeOutput( $this->textbox2 ) ) . "\n</textarea>" );
+ $this->showTextbox2();
}
$wgOut->addHTML( $this->editFormTextBottom );
$wgOut->addHTML( "</form>\n" );
if ( !$wgUser->getOption( 'previewontop' ) ) {
+ $this->displayPreviewArea( $previewOutput, false );
+ }
- if ( $this->formtype == 'preview') {
- $this->showPreview( $previewOutput );
- } else {
- $wgOut->addHTML( '<div id="wikiPreview"></div>' );
- }
+ wfProfileOut( $fname );
+ }
- if ( $this->formtype == 'diff') {
- $this->showDiff();
- }
+ protected function showFormBeforeText() {
+ global $wgOut;
+ $wgOut->addHTML( "
+<input type='hidden' value=\"" . htmlspecialchars( $this->section ) . "\" name=\"wpSection\" />
+<input type='hidden' value=\"{$this->starttime}\" name=\"wpStarttime\" />\n
+<input type='hidden' value=\"{$this->edittime}\" name=\"wpEdittime\" />\n
+<input type='hidden' value=\"{$this->scrolltop}\" name=\"wpScrolltop\" id=\"wpScrolltop\" />\n" );
+ }
+
+ protected function showTextbox1( $classes ) {
+ $attribs = array( 'tabindex' => 1 );
+
+ if ( $this->wasDeletedSinceLastEdit() )
+ $attribs['type'] = 'hidden';
+ if ( !empty($classes) )
+ $attribs['class'] = implode(' ',$classes);
+
+ $this->showTextbox( $this->textbox1, 'wpTextbox1', $attribs );
+ }
+
+ protected function showTextbox2() {
+ $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6 ) );
+ }
+
+ protected function showTextbox( $content, $name, $attribs = array() ) {
+ global $wgOut, $wgUser;
+
+ $wikitext = $this->safeUnicodeOutput( $content );
+ if ( $wikitext !== '' ) {
+ // Ensure there's a newline at the end, otherwise adding lines
+ // is awkward.
+ // But don't add a newline if the ext is empty, or Firefox in XHTML
+ // mode will show an extra newline. A bit annoying.
+ $wikitext .= "\n";
+ }
+
+ $attribs['accesskey'] = ',';
+ $attribs['id'] = $name;
+
+ if ( $wgUser->getOption( 'editwidth' ) )
+ $attribs['style'] = 'width: 100%';
+
+ $wgOut->addHTML( Xml::textarea(
+ $name,
+ $wikitext,
+ $wgUser->getIntOption( 'cols' ), $wgUser->getIntOption( 'rows' ),
+ $attribs ) );
+ }
+
+ protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
+ global $wgOut;
+ $classes = array();
+ if ( $isOnTop )
+ $classes[] = 'ontop';
+
+ $attribs = array( 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) );
+
+ if ( $this->formtype != 'preview' )
+ $attribs['style'] = 'display: none;';
+
+ $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
+ if ( $this->formtype == 'preview' ) {
+ $this->showPreview( $previewOutput );
}
- wfProfileOut( $fname );
+ $wgOut->addHTML( '</div>' );
+
+ if ( $this->formtype == 'diff') {
+ $this->showDiff();
+ }
}
/**
@@ -1451,17 +1568,16 @@ END
*/
protected function showPreview( $text ) {
global $wgOut;
-
- $wgOut->addHTML( '<div id="wikiPreview">' );
- if($this->mTitle->getNamespace() == NS_CATEGORY) {
+ if ( $this->mTitle->getNamespace() == NS_CATEGORY) {
$this->mArticle->openShowCategory();
}
+ # This hook seems slightly odd here, but makes things more
+ # consistent for extensions.
wfRunHooks( 'OutputPageBeforeHTML',array( &$wgOut, &$text ) );
$wgOut->addHTML( $text );
- if($this->mTitle->getNamespace() == NS_CATEGORY) {
+ if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
$this->mArticle->closeShowCategory();
}
- $wgOut->addHTML( '</div>' );
}
/**
@@ -1477,16 +1593,22 @@ END
function doLivePreviewScript() {
global $wgOut, $wgTitle;
$wgOut->addScriptFile( 'preview.js' );
- $liveAction = $wgTitle->getLocalUrl( 'action=submit&wpPreview=true&live=true' );
+ $liveAction = $wgTitle->getLocalUrl( "action={$this->action}&wpPreview=true&live=true" );
return "return !lpDoPreview(" .
"editform.wpTextbox1.value," .
'"' . $liveAction . '"' . ")";
}
+ protected function showEditTools() {
+ global $wgOut;
+ $wgOut->addHTML( '<div class="mw-editTools">' );
+ $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) );
+ $wgOut->addHTML( '</div>' );
+ }
+
function getLastDelete() {
$dbr = wfGetDB( DB_SLAVE );
- $fname = 'EditPage::getLastDelete';
- $res = $dbr->select(
+ $data = $dbr->selectRow(
array( 'logging', 'user' ),
array( 'log_type',
'log_action',
@@ -1502,27 +1624,20 @@ END
'log_type' => 'delete',
'log_action' => 'delete',
'user_id=log_user' ),
- $fname,
+ __METHOD__,
array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ) );
- if($dbr->numRows($res) == 1) {
- while ( $x = $dbr->fetchObject ( $res ) )
- $data = $x;
- $dbr->freeResult ( $res ) ;
- } else {
- $data = null;
- }
return $data;
}
/**
- * @todo document
+ * Get the rendered text for previewing.
+ * @return string
*/
function getPreviewText() {
- global $wgOut, $wgUser, $wgTitle, $wgParser, $wgLang, $wgContLang;
+ global $wgOut, $wgUser, $wgTitle, $wgParser, $wgLang, $wgContLang, $wgMessageCache;
- $fname = 'EditPage::getPreviewText';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
if ( $this->mTriedSave && !$this->mTokenOk ) {
if ( $this->mTokenOkExceptSuffix ) {
@@ -1538,7 +1653,7 @@ END
$parserOptions->setEditSection( false );
global $wgRawHtml;
- if( $wgRawHtml && !$this->mTokenOk ) {
+ if ( $wgRawHtml && !$this->mTokenOk ) {
// Could be an offsite preview attempt. This is very unsafe if
// HTML is enabled, as it could be an attack.
return $wgOut->parse( "<div class='previewnote'>" .
@@ -1549,21 +1664,22 @@ END
# XXX: stupid php bug won't let us use $wgTitle->isCssJsSubpage() here
if ( $this->isCssJsSubpage ) {
- if(preg_match("/\\.css$/", $this->mTitle->getText() ) ) {
+ if (preg_match("/\\.css$/", $this->mTitle->getText() ) ) {
$previewtext = wfMsg('usercsspreview');
- } else if(preg_match("/\\.js$/", $this->mTitle->getText() ) ) {
+ } else if (preg_match("/\\.js$/", $this->mTitle->getText() ) ) {
$previewtext = wfMsg('userjspreview');
}
$parserOptions->setTidy(true);
- $parserOutput = $wgParser->parse( $previewtext , $this->mTitle, $parserOptions );
- $wgOut->addHTML( $parserOutput->mText );
- $previewHTML = '';
+ $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
+ $previewHTML = $parserOutput->mText;
+ } elseif ( $rt = Title::newFromRedirect( $this->textbox1 ) ) {
+ $previewHTML = $this->mArticle->viewRedirect( $rt, false );
} else {
$toparse = $this->textbox1;
# If we're adding a comment, we need to show the
# summary as the headline
- if($this->section=="new" && $this->summary!="") {
+ if ( $this->section=="new" && $this->summary!="" ) {
$toparse="== {$this->summary} ==\n\n".$toparse;
}
@@ -1571,19 +1687,9 @@ END
// Parse mediawiki messages with correct target language
if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
- $pos = strrpos( $this->mTitle->getText(), '/' );
- if ( $pos !== false ) {
- $code = substr( $this->mTitle->getText(), $pos+1 );
- switch ($code) {
- case $wgLang->getCode():
- $obj = $wgLang; break;
- case $wgContLang->getCode():
- $obj = $wgContLang; break;
- default:
- $obj = Language::factory( $code );
- }
- $parserOptions->setTargetLanguage( $obj );
- }
+ list( /* $unused */, $lang ) = $wgMessageCache->figureMessage( $this->mTitle->getText() );
+ $obj = wfGetLangObj( $lang );
+ $parserOptions->setTargetLanguage( $obj );
}
@@ -1593,20 +1699,9 @@ END
$this->mTitle, $parserOptions );
$previewHTML = $parserOutput->getText();
+ $this->mParserOutput = $parserOutput;
$wgOut->addParserOutputNoText( $parserOutput );
- # ParserOutput might have altered the page title, so reset it
- # Also, use the title defined by DISPLAYTITLE magic word when present
- if( ( $dt = $parserOutput->getDisplayTitle() ) !== false ) {
- $wgOut->setPageTitle( wfMsg( 'editing', $dt ) );
- } else {
- $wgOut->setPageTitle( wfMsg( 'editing', $wgTitle->getPrefixedText() ) );
- }
-
- foreach ( $parserOutput->getTemplates() as $ns => $template)
- foreach ( array_keys( $template ) as $dbk)
- $this->mPreviewTemplates[] = Title::makeTitle($ns, $dbk);
-
if ( count( $parserOutput->getWarnings() ) ) {
$note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
}
@@ -1615,18 +1710,26 @@ END
$previewhead = '<h2>' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>\n" .
"<div class='previewnote'>" . $wgOut->parse( $note ) . "</div>\n";
if ( $this->isConflict ) {
- $previewhead.='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n";
+ $previewhead .='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n";
}
- if( $wgUser->getOption( 'previewontop' ) ) {
- // Spacer for the edit toolbar
- $previewfoot = '<p><br /></p>';
+ wfProfileOut( __METHOD__ );
+ return $previewhead . $previewHTML;
+ }
+
+ function getTemplates() {
+ if ( $this->preview || $this->section != '' ) {
+ $templates = array();
+ if ( !isset($this->mParserOutput) ) return $templates;
+ foreach( $this->mParserOutput->getTemplates() as $ns => $template) {
+ foreach( array_keys( $template ) as $dbk ) {
+ $templates[] = Title::makeTitle($ns, $dbk);
+ }
+ }
+ return $templates;
} else {
- $previewfoot = '';
+ return $this->mArticle->getUsedTemplates();
}
-
- wfProfileOut( $fname );
- return $previewhead . $previewHTML . $previewfoot;
}
/**
@@ -1639,22 +1742,22 @@ END
# If the user made changes, preserve them when showing the markup
# (This happens when a user is blocked during edit, for instance)
$first = $this->firsttime || ( !$this->save && $this->textbox1 == '' );
- if( $first ) {
+ if ( $first ) {
$source = $this->mTitle->exists() ? $this->getContent() : false;
} else {
$source = $this->textbox1;
}
# Spit out the source or the user's modified version
- if( $source !== false ) {
- $rows = $wgUser->getOption( 'rows' );
- $cols = $wgUser->getOption( 'cols' );
+ if ( $source !== false ) {
+ $rows = $wgUser->getIntOption( 'rows' );
+ $cols = $wgUser->getIntOption( 'cols' );
$attribs = array( 'id' => 'wpTextbox1', 'name' => 'wpTextbox1', 'cols' => $cols, 'rows' => $rows, 'readonly' => 'readonly' );
- $wgOut->addHtml( '<hr />' );
+ $wgOut->addHTML( '<hr />' );
$wgOut->addWikiMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() );
# Why we don't use Xml::element here?
# Is it because if $source is '', it returns <textarea />?
- $wgOut->addHtml( Xml::openElement( 'textarea', $attribs ) . htmlspecialchars( $source ) . Xml::closeElement( 'textarea' ) );
+ $wgOut->addHTML( Xml::openElement( 'textarea', $attribs ) . htmlspecialchars( $source ) . Xml::closeElement( 'textarea' ) );
}
}
@@ -1672,13 +1775,13 @@ END
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setArticleRelated( false );
- $wgOut->addHtml( wfMsgWikiHtml( 'whitelistedittext', $loginLink ) );
+ $wgOut->addHTML( wfMsgWikiHtml( 'whitelistedittext', $loginLink ) );
$wgOut->returnToMain( false, $wgTitle );
}
/**
* Creates a basic error page which informs the user that
- * they have attempted to edit a nonexistant section.
+ * they have attempted to edit a nonexistent section.
*/
function noSuchSectionPage() {
global $wgOut, $wgTitle;
@@ -1703,11 +1806,11 @@ END
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setArticleRelated( false );
- $wgOut->addHtml( '<div id="spamprotected">' );
+ $wgOut->addHTML( '<div id="spamprotected">' );
$wgOut->addWikiMsg( 'spamprotectiontext' );
if ( $match )
$wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
- $wgOut->addHtml( '</div>' );
+ $wgOut->addHTML( '</div>' );
$wgOut->returnToMain( false, $wgTitle );
}
@@ -1724,7 +1827,7 @@ END
// This is the revision the editor started from
$baseRevision = $this->getBaseRevision();
- if( is_null( $baseRevision ) ) {
+ if ( is_null( $baseRevision ) ) {
wfProfileOut( $fname );
return false;
}
@@ -1733,14 +1836,14 @@ END
// The current state, we want to merge updates into it
$currentRevision = Revision::loadFromTitle(
$db, $this->mTitle );
- if( is_null( $currentRevision ) ) {
+ if ( is_null( $currentRevision ) ) {
wfProfileOut( $fname );
return false;
}
$currentText = $currentRevision->getText();
$result = '';
- if( wfMerge( $baseText, $editText, $currentText, $result ) ){
+ if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
$editText = $result;
wfProfileOut( $fname );
return true;
@@ -1759,7 +1862,7 @@ END
*/
function checkUnicodeCompliantBrowser() {
global $wgBrowserBlackList;
- if( empty( $_SERVER["HTTP_USER_AGENT"] ) ) {
+ if ( empty( $_SERVER["HTTP_USER_AGENT"] ) ) {
// No User-Agent header sent? Trust it by default...
return true;
}
@@ -1861,7 +1964,7 @@ END
array(
'image' => $wgLang->getImageFile('button-image'),
'id' => 'mw-editbutton-image',
- 'open' => '[['.$wgContLang->getNsText(NS_IMAGE).':',
+ 'open' => '[['.$wgContLang->getNsText(NS_FILE).':',
'close' => ']]',
'sample' => wfMsg('image_sample'),
'tip' => wfMsg('image_tip'),
@@ -1951,7 +2054,7 @@ END
*
* @return array
*/
- public static function getCheckboxes( &$tabindex, $skin, $checked ) {
+ public function getCheckboxes( &$tabindex, $skin, $checked ) {
global $wgUser;
$checkboxes = array();
@@ -1981,6 +2084,7 @@ END
Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
"&nbsp;<label for='wpWatchthis'".$skin->tooltip('watch', 'withaccess').">{$watchLabel}</label>";
}
+ wfRunHooks( 'EditPageBeforeEditChecks', array( &$this, &$checkboxes, &$tabindex ) );
return $checkboxes;
}
@@ -2058,7 +2162,7 @@ END
);
$buttons['diff'] = Xml::element('input', $temp, '');
- wfRunHooks( 'EditPageBeforeEditButtons', array( &$this, &$buttons ) );
+ wfRunHooks( 'EditPageBeforeEditButtons', array( &$this, &$buttons, &$tabindex ) );
return $buttons;
}
@@ -2117,7 +2221,7 @@ END
}
global $wgOut;
- $wgOut->addHtml( '<div id="wikiDiff">' . $difftext . '</div>' );
+ $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
}
/**
@@ -2174,20 +2278,20 @@ END
$working = 0;
for( $i = 0; $i < strlen( $invalue ); $i++ ) {
$bytevalue = ord( $invalue{$i} );
- if( $bytevalue <= 0x7F ) { //0xxx xxxx
+ if ( $bytevalue <= 0x7F ) { //0xxx xxxx
$result .= chr( $bytevalue );
$bytesleft = 0;
- } elseif( $bytevalue <= 0xBF ) { //10xx xxxx
+ } elseif ( $bytevalue <= 0xBF ) { //10xx xxxx
$working = $working << 6;
$working += ($bytevalue & 0x3F);
$bytesleft--;
- if( $bytesleft <= 0 ) {
+ if ( $bytesleft <= 0 ) {
$result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
}
- } elseif( $bytevalue <= 0xDF ) { //110x xxxx
+ } elseif ( $bytevalue <= 0xDF ) { //110x xxxx
$working = $bytevalue & 0x1F;
$bytesleft = 1;
- } elseif( $bytevalue <= 0xEF ) { //1110 xxxx
+ } elseif ( $bytevalue <= 0xEF ) { //1110 xxxx
$working = $bytevalue & 0x0F;
$bytesleft = 2;
} else { //1111 0xxx
@@ -2210,7 +2314,7 @@ END
function unmakesafe( $invalue ) {
$result = "";
for( $i = 0; $i < strlen( $invalue ); $i++ ) {
- if( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue{$i+3} != '0' ) ) {
+ if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue{$i+3} != '0' ) ) {
$i += 3;
$hexstring = "";
do {
@@ -2221,7 +2325,7 @@ END
// Do some sanity checks. These aren't needed for reversability,
// but should help keep the breakage down if the editor
// breaks one of the entities whilst editing.
- if ((substr($invalue,$i,1)==";") and (strlen($hexstring) <= 6)) {
+ if ( (substr($invalue,$i,1)==";") and (strlen($hexstring) <= 6) ) {
$codepoint = hexdec($hexstring);
$result .= codepointToUtf8( $codepoint );
} else {
@@ -2251,16 +2355,30 @@ END
global $wgUser;
$loglist = new LogEventsList( $wgUser->getSkin(), $out );
$pager = new LogPager( $loglist, 'delete', false, $this->mTitle->getPrefixedText() );
- if( $pager->getNumRows() > 0 ) {
- $out->addHtml( '<div id="mw-recreate-deleted-warn">' );
+ $count = $pager->getNumRows();
+ if ( $count > 0 ) {
+ $pager->mLimit = 10;
+ $out->addHTML( '<div class="mw-warning-with-logexcerpt">' );
$out->addWikiMsg( 'recreate-deleted-warn' );
$out->addHTML(
$loglist->beginLogEventsList() .
$pager->getBody() .
$loglist->endLogEventsList()
);
- $out->addHtml( '</div>' );
+ if($count > 10){
+ $out->addHTML( $wgUser->getSkin()->link(
+ SpecialPage::getTitleFor( 'Log' ),
+ wfMsgHtml( 'deletelog-fulllog' ),
+ array(),
+ array(
+ 'type' => 'delete',
+ 'page' => $this->mTitle->getPrefixedText() ) ) );
+ }
+ $out->addHTML( '</div>' );
+ return true;
}
+
+ return false;
}
/**
@@ -2273,7 +2391,7 @@ END
$resultDetails = false;
$value = $this->internalAttemptSave( $resultDetails, $wgUser->isAllowed('bot') && $wgRequest->getBool('bot', true) );
- if( $value == self::AS_SUCCESS_UPDATE || $value == self::AS_SUCCESS_NEW_ARTICLE ) {
+ if ( $value == self::AS_SUCCESS_UPDATE || $value == self::AS_SUCCESS_NEW_ARTICLE ) {
$this->didSave = true;
}
@@ -2334,7 +2452,7 @@ END
}
function getBaseRevision() {
- if ($this->mBaseRevision == false) {
+ if ( $this->mBaseRevision == false ) {
$db = wfGetDB( DB_MASTER );
$baseRevision = Revision::loadFromTimestamp(
$db, $this->mTitle, $this->edittime );
diff --git a/includes/Exception.php b/includes/Exception.php
index ab25f0b8..eb715986 100644
--- a/includes/Exception.php
+++ b/includes/Exception.php
@@ -83,7 +83,7 @@ class MWException extends Exception {
function getHTML() {
global $wgShowExceptionDetails;
if( $wgShowExceptionDetails ) {
- return '<p>' . htmlspecialchars( $this->getMessage() ) .
+ return '<p>' . nl2br( htmlspecialchars( $this->getMessage() ) ) .
'</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ) .
"</p>\n";
} else {
@@ -129,7 +129,16 @@ class MWException extends Exception {
$file = $this->getFile();
$line = $this->getLine();
$message = $this->getMessage();
- return $wgRequest->getRequestURL() . " Exception from line $line of $file: $message";
+ if ( isset( $wgRequest ) ) {
+ $url = $wgRequest->getRequestURL();
+ if ( !$url ) {
+ $url = '[no URL]';
+ }
+ } else {
+ $url = '[no req]';
+ }
+
+ return "$url Exception from line $line of $file: $message";
}
/** Output the exception report using HTML */
@@ -137,7 +146,7 @@ class MWException extends Exception {
global $wgOut;
if ( $this->useOutputPage() ) {
$wgOut->setPageTitle( $this->getPageTitle() );
- $wgOut->setRobotpolicy( "noindex,nofollow" );
+ $wgOut->setRobotPolicy( "noindex,nofollow" );
$wgOut->setArticleRelated( false );
$wgOut->enableClientCache( false );
$wgOut->redirect( '' );
@@ -169,7 +178,7 @@ class MWException extends Exception {
wfDebugLog( 'exception', $log );
}
if ( $wgCommandLineMode ) {
- fwrite( STDERR, $this->getText() );
+ wfPrintError( $this->getText() );
} else {
$this->reportHTML();
}
@@ -268,7 +277,7 @@ function wfReportException( Exception $e ) {
$e2->__toString() . "\n";
if ( !empty( $GLOBALS['wgCommandLineMode'] ) ) {
- fwrite( STDERR, $message );
+ wfPrintError( $message );
} else {
echo nl2br( htmlspecialchars( $message ) ). "\n";
}
@@ -288,6 +297,21 @@ function wfReportException( Exception $e ) {
}
/**
+ * Print a message, if possible to STDERR.
+ * Use this in command line mode only (see wgCommandLineMode)
+ */
+function wfPrintError( $message ) {
+ #NOTE: STDERR may not be available, especially if php-cgi is used from the command line (bug #15602).
+ # Try to produce meaningful output anyway. Using echo may corrupt output to STDOUT though.
+ if ( defined( 'STDERR' ) ) {
+ fwrite( STDERR, $message );
+ }
+ else {
+ echo( $message );
+ }
+}
+
+/**
* Exception handler which simulates the appropriate catch() handling:
*
* try {
diff --git a/includes/Exif.php b/includes/Exif.php
index bd93eb76..d5cf09cf 100644
--- a/includes/Exif.php
+++ b/includes/Exif.php
@@ -48,7 +48,7 @@ class Exif {
/**
* Exif tags grouped by category, the tagname itself is the key and the type
* is the value, in the case of more than one possible value type they are
- * seperated by commas.
+ * separated by commas.
*/
var $mExifTags;
@@ -780,7 +780,28 @@ class FormatExif {
}
break;
- // TODO: Flash
+ case 'Flash':
+ $flashDecode = array(
+ 'fired' => $val & bindec( '00000001' ),
+ 'return' => ($val & bindec( '00000110' )) >> 1,
+ 'mode' => ($val & bindec( '00011000' )) >> 3,
+ 'function' => ($val & bindec( '00100000' )) >> 5,
+ 'redeye' => ($val & bindec( '01000000' )) >> 6,
+// 'reserved' => ($val & bindec( '10000000' )) >> 7,
+ );
+
+ # We do not need to handle unknown values since all are used.
+ foreach( $flashDecode as $subTag => $subValue ) {
+ # We do not need any message for zeroed values.
+ if( $subTag != 'fired' && $subValue == 0) {
+ continue;
+ }
+ $fullTag = $tag . '-' . $subTag ;
+ $flashMsgs[] = $this->msg( $fullTag, $subValue );
+ }
+ $tags[$tag] = $wgLang->commaList( $flashMsgs );
+ break;
+
case 'FocalPlaneResolutionUnit':
switch( $val ) {
case 2:
diff --git a/includes/Export.php b/includes/Export.php
index 7d0a824e..5f040b13 100644
--- a/includes/Export.php
+++ b/includes/Export.php
@@ -32,6 +32,7 @@ class WikiExporter {
const FULL = 0;
const CURRENT = 1;
+ const LOGS = 2;
const BUFFER = 0;
const STREAM = 1;
@@ -71,16 +72,16 @@ class WikiExporter {
*
* @param $sink mixed
*/
- function setOutputSink( &$sink ) {
+ public function setOutputSink( &$sink ) {
$this->sink =& $sink;
}
- function openStream() {
+ public function openStream() {
$output = $this->writer->openStream();
$this->sink->writeOpenStream( $output );
}
- function closeStream() {
+ public function closeStream() {
$output = $this->writer->closeStream();
$this->sink->writeCloseStream( $output );
}
@@ -90,7 +91,7 @@ class WikiExporter {
* in the database, either including complete history or only
* the most recent version.
*/
- function allPages() {
+ public function allPages() {
return $this->dumpFrom( '' );
}
@@ -101,7 +102,7 @@ class WikiExporter {
* @param $end Int: Exclusive upper limit (this id is not included)
* If 0, no upper limit.
*/
- function pagesByRange( $start, $end ) {
+ public function pagesByRange( $start, $end ) {
$condition = 'page_id >= ' . intval( $start );
if( $end ) {
$condition .= ' AND page_id < ' . intval( $end );
@@ -112,13 +113,13 @@ class WikiExporter {
/**
* @param $title Title
*/
- function pageByTitle( $title ) {
+ public function pageByTitle( $title ) {
return $this->dumpFrom(
'page_namespace=' . $title->getNamespace() .
' AND page_title=' . $this->db->addQuotes( $title->getDBkey() ) );
}
- function pageByName( $name ) {
+ public function pageByName( $name ) {
$title = Title::newFromText( $name );
if( is_null( $title ) ) {
return new WikiError( "Can't export invalid title" );
@@ -127,26 +128,36 @@ class WikiExporter {
}
}
- function pagesByName( $names ) {
+ public function pagesByName( $names ) {
foreach( $names as $name ) {
$this->pageByName( $name );
}
}
+ public function allLogs() {
+ return $this->dumpFrom( '' );
+ }
- // -------------------- private implementation below --------------------
+ public function logsByRange( $start, $end ) {
+ $condition = 'log_id >= ' . intval( $start );
+ if( $end ) {
+ $condition .= ' AND log_id < ' . intval( $end );
+ }
+ return $this->dumpFrom( $condition );
+ }
# Generates the distinct list of authors of an article
# Not called by default (depends on $this->list_authors)
# Can be set by Special:Export when not exporting whole history
- function do_list_authors ( $page , $revision , $cond ) {
+ protected function do_list_authors( $page , $revision , $cond ) {
$fname = "do_list_authors" ;
wfProfileIn( $fname );
$this->author_list = "<contributors>";
//rev_deleted
$nothidden = '(rev_deleted & '.Revision::DELETED_USER.') = 0';
- $sql = "SELECT DISTINCT rev_user_text,rev_user FROM {$page},{$revision} WHERE page_id=rev_page AND $nothidden AND " . $cond ;
+ $sql = "SELECT DISTINCT rev_user_text,rev_user FROM {$page},{$revision}
+ WHERE page_id=rev_page AND $nothidden AND " . $cond ;
$result = $this->db->query( $sql, $fname );
$resultset = $this->db->resultObject( $result );
while( $row = $resultset->fetchObject() ) {
@@ -163,87 +174,101 @@ class WikiExporter {
$this->author_list .= "</contributors>";
}
- function dumpFrom( $cond = '' ) {
+ protected function dumpFrom( $cond = '' ) {
$fname = 'WikiExporter::dumpFrom';
wfProfileIn( $fname );
+
+ # For logs dumps...
+ if( $this->history & self::LOGS ) {
+ $where = array( 'user_id = log_user' );
+ # Hide private logs
+ $where[] = LogEventsList::getExcludeClause( $this->db );
+ if( $cond ) $where[] = $cond;
+ $result = $this->db->select( array('logging','user'),
+ '*',
+ $where,
+ $fname,
+ array( 'ORDER BY' => 'log_id', 'USE INDEX' => array('logging' => 'PRIMARY') )
+ );
+ $wrapper = $this->db->resultObject( $result );
+ $this->outputLogStream( $wrapper );
+ # For page dumps...
+ } else {
+ list($page,$revision,$text) = $this->db->tableNamesN('page','revision','text');
- $page = $this->db->tableName( 'page' );
- $revision = $this->db->tableName( 'revision' );
- $text = $this->db->tableName( 'text' );
-
- $order = 'ORDER BY page_id';
- $limit = '';
+ $order = 'ORDER BY page_id';
+ $limit = '';
- if( $this->history == WikiExporter::FULL ) {
- $join = 'page_id=rev_page';
- } elseif( $this->history == WikiExporter::CURRENT ) {
- if ( $this->list_authors && $cond != '' ) { // List authors, if so desired
- $this->do_list_authors ( $page , $revision , $cond );
- }
- $join = 'page_id=rev_page AND page_latest=rev_id';
- } elseif ( is_array( $this->history ) ) {
- $join = 'page_id=rev_page';
- if ( $this->history['dir'] == 'asc' ) {
- $op = '>';
- $order .= ', rev_timestamp';
+ if( $this->history == WikiExporter::FULL ) {
+ $join = 'page_id=rev_page';
+ } elseif( $this->history == WikiExporter::CURRENT ) {
+ if ( $this->list_authors && $cond != '' ) { // List authors, if so desired
+ $this->do_list_authors ( $page , $revision , $cond );
+ }
+ $join = 'page_id=rev_page AND page_latest=rev_id';
+ } elseif ( is_array( $this->history ) ) {
+ $join = 'page_id=rev_page';
+ if ( $this->history['dir'] == 'asc' ) {
+ $op = '>';
+ $order .= ', rev_timestamp';
+ } else {
+ $op = '<';
+ $order .= ', rev_timestamp DESC';
+ }
+ if ( !empty( $this->history['offset'] ) ) {
+ $join .= " AND rev_timestamp $op " . $this->db->addQuotes(
+ $this->db->timestamp( $this->history['offset'] ) );
+ }
+ if ( !empty( $this->history['limit'] ) ) {
+ $limitNum = intval( $this->history['limit'] );
+ if ( $limitNum > 0 ) {
+ $limit = "LIMIT $limitNum";
+ }
+ }
} else {
- $op = '<';
- $order .= ', rev_timestamp DESC';
+ wfProfileOut( $fname );
+ return new WikiError( "$fname given invalid history dump type." );
}
- if ( !empty( $this->history['offset'] ) ) {
- $join .= " AND rev_timestamp $op " . $this->db->addQuotes(
- $this->db->timestamp( $this->history['offset'] ) );
+ $where = ( $cond == '' ) ? '' : "$cond AND";
+
+ if( $this->buffer == WikiExporter::STREAM ) {
+ $prev = $this->db->bufferResults( false );
}
- if ( !empty( $this->history['limit'] ) ) {
- $limitNum = intval( $this->history['limit'] );
- if ( $limitNum > 0 ) {
- $limit = "LIMIT $limitNum";
- }
+ if( $cond == '' ) {
+ // Optimization hack for full-database dump
+ $revindex = $pageindex = $this->db->useIndexClause("PRIMARY");
+ $straight = ' /*! STRAIGHT_JOIN */ ';
+ } else {
+ $pageindex = '';
+ $revindex = '';
+ $straight = '';
}
- } else {
- wfProfileOut( $fname );
- return new WikiError( "$fname given invalid history dump type." );
- }
- $where = ( $cond == '' ) ? '' : "$cond AND";
-
- if( $this->buffer == WikiExporter::STREAM ) {
- $prev = $this->db->bufferResults( false );
- }
- if( $cond == '' ) {
- // Optimization hack for full-database dump
- $revindex = $pageindex = $this->db->useIndexClause("PRIMARY");
- $straight = ' /*! STRAIGHT_JOIN */ ';
- } else {
- $pageindex = '';
- $revindex = '';
- $straight = '';
- }
- if( $this->text == WikiExporter::STUB ) {
- $sql = "SELECT $straight * FROM
+ if( $this->text == WikiExporter::STUB ) {
+ $sql = "SELECT $straight * FROM
$page $pageindex,
$revision $revindex
WHERE $where $join
$order $limit";
- } else {
- $sql = "SELECT $straight * FROM
+ } else {
+ $sql = "SELECT $straight * FROM
$page $pageindex,
$revision $revindex,
$text
WHERE $where $join AND rev_text_id=old_id
$order $limit";
- }
- $result = $this->db->query( $sql, $fname );
- $wrapper = $this->db->resultObject( $result );
- $this->outputStream( $wrapper );
+ }
+ $result = $this->db->query( $sql, $fname );
+ $wrapper = $this->db->resultObject( $result );
+ $this->outputPageStream( $wrapper );
- if ( $this->list_authors ) {
- $this->outputStream( $wrapper );
- }
+ if ( $this->list_authors ) {
+ $this->outputPageStream( $wrapper );
+ }
- if( $this->buffer == WikiExporter::STREAM ) {
- $this->db->bufferResults( $prev );
+ if( $this->buffer == WikiExporter::STREAM ) {
+ $this->db->bufferResults( $prev );
+ }
}
-
wfProfileOut( $fname );
}
@@ -258,9 +283,8 @@ class WikiExporter {
* blob storage types will make queries to pull source data.
*
* @param $resultset ResultWrapper
- * @access private
*/
- function outputStream( $resultset ) {
+ protected function outputPageStream( $resultset ) {
$last = null;
while( $row = $resultset->fetchObject() ) {
if( is_null( $last ) ||
@@ -292,6 +316,14 @@ class WikiExporter {
}
$resultset->free();
}
+
+ protected function outputLogStream( $resultset ) {
+ while( $row = $resultset->fetchObject() ) {
+ $output = $this->writer->writeLogItem( $row );
+ $this->sink->writeLogItem( $row, $output );
+ }
+ $resultset->free();
+ }
}
/**
@@ -320,7 +352,7 @@ class XmlDumpWriter {
function openStream() {
global $wgContLanguageCode;
$ver = $this->schemaVersion();
- return wfElement( 'mediawiki', array(
+ return Xml::element( 'mediawiki', array(
'xmlns' => "http://www.mediawiki.org/xml/export-$ver/",
'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
'xsi:schemaLocation' => "http://www.mediawiki.org/xml/export-$ver/ " .
@@ -346,30 +378,30 @@ class XmlDumpWriter {
function sitename() {
global $wgSitename;
- return wfElement( 'sitename', array(), $wgSitename );
+ return Xml::element( 'sitename', array(), $wgSitename );
}
function generator() {
global $wgVersion;
- return wfElement( 'generator', array(), "MediaWiki $wgVersion" );
+ return Xml::element( 'generator', array(), "MediaWiki $wgVersion" );
}
function homelink() {
- return wfElement( 'base', array(), Title::newMainPage()->getFullUrl() );
+ return Xml::element( 'base', array(), Title::newMainPage()->getFullUrl() );
}
function caseSetting() {
global $wgCapitalLinks;
// "case-insensitive" option is reserved for future
$sensitivity = $wgCapitalLinks ? 'first-letter' : 'case-sensitive';
- return wfElement( 'case', array(), $sensitivity );
+ return Xml::element( 'case', array(), $sensitivity );
}
function namespaces() {
global $wgContLang;
$spaces = " <namespaces>\n";
foreach( $wgContLang->getFormattedNamespaces() as $ns => $title ) {
- $spaces .= ' ' . wfElement( 'namespace', array( 'key' => $ns ), $title ) . "\n";
+ $spaces .= ' ' . Xml::element( 'namespace', array( 'key' => $ns ), $title ) . "\n";
}
$spaces .= " </namespaces>";
return $spaces;
@@ -395,10 +427,10 @@ class XmlDumpWriter {
function openPage( $row ) {
$out = " <page>\n";
$title = Title::makeTitle( $row->page_namespace, $row->page_title );
- $out .= ' ' . wfElementClean( 'title', array(), $title->getPrefixedText() ) . "\n";
- $out .= ' ' . wfElement( 'id', array(), strval( $row->page_id ) ) . "\n";
+ $out .= ' ' . Xml::elementClean( 'title', array(), $title->getPrefixedText() ) . "\n";
+ $out .= ' ' . Xml::element( 'id', array(), strval( $row->page_id ) ) . "\n";
if( '' != $row->page_restrictions ) {
- $out .= ' ' . wfElement( 'restrictions', array(),
+ $out .= ' ' . Xml::element( 'restrictions', array(),
strval( $row->page_restrictions ) ) . "\n";
}
return $out;
@@ -426,12 +458,12 @@ class XmlDumpWriter {
wfProfileIn( $fname );
$out = " <revision>\n";
- $out .= " " . wfElement( 'id', null, strval( $row->rev_id ) ) . "\n";
+ $out .= " " . Xml::element( 'id', null, strval( $row->rev_id ) ) . "\n";
$out .= $this->writeTimestamp( $row->rev_timestamp );
if( $row->rev_deleted & Revision::DELETED_USER ) {
- $out .= " " . wfElement( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n";
+ $out .= " " . Xml::element( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n";
} else {
$out .= $this->writeContributor( $row->rev_user, $row->rev_user_text );
}
@@ -440,22 +472,22 @@ class XmlDumpWriter {
$out .= " <minor/>\n";
}
if( $row->rev_deleted & Revision::DELETED_COMMENT ) {
- $out .= " " . wfElement( 'comment', array( 'deleted' => 'deleted' ) ) . "\n";
+ $out .= " " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n";
} elseif( $row->rev_comment != '' ) {
- $out .= " " . wfElementClean( 'comment', null, strval( $row->rev_comment ) ) . "\n";
+ $out .= " " . Xml::elementClean( 'comment', null, strval( $row->rev_comment ) ) . "\n";
}
if( $row->rev_deleted & Revision::DELETED_TEXT ) {
- $out .= " " . wfElement( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
+ $out .= " " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
} elseif( isset( $row->old_text ) ) {
// Raw text from the database may have invalid chars
$text = strval( Revision::getRevisionText( $row ) );
- $out .= " " . wfElementClean( 'text',
+ $out .= " " . Xml::elementClean( 'text',
array( 'xml:space' => 'preserve' ),
strval( $text ) ) . "\n";
} else {
// Stub output
- $out .= " " . wfElement( 'text',
+ $out .= " " . Xml::element( 'text',
array( 'id' => $row->rev_text_id ),
"" ) . "\n";
}
@@ -465,19 +497,67 @@ class XmlDumpWriter {
wfProfileOut( $fname );
return $out;
}
+
+ /**
+ * Dumps a <logitem> section on the output stream, with
+ * data filled in from the given database row.
+ *
+ * @param $row object
+ * @return string
+ * @access private
+ */
+ function writeLogItem( $row ) {
+ $fname = 'WikiExporter::writeLogItem';
+ wfProfileIn( $fname );
+
+ $out = " <logitem>\n";
+ $out .= " " . Xml::element( 'id', null, strval( $row->log_id ) ) . "\n";
+
+ $out .= $this->writeTimestamp( $row->log_timestamp );
+
+ if( $row->log_deleted & LogPage::DELETED_USER ) {
+ $out .= " " . Xml::element( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n";
+ } else {
+ $out .= $this->writeContributor( $row->log_user, $row->user_name );
+ }
+
+ if( $row->log_deleted & LogPage::DELETED_COMMENT ) {
+ $out .= " " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n";
+ } elseif( $row->log_comment != '' ) {
+ $out .= " " . Xml::elementClean( 'comment', null, strval( $row->log_comment ) ) . "\n";
+ }
+
+ $out .= " " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n";
+ $out .= " " . Xml::element( 'action', null, strval( $row->log_action ) ) . "\n";
+
+ if( $row->log_deleted & LogPage::DELETED_ACTION ) {
+ $out .= " " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
+ } else {
+ $title = Title::makeTitle( $row->log_namespace, $row->log_title );
+ $out .= " " . Xml::elementClean( 'logtitle', null, $title->getPrefixedText() ) . "\n";
+ $out .= " " . Xml::elementClean( 'params',
+ array( 'xml:space' => 'preserve' ),
+ strval( $row->log_params ) ) . "\n";
+ }
+
+ $out .= " </logitem>\n";
+
+ wfProfileOut( $fname );
+ return $out;
+ }
function writeTimestamp( $timestamp ) {
$ts = wfTimestamp( TS_ISO_8601, $timestamp );
- return " " . wfElement( 'timestamp', null, $ts ) . "\n";
+ return " " . Xml::element( 'timestamp', null, $ts ) . "\n";
}
function writeContributor( $id, $text ) {
$out = " <contributor>\n";
if( $id ) {
- $out .= " " . wfElementClean( 'username', null, strval( $text ) ) . "\n";
- $out .= " " . wfElement( 'id', null, strval( $id ) ) . "\n";
+ $out .= " " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n";
+ $out .= " " . Xml::element( 'id', null, strval( $id ) ) . "\n";
} else {
- $out .= " " . wfElementClean( 'ip', null, strval( $text ) ) . "\n";
+ $out .= " " . Xml::elementClean( 'ip', null, strval( $text ) ) . "\n";
}
$out .= " </contributor>\n";
return $out;
@@ -505,10 +585,10 @@ class XmlDumpWriter {
return " <upload>\n" .
$this->writeTimestamp( $file->getTimestamp() ) .
$this->writeContributor( $file->getUser( 'id' ), $file->getUser( 'text' ) ) .
- " " . wfElementClean( 'comment', null, $file->getDescription() ) . "\n" .
- " " . wfElement( 'filename', null, $file->getName() ) . "\n" .
- " " . wfElement( 'src', null, $file->getFullUrl() ) . "\n" .
- " " . wfElement( 'size', null, $file->getSize() ) . "\n" .
+ " " . Xml::elementClean( 'comment', null, $file->getDescription() ) . "\n" .
+ " " . Xml::element( 'filename', null, $file->getName() ) . "\n" .
+ " " . Xml::element( 'src', null, $file->getFullUrl() ) . "\n" .
+ " " . Xml::element( 'size', null, $file->getSize() ) . "\n" .
" </upload>\n";
}
@@ -539,6 +619,10 @@ class DumpOutput {
function writeRevision( $rev, $string ) {
$this->write( $string );
}
+
+ function writeLogItem( $rev, $string ) {
+ $this->write( $string );
+ }
/**
* Override to write to a different stream type.
@@ -654,6 +738,10 @@ class DumpFilter {
$this->sink->writeRevision( $rev, $string );
}
}
+
+ function writeLogItem( $rev, $string ) {
+ $this->sink->writeRevision( $rev, $string );
+ }
/**
* Override for page-based filter types.
@@ -692,7 +780,9 @@ class DumpNamespaceFilter extends DumpFilter {
"NS_USER_TALK" => NS_USER_TALK,
"NS_PROJECT" => NS_PROJECT,
"NS_PROJECT_TALK" => NS_PROJECT_TALK,
- "NS_IMAGE" => NS_IMAGE,
+ "NS_FILE" => NS_FILE,
+ "NS_FILE_TALK" => NS_FILE_TALK,
+ "NS_IMAGE" => NS_IMAGE, // NS_IMAGE is an alias for NS_FILE
"NS_IMAGE_TALK" => NS_IMAGE_TALK,
"NS_MEDIAWIKI" => NS_MEDIAWIKI,
"NS_MEDIAWIKI_TALK" => NS_MEDIAWIKI_TALK,
diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php
index e2b78566..d095aba0 100644
--- a/includes/ExternalStore.php
+++ b/includes/ExternalStore.php
@@ -14,7 +14,7 @@
*/
class ExternalStore {
/* Fetch data from given URL */
- static function fetchFromURL($url) {
+ static function fetchFromURL( $url ) {
global $wgExternalStores;
if( !$wgExternalStores )
@@ -44,7 +44,7 @@ class ExternalStore {
$class = 'ExternalStore' . ucfirst( $proto );
/* Any custom modules should be added to $wgAutoLoadClasses for on-demand loading */
- if( !class_exists( $class ) ){
+ if( !class_exists( $class ) ) {
return false;
}
@@ -66,4 +66,47 @@ class ExternalStore {
return $store->store( $params, $data );
}
}
+
+ /**
+ * Like insert() above, but does more of the work for us.
+ * This function does not need a url param, it builds it by
+ * itself. It also fails-over to the next possible clusters.
+ *
+ * @param string $data
+ * Returns the URL of the stored data item, or false on error
+ */
+ public static function insertToDefault( $data ) {
+ global $wgDefaultExternalStore;
+ $tryStores = (array)$wgDefaultExternalStore;
+ $error = false;
+ while ( count( $tryStores ) > 0 ) {
+ $index = mt_rand(0, count( $tryStores ) - 1);
+ $storeUrl = $tryStores[$index];
+ wfDebug( __METHOD__.": trying $storeUrl\n" );
+ list( $proto, $params ) = explode( '://', $storeUrl, 2 );
+ $store = self::getStoreObject( $proto );
+ if ( $store === false ) {
+ throw new MWException( "Invalid external storage protocol - $storeUrl" );
+ }
+ try {
+ $url = $store->store( $params, $data ); // Try to save the object
+ } catch ( DBConnectionError $error ) {
+ $url = false;
+ }
+ if ( $url ) {
+ return $url; // Done!
+ } else {
+ unset( $tryStores[$index] ); // Don't try this one again!
+ $tryStores = array_values( $tryStores ); // Must have consecutive keys
+ wfDebugLog( 'ExternalStorage', "Unable to store text to external storage $storeUrl" );
+ }
+ }
+ // All stores failed
+ if ( $error ) {
+ // Rethrow the last connection error
+ throw $error;
+ } else {
+ throw new MWException( "Unable to store text to external storage" );
+ }
+ }
}
diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php
index 549412d1..9fa7d1b1 100644
--- a/includes/ExternalStoreDB.php
+++ b/includes/ExternalStoreDB.php
@@ -56,7 +56,7 @@ class ExternalStoreDB {
* Fetch data from given URL
* @param string $url An url of the form DB://cluster/id or DB://cluster/id/itemid for concatened storage.
*/
- function fetchFromURL($url) {
+ function fetchFromURL( $url ) {
$path = explode( '/', $url );
$cluster = $path[2];
$id = $path[3];
@@ -122,12 +122,11 @@ class ExternalStoreDB {
* @return string URL
*/
function store( $cluster, $data ) {
- $fname = 'ExternalStoreDB::store';
-
- $dbw =& $this->getMaster( $cluster );
-
+ $dbw = $this->getMaster( $cluster );
$id = $dbw->nextSequenceValue( 'blob_blob_id_seq' );
- $dbw->insert( $this->getTable( $dbw ), array( 'blob_id' => $id, 'blob_text' => $data ), $fname );
+ $dbw->insert( $this->getTable( $dbw ),
+ array( 'blob_id' => $id, 'blob_text' => $data ),
+ __METHOD__ );
$id = $dbw->insertId();
if ( $dbw->getFlag( DBO_TRX ) ) {
$dbw->immediateCommit();
diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php
index 4c2eddc8..10bfa538 100644
--- a/includes/FakeTitle.php
+++ b/includes/FakeTitle.php
@@ -3,15 +3,13 @@
/**
* Fake title class that triggers an error if any members are called
*/
-class FakeTitle {
+class FakeTitle extends Title {
function error() { throw new MWException( "Attempt to call member function of FakeTitle\n" ); }
// PHP 5.1 method overload
function __call( $name, $args ) { $this->error(); }
// PHP <5.1 compatibility
- function getInterwikiLink() { $this->error(); }
- function getInterwikiCached() { $this->error(); }
function isLocal() { $this->error(); }
function isTrans() { $this->error(); }
function getText() { $this->error(); }
@@ -28,20 +26,20 @@ class FakeTitle {
function getPrefixedText() { $this->error(); }
function getFullText() { $this->error(); }
function getPrefixedURL() { $this->error(); }
- function getFullURL() {$this->error(); }
- function getLocalURL() { $this->error(); }
- function escapeLocalURL() { $this->error(); }
- function escapeFullURL() { $this->error(); }
- function getInternalURL() { $this->error(); }
+ function getFullURL( $query = '', $variant = false ) {$this->error(); }
+ function getLocalURL( $query = '', $variant = false ) { $this->error(); }
+ function escapeLocalURL( $query = '' ) { $this->error(); }
+ function escapeFullURL( $query = '' ) { $this->error(); }
+ function getInternalURL( $query = '', $variant = false ) { $this->error(); }
function getEditURL() { $this->error(); }
function getEscapedText() { $this->error(); }
function isExternal() { $this->error(); }
- function isSemiProtected() { $this->error(); }
- function isProtected() { $this->error(); }
+ function isSemiProtected( $action = 'edit' ) { $this->error(); }
+ function isProtected( $action = '' ) { $this->error(); }
function userIsWatching() { $this->error(); }
- function userCan() { $this->error(); }
+ function userCan( $action, $doExpensiveQueries = true ) { $this->error(); }
function userCanCreate() { $this->error(); }
- function userCanEdit() { $this->error(); }
+ function userCanEdit( $doExpensiveQueries = true ) { $this->error(); }
function userCanMove() { $this->error(); }
function isMovable() { $this->error(); }
function userCanRead() { $this->error(); }
@@ -79,6 +77,7 @@ class FakeTitle {
function equals() { $this->error(); }
function exists() { $this->error(); }
function isAlwaysKnown() { $this->error(); }
+ function isKnown() { $this->error(); }
function touchLinks() { $this->error(); }
function trackbackURL() { $this->error(); }
function trackbackRDF() { $this->error(); }
diff --git a/includes/Feed.php b/includes/Feed.php
index 512057d9..fe6d8feb 100644
--- a/includes/Feed.php
+++ b/includes/Feed.php
@@ -52,25 +52,45 @@ class FeedItem {
$this->Comments = $Comments;
}
- /**
- * @static
- */
- function xmlEncode( $string ) {
+ public function xmlEncode( $string ) {
$string = str_replace( "\r\n", "\n", $string );
$string = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', '', $string );
return htmlspecialchars( $string );
}
- function getTitle() { return $this->xmlEncode( $this->Title ); }
- function getUrl() { return $this->xmlEncode( $this->Url ); }
- function getDescription() { return $this->xmlEncode( $this->Description ); }
- function getLanguage() {
+ public function getTitle() {
+ return $this->xmlEncode( $this->Title );
+ }
+
+ public function getUrl() {
+ return $this->xmlEncode( $this->Url );
+ }
+
+ public function getDescription() {
+ return $this->xmlEncode( $this->Description );
+ }
+
+ public function getLanguage() {
global $wgContLanguageCode;
return $wgContLanguageCode;
}
- function getDate() { return $this->Date; }
- function getAuthor() { return $this->xmlEncode( $this->Author ); }
- function getComments() { return $this->xmlEncode( $this->Comments ); }
+
+ public function getDate() {
+ return $this->Date;
+ }
+ public function getAuthor() {
+ return $this->xmlEncode( $this->Author );
+ }
+ public function getComments() {
+ return $this->xmlEncode( $this->Comments );
+ }
+
+ /**
+ * Quickie hack... strip out wikilinks to more legible form from the comment.
+ */
+ public static function stripComment( $text ) {
+ return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text );
+ }
/**#@-*/
}
@@ -149,7 +169,7 @@ class ChannelFeed extends FeedItem {
global $wgStylePath, $wgStyleVersion;
$this->httpHeaders();
- echo '<?xml version="1.0" encoding="utf-8"?>' . "\n";
+ echo '<?xml version="1.0"?>' . "\n";
echo '<?xml-stylesheet type="text/css" href="' .
htmlspecialchars( wfExpandUrl( "$wgStylePath/common/feed.css?$wgStyleVersion" ) ) .
'"?' . ">\n";
diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php
index aa784c02..38bff363 100644
--- a/includes/FeedUtils.php
+++ b/includes/FeedUtils.php
@@ -75,17 +75,20 @@ class FeedUtils {
if( $oldid ) {
wfProfileIn( __FUNCTION__."-dodiff" );
- $de = new DifferenceEngine( $title, $oldid, $newid );
#$diffText = $de->getDiff( wfMsg( 'revisionasof',
# $wgContLang->timeanddate( $timestamp ) ),
# wfMsg( 'currentrev' ) );
- $diffText = $de->getDiff(
- wfMsg( 'previousrevision' ), // hack
- wfMsg( 'revisionasof',
- $wgContLang->timeanddate( $timestamp ) ) );
-
+
+ // Don't bother generating the diff if we won't be able to show it
+ if ( $wgFeedDiffCutoff > 0 ) {
+ $de = new DifferenceEngine( $title, $oldid, $newid );
+ $diffText = $de->getDiff(
+ wfMsg( 'previousrevision' ), // hack
+ wfMsg( 'revisionasof',
+ $wgContLang->timeanddate( $timestamp ) ) );
+ }
- if ( strlen( $diffText ) > $wgFeedDiffCutoff ) {
+ if ( ( strlen( $diffText ) > $wgFeedDiffCutoff ) || ( $wgFeedDiffCutoff <= 0 ) ) {
// Omit large diffs
$diffLink = $title->escapeFullUrl(
'diff=' . $newid .
diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php
index bc80c2b2..66086b0f 100644
--- a/includes/FileDeleteForm.php
+++ b/includes/FileDeleteForm.php
@@ -55,7 +55,7 @@ class FileDeleteForm {
$this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->oldimage );
if( !self::haveDeletableFile($this->file, $this->oldfile, $this->oldimage) ) {
- $wgOut->addHtml( $this->prepareMessage( 'filedelete-nofile' ) );
+ $wgOut->addHTML( $this->prepareMessage( 'filedelete-nofile' ) );
$wgOut->addReturnTo( $this->title );
return;
}
@@ -78,7 +78,7 @@ class FileDeleteForm {
$wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) );
if( $status->ok ) {
$wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
- $wgOut->addHtml( $this->prepareMessage( 'filedelete-success' ) );
+ $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() );
@@ -105,16 +105,24 @@ class FileDeleteForm {
} else {
$status = $file->delete( $reason, $suppress );
if( $status->ok ) {
+ $id = $title->getArticleID( GAID_FOR_UPDATE );
// Need to delete the associated article
$article = new Article( $title );
if( wfRunHooks('ArticleDelete', array(&$article, &$wgUser, &$reason)) ) {
- if( $article->doDeleteArticle( $reason, $suppress ) )
- wfRunHooks('ArticleDeleteComplete', array(&$article, &$wgUser, $reason));
+ if( $article->doDeleteArticle( $reason, $suppress, $id ) ) {
+ global $wgRequest;
+ if( $wgRequest->getCheck( 'wpWatch' ) ) {
+ $article->doWatch();
+ } elseif( $title->userIsWatching() ) {
+ $article->doUnwatch();
+ }
+ wfRunHooks('ArticleDeleteComplete', array(&$article, &$wgUser, $reason, $id));
+ }
}
}
}
- if( $status->isGood() ) wfRunHooks('FileDeleteComplete', array(
- &$file, &$oldimage, &$article, &$wgUser, &$reason));
+ if( $status->isGood() )
+ wfRunHooks('FileDeleteComplete', array( &$file, &$oldimage, &$article, &$wgUser, &$reason));
return $status;
}
@@ -123,46 +131,60 @@ class FileDeleteForm {
* Show the confirmation form
*/
private function showForm() {
- global $wgOut, $wgUser, $wgRequest, $wgContLang;
- $align = $wgContLang->isRtl() ? 'left' : 'right';
+ global $wgOut, $wgUser, $wgRequest;
if( $wgUser->isAllowed( 'suppressrevision' ) ) {
- $suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\"><td></td><td>";
- $suppress .= Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '2' ) );
- $suppress .= "</td></tr>";
+ $suppress = "<tr id=\"wpDeleteSuppressRow\">
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'revdelete-suppress' ),
+ 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '3' ) ) .
+ "</td>
+ </tr>";
} else {
$suppress = '';
}
- $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) ) .
+ $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $this->title->userIsWatching();
+ $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction(),
+ 'id' => 'mw-img-deleteconfirm' ) ) .
Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', null, wfMsg( 'filedelete-legend' ) ) .
Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) ) .
$this->prepareMessage( 'filedelete-intro' ) .
- Xml::openElement( 'table' ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-img-deleteconfirm-table' ) ) .
"<tr>
- <td align='$align'>" .
+ <td class='mw-label'>" .
Xml::label( wfMsg( 'filedelete-comment' ), 'wpDeleteReasonList' ) .
"</td>
- <td>" .
+ <td class='mw-input'>" .
Xml::listDropDown( 'wpDeleteReasonList',
wfMsgForContent( 'filedelete-reason-dropdown' ),
wfMsgForContent( 'filedelete-reason-otherlist' ), '', 'wpReasonDropDown', 1 ) .
"</td>
</tr>
<tr>
- <td align='$align'>" .
+ <td class='mw-label'>" .
Xml::label( wfMsg( 'filedelete-otherreason' ), 'wpReason' ) .
"</td>
- <td>" .
- Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ), array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) .
+ <td class='mw-input'>" .
+ Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ),
+ array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) .
"</td>
</tr>
{$suppress}
<tr>
<td></td>
- <td>" .
- Xml::submitButton( wfMsg( 'filedelete-submit' ), array( 'name' => 'mw-filedelete-submit', 'id' => 'mw-filedelete-submit', 'tabindex' => '3' ) ) .
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'watchthis' ),
+ 'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( wfMsg( 'filedelete-submit' ),
+ array( 'name' => 'mw-filedelete-submit', 'id' => 'mw-filedelete-submit', 'tabindex' => '4' ) ) .
"</td>
</tr>" .
Xml::closeElement( 'table' ) .
@@ -175,7 +197,7 @@ class FileDeleteForm {
$form .= '<p class="mw-filedelete-editreasons">' . $link . '</p>';
}
- $wgOut->addHtml( $form );
+ $wgOut->addHTML( $form );
}
/**
@@ -183,7 +205,7 @@ class FileDeleteForm {
*/
private function showLogEntries() {
global $wgOut;
- $wgOut->addHtml( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
+ $wgOut->addHTML( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
LogEventsList::showLogExtract( $wgOut, 'delete', $this->title->getPrefixedText() );
}
diff --git a/includes/FileRevertForm.php b/includes/FileRevertForm.php
index 385d83bc..c7c73246 100644
--- a/includes/FileRevertForm.php
+++ b/includes/FileRevertForm.php
@@ -57,7 +57,7 @@ class FileRevertForm {
}
if( !$this->haveOldVersion() ) {
- $wgOut->addHtml( wfMsgExt( 'filerevert-badversion', 'parse' ) );
+ $wgOut->addHTML( wfMsgExt( 'filerevert-badversion', 'parse' ) );
$wgOut->returnToMain( false, $this->title );
return;
}
@@ -69,7 +69,7 @@ class FileRevertForm {
// 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(),
+ $wgOut->addHTML( wfMsgExt( 'filerevert-success', 'parse', $this->title->getText(),
$wgLang->date( $this->getTimestamp(), true ),
$wgLang->time( $this->getTimestamp(), true ),
wfExpandUrl( $this->file->getArchiveUrl( $this->archiveName ) ) ) );
@@ -104,7 +104,7 @@ class FileRevertForm {
$form .= '</fieldset>';
$form .= '</form>';
- $wgOut->addHtml( $form );
+ $wgOut->addHTML( $form );
}
/**
diff --git a/includes/FileStore.php b/includes/FileStore.php
index c01350c0..278777b4 100644
--- a/includes/FileStore.php
+++ b/includes/FileStore.php
@@ -35,39 +35,22 @@ class FileStore {
* This is attached to your master database connection, so if you
* suffer an uncaught error the lock will be released when the
* connection is closed.
- *
- * @todo Probably only works on MySQL. Abstract to the Database class?
+ * @see Database::lock()
*/
static function lock() {
- global $wgDBtype;
- if ($wgDBtype != 'mysql')
- return true;
$dbw = wfGetDB( DB_MASTER );
$lockname = $dbw->addQuotes( FileStore::lockName() );
- $result = $dbw->query( "SELECT GET_LOCK($lockname, 5) AS lockstatus", __METHOD__ );
- $row = $dbw->fetchObject( $result );
- $dbw->freeResult( $result );
-
- if( $row->lockstatus == 1 ) {
- return true;
- } else {
- wfDebug( __METHOD__." failed to acquire lock\n" );
- return false;
- }
+ return $dbw->lock( $lockname, __METHOD__ );
}
/**
* Release the global file store lock.
+ * @see Database::unlock()
*/
static function unlock() {
- global $wgDBtype;
- if ($wgDBtype != 'mysql')
- return true;
$dbw = wfGetDB( DB_MASTER );
$lockname = $dbw->addQuotes( FileStore::lockName() );
- $result = $dbw->query( "SELECT RELEASE_LOCK($lockname)", __METHOD__ );
- $dbw->fetchObject( $result );
- $dbw->freeResult( $result );
+ return $dbw->unlock( $lockname, __METHOD__ );
}
private static function lockName() {
@@ -123,7 +106,7 @@ class FileStore {
} else {
if( !file_exists( dirname( $destPath ) ) ) {
wfSuppressWarnings();
- $ok = mkdir( dirname( $destPath ), 0777, true );
+ $ok = wfMkdirParents( dirname( $destPath ) );
wfRestoreWarnings();
if( !$ok ) {
diff --git a/includes/FormOptions.php b/includes/FormOptions.php
index 5888a0c4..262c8c7f 100644
--- a/includes/FormOptions.php
+++ b/includes/FormOptions.php
@@ -176,8 +176,8 @@ class FormOptions implements ArrayAccess {
throw new MWException( 'Unsupported datatype' );
}
- if ( $value !== $default && $value !== null ) {
- $this->options[$name]['value'] = $value;
+ if ( $value !== null ) {
+ $this->options[$name]['value'] = $value === $default ? null : $value;
}
}
}
diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
index d1336d47..33f5831d 100644
--- a/includes/GlobalFunctions.php
+++ b/includes/GlobalFunctions.php
@@ -8,10 +8,12 @@ if ( !defined( 'MEDIAWIKI' ) ) {
* Global functions used everywhere
*/
-require_once dirname(__FILE__) . '/LogPage.php';
require_once dirname(__FILE__) . '/normal/UtfNormalUtil.php';
require_once dirname(__FILE__) . '/XmlFunctions.php';
+// Hide compatibility functions from Doxygen
+/// @cond
+
/**
* Compatibility functions
*
@@ -87,6 +89,9 @@ if ( !function_exists( 'array_diff_key' ) ) {
}
}
+/// @endcond
+
+
/**
* Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
*/
@@ -145,16 +150,31 @@ function wfRandom() {
}
/**
- * We want / and : to be included as literal characters in our title URLs.
+ * We want some things to be included as literal characters in our title URLs
+ * for prettiness, which urlencode encodes by default. According to RFC 1738,
+ * all of the following should be safe:
+ *
+ * ;:@&=$-_.+!*'(),
+ *
+ * But + is not safe because it's used to indicate a space; &= are only safe in
+ * paths and not in queries (and we don't distinguish here); ' seems kind of
+ * scary; and urlencode() doesn't touch -_. to begin with. Plus, although /
+ * is reserved, we don't care. So the list we unescape is:
+ *
+ * ;:@$!*(),/
+ *
* %2F in the page titles seems to fatally break for some reason.
*
* @param $s String:
* @return string
*/
-function wfUrlencode ( $s ) {
+function wfUrlencode( $s ) {
$s = urlencode( $s );
- $s = preg_replace( '/%3[Aa]/', ':', $s );
- $s = preg_replace( '/%2[Ff]/', '/', $s );
+ $s = str_ireplace(
+ array( '%3B','%3A','%40','%24','%21','%2A','%28','%29','%2C','%2F' ),
+ array( ';', ':', '@', '$', '!', '*', '(', ')', ',', '/' ),
+ $s
+ );
return $s;
}
@@ -174,6 +194,7 @@ function wfUrlencode ( $s ) {
*/
function wfDebug( $text, $logonly = false ) {
global $wgOut, $wgDebugLogFile, $wgDebugComments, $wgProfileOnly, $wgDebugRawPage;
+ global $wgDebugLogPrefix;
static $recursion = 0;
static $cache = array(); // Cache of unoutputted messages
@@ -206,11 +227,26 @@ function wfDebug( $text, $logonly = false ) {
# Strip unprintables; they can switch terminal modes when binary data
# gets dumped, which is pretty annoying.
$text = preg_replace( '![\x00-\x08\x0b\x0c\x0e-\x1f]!', ' ', $text );
+ $text = $wgDebugLogPrefix . $text;
wfErrorLog( $text, $wgDebugLogFile );
}
}
/**
+ * Send a line giving PHP memory usage.
+ * @param $exact Bool : print exact values instead of kilobytes (default: false)
+ */
+function wfDebugMem( $exact = false ) {
+ $mem = memory_get_usage();
+ if( !$exact ) {
+ $mem = floor( $mem / 1024 ) . ' kilobytes';
+ } else {
+ $mem .= ' bytes';
+ }
+ wfDebug( "Memory usage: $mem\n" );
+}
+
+/**
* Send a line to a supplementary debug log file, if configured, or main debug log if not.
* $wgDebugLogGroups[$logGroup] should be set to a filename to send to a separate log.
*
@@ -220,12 +256,17 @@ function wfDebug( $text, $logonly = false ) {
* log file is specified, (default true)
*/
function wfDebugLog( $logGroup, $text, $public = true ) {
- global $wgDebugLogGroups;
- if( $text{strlen( $text ) - 1} != "\n" ) $text .= "\n";
+ global $wgDebugLogGroups, $wgShowHostnames;
+ $text = trim($text)."\n";
if( isset( $wgDebugLogGroups[$logGroup] ) ) {
$time = wfTimestamp( TS_DB );
$wiki = wfWikiID();
- wfErrorLog( "$time $wiki: $text", $wgDebugLogGroups[$logGroup] );
+ if ( $wgShowHostnames ) {
+ $host = wfHostname();
+ } else {
+ $host = '';
+ }
+ wfErrorLog( "$time $host $wiki: $text", $wgDebugLogGroups[$logGroup] );
} else if ( $public === true ) {
wfDebug( $text, true );
}
@@ -245,16 +286,50 @@ function wfLogDBError( $text ) {
}
/**
- * Log to a file without getting "file size exceeded" signals
+ * Log to a file without getting "file size exceeded" signals.
+ *
+ * Can also log to TCP or UDP with the syntax udp://host:port/prefix. This will
+ * send lines to the specified port, prefixed by the specified prefix and a space.
*/
function wfErrorLog( $text, $file ) {
- wfSuppressWarnings();
- $exists = file_exists( $file );
- $size = $exists ? filesize( $file ) : false;
- if ( !$exists || ( $size !== false && $size + strlen( $text ) < 0x7fffffff ) ) {
- error_log( $text, 3, $file );
+ if ( substr( $file, 0, 4 ) == 'udp:' ) {
+ if ( preg_match( '!^(tcp|udp):(?://)?\[([0-9a-fA-F:]+)\]:(\d+)(?:/(.*))?$!', $file, $m ) ) {
+ // IPv6 bracketed host
+ $protocol = $m[1];
+ $host = $m[2];
+ $port = $m[3];
+ $prefix = isset( $m[4] ) ? $m[4] : false;
+ } elseif ( preg_match( '!^(tcp|udp):(?://)?([a-zA-Z0-9.-]+):(\d+)(?:/(.*))?$!', $file, $m ) ) {
+ $protocol = $m[1];
+ $host = $m[2];
+ $port = $m[3];
+ $prefix = isset( $m[4] ) ? $m[4] : false;
+ } else {
+ throw new MWException( __METHOD__.": Invalid UDP specification" );
+ }
+ // Clean it up for the multiplexer
+ if ( strval( $prefix ) !== '' ) {
+ $text = preg_replace( '/^/m', $prefix . ' ', $text );
+ if ( substr( $text, -1 ) != "\n" ) {
+ $text .= "\n";
+ }
+ }
+
+ $sock = fsockopen( "$protocol://$host", $port );
+ if ( !$sock ) {
+ return;
+ }
+ fwrite( $sock, $text );
+ fclose( $sock );
+ } else {
+ wfSuppressWarnings();
+ $exists = file_exists( $file );
+ $size = $exists ? filesize( $file ) : false;
+ if ( !$exists || ( $size !== false && $size + strlen( $text ) < 0x7fffffff ) ) {
+ error_log( $text, 3, $file );
+ }
+ wfRestoreWarnings();
}
- wfRestoreWarnings();
}
/**
@@ -320,6 +395,47 @@ function wfReadOnlyReason() {
}
/**
+ * Return a Language object from $langcode
+ * @param $langcode Mixed: either:
+ * - a Language object
+ * - code of the language to get the message for, if it is
+ * a valid code create a language for that language, if
+ * it is a string but not a valid code then make a basic
+ * language object
+ * - a boolean: if it's false then use the current users
+ * language (as a fallback for the old parameter
+ * functionality), or if it is true then use the wikis
+ * @return Language object
+ */
+function wfGetLangObj( $langcode = false ){
+ # Identify which language to get or create a language object for.
+ if( $langcode instanceof Language )
+ # Great, we already have the object!
+ return $langcode;
+
+ global $wgContLang;
+ if( $langcode === $wgContLang->getCode() || $langcode === true )
+ # $langcode is the language code of the wikis content language object.
+ # or it is a boolean and value is true
+ return $wgContLang;
+
+ global $wgLang;
+ if( $langcode === $wgLang->getCode() || $langcode === false )
+ # $langcode is the language code of user language object.
+ # or it was a boolean and value is false
+ return $wgLang;
+
+ $validCodes = array_keys( Language::getLanguageNames() );
+ if( in_array( $langcode, $validCodes ) )
+ # $langcode corresponds to a valid language.
+ return Language::factory( $langcode );
+
+ # $langcode is a string, but not a valid language code; use content language.
+ wfDebug( 'Invalid language code passed to wfGetLangObj, falling back to content language.' );
+ return $wgContLang;
+}
+
+/**
* Get a message from anywhere, for the current user language.
*
* Use wfMsgForContent() instead if the message should NOT
@@ -458,7 +574,7 @@ function wfMsgWeirdKey ( $key ) {
* @private
*/
function wfMsgGetKey( $key, $useDB, $langCode = false, $transform = true ) {
- global $wgParser, $wgContLang, $wgMessageCache, $wgLang;
+ global $wgContLang, $wgMessageCache;
wfRunHooks('NormalizeMessageKey', array(&$key, &$useDB, &$langCode, &$transform));
@@ -469,21 +585,7 @@ function wfMsgGetKey( $key, $useDB, $langCode = false, $transform = true ) {
$message = $wgMessageCache->transform( $message );
}
} else {
- if( $langCode === true ) {
- $lang = &$wgContLang;
- } elseif( $langCode === false ) {
- $lang = &$wgLang;
- } else {
- $validCodes = array_keys( Language::getLanguageNames() );
- if( in_array( $langCode, $validCodes ) ) {
- # $langcode corresponds to a valid language.
- $lang = Language::factory( $langCode );
- } else {
- # $langcode is a string, but not a valid language code; use content language.
- $lang =& $wgContLang;
- wfDebug( 'Invalid language code passed to wfMsgGetKey, falling back to content language.' );
- }
- }
+ $lang = wfGetLangObj( $langCode );
# MessageCache::get() does this already, Language::getMessage() doesn't
# ISSUE: Should we try to handle "message/lang" here too?
@@ -565,40 +667,47 @@ function wfMsgWikiHtml( $key ) {
/**
* Returns message in the requested format
* @param string $key Key of the message
- * @param array $options Processing rules:
- * <i>parse</i>: parses wikitext to html
- * <i>parseinline</i>: parses wikitext to html and removes the surrounding p's added by parser or tidy
- * <i>escape</i>: filters message through htmlspecialchars
- * <i>escapenoentities</i>: same, but allows entity references like &nbsp; through
- * <i>replaceafter</i>: parameters are substituted after parsing or escaping
- * <i>parsemag</i>: transform the message using magic phrases
- * <i>content</i>: fetch message for content language instead of interface
- * <i>language</i>: language code to fetch message for (overriden by <i>content</i>), its behaviour
- * with parser, parseinline and parsemag is undefined.
+ * @param array $options Processing rules. Can take the following options:
+ * <i>parse</i>: parses wikitext to html
+ * <i>parseinline</i>: parses wikitext to html and removes the surrounding
+ * p's added by parser or tidy
+ * <i>escape</i>: filters message through htmlspecialchars
+ * <i>escapenoentities</i>: same, but allows entity references like &nbsp; through
+ * <i>replaceafter</i>: parameters are substituted after parsing or escaping
+ * <i>parsemag</i>: transform the message using magic phrases
+ * <i>content</i>: fetch message for content language instead of interface
+ * Also can accept a single associative argument, of the form 'language' => 'xx':
+ * <i>language</i>: Language object or language code to fetch message for
+ * (overriden by <i>content</i>), its behaviour with parser, parseinline
+ * and parsemag is undefined.
* Behavior for conflicting options (e.g., parse+parseinline) is undefined.
*/
function wfMsgExt( $key, $options ) {
- global $wgOut, $wgParser;
+ global $wgOut;
$args = func_get_args();
array_shift( $args );
array_shift( $args );
-
- if( !is_array($options) ) {
- $options = array($options);
+ $options = (array)$options;
+
+ foreach( $options as $arrayKey => $option ) {
+ if( !preg_match( '/^[0-9]+|language$/', $arrayKey ) ) {
+ # An unknown index, neither numeric nor "language"
+ trigger_error( "wfMsgExt called with incorrect parameter key $arrayKey", E_USER_WARNING );
+ } elseif( preg_match( '/^[0-9]+$/', $arrayKey ) && !in_array( $option,
+ array( 'parse', 'parseinline', 'escape', 'escapenoentities',
+ 'replaceafter', 'parsemag', 'content' ) ) ) {
+ # A numeric index with unknown value
+ trigger_error( "wfMsgExt called with incorrect parameter $option", E_USER_WARNING );
+ }
}
- if( in_array('content', $options) ) {
+ if( in_array('content', $options, true ) ) {
$forContent = true;
$langCode = true;
} elseif( array_key_exists('language', $options) ) {
$forContent = false;
- $langCode = $options['language'];
- $validCodes = array_keys( Language::getLanguageNames() );
- if( !in_array($options['language'], $validCodes) ) {
- # Fallback to en, instead of whatever interface language we might have
- $langCode = 'en';
- }
+ $langCode = wfGetLangObj( $options['language'] );
} else {
$forContent = false;
$langCode = false;
@@ -606,34 +715,34 @@ function wfMsgExt( $key, $options ) {
$string = wfMsgGetKey( $key, /*DB*/true, $langCode, /*Transform*/false );
- if( !in_array('replaceafter', $options) ) {
+ if( !in_array('replaceafter', $options, true ) ) {
$string = wfMsgReplaceArgs( $string, $args );
}
- if( in_array('parse', $options) ) {
+ if( in_array('parse', $options, true ) ) {
$string = $wgOut->parse( $string, true, !$forContent );
- } elseif ( in_array('parseinline', $options) ) {
+ } elseif ( in_array('parseinline', $options, true ) ) {
$string = $wgOut->parse( $string, true, !$forContent );
$m = array();
if( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $string, $m ) ) {
$string = $m[1];
}
- } elseif ( in_array('parsemag', $options) ) {
+ } elseif ( in_array('parsemag', $options, true ) ) {
global $wgMessageCache;
if ( isset( $wgMessageCache ) ) {
- $string = $wgMessageCache->transform( $string, !$forContent );
+ $string = $wgMessageCache->transform( $string,
+ !$forContent,
+ is_object( $langCode ) ? $langCode : null );
}
}
- if ( in_array('escape', $options) ) {
+ if ( in_array('escape', $options, true ) ) {
$string = htmlspecialchars ( $string );
- } elseif ( in_array( 'escapenoentities', $options ) ) {
- $string = htmlspecialchars( $string );
- $string = str_replace( '&amp;', '&', $string );
- $string = Sanitizer::normalizeCharReferences( $string );
+ } elseif ( in_array( 'escapenoentities', $options, true ) ) {
+ $string = Sanitizer::escapeHtmlAllowEntities( $string );
}
- if( in_array('replaceafter', $options) ) {
+ if( in_array('replaceafter', $options, true ) ) {
$string = wfMsgReplaceArgs( $string, $args );
}
@@ -707,18 +816,25 @@ function wfDebugDieBacktrace( $msg = '' ) {
* @return string
*/
function wfHostname() {
- if ( function_exists( 'posix_uname' ) ) {
- // This function not present on Windows
- $uname = @posix_uname();
- } else {
- $uname = false;
- }
- if( is_array( $uname ) && isset( $uname['nodename'] ) ) {
- return $uname['nodename'];
- } else {
- # This may be a virtual server.
- return $_SERVER['SERVER_NAME'];
+ static $host;
+ if ( is_null( $host ) ) {
+ if ( function_exists( 'posix_uname' ) ) {
+ // This function not present on Windows
+ $uname = @posix_uname();
+ } else {
+ $uname = false;
+ }
+ if( is_array( $uname ) && isset( $uname['nodename'] ) ) {
+ $host = $uname['nodename'];
+ } elseif ( getenv( 'COMPUTERNAME' ) ) {
+ # Windows computer name
+ $host = getenv( 'COMPUTERNAME' );
+ } else {
+ # This may be a virtual server.
+ $host = $_SERVER['SERVER_NAME'];
+ }
}
+ return $host;
}
/**
@@ -929,7 +1045,7 @@ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) {
*/
function wfEscapeWikiText( $text ) {
$text = str_replace(
- array( '[', '|', ']', '\'', 'ISBN ', 'RFC ', '://', "\n=", '{{' ),
+ array( '[', '|', ']', '\'', 'ISBN ', 'RFC ', '://', "\n=", '{{' ), # }}
array( '&#91;', '&#124;', '&#93;', '&#39;', 'ISBN&#32;', 'RFC&#32;', '&#58;//', "\n&#61;", '&#123;&#123;' ),
htmlspecialchars($text) );
return $text;
@@ -1029,6 +1145,34 @@ function wfArrayToCGI( $array1, $array2 = NULL )
}
/**
+ * This is the logical opposite of wfArrayToCGI(): it accepts a query string as
+ * its argument and returns the same string in array form. This allows compa-
+ * tibility with legacy functions that accept raw query strings instead of nice
+ * arrays. Of course, keys and values are urldecode()d. Don't try passing in-
+ * valid query strings, or it will explode.
+ *
+ * @param $query string Query string
+ * @return array Array version of input
+ */
+function wfCgiToArray( $query ) {
+ if( isset( $query[0] ) and $query[0] == '?' ) {
+ $query = substr( $query, 1 );
+ }
+ $bits = explode( '&', $query );
+ $ret = array();
+ foreach( $bits as $bit ) {
+ if( $bit === '' ) {
+ continue;
+ }
+ list( $key, $value ) = explode( '=', $bit );
+ $key = urldecode( $key );
+ $value = urldecode( $value );
+ $ret[$key] = $value;
+ }
+ return $ret;
+}
+
+/**
* Append a query string to an existing URL, which may or may not already
* have query string parameters already. If so, they will be combined.
*
@@ -1132,7 +1276,7 @@ function wfMerge( $old, $mine, $yours, &$result ){
# This check may also protect against code injection in
# case of broken installations.
- if(! file_exists( $wgDiff3 ) ){
+ if( !$wgDiff3 || !file_exists( $wgDiff3 ) ) {
wfDebug( "diff3 not found\n" );
return false;
}
@@ -1246,7 +1390,10 @@ function wfDiff( $before, $after, $params = '-u' ) {
}
/**
- * @todo document
+ * A wrapper around the PHP function var_export().
+ * Either print it or add it to the regular output ($wgOut).
+ *
+ * @param $var A PHP variable to dump.
*/
function wfVarDump( $var ) {
global $wgOut;
@@ -1322,6 +1469,7 @@ function wfResetOutputBuffers( $resetGzipEncoding=true ) {
// Reset the 'Content-Encoding' field set by this handler
// so we can start fresh.
header( 'Content-Encoding:' );
+ break;
}
}
}
@@ -1568,11 +1716,11 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) {
# TS_ORACLE
$uts = strtotime(preg_replace('/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3",
str_replace("+00:00", "UTC", $ts)));
- } elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/', $ts, $da)) {
+ } elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.*\d*)?Z$/', $ts, $da)) {
# TS_ISO_8601
- } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)[\+\- ](\d\d)$/',$ts,$da)) {
+ } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d*[\+\- ](\d\d)$/',$ts,$da)) {
# TS_POSTGRES
- } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/',$ts,$da)) {
+ } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d* GMT$/',$ts,$da)) {
# TS_POSTGRES
} else {
# Bogus value; fall back to the epoch...
@@ -1648,7 +1796,7 @@ function swap( &$x, &$y ) {
}
function wfGetCachedNotice( $name ) {
- global $wgOut, $parserMemc;
+ global $wgOut, $wgRenderHashAppend, $parserMemc;
$fname = 'wfGetCachedNotice';
wfProfileIn( $fname );
@@ -1670,7 +1818,9 @@ function wfGetCachedNotice( $name ) {
}
}
- $cachedNotice = $parserMemc->get( wfMemcKey( $name ) );
+ // Use the extra hash appender to let eg SSL variants separately cache.
+ $key = wfMemcKey( $name . $wgRenderHashAppend );
+ $cachedNotice = $parserMemc->get( $key );
if( is_array( $cachedNotice ) ) {
if( md5( $notice ) == $cachedNotice['hash'] ) {
$notice = $cachedNotice['html'];
@@ -1684,7 +1834,7 @@ function wfGetCachedNotice( $name ) {
if( $needParse ) {
if( is_object( $wgOut ) ) {
$parsed = $wgOut->parse( $notice );
- $parserMemc->set( wfMemcKey( $name ), array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 );
+ $parserMemc->set( $key, array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 );
$notice = $parsed;
} else {
wfDebug( 'wfGetCachedNotice called for ' . $name . ' with no $wgOut available' );
@@ -1777,69 +1927,20 @@ function wfTempDir() {
/**
* Make directory, and make all parent directories if they don't exist
*
- * @param string $fullDir Full path to directory to create
+ * @param string $dir Full path to directory to create
* @param int $mode Chmod value to use, default is $wgDirectoryMode
* @return bool
*/
-function wfMkdirParents( $fullDir, $mode = null ) {
+function wfMkdirParents( $dir, $mode = null ) {
global $wgDirectoryMode;
- if( strval( $fullDir ) === '' )
- return true;
- if( file_exists( $fullDir ) )
- return true;
- // If not defined or isn't an int, set to default
- if ( is_null( $mode ) ) {
- $mode = $wgDirectoryMode;
- }
-
-
- # 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( strval( $dir ) === '' || file_exists( $dir ) )
return true;
- } elseif ( $currentDir === false ) {
- # Went all the way back to root and it apparently doesn't exist
- wfDebugLog( 'mkdir', "Root doesn't exist?\n" );
- return false;
- }
- # Now go forward creating directories
- $createList = array_reverse( $createList );
- # Is the parent directory writable?
- if ( $currentDir === '' ) {
- $currentDir = '/';
- }
- if ( !is_writable( $currentDir ) ) {
- wfDebugLog( 'mkdir', "Not writable: $currentDir\n" );
- return false;
- }
+ if ( is_null( $mode ) )
+ $mode = $wgDirectoryMode;
- 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( $dir, $mode, true ); // PHP5 <3
}
/**
@@ -1998,7 +2099,7 @@ function wfIniGetBool( $setting ) {
* @return collected stdout as a string (trailing newlines stripped)
*/
function wfShellExec( $cmd, &$retval=null ) {
- global $IP, $wgMaxShellMemory, $wgMaxShellFileSize;
+ global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, $wgMaxShellTime;
if( wfIniGetBool( 'safe_mode' ) ) {
wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" );
@@ -2008,7 +2109,7 @@ function wfShellExec( $cmd, &$retval=null ) {
wfInitShellLocale();
if ( php_uname( 's' ) == 'Linux' ) {
- $time = intval( ini_get( 'max_execution_time' ) );
+ $time = intval( $wgMaxShellTime );
$mem = intval( $wgMaxShellMemory );
$filesize = intval( $wgMaxShellFileSize );
@@ -2030,6 +2131,10 @@ function wfShellExec( $cmd, &$retval=null ) {
passthru( $cmd, $retval );
$output = ob_get_contents();
ob_end_clean();
+
+ if ( $retval == 127 ) {
+ wfDebugLog( 'exec', "Possibly missing executable file: $cmd\n" );
+ }
return $output;
}
@@ -2167,28 +2272,51 @@ function wfRelativePath( $path, $from ) {
}
/**
- * array_merge() does awful things with "numeric" indexes, including
- * string indexes when happen to look like integers. When we want
- * to merge arrays with arbitrary string indexes, we don't want our
- * arrays to be randomly corrupted just because some of them consist
- * of numbers.
- *
- * Fuck you, PHP. Fuck you in the ear!
+ * Backwards array plus for people who haven't bothered to read the PHP manual
+ * XXX: will not darn your socks for you.
*
* @param array $array1, [$array2, [...]]
* @return array
*/
function wfArrayMerge( $array1/* ... */ ) {
- $out = $array1;
- for( $i = 1; $i < func_num_args(); $i++ ) {
- foreach( func_get_arg( $i ) as $key => $value ) {
- $out[$key] = $value;
- }
+ $args = func_get_args();
+ $args = array_reverse( $args, true );
+ $out = array();
+ foreach ( $args as $arg ) {
+ $out += $arg;
}
return $out;
}
/**
+ * Merge arrays in the style of getUserPermissionsErrors, with duplicate removal
+ * e.g.
+ * wfMergeErrorArrays(
+ * array( array( 'x' ) ),
+ * array( array( 'x', '2' ) ),
+ * array( array( 'x' ) ),
+ * array( array( 'y') )
+ * );
+ * returns:
+ * array(
+ * array( 'x', '2' ),
+ * array( 'x' ),
+ * array( 'y' )
+ * )
+ */
+function wfMergeErrorArrays(/*...*/) {
+ $args = func_get_args();
+ $out = array();
+ foreach ( $args as $errors ) {
+ foreach ( $errors as $params ) {
+ $spec = implode( "\t", $params );
+ $out[$spec] = $params;
+ }
+ }
+ return array_values( $out );
+}
+
+/**
* Make a URL index, appropriate for the el_index field of externallinks.
*/
function wfMakeUrlIndex( $url ) {
@@ -2560,7 +2688,7 @@ function wfSplitWikiID( $wiki ) {
* will always return the same object, unless the underlying connection or load
* balancer is manually destroyed.
*/
-function &wfGetDB( $db = DB_LAST, $groups = array(), $wiki = false ) {
+function &wfGetDB( $db, $groups = array(), $wiki = false ) {
return wfGetLB( $wiki )->getConnection( $db, $groups, $wiki );
}
@@ -2590,10 +2718,15 @@ function &wfGetLBFactory() {
* current version. An image object will be returned which
* was created at the specified time.
* @param mixed $flags FileRepo::FIND_ flags
+ * @param boolean $bypass Bypass the file cache even if it could be used
* @return File, or false if the file does not exist
*/
-function wfFindFile( $title, $time = false, $flags = 0 ) {
- return RepoGroup::singleton()->findFile( $title, $time, $flags );
+function wfFindFile( $title, $time = false, $flags = 0, $bypass = false ) {
+ if( !$time && !$flags && !$bypass ) {
+ return FileCache::singleton()->findFile( $title );
+ } else {
+ return RepoGroup::singleton()->findFile( $title, $time, $flags );
+ }
}
/**
@@ -2646,6 +2779,8 @@ function wfBoolToStr( $value ) {
* @param string $extensionName Name of extension to load messages from\for.
* @param string $langcode Language to load messages for, or false for default
* behvaiour (en, content language and user language).
+ * @since r24808 (v1.11) Using this method of loading extension messages will not work
+ * on MediaWiki prior to that
*/
function wfLoadExtensionMessages( $extensionName, $langcode = false ) {
global $wgExtensionMessagesFiles, $wgMessageCache, $wgLang, $wgContLang;
diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php
index 1f250214..402102ea 100644
--- a/includes/HTMLCacheUpdate.php
+++ b/includes/HTMLCacheUpdate.php
@@ -37,7 +37,7 @@ class HTMLCacheUpdate
$this->mRowsPerQuery = $wgUpdateRowsPerQuery;
}
- function doUpdate() {
+ public function doUpdate() {
# Fetch the IDs
$cond = $this->getToCondition();
$dbr = wfGetDB( DB_SLAVE );
@@ -50,16 +50,17 @@ class HTMLCacheUpdate
$this->invalidateIDs( $res );
}
}
+ wfRunHooks( 'HTMLCacheUpdate::doUpdate', array($this->mTitle) );
}
- function insertJobs( ResultWrapper $res ) {
+ protected function insertJobs( ResultWrapper $res ) {
$numRows = $res->numRows();
$numBatches = ceil( $numRows / $this->mRowsPerJob );
$realBatchSize = $numRows / $numBatches;
$start = false;
$jobs = array();
do {
- for ( $i = 0; $i < $realBatchSize - 1; $i++ ) {
+ for ( $i = 0; $i <= $realBatchSize - 1; $i++ ) {
$row = $res->fetchRow();
if ( $row ) {
$id = $row[0];
@@ -82,17 +83,13 @@ class HTMLCacheUpdate
Job::batchInsert( $jobs );
}
- function getPrefix() {
+ protected function getPrefix() {
static $prefixes = array(
'pagelinks' => 'pl',
'imagelinks' => 'il',
'categorylinks' => 'cl',
'templatelinks' => 'tl',
'redirect' => 'rd',
-
- # Not needed
- # 'externallinks' => 'el',
- # 'langlinks' => 'll'
);
if ( is_null( $this->mPrefix ) ) {
@@ -104,11 +101,11 @@ class HTMLCacheUpdate
return $this->mPrefix;
}
- function getFromField() {
+ public function getFromField() {
return $this->getPrefix() . '_from';
}
- function getToCondition() {
+ public function getToCondition() {
$prefix = $this->getPrefix();
switch ( $this->mTable ) {
case 'pagelinks':
@@ -129,7 +126,7 @@ class HTMLCacheUpdate
/**
* Invalidate a set of IDs, right now
*/
- function invalidateIDs( ResultWrapper $res ) {
+ public function invalidateIDs( ResultWrapper $res ) {
global $wgUseFileCache, $wgUseSquid;
if ( $res->numRows() == 0 ) {
@@ -175,8 +172,7 @@ class HTMLCacheUpdate
# Update file cache
if ( $wgUseFileCache ) {
foreach ( $titles as $title ) {
- $cm = new HTMLFileCache($title);
- @unlink($cm->fileCacheName());
+ HTMLFileCache::clearFileCache( $title );
}
}
}
@@ -185,7 +181,9 @@ class HTMLCacheUpdate
}
/**
- * @todo document (e.g. one-sentence top-level class description).
+ * Job wrapper for HTMLCacheUpdate. Gets run whenever a related
+ * job gets called from the queue.
+ *
* @ingroup JobQueue
*/
class HTMLCacheUpdateJob extends Job {
@@ -204,7 +202,7 @@ class HTMLCacheUpdateJob extends Job {
$this->end = $params['end'];
}
- function run() {
+ public function run() {
$update = new HTMLCacheUpdate( $this->title, $this->table );
$fromField = $update->getFromField();
diff --git a/includes/HTMLFileCache.php b/includes/HTMLFileCache.php
index ba2196eb..e267962c 100644
--- a/includes/HTMLFileCache.php
+++ b/includes/HTMLFileCache.php
@@ -20,25 +20,29 @@
* @ingroup Cache
*/
class HTMLFileCache {
- var $mTitle, $mFileCache;
+ var $mTitle, $mFileCache, $mType;
- function HTMLFileCache( &$title ) {
- $this->mTitle =& $title;
- $this->mFileCache = '';
+ public function __construct( &$title, $type = 'view' ) {
+ $this->mTitle = $title;
+ $this->mType = ($type == 'raw' || $type == 'view' ) ? $type : false;
+ $this->fileCacheName(); // init name
}
- function fileCacheName() {
- global $wgFileCacheDirectory;
+ public function fileCacheName() {
if( !$this->mFileCache ) {
+ global $wgFileCacheDirectory, $wgRequest;
+ # Store raw pages (like CSS hits) elsewhere
+ $subdir = ($this->mType === 'raw') ? 'raw/' : '';
$key = $this->mTitle->getPrefixedDbkey();
$hash = md5( $key );
+ # Avoid extension confusion
$key = str_replace( '.', '%2E', urlencode( $key ) );
-
+
$hash1 = substr( $hash, 0, 1 );
$hash2 = substr( $hash, 0, 2 );
- $this->mFileCache = "{$wgFileCacheDirectory}/{$hash1}/{$hash2}/{$key}.html";
+ $this->mFileCache = "{$wgFileCacheDirectory}/{$subdir}{$hash1}/{$hash2}/{$key}.html";
- if($this->useGzip())
+ if( $this->useGzip() )
$this->mFileCache .= '.gz';
wfDebug( " fileCacheName() - {$this->mFileCache}\n" );
@@ -46,38 +50,72 @@ class HTMLFileCache {
return $this->mFileCache;
}
- function isFileCached() {
+ public function isFileCached() {
+ if( $this->mType === false ) return false;
return file_exists( $this->fileCacheName() );
}
- function fileCacheTime() {
+ public function fileCacheTime() {
return wfTimestamp( TS_MW, filemtime( $this->fileCacheName() ) );
}
+
+ /**
+ * Check if pages can be cached for this request/user
+ * @return bool
+ */
+ public static function useFileCache() {
+ global $wgUser, $wgUseFileCache, $wgShowIPinHeader, $wgRequest, $wgLang, $wgContLang;
+ if( !$wgUseFileCache ) return false;
+ // Get all query values
+ $queryVals = $wgRequest->getValues();
+ foreach( $queryVals as $query => $val ) {
+ if( $query == 'title' || $query == 'curid' ) continue;
+ // Normal page view in query form can have action=view.
+ // Raw hits for pages also stored, like .css pages for example.
+ else if( $query == 'action' && ($val == 'view' || $val == 'raw') ) continue;
+ else if( $query == 'usemsgcache' && $val == 'yes' ) continue;
+ // Below are header setting params
+ else if( $query == 'maxage' || $query == 'smaxage' || $query == 'ctype' || $query == 'gen' )
+ continue;
+ else
+ return false;
+ }
+ // Check for non-standard user language; this covers uselang,
+ // and extensions for auto-detecting user language.
+ $ulang = $wgLang->getCode();
+ $clang = $wgContLang->getCode();
+ // Check that there are no other sources of variation
+ return !$wgShowIPinHeader && !$wgUser->getId() && !$wgUser->getNewtalk() && $ulang == $clang;
+ }
- function isFileCacheGood( $timestamp ) {
+ /*
+ * Check if up to date cache file exists
+ * @param $timestamp string
+ */
+ public function isFileCacheGood( $timestamp = '' ) {
global $wgCacheEpoch;
if( !$this->isFileCached() ) return false;
+ if( !$timestamp ) return true; // should be invalidated on change
$cachetime = $this->fileCacheTime();
- $good = (( $timestamp <= $cachetime ) &&
- ( $wgCacheEpoch <= $cachetime ));
+ $good = $timestamp <= $cachetime && $wgCacheEpoch <= $cachetime;
- wfDebug(" isFileCacheGood() - cachetime $cachetime, touched {$timestamp} epoch {$wgCacheEpoch}, good $good\n");
+ wfDebug(" isFileCacheGood() - cachetime $cachetime, touched '{$timestamp}' epoch {$wgCacheEpoch}, good $good\n");
return $good;
}
- function useGzip() {
+ public function useGzip() {
global $wgUseGzip;
return $wgUseGzip;
}
/* In handy string packages */
- function fetchRawText() {
+ public function fetchRawText() {
return file_get_contents( $this->fileCacheName() );
}
- function fetchPageText() {
+ public function fetchPageText() {
if( $this->useGzip() ) {
/* Why is there no gzfile_get_contents() or gzdecode()? */
return implode( '', gzfile( $this->fileCacheName() ) );
@@ -87,15 +125,18 @@ class HTMLFileCache {
}
/* Working directory to/from output */
- function loadFromFileCache() {
+ public function loadFromFileCache() {
global $wgOut, $wgMimeType, $wgOutputEncoding, $wgContLanguageCode;
wfDebug(" loadFromFileCache()\n");
- $filename=$this->fileCacheName();
- $wgOut->sendCacheControl();
-
- header( "Content-type: $wgMimeType; charset={$wgOutputEncoding}" );
- header( "Content-language: $wgContLanguageCode" );
+ $filename = $this->fileCacheName();
+ // Raw pages should handle cache control on their own,
+ // even when using file cache. This reduces hits from clients.
+ if( $this->mType !== 'raw' ) {
+ $wgOut->sendCacheControl();
+ header( "Content-Type: $wgMimeType; charset={$wgOutputEncoding}" );
+ header( "Content-Language: $wgContLanguageCode" );
+ }
if( $this->useGzip() ) {
if( wfClientAcceptsGzip() ) {
@@ -109,18 +150,22 @@ class HTMLFileCache {
readfile( $filename );
}
- function checkCacheDirs() {
+ protected function checkCacheDirs() {
$filename = $this->fileCacheName();
- $mydir2=substr($filename,0,strrpos($filename,'/')); # subdirectory level 2
- $mydir1=substr($mydir2,0,strrpos($mydir2,'/')); # subdirectory level 1
+ $mydir2 = substr($filename,0,strrpos($filename,'/')); # subdirectory level 2
+ $mydir1 = substr($mydir2,0,strrpos($mydir2,'/')); # subdirectory level 1
- if(!file_exists($mydir1)) { mkdir($mydir1,0775); } # create if necessary
- if(!file_exists($mydir2)) { mkdir($mydir2,0775); }
+ wfMkdirParents( $mydir1 );
+ wfMkdirParents( $mydir2 );
}
- function saveToFileCache( $origtext ) {
+ public function saveToFileCache( $origtext ) {
+ global $wgUseFileCache;
+ if( !$wgUseFileCache ) {
+ return $origtext; // return to output
+ }
$text = $origtext;
- if(strcmp($text,'') == 0) return '';
+ if( strcmp($text,'') == 0 ) return '';
wfDebug(" saveToFileCache()\n", false);
@@ -155,4 +200,13 @@ class HTMLFileCache {
return $text;
}
+ public static function clearFileCache( $title ) {
+ global $wgUseFileCache;
+ if( !$wgUseFileCache ) return false;
+ $fc = new self( $title, 'view' );
+ @unlink( $fc->fileCacheName() );
+ $fc = new self( $title, 'raw' );
+ @unlink( $fc->fileCacheName() );
+ return true;
+ }
}
diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php
index 3772926d..664ceb4f 100644
--- a/includes/HistoryBlob.php
+++ b/includes/HistoryBlob.php
@@ -1,41 +1,33 @@
<?php
/**
- * Pure virtual parent
- * @todo document (needs a one-sentence top-level class description, that answers the question: "what is a HistoryBlob?")
+ * Base class for general text storage via the "object" flag in old_flags, or
+ * two-part external storage URLs. Used for represent efficient concatenated
+ * storage, and migration-related pointer objects.
*/
interface HistoryBlob
{
/**
- * setMeta and getMeta currently aren't used for anything, I just thought
- * they might be useful in the future.
- * @param $meta String: a single string.
- */
- public function setMeta( $meta );
-
- /**
- * setMeta and getMeta currently aren't used for anything, I just thought
- * they might be useful in the future.
- * Gets the meta-value
- */
- public function getMeta();
-
- /**
* Adds an item of text, returns a stub object which points to the item.
* You must call setLocation() on the stub object before storing it to the
* database
+ * Returns the key for getItem()
*/
public function addItem( $text );
/**
- * Get item by hash
+ * Get item by key, or false if the key is not present
*/
- public function getItem( $hash );
+ public function getItem( $key );
- # Set the "default text"
- # This concept is an odd property of the current DB schema, whereby each text item has a revision
- # associated with it. The default text is the text of the associated revision. There may, however,
- # be other revisions in the same object
+ /**
+ * Set the "default text"
+ * This concept is an odd property of the current DB schema, whereby each text item has a revision
+ * associated with it. The default text is the text of the associated revision. There may, however,
+ * be other revisions in the same object.
+ *
+ * Default text is not required for two-part external storage URLs.
+ */
public function setText( $text );
/**
@@ -45,13 +37,15 @@ interface HistoryBlob
}
/**
- * The real object
- * @todo document (needs one-sentence top-level class description + function descriptions).
+ * Concatenated gzip (CGZ) storage
+ * Improves compression ratio by concatenating like objects before gzipping
*/
class ConcatenatedGzipHistoryBlob implements HistoryBlob
{
public $mVersion = 0, $mCompressed = false, $mItems = array(), $mDefaultHash = '';
- public $mFast = 0, $mSize = 0;
+ public $mSize = 0;
+ public $mMaxSize = 10000000;
+ public $mMaxCount = 100;
/** Constructor */
public function ConcatenatedGzipHistoryBlob() {
@@ -60,34 +54,16 @@ class ConcatenatedGzipHistoryBlob implements HistoryBlob
}
}
- #
- # HistoryBlob implementation:
- #
-
- /** @todo document */
- public function setMeta( $metaData ) {
- $this->uncompress();
- $this->mItems['meta'] = $metaData;
- }
-
- /** @todo document */
- public function getMeta() {
- $this->uncompress();
- return $this->mItems['meta'];
- }
-
- /** @todo document */
public function addItem( $text ) {
$this->uncompress();
$hash = md5( $text );
- $this->mItems[$hash] = $text;
- $this->mSize += strlen( $text );
-
- $stub = new HistoryBlobStub( $hash );
- return $stub;
+ if ( !isset( $this->mItems[$hash] ) ) {
+ $this->mItems[$hash] = $text;
+ $this->mSize += strlen( $text );
+ }
+ return $hash;
}
- /** @todo document */
public function getItem( $hash ) {
$this->uncompress();
if ( array_key_exists( $hash, $this->mItems ) ) {
@@ -97,29 +73,27 @@ class ConcatenatedGzipHistoryBlob implements HistoryBlob
}
}
- /** @todo document */
public function setText( $text ) {
$this->uncompress();
- $stub = $this->addItem( $text );
- $this->mDefaultHash = $stub->mHash;
+ $this->mDefaultHash = $this->addItem( $text );
}
- /** @todo document */
public function getText() {
$this->uncompress();
return $this->getItem( $this->mDefaultHash );
}
- # HistoryBlob implemented.
-
-
- /** @todo document */
+ /**
+ * Remove an item
+ */
public function removeItem( $hash ) {
$this->mSize -= strlen( $this->mItems[$hash] );
unset( $this->mItems[$hash] );
}
- /** @todo document */
+ /**
+ * Compress the bulk data in the object
+ */
public function compress() {
if ( !$this->mCompressed ) {
$this->mItems = gzdeflate( serialize( $this->mItems ) );
@@ -127,7 +101,9 @@ class ConcatenatedGzipHistoryBlob implements HistoryBlob
}
}
- /** @todo document */
+ /**
+ * Uncompress bulk data
+ */
public function uncompress() {
if ( $this->mCompressed ) {
$this->mItems = unserialize( gzinflate( $this->mItems ) );
@@ -136,39 +112,22 @@ class ConcatenatedGzipHistoryBlob implements HistoryBlob
}
- /** @todo document */
function __sleep() {
$this->compress();
return array( 'mVersion', 'mCompressed', 'mItems', 'mDefaultHash' );
}
- /** @todo document */
function __wakeup() {
$this->uncompress();
}
/**
- * Determines if this object is happy
+ * Helper function for compression jobs
+ * Returns true until the object is "full" and ready to be committed
*/
- public function isHappy( $maxFactor, $factorThreshold ) {
- if ( count( $this->mItems ) == 0 ) {
- return true;
- }
- if ( !$this->mFast ) {
- $this->uncompress();
- $record = serialize( $this->mItems );
- $size = strlen( $record );
- $avgUncompressed = $size / count( $this->mItems );
- $compressed = strlen( gzdeflate( $record ) );
-
- if ( $compressed < $factorThreshold * 1024 ) {
- return true;
- } else {
- return $avgUncompressed * $maxFactor < $compressed;
- }
- } else {
- return count( $this->mItems ) <= 10;
- }
+ public function isHappy() {
+ return $this->mSize < $this->mMaxSize
+ && count( $this->mItems ) < $this->mMaxCount;
}
}
@@ -184,12 +143,15 @@ $wgBlobCache = array();
/**
- * @todo document (needs one-sentence top-level class description + some function descriptions).
+ * Pointer object for an item within a CGZ blob stored in the text table.
*/
class HistoryBlobStub {
var $mOldId, $mHash, $mRef;
- /** @todo document */
+ /**
+ * @param string $hash The content hash of the text
+ * @param integer $oldid The old_id for the CGZ object
+ */
function HistoryBlobStub( $hash = '', $oldid = 0 ) {
$this->mHash = $hash;
}
@@ -216,7 +178,6 @@ class HistoryBlobStub {
return $this->mRef;
}
- /** @todo document */
function getText() {
$fname = 'HistoryBlobStub::getText';
global $wgBlobCache;
@@ -264,7 +225,9 @@ class HistoryBlobStub {
return $obj->getItem( $this->mHash );
}
- /** @todo document */
+ /**
+ * Get the content hash
+ */
function getHash() {
return $this->mHash;
}
@@ -282,7 +245,9 @@ class HistoryBlobStub {
class HistoryBlobCurStub {
var $mCurId;
- /** @todo document */
+ /**
+ * @param integer $curid The cur_id pointed to
+ */
function HistoryBlobCurStub( $curid = 0 ) {
$this->mCurId = $curid;
}
@@ -295,7 +260,6 @@ class HistoryBlobCurStub {
$this->mCurId = $id;
}
- /** @todo document */
function getText() {
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow( 'cur', array( 'cur_text' ), array( 'cur_id' => $this->mCurId ) );
@@ -305,3 +269,311 @@ class HistoryBlobCurStub {
return $row->cur_text;
}
}
+
+/**
+ * Diff-based history compression
+ * Requires xdiff 1.5+ and zlib
+ */
+class DiffHistoryBlob implements HistoryBlob {
+ /** Uncompressed item cache */
+ var $mItems = array();
+
+ /** Total uncompressed size */
+ var $mSize = 0;
+
+ /**
+ * Array of diffs. If a diff D from A to B is notated D = B - A, and Z is
+ * an empty string:
+ *
+ * { item[map[i]] - item[map[i-1]] where i > 0
+ * diff[i] = {
+ * { item[map[i]] - Z where i = 0
+ */
+ var $mDiffs;
+
+ /** The diff map, see above */
+ var $mDiffMap;
+
+ /**
+ * The key for getText()
+ */
+ var $mDefaultKey;
+
+ /**
+ * Compressed storage
+ */
+ var $mCompressed;
+
+ /**
+ * True if the object is locked against further writes
+ */
+ var $mFrozen = false;
+
+ /**
+ * The maximum uncompressed size before the object becomes sad
+ * Should be less than max_allowed_packet
+ */
+ var $mMaxSize = 10000000;
+
+ /**
+ * The maximum number of text items before the object becomes sad
+ */
+ var $mMaxCount = 100;
+
+ /** Constants from xdiff.h */
+ const XDL_BDOP_INS = 1;
+ const XDL_BDOP_CPY = 2;
+ const XDL_BDOP_INSB = 3;
+
+ function __construct() {
+ if ( !function_exists( 'gzdeflate' ) ) {
+ throw new MWException( "Need zlib support to read or write DiffHistoryBlob\n" );
+ }
+ }
+
+ function addItem( $text ) {
+ if ( $this->mFrozen ) {
+ throw new MWException( __METHOD__.": Cannot add more items after sleep/wakeup" );
+ }
+
+ $this->mItems[] = $text;
+ $this->mSize += strlen( $text );
+ $this->mDiffs = null; // later
+ return count( $this->mItems ) - 1;
+ }
+
+ function getItem( $key ) {
+ return $this->mItems[$key];
+ }
+
+ function setText( $text ) {
+ $this->mDefaultKey = $this->addItem( $text );
+ }
+
+ function getText() {
+ return $this->getItem( $this->mDefaultKey );
+ }
+
+ function compress() {
+ if ( !function_exists( 'xdiff_string_rabdiff' ) ){
+ throw new MWException( "Need xdiff 1.5+ support to write DiffHistoryBlob\n" );
+ }
+ if ( isset( $this->mDiffs ) ) {
+ // Already compressed
+ return;
+ }
+ if ( !count( $this->mItems ) ) {
+ // Empty
+ return;
+ }
+
+ // Create two diff sequences: one for main text and one for small text
+ $sequences = array(
+ 'small' => array(
+ 'tail' => '',
+ 'diffs' => array(),
+ 'map' => array(),
+ ),
+ 'main' => array(
+ 'tail' => '',
+ 'diffs' => array(),
+ 'map' => array(),
+ ),
+ );
+ $smallFactor = 0.5;
+
+ for ( $i = 0; $i < count( $this->mItems ); $i++ ) {
+ $text = $this->mItems[$i];
+ if ( $i == 0 ) {
+ $seqName = 'main';
+ } else {
+ $mainTail = $sequences['main']['tail'];
+ if ( strlen( $text ) < strlen( $mainTail ) * $smallFactor ) {
+ $seqName = 'small';
+ } else {
+ $seqName = 'main';
+ }
+ }
+ $seq =& $sequences[$seqName];
+ $tail = $seq['tail'];
+ $diff = $this->diff( $tail, $text );
+ $seq['diffs'][] = $diff;
+ $seq['map'][] = $i;
+ $seq['tail'] = $text;
+ }
+ unset( $seq ); // unlink dangerous alias
+
+ // Knit the sequences together
+ $tail = '';
+ $this->mDiffs = array();
+ $this->mDiffMap = array();
+ foreach ( $sequences as $seq ) {
+ if ( !count( $seq['diffs'] ) ) {
+ continue;
+ }
+ if ( $tail === '' ) {
+ $this->mDiffs[] = $seq['diffs'][0];
+ } else {
+ $head = $this->patch( '', $seq['diffs'][0] );
+ $this->mDiffs[] = $this->diff( $tail, $head );
+ }
+ $this->mDiffMap[] = $seq['map'][0];
+ for ( $i = 1; $i < count( $seq['diffs'] ); $i++ ) {
+ $this->mDiffs[] = $seq['diffs'][$i];
+ $this->mDiffMap[] = $seq['map'][$i];
+ }
+ $tail = $seq['tail'];
+ }
+ }
+
+ function diff( $t1, $t2 ) {
+ # Need to do a null concatenation with warnings off, due to bugs in the current version of xdiff
+ # "String is not zero-terminated"
+ wfSuppressWarnings();
+ $diff = xdiff_string_rabdiff( $t1, $t2 ) . '';
+ wfRestoreWarnings();
+ return $diff;
+ }
+
+ function patch( $base, $diff ) {
+ if ( function_exists( 'xdiff_string_bpatch' ) ) {
+ wfSuppressWarnings();
+ $text = xdiff_string_bpatch( $base, $diff ) . '';
+ wfRestoreWarnings();
+ return $text;
+ }
+
+ # Pure PHP implementation
+
+ $header = unpack( 'Vofp/Vcsize', substr( $diff, 0, 8 ) );
+
+ # Check the checksum if mhash is available
+ if ( extension_loaded( 'mhash' ) ) {
+ $ofp = mhash( MHASH_ADLER32, $base );
+ if ( $ofp !== substr( $diff, 0, 4 ) ) {
+ wfDebug( __METHOD__. ": incorrect base checksum\n" );
+ return false;
+ }
+ }
+ if ( $header['csize'] != strlen( $base ) ) {
+ wfDebug( __METHOD__. ": incorrect base length\n" );
+ return false;
+ }
+
+ $p = 8;
+ $out = '';
+ while ( $p < strlen( $diff ) ) {
+ $x = unpack( 'Cop', substr( $diff, $p, 1 ) );
+ $op = $x['op'];
+ ++$p;
+ switch ( $op ) {
+ case self::XDL_BDOP_INS:
+ $x = unpack( 'Csize', substr( $diff, $p, 1 ) );
+ $p++;
+ $out .= substr( $diff, $p, $x['size'] );
+ $p += $x['size'];
+ break;
+ case self::XDL_BDOP_INSB:
+ $x = unpack( 'Vcsize', substr( $diff, $p, 4 ) );
+ $p += 4;
+ $out .= substr( $diff, $p, $x['csize'] );
+ $p += $x['csize'];
+ break;
+ case self::XDL_BDOP_CPY:
+ $x = unpack( 'Voff/Vcsize', substr( $diff, $p, 8 ) );
+ $p += 8;
+ $out .= substr( $base, $x['off'], $x['csize'] );
+ break;
+ default:
+ wfDebug( __METHOD__.": invalid op\n" );
+ return false;
+ }
+ }
+ return $out;
+ }
+
+ function uncompress() {
+ if ( !$this->mDiffs ) {
+ return;
+ }
+ $tail = '';
+ for ( $diffKey = 0; $diffKey < count( $this->mDiffs ); $diffKey++ ) {
+ $textKey = $this->mDiffMap[$diffKey];
+ $text = $this->patch( $tail, $this->mDiffs[$diffKey] );
+ $this->mItems[$textKey] = $text;
+ $tail = $text;
+ }
+ }
+
+ function __sleep() {
+ $this->compress();
+ if ( !count( $this->mItems ) ) {
+ // Empty object
+ $info = false;
+ } else {
+ // Take forward differences to improve the compression ratio for sequences
+ $map = '';
+ $prev = 0;
+ foreach ( $this->mDiffMap as $i ) {
+ if ( $map !== '' ) {
+ $map .= ',';
+ }
+ $map .= $i - $prev;
+ $prev = $i;
+ }
+ $info = array(
+ 'diffs' => $this->mDiffs,
+ 'map' => $map
+ );
+ }
+ if ( isset( $this->mDefaultKey ) ) {
+ $info['default'] = $this->mDefaultKey;
+ }
+ $this->mCompressed = gzdeflate( serialize( $info ) );
+ return array( 'mCompressed' );
+ }
+
+ function __wakeup() {
+ // addItem() doesn't work if mItems is partially filled from mDiffs
+ $this->mFrozen = true;
+ $info = unserialize( gzinflate( $this->mCompressed ) );
+ unset( $this->mCompressed );
+
+ if ( !$info ) {
+ // Empty object
+ return;
+ }
+
+ if ( isset( $info['default'] ) ) {
+ $this->mDefaultKey = $info['default'];
+ }
+ $this->mDiffs = $info['diffs'];
+ if ( isset( $info['base'] ) ) {
+ // Old format
+ $this->mDiffMap = range( 0, count( $this->mDiffs ) - 1 );
+ array_unshift( $this->mDiffs,
+ pack( 'VVCV', 0, 0, self::XDL_BDOP_INSB, strlen( $info['base'] ) ) .
+ $info['base'] );
+ } else {
+ // New format
+ $map = explode( ',', $info['map'] );
+ $cur = 0;
+ $this->mDiffMap = array();
+ foreach ( $map as $i ) {
+ $cur += $i;
+ $this->mDiffMap[] = $cur;
+ }
+ }
+ $this->uncompress();
+ }
+
+ /**
+ * Helper function for compression jobs
+ * Returns true until the object is "full" and ready to be committed
+ */
+ function isHappy() {
+ return $this->mSize < $this->mMaxSize
+ && count( $this->mItems ) < $this->mMaxCount;
+ }
+
+}
diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php
index 555a79b7..269d45ff 100644
--- a/includes/HttpFunctions.php
+++ b/includes/HttpFunctions.php
@@ -1,24 +1,48 @@
<?php
/**
+ * @defgroup HTTP HTTP
+ * @file
+ * @ingroup HTTP
+ */
+
+/**
* Various HTTP related functions
+ * @ingroup HTTP
*/
class Http {
- static function get( $url, $timeout = 'default' ) {
- return Http::request( "GET", $url, $timeout );
+
+ /**
+ * Simple wrapper for Http::request( 'GET' )
+ * @see Http::request()
+ */
+ public static function get( $url, $timeout = 'default', $opts = array() ) {
+ return Http::request( "GET", $url, $timeout, $opts );
}
- static function post( $url, $timeout = 'default' ) {
- return Http::request( "POST", $url, $timeout );
+ /**
+ * Simple wrapper for Http::request( 'POST' )
+ * @see Http::request()
+ */
+ public static function post( $url, $timeout = 'default', $opts = array() ) {
+ return Http::request( "POST", $url, $timeout, $opts );
}
/**
* Get the contents of a file by HTTP
- *
- * if $timeout is 'default', $wgHTTPTimeout is used
+ * @param $method string HTTP method. Usually GET/POST
+ * @param $url string Full URL to act on
+ * @param $timeout int Seconds to timeout. 'default' falls to $wgHTTPTimeout
+ * @param $curlOptions array Optional array of extra params to pass
+ * to curl_setopt()
*/
- static function request( $method, $url, $timeout = 'default' ) {
- global $wgHTTPTimeout, $wgHTTPProxy, $wgVersion, $wgTitle;
+ public static function request( $method, $url, $timeout = 'default', $curlOptions = array() ) {
+ global $wgHTTPTimeout, $wgHTTPProxy, $wgTitle;
+
+ // Go ahead and set the timeout if not otherwise specified
+ if ( $timeout == 'default' ) {
+ $timeout = $wgHTTPTimeout;
+ }
wfDebug( __METHOD__ . ": $method $url\n" );
# Use curl if available
@@ -30,13 +54,12 @@ class Http {
curl_setopt($c, CURLOPT_PROXY, $wgHTTPProxy);
}
- if ( $timeout == 'default' ) {
- $timeout = $wgHTTPTimeout;
- }
curl_setopt( $c, CURLOPT_TIMEOUT, $timeout );
- curl_setopt( $c, CURLOPT_USERAGENT, "MediaWiki/$wgVersion" );
- if ( $method == 'POST' )
+ curl_setopt( $c, CURLOPT_USERAGENT, self :: userAgent() );
+ if ( $method == 'POST' ) {
curl_setopt( $c, CURLOPT_POST, true );
+ curl_setopt( $c, CURLOPT_POSTFIELDS, '' );
+ }
else
curl_setopt( $c, CURLOPT_CUSTOMREQUEST, $method );
@@ -48,6 +71,12 @@ class Http {
if ( is_object( $wgTitle ) ) {
curl_setopt( $c, CURLOPT_REFERER, $wgTitle->getFullURL() );
}
+
+ if ( is_array( $curlOptions ) ) {
+ foreach( $curlOptions as $option => $value ) {
+ curl_setopt( $c, $option, $value );
+ }
+ }
ob_start();
curl_exec( $c );
@@ -55,20 +84,24 @@ class Http {
ob_end_clean();
# Don't return the text of error messages, return false on error
- if ( curl_getinfo( $c, CURLINFO_HTTP_CODE ) != 200 ) {
+ $retcode = curl_getinfo( $c, CURLINFO_HTTP_CODE );
+ if ( $retcode != 200 ) {
+ wfDebug( __METHOD__ . ": HTTP return code $retcode\n" );
$text = false;
}
# Don't return truncated output
- if ( curl_errno( $c ) != CURLE_OK ) {
+ $errno = curl_errno( $c );
+ if ( $errno != CURLE_OK ) {
+ $errstr = curl_error( $c );
+ wfDebug( __METHOD__ . ": CURL error code $errno: $errstr\n" );
$text = false;
}
curl_close( $c );
} else {
# Otherwise use file_get_contents...
- # This may take 3 minutes to time out, and doesn't have local fetch capabilities
+ # This doesn't have local fetch capabilities...
- global $wgVersion;
- $headers = array( "User-Agent: MediaWiki/$wgVersion" );
+ $headers = array( "User-Agent: " . self :: userAgent() );
if( strcasecmp( $method, 'post' ) == 0 ) {
// Required for HTTP 1.0 POSTs
$headers[] = "Content-Length: 0";
@@ -76,20 +109,21 @@ class Http {
$opts = array(
'http' => array(
'method' => $method,
- 'header' => implode( "\r\n", $headers ) ) );
+ 'header' => implode( "\r\n", $headers ),
+ 'timeout' => $timeout ) );
$ctx = stream_context_create($opts);
- $url_fopen = ini_set( 'allow_url_fopen', 1 );
$text = file_get_contents( $url, false, $ctx );
- ini_set( 'allow_url_fopen', $url_fopen );
}
return $text;
}
/**
* Check if the URL can be served by localhost
+ * @param $url string Full url to check
+ * @return bool
*/
- static function isLocalURL( $url ) {
+ public static function isLocalURL( $url ) {
global $wgCommandLineMode, $wgConf;
if ( $wgCommandLineMode ) {
return false;
@@ -117,4 +151,12 @@ class Http {
}
return false;
}
+
+ /**
+ * Return a standard user-agent we can use for external requests.
+ */
+ public static function userAgent() {
+ global $wgVersion;
+ return "MediaWiki/$wgVersion";
+ }
}
diff --git a/includes/IEContentAnalyzer.php b/includes/IEContentAnalyzer.php
index 59abc6a6..df4d36f0 100644
--- a/includes/IEContentAnalyzer.php
+++ b/includes/IEContentAnalyzer.php
@@ -569,8 +569,9 @@ class IEContentAnalyzer {
$chunk3 = substr( $chunk, 0, 3 );
$chunk4 = substr( $chunk, 0, 4 );
$chunk5 = substr( $chunk, 0, 5 );
+ $chunk5uc = strtoupper( $chunk5 );
$chunk8 = substr( $chunk, 0, 8 );
- if ( $chunk5 == 'GIF87' || $chunk5 == 'GIF89' ) {
+ if ( $chunk5uc == 'GIF87' || $chunk5uc == 'GIF89' ) {
return 'image/gif';
}
if ( $chunk2 == "\xff\xd8" ) {
@@ -579,7 +580,7 @@ class IEContentAnalyzer {
if ( $chunk2 == 'BM'
&& substr( $chunk, 6, 2 ) == "\000\000"
- && substr( $chunk, 8, 2 ) != "\000\000" )
+ && substr( $chunk, 8, 2 ) == "\000\000" )
{
return 'image/bmp'; // another non-standard MIME
}
@@ -800,7 +801,7 @@ class IEContentAnalyzer {
}
// BinHex
- if ( !strncasecmp( $remainder, $binhexMagic, strlen( $binhexMagic ) ) ) {
+ if ( !strncmp( $remainder, $binhexMagic, strlen( $binhexMagic ) ) ) {
$found['binhex'] = true;
}
}
diff --git a/includes/IP.php b/includes/IP.php
index e76f66c1..e5973c2b 100644
--- a/includes/IP.php
+++ b/includes/IP.php
@@ -141,7 +141,7 @@ class IP {
public static function toOctet( $ip_int ) {
// Convert to padded uppercase hex
$ip_hex = wfBaseConvert($ip_int, 10, 16, 32, false);
- // Seperate into 8 octets
+ // Separate into 8 octets
$ip_oct = substr( $ip_hex, 0, 4 );
for ($n=1; $n < 8; $n++) {
$ip_oct .= ':' . substr($ip_hex, 4*$n, 4);
@@ -150,6 +150,41 @@ class IP {
$ip_oct = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip_oct );
return $ip_oct;
}
+
+ /**
+ * Given a hexadecimal number, returns to an IPv6 address in octet notation
+ * @param $ip string hex IP
+ * @return string
+ */
+ public static function HextoOctet( $ip_hex ) {
+ // Convert to padded uppercase hex
+ $ip_hex = str_pad( strtoupper($ip_hex), 32, '0');
+ // Separate into 8 octets
+ $ip_oct = substr( $ip_hex, 0, 4 );
+ for ($n=1; $n < 8; $n++) {
+ $ip_oct .= ':' . substr($ip_hex, 4*$n, 4);
+ }
+ // NO leading zeroes
+ $ip_oct = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip_oct );
+ return $ip_oct;
+ }
+
+ /**
+ * Converts a hexadecimal number to an IPv4 address in octet notation
+ * @param $ip string Hex IP
+ * @return string
+ */
+ public static function hexToQuad( $ip ) {
+ // Converts a hexadecimal IP to nnn.nnn.nnn.nnn format
+ $dec = wfBaseConvert( $ip, 16, 10 );
+ $parts[3] = $dec % 256;
+ $dec /= 256;
+ $parts[2] = $dec % 256;
+ $dec /= 256;
+ $parts[1] = $dec % 256;
+ $parts[0] = $dec / 256;
+ return implode( '.', array_reverse( $parts ) );
+ }
/**
* Convert a network specification in IPv6 CIDR notation to an integer network and a number of bits
@@ -320,7 +355,7 @@ class IP {
public static function toHex( $ip ) {
$n = self::toUnsigned( $ip );
if ( $n !== false ) {
- $n = ( self::isIPv6($ip) ) ? "v6-" . wfBaseConvert( $n, 10, 16, 32, false ) : wfBaseConvert( $n, 10, 16, 8, false );
+ $n = self::isIPv6($ip) ? "v6-" . wfBaseConvert( $n, 10, 16, 32, false ) : wfBaseConvert( $n, 10, 16, 8, false );
}
return $n;
}
@@ -426,12 +461,16 @@ class IP {
} elseif ( strpos( $range, '-' ) !== false ) {
# Explicit range
list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
- $start = self::toUnsigned( $start ); $end = self::toUnsigned( $end );
- if ( $start > $end ) {
- $start = $end = false;
+ if( self::isIPAddress( $start ) && self::isIPAddress( $end ) ) {
+ $start = self::toUnsigned( $start ); $end = self::toUnsigned( $end );
+ if ( $start > $end ) {
+ $start = $end = false;
+ } else {
+ $start = sprintf( '%08X', $start );
+ $end = sprintf( '%08X', $end );
+ }
} else {
- $start = sprintf( '%08X', $start );
- $end = sprintf( '%08X', $end );
+ $start = $end = false;
}
} else {
# Single IP
diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php
index af05c1c9..73d935a7 100644
--- a/includes/ImageFunctions.php
+++ b/includes/ImageFunctions.php
@@ -4,9 +4,10 @@
* http://www.w3.org/TR/SVG11/coords.html#UnitIdentifiers
*
* @param $length String: CSS/SVG length.
- * @return Integer: length in pixels
+ * @param $viewportSize: Float optional scale for percentage units...
+ * @return float: length in pixels
*/
-function wfScaleSVGUnit( $length ) {
+function wfScaleSVGUnit( $length, $viewportSize=512 ) {
static $unitLength = array(
'px' => 1.0,
'pt' => 1.25,
@@ -14,17 +15,74 @@ function wfScaleSVGUnit( $length ) {
'mm' => 3.543307,
'cm' => 35.43307,
'in' => 90.0,
+ 'em' => 16.0, // fake it?
+ 'ex' => 12.0, // fake it?
'' => 1.0, // "User units" pixels by default
- '%' => 2.0, // Fake it!
);
$matches = array();
- if( preg_match( '/^(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)$/', $length, $matches ) ) {
+ if( preg_match( '/^\s*(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)\s*$/', $length, $matches ) ) {
$length = floatval( $matches[1] );
$unit = $matches[2];
- return round( $length * $unitLength[$unit] );
+ if( $unit == '%' ) {
+ return $length * 0.01 * $viewportSize;
+ } else {
+ return $length * $unitLength[$unit];
+ }
} else {
// Assume pixels
- return round( floatval( $length ) );
+ return floatval( $length );
+ }
+}
+
+class XmlSizeFilter {
+ const DEFAULT_WIDTH = 512;
+ const DEFAULT_HEIGHT = 512;
+ var $first = true;
+ var $width = self::DEFAULT_WIDTH;
+ var $height = self::DEFAULT_HEIGHT;
+ function filter( $name, $attribs ) {
+ if( $this->first ) {
+ $defaultWidth = self::DEFAULT_WIDTH;
+ $defaultHeight = self::DEFAULT_HEIGHT;
+ $aspect = 1.0;
+ $width = null;
+ $height = null;
+
+ if( isset( $attribs['viewBox'] ) ) {
+ // min-x min-y width height
+ $viewBox = preg_split( '/\s+/', trim( $attribs['viewBox'] ) );
+ if( count( $viewBox ) == 4 ) {
+ $viewWidth = wfScaleSVGUnit( $viewBox[2] );
+ $viewHeight = wfScaleSVGUnit( $viewBox[3] );
+ if( $viewWidth > 0 && $viewHeight > 0 ) {
+ $aspect = $viewWidth / $viewHeight;
+ $defaultHeight = $defaultWidth / $aspect;
+ }
+ }
+ }
+ if( isset( $attribs['width'] ) ) {
+ $width = wfScaleSVGUnit( $attribs['width'], $defaultWidth );
+ }
+ if( isset( $attribs['height'] ) ) {
+ $height = wfScaleSVGUnit( $attribs['height'], $defaultHeight );
+ }
+
+ if( !isset( $width ) && !isset( $height ) ) {
+ $width = $defaultWidth;
+ $height = $width / $aspect;
+ } elseif( isset( $width ) && !isset( $height ) ) {
+ $height = $width / $aspect;
+ } elseif( isset( $height ) && !isset( $width ) ) {
+ $width = $height * $aspect;
+ }
+
+ if( $width > 0 && $height > 0 ) {
+ $this->width = intval( round( $width ) );
+ $this->height = intval( round( $height ) );
+ }
+
+ $this->first = false;
+ }
}
}
@@ -38,30 +96,14 @@ function wfScaleSVGUnit( $length ) {
* @return array
*/
function wfGetSVGsize( $filename ) {
- $width = 256;
- $height = 256;
-
- // Read a chunk of the file
- $f = fopen( $filename, "rt" );
- if( !$f ) return false;
- $chunk = fread( $f, 4096 );
- fclose( $f );
-
- // Uber-crappy hack! Run through a real XML parser.
- $matches = array();
- if( !preg_match( '/<svg\s*([^>]*)\s*>/s', $chunk, $matches ) ) {
- return false;
- }
- $tag = $matches[1];
- if( preg_match( '/(?:^|\s)width\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) {
- $width = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) );
+ $filter = new XmlSizeFilter();
+ $xml = new XmlTypeCheck( $filename, array( $filter, 'filter' ) );
+ if( $xml->wellFormed ) {
+ return array( $filter->width, $filter->height, 'SVG',
+ "width=\"$filter->width\" height=\"$filter->height\"" );
}
- if( preg_match( '/(?:^|\s)height\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) {
- $height = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) );
- }
-
- return array( $width, $height, 'SVG',
- "width=\"$width\" height=\"$height\"" );
+
+ return false;
}
/**
diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php
index 492a3e06..f3f525c1 100644
--- a/includes/ImageGallery.php
+++ b/includes/ImageGallery.php
@@ -244,7 +244,7 @@ class ImageGallery
$img = wfFindFile( $nt, $time );
- if( $nt->getNamespace() != NS_IMAGE || !$img ) {
+ if( $nt->getNamespace() != NS_FILE || !$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>';
diff --git a/includes/ImagePage.php b/includes/ImagePage.php
index 30fcf13e..314d478e 100644
--- a/includes/ImagePage.php
+++ b/includes/ImagePage.php
@@ -22,22 +22,28 @@ class ImagePage extends Article {
$this->dupes = null;
$this->repo = null;
}
+
+ public function setFile( $file ) {
+ $this->displayImg = $file;
+ $this->img = $file;
+ $this->fileLoaded = true;
+ }
protected function loadFile() {
- if ( $this->fileLoaded ) {
+ if( $this->fileLoaded ) {
return true;
}
$this->fileLoaded = true;
$this->displayImg = $this->img = false;
wfRunHooks( 'ImagePageFindFile', array( $this, &$this->img, &$this->displayImg ) );
- if ( !$this->img ) {
+ if( !$this->img ) {
$this->img = wfFindFile( $this->mTitle );
- if ( !$this->img ) {
+ if( !$this->img ) {
$this->img = wfLocalFile( $this->mTitle );
}
}
- if ( !$this->displayImg ) {
+ if( !$this->displayImg ) {
$this->displayImg = $this->img;
}
$this->repo = $this->img->getRepo();
@@ -47,18 +53,18 @@ class ImagePage extends Article {
* Handler for action=render
* Include body text only; none of the image extras
*/
- function render() {
+ public function render() {
global $wgOut;
$wgOut->setArticleBodyOnly( true );
parent::view();
}
- function view() {
+ public function view() {
global $wgOut, $wgShowEXIF, $wgRequest, $wgUser;
$this->loadFile();
- if ( $this->mTitle->getNamespace() == NS_IMAGE && $this->img->getRedirected() ) {
- if ( $this->mTitle->getDBkey() == $this->img->getName() ) {
+ if( $this->mTitle->getNamespace() == NS_FILE && $this->img->getRedirected() ) {
+ if( $this->mTitle->getDBkey() == $this->img->getName() ) {
// mTitle is the same as the redirect target so ask Article
// to perform the redirect for us.
return Article::view();
@@ -66,8 +72,8 @@ class ImagePage extends Article {
// mTitle is not the same as the redirect target so it is
// probably the redirect page itself. Fake the redirect symbol
$wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
- $this->viewRedirect( Title::makeTitle( NS_IMAGE, $this->img->getName() ),
- /* $appendSubtitle */ true, /* $forceKnown */ true );
+ $wgOut->addHTML( $this->viewRedirect( Title::makeTitle( NS_FILE, $this->img->getName() ),
+ /* $appendSubtitle */ true, /* $forceKnown */ true ) );
$this->viewUpdates();
return;
}
@@ -76,10 +82,10 @@ class ImagePage extends Article {
$diff = $wgRequest->getVal( 'diff' );
$diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
- if ( $this->mTitle->getNamespace() != NS_IMAGE || ( isset( $diff ) && $diffOnly ) )
+ if( $this->mTitle->getNamespace() != NS_FILE || ( isset( $diff ) && $diffOnly ) )
return Article::view();
- if ( $wgShowEXIF && $this->displayImg->exists() ) {
+ if( $wgShowEXIF && $this->displayImg->exists() ) {
// FIXME: bad interface, see note on MediaHandler::formatMetadata().
$formattedMetadata = $this->displayImg->formatMetadata();
$showmeta = $formattedMetadata !== false;
@@ -87,24 +93,25 @@ class ImagePage extends Article {
$showmeta = false;
}
- if ( $this->displayImg->exists() )
+ if( !$diff && $this->displayImg->exists() )
$wgOut->addHTML( $this->showTOC($showmeta) );
- $this->openShowImage();
+ if( !$diff )
+ $this->openShowImage();
# No need to display noarticletext, we use our own message, output in openShowImage()
- if ( $this->getID() ) {
+ if( $this->getID() ) {
Article::view();
} else {
# Just need to set the right headers
$wgOut->setArticleFlag( true );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
$this->viewUpdates();
}
# Show shared description, if needed
- if ( $this->mExtraDescription ) {
+ if( $this->mExtraDescription ) {
$fol = wfMsgNoTrans( 'shareddescriptionfollows' );
if( $fol != '-' && !wfEmptyMsg( 'shareddescriptionfollows', $fol ) ) {
$wgOut->addWikiText( $fol );
@@ -118,21 +125,21 @@ class ImagePage extends Article {
$this->imageHistory();
// TODO: Cleanup the following
- $wgOut->addHTML( Xml::element( 'h2',
- array( 'id' => 'filelinks' ),
+ $wgOut->addHTML( Xml::element( 'h2',
+ array( 'id' => 'filelinks' ),
wfMsg( 'imagelinks' ) ) . "\n" );
$this->imageDupes();
// TODO: We may want to find local images redirecting to a foreign
// file: "The following local files redirect to this file"
- if ( $this->img->isLocal() ) {
+ if( $this->img->isLocal() ) {
$this->imageRedirects();
}
$this->imageLinks();
- if ( $showmeta ) {
+ if( $showmeta ) {
global $wgStylePath, $wgStyleVersion;
- $expand = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-expand' ) ) );
- $collapse = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-collapse' ) ) );
+ $expand = htmlspecialchars( Xml::escapeJsString( wfMsg( 'metadata-expand' ) ) );
+ $collapse = htmlspecialchars( Xml::escapeJsString( wfMsg( 'metadata-collapse' ) ) );
$wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ). "\n" );
$wgOut->addWikiText( $this->makeMetadataTable( $formattedMetadata ) );
$wgOut->addScriptFile( 'metadata.js' );
@@ -143,32 +150,32 @@ class ImagePage extends Article {
public function getRedirectTarget() {
$this->loadFile();
- if ( $this->img->isLocal() ) {
+ if( $this->img->isLocal() ) {
return parent::getRedirectTarget();
}
// Foreign image page
$from = $this->img->getRedirected();
$to = $this->img->getName();
- if ( $from == $to ) {
+ if( $from == $to ) {
return null;
}
- return $this->mRedirectTarget = Title::makeTitle( NS_IMAGE, $to );
+ return $this->mRedirectTarget = Title::makeTitle( NS_FILE, $to );
}
public function followRedirect() {
$this->loadFile();
- if ( $this->img->isLocal() ) {
+ if( $this->img->isLocal() ) {
return parent::followRedirect();
}
$from = $this->img->getRedirected();
$to = $this->img->getName();
- if ( $from == $to ) {
+ if( $from == $to ) {
return false;
}
- return Title::makeTitle( NS_IMAGE, $to );
+ return Title::makeTitle( NS_FILE, $to );
}
public function isRedirect( $text = false ) {
$this->loadFile();
- if ( $this->img->isLocal() )
+ if( $this->img->isLocal() )
return parent::isRedirect( $text );
return (bool)$this->img->getRedirected();
@@ -191,10 +198,10 @@ class ImagePage extends Article {
public function getDuplicates() {
$this->loadFile();
- if ( !is_null($this->dupes) ) {
+ if( !is_null($this->dupes) ) {
return $this->dupes;
}
- if ( !( $hash = $this->img->getSha1() ) ) {
+ if( !( $hash = $this->img->getSha1() ) ) {
return $this->dupes = array();
}
$dupes = RepoGroup::singleton()->findBySha1( $hash );
@@ -203,9 +210,9 @@ class ImagePage extends Article {
$size = $this->img->getSize();
foreach ( $dupes as $index => $file ) {
$key = $file->getRepoName().':'.$file->getName();
- if ( $key == $self )
+ if( $key == $self )
unset( $dupes[$index] );
- if ( $file->getSize() != $size )
+ if( $file->getSize() != $size )
unset( $dupes[$index] );
}
return $this->dupes = $dupes;
@@ -216,15 +223,13 @@ class ImagePage extends Article {
/**
* Create the TOC
*
- * @access private
- *
* @param bool $metadata Whether or not to show the metadata link
* @return string
*/
- function showTOC( $metadata ) {
+ protected function showTOC( $metadata ) {
global $wgLang;
$r = '<ul id="filetoc">
- <li><a href="#file">' . $wgLang->getNsText( NS_IMAGE ) . '</a></li>
+ <li><a href="#file">' . $wgLang->getNsText( NS_FILE ) . '</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>' : '') . '
@@ -237,16 +242,15 @@ class ImagePage extends Article {
*
* FIXME: bad interface, see note on MediaHandler::formatMetadata().
*
- * @access private
- *
* @param array $exif The array containing the EXIF data
* @return string
*/
- function makeMetadataTable( $metadata ) {
+ protected function makeMetadataTable( $metadata ) {
$r = wfMsg( 'metadata-help' ) . "\n\n";
$r .= "{| id=mw_metadata class=mw_metadata\n";
foreach ( $metadata as $type => $stuff ) {
foreach ( $stuff as $v ) {
+ # FIXME, why is this using escapeId for a class?!
$class = Sanitizer::escapeId( $v['id'] );
if( $type == 'collapsed' ) {
$class .= ' collapsable';
@@ -266,7 +270,7 @@ class ImagePage extends Article {
* Omit noarticletext if sharedupload; text will be fetched from the
* shared upload server if possible.
*/
- function getContent() {
+ public function getContent() {
$this->loadFile();
if( $this->img && !$this->img->isLocal() && 0 == $this->getID() ) {
return '';
@@ -274,7 +278,7 @@ class ImagePage extends Article {
return Article::getContent();
}
- function openShowImage() {
+ protected function openShowImage() {
global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang, $wgContLang;
$this->loadFile();
@@ -298,10 +302,10 @@ class ImagePage extends Article {
$sk = $wgUser->getSkin();
$dirmark = $wgContLang->getDirMark();
- if ( $this->displayImg->exists() ) {
+ if( $this->displayImg->exists() ) {
# image
$page = $wgRequest->getIntOrNull( 'page' );
- if ( is_null( $page ) ) {
+ if( is_null( $page ) ) {
$params = array();
$page = 1;
} else {
@@ -318,16 +322,16 @@ class ImagePage extends Article {
wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this , &$wgOut ) ) ;
- if ( $this->displayImg->allowInlineDisplay() ) {
+ if( $this->displayImg->allowInlineDisplay() ) {
# image
# "Download high res version" link below the image
#$msgsize = wfMsgHtml('file-info-size', $width_orig, $height_orig, $sk->formatSize( $this->displayImg->getSize() ), $mime );
# We'll show a thumbnail of this image
- if ( $width > $maxWidth || $height > $maxHeight ) {
+ if( $width > $maxWidth || $height > $maxHeight ) {
# Calculate the thumbnail size.
# First case, the limiting factor is the width, not the height.
- if ( $width / $height >= $maxWidth / $maxHeight ) {
+ if( $width / $height >= $maxWidth / $maxHeight ) {
$height = round( $height * $maxWidth / $width);
$width = $maxWidth;
# Note that $height <= $maxHeight now.
@@ -339,8 +343,10 @@ class ImagePage extends Article {
# because of rounding.
}
$msgbig = wfMsgHtml( 'show-big-image' );
- $msgsmall = wfMsgExt( 'show-big-image-thumb',
- array( 'parseinline' ), $wgLang->formatNum( $width ), $wgLang->formatNum( $height ) );
+ $msgsmall = wfMsgExt( 'show-big-image-thumb', 'parseinline',
+ $wgLang->formatNum( $width ),
+ $wgLang->formatNum( $height )
+ );
} else {
# Image is small enough to show full size on image page
$msgbig = htmlspecialchars( $this->displayImg->getName() );
@@ -359,11 +365,11 @@ class ImagePage extends Article {
'<br />' . Xml::tags( 'a', $linkAttribs, $msgbig ) . "$dirmark " . $longDesc;
}
- if ( $this->displayImg->isMultipage() ) {
+ if( $this->displayImg->isMultipage() ) {
$wgOut->addHTML( '<table class="multipageimage"><tr><td>' );
}
- if ( $thumbnail ) {
+ if( $thumbnail ) {
$options = array(
'alt' => $this->displayImg->getTitle()->getPrefixedText(),
'file-link' => true,
@@ -373,10 +379,10 @@ class ImagePage extends Article {
$anchorclose . '</div>' );
}
- if ( $this->displayImg->isMultipage() ) {
+ if( $this->displayImg->isMultipage() ) {
$count = $this->displayImg->pageCount();
- if ( $page > 1 ) {
+ if( $page > 1 ) {
$label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false );
$link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page-1) );
$thumb1 = $sk->makeThumbLinkObj( $this->mTitle, $this->displayImg, $link, $label, 'none',
@@ -385,7 +391,7 @@ class ImagePage extends Article {
$thumb1 = '';
}
- if ( $page < $count ) {
+ if( $page < $count ) {
$label = wfMsg( 'imgmultipagenext' );
$link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page+1) );
$thumb2 = $sk->makeThumbLinkObj( $this->mTitle, $this->displayImg, $link, $label, 'none',
@@ -422,7 +428,7 @@ class ImagePage extends Article {
}
} else {
#if direct link is allowed but it's not a renderable image, show an icon.
- if ( $this->displayImg->isSafeFile() ) {
+ if( $this->displayImg->isSafeFile() ) {
$icon= $this->displayImg->iconThumb();
$wgOut->addHTML( '<div class="fullImageLink" id="file">' .
@@ -434,10 +440,10 @@ class ImagePage extends Article {
}
- if ($showLink) {
+ if($showLink) {
$filename = wfEscapeWikiText( $this->displayImg->getName() );
- if ( !$this->displayImg->isSafeFile() ) {
+ if( !$this->displayImg->isSafeFile() ) {
$warning = wfMsgNoTrans( 'mediawarning' );
$wgOut->addWikiText( <<<EOT
<div class="fullMedia">
@@ -474,7 +480,7 @@ EOT
/**
* Show a notice that the file is from a shared repository
*/
- function printSharedImageText() {
+ protected function printSharedImageText() {
global $wgOut, $wgUser;
$this->loadFile();
@@ -482,12 +488,12 @@ EOT
$descUrl = $this->img->getDescriptionUrl();
$descText = $this->img->getDescriptionText();
$s = "<div class='sharedUploadNotice'>" . wfMsgWikiHtml( 'sharedupload' );
- if ( $descUrl ) {
+ if( $descUrl ) {
$sk = $wgUser->getSkin();
$link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadwiki-linktext' ) );
$msg = ( $descText ) ? 'shareduploadwiki-desc' : 'shareduploadwiki';
$msg = wfMsgExt( $msg, array( 'parseinline', 'replaceafter' ), $link );
- if ( $msg != '-' ) {
+ if( $msg != '-' ) {
# Show message only if not voided by local sysops
$s .= $msg;
}
@@ -495,7 +501,7 @@ EOT
$s .= "</div>";
$wgOut->addHTML( $s );
- if ( $descText ) {
+ if( $descText ) {
$this->mExtraDescription = $descText;
}
}
@@ -503,7 +509,7 @@ EOT
/*
* Check for files with the same name on the foreign repos.
*/
- function checkSharedConflict() {
+ protected function checkSharedConflict() {
global $wgOut, $wgUser;
$repoGroup = RepoGroup::singleton();
@@ -538,7 +544,7 @@ EOT
}
}
- function checkSharedConflictCallback( $repo ) {
+ public function checkSharedConflictCallback( $repo ) {
$this->loadFile();
$dupfile = $repo->newFile( $this->img->getTitle() );
if( $dupfile && $dupfile->exists() ) {
@@ -548,7 +554,7 @@ EOT
return false;
}
- function getUploadUrl() {
+ public function getUploadUrl() {
$this->loadFile();
$uploadTitle = SpecialPage::getTitleFor( 'Upload' );
return $uploadTitle->getFullUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) );
@@ -558,7 +564,7 @@ EOT
* Print out the various links at the bottom of the image page, e.g. reupload,
* external editing (and instructions link) etc.
*/
- function uploadLinksBox() {
+ protected function uploadLinksBox() {
global $wgUser, $wgOut;
$this->loadFile();
@@ -567,69 +573,49 @@ EOT
$sk = $wgUser->getSkin();
- $wgOut->addHtml( '<br /><ul>' );
+ $wgOut->addHTML( '<br /><ul>' );
# "Upload a new version of this file" link
if( UploadForm::userCanReUpload($wgUser,$this->img->name) ) {
$ulink = $sk->makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) );
- $wgOut->addHtml( "<li><div class='plainlinks'>{$ulink}</div></li>" );
+ $wgOut->addHTML( "<li><div class='plainlinks'>{$ulink}</div></li>" );
}
# Link to Special:FileDuplicateSearch
$dupeLink = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'FileDuplicateSearch', $this->mTitle->getDBkey() ), wfMsgHtml( 'imagepage-searchdupe' ) );
- $wgOut->addHtml( "<li>{$dupeLink}</li>" );
+ $wgOut->addHTML( "<li>{$dupeLink}</li>" );
# External editing link
$elink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'edit-externally' ), 'action=edit&externaledit=true&mode=file' );
- $wgOut->addHtml( '<li>' . $elink . '<div>' . wfMsgWikiHtml( 'edit-externally-help' ) . '</div></li>' );
+ $wgOut->addHTML( '<li>' . $elink . ' <small>' . wfMsgExt( 'edit-externally-help', array( 'parseinline' ) ) . '</small></li>' );
- $wgOut->addHtml( '</ul>' );
+ $wgOut->addHTML( '</ul>' );
}
- function closeShowImage()
- {
- # For overloading
-
- }
+ protected function closeShowImage() {} # For overloading
/**
* If the page we've just displayed is in the "Image" namespace,
* we follow it with an upload history of the image and its usage.
*/
- function imageHistory()
- {
+ protected function imageHistory() {
global $wgOut, $wgUseExternalEditor;
$this->loadFile();
- if ( $this->img->exists() ) {
- $list = new ImageHistoryList( $this );
- $file = $this->img;
- $dims = $file->getDimensionsString();
- $s = $list->beginImageHistoryList();
- $s .= $list->imageHistoryLine( true, $file );
- // old image versions
- $hist = $this->img->getHistory();
- foreach( $hist as $file ) {
- $dims = $file->getDimensionsString();
- $s .= $list->imageHistoryLine( false, $file );
- }
- $s .= $list->endImageHistoryList();
- } else { $s=''; }
- $wgOut->addHTML( $s );
+ $pager = new ImageHistoryPseudoPager( $this );
+ $wgOut->addHTML( $pager->getBody() );
- $this->img->resetHistory(); // free db resources
+ $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() ) {
$this->uploadLinksBox();
}
-
}
- function imageLinks()
- {
- global $wgUser, $wgOut;
+ protected function imageLinks() {
+ global $wgUser, $wgOut, $wgLang;
$limit = 100;
@@ -643,21 +629,30 @@ EOT
array( 'LIMIT' => $limit + 1)
);
$count = $dbr->numRows( $res );
- if ( $count == 0 ) {
+ if( $count == 0 ) {
$wgOut->addHTML( "<div id='mw-imagepage-nolinkstoimage'>\n" );
$wgOut->addWikiMsg( 'nolinkstoimage' );
$wgOut->addHTML( "</div>\n" );
return;
}
+
$wgOut->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" );
- $wgOut->addWikiMsg( 'linkstoimage', $count );
- $wgOut->addHTML( "<ul class='mw-imagepage-linktoimage'>\n" );
+ if( $count <= $limit - 1 ) {
+ $wgOut->addWikiMsg( 'linkstoimage', $count );
+ } else {
+ // More links than the limit. Add a link to [[Special:Whatlinkshere]]
+ $wgOut->addWikiMsg( 'linkstoimage-more',
+ $wgLang->formatNum( $limit ),
+ $this->mTitle->getPrefixedDBkey()
+ );
+ }
+ $wgOut->addHTML( "<ul class='mw-imagepage-linkstoimage'>\n" );
$sk = $wgUser->getSkin();
$count = 0;
while ( $s = $res->fetchObject() ) {
$count++;
- if ( $count <= $limit ) {
+ if( $count <= $limit ) {
// We have not yet reached the extra one that tells us there is more to fetch
$name = Title::makeTitle( $s->page_namespace, $s->page_title );
$link = $sk->makeKnownLinkObj( $name, "" );
@@ -668,19 +663,20 @@ EOT
$res->free();
// Add a links to [[Special:Whatlinkshere]]
- if ( $count > $limit )
+ if( $count > $limit )
$wgOut->addWikiMsg( 'morelinkstoimage', $this->mTitle->getPrefixedDBkey() );
}
- function imageRedirects()
- {
- global $wgUser, $wgOut;
+ protected function imageRedirects() {
+ global $wgUser, $wgOut, $wgLang;
- $redirects = $this->getTitle()->getRedirectsHere( NS_IMAGE );
- if ( count( $redirects ) == 0 ) return;
+ $redirects = $this->getTitle()->getRedirectsHere( NS_FILE );
+ if( count( $redirects ) == 0 ) return;
$wgOut->addHTML( "<div id='mw-imagepage-section-redirectstofile'>\n" );
- $wgOut->addWikiMsg( 'redirectstofile', count( $redirects ) );
+ $wgOut->addWikiMsg( 'redirectstofile',
+ $wgLang->formatNum( count( $redirects ) )
+ );
$wgOut->addHTML( "<ul class='mw-imagepage-redirectstofile'>\n" );
$sk = $wgUser->getSkin();
@@ -692,25 +688,28 @@ EOT
}
- function imageDupes() {
- global $wgOut, $wgUser;
+ protected function imageDupes() {
+ global $wgOut, $wgUser, $wgLang;
$this->loadFile();
$dupes = $this->getDuplicates();
- if ( count( $dupes ) == 0 ) return;
+ if( count( $dupes ) == 0 ) return;
$wgOut->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" );
- $wgOut->addWikiMsg( 'duplicatesoffile', count( $dupes ) );
+ $wgOut->addWikiMsg( 'duplicatesoffile',
+ $wgLang->formatNum( count( $dupes ) )
+ );
$wgOut->addHTML( "<ul class='mw-imagepage-duplicates'>\n" );
$sk = $wgUser->getSkin();
foreach ( $dupes as $file ) {
- if ( $file->isLocal() )
+ if( $file->isLocal() )
$link = $sk->makeKnownLinkObj( $file->getTitle(), "" );
- else
- $link = $sk->makeExternalLink( $file->getDescriptionUrl(),
+ else {
+ $link = $sk->makeExternalLink( $file->getDescriptionUrl(),
$file->getTitle()->getPrefixedText() );
+ }
$wgOut->addHTML( "<li>{$link}</li>\n" );
}
$wgOut->addHTML( "</ul></div>\n" );
@@ -742,7 +741,7 @@ EOT
/**
* Override handling of action=purge
*/
- function doPurge() {
+ public function doPurge() {
$this->loadFile();
if( $this->img->exists() ) {
wfDebug( "ImagePage::doPurge purging " . $this->img->getName() . "\n" );
@@ -762,7 +761,7 @@ EOT
function showError( $description ) {
global $wgOut;
$wgOut->setPageTitle( wfMsg( "internalerror" ) );
- $wgOut->setRobotpolicy( "noindex,nofollow" );
+ $wgOut->setRobotPolicy( "noindex,nofollow" );
$wgOut->setArticleRelated( false );
$wgOut->enableClientCache( false );
$wgOut->addWikiText( $description );
@@ -788,34 +787,36 @@ class ImageHistoryList {
$this->imagePage = $imagePage;
}
- function getImagePage() {
+ public function getImagePage() {
return $this->imagePage;
}
- function getSkin() {
+ public function getSkin() {
return $this->skin;
}
- function getFile() {
+ public function getFile() {
return $this->img;
}
- public function beginImageHistoryList() {
+ public function beginImageHistoryList( $navLinks = '' ) {
global $wgOut, $wgUser;
return Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'filehist' ) )
. $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) )
+ . $navLinks
. Xml::openElement( 'table', array( 'class' => 'filehistory' ) ) . "\n"
. '<tr><td></td>'
. ( $this->current->isLocal() && ($wgUser->isAllowed('delete') || $wgUser->isAllowed('deleterevision') ) ? '<td></td>' : '' )
. '<th>' . wfMsgHtml( 'filehist-datetime' ) . '</th>'
+ . '<th>' . wfMsgHtml( 'filehist-thumb' ) . '</th>'
. '<th>' . wfMsgHtml( 'filehist-dimensions' ) . '</th>'
- . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>'
- . '<th>' . wfMsgHtml( 'filehist-comment' ) . '</th>'
+ . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>'
+ . '<th>' . wfMsgHtml( 'filehist-comment' ) . '</th>'
. "</tr>\n";
}
- public function endImageHistoryList() {
- return "</table>\n";
+ public function endImageHistoryList( $navLinks = '' ) {
+ return "</table>\n$navLinks\n";
}
public function imageHistoryLine( $iscur, $file ) {
@@ -910,6 +911,21 @@ class ImageHistoryList {
$row .= Xml::element( 'a', array( 'href' => $url ), $wgLang->timeAndDate( $timestamp, true ) );
}
+ // Thumbnail
+ if( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE ) && !$file->isDeleted( File::DELETED_FILE ) ) {
+ $params = array(
+ 'width' => '120',
+ 'height' => '120',
+ );
+ $thumbnail = $file->transform( $params );
+ $options = array(
+ 'alt' => wfMsg( 'filehist-thumbtext', $wgLang->timeAndDate( $timestamp, true ) ),
+ 'file-link' => true,
+ );
+ $row .= '</td><td>' . $thumbnail->toHtml( $options );
+ } else {
+ $row .= '</td><td>' . wfMsgHtml( 'filehist-nothumb' );
+ }
$row .= "</td><td>";
// Image dimensions
@@ -934,7 +950,7 @@ class ImageHistoryList {
$row .= '</td><td>';
// Don't show deleted descriptions
- if ( $file->isDeleted(File::DELETED_COMMENT) ) {
+ if( $file->isDeleted(File::DELETED_COMMENT) ) {
$row .= '<span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>';
} else {
$row .= $this->skin->commentBlock( $description, $this->title );
@@ -947,3 +963,128 @@ class ImageHistoryList {
return "<tr{$classAttr}>{$row}</tr>\n";
}
}
+
+class ImageHistoryPseudoPager extends ReverseChronologicalPager {
+ function __construct( $imagePage ) {
+ parent::__construct();
+ $this->mImagePage = $imagePage;
+ $this->mTitle = clone( $imagePage->getTitle() );
+ $this->mTitle->setFragment( '#filehistory' );
+ $this->mImg = NULL;
+ $this->mHist = array();
+ $this->mRange = array( 0, 0 ); // display range
+ }
+
+ function getTitle() {
+ return $this->mTitle;
+ }
+
+ function getQueryInfo() {
+ return false;
+ }
+
+ function getIndexField() {
+ return '';
+ }
+
+ function formatRow( $row ) {
+ return '';
+ }
+
+ function getBody() {
+ $s = '';
+ $this->doQuery();
+ if( count($this->mHist) ) {
+ $list = new ImageHistoryList( $this->mImagePage );
+ # Generate prev/next links
+ $navLink = $this->getNavigationBar();
+ $s = $list->beginImageHistoryList($navLink);
+ // Skip rows there just for paging links
+ for( $i = $this->mRange[0]; $i <= $this->mRange[1]; $i++ ) {
+ $file = $this->mHist[$i];
+ $s .= $list->imageHistoryLine( !$file->isOld(), $file );
+ }
+ $s .= $list->endImageHistoryList($navLink);
+ }
+ return $s;
+ }
+
+ function doQuery() {
+ if( $this->mQueryDone ) return;
+ $this->mImg = $this->mImagePage->getFile(); // ensure loading
+ if( !$this->mImg->exists() ) {
+ return;
+ }
+ $queryLimit = $this->mLimit + 1; // limit plus extra row
+ if( $this->mIsBackwards ) {
+ // Fetch the file history
+ $this->mHist = $this->mImg->getHistory($queryLimit,null,$this->mOffset,false);
+ // The current rev may not meet the offset/limit
+ $numRows = count($this->mHist);
+ if( $numRows <= $this->mLimit && $this->mImg->getTimestamp() > $this->mOffset ) {
+ $this->mHist = array_merge( array($this->mImg), $this->mHist );
+ }
+ } else {
+ // The current rev may not meet the offset
+ if( !$this->mOffset || $this->mImg->getTimestamp() < $this->mOffset ) {
+ $this->mHist[] = $this->mImg;
+ }
+ // Old image versions (fetch extra row for nav links)
+ $oiLimit = count($this->mHist) ? $this->mLimit : $this->mLimit+1;
+ // Fetch the file history
+ $this->mHist = array_merge( $this->mHist,
+ $this->mImg->getHistory($oiLimit,$this->mOffset,null,false) );
+ }
+ $numRows = count($this->mHist); // Total number of query results
+ if( $numRows ) {
+ # Index value of top item in the list
+ $firstIndex = $this->mIsBackwards ?
+ $this->mHist[$numRows-1]->getTimestamp() : $this->mHist[0]->getTimestamp();
+ # Discard the extra result row if there is one
+ if( $numRows > $this->mLimit && $numRows > 1 ) {
+ if( $this->mIsBackwards ) {
+ # Index value of item past the index
+ $this->mPastTheEndIndex = $this->mHist[0]->getTimestamp();
+ # Index value of bottom item in the list
+ $lastIndex = $this->mHist[1]->getTimestamp();
+ # Display range
+ $this->mRange = array( 1, $numRows-1 );
+ } else {
+ # Index value of item past the index
+ $this->mPastTheEndIndex = $this->mHist[$numRows-1]->getTimestamp();
+ # Index value of bottom item in the list
+ $lastIndex = $this->mHist[$numRows-2]->getTimestamp();
+ # Display range
+ $this->mRange = array( 0, $numRows-2 );
+ }
+ } else {
+ # 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 = '';
+ # Index value of bottom item in the list
+ $lastIndex = $this->mIsBackwards ?
+ $this->mHist[0]->getTimestamp() : $this->mHist[$numRows-1]->getTimestamp();
+ # Display range
+ $this->mRange = array( 0, $numRows-1 );
+ }
+ } else {
+ $firstIndex = '';
+ $lastIndex = '';
+ $this->mPastTheEndIndex = '';
+ }
+ if( $this->mIsBackwards ) {
+ $this->mIsFirst = ( $numRows < $queryLimit );
+ $this->mIsLast = ( $this->mOffset == '' );
+ $this->mLastShown = $firstIndex;
+ $this->mFirstShown = $lastIndex;
+ } else {
+ $this->mIsFirst = ( $this->mOffset == '' );
+ $this->mIsLast = ( $numRows < $queryLimit );
+ $this->mLastShown = $lastIndex;
+ $this->mFirstShown = $firstIndex;
+ }
+ $this->mQueryDone = true;
+ }
+}
diff --git a/includes/ImageQueryPage.php b/includes/ImageQueryPage.php
index da9b6fd6..3ab0b858 100644
--- a/includes/ImageQueryPage.php
+++ b/includes/ImageQueryPage.php
@@ -34,7 +34,7 @@ class ImageQueryPage extends QueryPage {
}
}
- $out->addHtml( $gallery->toHtml() );
+ $out->addHTML( $gallery->toHtml() );
}
}
@@ -45,9 +45,9 @@ class ImageQueryPage extends QueryPage {
* @return Image
*/
private function prepareImage( $row ) {
- $namespace = isset( $row->namespace ) ? $row->namespace : NS_IMAGE;
+ $namespace = isset( $row->namespace ) ? $row->namespace : NS_FILE;
$title = Title::makeTitleSafe( $namespace, $row->title );
- return ( $title instanceof Title && $title->getNamespace() == NS_IMAGE )
+ return ( $title instanceof Title && $title->getNamespace() == NS_FILE )
? wfFindFile( $title )
: null;
}
diff --git a/includes/Import.php b/includes/Import.php
new file mode 100644
index 00000000..56e7a7fb
--- /dev/null
+++ b/includes/Import.php
@@ -0,0 +1,1133 @@
+<?php
+/**
+ * MediaWiki page data importer
+ * Copyright (C) 2003,2005 Brion Vibber <brion@pobox.com>
+ * http://www.mediawiki.org/
+ *
+ * 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.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ * @ingroup SpecialPage
+ */
+class WikiRevision {
+ var $title = null;
+ var $id = 0;
+ var $timestamp = "20010115000000";
+ var $user = 0;
+ var $user_text = "";
+ var $text = "";
+ var $comment = "";
+ var $minor = false;
+ var $type = "";
+ var $action = "";
+ var $params = "";
+
+ function setTitle( $title ) {
+ if( is_object( $title ) ) {
+ $this->title = $title;
+ } elseif( is_null( $title ) ) {
+ throw new MWException( "WikiRevision given a null title in import. You may need to adjust \$wgLegalTitleChars." );
+ } else {
+ throw new MWException( "WikiRevision given non-object title in import." );
+ }
+ }
+
+ function setID( $id ) {
+ $this->id = $id;
+ }
+
+ function setTimestamp( $ts ) {
+ # 2003-08-05T18:30:02Z
+ $this->timestamp = wfTimestamp( TS_MW, $ts );
+ }
+
+ function setUsername( $user ) {
+ $this->user_text = $user;
+ }
+
+ function setUserIP( $ip ) {
+ $this->user_text = $ip;
+ }
+
+ function setText( $text ) {
+ $this->text = $text;
+ }
+
+ function setComment( $text ) {
+ $this->comment = $text;
+ }
+
+ function setMinor( $minor ) {
+ $this->minor = (bool)$minor;
+ }
+
+ function setSrc( $src ) {
+ $this->src = $src;
+ }
+
+ function setFilename( $filename ) {
+ $this->filename = $filename;
+ }
+
+ function setSize( $size ) {
+ $this->size = intval( $size );
+ }
+
+ function setType( $type ) {
+ $this->type = $type;
+ }
+
+ function setAction( $action ) {
+ $this->action = $action;
+ }
+
+ function setParams( $params ) {
+ $this->params = $params;
+ }
+
+ function getTitle() {
+ return $this->title;
+ }
+
+ function getID() {
+ return $this->id;
+ }
+
+ function getTimestamp() {
+ return $this->timestamp;
+ }
+
+ function getUser() {
+ return $this->user_text;
+ }
+
+ function getText() {
+ return $this->text;
+ }
+
+ function getComment() {
+ return $this->comment;
+ }
+
+ function getMinor() {
+ return $this->minor;
+ }
+
+ function getSrc() {
+ return $this->src;
+ }
+
+ function getFilename() {
+ return $this->filename;
+ }
+
+ function getSize() {
+ return $this->size;
+ }
+
+ function getType() {
+ return $this->type;
+ }
+
+ function getAction() {
+ return $this->action;
+ }
+
+ function getParams() {
+ return $this->params;
+ }
+
+ function importOldRevision() {
+ $dbw = wfGetDB( DB_MASTER );
+
+ # Sneak a single revision into place
+ $user = User::newFromName( $this->getUser() );
+ if( $user ) {
+ $userId = intval( $user->getId() );
+ $userText = $user->getName();
+ } else {
+ $userId = 0;
+ $userText = $this->getUser();
+ }
+
+ // avoid memory leak...?
+ $linkCache = LinkCache::singleton();
+ $linkCache->clear();
+
+ $article = new Article( $this->title );
+ $pageId = $article->getId();
+ if( $pageId == 0 ) {
+ # must create the page...
+ $pageId = $article->insertOn( $dbw );
+ $created = true;
+ } else {
+ $created = false;
+
+ $prior = $dbw->selectField( 'revision', '1',
+ array( 'rev_page' => $pageId,
+ 'rev_timestamp' => $dbw->timestamp( $this->timestamp ),
+ 'rev_user_text' => $userText,
+ 'rev_comment' => $this->getComment() ),
+ __METHOD__
+ );
+ if( $prior ) {
+ // FIXME: this could fail slightly for multiple matches :P
+ wfDebug( __METHOD__ . ": skipping existing revision for [[" .
+ $this->title->getPrefixedText() . "]], timestamp " . $this->timestamp . "\n" );
+ return false;
+ }
+ }
+
+ # FIXME: Use original rev_id optionally (better for backups)
+ # Insert the row
+ $revision = new Revision( array(
+ 'page' => $pageId,
+ 'text' => $this->getText(),
+ 'comment' => $this->getComment(),
+ 'user' => $userId,
+ 'user_text' => $userText,
+ 'timestamp' => $this->timestamp,
+ 'minor_edit' => $this->minor,
+ ) );
+ $revId = $revision->insertOn( $dbw );
+ $changed = $article->updateIfNewerOn( $dbw, $revision );
+
+ # To be on the safe side...
+ $tempTitle = $GLOBALS['wgTitle'];
+ $GLOBALS['wgTitle'] = $this->title;
+
+ if( $created ) {
+ wfDebug( __METHOD__ . ": running onArticleCreate\n" );
+ Article::onArticleCreate( $this->title );
+
+ wfDebug( __METHOD__ . ": running create updates\n" );
+ $article->createUpdates( $revision );
+
+ } elseif( $changed ) {
+ wfDebug( __METHOD__ . ": running onArticleEdit\n" );
+ Article::onArticleEdit( $this->title, 'skiptransclusions' ); // leave templatelinks for editUpdates()
+
+ wfDebug( __METHOD__ . ": running edit updates\n" );
+ $article->editUpdates(
+ $this->getText(),
+ $this->getComment(),
+ $this->minor,
+ $this->timestamp,
+ $revId );
+ }
+ $GLOBALS['wgTitle'] = $tempTitle;
+
+ return true;
+ }
+
+ function importLogItem() {
+ $dbw = wfGetDB( DB_MASTER );
+ # FIXME: this will not record autoblocks
+ if( !$this->getTitle() ) {
+ wfDebug( __METHOD__ . ": skipping invalid {$this->type}/{$this->action} log time, timestamp " .
+ $this->timestamp . "\n" );
+ return;
+ }
+ # Check if it exists already
+ // FIXME: use original log ID (better for backups)
+ $prior = $dbw->selectField( 'logging', '1',
+ array( 'log_type' => $this->getType(),
+ 'log_action' => $this->getAction(),
+ 'log_timestamp' => $dbw->timestamp( $this->timestamp ),
+ 'log_namespace' => $this->getTitle()->getNamespace(),
+ 'log_title' => $this->getTitle()->getDBkey(),
+ 'log_comment' => $this->getComment(),
+ #'log_user_text' => $this->user_text,
+ 'log_params' => $this->params ),
+ __METHOD__
+ );
+ // FIXME: this could fail slightly for multiple matches :P
+ if( $prior ) {
+ wfDebug( __METHOD__ . ": skipping existing item for Log:{$this->type}/{$this->action}, timestamp " .
+ $this->timestamp . "\n" );
+ return false;
+ }
+ $log_id = $dbw->nextSequenceValue( 'log_log_id_seq' );
+ $data = array(
+ 'log_id' => $log_id,
+ 'log_type' => $this->type,
+ 'log_action' => $this->action,
+ 'log_timestamp' => $dbw->timestamp( $this->timestamp ),
+ 'log_user' => User::idFromName( $this->user_text ),
+ #'log_user_text' => $this->user_text,
+ 'log_namespace' => $this->getTitle()->getNamespace(),
+ 'log_title' => $this->getTitle()->getDBkey(),
+ 'log_comment' => $this->getComment(),
+ 'log_params' => $this->params
+ );
+ $dbw->insert( 'logging', $data, __METHOD__ );
+ }
+
+ function importUpload() {
+ wfDebug( __METHOD__ . ": STUB\n" );
+
+ /**
+ // from file revert...
+ $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() ) {
+ */
+
+ /**
+ // from file upload...
+ $this->mLocalFile = wfLocalFile( $nt );
+ $this->mDestName = $this->mLocalFile->getName();
+ //....
+ $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
+ File::DELETE_SOURCE, $this->mFileProps );
+ if ( !$status->isGood() ) {
+ $resultDetails = array( 'internal' => $status->getWikiText() );
+ */
+
+ // @fixme upload() uses $wgUser, which is wrong here
+ // it may also create a page without our desire, also wrong potentially.
+ // and, it will record a *current* upload, but we might want an archive version here
+
+ $file = wfLocalFile( $this->getTitle() );
+ if( !$file ) {
+ var_dump( $file );
+ wfDebug( "IMPORT: Bad file. :(\n" );
+ return false;
+ }
+
+ $source = $this->downloadSource();
+ if( !$source ) {
+ wfDebug( "IMPORT: Could not fetch remote file. :(\n" );
+ return false;
+ }
+
+ $status = $file->upload( $source,
+ $this->getComment(),
+ $this->getComment(), // Initial page, if none present...
+ File::DELETE_SOURCE,
+ false, // props...
+ $this->getTimestamp() );
+
+ if( $status->isGood() ) {
+ // yay?
+ wfDebug( "IMPORT: is ok?\n" );
+ return true;
+ }
+
+ wfDebug( "IMPORT: is bad? " . $status->getXml() . "\n" );
+ return false;
+
+ }
+
+ function downloadSource() {
+ global $wgEnableUploads;
+ if( !$wgEnableUploads ) {
+ return false;
+ }
+
+ $tempo = tempnam( wfTempDir(), 'download' );
+ $f = fopen( $tempo, 'wb' );
+ if( !$f ) {
+ wfDebug( "IMPORT: couldn't write to temp file $tempo\n" );
+ return false;
+ }
+
+ // @fixme!
+ $src = $this->getSrc();
+ $data = Http::get( $src );
+ if( !$data ) {
+ wfDebug( "IMPORT: couldn't fetch source $src\n" );
+ fclose( $f );
+ unlink( $tempo );
+ return false;
+ }
+
+ fwrite( $f, $data );
+ fclose( $f );
+
+ return $tempo;
+ }
+
+}
+
+/**
+ * implements Special:Import
+ * @ingroup SpecialPage
+ */
+class WikiImporter {
+ var $mDebug = false;
+ var $mSource = null;
+ var $mPageCallback = null;
+ var $mPageOutCallback = null;
+ var $mRevisionCallback = null;
+ var $mLogItemCallback = null;
+ var $mUploadCallback = null;
+ var $mTargetNamespace = null;
+ var $mXmlNamespace = false;
+ var $lastfield;
+ var $tagStack = array();
+
+ function __construct( $source ) {
+ $this->setRevisionCallback( array( $this, "importRevision" ) );
+ $this->setUploadCallback( array( $this, "importUpload" ) );
+ $this->setLogItemCallback( array( $this, "importLogItem" ) );
+ $this->mSource = $source;
+ }
+
+ function throwXmlError( $err ) {
+ $this->debug( "FAILURE: $err" );
+ wfDebug( "WikiImporter XML error: $err\n" );
+ }
+
+ function handleXmlNamespace ( $parser, $data, $prefix=false, $uri=false ) {
+ if( preg_match( '/www.mediawiki.org/',$prefix ) ) {
+ $prefix = str_replace( '/','\/',$prefix );
+ $this->mXmlNamespace='/^'.$prefix.':/';
+ }
+ }
+
+ function stripXmlNamespace($name) {
+ if( $this->mXmlNamespace ) {
+ return(preg_replace($this->mXmlNamespace,'',$name,1));
+ }
+ else {
+ return($name);
+ }
+ }
+
+ # --------------
+
+ function doImport() {
+ if( empty( $this->mSource ) ) {
+ return new WikiErrorMsg( "importnotext" );
+ }
+
+ $parser = xml_parser_create_ns( "UTF-8" );
+
+ # case folding violates XML standard, turn it off
+ xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
+
+ xml_set_object( $parser, $this );
+ xml_set_element_handler( $parser, "in_start", "" );
+ xml_set_start_namespace_decl_handler( $parser, "handleXmlNamespace" );
+
+ $offset = 0; // for context extraction on error reporting
+ do {
+ $chunk = $this->mSource->readChunk();
+ if( !xml_parse( $parser, $chunk, $this->mSource->atEnd() ) ) {
+ wfDebug( "WikiImporter::doImport encountered XML parsing error\n" );
+ return new WikiXmlError( $parser, wfMsgHtml( 'import-parse-failure' ), $chunk, $offset );
+ }
+ $offset += strlen( $chunk );
+ } while( $chunk !== false && !$this->mSource->atEnd() );
+ xml_parser_free( $parser );
+
+ return true;
+ }
+
+ function debug( $data ) {
+ if( $this->mDebug ) {
+ wfDebug( "IMPORT: $data\n" );
+ }
+ }
+
+ function notice( $data ) {
+ global $wgCommandLineMode;
+ if( $wgCommandLineMode ) {
+ print "$data\n";
+ } else {
+ global $wgOut;
+ $wgOut->addHTML( "<li>" . htmlspecialchars( $data ) . "</li>\n" );
+ }
+ }
+
+ /**
+ * Set debug mode...
+ */
+ function setDebug( $debug ) {
+ $this->mDebug = $debug;
+ }
+
+ /**
+ * Sets the action to perform as each new page in the stream is reached.
+ * @param $callback callback
+ * @return callback
+ */
+ function setPageCallback( $callback ) {
+ $previous = $this->mPageCallback;
+ $this->mPageCallback = $callback;
+ return $previous;
+ }
+
+ /**
+ * Sets the action to perform as each page in the stream is completed.
+ * Callback accepts the page title (as a Title object), a second object
+ * with the original title form (in case it's been overridden into a
+ * local namespace), and a count of revisions.
+ *
+ * @param $callback callback
+ * @return callback
+ */
+ function setPageOutCallback( $callback ) {
+ $previous = $this->mPageOutCallback;
+ $this->mPageOutCallback = $callback;
+ return $previous;
+ }
+
+ /**
+ * Sets the action to perform as each page revision is reached.
+ * @param $callback callback
+ * @return callback
+ */
+ function setRevisionCallback( $callback ) {
+ $previous = $this->mRevisionCallback;
+ $this->mRevisionCallback = $callback;
+ return $previous;
+ }
+
+ /**
+ * Sets the action to perform as each file upload version is reached.
+ * @param $callback callback
+ * @return callback
+ */
+ function setUploadCallback( $callback ) {
+ $previous = $this->mUploadCallback;
+ $this->mUploadCallback = $callback;
+ return $previous;
+ }
+
+ /**
+ * Sets the action to perform as each log item reached.
+ * @param $callback callback
+ * @return callback
+ */
+ function setLogItemCallback( $callback ) {
+ $previous = $this->mLogItemCallback;
+ $this->mLogItemCallback = $callback;
+ return $previous;
+ }
+
+ /**
+ * Set a target namespace to override the defaults
+ */
+ function setTargetNamespace( $namespace ) {
+ if( is_null( $namespace ) ) {
+ // Don't override namespaces
+ $this->mTargetNamespace = null;
+ } elseif( $namespace >= 0 ) {
+ // FIXME: Check for validity
+ $this->mTargetNamespace = intval( $namespace );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Default per-revision callback, performs the import.
+ * @param $revision WikiRevision
+ * @private
+ */
+ function importRevision( $revision ) {
+ $dbw = wfGetDB( DB_MASTER );
+ return $dbw->deadlockLoop( array( $revision, 'importOldRevision' ) );
+ }
+
+ /**
+ * Default per-revision callback, performs the import.
+ * @param $revision WikiRevision
+ * @private
+ */
+ function importLogItem( $rev ) {
+ $dbw = wfGetDB( DB_MASTER );
+ return $dbw->deadlockLoop( array( $rev, 'importLogItem' ) );
+ }
+
+ /**
+ * Dummy for now...
+ */
+ function importUpload( $revision ) {
+ //$dbw = wfGetDB( DB_MASTER );
+ //return $dbw->deadlockLoop( array( $revision, 'importUpload' ) );
+ return false;
+ }
+
+ /**
+ * Alternate per-revision callback, for debugging.
+ * @param $revision WikiRevision
+ * @private
+ */
+ function debugRevisionHandler( &$revision ) {
+ $this->debug( "Got revision:" );
+ if( is_object( $revision->title ) ) {
+ $this->debug( "-- Title: " . $revision->title->getPrefixedText() );
+ } else {
+ $this->debug( "-- Title: <invalid>" );
+ }
+ $this->debug( "-- User: " . $revision->user_text );
+ $this->debug( "-- Timestamp: " . $revision->timestamp );
+ $this->debug( "-- Comment: " . $revision->comment );
+ $this->debug( "-- Text: " . $revision->text );
+ }
+
+ /**
+ * Notify the callback function when a new <page> is reached.
+ * @param $title Title
+ * @private
+ */
+ function pageCallback( $title ) {
+ if( is_callable( $this->mPageCallback ) ) {
+ call_user_func( $this->mPageCallback, $title );
+ }
+ }
+
+ /**
+ * Notify the callback function when a </page> is closed.
+ * @param $title Title
+ * @param $origTitle Title
+ * @param $revisionCount int
+ * @param $successCount Int: number of revisions for which callback returned true
+ * @private
+ */
+ function pageOutCallback( $title, $origTitle, $revisionCount, $successCount ) {
+ if( is_callable( $this->mPageOutCallback ) ) {
+ call_user_func( $this->mPageOutCallback, $title, $origTitle,
+ $revisionCount, $successCount );
+ }
+ }
+
+ # XML parser callbacks from here out -- beware!
+ function donothing( $parser, $x, $y="" ) {
+ #$this->debug( "donothing" );
+ }
+
+ function in_start( $parser, $name, $attribs ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "in_start $name" );
+ if( $name != "mediawiki" ) {
+ return $this->throwXMLerror( "Expected <mediawiki>, got <$name>" );
+ }
+ xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
+ }
+
+ function in_mediawiki( $parser, $name, $attribs ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "in_mediawiki $name" );
+ if( $name == 'siteinfo' ) {
+ xml_set_element_handler( $parser, "in_siteinfo", "out_siteinfo" );
+ } elseif( $name == 'page' ) {
+ $this->push( $name );
+ $this->workRevisionCount = 0;
+ $this->workSuccessCount = 0;
+ $this->uploadCount = 0;
+ $this->uploadSuccessCount = 0;
+ xml_set_element_handler( $parser, "in_page", "out_page" );
+ } elseif( $name == 'logitem' ) {
+ $this->push( $name );
+ $this->workRevision = new WikiRevision;
+ xml_set_element_handler( $parser, "in_logitem", "out_logitem" );
+ } else {
+ return $this->throwXMLerror( "Expected <page>, got <$name>" );
+ }
+ }
+ function out_mediawiki( $parser, $name ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "out_mediawiki $name" );
+ if( $name != "mediawiki" ) {
+ return $this->throwXMLerror( "Expected </mediawiki>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "donothing", "donothing" );
+ }
+
+
+ function in_siteinfo( $parser, $name, $attribs ) {
+ // no-ops for now
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "in_siteinfo $name" );
+ switch( $name ) {
+ case "sitename":
+ case "base":
+ case "generator":
+ case "case":
+ case "namespaces":
+ case "namespace":
+ break;
+ default:
+ return $this->throwXMLerror( "Element <$name> not allowed in <siteinfo>." );
+ }
+ }
+
+ function out_siteinfo( $parser, $name ) {
+ $name = $this->stripXmlNamespace($name);
+ if( $name == "siteinfo" ) {
+ xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
+ }
+ }
+
+
+ function in_page( $parser, $name, $attribs ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "in_page $name" );
+ switch( $name ) {
+ case "id":
+ case "title":
+ case "restrictions":
+ $this->appendfield = $name;
+ $this->appenddata = "";
+ xml_set_element_handler( $parser, "in_nothing", "out_append" );
+ xml_set_character_data_handler( $parser, "char_append" );
+ break;
+ case "revision":
+ $this->push( "revision" );
+ if( is_object( $this->pageTitle ) ) {
+ $this->workRevision = new WikiRevision;
+ $this->workRevision->setTitle( $this->pageTitle );
+ $this->workRevisionCount++;
+ } else {
+ // Skipping items due to invalid page title
+ $this->workRevision = null;
+ }
+ xml_set_element_handler( $parser, "in_revision", "out_revision" );
+ break;
+ case "upload":
+ $this->push( "upload" );
+ if( is_object( $this->pageTitle ) ) {
+ $this->workRevision = new WikiRevision;
+ $this->workRevision->setTitle( $this->pageTitle );
+ $this->uploadCount++;
+ } else {
+ // Skipping items due to invalid page title
+ $this->workRevision = null;
+ }
+ xml_set_element_handler( $parser, "in_upload", "out_upload" );
+ break;
+ default:
+ return $this->throwXMLerror( "Element <$name> not allowed in a <page>." );
+ }
+ }
+
+ function out_page( $parser, $name ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "out_page $name" );
+ $this->pop();
+ if( $name != "page" ) {
+ return $this->throwXMLerror( "Expected </page>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
+
+ $this->pageOutCallback( $this->pageTitle, $this->origTitle,
+ $this->workRevisionCount, $this->workSuccessCount );
+
+ $this->workTitle = null;
+ $this->workRevision = null;
+ $this->workRevisionCount = 0;
+ $this->workSuccessCount = 0;
+ $this->pageTitle = null;
+ $this->origTitle = null;
+ }
+
+ function in_nothing( $parser, $name, $attribs ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "in_nothing $name" );
+ return $this->throwXMLerror( "No child elements allowed here; got <$name>" );
+ }
+
+ function char_append( $parser, $data ) {
+ $this->debug( "char_append '$data'" );
+ $this->appenddata .= $data;
+ }
+
+ function out_append( $parser, $name ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "out_append $name" );
+ if( $name != $this->appendfield ) {
+ return $this->throwXMLerror( "Expected </{$this->appendfield}>, got </$name>" );
+ }
+
+ switch( $this->appendfield ) {
+ case "title":
+ $this->workTitle = $this->appenddata;
+ $this->origTitle = Title::newFromText( $this->workTitle );
+ if( !is_null( $this->mTargetNamespace ) && !is_null( $this->origTitle ) ) {
+ $this->pageTitle = Title::makeTitle( $this->mTargetNamespace,
+ $this->origTitle->getDBkey() );
+ } else {
+ $this->pageTitle = Title::newFromText( $this->workTitle );
+ }
+ if( is_null( $this->pageTitle ) ) {
+ // Invalid page title? Ignore the page
+ $this->notice( "Skipping invalid page title '$this->workTitle'" );
+ } elseif( $this->pageTitle->getInterwiki() != '' ) {
+ $this->notice( "Skipping interwiki page title '$this->workTitle'" );
+ $this->pageTitle = null;
+ } else {
+ $this->pageCallback( $this->workTitle );
+ }
+ break;
+ case "id":
+ if ( $this->parentTag() == 'revision' || $this->parentTag() == 'logitem' ) {
+ if( $this->workRevision )
+ $this->workRevision->setID( $this->appenddata );
+ }
+ break;
+ case "text":
+ if( $this->workRevision )
+ $this->workRevision->setText( $this->appenddata );
+ break;
+ case "username":
+ if( $this->workRevision )
+ $this->workRevision->setUsername( $this->appenddata );
+ break;
+ case "ip":
+ if( $this->workRevision )
+ $this->workRevision->setUserIP( $this->appenddata );
+ break;
+ case "timestamp":
+ if( $this->workRevision )
+ $this->workRevision->setTimestamp( $this->appenddata );
+ break;
+ case "comment":
+ if( $this->workRevision )
+ $this->workRevision->setComment( $this->appenddata );
+ break;
+ case "type":
+ if( $this->workRevision )
+ $this->workRevision->setType( $this->appenddata );
+ break;
+ case "action":
+ if( $this->workRevision )
+ $this->workRevision->setAction( $this->appenddata );
+ break;
+ case "logtitle":
+ if( $this->workRevision )
+ $this->workRevision->setTitle( Title::newFromText( $this->appenddata ) );
+ break;
+ case "params":
+ if( $this->workRevision )
+ $this->workRevision->setParams( $this->appenddata );
+ break;
+ case "minor":
+ if( $this->workRevision )
+ $this->workRevision->setMinor( true );
+ break;
+ case "filename":
+ if( $this->workRevision )
+ $this->workRevision->setFilename( $this->appenddata );
+ break;
+ case "src":
+ if( $this->workRevision )
+ $this->workRevision->setSrc( $this->appenddata );
+ break;
+ case "size":
+ if( $this->workRevision )
+ $this->workRevision->setSize( intval( $this->appenddata ) );
+ break;
+ default:
+ $this->debug( "Bad append: {$this->appendfield}" );
+ }
+ $this->appendfield = "";
+ $this->appenddata = "";
+
+ $parent = $this->parentTag();
+ xml_set_element_handler( $parser, "in_$parent", "out_$parent" );
+ xml_set_character_data_handler( $parser, "donothing" );
+ }
+
+ function in_revision( $parser, $name, $attribs ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "in_revision $name" );
+ switch( $name ) {
+ case "id":
+ case "timestamp":
+ case "comment":
+ case "minor":
+ case "text":
+ $this->appendfield = $name;
+ xml_set_element_handler( $parser, "in_nothing", "out_append" );
+ xml_set_character_data_handler( $parser, "char_append" );
+ break;
+ case "contributor":
+ $this->push( "contributor" );
+ xml_set_element_handler( $parser, "in_contributor", "out_contributor" );
+ break;
+ default:
+ return $this->throwXMLerror( "Element <$name> not allowed in a <revision>." );
+ }
+ }
+
+ function out_revision( $parser, $name ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "out_revision $name" );
+ $this->pop();
+ if( $name != "revision" ) {
+ return $this->throwXMLerror( "Expected </revision>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "in_page", "out_page" );
+
+ if( $this->workRevision ) {
+ $ok = call_user_func_array( $this->mRevisionCallback,
+ array( $this->workRevision, $this ) );
+ if( $ok ) {
+ $this->workSuccessCount++;
+ }
+ }
+ }
+
+ function in_logitem( $parser, $name, $attribs ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "in_logitem $name" );
+ switch( $name ) {
+ case "id":
+ case "timestamp":
+ case "comment":
+ case "type":
+ case "action":
+ case "logtitle":
+ case "params":
+ $this->appendfield = $name;
+ xml_set_element_handler( $parser, "in_nothing", "out_append" );
+ xml_set_character_data_handler( $parser, "char_append" );
+ break;
+ case "contributor":
+ $this->push( "contributor" );
+ xml_set_element_handler( $parser, "in_contributor", "out_contributor" );
+ break;
+ default:
+ return $this->throwXMLerror( "Element <$name> not allowed in a <revision>." );
+ }
+ }
+
+ function out_logitem( $parser, $name ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "out_logitem $name" );
+ $this->pop();
+ if( $name != "logitem" ) {
+ return $this->throwXMLerror( "Expected </logitem>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
+
+ if( $this->workRevision ) {
+ $ok = call_user_func_array( $this->mLogItemCallback,
+ array( $this->workRevision, $this ) );
+ if( $ok ) {
+ $this->workSuccessCount++;
+ }
+ }
+ }
+
+ function in_upload( $parser, $name, $attribs ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "in_upload $name" );
+ switch( $name ) {
+ case "timestamp":
+ case "comment":
+ case "text":
+ case "filename":
+ case "src":
+ case "size":
+ $this->appendfield = $name;
+ xml_set_element_handler( $parser, "in_nothing", "out_append" );
+ xml_set_character_data_handler( $parser, "char_append" );
+ break;
+ case "contributor":
+ $this->push( "contributor" );
+ xml_set_element_handler( $parser, "in_contributor", "out_contributor" );
+ break;
+ default:
+ return $this->throwXMLerror( "Element <$name> not allowed in an <upload>." );
+ }
+ }
+
+ function out_upload( $parser, $name ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "out_revision $name" );
+ $this->pop();
+ if( $name != "upload" ) {
+ return $this->throwXMLerror( "Expected </upload>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "in_page", "out_page" );
+
+ if( $this->workRevision ) {
+ $ok = call_user_func_array( $this->mUploadCallback,
+ array( $this->workRevision, $this ) );
+ if( $ok ) {
+ $this->workUploadSuccessCount++;
+ }
+ }
+ }
+
+ function in_contributor( $parser, $name, $attribs ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "in_contributor $name" );
+ switch( $name ) {
+ case "username":
+ case "ip":
+ case "id":
+ $this->appendfield = $name;
+ xml_set_element_handler( $parser, "in_nothing", "out_append" );
+ xml_set_character_data_handler( $parser, "char_append" );
+ break;
+ default:
+ $this->throwXMLerror( "Invalid tag <$name> in <contributor>" );
+ }
+ }
+
+ function out_contributor( $parser, $name ) {
+ $name = $this->stripXmlNamespace($name);
+ $this->debug( "out_contributor $name" );
+ $this->pop();
+ if( $name != "contributor" ) {
+ return $this->throwXMLerror( "Expected </contributor>, got </$name>" );
+ }
+ $parent = $this->parentTag();
+ xml_set_element_handler( $parser, "in_$parent", "out_$parent" );
+ }
+
+ private function push( $name ) {
+ array_push( $this->tagStack, $name );
+ $this->debug( "PUSH $name" );
+ }
+
+ private function pop() {
+ $name = array_pop( $this->tagStack );
+ $this->debug( "POP $name" );
+ return $name;
+ }
+
+ private function parentTag() {
+ $name = $this->tagStack[count( $this->tagStack ) - 1];
+ $this->debug( "PARENT $name" );
+ return $name;
+ }
+
+}
+
+/**
+ * @todo document (e.g. one-sentence class description).
+ * @ingroup SpecialPage
+ */
+class ImportStringSource {
+ function __construct( $string ) {
+ $this->mString = $string;
+ $this->mRead = false;
+ }
+
+ function atEnd() {
+ return $this->mRead;
+ }
+
+ function readChunk() {
+ if( $this->atEnd() ) {
+ return false;
+ } else {
+ $this->mRead = true;
+ return $this->mString;
+ }
+ }
+}
+
+/**
+ * @todo document (e.g. one-sentence class description).
+ * @ingroup SpecialPage
+ */
+class ImportStreamSource {
+ function __construct( $handle ) {
+ $this->mHandle = $handle;
+ }
+
+ function atEnd() {
+ return feof( $this->mHandle );
+ }
+
+ function readChunk() {
+ return fread( $this->mHandle, 32768 );
+ }
+
+ static function newFromFile( $filename ) {
+ $file = @fopen( $filename, 'rt' );
+ if( !$file ) {
+ return new WikiErrorMsg( "importcantopen" );
+ }
+ return new ImportStreamSource( $file );
+ }
+
+ static function newFromUpload( $fieldname = "xmlimport" ) {
+ $upload =& $_FILES[$fieldname];
+
+ if( !isset( $upload ) || !$upload['name'] ) {
+ return new WikiErrorMsg( 'importnofile' );
+ }
+ if( !empty( $upload['error'] ) ) {
+ switch($upload['error']){
+ case 1: # The uploaded file exceeds the upload_max_filesize directive in php.ini.
+ return new WikiErrorMsg( 'importuploaderrorsize' );
+ case 2: # The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.
+ return new WikiErrorMsg( 'importuploaderrorsize' );
+ case 3: # The uploaded file was only partially uploaded
+ return new WikiErrorMsg( 'importuploaderrorpartial' );
+ case 6: #Missing a temporary folder. Introduced in PHP 4.3.10 and PHP 5.0.3.
+ return new WikiErrorMsg( 'importuploaderrortemp' );
+ # case else: # Currently impossible
+ }
+
+ }
+ $fname = $upload['tmp_name'];
+ if( is_uploaded_file( $fname ) ) {
+ return ImportStreamSource::newFromFile( $fname );
+ } else {
+ return new WikiErrorMsg( 'importnofile' );
+ }
+ }
+
+ static function newFromURL( $url, $method = 'GET' ) {
+ wfDebug( __METHOD__ . ": opening $url\n" );
+ # 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 ) {
+ if( $page == '' ) {
+ return new WikiErrorMsg( 'import-noarticle' );
+ }
+ $link = Title::newFromText( "$interwiki:Special:Export/$page" );
+ if( is_null( $link ) || $link->getInterwiki() == '' ) {
+ return new WikiErrorMsg( 'importbadinterwiki' );
+ } else {
+ $params = $history ? 'history=1' : '';
+ $url = $link->getFullUrl( $params );
+ # For interwikis, use POST to avoid redirects.
+ return ImportStreamSource::newFromURL( $url, "POST" );
+ }
+ }
+}
diff --git a/includes/Interwiki.php b/includes/Interwiki.php
new file mode 100644
index 00000000..3522fadb
--- /dev/null
+++ b/includes/Interwiki.php
@@ -0,0 +1,207 @@
+<?php
+/**
+ * @file
+ * Interwiki table entry
+ */
+
+/**
+ * The interwiki class
+ * All information is loaded on creation when called by Interwiki::fetch( $prefix ).
+ * All work is done on slave, because this should *never* change (except during schema updates etc, which arent wiki-related)
+ */
+class Interwiki {
+
+ // Cache - removes oldest entry when it hits limit
+ protected static $smCache = array();
+ const CACHE_LIMIT = 100; // 0 means unlimited, any other value is max number of entries.
+
+ protected $mPrefix, $mURL, $mLocal, $mTrans;
+
+ function __construct( $prefix = null, $url = '', $local = 0, $trans = 0 )
+ {
+ $this->mPrefix = $prefix;
+ $this->mURL = $url;
+ $this->mLocal = $local;
+ $this->mTrans = $trans;
+ }
+
+ /**
+ * Check whether an interwiki prefix exists
+ *
+ * @return bool Whether it exists
+ * @param $prefix string Interwiki prefix to use
+ */
+ static public function isValidInterwiki( $prefix ){
+ $result = self::fetch( $prefix );
+ return (bool)$result;
+ }
+
+ /**
+ * Fetch an Interwiki object
+ *
+ * @return Interwiki Object, or null if not valid
+ * @param $prefix string Interwiki prefix to use
+ */
+ static public function fetch( $prefix ) {
+ global $wgContLang;
+ if( $prefix == '' ) {
+ return null;
+ }
+ $prefix = $wgContLang->lc( $prefix );
+ if( isset( self::$smCache[$prefix] ) ){
+ return self::$smCache[$prefix];
+ }
+ global $wgInterwikiCache;
+ if ($wgInterwikiCache) {
+ $iw = Interwiki::getInterwikiCached( $prefix );
+ } else {
+ $iw = Interwiki::load( $prefix );
+ if( !$iw ){
+ $iw = false;
+ }
+ }
+ if( self::CACHE_LIMIT && count( self::$smCache ) >= self::CACHE_LIMIT ){
+ reset( self::$smCache );
+ unset( self::$smCache[ key( self::$smCache ) ] );
+ }
+ self::$smCache[$prefix] = $iw;
+ return $iw;
+ }
+
+ /**
+ * Fetch interwiki prefix data from local cache in constant database.
+ *
+ * @note More logic is explained in DefaultSettings.
+ *
+ * @param $prefix \type{\string} Interwiki prefix
+ * @return \type{\Interwiki} An interwiki object
+ */
+ protected static function getInterwikiCached( $prefix ) {
+ $value = self::getInterwikiCacheEntry( $prefix );
+
+ $s = new Interwiki( $prefix );
+ if ( $value != '' ) {
+ // Split values
+ list( $local, $url ) = explode( ' ', $value, 2 );
+ $s->mURL = $url;
+ $s->mLocal = (int)$local;
+ }else{
+ $s = false;
+ }
+ return $s;
+ }
+
+ /**
+ * Get entry from interwiki cache
+ *
+ * @note More logic is explained in DefaultSettings.
+ *
+ * @param $prefix \type{\string} Database key
+ * @return \type{\string) The entry
+ */
+ protected static function getInterwikiCacheEntry( $prefix ){
+ global $wgInterwikiCache, $wgInterwikiScopes, $wgInterwikiFallbackSite;
+ static $db, $site;
+
+ wfDebug( __METHOD__ . "( $prefix )\n" );
+ if( !$db ){
+ $db = dba_open( $wgInterwikiCache, 'r', 'cdb' );
+ }
+ /* Resolve site name */
+ if( $wgInterwikiScopes>=3 && !$site ) {
+ $site = dba_fetch( '__sites:' . wfWikiID(), $db );
+ if ( $site == "" ){
+ $site = $wgInterwikiFallbackSite;
+ }
+ }
+
+ $value = dba_fetch( wfMemcKey( $prefix ), $db );
+ // Site level
+ if ( $value == '' && $wgInterwikiScopes >= 3 ) {
+ $value = dba_fetch( "_{$site}:{$prefix}", $db );
+ }
+ // Global Level
+ if ( $value == '' && $wgInterwikiScopes >= 2 ) {
+ $value = dba_fetch( "__global:{$prefix}", $db );
+ }
+ if ( $value == 'undef' )
+ $value = '';
+
+ return $value;
+ }
+
+ /**
+ * Load the interwiki, trying first memcached then the DB
+ *
+ * @param $prefix The interwiki prefix
+ * @return bool The prefix is valid
+ * @static
+ *
+ */
+ protected static function load( $prefix ) {
+ global $wgMemc, $wgInterwikiExpiry;
+ $key = wfMemcKey( 'interwiki', $prefix );
+ $mc = $wgMemc->get( $key );
+ $iw = false;
+ if( $mc && is_array( $mc ) ){ // is_array is hack for old keys
+ $iw = Interwiki::loadFromArray( $mc );
+ if( $iw ){
+ return $iw;
+ }
+ }
+
+ $db = wfGetDB( DB_SLAVE );
+
+ $row = $db->fetchRow( $db->select( 'interwiki', '*', array( 'iw_prefix' => $prefix ),
+ __METHOD__ ) );
+ $iw = Interwiki::loadFromArray( $row );
+ if ( $iw ) {
+ $mc = array( 'iw_url' => $iw->mURL, 'iw_local' => $iw->mLocal, 'iw_trans' => $iw->mTrans );
+ $wgMemc->add( $key, $mc, $wgInterwikiExpiry );
+ return $iw;
+ }
+
+ return false;
+ }
+
+ /**
+ * Fill in member variables from an array (e.g. memcached result, Database::fetchRow, etc)
+ *
+ * @return bool Whether everything was there
+ * @param $res ResultWrapper Row from the interwiki table
+ * @static
+ */
+ protected static function loadFromArray( $mc ) {
+ if( isset( $mc['iw_url'] ) && isset( $mc['iw_local'] ) && isset( $mc['iw_trans'] ) ){
+ $iw = new Interwiki();
+ $iw->mURL = $mc['iw_url'];
+ $iw->mLocal = $mc['iw_local'];
+ $iw->mTrans = $mc['iw_trans'];
+ return $iw;
+ }
+ return false;
+ }
+
+ /**
+ * Get the URL for a particular title (or with $1 if no title given)
+ *
+ * @param $title string What text to put for the article name
+ * @return string The URL
+ */
+ function getURL( $title = null ){
+ $url = $this->mURL;
+ if( $title != null ){
+ $url = str_replace( "$1", $title, $url );
+ }
+ return $url;
+ }
+
+ function isLocal(){
+ return $this->mLocal;
+ }
+
+ function isTranscludable(){
+ return $this->mTrans;
+ }
+
+}
diff --git a/includes/JobQueue.php b/includes/JobQueue.php
index 8bfd1b3e..afa757d7 100644
--- a/includes/JobQueue.php
+++ b/includes/JobQueue.php
@@ -127,7 +127,7 @@ abstract class Job {
// Failed, someone else beat us to it
// Try getting a random row
$row = $dbw->selectRow( 'job', array( 'MIN(job_id) as minjob',
- 'MAX(job_id) as maxjob' ), "job_id >= $offset", __METHOD__ );
+ 'MAX(job_id) as maxjob' ), '1=1', __METHOD__ );
if ( $row === false || is_null( $row->minjob ) || is_null( $row->maxjob ) ) {
// No jobs to get
wfProfileOut( __METHOD__ );
diff --git a/includes/Licenses.php b/includes/Licenses.php
index e76ac23c..6398c887 100644
--- a/includes/Licenses.php
+++ b/includes/Licenses.php
@@ -121,7 +121,7 @@ class Licenses {
function outputOption( $val, $attribs = null, $depth ) {
$val = str_repeat( /* &nbsp */ "\xc2\xa0", $depth * 2 ) . $val;
- return str_repeat( "\t", $depth ) . wfElement( 'option', $attribs, $val ) . "\n";
+ return str_repeat( "\t", $depth ) . Xml::element( 'option', $attribs, $val ) . "\n";
}
function msg( $str ) {
diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php
index bdc4b43a..d9a9666d 100644
--- a/includes/LinkBatch.php
+++ b/includes/LinkBatch.php
@@ -134,7 +134,7 @@ class LinkBatch {
$sql = "SELECT page_id, page_namespace, page_title, page_len, page_is_redirect FROM $page WHERE $set";
// Do query
- $res = new ResultWrapper( $dbr, $dbr->query( $sql, __METHOD__ ) );
+ $res = $dbr->query( $sql, __METHOD__ );
wfProfileOut( __METHOD__ );
return $res;
}
diff --git a/includes/LinkCache.php b/includes/LinkCache.php
index 79727615..4f74cdd7 100644
--- a/includes/LinkCache.php
+++ b/includes/LinkCache.php
@@ -9,7 +9,6 @@ class LinkCache {
// becomes incompatible with the new version.
/* private */ var $mClassVer = 4;
- /* private */ var $mPageLinks;
/* private */ var $mGoodLinks, $mBadLinks;
/* private */ var $mForUpdate;
@@ -26,7 +25,6 @@ class LinkCache {
function __construct() {
$this->mForUpdate = false;
- $this->mPageLinks = array();
$this->mGoodLinks = array();
$this->mGoodLinkFields = array();
$this->mBadLinks = array();
@@ -78,14 +76,12 @@ class LinkCache {
$dbkey = $title->getPrefixedDbKey();
$this->mGoodLinks[$dbkey] = $id;
$this->mGoodLinkFields[$dbkey] = array( 'length' => $len, 'redirect' => $redir );
- $this->mPageLinks[$dbkey] = $title;
}
public function addBadLinkObj( $title ) {
$dbkey = $title->getPrefixedDbKey();
- if ( ! $this->isBadLink( $dbkey ) ) {
+ if ( !$this->isBadLink( $dbkey ) ) {
$this->mBadLinks[$dbkey] = 1;
- $this->mPageLinks[$dbkey] = $title;
}
}
@@ -93,10 +89,19 @@ class LinkCache {
unset( $this->mBadLinks[$title] );
}
- /* obsolete, for old $wgLinkCacheMemcached stuff */
- public function clearLink( $title ) {}
+ public function clearLink( $title ) {
+ $dbkey = $title->getPrefixedDbKey();
+ if( isset($this->mBadLinks[$dbkey]) ) {
+ unset($this->mBadLinks[$dbkey]);
+ }
+ if( isset($this->mGoodLinks[$dbkey]) ) {
+ unset($this->mGoodLinks[$dbkey]);
+ }
+ if( isset($this->mGoodLinkFields[$dbkey]) ) {
+ unset($this->mGoodLinkFields[$dbkey]);
+ }
+ }
- public function getPageLinks() { return $this->mPageLinks; }
public function getGoodLinks() { return $this->mGoodLinks; }
public function getBadLinks() { return array_keys( $this->mBadLinks ); }
@@ -125,27 +130,24 @@ class LinkCache {
*/
public function addLinkObj( &$nt, $len = -1, $redirect = NULL ) {
global $wgAntiLockFlags, $wgProfiler;
+ wfProfileIn( __METHOD__ );
- $title = $nt->getPrefixedDBkey();
- if ( $this->isBadLink( $title ) ) { return 0; }
- $id = $this->getGoodLinkID( $title );
- if ( 0 != $id ) { return $id; }
-
- $fname = 'LinkCache::addLinkObj';
- if ( isset( $wgProfiler ) ) {
- $fname .= ' (' . $wgProfiler->getCurrentSection() . ')';
+ $key = $nt->getPrefixedDBkey();
+ if ( $this->isBadLink( $key ) ) {
+ wfProfileOut( __METHOD__ );
+ return 0;
+ }
+ $id = $this->getGoodLinkID( $key );
+ if ( $id != 0 ) {
+ wfProfileOut( __METHOD__ );
+ return $id;
}
- wfProfileIn( $fname );
-
- $ns = $nt->getNamespace();
- $t = $nt->getDBkey();
-
- if ( '' == $title ) {
- wfProfileOut( $fname );
+ if ( $key === '' ) {
+ wfProfileOut( __METHOD__ );
return 0;
}
-
+
# Some fields heavily used for linking...
if ( $this->mForUpdate ) {
$db = wfGetDB( DB_MASTER );
@@ -161,19 +163,24 @@ class LinkCache {
$s = $db->selectRow( 'page',
array( 'page_id', 'page_len', 'page_is_redirect' ),
- array( 'page_namespace' => $ns, 'page_title' => $t ),
- $fname, $options );
+ array( 'page_namespace' => $nt->getNamespace(), 'page_title' => $nt->getDBkey() ),
+ __METHOD__, $options );
# Set fields...
- $id = $s ? $s->page_id : 0;
- $len = $s ? $s->page_len : -1;
- $redirect = $s ? $s->page_is_redirect : 0;
+ if ( $s !== false ) {
+ $id = $s->page_id;
+ $len = $s->page_len;
+ $redirect = $s->page_is_redirect;
+ } else {
+ $len = -1;
+ $redirect = 0;
+ }
- if( 0 == $id ) {
+ if ( $id == 0 ) {
$this->addBadLinkObj( $nt );
} else {
$this->addGoodLinkObj( $id, $nt, $len, $redirect );
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $id;
}
@@ -181,7 +188,6 @@ class LinkCache {
* Clears cache
*/
public function clear() {
- $this->mPageLinks = array();
$this->mGoodLinks = array();
$this->mGoodLinkFields = array();
$this->mBadLinks = array();
diff --git a/includes/Linker.php b/includes/Linker.php
index 32c506a4..f116fb4a 100644
--- a/includes/Linker.php
+++ b/includes/Linker.php
@@ -21,6 +21,7 @@ class Linker {
* @deprecated
*/
function postParseLinkColour( $s = null ) {
+ wfDeprecated( __METHOD__ );
return null;
}
@@ -123,7 +124,9 @@ class Linker {
if ( $t->isRedirect() ) {
# Page is a redirect
$colour = 'mw-redirect';
- } elseif ( $threshold > 0 && $t->getLength() < $threshold && MWNamespace::isContent( $t->getNamespace() ) ) {
+ } elseif ( $threshold > 0 &&
+ $t->exists() && $t->getLength() < $threshold &&
+ MWNamespace::isContent( $t->getNamespace() ) ) {
# Page is a stub
$colour = 'stub';
}
@@ -131,6 +134,194 @@ class Linker {
}
/**
+ * This function returns an HTML link to the given target. It serves a few
+ * purposes:
+ * 1) If $target is a Title, the correct URL to link to will be figured
+ * out automatically.
+ * 2) It automatically adds the usual classes for various types of link
+ * targets: "new" for red links, "stub" for short articles, etc.
+ * 3) It escapes all attribute values safely so there's no risk of XSS.
+ * 4) It provides a default tooltip if the target is a Title (the page
+ * name of the target).
+ * link() replaces the old functions in the makeLink() family.
+ *
+ * @param $target Title Can currently only be a Title, but this may
+ * change to support Images, literal URLs, etc.
+ * @param $text string The HTML contents of the <a> element, i.e.,
+ * the link text. This is raw HTML and will not be escaped. If null,
+ * defaults to the prefixed text of the Title; or if the Title is just a
+ * fragment, the contents of the fragment.
+ * @param $customAttribs array A key => value array of extra HTML attri-
+ * butes, such as title and class. (href is ignored.) Classes will be
+ * merged with the default classes, while other attributes will replace
+ * default attributes. All passed attribute values will be HTML-escaped.
+ * A false attribute value means to suppress that attribute.
+ * @param $query array The query string to append to the URL
+ * you're linking to, in key => value array form. Query keys and values
+ * will be URL-encoded.
+ * @param $options mixed String or array of strings:
+ * 'known': Page is known to exist, so don't check if it does.
+ * 'broken': Page is known not to exist, so don't check if it does.
+ * 'noclasses': Don't add any classes automatically (includes "new",
+ * "stub", "mw-redirect", "extiw"). Only use the class attribute
+ * provided, if any, so you get a simple blue link with no funny i-
+ * cons.
+ * 'forcearticlepath': Use the article path always, even with a querystring.
+ * Has compatibility issues on some setups, so avoid wherever possible.
+ * @return string HTML <a> attribute
+ */
+ public function link( $target, $text = null, $customAttribs = array(), $query = array(), $options = array() ) {
+ wfProfileIn( __METHOD__ );
+ if( !$target instanceof Title ) {
+ return "<!-- ERROR -->$text";
+ }
+ $options = (array)$options;
+
+ $ret = null;
+ if( !wfRunHooks( 'LinkBegin', array( $this, $target, &$text,
+ &$customAttribs, &$query, &$options, &$ret ) ) ) {
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
+ # Normalize the Title if it's a special page
+ $target = $this->normaliseSpecialPage( $target );
+
+ # If we don't know whether the page exists, let's find out.
+ wfProfileIn( __METHOD__ . '-checkPageExistence' );
+ if( !in_array( 'known', $options ) and !in_array( 'broken', $options ) ) {
+ if( $target->isKnown() ) {
+ $options []= 'known';
+ } else {
+ $options []= 'broken';
+ }
+ }
+ wfProfileOut( __METHOD__ . '-checkPageExistence' );
+
+ $oldquery = array();
+ if( in_array( "forcearticlepath", $options ) && $query ){
+ $oldquery = $query;
+ $query = array();
+ }
+
+ # Note: we want the href attribute first, for prettiness.
+ $attribs = array( 'href' => $this->linkUrl( $target, $query, $options ) );
+ if( in_array( 'forcearticlepath', $options ) && $oldquery ){
+ $attribs['href'] = wfAppendQuery( $attribs['href'], wfArrayToCgi( $oldquery ) );
+ }
+
+ $attribs = array_merge(
+ $attribs,
+ $this->linkAttribs( $target, $customAttribs, $options )
+ );
+ if( is_null( $text ) ) {
+ $text = $this->linkText( $target );
+ }
+
+ $ret = null;
+ if( wfRunHooks( 'LinkEnd', array( $this, $target, $options, &$text, &$attribs, &$ret ) ) ) {
+ $ret = Xml::openElement( 'a', $attribs ) . $text . Xml::closeElement( 'a' );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
+ private function linkUrl( $target, $query, $options ) {
+ wfProfileIn( __METHOD__ );
+ # We don't want to include fragments for broken links, because they
+ # generally make no sense.
+ if( in_array( 'broken', $options ) and $target->mFragment !== '' ) {
+ $target = clone $target;
+ $target->mFragment = '';
+ }
+
+ # If it's a broken link, add the appropriate query pieces, unless
+ # there's already an action specified, or unless 'edit' makes no sense
+ # (i.e., for a nonexistent special page).
+ if( in_array( 'broken', $options ) and empty( $query['action'] )
+ and $target->getNamespace() != NS_SPECIAL ) {
+ $query['action'] = 'edit';
+ $query['redlink'] = '1';
+ }
+ $ret = $target->getLinkUrl( $query );
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
+ private function linkAttribs( $target, $attribs, $options ) {
+ wfProfileIn( __METHOD__ );
+ global $wgUser;
+ $defaults = array();
+
+ if( !in_array( 'noclasses', $options ) ) {
+ wfProfileIn( __METHOD__ . '-getClasses' );
+ # Now build the classes.
+ $classes = array();
+
+ if( in_array( 'broken', $options ) ) {
+ $classes[] = 'new';
+ }
+
+ if( $target->isExternal() ) {
+ $classes[] = 'extiw';
+ }
+
+ # Note that redirects never count as stubs here.
+ if ( $target->isRedirect() ) {
+ $classes[] = 'mw-redirect';
+ } elseif( $target->isContentPage() ) {
+ # Check for stub.
+ $threshold = $wgUser->getOption( 'stubthreshold' );
+ if( $threshold > 0 and $target->exists() and $target->getLength() < $threshold ) {
+ $classes[] = 'stub';
+ }
+ }
+ if( $classes != array() ) {
+ $defaults['class'] = implode( ' ', $classes );
+ }
+ wfProfileOut( __METHOD__ . '-getClasses' );
+ }
+
+ # Get a default title attribute.
+ if( in_array( 'known', $options ) ) {
+ $defaults['title'] = $target->getPrefixedText();
+ } else {
+ $defaults['title'] = wfMsg( 'red-link-title', $target->getPrefixedText() );
+ }
+
+ # Finally, merge the custom attribs with the default ones, and iterate
+ # over that, deleting all "false" attributes.
+ $ret = array();
+ $merged = Sanitizer::mergeAttributes( $defaults, $attribs );
+ foreach( $merged as $key => $val ) {
+ # A false value suppresses the attribute, and we don't want the
+ # href attribute to be overridden.
+ if( $key != 'href' and $val !== false ) {
+ $ret[$key] = $val;
+ }
+ }
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
+ private function linkText( $target ) {
+ # We might be passed a non-Title by make*LinkObj(). Fail gracefully.
+ if( !$target instanceof Title ) {
+ return '';
+ }
+
+ # If the target is just a fragment, with no title, we return the frag-
+ # ment text. Otherwise, we return the title text itself.
+ if( $target->getPrefixedText() === '' and $target->getFragment() !== '' ) {
+ return htmlspecialchars( $target->getFragment() );
+ }
+ return htmlspecialchars( $target->getPrefixedText() );
+ }
+
+ /**
+ * @deprecated Use link()
+ *
* This function is a shortcut to makeLinkObj(Title::newFromText($title),...). Do not call
* it if you already have a title object handy. See makeLinkObj for further documentation.
*
@@ -156,6 +347,8 @@ class Linker {
}
/**
+ * @deprecated Use link()
+ *
* This function is a shortcut to makeKnownLinkObj(Title::newFromText($title),...). Do not call
* it if you already have a title object handy. See makeKnownLinkObj for further documentation.
*
@@ -177,6 +370,8 @@ class Linker {
}
/**
+ * @deprecated Use link()
+ *
* This function is a shortcut to makeBrokenLinkObj(Title::newFromText($title),...). Do not call
* it if you already have a title object handy. See makeBrokenLinkObj for further documentation.
*
@@ -198,7 +393,7 @@ class Linker {
}
/**
- * @deprecated use makeColouredLinkObj
+ * @deprecated Use link()
*
* This function is a shortcut to makeStubLinkObj(Title::newFromText($title),...). Do not call
* it if you already have a title object handy. See makeStubLinkObj for further documentation.
@@ -211,6 +406,7 @@ class Linker {
* the end of the link.
*/
function makeStubLink( $title, $text = '', $query = '', $trail = '' ) {
+ wfDeprecated( __METHOD__ );
$nt = Title::newFromText( $title );
if ( $nt instanceof Title ) {
return $this->makeStubLinkObj( $nt, $text, $query, $trail );
@@ -221,6 +417,8 @@ class Linker {
}
/**
+ * @deprecated Use link()
+ *
* Make a link for a title which may or may not be in the database. If you need to
* call this lots of times, pre-fill the link cache with a LinkBatch, otherwise each
* call to this will result in a DB query.
@@ -238,67 +436,21 @@ class Linker {
global $wgUser;
wfProfileIn( __METHOD__ );
- if ( !$nt instanceof Title ) {
- # Fail gracefully
- wfProfileOut( __METHOD__ );
- return "<!-- ERROR -->{$prefix}{$text}{$trail}";
+ $query = wfCgiToArray( $query );
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+ if( $text === '' ) {
+ $text = $this->linkText( $nt );
}
- if ( $nt->isExternal() ) {
- $u = $nt->getFullURL();
- $link = $nt->getPrefixedURL();
- if ( '' == $text ) { $text = $nt->getPrefixedText(); }
- $style = $this->getInterwikiLinkAttributes( $link, $text, 'extiw' );
-
- $inside = '';
- if ( '' != $trail ) {
- $m = array();
- if ( preg_match( '/^([a-z]+)(.*)$$/sD', $trail, $m ) ) {
- $inside = $m[1];
- $trail = $m[2];
- }
- }
- $t = "<a href=\"{$u}\"{$style}>{$text}{$inside}</a>";
-
- wfProfileOut( __METHOD__ );
- return $t;
- } elseif ( $nt->isAlwaysKnown() ) {
- # Image links, special page links and self-links with fragments are always known.
- $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix );
- } else {
- wfProfileIn( __METHOD__.'-immediate' );
+ $ret = $this->link( $nt, "$prefix$text$inside", array(), $query ) . $trail;
- # Handles links to special pages which 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( __METHOD__.'-immediate' );
- wfProfileOut( __METHOD__ );
- return $retVal;
- }
-
- # Work out link colour immediately
- $aid = $nt->getArticleID() ;
- if ( 0 == $aid ) {
- $retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix );
- } else {
- $colour = '';
- if ( $nt->isContentPage() ) {
- $threshold = $wgUser->getOption('stubthreshold');
- $colour = $this->getLinkColour( $nt, $threshold );
- }
- $retVal = $this->makeColouredLinkObj( $nt, $colour, $text, $query, $trail, $prefix );
- }
- wfProfileOut( __METHOD__.'-immediate' );
- }
wfProfileOut( __METHOD__ );
- return $retVal;
+ return $ret;
}
/**
+ * @deprecated Use link()
+ *
* Make a link for a title which definitely exists. This is faster than makeLinkObj because
* it doesn't have to do a database query. It's also valid for interwiki titles and special
* pages.
@@ -315,40 +467,26 @@ class Linker {
function makeKnownLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) {
wfProfileIn( __METHOD__ );
- if ( !$title instanceof Title ) {
- # Fail gracefully
- wfProfileOut( __METHOD__ );
- return "<!-- ERROR -->{$prefix}{$text}{$trail}";
- }
-
- $nt = $this->normaliseSpecialPage( $title );
-
- $u = $nt->escapeLocalURL( $query );
- if ( $nt->getFragment() != '' ) {
- if( $nt->getPrefixedDbkey() == '' ) {
- $u = '';
- if ( '' == $text ) {
- $text = htmlspecialchars( $nt->getFragment() );
- }
- }
- $u .= $nt->getFragmentForURL();
- }
if ( $text == '' ) {
- $text = htmlspecialchars( $nt->getPrefixedText() );
- }
- if ( $style == '' ) {
- $style = $this->getInternalLinkAttributesObj( $nt, $text );
+ $text = $this->linkText( $title );
}
+ $attribs = Sanitizer::mergeAttributes(
+ Sanitizer::decodeTagAttributes( $aprops ),
+ Sanitizer::decodeTagAttributes( $style )
+ );
+ $query = wfCgiToArray( $query );
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
- if ( $aprops !== '' ) $aprops = ' ' . $aprops;
+ $ret = $this->link( $title, "$prefix$text$inside", $attribs, $query,
+ array( 'known', 'noclasses' ) ) . $trail;
- list( $inside, $trail ) = Linker::splitTrail( $trail );
- $r = "<a href=\"{$u}\"{$style}{$aprops}>{$prefix}{$text}{$inside}</a>{$trail}";
wfProfileOut( __METHOD__ );
- return $r;
+ return $ret;
}
/**
+ * @deprecated Use link()
+ *
* Make a red link to the edit page of a given title.
*
* @param $nt Title object of the target page
@@ -361,40 +499,21 @@ class Linker {
function makeBrokenLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) {
wfProfileIn( __METHOD__ );
- if ( !$title instanceof Title ) {
- # Fail gracefully
- wfProfileOut( __METHOD__ );
- return "<!-- ERROR -->{$prefix}{$text}{$trail}";
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+ if( $text === '' ) {
+ $text = $this->linkText( $title );
}
-
$nt = $this->normaliseSpecialPage( $title );
- if( $nt->getNamespace() == NS_SPECIAL ) {
- $q = $query;
- } else if ( '' == $query ) {
- $q = 'action=edit&redlink=1';
- } else {
- $q = 'action=edit&redlink=1&'.$query;
- }
- $u = $nt->escapeLocalURL( $q );
-
- $titleText = $nt->getPrefixedText();
- if ( '' == $text ) {
- $text = htmlspecialchars( $titleText );
- }
- $titleAttr = wfMsg( 'red-link-title', $titleText );
- $style = $this->getInternalLinkAttributesObj( $nt, $text, 'new', $titleAttr );
- list( $inside, $trail ) = Linker::splitTrail( $trail );
-
- wfRunHooks( 'BrokenLink', array( &$this, $nt, $query, &$u, &$style, &$prefix, &$text, &$inside, &$trail ) );
- $s = "<a href=\"{$u}\"{$style}>{$prefix}{$text}{$inside}</a>{$trail}";
+ $ret = $this->link( $title, "$prefix$text$inside", array(),
+ wfCgiToArray( $query ), 'broken' ) . $trail;
wfProfileOut( __METHOD__ );
- return $s;
+ return $ret;
}
/**
- * @deprecated use makeColouredLinkObj
+ * @deprecated Use link()
*
* Make a brown link to a short article.
*
@@ -406,10 +525,13 @@ class Linker {
* the end of the link.
*/
function makeStubLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ wfDeprecated( __METHOD__ );
return $this->makeColouredLinkObj( $nt, 'stub', $text, $query, $trail, $prefix );
}
/**
+ * @deprecated Use link()
+ *
* Make a coloured link.
*
* @param $nt Title object of the target page
@@ -421,7 +543,6 @@ class Linker {
* the end of the link.
*/
function makeColouredLinkObj( $nt, $colour, $text = '', $query = '', $trail = '', $prefix = '' ) {
-
if($colour != ''){
$style = $this->getInternalLinkAttributesObj( $nt, $text, $colour );
} else $style = '';
@@ -464,7 +585,9 @@ class Linker {
if ( $title->getNamespace() == NS_SPECIAL ) {
list( $name, $subpage ) = SpecialPage::resolveAliasWithSubpage( $title->getDBkey() );
if ( !$name ) return $title;
- return SpecialPage::getTitleFor( $name, $subpage );
+ $ret = SpecialPage::getTitleFor( $name, $subpage );
+ $ret->mFragment = $title->getFragment();
+ return $ret;
} else {
return $title;
}
@@ -483,6 +606,7 @@ class Linker {
/** Obsolete alias */
function makeImage( $url, $alt = '' ) {
+ wfDeprecated( __METHOD__ );
return $this->makeExternalImage( $url, $alt );
}
@@ -564,6 +688,9 @@ class Linker {
* bottom, text-bottom)
* alt Alternate text for image (i.e. alt attribute). Plain text.
* caption HTML for image caption.
+ * link-url URL to link to
+ * link-title Title object to link to
+ * no-link Boolean, suppress description link
*
* @param array $handlerParams Associative array of media handler parameters, to be passed
* to transform(). Typical keys are "width" and "page".
@@ -581,7 +708,7 @@ class Linker {
global $wgContLang, $wgUser, $wgThumbLimits, $wgThumbUpright;
if ( $file && !$file->allowInlineDisplay() ) {
wfDebug( __METHOD__.': '.$title->getPrefixedDBkey()." does not allow inline display\n" );
- return $this->makeKnownLinkObj( $title );
+ return $this->link( $title );
}
// Shortcuts
@@ -592,11 +719,12 @@ class Linker {
$page = isset( $hp['page'] ) ? $hp['page'] : false;
if ( !isset( $fp['align'] ) ) $fp['align'] = '';
if ( !isset( $fp['alt'] ) ) $fp['alt'] = '';
+ # Backward compatibility, title used to always be equal to alt text
+ if ( !isset( $fp['title'] ) ) $fp['title'] = $fp['alt'];
$prefix = $postfix = '';
- if ( 'center' == $fp['align'] )
- {
+ if ( 'center' == $fp['align'] ) {
$prefix = '<div class="center">';
$postfix = '</div>';
$fp['align'] = 'none';
@@ -627,7 +755,6 @@ class Linker {
}
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-
# languages ("Western languages"), left-aligned
@@ -660,15 +787,26 @@ class Linker {
if ( !$thumb ) {
$s = $this->makeBrokenImageLinkObj( $title, '', '', '', '', $time==true );
} else {
- $s = $thumb->toHtml( array(
- 'desc-link' => true,
- 'desc-query' => $query,
+ $params = array(
'alt' => $fp['alt'],
+ 'title' => $fp['title'],
'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false ,
- 'img-class' => isset( $fp['border'] ) ? 'thumbborder' : false ) );
+ 'img-class' => isset( $fp['border'] ) ? 'thumbborder' : false );
+ if ( !empty( $fp['link-url'] ) ) {
+ $params['custom-url-link'] = $fp['link-url'];
+ } elseif ( !empty( $fp['link-title'] ) ) {
+ $params['custom-title-link'] = $fp['link-title'];
+ } elseif ( !empty( $fp['no-link'] ) ) {
+ // No link
+ } else {
+ $params['desc-link'] = true;
+ $params['desc-query'] = $query;
+ }
+
+ $s = $thumb->toHtml( $params );
}
if ( '' != $fp['align'] ) {
- $s = "<div class=\"float{$fp['align']}\"><span>{$s}</span></div>";
+ $s = "<div class=\"float{$fp['align']}\">{$s}</div>";
}
return str_replace("\n", ' ',$prefix.$s.$postfix);
}
@@ -700,6 +838,8 @@ class Linker {
$page = isset( $hp['page'] ) ? $hp['page'] : false;
if ( !isset( $fp['align'] ) ) $fp['align'] = 'right';
if ( !isset( $fp['alt'] ) ) $fp['alt'] = '';
+ # Backward compatibility, title used to always be equal to alt text
+ if ( !isset( $fp['title'] ) ) $fp['title'] = $fp['alt'];
if ( !isset( $fp['caption'] ) ) $fp['caption'] = '';
if ( empty( $hp['width'] ) ) {
@@ -713,7 +853,7 @@ class Linker {
} else {
if ( isset( $fp['manualthumb'] ) ) {
# Use manually specified thumbnail
- $manual_title = Title::makeTitleSafe( NS_IMAGE, $fp['manualthumb'] );
+ $manual_title = Title::makeTitleSafe( NS_FILE, $fp['manualthumb'] );
if( $manual_title ) {
$manual_img = wfFindFile( $manual_title );
if ( $manual_img ) {
@@ -759,6 +899,7 @@ class Linker {
} else {
$s .= $thumb->toHtml( array(
'alt' => $fp['alt'],
+ 'title' => $fp['title'],
'img-class' => 'thumbimage',
'desc-link' => true,
'desc-query' => $query ) );
@@ -818,7 +959,7 @@ class Linker {
/** @deprecated use Linker::makeMediaLinkObj() */
function makeMediaLink( $name, $unused = '', $text = '', $time = false ) {
- $nt = Title::makeTitleSafe( NS_IMAGE, $name );
+ $nt = Title::makeTitleSafe( NS_FILE, $name );
return $this->makeMediaLinkObj( $nt, $text, $time );
}
@@ -867,11 +1008,10 @@ class Linker {
}
/** @todo document */
- function makeExternalLink( $url, $text, $escape = true, $linktype = '', $ns = null ) {
- $style = $this->getExternalLinkAttributes( $url, $text, 'external ' . $linktype );
- global $wgNoFollowLinks, $wgNoFollowNsExceptions;
- if( $wgNoFollowLinks && !(isset($ns) && in_array($ns, $wgNoFollowNsExceptions)) ) {
- $style .= ' rel="nofollow"';
+ function makeExternalLink( $url, $text, $escape = true, $linktype = '', $attribs = array() ) {
+ $attribsText = $this->getExternalLinkAttributes( $url, $text, 'external ' . $linktype );
+ if ( $attribs ) {
+ $attribsText .= Xml::expandAttributes( $attribs );
}
$url = htmlspecialchars( $url );
if( $escape ) {
@@ -883,7 +1023,7 @@ class Linker {
wfDebug("Hook LinkerMakeExternalLink changed the output of link with url {$url} and text {$text} to {$link}", true);
return $link;
}
- return '<a href="'.$url.'"'.$style.'>'.$text.'</a>';
+ return '<a href="'.$url.'"'.$attribsText.'>'.$text.'</a>';
}
/**
@@ -894,15 +1034,12 @@ class Linker {
* @private
*/
function userLink( $userId, $userText ) {
- $encName = htmlspecialchars( $userText );
if( $userId == 0 ) {
- $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
- return $this->makeKnownLinkObj( $contribsPage,
- $encName);
+ $page = SpecialPage::getTitleFor( 'Contributions', $userText );
} else {
- $userPage = Title::makeTitle( NS_USER, $userText );
- return $this->makeLinkObj( $userPage, $encName );
+ $page = Title::makeTitle( NS_USER, $userText );
}
+ return $this->link( $page, htmlspecialchars( $userText ), array( 'class' => 'mw-userlink' ) );
}
/**
@@ -926,22 +1063,23 @@ class Linker {
}
if( $userId ) {
// check if the user has an edit
+ $attribs = array();
if( $redContribsWhenNoEdits ) {
$count = !is_null($edits) ? $edits : User::edits( $userId );
- $style = ($count == 0) ? " class='new'" : '';
- } else {
- $style = '';
+ if( $count == 0 ) {
+ $attribs['class'] = 'new';
+ }
}
$contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
- $items[] = $this->makeKnownLinkObj( $contribsPage, wfMsgHtml( 'contribslink' ), '', '', '', '', $style );
+ $items[] = $this->link( $contribsPage, wfMsgHtml( 'contribslink' ), $attribs );
}
if( $blockable && $wgUser->isAllowed( 'block' ) ) {
$items[] = $this->blockLink( $userId, $userText );
}
if( $items ) {
- return ' (' . implode( ' | ', $items ) . ')';
+ return ' <span class="mw-usertoollinks">(' . implode( ' | ', $items ) . ')</span>';
} else {
return '';
}
@@ -966,7 +1104,7 @@ class Linker {
*/
function userTalkLink( $userId, $userText ) {
$userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
- $userTalkLink = $this->makeLinkObj( $userTalkPage, wfMsgHtml( 'talkpagelinktext' ) );
+ $userTalkLink = $this->link( $userTalkPage, wfMsgHtml( 'talkpagelinktext' ) );
return $userTalkLink;
}
@@ -978,8 +1116,7 @@ class Linker {
*/
function blockLink( $userId, $userText ) {
$blockPage = SpecialPage::getTitleFor( 'Blockip', $userText );
- $blockLink = $this->makeKnownLinkObj( $blockPage,
- wfMsgHtml( 'blocklink' ) );
+ $blockLink = $this->link( $blockPage, wfMsgHtml( 'blocklink' ) );
return $blockLink;
}
@@ -993,7 +1130,8 @@ class Linker {
if( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
$link = wfMsgHtml( 'rev-deleted-user' );
} else if( $rev->userCan( Revision::DELETED_USER ) ) {
- $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() );
+ $link = $this->userLink( $rev->getUser( Revision::FOR_THIS_USER ),
+ $rev->getUserText( Revision::FOR_THIS_USER ) );
} else {
$link = wfMsgHtml( 'rev-deleted-user' );
}
@@ -1013,8 +1151,10 @@ class Linker {
if( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
$link = wfMsgHtml( 'rev-deleted-user' );
} else if( $rev->userCan( Revision::DELETED_USER ) ) {
- $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() ) .
- ' ' . $this->userToolLinks( $rev->getRawUser(), $rev->getRawUserText() );
+ $userId = $rev->getUser( Revision::FOR_THIS_USER );
+ $userText = $rev->getUserText( Revision::FOR_THIS_USER );
+ $link = $this->userLink( $userId, $userText ) .
+ ' ' . $this->userToolLinks( $userId, $userText );
} else {
$link = wfMsgHtml( 'rev-deleted-user' );
}
@@ -1045,7 +1185,8 @@ class Linker {
# Sanitize text a bit:
$comment = str_replace( "\n", " ", $comment );
- $comment = htmlspecialchars( $comment );
+ # Allow HTML entities (for bug 13815)
+ $comment = Sanitizer::escapeHtmlAllowEntities( $comment );
# Render autocomments and make links:
$comment = $this->formatAutoComments( $comment, $title, $local );
@@ -1068,45 +1209,63 @@ class Linker {
*
* @todo Document the $local parameter.
*/
- private function formatAutocomments( $comment, $title = NULL, $local = false ) {
- $match = array();
- while (preg_match('!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment,$match)) {
- $pre=$match[1];
- $auto=$match[2];
- $post=$match[3];
- $link='';
- if( $title ) {
- $section = $auto;
-
- # Generate a valid anchor name from the section title.
- # Hackish, but should generally work - we strip wiki
- # syntax, including the magic [[: that is used to
- # "link rather than show" in case of images and
- # interlanguage links.
- $section = str_replace( '[[:', '', $section );
- $section = str_replace( '[[', '', $section );
- $section = str_replace( ']]', '', $section );
- if ( $local ) {
- $sectionTitle = Title::newFromText( '#' . $section);
- } else {
- $sectionTitle = wfClone( $title );
- $sectionTitle->mFragment = $section;
- }
- $link = $this->makeKnownLinkObj( $sectionTitle, wfMsgForContent( 'sectionlink' ) );
- }
- $auto = $link . $auto;
- if( $pre ) {
- # written summary $presep autocomment (summary /* section */)
- $auto = wfMsgExt( 'autocomment-prefix', array( 'escapenoentities', 'content' ) ) . $auto;
+ private function formatAutocomments( $comment, $title = null, $local = false ) {
+ // Bah!
+ $this->autocommentTitle = $title;
+ $this->autocommentLocal = $local;
+ $comment = preg_replace_callback(
+ '!(.*)/\*\s*(.*?)\s*\*/(.*)!',
+ array( $this, 'formatAutocommentsCallback' ),
+ $comment );
+ unset( $this->autocommentTitle );
+ unset( $this->autocommentLocal );
+ return $comment;
+ }
+
+ private function formatAutocommentsCallback( $match ) {
+ $title = $this->autocommentTitle;
+ $local = $this->autocommentLocal;
+
+ $pre=$match[1];
+ $auto=$match[2];
+ $post=$match[3];
+ $link='';
+ if( $title ) {
+ $section = $auto;
+
+ # Generate a valid anchor name from the section title.
+ # Hackish, but should generally work - we strip wiki
+ # syntax, including the magic [[: that is used to
+ # "link rather than show" in case of images and
+ # interlanguage links.
+ $section = str_replace( '[[:', '', $section );
+ $section = str_replace( '[[', '', $section );
+ $section = str_replace( ']]', '', $section );
+ if ( $local ) {
+ $sectionTitle = Title::newFromText( '#' . $section );
+ } else {
+ $sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
+ $title->getDBkey(), $section );
}
- if( $post ) {
- # autocomment $postsep written summary (/* section */ summary)
- $auto .= wfMsgExt( 'colon-separator', array( 'escapenoentities', 'content' ) );
+ if ( $sectionTitle ) {
+ $link = $this->link( $sectionTitle,
+ wfMsgForContent( 'sectionlink' ), array(), array(),
+ 'noclasses' );
+ } else {
+ $link = '';
}
- $auto = '<span class="autocomment">' . $auto . '</span>';
- $comment = $pre . $auto . $post;
}
-
+ $auto = "$link$auto";
+ if( $pre ) {
+ # written summary $presep autocomment (summary /* section */)
+ $auto = wfMsgExt( 'autocomment-prefix', array( 'escapenoentities', 'content' ) ) . $auto;
+ }
+ if( $post ) {
+ # autocomment $postsep written summary (/* section */ summary)
+ $auto .= wfMsgExt( 'colon-separator', array( 'escapenoentities', 'content' ) );
+ }
+ $auto = '<span class="autocomment">' . $auto . '</span>';
+ $comment = $pre . $auto . $post;
return $comment;
}
@@ -1201,7 +1360,8 @@ class Linker {
if( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) {
$block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
} else if( $rev->userCan( Revision::DELETED_COMMENT ) ) {
- $block = $this->commentBlock( $rev->getRawComment(), $rev->getTitle(), $local );
+ $block = $this->commentBlock( $rev->getComment( Revision::FOR_THIS_USER ),
+ $rev->getTitle(), $local );
} else {
$block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
}
@@ -1261,8 +1421,8 @@ class Linker {
. "</ul>\n</td></tr></table>"
. '<script type="' . $wgJsMimeType . '">'
. ' if (window.showTocToggle) {'
- . ' var tocShowText = "' . wfEscapeJsString( wfMsg('showtoc') ) . '";'
- . ' var tocHideText = "' . wfEscapeJsString( wfMsg('hidetoc') ) . '";'
+ . ' var tocShowText = "' . Xml::escapeJsString( wfMsg('showtoc') ) . '";'
+ . ' var tocHideText = "' . Xml::escapeJsString( wfMsg('hidetoc') ) . '";'
. ' showTocToggle();'
. ' } '
. "</script>\n";
@@ -1276,8 +1436,9 @@ class Linker {
* @param $section Integer: section number.
*/
public function editSectionLinkForOther( $title, $section ) {
+ wfDeprecated( __METHOD__ );
$title = Title::newFromText( $title );
- return $this->doEditSectionLink( $title, $section, '', 'EditSectionLinkForOther' );
+ return $this->doEditSectionLink( $title, $section );
}
/**
@@ -1285,49 +1446,64 @@ class Linker {
* @param $section Integer: section number.
* @param $hint Link String: title, or default if omitted or empty
*/
- public function editSectionLink( Title $nt, $section, $hint='' ) {
- if( $hint != '' ) {
- $hint = wfMsgHtml( 'editsectionhint', htmlspecialchars( $hint ) );
- $hint = " title=\"$hint\"";
- }
- return $this->doEditSectionLink( $nt, $section, $hint, 'EditSectionLink' );
+ public function editSectionLink( Title $nt, $section, $hint = '' ) {
+ wfDeprecated( __METHOD__ );
+ if( $hint === '' ) {
+ # No way to pass an actual empty $hint here! The new interface al-
+ # lows this, so we have to do this for compatibility.
+ $hint = null;
+ }
+ return $this->doEditSectionLink( $nt, $section, $hint );
}
/**
- * Implement editSectionLink and editSectionLinkForOther.
+ * Create a section edit link. This supersedes 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
+ * @param $nt Title The title being linked to (may not be the same as
+ * $wgTitle, if the section is included from a template)
+ * @param $section string The designation of the section being pointed to,
+ * to be included in the link, like "&section=$section"
+ * @param $tooltip string The tooltip to use for the link: will be escaped
+ * and wrapped in the 'editsectionhint' message
+ * @return string HTML to use for edit link
*/
- protected function doEditSectionLink( Title $nt, $section, $hint, $hook ) {
- global $wgContLang;
- $editurl = '&section='.$section;
- $url = $this->makeKnownLinkObj(
- $nt,
- htmlspecialchars(wfMsg('editsection')),
- 'action=edit'.$editurl,
- '', '', '', $hint
+ public function doEditSectionLink( Title $nt, $section, $tooltip = null ) {
+ $attribs = array();
+ if( !is_null( $tooltip ) ) {
+ $attribs['title'] = wfMsg( 'editsectionhint', $tooltip );
+ }
+ $link = $this->link( $nt, wfMsg('editsection'),
+ $attribs,
+ array( 'action' => 'edit', 'section' => $section ),
+ array( 'noclasses', 'known' )
);
- $result = null;
- // The two hooks have slightly different interfaces . . .
- if( $hook == 'EditSectionLink' ) {
- wfRunHooks( 'EditSectionLink', array( &$this, $nt, $section, $hint, $url, &$result ) );
- } elseif( $hook == 'EditSectionLinkForOther' ) {
- wfRunHooks( 'EditSectionLinkForOther', array( &$this, $nt, $section, $url, &$result ) );
+ # Run the old hook. This takes up half of the function . . . hopefully
+ # we can rid of it someday.
+ $attribs = '';
+ if( $tooltip ) {
+ $attribs = wfMsgHtml( 'editsectionhint', htmlspecialchars( $tooltip ) );
+ $attribs = " title=\"$attribs\"";
}
-
- // For reverse compatibility, add the brackets *after* the hook is run,
- // and even add them to hook-provided text.
- if( is_null( $result ) ) {
- $result = wfMsgHtml( 'editsection-brackets', $url );
- } else {
+ $result = null;
+ wfRunHooks( 'EditSectionLink', array( &$this, $nt, $section, $attribs, $link, &$result ) );
+ if( !is_null( $result ) ) {
+ # For reverse compatibility, add the brackets *after* the hook is
+ # run, and even add them to hook-provided text. (This is the main
+ # reason that the EditSectionLink hook is deprecated in favor of
+ # DoEditSectionLink: it can't change the brackets or the span.)
$result = wfMsgHtml( 'editsection-brackets', $result );
+ return "<span class=\"editsection\">$result</span>";
}
- return "<span class=\"editsection\">$result</span>";
+
+ # Add the brackets and the span, and *then* run the nice new hook, with
+ # clean and non-redundant arguments.
+ $result = wfMsgHtml( 'editsection-brackets', $link );
+ $result = "<span class=\"editsection\">$result</span>";
+
+ wfRunHooks( 'DoEditSectionLink', array( $this, $nt, $section, $tooltip, &$result ) );
+ return $result;
}
/**
@@ -1339,11 +1515,21 @@ class Linker {
* @param string $anchor The anchor to give the headline (the bit after the #)
* @param string $text The text of the header
* @param string $link HTML to add for the section edit link
+ * @param mixed $legacyAnchor A second, optional anchor to give for
+ * backward compatibility (false to omit)
*
* @return string HTML headline
*/
- public function makeHeadline( $level, $attribs, $anchor, $text, $link ) {
- return "<a name=\"$anchor\"></a><h$level$attribs$link <span class=\"mw-headline\">$text</span></h$level>";
+ public function makeHeadline( $level, $attribs, $anchor, $text, $link, $legacyAnchor = false ) {
+ $ret = "<a name=\"$anchor\" id=\"$anchor\"></a>"
+ . "<h$level$attribs"
+ . $link
+ . " <span class=\"mw-headline\">$text</span>"
+ . "</h$level>";
+ if ( $legacyAnchor !== false ) {
+ $ret = "<a name=\"$legacyAnchor\" id=\"$legacyAnchor\"></a>$ret";
+ }
+ return $ret;
}
/**
@@ -1397,14 +1583,19 @@ class Linker {
public function buildRollbackLink( $rev ) {
global $wgRequest, $wgUser;
$title = $rev->getTitle();
- $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
+ $query = array(
+ 'action' => 'rollback',
+ 'from' => $rev->getUserText()
);
+ if( $wgRequest->getBool( 'bot' ) ) {
+ $query['bot'] = '1';
+ $query['hidediff'] = '1'; // bug 15999
+ }
+ $query['token'] = $wgUser->editToken( array( $title->getPrefixedText(),
+ $rev->getUserText() ) );
+ return $this->link( $title, wfMsgHtml( 'rollbacklink' ),
+ array( 'title' => wfMsg( 'tooltip-rollback' ) ),
+ $query, array( 'known', 'noclasses' ) );
}
/**
@@ -1416,12 +1607,9 @@ class Linker {
* @param bool $section Whether this is for a section edit
* @return string HTML output
*/
- public function formatTemplates( $templates, $preview = false, $section = false) {
- global $wgUser;
+ public function formatTemplates( $templates, $preview = false, $section = false ) {
wfProfileIn( __METHOD__ );
- $sk = $wgUser->getSkin();
-
$outText = '';
if ( count( $templates ) > 0 ) {
# Do a batch existence check
@@ -1440,7 +1628,7 @@ class Linker {
} else {
$outText .= wfMsgExt( 'templatesused', array( 'parse' ) );
}
- $outText .= '</div><ul>';
+ $outText .= "</div><ul>\n";
usort( $templates, array( 'Title', 'compare' ) );
foreach ( $templates as $titleObj ) {
@@ -1452,7 +1640,12 @@ class Linker {
} else {
$protected = '';
}
- $outText .= '<li>' . $sk->makeLinkObj( $titleObj ) . ' ' . $protected . '</li>';
+ if( $titleObj->quickUserCan( 'edit' ) ) {
+ $editLink = $this->makeLinkObj( $titleObj, wfMsg('editlink'), 'action=edit' );
+ } else {
+ $editLink = $this->makeLinkObj( $titleObj, wfMsg('viewsourcelink'), 'action=edit' );
+ }
+ $outText .= '<li>' . $this->link( $titleObj ) . ' (' . $editLink . ') ' . $protected . '</li>';
}
$outText .= '</ul>';
}
@@ -1467,21 +1660,19 @@ class Linker {
* or similar
* @return string HTML output
*/
- public function formatHiddenCategories( $hiddencats) {
- global $wgUser, $wgLang;
+ public function formatHiddenCategories( $hiddencats ) {
+ global $wgLang;
wfProfileIn( __METHOD__ );
- $sk = $wgUser->getSkin();
-
$outText = '';
if ( count( $hiddencats ) > 0 ) {
# Construct the HTML
$outText = '<div class="mw-hiddenCategoriesExplanation">';
$outText .= wfMsgExt( 'hiddencategories', array( 'parse' ), $wgLang->formatnum( count( $hiddencats ) ) );
- $outText .= '</div><ul>';
+ $outText .= "</div><ul>\n";
foreach ( $hiddencats as $titleObj ) {
- $outText .= '<li>' . $sk->makeKnownLinkObj( $titleObj ) . '</li>'; # If it's hidden, it must exist - no need to check with a LinkBatch
+ $outText .= '<li>' . $this->link( $titleObj, null, array(), array(), 'known' ) . "</li>\n"; # If it's hidden, it must exist - no need to check with a LinkBatch
}
$outText .= '</ul>';
}
@@ -1502,38 +1693,37 @@ class Linker {
}
/**
- * Given the id of an interface element, constructs the appropriate title
- * and accesskey attributes from the system messages. (Note, this is usu-
- * ally the id but isn't always, because sometimes the accesskey needs to
- * go on a different element than the id, for reverse-compatibility, etc.)
- *
- * @param string $name Id of the element, minus prefixes.
- * @return string title and accesskey attributes, ready to drop in an
- * element (e.g., ' title="This does something [x]" accesskey="x"').
+ * @deprecated Returns raw bits of HTML, use titleAttrib() and accesskey()
*/
public function tooltipAndAccesskey( $name ) {
- wfProfileIn( __METHOD__ );
- $attribs = array();
-
- $tooltip = wfMsg( "tooltip-$name" );
- if( !wfEmptyMsg( "tooltip-$name", $tooltip ) && $tooltip != '-' ) {
- // Compatibility: formerly some tooltips had [alt-.] hardcoded
- $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
- $attribs['title'] = $tooltip;
+ # FIXME: If Sanitizer::expandAttributes() treated "false" as "output
+ # no attribute" instead of "output '' as value for attribute", this
+ # would be three lines.
+ $attribs = array(
+ 'title' => $this->titleAttrib( $name, 'withaccess' ),
+ 'accesskey' => $this->accesskey( $name )
+ );
+ if ( $attribs['title'] === false ) {
+ unset( $attribs['title'] );
}
-
- $accesskey = wfMsg( "accesskey-$name" );
- if( $accesskey && $accesskey != '-' &&
- !wfEmptyMsg( "accesskey-$name", $accesskey ) ) {
- if( isset( $attribs['title'] ) ) {
- $attribs['title'] .= " [$accesskey]";
- }
- $attribs['accesskey'] = $accesskey;
+ if ( $attribs['accesskey'] === false ) {
+ unset( $attribs['accesskey'] );
}
+ return Xml::expandAttributes( $attribs );
+ }
- $ret = Xml::expandAttributes( $attribs );
- wfProfileOut( __METHOD__ );
- return $ret;
+ /** @deprecated Returns raw bits of HTML, use titleAttrib() */
+ public function tooltip( $name, $options = null ) {
+ # FIXME: If Sanitizer::expandAttributes() treated "false" as "output
+ # no attribute" instead of "output '' as value for attribute", this
+ # would be two lines.
+ $tooltip = $this->titleAttrib( $name, $options );
+ if ( $tooltip === false ) {
+ return '';
+ }
+ return Xml::expandAttributes( array(
+ 'title' => $this->titleAttrib( $name, $options )
+ ) );
}
/**
@@ -1545,29 +1735,62 @@ class Linker {
* @param string $name Id of the element, minus prefixes.
* @param mixed $options null or the string 'withaccess' to add an access-
* key hint
- * @return string title attribute, ready to drop in an element
- * (e.g., ' title="This does something"').
+ * @return string Contents of the title attribute (which you must HTML-
+ * escape), or false for no title attribute
*/
- public function tooltip( $name, $options = null ) {
+ public function titleAttrib( $name, $options = null ) {
wfProfileIn( __METHOD__ );
- $attribs = array();
-
$tooltip = wfMsg( "tooltip-$name" );
- if( !wfEmptyMsg( "tooltip-$name", $tooltip ) && $tooltip != '-' ) {
- $attribs['title'] = $tooltip;
+ # Compatibility: formerly some tooltips had [alt-.] hardcoded
+ $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
+
+ # Message equal to '-' means suppress it.
+ if ( wfEmptyMsg( "tooltip-$name", $tooltip ) || $tooltip == '-' ) {
+ $tooltip = false;
}
- if( isset( $attribs['title'] ) && $options == 'withaccess' ) {
- $accesskey = wfMsg( "accesskey-$name" );
- if( $accesskey && $accesskey != '-' &&
- !wfEmptyMsg( "accesskey-$name", $accesskey ) ) {
- $attribs['title'] .= " [$accesskey]";
+ if ( $options == 'withaccess' ) {
+ $accesskey = $this->accesskey( $name );
+ if( $accesskey !== false ) {
+ if ( $tooltip === false || $tooltip === '' ) {
+ $tooltip = "[$accesskey]";
+ } else {
+ $tooltip .= " [$accesskey]";
+ }
}
}
- $ret = Xml::expandAttributes( $attribs );
wfProfileOut( __METHOD__ );
- return $ret;
+ return $tooltip;
+ }
+
+ /**
+ * Given the id of an interface element, constructs the appropriate
+ * accesskey attribute from the system messages. (Note, this is usually
+ * the id but isn't always, because sometimes the accesskey needs to go on
+ * a different element than the id, for reverse-compatibility, etc.)
+ *
+ * @param string $name Id of the element, minus prefixes.
+ * @return string Contents of the accesskey attribute (which you must HTML-
+ * escape), or false for no accesskey attribute
+ */
+ public function accesskey( $name ) {
+ wfProfileIn( __METHOD__ );
+
+ $accesskey = wfMsg( "accesskey-$name" );
+
+ # FIXME: Per standard MW behavior, a value of '-' means to suppress the
+ # attribute, but this is broken for accesskey: that might be a useful
+ # value.
+ if( $accesskey != ''
+ && $accesskey != '-'
+ && !wfEmptyMsg( "accesskey-$name", $accesskey ) ) {
+ wfProfileOut( __METHOD__ );
+ return $accesskey;
+ }
+
+ wfProfileOut( __METHOD__ );
+ return false;
}
}
diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php
index bb192fb9..13f35b5a 100644
--- a/includes/LinksUpdate.php
+++ b/includes/LinksUpdate.php
@@ -20,7 +20,8 @@ class LinksUpdate {
$mProperties, //!< Map of arbitrary name to value
$mDb, //!< Database connection reference
$mOptions, //!< SELECT options to be used (array)
- $mRecursive; //!< Whether to queue jobs for recursive updates
+ $mRecursive, //!< Whether to queue jobs for recursive updates
+ $mTouchTmplLinks; //!< Whether to queue HTMLCacheUpdate jobs IF recursive
/**@}}*/
/**
@@ -67,14 +68,24 @@ class LinksUpdate {
}
$this->mRecursive = $recursive;
+ $this->mTouchTmplLinks = false;
wfRunHooks( 'LinksUpdateConstructed', array( &$this ) );
}
+
+ /**
+ * Invalidate HTML cache of pages that include this page?
+ */
+ public function setRecursiveTouch( $val ) {
+ $this->mTouchTmplLinks = (bool)$val;
+ if( $val ) // Cannot invalidate without queueRecursiveJobs()
+ $this->mRecursive = true;
+ }
/**
* Update link tables with outgoing links from an updated article
*/
- function doUpdate() {
+ public function doUpdate() {
global $wgUseDumbLinkUpdate;
wfRunHooks( 'LinksUpdate', array( &$this ) );
@@ -87,7 +98,7 @@ class LinksUpdate {
}
- function doIncrementalUpdate() {
+ protected function doIncrementalUpdate() {
wfProfileIn( __METHOD__ );
# Page links
@@ -158,7 +169,7 @@ class LinksUpdate {
* May be slower or faster depending on level of lock contention and write speed of DB
* Also useful where link table corruption needs to be repaired, e.g. in refreshLinks.php
*/
- function doDumbUpdate() {
+ protected function doDumbUpdate() {
wfProfileIn( __METHOD__ );
# Refresh category pages and image description pages
@@ -193,34 +204,54 @@ class LinksUpdate {
}
function queueRecursiveJobs() {
+ global $wgUpdateRowsPerJob;
wfProfileIn( __METHOD__ );
- $batchSize = 100;
$dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( array( 'templatelinks', 'page' ),
- array( 'page_namespace', 'page_title' ),
- array(
- 'page_id=tl_from',
+ $res = $dbr->select( 'templatelinks',
+ array( 'tl_from' ),
+ array(
'tl_namespace' => $this->mTitle->getNamespace(),
'tl_title' => $this->mTitle->getDBkey()
), __METHOD__
);
- $done = false;
- while ( !$done ) {
- $jobs = array();
- for ( $i = 0; $i < $batchSize; $i++ ) {
- $row = $dbr->fetchObject( $res );
- if ( !$row ) {
- $done = true;
+ $numRows = $res->numRows();
+ if( !$numRows ) {
+ wfProfileOut( __METHOD__ );
+ return; // nothing to do
+ }
+ $numBatches = ceil( $numRows / $wgUpdateRowsPerJob );
+ $realBatchSize = $numRows / $numBatches;
+ $start = false;
+ $jobs = array();
+ do {
+ for( $i = 0; $i <= $realBatchSize - 1; $i++ ) {
+ $row = $res->fetchRow();
+ if( $row ) {
+ $id = $row[0];
+ } else {
+ $id = false;
break;
}
- $title = Title::makeTitle( $row->page_namespace, $row->page_title );
- $jobs[] = new RefreshLinksJob( $title, '' );
}
- Job::batchInsert( $jobs );
- }
+ $params = array(
+ 'start' => $start,
+ 'end' => ( $id !== false ? $id - 1 : false ),
+ );
+ $jobs[] = new RefreshLinksJob2( $this->mTitle, $params );
+ # Hit page caches while we're at it if set to do so...
+ if( $this->mTouchTmplLinks ) {
+ $params['table'] = 'templatelinks';
+ $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params );
+ }
+ $start = $id;
+ } while ( $start );
+
$dbr->freeResult( $res );
+
+ Job::batchInsert( $jobs );
+
wfProfileOut( __METHOD__ );
}
@@ -286,7 +317,7 @@ class LinksUpdate {
}
function invalidateImageDescriptions( $images ) {
- $this->invalidatePages( NS_IMAGE, array_keys( $images ) );
+ $this->invalidatePages( NS_FILE, array_keys( $images ) );
}
function dumbTableUpdate( $table, $insertions, $fromField ) {
diff --git a/includes/LogEventsList.php b/includes/LogEventsList.php
index d49f636b..528bd3aa 100644
--- a/includes/LogEventsList.php
+++ b/includes/LogEventsList.php
@@ -24,7 +24,7 @@ class LogEventsList {
private $out;
public $flags;
- function __construct( $skin, $out, $flags = 0 ) {
+ public function __construct( $skin, $out, $flags = 0 ) {
$this->skin = $skin;
$this->out = $out;
$this->flags = $flags;
@@ -38,34 +38,38 @@ class LogEventsList {
private function preCacheMessages() {
// Precache various messages
if( !isset( $this->message ) ) {
- $messages = 'revertmerge protect_change unblocklink revertmove undeletelink revdel-restore rev-delundel';
- foreach( explode(' ', $messages ) as $msg ) {
- $this->message[$msg] = wfMsgExt( $msg, array( 'escape') );
+ $messages = array( 'revertmerge', 'protect_change', 'unblocklink', 'change-blocklink',
+ 'revertmove', 'undeletelink', 'revdel-restore', 'rev-delundel', 'hist', 'pipe-separator' );
+ foreach( $messages as $msg ) {
+ $this->message[$msg] = wfMsgExt( $msg, array( 'escape' ) );
}
}
}
/**
* Set page title and show header for this log type
- * @param string $type
+ * @param $type String
*/
public function showHeader( $type ) {
if( LogPage::isLogType( $type ) ) {
$this->out->setPageTitle( LogPage::logName( $type ) );
- $this->out->addHtml( LogPage::logHeader( $type ) );
+ $this->out->addHTML( LogPage::logHeader( $type ) );
}
}
/**
* Show options for the log list
- * @param string $type,
- * @param string $user,
- * @param string $page,
- * @param string $pattern
- * @param int $year
- * @parm int $month
+ * @param $type String
+ * @param $user String
+ * @param $page String
+ * @param $pattern String
+ * @param $year Integer: year
+ * @param $month Integer: month
+ * @param $filter Boolean
*/
- public function showOptions( $type='', $user='', $page='', $pattern='', $year='', $month='' ) {
+ public function showOptions( $type = '', $user = '', $page = '', $pattern = '', $year = '',
+ $month = '', $filter = null )
+ {
global $wgScript, $wgMiserMode;
$action = htmlspecialchars( $wgScript );
$title = SpecialPage::getTitleFor( 'Log' );
@@ -79,13 +83,46 @@ class LogEventsList {
$this->getTitleInput( $page ) . "\n" .
( !$wgMiserMode ? ($this->getTitlePattern( $pattern )."\n") : "" ) .
"<p>" . $this->getDateMenu( $year, $month ) . "\n" .
+ ( $filter ? "</p><p>".$this->getFilterLinks( $type, $filter )."\n" : "" ) .
Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "</p>\n" .
- "</fieldset></form>" );
+ "</fieldset></form>"
+ );
+ }
+
+ private function getFilterLinks( $logType, $filter ) {
+ global $wgTitle;
+ // show/hide links
+ $messages = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ) );
+ // Option value -> message mapping
+ $links = array();
+ foreach( $filter as $type => $val ) {
+ $hideVal = 1 - intval($val);
+ $link = $this->skin->makeKnownLinkObj( $wgTitle, $messages[$hideVal],
+ wfArrayToCGI( array( "hide_{$type}_log" => $hideVal ), $this->getDefaultQuery() )
+ );
+ $links[$type] = wfMsgHtml( "log-show-hide-{$type}", $link );
+ }
+ // Build links
+ return implode( ' | ', $links );
+ }
+
+ private function getDefaultQuery() {
+ if ( !isset( $this->mDefaultQuery ) ) {
+ $this->mDefaultQuery = $_GET;
+ unset( $this->mDefaultQuery['title'] );
+ unset( $this->mDefaultQuery['dir'] );
+ unset( $this->mDefaultQuery['offset'] );
+ unset( $this->mDefaultQuery['limit'] );
+ unset( $this->mDefaultQuery['order'] );
+ unset( $this->mDefaultQuery['month'] );
+ unset( $this->mDefaultQuery['year'] );
+ }
+ return $this->mDefaultQuery;
}
/**
- * @return string Formatted HTML
- * @param string $queryType
+ * @param $queryType String
+ * @return String: Formatted HTML
*/
private function getTypeMenu( $queryType ) {
global $wgLogRestrictions, $wgUser;
@@ -93,19 +130,19 @@ class LogEventsList {
$html = "<select name='type'>\n";
$validTypes = LogPage::validTypes();
- $m = array(); // Temporary array
+ $typesByName = array(); // Temporary array
// First pass to load the log names
foreach( $validTypes as $type ) {
$text = LogPage::logName( $type );
- $m[$text] = $type;
+ $typesByName[$text] = $type;
}
// Second pass to sort by name
- ksort($m);
+ ksort($typesByName);
// Third pass generates sorted XHTML content
- foreach( $m as $text => $type ) {
+ foreach( $typesByName as $text => $type ) {
$selected = ($type == $queryType);
// Restricted types
if ( isset($wgLogRestrictions[$type]) ) {
@@ -122,25 +159,25 @@ class LogEventsList {
}
/**
- * @return string Formatted HTML
- * @param string $user
+ * @param $user String
+ * @return String: Formatted HTML
*/
private function getUserInput( $user ) {
return Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'user', 15, $user );
}
/**
- * @return string Formatted HTML
- * @param string $title
+ * @param $title String
+ * @return String: Formatted HTML
*/
private function getTitleInput( $title ) {
return Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'page', 20, $title );
}
/**
+ * @param $year Integer
+ * @param $month Integer
* @return string Formatted HTML
- * @param int $year
- * @param int $month
*/
private function getDateMenu( $year, $month ) {
# Offset overrides year/month selection
@@ -185,10 +222,9 @@ class LogEventsList {
return "</ul>\n";
}
- /**
- * @param Row $row a single row from the result set
- * @return string Formatted HTML list item
- * @private
+ /**
+ * @param $row Row: a single row from the result set
+ * @return String: Formatted HTML list item
*/
public function logLine( $row ) {
global $wgLang, $wgUser, $wgContLang;
@@ -213,88 +249,127 @@ class LogEventsList {
$revert = $del = '';
// Some user can hide log items and have review links
if( $wgUser->isAllowed( 'deleterevision' ) ) {
- $del = $this->showhideLinks( $row ) . ' ';
+ $del = $this->getShowHideLinks( $row ) . ' ';
}
// Add review links and such...
- if( !($this->flags & self::NO_ACTION_LINK) && !($row->log_deleted & LogPage::DELETED_ACTION) ) {
- if( self::typeAction($row,'move','move') && isset( $paramArray[0] ) && $wgUser->isAllowed( 'move' ) ) {
- $destTitle = Title::newFromText( $paramArray[0] );
- if( $destTitle ) {
- $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
- $this->message['revertmove'],
- 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
- '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
- '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
- '&wpMovetalk=0' ) . ')';
- }
- // Show undelete link
- } else if( self::typeAction($row,array('delete','suppress'),'delete') && $wgUser->isAllowed( 'delete' ) ) {
- $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
- $this->message['undeletelink'], 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')';
- // Show unblock link
- } else if( self::typeAction($row,array('block','suppress'),'block') && $wgUser->isAllowed( 'block' ) ) {
- $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ),
+ if( ($this->flags & self::NO_ACTION_LINK) || ($row->log_deleted & LogPage::DELETED_ACTION) ) {
+ // Action text is suppressed...
+ } else if( self::typeAction($row,'move','move','move') && !empty($paramArray[0]) ) {
+ $destTitle = Title::newFromText( $paramArray[0] );
+ if( $destTitle ) {
+ $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
+ $this->message['revertmove'],
+ 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
+ '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
+ '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
+ '&wpMovetalk=0' ) . ')';
+ }
+ // Show undelete link
+ } else if( self::typeAction($row,array('delete','suppress'),'delete','delete') ) {
+ $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
+ $this->message['undeletelink'], 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')';
+ // Show unblock/change block link
+ } else if( self::typeAction($row,array('block','suppress'),array('block','reblock'),'block') ) {
+ $revert = '(' .
+ $this->skin->link( SpecialPage::getTitleFor( 'Ipblocklist' ),
$this->message['unblocklink'],
- 'action=unblock&ip=' . urlencode( $row->log_title ) ) . ')';
- // Show change protection link
- } else if( self::typeAction($row,'protect','modify') && $wgUser->isAllowed( 'protect' ) ) {
- $revert = '(' . $this->skin->makeKnownLinkObj( $title, $this->message['protect_change'], 'action=unprotect' ) . ')';
- // Show unmerge link
- } else if ( self::typeAction($row,'merge','merge') ) {
- $merge = SpecialPage::getTitleFor( 'Mergehistory' );
- $revert = '(' . $this->skin->makeKnownLinkObj( $merge, $this->message['revertmerge'],
- wfArrayToCGI( array('target' => $paramArray[0], 'dest' => $title->getPrefixedDBkey(),
- 'mergepoint' => $paramArray[1] ) ) ) . ')';
- // If an edit was hidden from a page give a review link to the history
- } else if( self::typeAction($row,array('delete','suppress'),'revision') && $wgUser->isAllowed( 'deleterevision' ) ) {
- if( count($paramArray) == 2 ) {
- $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
- // Different revision types use different URL params...
- $key = $paramArray[0];
- // Link to each hidden object ID, $paramArray[1] is the url param
- $Ids = explode( ',', $paramArray[1] );
- $revParams = '';
- foreach( $Ids as $n => $id ) {
- $revParams .= '&' . urlencode($key) . '[]=' . urlencode($id);
- }
- $revert = '(' . $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'],
- 'target=' . $title->getPrefixedUrl() . $revParams ) . ')';
+ array(),
+ array( 'action' => 'unblock', 'ip' => $row->log_title ),
+ 'known' )
+ . ' ' . $this->message['pipe-separator'] . ' ' .
+ $this->skin->link( SpecialPage::getTitleFor( 'Blockip', $row->log_title ),
+ $this->message['change-blocklink'],
+ array(), array(), 'known' ) .
+ ')';
+ // Show change protection link
+ } else if( self::typeAction( $row, 'protect', array( 'modify', 'protect', 'unprotect' ) ) ) {
+ $revert .= ' (' .
+ $this->skin->link( $title,
+ $this->message['hist'],
+ array(),
+ array( 'action' => 'history', 'offset' => $row->log_timestamp ) );
+ if( $wgUser->isAllowed( 'protect' ) ) {
+ $revert .= ' ' . $this->message['pipe-separator'] . ' ' .
+ $this->skin->link( $title,
+ $this->message['protect_change'],
+ array(),
+ array( 'action' => 'protect' ),
+ 'known' );
+ }
+ $revert .= ')';
+ // Show unmerge link
+ } else if( self::typeAction($row,'merge','merge','mergehistory') ) {
+ $merge = SpecialPage::getTitleFor( 'Mergehistory' );
+ $revert = '(' . $this->skin->makeKnownLinkObj( $merge, $this->message['revertmerge'],
+ wfArrayToCGI( array('target' => $paramArray[0], 'dest' => $title->getPrefixedDBkey(),
+ 'mergepoint' => $paramArray[1] ) ) ) . ')';
+ // If an edit was hidden from a page give a review link to the history
+ } else if( self::typeAction($row,array('delete','suppress'),'revision','deleterevision') ) {
+ if( count($paramArray) == 2 ) {
+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+ // Different revision types use different URL params...
+ $key = $paramArray[0];
+ // Link to each hidden object ID, $paramArray[1] is the url param
+ $Ids = explode( ',', $paramArray[1] );
+ $revParams = '';
+ foreach( $Ids as $n => $id ) {
+ $revParams .= '&' . urlencode($key) . '[]=' . urlencode($id);
}
- // Hidden log items, give review link
- } else if( self::typeAction($row,array('delete','suppress'),'event') && $wgUser->isAllowed( 'deleterevision' ) ) {
- if( count($paramArray) == 1 ) {
- $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
- $Ids = explode( ',', $paramArray[0] );
- // Link to each hidden object ID, $paramArray[1] is the url param
- $logParams = '';
- foreach( $Ids as $n => $id ) {
- $logParams .= '&logid[]=' . intval($id);
- }
- $revert = '(' . $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'],
- 'target=' . $title->getPrefixedUrl() . $logParams ) . ')';
+ $revert = '(' . $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'],
+ 'target=' . $title->getPrefixedUrl() . $revParams ) . ')';
+ }
+ // Hidden log items, give review link
+ } else if( self::typeAction($row,array('delete','suppress'),'event','deleterevision') ) {
+ if( count($paramArray) == 1 ) {
+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+ $Ids = explode( ',', $paramArray[0] );
+ // Link to each hidden object ID, $paramArray[1] is the url param
+ $logParams = '';
+ foreach( $Ids as $n => $id ) {
+ $logParams .= '&logid[]=' . intval($id);
}
+ $revert = '(' . $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'],
+ 'target=' . $title->getPrefixedUrl() . $logParams ) . ')';
+ }
+ // Self-created users
+ } else if( self::typeAction($row,'newusers','create2') ) {
+ if( isset( $paramArray[0] ) ) {
+ $revert = $this->skin->userToolLinks( $paramArray[0], $title->getDBkey(), true );
} else {
- wfRunHooks( 'LogLine', array( $row->log_type, $row->log_action, $title, $paramArray,
- &$comment, &$revert, $row->log_timestamp ) );
- // wfDebug( "Invoked LogLine hook for " $row->log_type . ", " . $row->log_action . "\n" );
- // Do nothing. The implementation is handled by the hook modifiying the passed-by-ref parameters.
+ # Fall back to a blue contributions link
+ $revert = $this->skin->userToolLinks( 1, $title->getDBkey() );
+ }
+ if( $time < '20080129000000' ) {
+ # Suppress $comment from old entries (before 2008-01-29),
+ # not needed and can contain incorrect links
+ $comment = '';
}
+ // Do nothing. The implementation is handled by the hook modifiying the passed-by-ref parameters.
+ } else {
+ wfRunHooks( 'LogLine', array( $row->log_type, $row->log_action, $title, $paramArray,
+ &$comment, &$revert, $row->log_timestamp ) );
}
// Event description
if( self::isDeleted($row,LogPage::DELETED_ACTION) ) {
$action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
} else {
- $action = LogPage::actionText( $row->log_type, $row->log_action, $title, $this->skin, $paramArray, true );
+ $action = LogPage::actionText( $row->log_type, $row->log_action, $title,
+ $this->skin, $paramArray, true );
+ }
+
+ if( $revert != '' ) {
+ $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>';
}
- return "<li>$del$time $userLink $action $comment $revert</li>\n";
+ return Xml::tags( 'li', array( "class" => "mw-logline-$row->log_type" ),
+ $del . $time . ' ' . $userLink . ' ' . $action . ' ' . $comment . ' ' . $revert );
}
/**
- * @param Row $row
+ * @param $row Row
* @return string
*/
- private function showhideLinks( $row ) {
+ private function getShowHideLinks( $row ) {
$revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
// If event was hidden from sysops
if( !self::userCan( $row, LogPage::DELETED_RESTRICTED ) ) {
@@ -314,25 +389,31 @@ class LogEventsList {
}
/**
- * @param Row $row
- * @param mixed $type (string/array)
- * @param string $action
+ * @param $row Row
+ * @param $type Mixed: string/array
+ * @param $action Mixed: string/array
+ * @param $right string
* @return bool
*/
- public static function typeAction( $row, $type, $action ) {
- if( is_array($type) ) {
- return ( in_array($row->log_type,$type) && $row->log_action == $action );
- } else {
- return ( $row->log_type == $type && $row->log_action == $action );
+ public static function typeAction( $row, $type, $action, $right='' ) {
+ $match = is_array($type) ? in_array($row->log_type,$type) : $row->log_type == $type;
+ if( $match ) {
+ $match = is_array($action) ?
+ in_array($row->log_action,$action) : $row->log_action == $action;
+ if( $match && $right ) {
+ global $wgUser;
+ $match = $wgUser->isAllowed( $right );
+ }
}
+ return $match;
}
/**
* Determine if the current user is allowed to view a particular
* field of this log row, if it's marked as deleted.
- * @param Row $row
- * @param int $field
- * @return bool
+ * @param $row Row
+ * @param $field Integer
+ * @return Boolean
*/
public static function userCan( $row, $field ) {
if( ( $row->log_deleted & $field ) == $field ) {
@@ -348,9 +429,9 @@ class LogEventsList {
}
/**
- * @param Row $row
- * @param int $field one of DELETED_* bitfield constants
- * @return bool
+ * @param $row Row
+ * @param $field Integer: one of DELETED_* bitfield constants
+ * @return Boolean
*/
public static function isDeleted( $row, $field ) {
return ($row->log_deleted & $field) == $field;
@@ -358,16 +439,19 @@ class LogEventsList {
/**
* Quick function to show a short log extract
- * @param OutputPage $out
- * @param string $type
- * @param string $page
- * @param string $user
+ * @param $out OutputPage
+ * @param $type String
+ * @param $page String
+ * @param $user String
+ * @param $lim Integer
+ * @param $conds Array
*/
- public static function showLogExtract( $out, $type='', $page='', $user='' ) {
+ public static function showLogExtract( $out, $type='', $page='', $user='', $lim=0, $conds=array() ) {
global $wgUser;
# Insert list of top 50 or so items
$loglist = new LogEventsList( $wgUser->getSkin(), $out, 0 );
- $pager = new LogPager( $loglist, $type, $user, $page, '' );
+ $pager = new LogPager( $loglist, $type, $user, $page, '', $conds );
+ if( $lim > 0 ) $pager->mLimit = $lim;
$logBody = $pager->getBody();
if( $logBody ) {
$out->addHTML(
@@ -378,27 +462,28 @@ class LogEventsList {
} else {
$out->addWikiMsg( 'logempty' );
}
- }
+ return $pager->getNumRows();
+ }
- /**
+ /**
* SQL clause to skip forbidden log types for this user
- * @param Database $db
- * @returns mixed (string or false)
+ * @param $db Database
+ * @return mixed (string or false)
*/
public static function getExcludeClause( $db ) {
global $wgLogRestrictions, $wgUser;
// Reset the array, clears extra "where" clauses when $par is used
$hiddenLogs = array();
// Don't show private logs to unprivileged users
- foreach( $wgLogRestrictions as $logtype => $right ) {
+ foreach( $wgLogRestrictions as $logType => $right ) {
if( !$wgUser->isAllowed($right) ) {
- $safetype = $db->strencode( $logtype );
- $hiddenLogs[] = $safetype;
+ $safeType = $db->strencode( $logType );
+ $hiddenLogs[] = $safeType;
}
}
if( count($hiddenLogs) == 1 ) {
return 'log_type != ' . $db->addQuotes( $hiddenLogs[0] );
- } elseif( !empty( $hiddenLogs ) ) {
+ } elseif( $hiddenLogs ) {
return 'log_type NOT IN (' . $db->makeList($hiddenLogs) . ')';
}
return false;
@@ -409,18 +494,23 @@ class LogEventsList {
* @ingroup Pager
*/
class LogPager extends ReverseChronologicalPager {
- private $type = '', $user = '', $title = '', $pattern = '', $year = '', $month = '';
+ private $type = '', $user = '', $title = '', $pattern = '';
public $mLogEventsList;
+
/**
- * constructor
- * @param LogEventsList $loglist,
- * @param string $type,
- * @param string $user,
- * @param string $page,
- * @param string $pattern
- * @param array $conds
- */
- function __construct( $list, $type='', $user='', $title='', $pattern='', $conds=array(), $y=false, $m=false ) {
+ * constructor
+ * @param $list LogEventsList
+ * @param $type String
+ * @param $user String
+ * @param $title String
+ * @param $pattern String
+ * @param $conds Array
+ * @param $year Integer
+ * @param $month Integer
+ */
+ public function __construct( $list, $type = '', $user = '', $title = '', $pattern = '',
+ $conds = array(), $year = false, $month = false )
+ {
parent::__construct();
$this->mConds = $conds;
@@ -429,22 +519,40 @@ class LogPager extends ReverseChronologicalPager {
$this->limitType( $type );
$this->limitUser( $user );
$this->limitTitle( $title, $pattern );
- $this->limitDate( $y, $m );
+ $this->getDateCond( $year, $month );
}
- function getDefaultQuery() {
+ public function getDefaultQuery() {
$query = parent::getDefaultQuery();
$query['type'] = $this->type;
- $query['month'] = $this->month;
- $query['year'] = $this->year;
+ $query['user'] = $this->user;
+ $query['month'] = $this->mMonth;
+ $query['year'] = $this->mYear;
return $query;
}
+ public function getFilterParams() {
+ global $wgFilterLogTypes, $wgUser, $wgRequest;
+ $filters = array();
+ if( $this->type ) {
+ return $filters;
+ }
+ foreach( $wgFilterLogTypes as $type => $default ) {
+ // Avoid silly filtering
+ if( $type !== 'patrol' || $wgUser->useNPPatrol() ) {
+ $hide = $wgRequest->getInt( "hide_{$type}_log", $default );
+ $filters[$type] = $hide;
+ if( $hide )
+ $this->mConds[] = 'log_type != ' . $this->mDb->addQuotes( $type );
+ }
+ }
+ return $filters;
+ }
+
/**
* Set the log reader to return only entries of the given type.
* Type restrictions enforced here
- * @param string $type A log type ('upload', 'delete', etc)
- * @private
+ * @param $type String: A log type ('upload', 'delete', etc)
*/
private function limitType( $type ) {
global $wgLogRestrictions, $wgUser;
@@ -457,7 +565,7 @@ class LogPager extends ReverseChronologicalPager {
if( $hideLogs !== false ) {
$this->mConds[] = $hideLogs;
}
- if( empty($type) ) {
+ if( !$type ) {
return false;
}
$this->type = $type;
@@ -466,10 +574,9 @@ class LogPager extends ReverseChronologicalPager {
/**
* Set the log reader to return only entries by the given user.
- * @param string $name (In)valid user name
- * @private
+ * @param $name String: (In)valid user name
*/
- function limitUser( $name ) {
+ private function limitUser( $name ) {
if( $name == '' ) {
return false;
}
@@ -492,10 +599,10 @@ class LogPager extends ReverseChronologicalPager {
/**
* Set the log reader to return only entries affecting the given page.
* (For the block and rights logs, this is a user page.)
- * @param string $page Title name as text
- * @private
+ * @param $page String: Title name as text
+ * @param $pattern String
*/
- function limitTitle( $page, $pattern ) {
+ private function limitTitle( $page, $pattern ) {
global $wgMiserMode;
$title = Title::newFromText( $page );
@@ -527,46 +634,7 @@ class LogPager extends ReverseChronologicalPager {
}
}
- /**
- * Set the log reader to return only entries from given date.
- * @param int $year
- * @param int $month
- * @private
- */
- function limitDate( $year, $month ) {
- $year = intval($year);
- $month = intval($month);
-
- $this->year = ($year > 0 && $year < 10000) ? $year : '';
- $this->month = ($month > 0 && $month < 13) ? $month : '';
-
- 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 getQueryInfo() {
+ public function getQueryInfo() {
$this->mConds[] = 'user_id = log_user';
# Don't use the wrong logging index
if( $this->title || $this->pattern || $this->user ) {
@@ -589,7 +657,7 @@ class LogPager extends ReverseChronologicalPager {
return 'log_timestamp';
}
- function getStartBody() {
+ public function getStartBody() {
wfProfileIn( __METHOD__ );
# Do a link batch query
if( $this->getNumRows() > 0 ) {
@@ -606,7 +674,7 @@ class LogPager extends ReverseChronologicalPager {
return '';
}
- function formatRow( $row ) {
+ public function formatRow( $row ) {
return $this->mLogEventsList->logLine( $row );
}
@@ -627,11 +695,11 @@ class LogPager extends ReverseChronologicalPager {
}
public function getYear() {
- return $this->year;
+ return $this->mYear;
}
public function getMonth() {
- return $this->month;
+ return $this->mMonth;
}
}
@@ -642,26 +710,27 @@ class LogPager extends ReverseChronologicalPager {
class LogReader {
var $pager;
/**
- * @param WebRequest $request For internal use use a FauxRequest object to pass arbitrary parameters.
+ * @param $request WebRequest: for internal use use a FauxRequest object to pass arbitrary parameters.
*/
function __construct( $request ) {
global $wgUser, $wgOut;
+ wfDeprecated(__METHOD__);
# Get parameters
$type = $request->getVal( 'type' );
$user = $request->getText( 'user' );
$title = $request->getText( 'page' );
$pattern = $request->getBool( 'pattern' );
- $y = $request->getIntOrNull( 'year' );
- $m = $request->getIntOrNull( 'month' );
+ $year = $request->getIntOrNull( 'year' );
+ $month = $request->getIntOrNull( 'month' );
# Don't let the user get stuck with a certain date
$skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev';
if( $skip ) {
- $y = '';
- $m = '';
+ $year = '';
+ $month = '';
}
# Use new list class to output results
$loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 );
- $this->pager = new LogPager( $loglist, $type, $user, $title, $pattern, $y, $m );
+ $this->pager = new LogPager( $loglist, $type, $user, $title, $pattern, $year, $month );
}
/**
@@ -679,17 +748,20 @@ class LogReader {
*/
class LogViewer {
const NO_ACTION_LINK = 1;
+
/**
- * @var LogReader $reader
+ * LogReader object
*/
var $reader;
+
/**
- * @param LogReader &$reader where to get our data from
- * @param integer $flags Bitwise combination of flags:
+ * @param &$reader LogReader: where to get our data from
+ * @param $flags Integer: Bitwise combination of flags:
* LogEventsList::NO_ACTION_LINK Don't show restore/unblock/block links
*/
function __construct( &$reader, $flags = 0 ) {
global $wgUser;
+ wfDeprecated(__METHOD__);
$this->reader =& $reader;
$this->reader->pager->mLogEventsList->flags = $flags;
# Aliases for shorter code...
@@ -725,7 +797,7 @@ class LogViewer {
* Output just the list of entries given by the linked LogReader,
* with extraneous UI elements. Use for displaying log fragments in
* another page (eg at Special:Undelete)
- * @param OutputPage $out where to send output
+ * @param $out OutputPage: where to send output
*/
public function showList( &$out ) {
$logBody = $this->pager->getBody();
diff --git a/includes/LogPage.php b/includes/LogPage.php
index 27554308..50a9a232 100644
--- a/includes/LogPage.php
+++ b/includes/LogPage.php
@@ -89,6 +89,9 @@ class LogPage {
return true;
}
+ /**
+ * Get the RC comment from the last addEntry() call
+ */
public function getRcComment() {
$rcComment = $this->actionText;
if( '' != $this->comment ) {
@@ -101,6 +104,13 @@ class LogPage {
}
/**
+ * Get the comment from the last addEntry() call
+ */
+ public function getComment() {
+ return $this->comment;
+ }
+
+ /**
* @static
*/
public static function validTypes() {
@@ -136,7 +146,8 @@ class LogPage {
* @return string Headertext of this logtype
*/
static function logHeader( $type ) {
- global $wgLogHeaders;
+ global $wgLogHeaders, $wgMessageCache;
+ $wgMessageCache->loadAllMessages();
return wfMsgExt($wgLogHeaders[$type],array('parseinline'));
}
@@ -144,54 +155,24 @@ class LogPage {
* @static
* @return HTML string
*/
- static function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks=false ) {
- global $wgLang, $wgContLang, $wgLogActions;
+ static function actionText( $type, $action, $title = NULL, $skin = NULL,
+ $params = array(), $filterWikilinks = false )
+ {
+ global $wgLang, $wgContLang, $wgLogActions, $wgMessageCache;
+ $wgMessageCache->loadAllMessages();
$key = "$type/$action";
-
- if( $key == 'patrol/patrol' )
+ # Defer patrol log to PatrolLog class
+ if( $key == 'patrol/patrol' ) {
return PatrolLog::makeActionText( $title, $params, $skin );
-
+ }
if( isset( $wgLogActions[$key] ) ) {
if( is_null( $title ) ) {
- $rv=wfMsg( $wgLogActions[$key] );
+ $rv = wfMsg( $wgLogActions[$key] );
} else {
- if( $skin ) {
-
- switch( $type ) {
- case 'move':
- $titleLink = $skin->makeLinkObj( $title, htmlspecialchars( $title->getPrefixedText() ), 'redirect=no' );
- $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) );
- break;
- case 'block':
- if( substr( $title->getText(), 0, 1 ) == '#' ) {
- $titleLink = $title->getText();
- } else {
- // 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':
- $text = $wgContLang->ucfirst( $title->getText() );
- $titleLink = $skin->makeLinkObj( Title::makeTitle( NS_USER, $text ) );
- break;
- case 'merge':
- $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' );
- $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) );
- $params[1] = $wgLang->timeanddate( $params[1] );
- break;
- default:
- $titleLink = $skin->makeLinkObj( $title );
- }
-
- } else {
- $titleLink = $title->getPrefixedText();
- }
+ $titleLink = self::getTitleLink( $type, $skin, $title, $params );
if( $key == 'rights/rights' ) {
- if ($skin) {
+ if( $skin ) {
$rightsnone = wfMsg( 'rightsnone' );
foreach ( $params as &$param ) {
$groupArray = array_map( 'trim', explode( ',', $param ) );
@@ -213,18 +194,28 @@ class LogPage {
$rv = wfMsgForContent( $wgLogActions[$key], $titleLink );
}
} else {
+ $details = '';
array_unshift( $params, $titleLink );
- if ( $key == 'block/block' || $key == 'suppress/block' ) {
+ if ( $key == 'block/block' || $key == 'suppress/block' || $key == 'block/reblock' ) {
if ( $skin ) {
- $params[1] = '<span title="' . htmlspecialchars( $params[1] ). '">' . $wgLang->translateBlockExpiry( $params[1] ) . '</span>';
+ $params[1] = '<span title="' . htmlspecialchars( $params[1] ). '">' .
+ $wgLang->translateBlockExpiry( $params[1] ) . '</span>';
} else {
$params[1] = $wgContLang->translateBlockExpiry( $params[1] );
}
- $params[2] = isset( $params[2] )
- ? self::formatBlockFlags( $params[2], is_null( $skin ) )
- : '';
+ $params[2] = isset( $params[2] ) ?
+ self::formatBlockFlags( $params[2], is_null( $skin ) ) : '';
+ } else if ( $type == 'protect' && count($params) == 3 ) {
+ $details .= " {$params[1]}"; // restrictions and expiries
+ if( $params[2] ) {
+ $details .= ' ['.wfMsg('protect-summary-cascade').']';
+ }
+ } else if ( $type == 'move' && count( $params ) == 3 ) {
+ if( $params[2] ) {
+ $details .= ' [' . wfMsg( 'move-redirect-suppressed' ) . ']';
+ }
}
- $rv = wfMsgReal( $wgLogActions[$key], $params, true, !$skin );
+ $rv = wfMsgReal( $wgLogActions[$key], $params, true, !$skin ) . $details;
}
}
} else {
@@ -243,6 +234,59 @@ class LogPage {
}
return $rv;
}
+
+ protected static function getTitleLink( $type, $skin, $title, &$params ) {
+ global $wgLang, $wgContLang;
+ if( !$skin ) {
+ return $title->getPrefixedText();
+ }
+ switch( $type ) {
+ case 'move':
+ $titleLink = $skin->makeLinkObj( $title,
+ htmlspecialchars( $title->getPrefixedText() ), 'redirect=no' );
+ $targetTitle = Title::newFromText( $params[0] );
+ if ( !$targetTitle ) {
+ # Workaround for broken database
+ $params[0] = htmlspecialchars( $params[0] );
+ } else {
+ $params[0] = $skin->makeLinkObj( $targetTitle, htmlspecialchars( $params[0] ) );
+ }
+ break;
+ case 'block':
+ if( substr( $title->getText(), 0, 1 ) == '#' ) {
+ $titleLink = $title->getText();
+ } else {
+ // 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':
+ $text = $wgContLang->ucfirst( $title->getText() );
+ $titleLink = $skin->makeLinkObj( Title::makeTitle( NS_USER, $text ) );
+ break;
+ case 'merge':
+ $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' );
+ $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) );
+ $params[1] = $wgLang->timeanddate( $params[1] );
+ break;
+ default:
+ if( $title->getNamespace() == NS_SPECIAL ) {
+ list( $name, $par ) = SpecialPage::resolveAliasWithSubpage( $title->getDBKey() );
+ # Use the language name for log titles, rather than Log/X
+ if( $name == 'Log' ) {
+ $titleLink = '('.$skin->makeLinkObj( $title, LogPage::logName( $par ) ).')';
+ } else {
+ $titleLink = $skin->makeLinkObj( $title );
+ }
+ } else {
+ $titleLink = $skin->makeLinkObj( $title );
+ }
+ }
+ return $titleLink;
+ }
/**
* Add a log entry
diff --git a/includes/MagicWord.php b/includes/MagicWord.php
index 3b22cb9b..5b5b77f0 100644
--- a/includes/MagicWord.php
+++ b/includes/MagicWord.php
@@ -103,8 +103,12 @@ class MagicWord {
'contentlanguage',
'pagesinnamespace',
'numberofadmins',
+ 'numberofviews',
'defaultsort',
'pagesincategory',
+ 'index',
+ 'noindex',
+ 'numberingroup',
);
/* Array of caching hints for ParserCache */
@@ -143,6 +147,8 @@ class MagicWord {
'localtimestamp' => 3600,
'pagesinnamespace' => 3600,
'numberofadmins' => 3600,
+ 'numberofviews' => 3600,
+ 'numberingroup' => 3600,
);
static public $mDoubleUnderscoreIDs = array(
@@ -153,6 +159,8 @@ class MagicWord {
'noeditsection',
'newsectionlink',
'hiddencat',
+ 'index',
+ 'noindex',
'staticredirect',
);
diff --git a/includes/Math.php b/includes/Math.php
index 871e9fc3..2ed16033 100644
--- a/includes/Math.php
+++ b/includes/Math.php
@@ -47,7 +47,7 @@ class MathRenderer {
if( !$this->_recall() ) {
# Ensure that the temp and output directories are available before continuing...
if( !file_exists( $wgTmpDirectory ) ) {
- if( !@mkdir( $wgTmpDirectory ) ) {
+ if( !wfMkdirParents( $wgTmpDirectory ) ) {
return $this->_error( 'math_bad_tmpdir' );
}
} elseif( !is_dir( $wgTmpDirectory ) || !is_writable( $wgTmpDirectory ) ) {
@@ -145,6 +145,10 @@ class MathRenderer {
return $this->_error( 'math_image_error' );
}
+ if( filesize( "$wgTmpDirectory/{$this->hash}.png" ) == 0 ) {
+ return $this->_error( 'math_image_error' );
+ }
+
$hashpath = $this->_getHashPath();
if( !file_exists( $hashpath ) ) {
if( !@wfMkdirParents( $hashpath, 0755 ) ) {
@@ -172,10 +176,17 @@ class MathRenderer {
'math_html_conservativeness' => $this->conservativeness,
'math_html' => $this->html,
'math_mathml' => $this->mathml,
- ), $fname, array( 'IGNORE' )
+ ), $fname
);
}
-
+
+ // If we're replacing an older version of the image, make sure it's current.
+ global $wgUseSquid;
+ if ( $wgUseSquid ) {
+ $urls = array( $this->_mathImageUrl() );
+ $u = new SquidUpdate( $urls );
+ $u->doUpdate();
+ }
}
return $this->_doRender();
@@ -209,8 +220,14 @@ class MathRenderer {
$this->html = $rpage->math_html;
$this->mathml = $rpage->math_mathml;
- if( file_exists( $this->_getHashPath() . "/{$this->hash}.png" ) ) {
- return true;
+ $filename = $this->_getHashPath() . "/{$this->hash}.png";
+ if( file_exists( $filename ) ) {
+ if( filesize( $filename ) == 0 ) {
+ // Some horrible error corrupted stuff :(
+ @unlink( $filename );
+ } else {
+ return true;
+ }
}
if( file_exists( $wgMathDirectory . "/{$this->hash}.png" ) ) {
@@ -268,10 +285,7 @@ class MathRenderer {
}
function _linkToMathImage() {
- global $wgMathPath;
- $url = "$wgMathPath/" . substr($this->hash, 0, 1)
- .'/'. substr($this->hash, 1, 1) .'/'. substr($this->hash, 2, 1)
- . "/{$this->hash}.png";
+ $url = $this->_mathImageUrl();
return Xml::element( 'img',
$this->_attribs(
@@ -283,14 +297,24 @@ class MathRenderer {
'src' => $url ) ) );
}
+ function _mathImageUrl() {
+ global $wgMathPath;
+ $dir = $this->_getHashSubPath();
+ return "$wgMathPath/$dir/{$this->hash}.png";
+ }
+
function _getHashPath() {
global $wgMathDirectory;
- $path = $wgMathDirectory .'/'. substr($this->hash, 0, 1)
- .'/'. substr($this->hash, 1, 1)
- .'/'. substr($this->hash, 2, 1);
+ $path = $wgMathDirectory .'/' . $this->_getHashSubPath();
wfDebug( "TeX: getHashPath, hash is: $this->hash, path is: $path\n" );
return $path;
}
+
+ function _getHashSubPath() {
+ return substr($this->hash, 0, 1)
+ .'/'. substr($this->hash, 1, 1)
+ .'/'. substr($this->hash, 2, 1);
+ }
public static function renderMath( $tex, $params=array() ) {
global $wgUser;
diff --git a/includes/MediaTransformOutput.php b/includes/MediaTransformOutput.php
index 9e94f06b..0367494f 100644
--- a/includes/MediaTransformOutput.php
+++ b/includes/MediaTransformOutput.php
@@ -50,6 +50,8 @@ abstract class MediaTransformOutput {
* alt Alternate text or caption
* desc-link Boolean, show a description link
* file-link Boolean, show a file download link
+ * custom-url-link Custom URL to link to
+ * custom-title-link Custom Title object to link to
* 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
*
@@ -127,12 +129,15 @@ class ThumbnailImage extends MediaTransformOutput {
* should be indicated with a value of true for true, and false or
* absent for false.
*
- * alt Alternate text or caption
+ * alt HTML alt attribute
+ * title HTML title attribute
* 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
* desc-query String, description link query params
+ * custom-url-link Custom URL to link to
+ * custom-title-link Custom Title object to link to
*
* For images, desc-link and file-link are implemented as a click-through. For
* sounds and videos, they may be displayed in other ways.
@@ -146,9 +151,18 @@ class ThumbnailImage extends MediaTransformOutput {
}
$alt = empty( $options['alt'] ) ? '' : $options['alt'];
+ # Note: if title is empty and alt is not, make the title empty, don't
+ # use alt; only use alt if title is not set
+ $title = !isset( $options['title'] ) ? $alt : $options['title'];
$query = empty($options['desc-query']) ? '' : $options['desc-query'];
- if ( !empty( $options['desc-link'] ) ) {
- $linkAttribs = $this->getDescLinkAttribs( $alt, $query );
+
+ if ( !empty( $options['custom-url-link'] ) ) {
+ $linkAttribs = array( 'href' => $options['custom-url-link'] );
+ } elseif ( !empty( $options['custom-title-link'] ) ) {
+ $title = $options['custom-title-link'];
+ $linkAttribs = array( 'href' => $title->getLinkUrl(), 'title' => $title->getFullText() );
+ } elseif ( !empty( $options['desc-link'] ) ) {
+ $linkAttribs = $this->getDescLinkAttribs( $title, $query );
} elseif ( !empty( $options['file-link'] ) ) {
$linkAttribs = array( 'href' => $this->file->getURL() );
} else {
diff --git a/includes/MessageCache.php b/includes/MessageCache.php
index f24d3b4d..a06b0cb9 100644
--- a/includes/MessageCache.php
+++ b/includes/MessageCache.php
@@ -25,7 +25,7 @@ class MessageCache {
var $mKeys, $mParserOptions, $mParser;
var $mExtensionMessages = array();
var $mInitialised = false;
- var $mAllMessagesLoaded; // Extension messages
+ var $mAllMessagesLoaded = array(); // Extension messages
// Variable for tracking which variables are loaded
var $mLoadedLanguages = array();
@@ -44,7 +44,6 @@ class MessageCache {
/**
* ParserOptions is lazy initialised.
- * Access should probably be protected.
*/
function getParserOptions() {
if ( !$this->mParserOptions ) {
@@ -110,7 +109,7 @@ class MessageCache {
global $wgLocalMessageCache;
$filename = "$wgLocalMessageCache/messages-" . wfWikiID() . "-$code";
- wfMkdirParents( $wgLocalMessageCache, 0777 ); // might fail
+ wfMkdirParents( $wgLocalMessageCache ); // might fail
wfSuppressWarnings();
$file = fopen( $filename, 'w' );
@@ -131,7 +130,7 @@ class MessageCache {
$filename = "$wgLocalMessageCache/messages-" . wfWikiID() . "-$code";
$tempFilename = $filename . '.tmp';
- wfMkdirParents( $wgLocalMessageCache, 0777 ); // might fail
+ wfMkdirParents( $wgLocalMessageCache ); // might fail
wfSuppressWarnings();
$file = fopen( $tempFilename, 'w');
@@ -261,12 +260,23 @@ class MessageCache {
$this->lock($cacheKey);
- $cache = $this->loadFromDB( $code );
- $success = $this->setCache( $cache, $code );
+ # Limit the concurrency of loadFromDB to a single process
+ # This prevents the site from going down when the cache expires
+ $statusKey = wfMemcKey( 'messages', $code, 'status' );
+ $success = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT );
if ( $success ) {
- $this->saveToCaches( $cache, true, $code );
+ $cache = $this->loadFromDB( $code );
+ $success = $this->setCache( $cache, $code );
+ }
+ if ( $success ) {
+ $success = $this->saveToCaches( $cache, true, $code );
+ if ( $success ) {
+ $this->mMemc->delete( $statusKey );
+ } else {
+ $this->mMemc->set( $statusKey, 'error', 60*5 );
+ wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" );
+ }
}
-
$this->unlock($cacheKey);
}
@@ -414,10 +424,6 @@ class MessageCache {
global $wgLocalMessageCache, $wgLocalMessageCacheSerialized;
$cacheKey = wfMemcKey( 'messages', $code );
- $statusKey = wfMemcKey( 'messages', $code, 'status' );
-
- $success = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT );
- if ( !$success ) return true; # Other process should be updating them now
$i = 0;
if ( $memc ) {
@@ -444,11 +450,8 @@ class MessageCache {
}
if ( $i == 20 ) {
- $this->mMemc->set( $statusKey, 'error', 60*5 );
- wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" );
$success = false;
} else {
- $this->mMemc->delete( $statusKey );
$success = true;
}
wfProfileOut( __METHOD__ );
@@ -498,29 +501,9 @@ class MessageCache {
* @param bool $isFullKey Specifies whether $key is a two part key "lang/msg".
*/
function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) {
- global $wgContLanguageCode, $wgContLang, $wgLang;
-
- # Identify which language to get or create a language object for.
- if( $langcode === $wgContLang->getCode() || $langcode === true ) {
- # $langcode is the language code of the wikis content language object.
- # or it is a boolean and value is true
- $lang =& $wgContLang;
- } elseif( $langcode === $wgLang->getCode() || $langcode === false ) {
- # $langcode is the language code of user language object.
- # or it was a boolean and value is false
- $lang =& $wgLang;
- } else {
- $validCodes = array_keys( Language::getLanguageNames() );
- if( in_array( $langcode, $validCodes ) ) {
- # $langcode corresponds to a valid language.
- $lang = Language::factory( $langcode );
- } else {
- # $langcode is a string, but not a valid language code; use content language.
- $lang =& $wgContLang;
- wfDebug( 'Invalid language code passed to MessageCache::get, falling back to content language.' );
- }
- }
+ global $wgContLanguageCode, $wgContLang;
+ $lang = wfGetLangObj( $langcode );
$langcode = $lang->getCode();
# If uninitialised, someone is trying to call this halfway through Setup.php
@@ -664,23 +647,30 @@ class MessageCache {
return $message;
}
- function transform( $message, $interface = false ) {
+ function transform( $message, $interface = false, $language = null ) {
// Avoid creating parser if nothing to transfrom
if( strpos( $message, '{{' ) === false ) {
return $message;
}
- global $wgParser;
+ global $wgParser, $wgParserConf;
if ( !$this->mParser && isset( $wgParser ) ) {
# Do some initialisation so that we don't have to do it twice
$wgParser->firstCallInit();
# Clone it and store it
- $this->mParser = clone $wgParser;
+ $class = $wgParserConf['class'];
+ if ( $class == 'Parser_DiffTest' ) {
+ # Uncloneable
+ $this->mParser = new $class( $wgParserConf );
+ } else {
+ $this->mParser = clone $wgParser;
+ }
#wfDebug( __METHOD__ . ": following contents triggered transform: $message\n" );
}
if ( $this->mParser ) {
$popts = $this->getParserOptions();
$popts->setInterfaceMessage( $interface );
+ $popts->setTargetLanguage( $language );
$message = $this->mParser->transformMsg( $message, $popts );
}
return $message;
@@ -781,12 +771,13 @@ class MessageCache {
}
}
- function loadAllMessages() {
+ function loadAllMessages( $lang = false ) {
global $wgExtensionMessagesFiles;
- if ( $this->mAllMessagesLoaded ) {
+ $key = $lang === false ? '*' : $lang;
+ if ( isset( $this->mAllMessagesLoaded[$key] ) ) {
return;
}
- $this->mAllMessagesLoaded = true;
+ $this->mAllMessagesLoaded[$key] = true;
# Some extensions will load their messages when you load their class file
wfLoadAllExtensions();
@@ -794,7 +785,7 @@ class MessageCache {
wfRunHooks( 'LoadAllMessages' );
# Some register their messages in $wgExtensionMessagesFiles
foreach ( $wgExtensionMessagesFiles as $name => $file ) {
- wfLoadExtensionMessages( $name );
+ wfLoadExtensionMessages( $name, $lang );
}
# Still others will respond to neither, they are EVIL. We sometimes need to know!
}
@@ -855,13 +846,17 @@ class MessageCache {
public function figureMessage( $key ) {
global $wgContLanguageCode;
- $pieces = explode('/', $key, 2);
+ $pieces = explode( '/', $key );
+ if( count( $pieces ) < 2 )
+ return array( $key, $wgContLanguageCode );
- $key = $pieces[0];
+ $lang = array_pop( $pieces );
+ $validCodes = Language::getLanguageNames();
+ if( !array_key_exists( $lang, $validCodes ) )
+ return array( $key, $wgContLanguageCode );
- # Language the user is translating to
- $langCode = isset($pieces[1]) ? $pieces[1] : $wgContLanguageCode;
- return array( $key, $langCode );
+ $message = implode( '/', $pieces );
+ return array( $message, $lang );
}
}
diff --git a/includes/Metadata.php b/includes/Metadata.php
index a543c73c..0b4fbf8c 100644
--- a/includes/Metadata.php
+++ b/includes/Metadata.php
@@ -20,347 +20,299 @@
* @author Evan Prodromou <evan@wikitravel.org>
*/
-/**
- * TODO: Perhaps make this file into a Metadata class, with static methods (declared
- * as private where indicated), to move these functions out of the global namespace?
- */
-define('RDF_TYPE_PREFS', "application/rdf+xml,text/xml;q=0.7,application/xml;q=0.5,text/rdf;q=0.1");
-
-function wfDublinCoreRdf($article) {
-
- $url = dcReallyFullUrl($article->mTitle);
-
- if (rdfSetup()) {
- dcPrologue($url);
- dcBasics($article);
- dcEpilogue();
+abstract class RdfMetaData {
+ const RDF_TYPE_PREFS = 'application/rdf+xml,text/xml;q=0.7,application/xml;q=0.5,text/rdf;q=0.1';
+
+ /**
+ * Constructor
+ * @param $article Article object
+ */
+ public function __construct( Article $article ){
+ $this->mArticle = $article;
}
-}
-function wfCreativeCommonsRdf($article) {
+ public abstract function show();
- if (rdfSetup()) {
- global $wgRightsUrl;
+ /**
+ *
+ */
+ protected function setup() {
+ global $wgOut, $wgRequest;
- $url = dcReallyFullUrl($article->mTitle);
+ $httpaccept = isset( $_SERVER['HTTP_ACCEPT'] ) ? $_SERVER['HTTP_ACCEPT'] : null;
+ $rdftype = wfNegotiateType( wfAcceptToPrefs( $httpaccept ), wfAcceptToPrefs( self::RDF_TYPE_PREFS ) );
- ccPrologue();
- ccSubPrologue('Work', $url);
- dcBasics($article);
- if (isset($wgRightsUrl)) {
- $url = htmlspecialchars( $wgRightsUrl );
- print " <cc:license rdf:resource=\"$url\" />\n";
+ if( !$rdftype ){
+ wfHttpError( 406, 'Not Acceptable', wfMsg( 'notacceptable' ) );
+ return false;
+ } else {
+ $wgOut->disable();
+ $wgRequest->response()->header( "Content-type: {$rdftype}; charset=utf-8" );
+ $wgOut->sendCacheControl();
+ return true;
}
+ }
- ccSubEpilogue('Work');
-
- if (isset($wgRightsUrl)) {
- $terms = ccGetTerms($wgRightsUrl);
- if ($terms) {
- ccSubPrologue('License', $wgRightsUrl);
- ccLicense($terms);
- ccSubEpilogue('License');
- }
- }
+ /**
+ *
+ */
+ protected function reallyFullUrl() {
+ return $this->mArticle->getTitle()->getFullURL();
}
- ccEpilogue();
-}
+ protected function basics() {
+ global $wgContLanguageCode, $wgSitename;
-/**
- * @private
- */
-function rdfSetup() {
- global $wgOut, $_SERVER;
+ $this->element( 'title', $this->mArticle->mTitle->getText() );
+ $this->pageOrString( 'publisher', wfMsg( 'aboutpage' ), $wgSitename );
+ $this->element( 'language', $wgContLanguageCode );
+ $this->element( 'type', 'Text' );
+ $this->element( 'format', 'text/html' );
+ $this->element( 'identifier', $this->reallyFullUrl() );
+ $this->element( 'date', $this->date( $this->mArticle->getTimestamp() ) );
- $httpaccept = isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : null;
+ $lastEditor = User::newFromId( $this->mArticle->getUser() );
+ $this->person( 'creator', $lastEditor );
- $rdftype = wfNegotiateType(wfAcceptToPrefs($httpaccept), wfAcceptToPrefs(RDF_TYPE_PREFS));
+ foreach( $this->mArticle->getContributors() as $user ){
+ $this->person( 'contributor', $user );
+ }
- if (!$rdftype) {
- wfHttpError(406, "Not Acceptable", wfMsg("notacceptable"));
- return false;
- } else {
- $wgOut->disable();
- header( "Content-type: {$rdftype}; charset=utf-8" );
- $wgOut->sendCacheControl();
- return true;
+ $this->rights();
}
-}
-/**
- * @private
- */
-function dcPrologue($url) {
- global $wgOutputEncoding;
+ protected function element( $name, $value ) {
+ $value = htmlspecialchars( $value );
+ print "\t\t<dc:{$name}>{$value}</dc:{$name}>\n";
+ }
- $url = htmlspecialchars( $url );
- print "<" . "?xml version=\"1.0\" encoding=\"{$wgOutputEncoding}\" ?" . ">
+ protected function date($timestamp) {
+ return substr($timestamp, 0, 4) . '-'
+ . substr($timestamp, 4, 2) . '-'
+ . substr($timestamp, 6, 2);
+ }
- <!DOCTYPE rdf:RDF PUBLIC \"-//DUBLIN CORE//DCMES DTD 2002/07/31//EN\" \"http://dublincore.org/documents/2002/07/31/dcmes-xml/dcmes-xml-dtd.dtd\">
+ protected function pageOrString( $name, $page, $str ){
+ if( $page instanceof Title )
+ $nt = $page;
+ else
+ $nt = Title::newFromText( $page );
- <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"
- xmlns:dc=\"http://purl.org/dc/elements/1.1/\">
- <rdf:Description rdf:about=\"$url\">
- ";
-}
+ if( !$nt || $nt->getArticleID() == 0 ){
+ $this->element( $name, $str );
+ } else {
+ $this->page( $name, $nt );
+ }
+ }
-/**
- * @private
- */
-function dcEpilogue() {
- print "
- </rdf:Description>
- </rdf:RDF>
- ";
-}
+ protected function page( $name, $title ){
+ $this->url( $name, $title->getFullUrl() );
+ }
-/**
- * @private
- */
-function dcBasics($article) {
- global $wgContLanguageCode, $wgSitename;
-
- dcElement('title', $article->mTitle->getText());
- dcPageOrString('publisher', wfMsg('aboutpage'), $wgSitename);
- dcElement('language', $wgContLanguageCode);
- dcElement('type', 'Text');
- dcElement('format', 'text/html');
- dcElement('identifier', dcReallyFullUrl($article->mTitle));
- dcElement('date', dcDate($article->getTimestamp()));
-
- $last_editor = $article->getUser();
-
- if ($last_editor == 0) {
- dcPerson('creator', 0);
- } else {
- dcPerson('creator', $last_editor, $article->getUserText(),
- User::whoIsReal($last_editor));
+ protected function url($name, $url) {
+ $url = htmlspecialchars( $url );
+ print "\t\t<dc:{$name} rdf:resource=\"{$url}\" />\n";
}
- $contributors = $article->getContributors();
+ protected function person($name, User $user ){
+ global $wgContLang;
- foreach ($contributors as $user_parts) {
- dcPerson('contributor', $user_parts[0], $user_parts[1], $user_parts[2]);
+ if( $user->isAnon() ){
+ $this->element( $name, wfMsgExt( 'anonymous', array( 'parsemag' ), 1 ) );
+ } else if( $real = $user->getRealName() ) {
+ $this->element( $name, $real );
+ } else {
+ $this->pageOrString( $name, $user->getUserPage(), wfMsg( 'siteuser', $user->getName() ) );
+ }
}
- dcRights();
-}
+ /**
+ * Takes an arg, for future enhancement with different rights for
+ * different pages.
+ */
+ protected function rights() {
+ global $wgRightsPage, $wgRightsUrl, $wgRightsText;
+
+ if( $wgRightsPage && ( $nt = Title::newFromText( $wgRightsPage ) )
+ && ($nt->getArticleID() != 0)) {
+ $this->page('rights', $nt);
+ } else if( $wgRightsUrl ){
+ $this->url('rights', $wgRightsUrl);
+ } else if( $wgRightsText ){
+ $this->element( 'rights', $wgRightsText );
+ }
+ }
-/**
- * @private
- */
-function ccPrologue() {
- global $wgOutputEncoding;
+ protected function getTerms( $url ){
+ global $wgLicenseTerms;
- echo "<" . "?xml version='1.0' encoding='{$wgOutputEncoding}' ?" . ">
+ if( $wgLicenseTerms ){
+ return $wgLicenseTerms;
+ } else {
+ $known = $this->getKnownLicenses();
+ if( isset( $known[$url] ) ) {
+ return $known[$url];
+ } else {
+ return array();
+ }
+ }
+ }
- <rdf:RDF xmlns:cc=\"http://web.resource.org/cc/\"
- xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
- xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">
- ";
-}
+ protected function getKnownLicenses() {
+ $ccLicenses = array('by', 'by-nd', 'by-nd-nc', 'by-nc',
+ 'by-nc-sa', 'by-sa');
+ $ccVersions = array('1.0', '2.0');
+ $knownLicenses = array();
+
+ foreach ($ccVersions as $version) {
+ foreach ($ccLicenses as $license) {
+ if( $version == '2.0' && substr( $license, 0, 2) != 'by' ) {
+ # 2.0 dropped the non-attribs licenses
+ continue;
+ }
+ $lurl = "http://creativecommons.org/licenses/{$license}/{$version}/";
+ $knownLicenses[$lurl] = explode('-', $license);
+ $knownLicenses[$lurl][] = 're';
+ $knownLicenses[$lurl][] = 'di';
+ $knownLicenses[$lurl][] = 'no';
+ if (!in_array('nd', $knownLicenses[$lurl])) {
+ $knownLicenses[$lurl][] = 'de';
+ }
+ }
+ }
-/**
- * @private
- */
-function ccSubPrologue($type, $url) {
- $url = htmlspecialchars( $url );
- echo " <cc:{$type} rdf:about=\"{$url}\">\n";
-}
+ /* Handle the GPL and LGPL, too. */
-/**
- * @private
- */
-function ccSubEpilogue($type) {
- echo " </cc:{$type}>\n";
-}
+ $knownLicenses['http://creativecommons.org/licenses/GPL/2.0/'] =
+ array('de', 're', 'di', 'no', 'sa', 'sc');
+ $knownLicenses['http://creativecommons.org/licenses/LGPL/2.1/'] =
+ array('de', 're', 'di', 'no', 'sa', 'sc');
+ $knownLicenses['http://www.gnu.org/copyleft/fdl.html'] =
+ array('de', 're', 'di', 'no', 'sa', 'sc');
-/**
- * @private
- */
-function ccLicense($terms) {
-
- foreach ($terms as $term) {
- switch ($term) {
- case 're':
- ccTerm('permits', 'Reproduction'); break;
- case 'di':
- ccTerm('permits', 'Distribution'); break;
- case 'de':
- ccTerm('permits', 'DerivativeWorks'); break;
- case 'nc':
- ccTerm('prohibits', 'CommercialUse'); break;
- case 'no':
- ccTerm('requires', 'Notice'); break;
- case 'by':
- ccTerm('requires', 'Attribution'); break;
- case 'sa':
- ccTerm('requires', 'ShareAlike'); break;
- case 'sc':
- ccTerm('requires', 'SourceCode'); break;
- }
+ return $knownLicenses;
}
}
-/**
- * @private
- */
-function ccTerm($term, $name) {
- print " <cc:{$term} rdf:resource=\"http://web.resource.org/cc/{$name}\" />\n";
-}
+class DublinCoreRdf extends RdfMetaData {
-/**
- * @private
- */
-function ccEpilogue() {
- echo "</rdf:RDF>\n";
-}
+ public function show(){
+ if( $this->setup() ){
+ $this->prologue();
+ $this->basics();
+ $this->epilogue();
+ }
+ }
-/**
- * @private
- */
-function dcElement($name, $value) {
- $value = htmlspecialchars( $value );
- print " <dc:{$name}>{$value}</dc:{$name}>\n";
-}
+ /**
+ * begin of the page
+ */
+ protected function prologue() {
+ global $wgOutputEncoding;
+
+ $url = htmlspecialchars( $this->reallyFullUrl() );
+ print <<<PROLOGUE
+<?xml version="1.0" encoding="{$wgOutputEncoding}" ?>
+<!DOCTYPE rdf:RDF PUBLIC "-//DUBLIN CORE//DCMES DTD 2002/07/31//EN" "http://dublincore.org/documents/2002/07/31/dcmes-xml/dcmes-xml-dtd.dtd">
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rdf:Description rdf:about="{$url}">
+
+PROLOGUE;
+ }
-/**
- * @private
- */
-function dcDate($timestamp) {
- return substr($timestamp, 0, 4) . '-'
- . substr($timestamp, 4, 2) . '-'
- . substr($timestamp, 6, 2);
+ /**
+ * end of the page
+ */
+ protected function epilogue() {
+ print <<<EPILOGUE
+ </rdf:Description>
+</rdf:RDF>
+EPILOGUE;
+ }
}
-/**
- * @private
- */
-function dcReallyFullUrl($title) {
- return $title->getFullURL();
-}
+class CreativeCommonsRdf extends RdfMetaData {
-/**
- * @private
- */
-function dcPageOrString($name, $page, $str) {
- $nt = Title::newFromText($page);
+ public function show(){
+ if( $this->setup() ){
+ global $wgRightsUrl;
- if (!$nt || $nt->getArticleID() == 0) {
- dcElement($name, $str);
- } else {
- dcPage($name, $nt);
- }
-}
+ $url = $this->reallyFullUrl();
-/**
- * @private
- */
-function dcPage($name, $title) {
- dcUrl($name, dcReallyFullUrl($title));
-}
+ $this->prologue();
+ $this->subPrologue('Work', $url);
-/**
- * @private
- */
-function dcUrl($name, $url) {
- $url = htmlspecialchars( $url );
- print " <dc:{$name} rdf:resource=\"{$url}\" />\n";
-}
+ $this->basics();
+ if( $wgRightsUrl ){
+ $url = htmlspecialchars( $wgRightsUrl );
+ print "\t\t<cc:license rdf:resource=\"$url\" />\n";
+ }
-/**
- * @private
- */
-function dcPerson($name, $id, $user_name='', $user_real_name='') {
- global $wgContLang;
-
- if ($id == 0) {
- dcElement($name, wfMsg('anonymous'));
- } else if ( !empty($user_real_name) ) {
- dcElement($name, $user_real_name);
- } else {
- # XXX: This shouldn't happen.
- if( empty( $user_name ) ) {
- $user_name = User::whoIs($id);
+ $this->subEpilogue('Work');
+
+ if( $wgRightsUrl ){
+ $terms = $this->getTerms( $wgRightsUrl );
+ if( $terms ){
+ $this->subPrologue( 'License', $wgRightsUrl );
+ $this->license( $terms );
+ $this->subEpilogue( 'License' );
+ }
+ }
}
- dcPageOrString($name, $wgContLang->getNsText(NS_USER) . ':' . $user_name, wfMsg('siteuser', $user_name));
+
+ $this->epilogue();
}
-}
-/**
- * Takes an arg, for future enhancement with different rights for
- * different pages.
- * @private
- */
-function dcRights() {
-
- global $wgRightsPage, $wgRightsUrl, $wgRightsText;
-
- if (isset($wgRightsPage) &&
- ($nt = Title::newFromText($wgRightsPage))
- && ($nt->getArticleID() != 0)) {
- dcPage('rights', $nt);
- } else if (isset($wgRightsUrl)) {
- dcUrl('rights', $wgRightsUrl);
- } else if (isset($wgRightsText)) {
- dcElement('rights', $wgRightsText);
+ protected function prologue() {
+ global $wgOutputEncoding;
+ echo <<<PROLOGUE
+<?xml version='1.0' encoding="{$wgOutputEncoding}" ?>
+<rdf:RDF xmlns:cc="http://web.resource.org/cc/"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+
+PROLOGUE;
}
-}
-/**
- * @private
- */
-function ccGetTerms($url) {
- global $wgLicenseTerms;
-
- if (isset($wgLicenseTerms)) {
- return $wgLicenseTerms;
- } else {
- $known = getKnownLicenses();
- if( isset( $known[$url] ) ) {
- return $known[$url];
- } else {
- return array();
- }
+ protected function subPrologue( $type, $url ){
+ $url = htmlspecialchars( $url );
+ echo "\t<cc:{$type} rdf:about=\"{$url}\">\n";
}
-}
-/**
- * @private
- */
-function getKnownLicenses() {
-
- $ccLicenses = array('by', 'by-nd', 'by-nd-nc', 'by-nc',
- 'by-nc-sa', 'by-sa');
- $ccVersions = array('1.0', '2.0');
- $knownLicenses = array();
-
- foreach ($ccVersions as $version) {
- foreach ($ccLicenses as $license) {
- if( $version == '2.0' && substr( $license, 0, 2) != 'by' ) {
- # 2.0 dropped the non-attribs licenses
- continue;
- }
- $lurl = "http://creativecommons.org/licenses/{$license}/{$version}/";
- $knownLicenses[$lurl] = explode('-', $license);
- $knownLicenses[$lurl][] = 're';
- $knownLicenses[$lurl][] = 'di';
- $knownLicenses[$lurl][] = 'no';
- if (!in_array('nd', $knownLicenses[$lurl])) {
- $knownLicenses[$lurl][] = 'de';
+ protected function subEpilogue($type) {
+ echo "\t</cc:{$type}>\n";
+ }
+
+ protected function license($terms) {
+
+ foreach( $terms as $term ){
+ switch( $term ) {
+ case 're':
+ $this->term('permits', 'Reproduction'); break;
+ case 'di':
+ $this->term('permits', 'Distribution'); break;
+ case 'de':
+ $this->term('permits', 'DerivativeWorks'); break;
+ case 'nc':
+ $this->term('prohibits', 'CommercialUse'); break;
+ case 'no':
+ $this->term('requires', 'Notice'); break;
+ case 'by':
+ $this->term('requires', 'Attribution'); break;
+ case 'sa':
+ $this->term('requires', 'ShareAlike'); break;
+ case 'sc':
+ $this->term('requires', 'SourceCode'); break;
}
}
}
- /* Handle the GPL and LGPL, too. */
-
- $knownLicenses['http://creativecommons.org/licenses/GPL/2.0/'] =
- array('de', 're', 'di', 'no', 'sa', 'sc');
- $knownLicenses['http://creativecommons.org/licenses/LGPL/2.1/'] =
- array('de', 're', 'di', 'no', 'sa', 'sc');
- $knownLicenses['http://www.gnu.org/copyleft/fdl.html'] =
- array('de', 're', 'di', 'no', 'sa', 'sc');
+ protected function term( $term, $name ){
+ print "\t\t<cc:{$term} rdf:resource=\"http://web.resource.org/cc/{$name}\" />\n";
+ }
- return $knownLicenses;
-}
+ protected function epilogue() {
+ echo "</rdf:RDF>\n";
+ }
+} \ No newline at end of file
diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php
index e33b1c0a..4797752d 100644
--- a/includes/MimeMagic.php
+++ b/includes/MimeMagic.php
@@ -11,6 +11,22 @@
define('MM_WELL_KNOWN_MIME_TYPES',<<<END_STRING
application/ogg ogg ogm ogv
application/pdf pdf
+application/vnd.oasis.opendocument.chart odc
+application/vnd.oasis.opendocument.chart-template otc
+application/vnd.oasis.opendocument.formula odf
+application/vnd.oasis.opendocument.formula-template otf
+application/vnd.oasis.opendocument.graphics odg
+application/vnd.oasis.opendocument.graphics-template otg
+application/vnd.oasis.opendocument.image odi
+application/vnd.oasis.opendocument.image-template oti
+application/vnd.oasis.opendocument.presentation odp
+application/vnd.oasis.opendocument.presentation-template otp
+application/vnd.oasis.opendocument.spreadsheet ods
+application/vnd.oasis.opendocument.spreadsheet-template ots
+application/vnd.oasis.opendocument.text odt
+application/vnd.oasis.opendocument.text-template ott
+application/vnd.oasis.opendocument.text-master otm
+application/vnd.oasis.opendocument.text-web oth
application/x-javascript js
application/x-shockwave-flash swf
audio/midi mid midi kar
@@ -41,6 +57,22 @@ END_STRING
*/
define('MM_WELL_KNOWN_MIME_INFO', <<<END_STRING
application/pdf [OFFICE]
+application/vnd.oasis.opendocument.chart [OFFICE]
+application/vnd.oasis.opendocument.chart-template [OFFICE]
+application/vnd.oasis.opendocument.formula [OFFICE]
+application/vnd.oasis.opendocument.formula-template [OFFICE]
+application/vnd.oasis.opendocument.graphics [OFFICE]
+application/vnd.oasis.opendocument.graphics-template [OFFICE]
+application/vnd.oasis.opendocument.image [OFFICE]
+application/vnd.oasis.opendocument.image-template [OFFICE]
+application/vnd.oasis.opendocument.presentation [OFFICE]
+application/vnd.oasis.opendocument.presentation-template [OFFICE]
+application/vnd.oasis.opendocument.spreadsheet [OFFICE]
+application/vnd.oasis.opendocument.spreadsheet-template [OFFICE]
+application/vnd.oasis.opendocument.text [OFFICE]
+application/vnd.oasis.opendocument.text-template [OFFICE]
+application/vnd.oasis.opendocument.text-master [OFFICE]
+application/vnd.oasis.opendocument.text-web [OFFICE]
text/javascript application/x-javascript [EXECUTABLE]
application/x-shockwave-flash [MULTIMEDIA]
audio/midi [AUDIO]
@@ -406,6 +438,8 @@ class MimeMagic {
wfRestoreWarnings();
if( !$f ) return "unknown/unknown";
$head = fread( $f, 1024 );
+ fseek( $f, -65558, SEEK_END );
+ $tail = fread( $f, 65558 ); // 65558 = maximum size of a zip EOCDR
fclose( $f );
// Hardcode a few magic number checks...
@@ -462,8 +496,8 @@ class MimeMagic {
$xml = new XmlTypeCheck( $file );
if( $xml->wellFormed ) {
global $wgXMLMimeTypes;
- if( isset( $wgXMLMimeTypes[$xml->rootElement] ) ) {
- return $wgXMLMimeTypes[$xml->rootElement];
+ if( isset( $wgXMLMimeTypes[$xml->getRootElement()] ) ) {
+ return $wgXMLMimeTypes[$xml->getRootElement()];
} else {
return 'application/xml';
}
@@ -509,6 +543,12 @@ class MimeMagic {
}
}
+ // Check for ZIP (before getimagesize)
+ if ( strpos( $tail, "PK\x05\x06" ) !== false ) {
+ wfDebug( __METHOD__.": ZIP header present at end of $file\n" );
+ return $this->detectZipType( $head );
+ }
+
wfSuppressWarnings();
$gis = getimagesize( $file );
wfRestoreWarnings();
@@ -517,8 +557,6 @@ class MimeMagic {
$mime = $gis['mime'];
wfDebug( __METHOD__.": getimagesize detected $file as $mime\n" );
return $mime;
- } else {
- return false;
}
// Also test DjVu
@@ -527,6 +565,50 @@ class MimeMagic {
wfDebug( __METHOD__.": detected $file as image/vnd.djvu\n" );
return 'image/vnd.djvu';
}
+
+ return false;
+ }
+
+ /**
+ * Detect application-specific file type of a given ZIP file from its
+ * header data. Currently works for OpenDocument types...
+ * If can't tell, returns 'application/zip'.
+ *
+ * @param string $header Some reasonably-sized chunk of file header
+ * @return string
+ */
+ function detectZipType( $header ) {
+ $opendocTypes = array(
+ 'chart',
+ 'chart-template',
+ 'formula',
+ 'formula-template',
+ 'graphics',
+ 'graphics-template',
+ 'image',
+ 'image-template',
+ 'presentation',
+ 'presentation-template',
+ 'spreadsheet',
+ 'spreadsheet-template',
+ 'text',
+ 'text-template',
+ 'text-master',
+ 'text-web' );
+
+ // http://lists.oasis-open.org/archives/office/200505/msg00006.html
+ $types = '(?:' . implode( '|', $opendocTypes ) . ')';
+ $opendocRegex = "/^mimetype(application\/vnd\.oasis\.opendocument\.$types)/";
+ wfDebug( __METHOD__.": $opendocRegex\n" );
+
+ if( preg_match( $opendocRegex, substr( $header, 30 ), $matches ) ) {
+ $mime = $matches[1];
+ wfDebug( __METHOD__.": detected $mime from ZIP archive\n" );
+ return $mime;
+ } else {
+ wfDebug( __METHOD__.": unable to identify type of ZIP archive\n" );
+ return 'application/zip';
+ }
}
/** Internal mime type detection, please use guessMimeType() for application code instead.
diff --git a/includes/Namespace.php b/includes/Namespace.php
index 7c7b7ded..3d618e64 100644
--- a/includes/Namespace.php
+++ b/includes/Namespace.php
@@ -16,8 +16,8 @@ $wgCanonicalNamespaceNames = array(
NS_USER_TALK => 'User_talk',
NS_PROJECT => 'Project',
NS_PROJECT_TALK => 'Project_talk',
- NS_IMAGE => 'Image',
- NS_IMAGE_TALK => 'Image_talk',
+ NS_FILE => 'File',
+ NS_FILE_TALK => 'File_talk',
NS_MEDIAWIKI => 'MediaWiki',
NS_MEDIAWIKI_TALK => 'MediaWiki_talk',
NS_TEMPLATE => 'Template',
@@ -53,7 +53,7 @@ class MWNamespace {
*/
public static function isMovable( $index ) {
global $wgAllowImageMoving;
- return !( $index < NS_MAIN || ($index == NS_IMAGE && !$wgAllowImageMoving) || $index == NS_CATEGORY );
+ return !( $index < NS_MAIN || ($index == NS_FILE && !$wgAllowImageMoving) || $index == NS_CATEGORY );
}
/**
@@ -105,11 +105,15 @@ class MWNamespace {
* Returns the canonical (English Wikipedia) name for a given index
*
* @param $index Int: namespace index
- * @return string
+ * @return string or false if no canonical definition.
*/
public static function getCanonicalName( $index ) {
global $wgCanonicalNamespaceNames;
- return $wgCanonicalNamespaceNames[$index];
+ if( isset( $wgCanonicalNamespaceNames[$index] ) ) {
+ return $wgCanonicalNamespaceNames[$index];
+ } else {
+ return false;
+ }
}
/**
diff --git a/includes/ObjectCache.php b/includes/ObjectCache.php
index 01b61dfb..6cfb2340 100644
--- a/includes/ObjectCache.php
+++ b/includes/ObjectCache.php
@@ -32,7 +32,10 @@ class FakeMemCachedClient {
global $wgCaches;
$wgCaches = array();
-/** @todo document */
+/**
+ * Get a cache object.
+ * @param int $inputType cache type, one the the CACHE_* constants.
+ */
function &wfGetCache( $inputType ) {
global $wgCaches, $wgMemCachedServers, $wgMemCachedDebug, $wgMemCachedPersistent;
$cache = false;
@@ -48,23 +51,20 @@ function &wfGetCache( $inputType ) {
}
if ( $type == CACHE_MEMCACHED ) {
- if ( !array_key_exists( CACHE_MEMCACHED, $wgCaches ) ){
- require_once( 'memcached-client.php' );
-
- if (!class_exists("MemcachedClientforWiki")) {
+ if ( !array_key_exists( CACHE_MEMCACHED, $wgCaches ) ) {
+ if ( !class_exists( 'MemcachedClientforWiki' ) ) {
class MemCachedClientforWiki extends memcached {
function _debugprint( $text ) {
wfDebug( "memcached: $text" );
}
}
}
-
- $wgCaches[CACHE_DB] = new MemCachedClientforWiki(
+ $wgCaches[CACHE_MEMCACHED] = new MemCachedClientforWiki(
array('persistant' => $wgMemCachedPersistent, 'compress_threshold' => 1500 ) );
- $cache =& $wgCaches[CACHE_DB];
- $cache->set_servers( $wgMemCachedServers );
- $cache->set_debug( $wgMemCachedDebug );
+ $wgCaches[CACHE_MEMCACHED]->set_servers( $wgMemCachedServers );
+ $wgCaches[CACHE_MEMCACHED]->set_debug( $wgMemCachedDebug );
}
+ $cache =& $wgCaches[CACHE_MEMCACHED];
} elseif ( $type == CACHE_ACCEL ) {
if ( !array_key_exists( CACHE_ACCEL, $wgCaches ) ) {
if ( function_exists( 'eaccelerator_get' ) ) {
@@ -106,18 +106,21 @@ function &wfGetCache( $inputType ) {
return $cache;
}
+/** Get the main cache object */
function &wfGetMainCache() {
global $wgMainCacheType;
$ret =& wfGetCache( $wgMainCacheType );
return $ret;
}
+/** Get the cache object used by the message cache */
function &wfGetMessageCacheStorage() {
global $wgMessageCacheType;
$ret =& wfGetCache( $wgMessageCacheType );
return $ret;
}
+/** Get the cache object used by the parser cache */
function &wfGetParserCacheStorage() {
global $wgParserCacheType;
$ret =& wfGetCache( $wgParserCacheType );
diff --git a/includes/OutputPage.php b/includes/OutputPage.php
index 8226cb2f..f8dba714 100644
--- a/includes/OutputPage.php
+++ b/includes/OutputPage.php
@@ -6,20 +6,23 @@ if ( ! defined( 'MEDIAWIKI' ) )
* @todo document
*/
class OutputPage {
- var $mMetatags, $mKeywords;
- var $mLinktags, $mPagetitle, $mBodytext, $mDebugtext;
- var $mHTMLtitle, $mRobotpolicy, $mIsarticle, $mPrintable;
- var $mSubtitle, $mRedirect, $mStatusCode;
- var $mLastModified, $mETag, $mCategoryLinks;
- var $mScripts, $mLinkColours, $mPageLinkTitle;
+ var $mMetatags = array(), $mKeywords = array(), $mLinktags = array();
+ var $mExtStyles = array();
+ var $mPagetitle = '', $mBodytext = '', $mDebugtext = '';
+ var $mHTMLtitle = '', $mIsarticle = true, $mPrintable = false;
+ var $mSubtitle = '', $mRedirect = '', $mStatusCode;
+ var $mLastModified = '', $mETag = false;
+ var $mCategoryLinks = array(), $mLanguageLinks = array();
+ var $mScripts = '', $mLinkColours, $mPageLinkTitle = '', $mHeadItems = array();
+ var $mTemplateIds = array();
var $mAllowUserJs;
- var $mSuppressQuickbar;
- var $mOnloadHandler;
- var $mDoNothing;
- var $mContainsOldMagic, $mContainsNewMagic;
- var $mIsArticleRelated;
- protected $mParserOptions; // lazy initialised, use parserOptions()
+ var $mSuppressQuickbar = false;
+ var $mOnloadHandler = '';
+ var $mDoNothing = false;
+ var $mContainsOldMagic = 0, $mContainsNewMagic = 0;
+ var $mIsArticleRelated = true;
+ protected $mParserOptions = null; // lazy initialised, use parserOptions()
var $mShowFeedLinks = false;
var $mFeedLinksAppendQuery = false;
var $mEnableClientCache = true;
@@ -29,6 +32,18 @@ class OutputPage {
var $mNoGallery = false;
var $mPageTitleActionText = '';
var $mParseWarnings = array();
+ var $mSquidMaxage = 0;
+ var $mRevisionId = null;
+
+ /**
+ * An array of stylesheet filenames (relative from skins path), with options
+ * for CSS media, IE conditions, and RTL/LTR direction.
+ * For internal use; add settings in the skin via $this->addStyle()
+ */
+ var $styles = array();
+
+ private $mIndexPolicy = 'index';
+ private $mFollowPolicy = 'follow';
/**
* Constructor
@@ -37,25 +52,6 @@ class OutputPage {
function __construct() {
global $wgAllowUserJs;
$this->mAllowUserJs = $wgAllowUserJs;
- $this->mMetatags = $this->mKeywords = $this->mLinktags = array();
- $this->mHTMLtitle = $this->mPagetitle = $this->mBodytext =
- $this->mRedirect = $this->mLastModified =
- $this->mSubtitle = $this->mDebugtext = $this->mRobotpolicy =
- $this->mOnloadHandler = $this->mPageLinkTitle = '';
- $this->mIsArticleRelated = $this->mIsarticle = $this->mPrintable = true;
- $this->mSuppressQuickbar = $this->mPrintable = false;
- $this->mLanguageLinks = array();
- $this->mCategoryLinks = array();
- $this->mDoNothing = false;
- $this->mContainsOldMagic = $this->mContainsNewMagic = 0;
- $this->mParserOptions = null;
- $this->mSquidMaxage = 0;
- $this->mScripts = '';
- $this->mHeadItems = array();
- $this->mETag = false;
- $this->mRevisionId = null;
- $this->mNewSectionLink = false;
- $this->mTemplateIds = array();
}
public function redirect( $url, $responsecode = '302' ) {
@@ -76,17 +72,23 @@ class OutputPage {
*/
function setStatusCode( $statusCode ) { $this->mStatusCode = $statusCode; }
- # To add an http-equiv meta tag, precede the name with "http:"
- function addMeta( $name, $val ) { array_push( $this->mMetatags, array( $name, $val ) ); }
+ /**
+ * Add a new <meta> tag
+ * To add an http-equiv meta tag, precede the name with "http:"
+ *
+ * @param $name tag name
+ * @param $val tag value
+ */
+ 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,
- 'type' => 'text/css' ) );
+
+ function addExtensionStyle( $url ) {
+ $linkarr = array( 'rel' => 'stylesheet', 'href' => $url, 'type' => 'text/css' );
+ array_push( $this->mExtStyles, $linkarr );
}
/**
@@ -100,7 +102,6 @@ class OutputPage {
} else {
$path = "{$wgStylePath}/common/{$file}";
}
- $encPath = htmlspecialchars( $path );
$this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"$path?$wgStyleVersion\"></script>\n" );
}
@@ -141,6 +142,11 @@ class OutputPage {
# $linkarr should be an associative array of attributes. We'll escape on output.
array_push( $this->mLinktags, $linkarr );
}
+
+ # Get all links added by extensions
+ function getExtStyle() {
+ return $this->mExtStyles;
+ }
function addMetadataLink( $linkarr ) {
# note: buggy CC software only reads first "meta" link
@@ -155,62 +161,87 @@ class OutputPage {
* possible. If sucessful, the OutputPage is disabled so that
* any future call to OutputPage->output() have no effect.
*
+ * Side effect: sets mLastModified for Last-Modified header
+ *
* @return bool True iff cache-ok headers was sent.
*/
function checkLastModified ( $timestamp ) {
global $wgCachePages, $wgCacheEpoch, $wgUser, $wgRequest;
-
+
if ( !$timestamp || $timestamp == '19700101000000' ) {
wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" );
- return;
+ return false;
}
if( !$wgCachePages ) {
wfDebug( __METHOD__ . ": CACHE DISABLED\n", false );
- return;
+ return false;
}
if( $wgUser->getOption( 'nocache' ) ) {
wfDebug( __METHOD__ . ": USER DISABLED CACHE\n", false );
- return;
+ return false;
}
- $timestamp=wfTimestamp(TS_MW,$timestamp);
- $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->mTouched, $wgCacheEpoch ) );
+ $timestamp = wfTimestamp( TS_MW, $timestamp );
+ $modifiedTimes = array(
+ 'page' => $timestamp,
+ 'user' => $wgUser->getTouched(),
+ 'epoch' => $wgCacheEpoch
+ );
+ wfRunHooks( 'OutputPageCheckLastModified', array( &$modifiedTimes ) );
- if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
- # IE sends sizes after the date like this:
- # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
- # this breaks strtotime().
- $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] );
+ $maxModified = max( $modifiedTimes );
+ $this->mLastModified = wfTimestamp( TS_RFC2822, $maxModified );
- wfSuppressWarnings(); // E_STRICT system time bitching
- $modsinceTime = strtotime( $modsince );
- wfRestoreWarnings();
+ if( empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
+ wfDebug( __METHOD__ . ": client did not send If-Modified-Since header\n", false );
+ return false;
+ }
- $ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 );
- wfDebug( __METHOD__ . ": -- client send If-Modified-Since: " . $modsince . "\n", false );
- wfDebug( __METHOD__ . ": -- we might send Last-Modified : $lastmod\n", false );
- if( ($ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) {
- # Make sure you're in a place you can leave when you call us!
- $wgRequest->response()->header( "HTTP/1.0 304 Not Modified" );
- $this->mLastModified = $lastmod;
- $this->sendCacheControl();
- wfDebug( __METHOD__ . ": CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false );
- $this->disable();
+ # Make debug info
+ $info = '';
+ foreach ( $modifiedTimes as $name => $value ) {
+ if ( $info !== '' ) {
+ $info .= ', ';
+ }
+ $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value );
+ }
- // Don't output a compressed blob when using ob_gzhandler;
- // it's technically against HTTP spec and seems to confuse
- // Firefox when the response gets split over two packets.
- wfClearOutputBuffers();
+ # IE sends sizes after the date like this:
+ # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
+ # this breaks strtotime().
+ $clientHeader = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] );
- return true;
- } else {
- wfDebug( __METHOD__ . ": READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false );
- $this->mLastModified = $lastmod;
- }
- } else {
- wfDebug( __METHOD__ . ": client did not send If-Modified-Since header\n", false );
- $this->mLastModified = $lastmod;
+ wfSuppressWarnings(); // E_STRICT system time bitching
+ $clientHeaderTime = strtotime( $clientHeader );
+ wfRestoreWarnings();
+ if ( !$clientHeaderTime ) {
+ wfDebug( __METHOD__ . ": unable to parse the client's If-Modified-Since header: $clientHeader\n" );
+ return false;
}
+ $clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime );
+
+ wfDebug( __METHOD__ . ": client sent If-Modified-Since: " .
+ wfTimestamp( TS_ISO_8601, $clientHeaderTime ) . "\n", false );
+ wfDebug( __METHOD__ . ": effective Last-Modified: " .
+ wfTimestamp( TS_ISO_8601, $maxModified ) . "\n", false );
+ if( $clientHeaderTime < $maxModified ) {
+ wfDebug( __METHOD__ . ": STALE, $info\n", false );
+ return false;
+ }
+
+ # Not modified
+ # Give a 304 response code and disable body output
+ wfDebug( __METHOD__ . ": NOT MODIFIED, $info\n", false );
+ $wgRequest->response()->header( "HTTP/1.1 304 Not Modified" );
+ $this->sendCacheControl();
+ $this->disable();
+
+ // Don't output a compressed blob when using ob_gzhandler;
+ // it's technically against HTTP spec and seems to confuse
+ // Firefox when the response gets split over two packets.
+ wfClearOutputBuffers();
+
+ return true;
}
function setPageTitleActionText( $text ) {
@@ -223,7 +254,61 @@ class OutputPage {
}
}
- public function setRobotpolicy( $str ) { $this->mRobotpolicy = $str; }
+ /**
+ * Set the robot policy for the page: <http://www.robotstxt.org/meta.html>
+ *
+ * @param $policy string The literal string to output as the contents of
+ * the meta tag. Will be parsed according to the spec and output in
+ * standardized form.
+ * @return null
+ */
+ public function setRobotPolicy( $policy ) {
+ $policy = explode( ',', $policy );
+ $policy = array_map( 'trim', $policy );
+
+ # The default policy is follow, so if nothing is said explicitly, we
+ # do that.
+ if( in_array( 'nofollow', $policy ) ) {
+ $this->mFollowPolicy = 'nofollow';
+ } else {
+ $this->mFollowPolicy = 'follow';
+ }
+
+ if( in_array( 'noindex', $policy ) ) {
+ $this->mIndexPolicy = 'noindex';
+ } else {
+ $this->mIndexPolicy = 'index';
+ }
+ }
+
+ /**
+ * Set the index policy for the page, but leave the follow policy un-
+ * touched.
+ *
+ * @param $policy string Either 'index' or 'noindex'.
+ * @return null
+ */
+ public function setIndexPolicy( $policy ) {
+ $policy = trim( $policy );
+ if( in_array( $policy, array( 'index', 'noindex' ) ) ) {
+ $this->mIndexPolicy = $policy;
+ }
+ }
+
+ /**
+ * Set the follow policy for the page, but leave the index policy un-
+ * touched.
+ *
+ * @param $policy string Either 'follow' or 'nofollow'.
+ * @return null
+ */
+ public function setFollowPolicy( $policy ) {
+ $policy = trim( $policy );
+ if( in_array( $policy, array( 'follow', 'nofollow' ) ) ) {
+ $this->mFollowPolicy = $policy;
+ }
+ }
+
public function setHTMLTitle( $name ) {$this->mHTMLtitle = $name; }
public function setPageTitle( $name ) {
global $action, $wgContLang;
@@ -341,6 +426,7 @@ class OutputPage {
public function disallowUserJs() { $this->mAllowUserJs = false; }
public function isUserJsAllowed() { return $this->mAllowUserJs; }
+ public function prependHTML( $text ) { $this->mBodytext = $text . $this->mBodytext; }
public function addHTML( $text ) { $this->mBodytext .= $text; }
public function clearHTML() { $this->mBodytext = ''; }
public function getHTML() { return $this->mBodytext; }
@@ -369,6 +455,10 @@ class OutputPage {
$val = is_null( $revid ) ? null : intval( $revid );
return wfSetVar( $this->mRevisionId, $val );
}
+
+ public function getRevisionId() {
+ return $this->mRevisionId;
+ }
/**
* Convert wikitext to HTML and add it to the buffer
@@ -416,9 +506,23 @@ class OutputPage {
* @param ParserOutput object &$parserOutput
*/
public function addParserOutputNoText( &$parserOutput ) {
+ global $wgTitle, $wgExemptFromUserRobotsControl, $wgContentNamespaces;
+
$this->mLanguageLinks += $parserOutput->getLanguageLinks();
$this->addCategoryLinks( $parserOutput->getCategories() );
$this->mNewSectionLink = $parserOutput->getNewSection();
+
+ if( is_null( $wgExemptFromUserRobotsControl ) ) {
+ $bannedNamespaces = $wgContentNamespaces;
+ } else {
+ $bannedNamespaces = $wgExemptFromUserRobotsControl;
+ }
+ if( !in_array( $wgTitle->getNamespace(), $bannedNamespaces ) ) {
+ # FIXME (bug 14900): This overrides $wgArticleRobotPolicies, and it
+ # shouldn't
+ $this->setIndexPolicy( $parserOutput->getIndexPolicy() );
+ }
+
$this->addKeywords( $parserOutput );
$this->mParseWarnings = $parserOutput->getWarnings();
if ( $parserOutput->getCacheTime() == -1 ) {
@@ -427,8 +531,13 @@ class OutputPage {
$this->mNoGallery = $parserOutput->getNoGallery();
$this->mHeadItems = array_merge( $this->mHeadItems, (array)$parserOutput->mHeadItems );
// Versioning...
- $this->mTemplateIds = wfArrayMerge( $this->mTemplateIds, (array)$parserOutput->mTemplateIds );
-
+ foreach ( (array)$parserOutput->mTemplateIds as $ns => $dbks ) {
+ if ( isset( $this->mTemplateIds[$ns] ) ) {
+ $this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns];
+ } else {
+ $this->mTemplateIds[$ns] = $dbks;
+ }
+ }
// Display title
if( ( $dt = $parserOutput->getDisplayTitle() ) !== false )
$this->setPageTitle( $dt );
@@ -522,6 +631,9 @@ class OutputPage {
*/
public function parse( $text, $linestart = true, $interface = false ) {
global $wgParser, $wgTitle;
+ if( is_null( $wgTitle ) ) {
+ throw new MWException( 'Empty $wgTitle in ' . __METHOD__ );
+ }
$popts = $this->parserOptions();
if ( $interface) { $popts->setInterfaceMessage(true); }
$parserOutput = $wgParser->parse( $text, $wgTitle, $popts,
@@ -590,7 +702,7 @@ class OutputPage {
* If it does, it's very important that we don't allow public caching
*/
function haveCacheVaryCookies() {
- global $wgRequest, $wgCookiePrefix;
+ global $wgRequest;
$cookieHeader = $wgRequest->getHeader( 'cookie' );
if ( $cookieHeader === false ) {
return false;
@@ -609,7 +721,6 @@ class OutputPage {
/** Get a complete X-Vary-Options header */
public function getXVO() {
- global $wgCookiePrefix;
$cvCookies = $this->getCacheVaryCookies();
$xvo = 'X-Vary-Options: Accept-Encoding;list-contains=gzip,Cookie;';
$first = true;
@@ -668,7 +779,9 @@ class OutputPage {
$response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
$response->header( "Cache-Control: private, must-revalidate, max-age=0" );
}
- if($this->mLastModified) $response->header( "Last-modified: {$this->mLastModified}" );
+ if($this->mLastModified) {
+ $response->header( "Last-Modified: {$this->mLastModified}" );
+ }
} else {
wfDebug( __METHOD__ . ": no caching **\n", false );
@@ -687,8 +800,9 @@ class OutputPage {
public function output() {
global $wgUser, $wgOutputEncoding, $wgRequest;
global $wgContLanguageCode, $wgDebugRedirects, $wgMimeType;
- global $wgJsMimeType, $wgUseAjax, $wgAjaxSearch, $wgAjaxWatch;
- global $wgServer, $wgEnableMWSuggest;
+ global $wgJsMimeType, $wgUseAjax, $wgAjaxWatch;
+ global $wgEnableMWSuggest, $wgUniversalEditButton;
+ global $wgArticle, $wgTitle;
if( $this->mDoNothing ){
return;
@@ -782,11 +896,6 @@ class OutputPage {
wfRunHooks( 'AjaxAddScript', array( &$this ) );
- if( $wgAjaxSearch && $wgUser->getBoolOption( 'ajaxsearch' ) ) {
- $this->addScriptFile( 'ajaxsearch.js' );
- $this->addScript( "<script type=\"{$wgJsMimeType}\">hookEvent(\"load\", sajax_onload);</script>\n" );
- }
-
if( $wgAjaxWatch && $wgUser->isLoggedIn() ) {
$this->addScriptFile( 'ajaxwatch.js' );
}
@@ -800,13 +909,28 @@ class OutputPage {
$this->addScriptFile( 'rightclickedit.js' );
}
-
+ if( $wgUniversalEditButton ) {
+ if( isset( $wgArticle ) && isset( $wgTitle ) && $wgTitle->quickUserCan( 'edit' )
+ && ( $wgTitle->exists() || $wgTitle->quickUserCan( 'create' ) ) ) {
+ // Original UniversalEditButton
+ $this->addLink( array(
+ 'rel' => 'alternate',
+ 'type' => 'application/x-wiki',
+ 'title' => wfMsg( 'edit' ),
+ 'href' => $wgTitle->getFullURL( 'action=edit' )
+ ) );
+ // Alternate edit link
+ $this->addLink( array(
+ 'rel' => 'edit',
+ 'title' => wfMsg( 'edit' ),
+ 'href' => $wgTitle->getFullURL( 'action=edit' )
+ ) );
+ }
+ }
+
# Buffer output; final headers may depend on later processing
ob_start();
- # Disable temporary placeholders, so that the skin produces HTML
- $sk->postParseLinkColour( false );
-
$wgRequest->response()->header( "Content-type: $wgMimeType; charset={$wgOutputEncoding}" );
$wgRequest->response()->header( 'Content-language: '.$wgContLanguageCode );
@@ -879,7 +1003,7 @@ class OutputPage {
global $wgUser, $wgContLang, $wgTitle, $wgLang;
$this->setPageTitle( wfMsg( 'blockedtitle' ) );
- $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setRobotPolicy( 'noindex,nofollow' );
$this->setArticleRelated( false );
$name = User::whoIs( $wgUser->blockedBy() );
@@ -945,7 +1069,7 @@ class OutputPage {
}
$this->setPageTitle( wfMsg( $title ) );
$this->setHTMLTitle( wfMsg( 'errorpagetitle' ) );
- $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setRobotPolicy( 'noindex,nofollow' );
$this->setArticleRelated( false );
$this->enableClientCache( false );
$this->mRedirect = '';
@@ -953,7 +1077,7 @@ class OutputPage {
array_unshift( $params, 'parse' );
array_unshift( $params, $msg );
- $this->addHtml( call_user_func_array( 'wfMsgExt', $params ) );
+ $this->addHTML( call_user_func_array( 'wfMsgExt', $params ) );
$this->returnToMain();
}
@@ -971,7 +1095,7 @@ class OutputPage {
$wgTitle->getPrefixedText() . "\n";
$this->setPageTitle( wfMsg( 'permissionserrors' ) );
$this->setHTMLTitle( wfMsg( 'permissionserrors' ) );
- $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setRobotPolicy( 'noindex,nofollow' );
$this->setArticleRelated( false );
$this->enableClientCache( false );
$this->mRedirect = '';
@@ -994,7 +1118,7 @@ class OutputPage {
public function versionRequired( $version ) {
$this->setPageTitle( wfMsg( 'versionrequired', $version ) );
$this->setHTMLTitle( wfMsg( 'versionrequired', $version ) );
- $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setRobotPolicy( 'noindex,nofollow' );
$this->setArticleRelated( false );
$this->mBodytext = '';
@@ -1008,39 +1132,23 @@ class OutputPage {
* @param string $permission key required
*/
public function permissionRequired( $permission ) {
- global $wgGroupPermissions, $wgUser;
+ global $wgUser;
$this->setPageTitle( wfMsg( 'badaccess' ) );
$this->setHTMLTitle( wfMsg( 'errorpagetitle' ) );
- $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setRobotPolicy( 'noindex,nofollow' );
$this->setArticleRelated( false );
$this->mBodytext = '';
- $groups = array();
- foreach( $wgGroupPermissions as $key => $value ) {
- if( isset( $value[$permission] ) && $value[$permission] == true ) {
- $groupName = User::getGroupName( $key );
- $groupPage = User::getGroupPage( $key );
- if( $groupPage ) {
- $skin = $wgUser->getSkin();
- $groups[] = $skin->makeLinkObj( $groupPage, $groupName );
- } else {
- $groups[] = $groupName;
- }
- }
+ $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
+ User::getGroupsWithPermission( $permission ) );
+ if( $groups ) {
+ $this->addWikiMsg( 'badaccess-groups',
+ implode( ', ', $groups ),
+ count( $groups) );
+ } else {
+ $this->addWikiMsg( 'badaccess-group0' );
}
- $n = count( $groups );
- $groups = implode( ', ', $groups );
- switch( $n ) {
- case 0:
- case 1:
- case 2:
- $message = wfMsgHtml( "badaccess-group$n", $groups );
- break;
- default:
- $message = wfMsgHtml( 'badaccess-groups', $groups );
- }
- $this->addHtml( $message );
$this->returnToMain();
}
@@ -1080,8 +1188,8 @@ class OutputPage {
$loginTitle = SpecialPage::getTitleFor( 'Userlogin' );
$loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $wgTitle->getPrefixedUrl() );
- $this->addHtml( wfMsgWikiHtml( 'loginreqpagetext', $loginLink ) );
- $this->addHtml( "\n<!--" . $wgTitle->getPrefixedUrl() . "-->" );
+ $this->addHTML( wfMsgWikiHtml( 'loginreqpagetext', $loginLink ) );
+ $this->addHTML( "\n<!--" . $wgTitle->getPrefixedUrl() . "-->" );
# Don't return to the main page if the user can't read it
# otherwise we'll end up in a pointless loop
@@ -1103,8 +1211,8 @@ class OutputPage {
if ($action == null) {
$text = wfMsgNoTrans( 'permissionserrorstext', count($errors)). "\n\n";
} else {
- $action_desc = wfMsg( "right-$action" );
- $action_desc[0] = strtolower($action_desc[0]);
+ global $wgLang;
+ $action_desc = wfMsg( "action-$action" );
$text = wfMsgNoTrans( 'permissionserrorstext-withaction', count($errors), $action_desc ) . "\n\n";
}
@@ -1148,7 +1256,7 @@ class OutputPage {
global $wgUser, $wgTitle;
$skin = $wgUser->getSkin();
- $this->setRobotpolicy( 'noindex,nofollow' );
+ $this->setRobotPolicy( 'noindex,nofollow' );
$this->setArticleRelated( false );
// If no reason is given, just supply a default "I can't let you do
@@ -1170,7 +1278,7 @@ class OutputPage {
// Wiki is read only
$this->setPageTitle( wfMsg( 'readonly' ) );
$reason = wfReadOnlyReason();
- $this->addWikiMsg( 'readonlytext', $reason );
+ $this->wrapWikiMsg( '<div class="mw-readonly-error">$1</div>', array( 'readonlytext', $reason ) );
}
// Show source, if supplied
@@ -1189,7 +1297,10 @@ class OutputPage {
// Show templates used by this article
$skin = $wgUser->getSkin();
$article = new Article( $wgTitle );
- $this->addHTML( $skin->formatTemplates( $article->getUsedTemplates() ) );
+ $this->addHTML( "<div class='templatesUsed'>
+{$skin->formatTemplates( $article->getUsedTemplates() )}
+</div>
+" );
}
# If the title doesn't exist, it's fairly pointless to print a return
@@ -1238,7 +1349,7 @@ class OutputPage {
public function showFatalError( $message ) {
$this->setPageTitle( wfMsg( "internalerror" ) );
- $this->setRobotpolicy( "noindex,nofollow" );
+ $this->setRobotPolicy( "noindex,nofollow" );
$this->setArticleRelated( false );
$this->enableClientCache( false );
$this->mRedirect = '';
@@ -1272,8 +1383,9 @@ class OutputPage {
*/
public function addReturnTo( $title ) {
global $wgUser;
+ $this->addLink( array( 'rel' => 'next', 'href' => $title->getFullUrl() ) );
$link = wfMsg( 'returnto', $wgUser->getSkin()->makeLinkObj( $title ) );
- $this->addHtml( "<p>{$link}</p>\n" );
+ $this->addHTML( "<p>{$link}</p>\n" );
}
/**
@@ -1333,15 +1445,19 @@ class OutputPage {
/**
* @return string The doctype, opening <html>, and head element.
*/
- public function headElement() {
+ public function headElement( Skin $sk ) {
global $wgDocType, $wgDTD, $wgContLanguageCode, $wgOutputEncoding, $wgMimeType;
global $wgXhtmlDefaultNamespace, $wgXhtmlNamespaces;
global $wgUser, $wgContLang, $wgUseTrackbacks, $wgTitle, $wgStyleVersion;
+ $this->addMeta( "http:Content-type", "$wgMimeType; charset={$wgOutputEncoding}" );
+ $this->addStyle( 'common/wikiprintable.css', 'print' );
+ $sk->setupUserCss( $this );
+
+ $ret = '';
+
if( $wgMimeType == 'text/xml' || $wgMimeType == 'application/xhtml+xml' || $wgMimeType == 'application/xml' ) {
- $ret = "<?xml version=\"1.0\" encoding=\"$wgOutputEncoding\" ?>\n";
- } else {
- $ret = '';
+ $ret .= "<?xml version=\"1.0\" encoding=\"$wgOutputEncoding\" ?>\n";
}
$ret .= "<!DOCTYPE html PUBLIC \"$wgDocType\"\n \"$wgDTD\">\n";
@@ -1356,24 +1472,17 @@ class OutputPage {
$ret .= "xmlns:{$tag}=\"{$ns}\" ";
}
$ret .= "xml:lang=\"$wgContLanguageCode\" lang=\"$wgContLanguageCode\" $rtl>\n";
- $ret .= "<head>\n<title>" . htmlspecialchars( $this->getHTMLTitle() ) . "</title>\n";
- $this->addMeta( "http:Content-type", "$wgMimeType; charset={$wgOutputEncoding}" );
-
- $ret .= $this->getHeadLinks();
- global $wgStylePath;
- if( $this->isPrintable() ) {
- $media = '';
- } else {
- $media = "media='print'";
+ $ret .= "<head>\n<title>" . htmlspecialchars( $this->getHTMLTitle() ) . "</title>\n\t\t";
+ $ret .= implode( "\t\t", array(
+ $this->getHeadLinks(),
+ $this->buildCssLinks(),
+ $sk->getHeadScripts( $this->mAllowUserJs ),
+ $this->mScripts,
+ $this->getHeadItems(),
+ ));
+ if( $sk->usercss ){
+ $ret .= "<style type='text/css'>{$sk->usercss}</style>";
}
- $printsheet = htmlspecialchars( "$wgStylePath/common/wikiprintable.css?$wgStyleVersion" );
- $ret .= "<link rel='stylesheet' type='text/css' $media href='$printsheet' />\n";
-
- $sk = $wgUser->getSkin();
- $ret .= $sk->getHeadScripts( $this->mAllowUserJs );
- $ret .= $this->mScripts;
- $ret .= $sk->getUserStyles();
- $ret .= $this->getHeadItems();
if ($wgUseTrackbacks && $this->isArticleRelated())
$ret .= $wgTitle->trackbackRDF();
@@ -1384,10 +1493,11 @@ class OutputPage {
protected function addDefaultMeta() {
global $wgVersion;
- $this->addMeta( "generator", "MediaWiki $wgVersion" );
+ $this->addMeta( 'http:Content-Style-Type', 'text/css' ); //bug 15835
+ $this->addMeta( 'generator', "MediaWiki $wgVersion" );
- $p = $this->mRobotpolicy;
- if( $p !== '' && $p != 'index,follow' ) {
+ $p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
+ if( $p !== 'index,follow' ) {
// http://www.robotstxt.org/wc/meta-user.html
// Only show if it's different from the default robots policy
$this->addMeta( 'robots', $p );
@@ -1446,20 +1556,29 @@ class OutputPage {
# Recent changes feed should appear on every page (except recentchanges,
# that would be redundant). Put it after the per-page feed to avoid
# changing existing behavior. It's still available, probably via a
- # menu in your browser.
-
+ # menu in your browser. Some sites might have a different feed they'd
+ # like to promote instead of the RC feed (maybe like a "Recent New Articles"
+ # or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined.
+ # If so, use it instead.
+
+ global $wgOverrideSiteFeed, $wgSitename, $wgFeedClasses;
$rctitle = SpecialPage::getTitleFor( 'Recentchanges' );
- if ( $wgTitle->getPrefixedText() != $rctitle->getPrefixedText() ) {
- global $wgSitename;
-
- $tags[] = $this->feedLink(
- 'rss',
- $rctitle->getFullURL( 'feed=rss' ),
- wfMsg( 'site-rss-feed', $wgSitename ) );
- $tags[] = $this->feedLink(
- 'atom',
- $rctitle->getFullURL( 'feed=atom' ),
- wfMsg( 'site-atom-feed', $wgSitename ) );
+
+ if ( $wgOverrideSiteFeed ) {
+ foreach ( $wgOverrideSiteFeed as $type => $feedUrl ) {
+ $tags[] = $this->feedLink (
+ $type,
+ htmlspecialchars( $feedUrl ),
+ wfMsg( "site-{$type}-feed", $wgSitename ) );
+ }
+ }
+ else if ( $wgTitle->getPrefixedText() != $rctitle->getPrefixedText() ) {
+ foreach( $wgFeedClasses as $format => $class ) {
+ $tags[] = $this->feedLink(
+ $format,
+ $rctitle->getFullURL( "feed={$format}" ),
+ wfMsg( "site-{$format}-feed", $wgSitename ) ); # For grep: 'site-rss-feed', 'site-atom-feed'.
+ }
}
}
@@ -1500,6 +1619,118 @@ class OutputPage {
}
/**
+ * Add a local or specified stylesheet, with the given media options.
+ * Meant primarily for internal use...
+ *
+ * @param $media -- to specify a media type, 'screen', 'printable', 'handheld' or any.
+ * @param $conditional -- for IE conditional comments, specifying an IE version
+ * @param $dir -- set to 'rtl' or 'ltr' for direction-specific sheets
+ */
+ public function addStyle( $style, $media='', $condition='', $dir='' ) {
+ $options = array();
+ if( $media )
+ $options['media'] = $media;
+ if( $condition )
+ $options['condition'] = $condition;
+ if( $dir )
+ $options['dir'] = $dir;
+ $this->styles[$style] = $options;
+ }
+
+ /**
+ * Build a set of <link>s for the stylesheets specified in the $this->styles array.
+ * These will be applied to various media & IE conditionals.
+ */
+ public function buildCssLinks() {
+ $links = array();
+ foreach( $this->styles as $file => $options ) {
+ $link = $this->styleLink( $file, $options );
+ if( $link )
+ $links[] = $link;
+ }
+
+ return implode( "\n\t\t", $links );
+ }
+
+ protected function styleLink( $style, $options ) {
+ global $wgRequest;
+
+ if( isset( $options['dir'] ) ) {
+ global $wgContLang;
+ $siteDir = $wgContLang->isRTL() ? 'rtl' : 'ltr';
+ if( $siteDir != $options['dir'] )
+ return '';
+ }
+
+ if( isset( $options['media'] ) ) {
+ $media = $this->transformCssMedia( $options['media'] );
+ if( is_null( $media ) ) {
+ return '';
+ }
+ } else {
+ $media = '';
+ }
+
+ if( substr( $style, 0, 1 ) == '/' ||
+ substr( $style, 0, 5 ) == 'http:' ||
+ substr( $style, 0, 6 ) == 'https:' ) {
+ $url = $style;
+ } else {
+ global $wgStylePath, $wgStyleVersion;
+ $url = $wgStylePath . '/' . $style . '?' . $wgStyleVersion;
+ }
+
+ $attribs = array(
+ 'rel' => 'stylesheet',
+ 'href' => $url,
+ 'type' => 'text/css' );
+ if( $media ) {
+ $attribs['media'] = $media;
+ }
+
+ $link = Xml::element( 'link', $attribs );
+
+ if( isset( $options['condition'] ) ) {
+ $condition = htmlspecialchars( $options['condition'] );
+ $link = "<!--[if $condition]>$link<![endif]-->";
+ }
+ return $link;
+ }
+
+ function transformCssMedia( $media ) {
+ global $wgRequest, $wgHandheldForIPhone;
+
+ // Switch in on-screen display for media testing
+ $switches = array(
+ 'printable' => 'print',
+ 'handheld' => 'handheld',
+ );
+ foreach( $switches as $switch => $targetMedia ) {
+ if( $wgRequest->getBool( $switch ) ) {
+ if( $media == $targetMedia ) {
+ $media = '';
+ } elseif( $media == 'screen' ) {
+ return null;
+ }
+ }
+ }
+
+ // Expand longer media queries as iPhone doesn't grok 'handheld'
+ if( $wgHandheldForIPhone ) {
+ $mediaAliases = array(
+ 'screen' => 'screen and (min-device-width: 481px)',
+ 'handheld' => 'handheld, only screen and (max-device-width: 480px)',
+ );
+
+ if( isset( $mediaAliases[$media] ) ) {
+ $media = $mediaAliases[$media];
+ }
+ }
+
+ return $media;
+ }
+
+ /**
* Turn off regular page output and return an error reponse
* for when rate limiting has triggered.
*/
@@ -1543,7 +1774,7 @@ class OutputPage {
? 'lag-warn-normal'
: 'lag-warn-high';
$warning = wfMsgExt( $message, 'parse', $lag );
- $this->addHtml( "<div class=\"mw-{$message}\">\n{$warning}\n</div>\n" );
+ $this->addHTML( "<div class=\"mw-{$message}\">\n{$warning}\n</div>\n" );
}
}
diff --git a/includes/PageHistory.php b/includes/PageHistory.php
index 870b57b7..b01b485e 100644
--- a/includes/PageHistory.php
+++ b/includes/PageHistory.php
@@ -22,7 +22,6 @@ class PageHistory {
var $mArticle, $mTitle, $mSkin;
var $lastdate;
var $linesonpage;
- var $mNotificationTimestamp;
var $mLatestId = null;
/**
@@ -31,12 +30,10 @@ class PageHistory {
* @param Article $article
* @returns nothing
*/
- function __construct($article) {
+ function __construct( $article ) {
global $wgUser;
-
$this->mArticle =& $article;
$this->mTitle =& $article->mTitle;
- $this->mNotificationTimestamp = NULL;
$this->mSkin = $wgUser->getSkin();
$this->preCacheMessages();
}
@@ -44,7 +41,7 @@ class PageHistory {
function getArticle() {
return $this->mArticle;
}
-
+
function getTitle() {
return $this->mTitle;
}
@@ -68,15 +65,13 @@ class PageHistory {
* @returns nothing
*/
function history() {
- global $wgOut, $wgRequest, $wgTitle;
+ global $wgOut, $wgRequest, $wgTitle, $wgScript;
/*
* Allow client caching.
*/
-
if( $wgOut->checkLastModified( $this->mArticle->getTouched() ) )
- /* Client cache fresh and headers sent, nothing more to do. */
- return;
+ return; // Client cache fresh and headers sent, nothing more to do.
wfProfileIn( __METHOD__ );
@@ -87,13 +82,14 @@ class PageHistory {
$wgOut->setPageTitleActionText( wfMsg( 'history_short' ) );
$wgOut->setArticleFlag( false );
$wgOut->setArticleRelated( true );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setSyndicated( true );
$wgOut->setFeedAppendQuery( 'action=history' );
$wgOut->addScriptFile( 'history.js' );
$logPage = SpecialPage::getTitleFor( 'Log' );
- $logLink = $this->mSkin->makeKnownLinkObj( $logPage, wfMsgHtml( 'viewpagelogs' ), 'page=' . $this->mTitle->getPrefixedUrl() );
+ $logLink = $this->mSkin->makeKnownLinkObj( $logPage, wfMsgHtml( 'viewpagelogs' ),
+ 'page=' . $this->mTitle->getPrefixedUrl() );
$wgOut->setSubtitle( $logLink );
$feedType = $wgRequest->getVal( 'feed' );
@@ -111,26 +107,29 @@ class PageHistory {
return;
}
- /*
- * "go=first" means to jump to the last (earliest) history page.
- * This is deprecated, it no longer appears in the user interface
+ /**
+ * Add date selector to quickly get to a certain time
*/
- if ( $wgRequest->getText("go") == 'first' ) {
- $limit = $wgRequest->getInt( 'limit', 50 );
- global $wgFeedLimit;
- if( $limit > $wgFeedLimit ) {
- $limit = $wgFeedLimit;
- }
- $wgOut->redirect( $wgTitle->getLocalURL( "action=history&limit={$limit}&dir=prev" ) );
- return;
- }
+ $year = $wgRequest->getInt( 'year' );
+ $month = $wgRequest->getInt( 'month' );
+
+ $action = htmlspecialchars( $wgScript );
+ $wgOut->addHTML(
+ "<form action=\"$action\" method=\"get\" id=\"mw-history-searchform\">" .
+ Xml::fieldset( wfMsg( 'history-fieldset-title' ), false, array( 'id' => 'mw-history-search' ) ) .
+ Xml::hidden( 'title', $this->mTitle->getPrefixedDBKey() ) . "\n" .
+ Xml::hidden( 'action', 'history' ) . "\n" .
+ $this->getDateMenu( $year, $month ) . '&nbsp;' .
+ Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
+ '</fieldset></form>'
+ );
wfRunHooks( 'PageHistoryBeforeList', array( &$this->mArticle ) );
/**
* Do the list
*/
- $pager = new PageHistoryPager( $this );
+ $pager = new PageHistoryPager( $this, $year, $month );
$this->linesonpage = $pager->getNumRows();
$wgOut->addHTML(
$pager->getNavigationBar() .
@@ -139,21 +138,77 @@ class PageHistory {
$this->endHistoryList() .
$pager->getNavigationBar()
);
+
wfProfileOut( __METHOD__ );
}
/**
+ * @return string Formatted HTML
+ * @param int $year
+ * @param int $month
+ */
+ private function getDateMenu( $year, $month ) {
+ # Offset overrides year/month selection
+ if( $month && $month !== -1 ) {
+ $encMonth = intval( $month );
+ } else {
+ $encMonth = '';
+ }
+ if( $year ) {
+ $encYear = intval( $year );
+ } else if( $encMonth ) {
+ $thisMonth = intval( gmdate( 'n' ) );
+ $thisYear = intval( gmdate( 'Y' ) );
+ if( intval($encMonth) > $thisMonth ) {
+ $thisYear--;
+ }
+ $encYear = $thisYear;
+ } else {
+ $encYear = '';
+ }
+ return Xml::label( wfMsg( 'year' ), 'year' ) . ' '.
+ Xml::input( 'year', 4, $encYear, array('id' => 'year', 'maxlength' => 4) ) .
+ ' '.
+ Xml::label( wfMsg( 'month' ), 'month' ) . ' '.
+ Xml::monthSelector( $encMonth, -1 );
+ }
+
+ /**
* Creates begin of history list with a submit button
*
* @return string HTML output
*/
function beginHistoryList() {
- global $wgTitle, $wgScript;
+ global $wgTitle, $wgScript, $wgEnableHtmlDiff;
$this->lastdate = '';
$s = wfMsgExt( 'histlegend', array( 'parse') );
- $s .= Xml::openElement( 'form', array( 'action' => $wgScript ) );
+ $s .= Xml::openElement( 'form', array( 'action' => $wgScript, 'id' => 'mw-history-compare' ) );
$s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() );
- $s .= $this->submitButton();
+ if( $wgEnableHtmlDiff ) {
+ $s .= $this->submitButton( wfMsg( 'visualcomparison'),
+ array(
+ 'name' => 'htmldiff',
+ 'class' => 'historysubmit',
+ 'accesskey' => wfMsg( 'accesskey-visualcomparison' ),
+ 'title' => wfMsg( 'tooltip-compareselectedversions' ),
+ )
+ );
+ $s .= $this->submitButton( wfMsg( 'wikicodecomparison'),
+ array(
+ 'class' => 'historysubmit',
+ 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
+ 'title' => wfMsg( 'tooltip-compareselectedversions' ),
+ )
+ );
+ } else {
+ $s .= $this->submitButton( wfMsg( 'compareselectedversions'),
+ array(
+ 'class' => 'historysubmit',
+ 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
+ 'title' => wfMsg( 'tooltip-compareselectedversions' ),
+ )
+ );
+ }
$s .= '<ul id="pagehistory">' . "\n";
return $s;
}
@@ -164,8 +219,33 @@ class PageHistory {
* @return string HTML output
*/
function endHistoryList() {
+ global $wgEnableHtmlDiff;
$s = '</ul>';
- $s .= $this->submitButton( array( 'id' => 'historysubmit' ) );
+ if( $wgEnableHtmlDiff ) {
+ $s .= $this->submitButton( wfMsg( 'visualcomparison'),
+ array(
+ 'name' => 'htmldiff',
+ 'class' => 'historysubmit',
+ 'accesskey' => wfMsg( 'accesskey-visualcomparison' ),
+ 'title' => wfMsg( 'tooltip-compareselectedversions' ),
+ )
+ );
+ $s .= $this->submitButton( wfMsg( 'wikicodecomparison'),
+ array(
+ 'class' => 'historysubmit',
+ 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
+ 'title' => wfMsg( 'tooltip-compareselectedversions' ),
+ )
+ );
+ } else {
+ $s .= $this->submitButton( wfMsg( 'compareselectedversions'),
+ array(
+ 'class' => 'historysubmit',
+ 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
+ 'title' => wfMsg( 'tooltip-compareselectedversions' ),
+ )
+ );
+ }
$s .= '</form>';
return $s;
}
@@ -173,19 +253,13 @@ class PageHistory {
/**
* Creates a submit button
*
- * @param array $bits optional CSS ID
+ * @param array $attributes attributes
* @return string HTML output for the submit button
*/
- function submitButton( $bits = array() ) {
+ function submitButton($message, $attributes = array() ) {
# Disable submit button if history has 1 revision only
- if ( $this->linesonpage > 1 ) {
- return Xml::submitButton( wfMsg( 'compareselectedversions' ),
- $bits + array(
- 'class' => 'historysubmit',
- 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
- 'title' => wfMsg( 'tooltip-compareselectedversions' ),
- )
- );
+ if( $this->linesonpage > 1 ) {
+ return Xml::submitButton( $message , $attributes );
} else {
return '';
}
@@ -209,30 +283,29 @@ class PageHistory {
$rev = new Revision( $row );
$rev->setTitle( $this->mTitle );
- $s = '';
$curlink = $this->curLink( $rev, $latest );
$lastlink = $this->lastLink( $rev, $next, $counter );
$arbitrary = $this->diffButtons( $rev, $firstInList, $counter );
$link = $this->revLink( $rev );
- $s .= "($curlink) ($lastlink) $arbitrary";
+ $s = "($curlink) ($lastlink) $arbitrary";
if( $wgUser->isAllowed( 'deleterevision' ) ) {
$revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
if( $firstInList ) {
- // We don't currently handle well changing the top revision's settings
+ // We don't currently handle well changing the top revision's settings
$del = $this->message['rev-delundel'];
} else if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
- // If revision was hidden from sysops
+ // If revision was hidden from sysops
$del = $this->message['rev-delundel'];
} else {
$del = $this->mSkin->makeKnownLinkObj( $revdel,
- $this->message['rev-delundel'],
+ $this->message['rev-delundel'],
'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) .
'&oldid=' . urlencode( $rev->getId() ) );
// Bolden oversighted content
if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) )
- $del = "<strong>$del</strong>";
+ $del = "<strong>$del</strong>";
}
$s .= " <tt>(<small>$del</small>)</tt> ";
}
@@ -244,38 +317,39 @@ class PageHistory {
$s .= ' ' . Xml::element( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') );
}
- if ( !is_null( $size = $rev->getSize() ) && $rev->userCan( Revision::DELETED_TEXT ) ) {
+ if( !is_null( $size = $rev->getSize() ) && $rev->userCan( Revision::DELETED_TEXT ) ) {
$s .= ' ' . $this->mSkin->formatRevisionSize( $size );
}
$s .= $this->mSkin->revComment( $rev, false, true );
- if ($notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp)) {
+ if( $notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp) ) {
$s .= ' <span class="updatedmarker">' . wfMsgHtml( 'updatedmarker' ) . '</span>';
}
- #add blurb about text having been deleted
if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
$s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
}
$tools = array();
- if ( !is_null( $next ) && is_object( $next ) ) {
- if( !$this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser )
- && !$this->mTitle->getUserPermissionsErrors( 'edit', $wgUser )
- && $latest ) {
- $tools[] = '<span class="mw-rollback-link">'
- . $this->mSkin->buildRollbackLink( $rev )
- . '</span>';
+ if( !is_null( $next ) && is_object( $next ) ) {
+ if( $latest && $this->mTitle->userCan( 'rollback' ) && $this->mTitle->userCan( 'edit' ) ) {
+ $tools[] = '<span class="mw-rollback-link">'.$this->mSkin->buildRollbackLink( $rev ).'</span>';
}
- if( $this->mTitle->quickUserCan( 'edit' ) &&
- !$rev->isDeleted( Revision::DELETED_TEXT ) &&
- !$next->rev_deleted & Revision::DELETED_TEXT ) {
- $undolink = $this->mSkin->makeKnownLinkObj(
+ if( $this->mTitle->quickUserCan( 'edit' ) && !$rev->isDeleted( Revision::DELETED_TEXT ) &&
+ !$next->rev_deleted & Revision::DELETED_TEXT )
+ {
+ # Create undo tooltip for the first (=latest) line only
+ $undoTooltip = $latest
+ ? array( 'title' => wfMsg( 'tooltip-undo' ) )
+ : array();
+ $undolink = $this->mSkin->link(
$this->mTitle,
wfMsgHtml( 'editundo' ),
- 'action=edit&undoafter=' . $next->rev_id . '&undo=' . $rev->getId()
+ $undoTooltip,
+ array( 'action' => 'edit', 'undoafter' => $next->rev_id, 'undo' => $rev->getId() ),
+ array( 'known', 'noclasses' )
);
$tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>";
}
@@ -291,16 +365,16 @@ class PageHistory {
}
/**
- * Create a link to view this revision of the page
- * @param Revision $rev
- * @returns string
- */
+ * Create a link to view this revision of the page
+ * @param Revision $rev
+ * @returns string
+ */
function revLink( $rev ) {
global $wgLang;
$date = $wgLang->timeanddate( wfTimestamp(TS_MW, $rev->getTimestamp()), true );
if( $rev->userCan( Revision::DELETED_TEXT ) ) {
$link = $this->mSkin->makeKnownLinkObj(
- $this->mTitle, $date, "oldid=" . $rev->getId() );
+ $this->mTitle, $date, "oldid=" . $rev->getId() );
} else {
$link = $date;
}
@@ -311,30 +385,28 @@ class PageHistory {
}
/**
- * Create a diff-to-current link for this revision for this page
- * @param Revision $rev
- * @param Bool $latest, this is the latest revision of the page?
- * @returns string
- */
+ * Create a diff-to-current link for this revision for this page
+ * @param Revision $rev
+ * @param Bool $latest, this is the latest revision of the page?
+ * @returns string
+ */
function curLink( $rev, $latest ) {
$cur = $this->message['cur'];
if( $latest || !$rev->userCan( Revision::DELETED_TEXT ) ) {
return $cur;
} else {
- return $this->mSkin->makeKnownLinkObj(
- $this->mTitle, $cur,
- 'diff=' . $this->getLatestID() .
- "&oldid=" . $rev->getId() );
+ return $this->mSkin->makeKnownLinkObj( $this->mTitle, $cur,
+ 'diff=' . $this->mTitle->getLatestRevID() . "&oldid=" . $rev->getId() );
}
}
/**
- * Create a diff-to-previous link for this revision for this page.
- * @param Revision $prevRev, the previous revision
- * @param mixed $next, the newer revision
- * @param int $counter, what row on the history list this is
- * @returns string
- */
+ * Create a diff-to-previous link for this revision for this page.
+ * @param Revision $prevRev, the previous revision
+ * @param mixed $next, the newer revision
+ * @param int $counter, what row on the history list this is
+ * @returns string
+ */
function lastLink( $prevRev, $next, $counter ) {
$last = $this->message['last'];
# $next may either be a Row, null, or "unkown"
@@ -344,21 +416,13 @@ class PageHistory {
return $last;
} elseif( $next === 'unknown' ) {
# Next row probably exists but is unknown, use an oldid=prev link
- return $this->mSkin->makeKnownLinkObj(
- $this->mTitle,
- $last,
+ return $this->mSkin->makeKnownLinkObj( $this->mTitle, $last,
"diff=" . $prevRev->getId() . "&oldid=prev" );
} elseif( !$prevRev->userCan(Revision::DELETED_TEXT) || !$nextRev->userCan(Revision::DELETED_TEXT) ) {
return $last;
} else {
- return $this->mSkin->makeKnownLinkObj(
- $this->mTitle,
- $last,
- "diff=" . $prevRev->getId() . "&oldid={$next->rev_id}"
- /*,
- '',
- '',
- "tabindex={$counter}"*/ );
+ return $this->mSkin->makeKnownLinkObj( $this->mTitle, $last,
+ "diff=" . $prevRev->getId() . "&oldid={$next->rev_id}" );
}
}
@@ -382,10 +446,10 @@ class PageHistory {
}
/** @todo: move title texts to javascript */
- if ( $firstInList ) {
+ if( $firstInList ) {
$first = Xml::element( 'input', array_merge(
- $radio,
- array(
+ $radio,
+ array(
'style' => 'visibility:hidden',
'name' => 'oldid' ) ) );
$checkmark = array( 'checked' => 'checked' );
@@ -396,34 +460,21 @@ class PageHistory {
$checkmark = array();
}
$first = Xml::element( 'input', array_merge(
- $radio,
- $checkmark,
- array( 'name' => 'oldid' ) ) );
+ $radio,
+ $checkmark,
+ array( 'name' => 'oldid' ) ) );
$checkmark = array();
}
$second = Xml::element( 'input', array_merge(
- $radio,
- $checkmark,
- array( 'name' => 'diff' ) ) );
+ $radio,
+ $checkmark,
+ array( 'name' => 'diff' ) ) );
return $first . $second;
} else {
return '';
}
}
- /** @todo document */
- function getLatestId() {
- if( is_null( $this->mLatestId ) ) {
- $id = $this->mTitle->getArticleID();
- $db = wfGetDB( DB_SLAVE );
- $this->mLatestId = $db->selectField( 'page',
- "page_latest",
- array( 'page_id' => $id ),
- __METHOD__ );
- }
- return $this->mLatestId;
- }
-
/**
* Fetch an array of revisions, specified by a given limit, offset and
* direction. This is now only used by the feeds. It was previously
@@ -432,61 +483,25 @@ class PageHistory {
function fetchRevisions($limit, $offset, $direction) {
$dbr = wfGetDB( DB_SLAVE );
- if ($direction == PageHistory::DIR_PREV)
+ if( $direction == PageHistory::DIR_PREV )
list($dirs, $oper) = array("ASC", ">=");
else /* $direction == PageHistory::DIR_NEXT */
list($dirs, $oper) = array("DESC", "<=");
- if ($offset)
+ if( $offset )
$offsets = array("rev_timestamp $oper '$offset'");
else
$offsets = array();
$page_id = $this->mTitle->getArticleID();
- $res = $dbr->select(
- 'revision',
+ return $dbr->select( 'revision',
Revision::selectFields(),
array_merge(array("rev_page=$page_id"), $offsets),
__METHOD__,
- array('ORDER BY' => "rev_timestamp $dirs",
+ array( 'ORDER BY' => "rev_timestamp $dirs",
'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit)
- );
-
- $result = array();
- while (($obj = $dbr->fetchObject($res)) != NULL)
- $result[] = $obj;
-
- return $result;
- }
-
- /** @todo document */
- function getNotificationTimestamp() {
- global $wgUser, $wgShowUpdatedMarker;
-
- if ($this->mNotificationTimestamp !== NULL)
- return $this->mNotificationTimestamp;
-
- if ($wgUser->isAnon() || !$wgShowUpdatedMarker)
- return $this->mNotificationTimestamp = false;
-
- $dbr = wfGetDB(DB_SLAVE);
-
- $this->mNotificationTimestamp = $dbr->selectField(
- 'watchlist',
- 'wl_notificationtimestamp',
- array( 'wl_namespace' => $this->mTitle->getNamespace(),
- 'wl_title' => $this->mTitle->getDBkey(),
- 'wl_user' => $wgUser->getId()
- ),
- __METHOD__ );
-
- // Don't use the special value reserved for telling whether the field is filled
- if ( is_null( $this->mNotificationTimestamp ) ) {
- $this->mNotificationTimestamp = false;
- }
-
- return $this->mNotificationTimestamp;
+ );
}
/**
@@ -495,15 +510,15 @@ class PageHistory {
*/
function feed( $type ) {
global $wgFeedClasses, $wgRequest, $wgFeedLimit;
- if ( !FeedUtils::checkFeedOutput($type) ) {
+ if( !FeedUtils::checkFeedOutput($type) ) {
return;
}
$feed = new $wgFeedClasses[$type](
- $this->mTitle->getPrefixedText() . ' - ' .
- wfMsgForContent( 'history-feed-title' ),
- wfMsgForContent( 'history-feed-description' ),
- $this->mTitle->getFullUrl( 'action=history' ) );
+ $this->mTitle->getPrefixedText() . ' - ' .
+ wfMsgForContent( 'history-feed-title' ),
+ wfMsgForContent( 'history-feed-description' ),
+ $this->mTitle->getFullUrl( 'action=history' ) );
// Get a limit on number of feed entries. Provide a sane default
// of 10 if none is defined (but limit to $wgFeedLimit max)
@@ -511,7 +526,7 @@ class PageHistory {
if( $limit > $wgFeedLimit || $limit < 1 ) {
$limit = 10;
}
- $items = $this->fetchRevisions($limit, 0, PageHistory::DIR_NEXT);
+ $items = $this->fetchRevisions($limit, 0, PageHistory::DIR_NEXT);
$feed->outHeader();
if( $items ) {
@@ -531,7 +546,7 @@ class PageHistory {
$wgOut->parse( wfMsgForContent( 'history-feed-empty' ) ),
$this->mTitle->getFullUrl(),
wfTimestamp( TS_MW ),
- '',
+ '',
$this->mTitle->getTalkPage()->getFullUrl() );
}
@@ -547,18 +562,18 @@ class PageHistory {
$rev = new Revision( $row );
$rev->setTitle( $this->mTitle );
$text = FeedUtils::formatDiffRow( $this->mTitle,
- $this->mTitle->getPreviousRevisionID( $rev->getId() ),
- $rev->getId(),
- $rev->getTimestamp(),
- $rev->getComment() );
+ $this->mTitle->getPreviousRevisionID( $rev->getId() ),
+ $rev->getId(),
+ $rev->getTimestamp(),
+ $rev->getComment() );
if( $rev->getComment() == '' ) {
global $wgContLang;
$title = wfMsgForContent( 'history-feed-item-nocomment',
- $rev->getUserText(),
- $wgContLang->timeanddate( $rev->getTimestamp() ) );
+ $rev->getUserText(),
+ $wgContLang->timeanddate( $rev->getTimestamp() ) );
} else {
- $title = $rev->getUserText() . ": " . $this->stripComment( $rev->getComment() );
+ $title = $rev->getUserText() . ": " . FeedItem::stripComment( $rev->getComment() );
}
return new FeedItem(
@@ -569,13 +584,6 @@ class PageHistory {
$rev->getUserText(),
$this->mTitle->getTalkPage()->getFullUrl() );
}
-
- /**
- * Quickie hack... strip out wikilinks to more legible form from the comment.
- */
- function stripComment( $text ) {
- return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text );
- }
}
@@ -583,11 +591,13 @@ class PageHistory {
* @ingroup Pager
*/
class PageHistoryPager extends ReverseChronologicalPager {
- public $mLastRow = false, $mPageHistory;
+ public $mLastRow = false, $mPageHistory, $mTitle;
- function __construct( $pageHistory ) {
+ function __construct( $pageHistory, $year='', $month='' ) {
parent::__construct();
$this->mPageHistory = $pageHistory;
+ $this->mTitle =& $this->mPageHistory->mTitle;
+ $this->getDateCond( $year, $month );
}
function getQueryInfo() {
@@ -606,11 +616,11 @@ class PageHistoryPager extends ReverseChronologicalPager {
}
function formatRow( $row ) {
- if ( $this->mLastRow ) {
+ if( $this->mLastRow ) {
$latest = $this->mCounter == 1 && $this->mIsFirst;
$firstInList = $this->mCounter == 1;
$s = $this->mPageHistory->historyLine( $this->mLastRow, $row, $this->mCounter++,
- $this->mPageHistory->getNotificationTimestamp(), $latest, $firstInList );
+ $this->mTitle->getNotificationTimestamp(), $latest, $firstInList );
} else {
$s = '';
}
@@ -625,12 +635,12 @@ class PageHistoryPager extends ReverseChronologicalPager {
}
function getEndBody() {
- if ( $this->mLastRow ) {
+ if( $this->mLastRow ) {
$latest = $this->mCounter == 1 && $this->mIsFirst;
$firstInList = $this->mCounter == 1;
- if ( $this->mIsBackwards ) {
+ if( $this->mIsBackwards ) {
# Next row is unknown, but for UI reasons, probably exists if an offset has been specified
- if ( $this->mOffset == '' ) {
+ if( $this->mOffset == '' ) {
$next = null;
} else {
$next = 'unknown';
@@ -640,7 +650,7 @@ class PageHistoryPager extends ReverseChronologicalPager {
$next = $this->mPastTheEndRow;
}
$s = $this->mPageHistory->historyLine( $this->mLastRow, $next, $this->mCounter++,
- $this->mPageHistory->getNotificationTimestamp(), $latest, $firstInList );
+ $this->mTitle->getNotificationTimestamp(), $latest, $firstInList );
} else {
$s = '';
}
diff --git a/includes/PageQueryPage.php b/includes/PageQueryPage.php
index 0d1789ee..a2091e8b 100644
--- a/includes/PageQueryPage.php
+++ b/includes/PageQueryPage.php
@@ -17,7 +17,9 @@ class PageQueryPage extends QueryPage {
public function formatResult( $skin, $row ) {
global $wgContLang;
$title = Title::makeTitleSafe( $row->namespace, $row->title );
- return $skin->makeKnownLinkObj( $title,
- htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) );
+ $text = $row->title;
+ if ($title instanceof Title)
+ $text = $wgContLang->convert( $title->getPrefixedText() );
+ return $skin->link( $title, htmlspecialchars($text), array(), array(), array('known', 'noclasses') );
}
}
diff --git a/includes/Pager.php b/includes/Pager.php
index 62c4e551..8ec32ff4 100644
--- a/includes/Pager.php
+++ b/includes/Pager.php
@@ -154,6 +154,26 @@ abstract class IndexPager implements Pager {
wfProfileOut( $fname );
}
+
+ /**
+ * Return the result wrapper.
+ */
+ function getResult() {
+ return $this->mResult;
+ }
+
+ /**
+ * Set the offset from an other source than $wgRequest
+ */
+ function setOffset( $offset ) {
+ $this->mOffset = $offset;
+ }
+ /**
+ * Set the limit from an other source than $wgRequest
+ */
+ function setLimit( $limit ) {
+ $this->mLimit = $limit;
+ }
/**
* Extract some useful data from the result object for use by
@@ -292,9 +312,12 @@ abstract class IndexPager implements Pager {
# HTML 4 has no rel="end" . . .
$attrs = '';
}
+
+ if( $type ) {
+ $attrs .= " class=\"mw-{$type}link\"" ;
+ }
return $this->getSkin()->makeKnownLinkObj( $this->getTitle(), $text,
- wfArrayToCGI( $query, $this->getDefaultQuery() ), '', '',
- $attrs );
+ wfArrayToCGI( $query, $this->getDefaultQuery() ), '', '', $attrs );
}
/**
@@ -352,6 +375,8 @@ abstract class IndexPager implements Pager {
unset( $this->mDefaultQuery['offset'] );
unset( $this->mDefaultQuery['limit'] );
unset( $this->mDefaultQuery['order'] );
+ unset( $this->mDefaultQuery['month'] );
+ unset( $this->mDefaultQuery['year'] );
}
return $this->mDefaultQuery;
}
@@ -425,7 +450,7 @@ abstract class IndexPager implements Pager {
}
foreach ( $this->mLimitsShown as $limit ) {
$links[] = $this->makeLink( $wgLang->formatNum( $limit ),
- array( 'offset' => $offset, 'limit' => $limit ) );
+ array( 'offset' => $offset, 'limit' => $limit ), 'num' );
}
return $links;
}
@@ -564,6 +589,8 @@ abstract class AlphabeticPager extends IndexPager {
*/
abstract class ReverseChronologicalPager extends IndexPager {
public $mDefaultDirection = true;
+ public $mYear;
+ public $mMonth;
function __construct() {
parent::__construct();
@@ -591,6 +618,53 @@ abstract class ReverseChronologicalPager extends IndexPager {
wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits);
return $this->mNavigationBar;
}
+
+ function getDateCond( $year, $month ) {
+ $year = intval($year);
+ $month = intval($month);
+ // Basic validity checks
+ $this->mYear = $year > 0 ? $year : false;
+ $this->mMonth = ($month > 0 && $month < 13) ? $month : false;
+ // Given an optional year and month, we need to generate a timestamp
+ // to use as "WHERE rev_timestamp <= result"
+ // Examples: year = 2006 equals < 20070101 (+000000)
+ // year=2005, month=1 equals < 20050201
+ // year=2005, month=12 equals < 20060101
+ if ( !$this->mYear && !$this->mMonth ) {
+ return;
+ }
+ if ( $this->mYear ) {
+ $year = $this->mYear;
+ } else {
+ // If no year given, assume the current one
+ $year = gmdate( 'Y' );
+ // If this month hasn't happened yet this year, go back to last year's month
+ if( $this->mMonth > gmdate( 'n' ) ) {
+ $year--;
+ }
+ }
+ if ( $this->mMonth ) {
+ $month = $this->mMonth + 1;
+ // For December, we want January 1 of the next year
+ if ($month > 12) {
+ $month = 1;
+ $year++;
+ }
+ } else {
+ // No month implies we want up to the end of the year in question
+ $month = 1;
+ $year++;
+ }
+ // Y2K38 bug
+ if ( $year > 2032 ) {
+ $year = 2032;
+ }
+ $ymd = (int)sprintf( "%04d%02d01", $year, $month );
+ if ( $ymd > 20320101 ) {
+ $ymd = 20320101;
+ }
+ $this->mOffset = $this->mDb->timestamp( "${ymd}000000" );
+ }
}
/**
@@ -795,7 +869,7 @@ abstract class TablePager extends IndexPager {
"<form method=\"get\" action=\"$url\">" .
wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) .
"\n<input type=\"submit\" value=\"$msgSubmit\"/>\n" .
- $this->getHiddenFields( 'limit' ) .
+ $this->getHiddenFields( array('limit','title') ) .
"</form>\n";
}
diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php
index a3ff05e2..af569112 100644
--- a/includes/PrefixSearch.php
+++ b/includes/PrefixSearch.php
@@ -1,5 +1,12 @@
<?php
+/**
+ * PrefixSearch - Handles searching prefixes of titles and finding any page
+ * names that match. Used largely by the OpenSearch implementation.
+ *
+ * @ingroup Search
+ */
+
class PrefixSearch {
/**
* Do a prefix search of titles and return a list of matching page names.
@@ -48,7 +55,7 @@ class PrefixSearch {
if( count($namespaces) == 1 ){
$ns = $namespaces[0];
if( $ns == NS_MEDIA ) {
- $namespaces = array(NS_IMAGE);
+ $namespaces = array(NS_FILE);
} elseif( $ns == NS_SPECIAL ) {
return self::specialSearch( $search, $limit );
}
@@ -96,7 +103,9 @@ class PrefixSearch {
/**
* Unless overridden by PrefixSearchBackend hook...
- * This is case-sensitive except the first letter (per $wgCapitalLinks)
+ * This is case-sensitive (First character may
+ * be automatically capitalized by Title::secureAndSpit()
+ * later on depending on $wgCapitalLinks)
*
* @param array $namespaces Namespaces to search in
* @param string $search term
@@ -104,12 +113,6 @@ class PrefixSearch {
* @return array of title strings
*/
protected static function defaultSearchBackend( $namespaces, $search, $limit ) {
- global $wgCapitalLinks, $wgContLang;
-
- if( $wgCapitalLinks ) {
- $search = $wgContLang->ucfirst( $search );
- }
-
$ns = array_shift($namespaces); // support only one namespace
if( in_array(NS_MAIN,$namespaces))
$ns = NS_MAIN; // if searching on many always default to main
diff --git a/includes/Profiler.php b/includes/Profiler.php
index cef89dd3..ffb48978 100644
--- a/includes/Profiler.php
+++ b/includes/Profiler.php
@@ -355,8 +355,7 @@ class Profiler {
# 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;
+ global $wgProfilePerHost;
$dbw = wfGetDB( DB_MASTER );
if( !is_object( $dbw ) )
@@ -366,7 +365,7 @@ class Profiler {
$name = substr($name, 0, 255);
if( $wgProfilePerHost ){
- $pfhost = $wguname['nodename'];
+ $pfhost = wfHostname();
} else {
$pfhost = '';
}
diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php
index e7787822..372edfcd 100644
--- a/includes/ProtectionForm.php
+++ b/includes/ProtectionForm.php
@@ -20,79 +20,146 @@
*/
/**
- * @todo document, briefly.
+ * Handles the page protection UI and backend
*/
class ProtectionForm {
+ /** A map of action to restriction level, from request or default */
var $mRestrictions = array();
+
+ /** The custom/additional protection reason */
var $mReason = '';
+
+ /** The reason selected from the list, blank for other/additional */
+ var $mReasonSelection = '';
+
+ /** True if the restrictions are cascading, from request or existing protection */
var $mCascade = false;
- var $mExpiry = null;
+
+ /** Map of action to "other" expiry time. Used in preference to mExpirySelection. */
+ var $mExpiry = array();
+
+ /**
+ * Map of action to value selected in expiry drop-down list.
+ * Will be set to 'othertime' whenever mExpiry is set.
+ */
+ var $mExpirySelection = array();
+
+ /** Permissions errors for the protect action */
var $mPermErrors = array();
+
+ /** Types (i.e. actions) for which levels can be selected */
var $mApplicableTypes = array();
- function __construct( &$article ) {
+ /** Map of action to the expiry time of the existing protection */
+ var $mExistingExpiry = array();
+
+ function __construct( Article $article ) {
global $wgRequest, $wgUser;
global $wgRestrictionTypes, $wgRestrictionLevels;
- $this->mArticle =& $article;
- $this->mTitle =& $article->mTitle;
+ $this->mArticle = $article;
+ $this->mTitle = $article->mTitle;
$this->mApplicableTypes = $this->mTitle->exists() ? $wgRestrictionTypes : array('create');
- if( $this->mTitle ) {
- $this->mTitle->loadRestrictions();
+ $this->mCascade = $this->mTitle->areRestrictionsCascading();
- foreach( $this->mApplicableTypes as $action ) {
- // Fixme: this form currently requires individual selections,
- // but the db allows multiples separated by commas.
- $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
- }
+ // The form will be available in read-only to show levels.
+ $this->mPermErrors = $this->mTitle->getUserPermissionsErrors('protect',$wgUser);
+ $this->disabled = wfReadOnly() || $this->mPermErrors != array();
+ $this->disabledAttrib = $this->disabled
+ ? array( 'disabled' => 'disabled' )
+ : array();
+
+ $this->mReason = $wgRequest->getText( 'mwProtect-reason' );
+ $this->mReasonSelection = $wgRequest->getText( 'wpProtectReasonSelection' );
+ $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade', $this->mCascade );
- $this->mCascade = $this->mTitle->areRestrictionsCascading();
+ foreach( $this->mApplicableTypes as $action ) {
+ // Fixme: this form currently requires individual selections,
+ // but the db allows multiples separated by commas.
+ $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
- if ( $this->mTitle->mRestrictionsExpiry == 'infinity' ) {
- $this->mExpiry = 'infinite';
- } else if ( strlen($this->mTitle->mRestrictionsExpiry) == 0 ) {
- $this->mExpiry = '';
+ if ( !$this->mRestrictions[$action] ) {
+ // No existing expiry
+ $existingExpiry = '';
} else {
- // FIXME: this format is not user friendly
- $this->mExpiry = wfTimestamp( TS_ISO_8601, $this->mTitle->mRestrictionsExpiry );
+ $existingExpiry = $this->mTitle->getRestrictionExpiry( $action );
+ }
+ $this->mExistingExpiry[$action] = $existingExpiry;
+
+ $requestExpiry = $wgRequest->getText( "mwProtect-expiry-$action" );
+ $requestExpirySelection = $wgRequest->getVal( "wpProtectExpirySelection-$action" );
+
+ if ( $requestExpiry ) {
+ // Custom expiry takes precedence
+ $this->mExpiry[$action] = $requestExpiry;
+ $this->mExpirySelection[$action] = 'othertime';
+ } elseif ( $requestExpirySelection ) {
+ // Expiry selected from list
+ $this->mExpiry[$action] = '';
+ $this->mExpirySelection[$action] = $requestExpirySelection;
+ } elseif ( $existingExpiry == 'infinity' ) {
+ // Existing expiry is infinite, use "infinite" in drop-down
+ $this->mExpiry[$action] = '';
+ $this->mExpirySelection[$action] = 'infinite';
+ } elseif ( $existingExpiry ) {
+ // Use existing expiry in its own list item
+ $this->mExpiry[$action] = '';
+ $this->mExpirySelection[$action] = $existingExpiry;
+ } else {
+ // Final default: infinite
+ $this->mExpiry[$action] = '';
+ $this->mExpirySelection[$action] = 'infinite';
+ }
+
+ $val = $wgRequest->getVal( "mwProtect-level-$action" );
+ if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) {
+ // Prevent users from setting levels that they cannot later unset
+ if( $val == 'sysop' ) {
+ // Special case, rewrite sysop to either protect and editprotected
+ if( !$wgUser->isAllowed('protect') && !$wgUser->isAllowed('editprotected') )
+ continue;
+ } else {
+ if( !$wgUser->isAllowed($val) )
+ continue;
+ }
+ $this->mRestrictions[$action] = $val;
}
}
+ }
- // The form will be available in read-only to show levels.
- $this->disabled = wfReadOnly() || ($this->mPermErrors = $this->mTitle->getUserPermissionsErrors('protect',$wgUser)) != array();
- $this->disabledAttrib = $this->disabled
- ? array( 'disabled' => 'disabled' )
- : array();
+ /**
+ * Get the expiry time for a given action, by combining the relevant inputs.
+ * Returns a 14-char timestamp or "infinity", or false if the input was invalid
+ */
+ function getExpiry( $action ) {
+ if ( $this->mExpirySelection[$action] == 'existing' ) {
+ return $this->mExistingExpiry[$action];
+ } elseif ( $this->mExpirySelection[$action] == 'othertime' ) {
+ $value = $this->mExpiry[$action];
+ } else {
+ $value = $this->mExpirySelection[$action];
+ }
+ if ( $value == 'infinite' || $value == 'indefinite' || $value == 'infinity' ) {
+ $time = Block::infinity();
+ } else {
+ $unix = strtotime( $value );
- if( $wgRequest->wasPosted() ) {
- $this->mReason = $wgRequest->getText( 'mwProtect-reason' );
- $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' );
- $this->mExpiry = $wgRequest->getText( 'mwProtect-expiry' );
-
- foreach( $this->mApplicableTypes as $action ) {
- $val = $wgRequest->getVal( "mwProtect-level-$action" );
- if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) {
- //prevent users from setting levels that they cannot later unset
- if( $val == 'sysop' ) {
- //special case, rewrite sysop to either protect and editprotected
- if( !$wgUser->isAllowed('protect') && !$wgUser->isAllowed('editprotected') )
- continue;
- } else {
- if( !$wgUser->isAllowed($val) )
- continue;
- }
- $this->mRestrictions[$action] = $val;
- }
+ if ( !$unix || $unix === -1 ) {
+ return false;
}
+
+ // Fixme: non-qualified absolute times are not in users specified timezone
+ // and there isn't notice about it in the ui
+ $time = wfTimestamp( TS_MW, $unix );
}
+ return $time;
}
function execute() {
global $wgRequest, $wgOut;
if( $wgRequest->wasPosted() ) {
if( $this->save() ) {
- $article = new Article( $this->mTitle );
- $q = $article->isRedirect() ? 'redirect=no' : '';
+ $q = $this->mArticle->isRedirect() ? 'redirect=no' : '';
$wgOut->redirect( $this->mTitle->getFullUrl( $q ) );
}
} else {
@@ -103,7 +170,7 @@ class ProtectionForm {
function show( $err = null ) {
global $wgOut, $wgUser;
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
if( is_null( $this->mTitle ) ||
$this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
@@ -152,42 +219,39 @@ class ProtectionForm {
function save() {
global $wgRequest, $wgUser, $wgOut;
-
- if( $this->disabled ) {
+ # Permission check!
+ if ( $this->disabled ) {
$this->show();
return false;
}
$token = $wgRequest->getVal( 'wpEditToken' );
- if( !$wgUser->matchEditToken( $token ) ) {
+ if ( !$wgUser->matchEditToken( $token ) ) {
$this->show( wfMsg( 'sessionfailure' ) );
return false;
}
-
- if ( strlen( $this->mExpiry ) == 0 ) {
- $this->mExpiry = 'infinite';
+
+ # Create reason string. Use list and/or custom string.
+ $reasonstr = $this->mReasonSelection;
+ if ( $reasonstr != 'other' && $this->mReason != '' ) {
+ // Entry from drop down menu + additional comment
+ $reasonstr .= ': ' . $this->mReason;
+ } elseif ( $reasonstr == 'other' ) {
+ $reasonstr = $this->mReason;
}
-
- if ( $this->mExpiry == 'infinite' || $this->mExpiry == 'indefinite' ) {
- $expiry = Block::infinity();
- } else {
- # Convert GNU-style date, on error returns -1 for PHP <5.1 and false for PHP >=5.1
- $expiry = strtotime( $this->mExpiry );
-
- if ( $expiry < 0 || $expiry === false ) {
+ $expiry = array();
+ foreach( $this->mApplicableTypes as $action ) {
+ $expiry[$action] = $this->getExpiry( $action );
+ if( empty($this->mRestrictions[$action]) )
+ continue; // unprotected
+ if ( !$expiry[$action] ) {
$this->show( wfMsg( 'protect_expiry_invalid' ) );
return false;
}
-
- // Fixme: non-qualified absolute times are not in users specified timezone
- // and there isn't notice about it in the ui
- $expiry = wfTimestamp( TS_MW, $expiry );
-
- if ( $expiry < wfTimestampNow() ) {
+ if ( $expiry[$action] < wfTimestampNow() ) {
$this->show( wfMsg( 'protect_expiry_old' ) );
return false;
}
-
}
# They shouldn't be able to do this anyway, but just to make sure, ensure that cascading restrictions aren't being applied
@@ -195,15 +259,15 @@ class ProtectionForm {
global $wgGroupPermissions;
$edit_restriction = $this->mRestrictions['edit'];
-
+ $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' );
if ($this->mCascade && ($edit_restriction != 'protect') &&
!(isset($wgGroupPermissions[$edit_restriction]['protect']) && $wgGroupPermissions[$edit_restriction]['protect'] ) )
$this->mCascade = false;
if ($this->mTitle->exists()) {
- $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $this->mReason, $this->mCascade, $expiry );
+ $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $reasonstr, $this->mCascade, $expiry );
} else {
- $ok = $this->mTitle->updateTitleProtection( $this->mRestrictions['create'], $this->mReason, $expiry );
+ $ok = $this->mTitle->updateTitleProtection( $this->mRestrictions['create'], $reasonstr, $expiry['create'] );
}
if( !$ok ) {
@@ -215,7 +279,6 @@ class ProtectionForm {
} elseif( $this->mTitle->userIsWatching() ) {
$this->mArticle->doUnwatch();
}
-
return $ok;
}
@@ -225,73 +288,143 @@ class ProtectionForm {
* @return $out string HTML form
*/
function buildForm() {
- global $wgUser;
+ global $wgUser, $wgLang;
+
+ $mProtectreasonother = Xml::label( wfMsg( 'protectcomment' ), 'wpProtectReasonSelection' );
+ $mProtectreason = Xml::label( wfMsg( 'protect-otherreason' ), 'mwProtect-reason' );
$out = '';
if( !$this->disabled ) {
$out .= $this->buildScript();
- // The submission needs to reenable the move permission selector
- // if it's in locked mode, or some browsers won't submit the data.
- $out .= Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->mTitle->getLocalUrl( 'action=protect' ), 'id' => 'mw-Protect-Form', 'onsubmit' => 'protectEnable(true)' ) ) .
- Xml::hidden( 'wpEditToken',$wgUser->editToken() );
+ $out .= Xml::openElement( 'form', array( 'method' => 'post',
+ 'action' => $this->mTitle->getLocalUrl( 'action=protect' ),
+ 'id' => 'mw-Protect-Form', 'onsubmit' => 'ProtectionForm.enableUnchainedInputs(true)' ) );
+ $out .= Xml::hidden( 'wpEditToken',$wgUser->editToken() );
}
$out .= Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', null, wfMsg( 'protect-legend' ) ) .
Xml::openElement( 'table', array( 'id' => 'mwProtectSet' ) ) .
- Xml::openElement( 'tbody' ) .
- "<tr>\n";
+ Xml::openElement( 'tbody' );
- foreach( $this->mRestrictions as $action => $required ) {
- /* Not all languages have V_x <-> N_x relation */
- $label = Xml::element( 'label',
- array( 'for' => "mwProtect-level-$action" ),
- wfMsg( 'restriction-' . $action ) );
- $out .= "<th>$label</th>";
- }
- $out .= "</tr>
- <tr>\n";
foreach( $this->mRestrictions as $action => $selected ) {
- $out .= "<td>" .
- $this->buildSelector( $action, $selected ) .
- "</td>";
+ /* Not all languages have V_x <-> N_x relation */
+ $msg = wfMsg( 'restriction-' . $action );
+ if( wfEmptyMsg( 'restriction-' . $action, $msg ) ) {
+ $msg = $action;
+ }
+ $out .= "<tr><td>".
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, $msg ) .
+ Xml::openElement( 'table', array( 'id' => "mw-protect-table-$action" ) ) .
+ "<tr><td>" . $this->buildSelector( $action, $selected ) . "</td></tr><tr><td>";
+
+ $reasonDropDown = Xml::listDropDown( 'wpProtectReasonSelection',
+ wfMsgForContent( 'protect-dropdown' ),
+ wfMsgForContent( 'protect-otherreason-op' ),
+ $this->mReasonSelection,
+ 'mwProtect-reason', 4 );
+ $scExpiryOptions = wfMsgForContent( 'protect-expiry-options' );
+
+ $showProtectOptions = ($scExpiryOptions !== '-' && !$this->disabled);
+
+ $mProtectexpiry = Xml::label( wfMsg( 'protectexpiry' ), "mwProtectExpirySelection-$action" );
+ $mProtectother = Xml::label( wfMsg( 'protect-othertime' ), "mwProtect-$action-expires" );
+
+ $expiryFormOptions = '';
+ if ( $this->mExistingExpiry[$action] && $this->mExistingExpiry[$action] != 'infinity' ) {
+ $timestamp = $wgLang->timeanddate( $this->mExistingExpiry[$action] );
+ $d = $wgLang->date( $this->mExistingExpiry[$action] );
+ $t = $wgLang->time( $this->mExistingExpiry[$action] );
+ $expiryFormOptions .=
+ Xml::option(
+ wfMsg( 'protect-existing-expiry', $timestamp, $d, $t ),
+ 'existing',
+ $this->mExpirySelection[$action] == 'existing'
+ ) . "\n";
+ }
+
+ $expiryFormOptions .= Xml::option( wfMsg( 'protect-othertime-op' ), "othertime" ) . "\n";
+ foreach( explode(',', $scExpiryOptions) as $option ) {
+ if ( strpos($option, ":") === false ) {
+ $show = $value = $option;
+ } else {
+ list($show, $value) = explode(":", $option);
+ }
+ $show = htmlspecialchars($show);
+ $value = htmlspecialchars($value);
+ $expiryFormOptions .= Xml::option( $show, $value, $this->mExpirySelection[$action] === $value ) . "\n";
+ }
+ # Add expiry dropdown
+ if( $showProtectOptions && !$this->disabled ) {
+ $out .= "
+ <table><tr>
+ <td class='mw-label'>
+ {$mProtectexpiry}
+ </td>
+ <td class='mw-input'>" .
+ Xml::tags( 'select',
+ array(
+ 'id' => "mwProtectExpirySelection-$action",
+ 'name' => "wpProtectExpirySelection-$action",
+ 'onchange' => "ProtectionForm.updateExpiryList(this)",
+ 'tabindex' => '2' ) + $this->disabledAttrib,
+ $expiryFormOptions ) .
+ "</td>
+ </tr></table>";
+ }
+ # Add custom expiry field
+ $attribs = array( 'id' => "mwProtect-$action-expires", 'onkeyup' => 'ProtectionForm.updateExpiry(this)' ) + $this->disabledAttrib;
+ $out .= "<table><tr>
+ <td class='mw-label'>" .
+ $mProtectother .
+ '</td>
+ <td class="mw-input">' .
+ Xml::input( "mwProtect-expiry-$action", 50, $this->mExpiry[$action], $attribs ) .
+ '</td>
+ </tr></table>';
+ $out .= "</td></tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' ) .
+ "</td></tr>";
}
- $out .= "</tr>\n";
-
- // JavaScript will add another row with a value-chaining checkbox
- $out .= Xml::closeElement( 'tbody' ) .
- Xml::closeElement( 'table' ) .
- Xml::openElement( 'table', array( 'id' => 'mw-protect-table2' ) ) .
- Xml::openElement( 'tbody' );
+ $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
+ // JavaScript will add another row with a value-chaining checkbox
if( $this->mTitle->exists() ) {
+ $out .= Xml::openElement( 'table', array( 'id' => 'mw-protect-table2' ) ) .
+ Xml::openElement( 'tbody' );
$out .= '<tr>
<td></td>
<td class="mw-input">' .
- Xml::checkLabel( wfMsg( 'protect-cascade' ), 'mwProtect-cascade', 'mwProtect-cascade', $this->mCascade, $this->disabledAttrib ) .
+ Xml::checkLabel( wfMsg( 'protect-cascade' ), 'mwProtect-cascade', 'mwProtect-cascade',
+ $this->mCascade, $this->disabledAttrib ) .
"</td>
</tr>\n";
+ $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
}
-
- $attribs = array( 'id' => 'expires' ) + $this->disabledAttrib;
- $out .= "<tr>
- <td class='mw-label'>" .
- Xml::label( wfMsgExt( 'protectexpiry', array( 'parseinline' ) ), 'expires' ) .
- '</td>
- <td class="mw-input">' .
- Xml::input( 'mwProtect-expiry', 60, $this->mExpiry, $attribs ) .
- '</td>
- </tr>';
-
+
+ # Add manual and custom reason field/selects as well as submit
if( !$this->disabled ) {
- $id = 'mwProtect-reason';
- $out .= "<tr>
- <td class='mw-label'>" .
- Xml::label( wfMsg( 'protectcomment' ), $id ) .
- '</td>
- <td class="mw-input">' .
- Xml::input( $id, 60, $this->mReason, array( 'type' => 'text', 'id' => $id, 'maxlength' => 255 ) ) .
+ $out .= Xml::openElement( 'table', array( 'id' => 'mw-protect-table3' ) ) .
+ Xml::openElement( 'tbody' );
+ $out .= "
+ <tr>
+ <td class='mw-label'>
+ {$mProtectreasonother}
+ </td>
+ <td class='mw-input'>
+ {$reasonDropDown}
+ </td>
+ </tr>
+ <tr>
+ <td class='mw-label'>
+ {$mProtectreason}
+ </td>
+ <td class='mw-input'>" .
+ Xml::input( 'mwProtect-reason', 60, $this->mReason, array( 'type' => 'text',
+ 'id' => 'mwProtect-reason', 'maxlength' => 255 ) ) .
"</td>
</tr>
<tr>
@@ -308,11 +441,15 @@ class ProtectionForm {
Xml::submitButton( wfMsg( 'confirm' ), array( 'id' => 'mw-Protect-submit' ) ) .
"</td>
</tr>\n";
+ $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
}
+ $out .= Xml::closeElement( 'fieldset' );
- $out .= Xml::closeElement( 'tbody' ) .
- Xml::closeElement( 'table' ) .
- Xml::closeElement( 'fieldset' );
+ if ( $wgUser->isAllowed( 'editinterface' ) ) {
+ $linkTitle = Title::makeTitleSafe( NS_MEDIAWIKI, 'protect-dropdown' );
+ $link = $wgUser->getSkin()->Link ( $linkTitle, wfMsgHtml( 'protect-edit-reasonlist' ) );
+ $out .= '<p class="mw-protect-editreasons">' . $link . '</p>';
+ }
if ( !$this->disabled ) {
$out .= Xml::closeElement( 'form' ) .
@@ -324,15 +461,8 @@ class ProtectionForm {
function buildSelector( $action, $selected ) {
global $wgRestrictionLevels, $wgUser;
- $id = 'mwProtect-level-' . $action;
- $attribs = array(
- 'id' => $id,
- 'name' => $id,
- 'size' => count( $wgRestrictionLevels ),
- 'onchange' => 'protectLevelsUpdate(this)',
- ) + $this->disabledAttrib;
- $out = Xml::openElement( 'select', $attribs );
+ $levels = array();
foreach( $wgRestrictionLevels as $key ) {
//don't let them choose levels above their own (aka so they can still unprotect and edit the page). but only when the form isn't disabled
if( $key == 'sysop' ) {
@@ -343,6 +473,19 @@ class ProtectionForm {
if( !$wgUser->isAllowed($key) && !$this->disabled )
continue;
}
+ $levels[] = $key;
+ }
+
+ $id = 'mwProtect-level-' . $action;
+ $attribs = array(
+ 'id' => $id,
+ 'name' => $id,
+ 'size' => count( $levels ),
+ 'onchange' => 'ProtectionForm.updateLevels(this)',
+ ) + $this->disabledAttrib;
+
+ $out = Xml::openElement( 'select', $attribs );
+ foreach( $levels as $key ) {
$out .= Xml::option( $this->getOptionLabel( $key ), $key, $key == $selected );
}
$out .= Xml::closeElement( 'select' );
@@ -371,7 +514,7 @@ class ProtectionForm {
global $wgStylePath, $wgStyleVersion;
return Xml::tags( 'script', array(
'type' => 'text/javascript',
- 'src' => $wgStylePath . "/common/protect.js?$wgStyleVersion" ), '' );
+ 'src' => $wgStylePath . "/common/protect.js?$wgStyleVersion.1" ), '' );
}
function buildCleanupScript() {
@@ -384,7 +527,15 @@ class ProtectionForm {
}
}
$script .= "[" . implode(',',$CascadeableLevels) . "];\n";
- $script .= 'protectInitialize("mwProtectSet","' . Xml::escapeJsString( wfMsg( 'protect-unchain' ) ) . '","' . count($this->mApplicableTypes) . '")';
+ $options = (object)array(
+ 'tableId' => 'mw-protect-table-move',
+ 'labelText' => wfMsg( 'protect-unchain' ),
+ 'numTypes' => count($this->mApplicableTypes),
+ 'existingMatch' => 1 == count( array_unique( $this->mExistingExpiry ) ),
+ );
+ $encOptions = Xml::encodeJsVar( $options );
+
+ $script .= "ProtectionForm.init($encOptions)";
return Xml::tags( 'script', array( 'type' => 'text/javascript' ), $script );
}
diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php
index 0f010421..771fd577 100644
--- a/includes/ProxyTools.php
+++ b/includes/ProxyTools.php
@@ -67,7 +67,7 @@ function wfGetAgent() {
* @return string
*/
function wfGetIP() {
- global $wgIP;
+ global $wgIP, $wgUsePrivateIPs;
# Return cached result
if ( !empty( $wgIP ) ) {
@@ -97,8 +97,10 @@ function wfGetIP() {
foreach ( $ipchain as $i => $curIP ) {
$curIP = IP::canonicalize( $curIP );
if ( wfIsTrustedProxy( $curIP ) ) {
- if ( isset( $ipchain[$i + 1] ) && IP::isPublic( $ipchain[$i + 1] ) ) {
- $ip = $ipchain[$i + 1];
+ if ( isset( $ipchain[$i + 1] ) ) {
+ if( $wgUsePrivateIPs || IP::isPublic( $ipchain[$i + 1 ] ) ) {
+ $ip = $ipchain[$i + 1];
+ }
}
} else {
break;
@@ -121,8 +123,7 @@ function wfIsTrustedProxy( $ip ) {
global $wgSquidServers, $wgSquidServersNoPurge;
if ( in_array( $ip, $wgSquidServers ) ||
- in_array( $ip, $wgSquidServersNoPurge ) ||
- wfIsAOLProxy( $ip )
+ in_array( $ip, $wgSquidServersNoPurge )
) {
$trusted = true;
} else {
@@ -212,50 +213,3 @@ function wfIsLocallyBlockedProxy( $ip ) {
return $ret;
}
-/**
- * TODO: move this list to the database in a global IP info table incorporating
- * trusted ISP proxies, blocked IP addresses and open proxies.
- * @return bool
- */
-function wfIsAOLProxy( $ip ) {
- # From http://webmaster.info.aol.com/proxyinfo.html
- $ranges = array(
- '64.12.96.0/19',
- '149.174.160.0/20',
- '152.163.240.0/21',
- '152.163.248.0/22',
- '152.163.252.0/23',
- '152.163.96.0/22',
- '152.163.100.0/23',
- '195.93.32.0/22',
- '195.93.48.0/22',
- '195.93.64.0/19',
- '195.93.96.0/19',
- '195.93.16.0/20',
- '198.81.0.0/22',
- '198.81.16.0/20',
- '198.81.8.0/23',
- '202.67.64.128/25',
- '205.188.192.0/20',
- '205.188.208.0/23',
- '205.188.112.0/20',
- '205.188.146.144/30',
- '207.200.112.0/21',
- );
-
- static $parsedRanges;
- if ( is_null( $parsedRanges ) ) {
- $parsedRanges = array();
- foreach ( $ranges as $range ) {
- $parsedRanges[] = IP::parseRange( $range );
- }
- }
-
- $hex = IP::toHex( $ip );
- foreach ( $parsedRanges as $range ) {
- if ( $hex >= $range[0] && $hex <= $range[1] ) {
- return true;
- }
- }
- return false;
-}
diff --git a/includes/QueryPage.php b/includes/QueryPage.php
index 16dc7c04..0b587508 100644
--- a/includes/QueryPage.php
+++ b/includes/QueryPage.php
@@ -21,6 +21,7 @@ $wgQueryPages = array(
array( 'DeadendPagesPage', 'Deadendpages' ),
array( 'DisambiguationsPage', 'Disambiguations' ),
array( 'DoubleRedirectsPage', 'DoubleRedirects' ),
+ array( 'LinkSearchPage', 'LinkSearch' ),
array( 'ListredirectsPage', 'Listredirects' ),
array( 'LonelyPagesPage', 'Lonelypages' ),
array( 'LongPagesPage', 'Longpages' ),
@@ -39,7 +40,9 @@ $wgQueryPages = array(
array( 'UnusedCategoriesPage', 'Unusedcategories' ),
array( 'UnusedimagesPage', 'Unusedimages' ),
array( 'WantedCategoriesPage', 'Wantedcategories' ),
+ array( 'WantedFilesPage', 'Wantedfiles' ),
array( 'WantedPagesPage', 'Wantedpages' ),
+ array( 'WantedTemplatesPage', 'Wantedtemplates' ),
array( 'UnwatchedPagesPage', 'Unwatchedpages' ),
array( 'UnusedtemplatesPage', 'Unusedtemplates' ),
array( 'WithoutInterwikiPage', 'Withoutinterwiki' ),
@@ -334,22 +337,22 @@ class QueryPage {
$this->preprocessResults( $dbr, $res );
- $wgOut->addHtml( XML::openElement( 'div', array('class' => 'mw-spcontent') ) );
+ $wgOut->addHTML( XML::openElement( 'div', array('class' => 'mw-spcontent') ) );
# Top header and navigation
if( $shownavigation ) {
- $wgOut->addHtml( $this->getPageHeader() );
+ $wgOut->addHTML( $this->getPageHeader() );
if( $num > 0 ) {
- $wgOut->addHtml( '<p>' . wfShowingResults( $offset, $num ) . '</p>' );
+ $wgOut->addHTML( '<p>' . wfShowingResults( $offset, $num ) . '</p>' );
# Disable the "next" link when we reach the end
$paging = wfViewPrevNext( $offset, $limit, $wgContLang->specialPage( $sname ),
wfArrayToCGI( $this->linkParameters() ), ( $num < $limit ) );
- $wgOut->addHtml( '<p>' . $paging . '</p>' );
+ $wgOut->addHTML( '<p>' . $paging . '</p>' );
} else {
# No results to show, so don't bother with "showing X of Y" etc.
# -- just let the user know and give up now
- $wgOut->addHtml( '<p>' . wfMsgHtml( 'specialpage-empty' ) . '</p>' );
- $wgOut->addHtml( XML::closeElement( 'div' ) );
+ $wgOut->addHTML( '<p>' . wfMsgHtml( 'specialpage-empty' ) . '</p>' );
+ $wgOut->addHTML( XML::closeElement( 'div' ) );
return;
}
}
@@ -366,10 +369,10 @@ class QueryPage {
# Repeat the paging links at the bottom
if( $shownavigation ) {
- $wgOut->addHtml( '<p>' . $paging . '</p>' );
+ $wgOut->addHTML( '<p>' . $paging . '</p>' );
}
- $wgOut->addHtml( XML::closeElement( 'div' ) );
+ $wgOut->addHTML( XML::closeElement( 'div' ) );
return $num;
}
@@ -428,7 +431,7 @@ class QueryPage {
? $wgContLang->listToText( $html )
: implode( '', $html );
- $out->addHtml( $html );
+ $out->addHTML( $html );
}
}
@@ -531,7 +534,7 @@ class QueryPage {
}
function feedDesc() {
- return wfMsg( 'tagline' );
+ return wfMsgExt( 'tagline', 'parsemag' );
}
function feedUrl() {
diff --git a/includes/RawPage.php b/includes/RawPage.php
index b1e2539a..7093367f 100644
--- a/includes/RawPage.php
+++ b/includes/RawPage.php
@@ -21,20 +21,20 @@ class RawPage {
var $mContentType, $mExpandTemplates;
function __construct( &$article, $request = false ) {
- global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType, $wgForcedRawSMaxage, $wgGroupPermissions;
+ global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType, $wgGroupPermissions;
$allowedCTypes = array('text/x-wiki', $wgJsMimeType, 'text/css', 'application/x-zope-edit');
$this->mArticle =& $article;
$this->mTitle =& $article->mTitle;
- if ( $request === false ) {
+ if( $request === false ) {
$this->mRequest =& $wgRequest;
} else {
$this->mRequest = $request;
}
$ctype = $this->mRequest->getVal( 'ctype' );
- $smaxage = $this->mRequest->getIntOrNull( 'smaxage', $wgSquidMaxage );
+ $smaxage = $this->mRequest->getIntOrNull( 'smaxage' );
$maxage = $this->mRequest->getInt( 'maxage', $wgSquidMaxage );
$this->mExpandTemplates = $this->mRequest->getVal( 'templates' ) === 'expand';
@@ -44,17 +44,17 @@ class RawPage {
$oldid = $this->mRequest->getInt( 'oldid' );
- switch ( $wgRequest->getText( 'direction' ) ) {
+ switch( $wgRequest->getText( 'direction' ) ) {
case 'next':
# output next revision, or nothing if there isn't one
- if ( $oldid ) {
+ if( $oldid ) {
$oldid = $this->mTitle->getNextRevisionId( $oldid );
}
$oldid = $oldid ? $oldid : -1;
break;
case 'prev':
# output previous revision, or nothing if there isn't one
- if ( ! $oldid ) {
+ if( ! $oldid ) {
# get the current revision so we can get the penultimate one
$this->mArticle->getTouched();
$oldid = $this->mArticle->mLatest;
@@ -71,11 +71,11 @@ class RawPage {
# special case for 'generated' raw things: user css/js
$gen = $this->mRequest->getVal( 'gen' );
- if($gen == 'css') {
+ if( $gen == 'css' ) {
$this->mGen = $gen;
if( is_null( $smaxage ) ) $smaxage = $wgSquidMaxage;
if($ctype == '') $ctype = 'text/css';
- } elseif ($gen == 'js') {
+ } elseif( $gen == 'js' ) {
$this->mGen = $gen;
if( is_null( $smaxage ) ) $smaxage = $wgSquidMaxage;
if($ctype == '') $ctype = $wgJsMimeType;
@@ -85,7 +85,8 @@ class RawPage {
$this->mCharset = $wgInputEncoding;
# Force caching for CSS and JS raw content, default: 5 minutes
- if (is_null($smaxage) and ($ctype=='text/css' or $ctype==$wgJsMimeType)) {
+ if( is_null($smaxage) and ($ctype=='text/css' or $ctype==$wgJsMimeType) ) {
+ global $wgForcedRawSMaxage;
$this->mSmaxage = intval($wgForcedRawSMaxage);
} else {
$this->mSmaxage = intval( $smaxage );
@@ -94,14 +95,13 @@ class RawPage {
# Output may contain user-specific data;
# vary generated content for open sessions and private wikis
- if ($this->mGen or !$wgGroupPermissions['*']['read']) {
- $this->mPrivateCache = ( $this->mSmaxage == 0 ) ||
- ( session_id() != '' );
+ if( $this->mGen or !$wgGroupPermissions['*']['read'] ) {
+ $this->mPrivateCache = $this->mSmaxage == 0 || session_id() != '';
} else {
$this->mPrivateCache = false;
}
- if ( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) {
+ if( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) {
$this->mContentType = 'text/x-wiki';
} else {
$this->mContentType = $ctype;
@@ -149,6 +149,18 @@ class RawPage {
# allow the client to cache this for 24 hours
$mode = $this->mPrivateCache ? 'private' : 'public';
header( 'Cache-Control: '.$mode.', s-maxage='.$this->mSmaxage.', max-age='.$this->mMaxage );
+
+ if( HTMLFileCache::useFileCache() ) {
+ $cache = new HTMLFileCache( $this->mTitle, 'raw' );
+ if( $cache->isFileCacheGood( /* Assume up to date */ ) ) {
+ $cache->loadFromFileCache();
+ $wgOut->disable();
+ return;
+ } else {
+ ob_start( array(&$cache, 'saveToFileCache' ) );
+ }
+ }
+
$text = $this->getRawText();
if( !wfRunHooks( 'RawPageViewBeforeOutput', array( &$this, &$text ) ) ) {
@@ -161,13 +173,15 @@ class RawPage {
function getRawText() {
global $wgUser, $wgOut, $wgRequest;
- if($this->mGen) {
+ if( $this->mGen ) {
$sk = $wgUser->getSkin();
- $sk->initPage($wgOut);
- if($this->mGen == 'css') {
- return $sk->getUserStylesheet();
- } else if($this->mGen == 'js') {
- return $sk->getUserJs();
+ if( !StubObject::isRealObject( $wgOut ) )
+ $wgOut->_unstub( 2 );
+ $sk->initPage( $wgOut );
+ if( $this->mGen == 'css' ) {
+ return $sk->generateUserStylesheet();
+ } else if( $this->mGen == 'js' ) {
+ return $sk->generateUserJs();
}
} else {
return $this->getArticleText();
@@ -179,7 +193,7 @@ class RawPage {
$text = '';
if( $this->mTitle ) {
// If it's a MediaWiki message we can just hit the message cache
- if ( $this->mUseMessageCache && $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ if( $this->mUseMessageCache && $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
$key = $this->mTitle->getDBkey();
$text = wfMsgForContentNoTrans( $key );
# If the message doesn't exist, return a blank
@@ -189,11 +203,11 @@ class RawPage {
} else {
// Get it from the DB
$rev = Revision::newFromTitle( $this->mTitle, $this->mOldId );
- if ( $rev ) {
+ if( $rev ) {
$lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() );
header( "Last-modified: $lastmod" );
- if ( !is_null($this->mSection ) ) {
+ if( !is_null($this->mSection ) ) {
global $wgParser;
$text = $wgParser->getSection ( $rev->getText(), $this->mSection );
} else
@@ -230,10 +244,10 @@ class RawPage {
}
function parseArticleText( $text ) {
- if ( $text === '' )
+ if( $text === '' )
return '';
else
- if ( $this->mExpandTemplates ) {
+ if( $this->mExpandTemplates ) {
global $wgParser;
return $wgParser->preprocess( $text, $this->mTitle, new ParserOptions() );
} else
diff --git a/includes/RecentChange.php b/includes/RecentChange.php
index 4daf6f87..f03fbcbb 100644
--- a/includes/RecentChange.php
+++ b/includes/RecentChange.php
@@ -49,15 +49,13 @@ class RecentChange
# Factory methods
- public static function newFromRow( $row )
- {
+ public static function newFromRow( $row ) {
$rc = new RecentChange;
$rc->loadFromRow( $row );
return $rc;
}
- public static function newFromCurRow( $row )
- {
+ public static function newFromCurRow( $row ) {
$rc = new RecentChange;
$rc->loadFromCurRow( $row );
$rc->notificationtimestamp = false;
@@ -110,27 +108,23 @@ class RecentChange
# Accessors
- function setAttribs( $attribs )
- {
+ public function setAttribs( $attribs ) {
$this->mAttribs = $attribs;
}
- function setExtra( $extra )
- {
+ public function setExtra( $extra ) {
$this->mExtra = $extra;
}
- function &getTitle()
- {
- if ( $this->mTitle === false ) {
+ public function &getTitle() {
+ if( $this->mTitle === false ) {
$this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
}
return $this->mTitle;
}
- function getMovedToTitle()
- {
- if ( $this->mMovedToTitle === false ) {
+ public function getMovedToTitle() {
+ if( $this->mMovedToTitle === false ) {
$this->mMovedToTitle = Title::makeTitle( $this->mAttribs['rc_moved_to_ns'],
$this->mAttribs['rc_moved_to_title'] );
}
@@ -138,24 +132,22 @@ class RecentChange
}
# Writes the data in this object to the database
- function save()
- {
- global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress,
- $wgRC2UDPPort, $wgRC2UDPPrefix, $wgRC2UDPOmitBots;
+ public function save() {
+ global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress, $wgRC2UDPOmitBots;
$fname = 'RecentChange::save';
$dbw = wfGetDB( DB_MASTER );
- if ( !is_array($this->mExtra) ) {
+ if( !is_array($this->mExtra) ) {
$this->mExtra = array();
}
$this->mExtra['lang'] = $wgLocalInterwiki;
- if ( !$wgPutIPinRC ) {
+ if( !$wgPutIPinRC ) {
$this->mAttribs['rc_ip'] = '';
}
- ## If our database is strict about IP addresses, use NULL instead of an empty string
- if ( $dbw->strictIPs() and $this->mAttribs['rc_ip'] == '' ) {
+ # If our database is strict about IP addresses, use NULL instead of an empty string
+ if( $dbw->strictIPs() and $this->mAttribs['rc_ip'] == '' ) {
unset( $this->mAttribs['rc_ip'] );
}
@@ -165,7 +157,7 @@ class RecentChange
$this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'rc_rc_id_seq' );
## If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
- if ( $dbw->cascadingDeletes() and $this->mAttribs['rc_cur_id']==0 ) {
+ if( $dbw->cascadingDeletes() and $this->mAttribs['rc_cur_id']==0 ) {
unset ( $this->mAttribs['rc_cur_id'] );
}
@@ -175,48 +167,9 @@ class RecentChange
# Set the ID
$this->mAttribs['rc_id'] = $dbw->insertId();
- # Update old rows, if necessary
- if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
- $lastTime = $this->mExtra['lastTimestamp'];
- #$now = $this->mAttribs['rc_timestamp'];
- #$curId = $this->mAttribs['rc_cur_id'];
-
- # Don't bother looking for entries that have probably
- # been purged, it just locks up the indexes needlessly.
- global $wgRCMaxAge;
- $age = time() - wfTimestamp( TS_UNIX, $lastTime );
- if( $age < $wgRCMaxAge ) {
- # live hack, will commit once tested - kate
- # Update rc_this_oldid for the entries which were current
- #
- #$oldid = $this->mAttribs['rc_last_oldid'];
- #$ns = $this->mAttribs['rc_namespace'];
- #$title = $this->mAttribs['rc_title'];
- #
- #$dbw->update( 'recentchanges',
- # array( /* SET */
- # 'rc_this_oldid' => $oldid
- # ), array( /* WHERE */
- # 'rc_namespace' => $ns,
- # 'rc_title' => $title,
- # 'rc_timestamp' => $dbw->timestamp( $lastTime )
- # ), $fname
- #);
- }
-
- # Update rc_cur_time
- #$dbw->update( 'recentchanges', array( 'rc_cur_time' => $now ),
- # array( 'rc_cur_id' => $curId ), $fname );
- }
-
# Notify external application via UDP
- if ( $wgRC2UDPAddress && ( !$this->mAttribs['rc_bot'] || !$wgRC2UDPOmitBots ) ) {
- $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
- if ( $conn ) {
- $line = $wgRC2UDPPrefix . $this->getIRCLine();
- socket_sendto( $conn, $line, strlen($line), 0, $wgRC2UDPAddress, $wgRC2UDPPort );
- socket_close( $conn );
- }
+ if( $wgRC2UDPAddress && ( !$this->mAttribs['rc_bot'] || !$wgRC2UDPOmitBots ) ) {
+ self::sendToUDP( $this->getIRCLine() );
}
# E-mail notifications
@@ -246,15 +199,105 @@ class RecentChange
}
/**
+ * Send some text to UDP
+ * @param string $line
+ * @param string $prefix
+ * @param string $address
+ * @return bool success
+ */
+ public static function sendToUDP( $line, $address = '', $prefix = '' ) {
+ global $wgRC2UDPAddress, $wgRC2UDPPrefix, $wgRC2UDPPort;
+ # Assume default for standard RC case
+ $address = $address ? $address : $wgRC2UDPAddress;
+ $prefix = $prefix ? $prefix : $wgRC2UDPPrefix;
+ # Notify external application via UDP
+ if( $address ) {
+ $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
+ if( $conn ) {
+ $line = $prefix . $line;
+ wfDebug( __METHOD__ . ": sending UDP line: $line\n" );
+ socket_sendto( $conn, $line, strlen($line), 0, $address, $wgRC2UDPPort );
+ socket_close( $conn );
+ return true;
+ } else {
+ wfDebug( __METHOD__ . ": failed to create UDP socket\n" );
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Remove newlines and carriage returns
+ * @param string $line
+ * @return string
+ */
+ public static function cleanupForIRC( $text ) {
+ return str_replace(array("\n", "\r"), array("", ""), $text);
+ }
+
+ /**
* Mark a given change as patrolled
*
* @param mixed $change RecentChange or corresponding rc_id
- * @returns integer number of affected rows
+ * @param bool $auto for automatic patrol
+ * @return See doMarkPatrolled(), or null if $change is not an existing rc_id
+ */
+ public static function markPatrolled( $change, $auto = false ) {
+ $change = $change instanceof RecentChange
+ ? $change
+ : RecentChange::newFromId($change);
+ if( !$change instanceof RecentChange ) {
+ return null;
+ }
+ return $change->doMarkPatrolled( $auto );
+ }
+
+ /**
+ * Mark this RecentChange as patrolled
+ *
+ * NOTE: Can also return 'rcpatroldisabled', 'hookaborted' and 'markedaspatrollederror-noautopatrol' as errors
+ * @param bool $auto for automatic patrol
+ * @return array of permissions errors, see Title::getUserPermissionsErrors()
+ */
+ public function doMarkPatrolled( $auto = false ) {
+ global $wgUser, $wgUseRCPatrol, $wgUseNPPatrol;
+ $errors = array();
+ // If recentchanges patrol is disabled, only new pages
+ // can be patrolled
+ if( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute('rc_type') != RC_NEW ) ) {
+ $errors[] = array('rcpatroldisabled');
+ }
+ // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
+ $right = $auto ? 'autopatrol' : 'patrol';
+ $errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $wgUser ) );
+ if( !wfRunHooks('MarkPatrolled', array($this->getAttribute('rc_id'), &$wgUser, false)) ) {
+ $errors[] = array('hookaborted');
+ }
+ // Users without the 'autopatrol' right can't patrol their
+ // own revisions
+ if( $wgUser->getName() == $this->getAttribute('rc_user_text') && !$wgUser->isAllowed('autopatrol') ) {
+ $errors[] = array('markedaspatrollederror-noautopatrol');
+ }
+ if( $errors ) {
+ return $errors;
+ }
+ // If the change was patrolled already, do nothing
+ if( $this->getAttribute('rc_patrolled') ) {
+ return array();
+ }
+ // Actually set the 'patrolled' flag in RC
+ $this->reallyMarkPatrolled();
+ // Log this patrol event
+ PatrolLog::record( $this, $auto );
+ wfRunHooks( 'MarkPatrolledComplete', array($this->getAttribute('rc_id'), &$wgUser, false) );
+ return array();
+ }
+
+ /**
+ * Mark this RecentChange patrolled, without error checking
+ * @return int Number of affected rows
*/
- public static function markPatrolled( $change ) {
- $rcid = $change instanceof RecentChange
- ? $change->mAttribs['rc_id']
- : $change;
+ public function reallyMarkPatrolled() {
$dbw = wfGetDB( DB_MASTER );
$dbw->update(
'recentchanges',
@@ -262,7 +305,7 @@ class RecentChange
'rc_patrolled' => 1
),
array(
- 'rc_id' => $rcid
+ 'rc_id' => $this->getAttribute('rc_id')
),
__METHOD__
);
@@ -270,13 +313,12 @@ class RecentChange
}
# Makes an entry in the database corresponding to an edit
- public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment,
- $oldId, $lastTimestamp, $bot, $ip = '', $oldSize = 0, $newSize = 0,
- $newId = 0)
+ public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, $oldId,
+ $lastTimestamp, $bot, $ip='', $oldSize=0, $newSize=0, $newId=0, $patrol=0 )
{
- if ( !$ip ) {
+ if( !$ip ) {
$ip = wfGetIP();
- if ( !$ip ) {
+ if( !$ip ) {
$ip = '';
}
}
@@ -299,7 +341,7 @@ class RecentChange
'rc_moved_to_ns' => 0,
'rc_moved_to_title' => '',
'rc_ip' => $ip,
- 'rc_patrolled' => 0,
+ 'rc_patrolled' => intval($patrol),
'rc_new' => 0, # obsolete
'rc_old_len' => $oldSize,
'rc_new_len' => $newSize,
@@ -317,7 +359,7 @@ class RecentChange
'newSize' => $newSize,
);
$rc->save();
- return( $rc->mAttribs['rc_id'] );
+ return $rc;
}
/**
@@ -326,11 +368,11 @@ class RecentChange
* @todo Document parameters and return
*/
public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot,
- $ip='', $size = 0, $newId = 0 )
+ $ip='', $size=0, $newId=0, $patrol=0 )
{
- if ( !$ip ) {
+ if( !$ip ) {
$ip = wfGetIP();
- if ( !$ip ) {
+ if( !$ip ) {
$ip = '';
}
}
@@ -353,7 +395,7 @@ class RecentChange
'rc_moved_to_ns' => 0,
'rc_moved_to_title' => '',
'rc_ip' => $ip,
- 'rc_patrolled' => 0,
+ 'rc_patrolled' => intval($patrol),
'rc_new' => 1, # obsolete
'rc_old_len' => 0,
'rc_new_len' => $size,
@@ -371,7 +413,7 @@ class RecentChange
'newSize' => $size
);
$rc->save();
- return( $rc->mAttribs['rc_id'] );
+ return $rc;
}
# Makes an entry in the database corresponding to a rename
@@ -379,9 +421,9 @@ class RecentChange
{
global $wgRequest;
- if ( !$ip ) {
+ if( !$ip ) {
$ip = wfGetIP();
- if ( !$ip ) {
+ if( !$ip ) {
$ip = '';
}
}
@@ -431,15 +473,14 @@ class RecentChange
RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, true );
}
- # A log entry is different to an edit in that previous revisions are not kept
public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip='',
$type, $action, $target, $logComment, $params, $newId=0 )
{
global $wgRequest;
- if ( !$ip ) {
+ if( !$ip ) {
$ip = wfGetIP();
- if ( !$ip ) {
+ if( !$ip ) {
$ip = '';
}
}
@@ -458,7 +499,7 @@ class RecentChange
'rc_comment' => $logComment,
'rc_this_oldid' => 0,
'rc_last_oldid' => 0,
- 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot' , true ) : 0,
+ 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot', true ) : 0,
'rc_moved_to_ns' => 0,
'rc_moved_to_title' => '',
'rc_ip' => $ip,
@@ -481,16 +522,14 @@ class RecentChange
}
# Initialises the members of this object from a mysql row object
- function loadFromRow( $row )
- {
+ public function loadFromRow( $row ) {
$this->mAttribs = get_object_vars( $row );
- $this->mAttribs["rc_timestamp"] = wfTimestamp(TS_MW, $this->mAttribs["rc_timestamp"]);
- $this->mExtra = array();
+ $this->mAttribs['rc_timestamp'] = wfTimestamp(TS_MW, $this->mAttribs['rc_timestamp']);
+ $this->mAttribs['rc_deleted'] = $row->rc_deleted; // MUST be set
}
# Makes a pseudo-RC entry from a cur row
- function loadFromCurRow( $row )
- {
+ public function loadFromCurRow( $row ) {
$this->mAttribs = array(
'rc_timestamp' => wfTimestamp(TS_MW, $row->rev_timestamp),
'rc_cur_time' => $row->rev_timestamp,
@@ -517,11 +556,8 @@ class RecentChange
'rc_log_type' => isset($row->rc_log_type) ? $row->rc_log_type : null,
'rc_log_action' => isset($row->rc_log_action) ? $row->rc_log_action : null,
'rc_log_id' => isset($row->rc_log_id) ? $row->rc_log_id: 0,
- // this one REALLY should be set...
- 'rc_deleted' => isset($row->rc_deleted) ? $row->rc_deleted: 0,
+ 'rc_deleted' => $row->rc_deleted // MUST be set
);
-
- $this->mExtra = array();
}
/**
@@ -538,12 +574,11 @@ class RecentChange
* Gets the end part of the diff URL associated with this object
* Blank if no diff link should be displayed
*/
- function diffLinkTrail( $forceCur )
- {
- if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
+ public function diffLinkTrail( $forceCur ) {
+ if( $this->mAttribs['rc_type'] == RC_EDIT ) {
$trail = "curid=" . (int)($this->mAttribs['rc_cur_id']) .
"&oldid=" . (int)($this->mAttribs['rc_last_oldid']);
- if ( $forceCur ) {
+ if( $forceCur ) {
$trail .= '&diff=0' ;
} else {
$trail .= '&diff=' . (int)($this->mAttribs['rc_this_oldid']);
@@ -554,44 +589,45 @@ class RecentChange
return $trail;
}
- function cleanupForIRC( $text ) {
- return str_replace(array("\n", "\r"), array("", ""), $text);
- }
-
- function getIRCLine() {
- global $wgUseRCPatrol;
+ protected function getIRCLine() {
+ global $wgUseRCPatrol, $wgUseNPPatrol, $wgRC2UDPInterwikiPrefix, $wgLocalInterwiki;
// FIXME: Would be good to replace these 2 extract() calls with something more explicit
// e.g. list ($rc_type, $rc_id) = array_values ($this->mAttribs); [or something like that]
extract($this->mAttribs);
extract($this->mExtra);
- if ( $rc_type == RC_LOG ) {
+ if( $rc_type == RC_LOG ) {
$titleObj = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL );
} else {
$titleObj =& $this->getTitle();
}
$title = $titleObj->getPrefixedText();
- $title = $this->cleanupForIRC( $title );
+ $title = self::cleanupForIRC( $title );
- // FIXME: *HACK* these should be getFullURL(), hacked for SSL madness --brion 2005-12-26
- if ( $rc_type == RC_LOG ) {
+ if( $rc_type == RC_LOG ) {
$url = '';
- } elseif ( $rc_new && $wgUseRCPatrol ) {
- $url = $titleObj->getInternalURL("rcid=$rc_id");
- } else if ( $rc_new ) {
- $url = $titleObj->getInternalURL();
- } else if ( $wgUseRCPatrol ) {
- $url = $titleObj->getInternalURL("diff=$rc_this_oldid&oldid=$rc_last_oldid&rcid=$rc_id");
} else {
- $url = $titleObj->getInternalURL("diff=$rc_this_oldid&oldid=$rc_last_oldid");
+ if( $rc_type == RC_NEW ) {
+ $url = "oldid=$rc_this_oldid";
+ } else {
+ $url = "diff=$rc_this_oldid&oldid=$rc_last_oldid";
+ }
+ if( $wgUseRCPatrol || ($rc_type == RC_NEW && $wgUseNPPatrol) ) {
+ $url .= "&rcid=$rc_id";
+ }
+ // XXX: *HACK* this should use getFullURL(), hacked for SSL madness --brion 2005-12-26
+ // XXX: *HACK^2* the preg_replace() undoes much of what getInternalURL() does, but we
+ // XXX: need to call it so that URL paths on the Wikimedia secure server can be fixed
+ // XXX: by a custom GetInternalURL hook --vyznev 2008-12-10
+ $url = preg_replace( '/title=[^&]*&/', '', $titleObj->getInternalURL( $url ) );
}
- if ( isset( $oldSize ) && isset( $newSize ) ) {
+ if( isset( $oldSize ) && isset( $newSize ) ) {
$szdiff = $newSize - $oldSize;
- if ($szdiff < -500) {
+ if($szdiff < -500) {
$szdiff = "\002$szdiff\002";
- } elseif ($szdiff >= 0) {
+ } elseif($szdiff >= 0) {
$szdiff = '+' . $szdiff ;
}
$szdiff = '(' . $szdiff . ')' ;
@@ -599,20 +635,35 @@ class RecentChange
$szdiff = '';
}
- $user = $this->cleanupForIRC( $rc_user_text );
+ $user = self::cleanupForIRC( $rc_user_text );
- if ( $rc_type == RC_LOG ) {
- $logTargetText = $this->getTitle()->getPrefixedText();
- $comment = $this->cleanupForIRC( str_replace($logTargetText,"\00302$logTargetText\00310",$actionComment) );
+ if( $rc_type == RC_LOG ) {
+ $targetText = $this->getTitle()->getPrefixedText();
+ $comment = self::cleanupForIRC( str_replace("[[$targetText]]","[[\00302$targetText\00310]]",$actionComment) );
$flag = $rc_log_action;
} else {
- $comment = $this->cleanupForIRC( $rc_comment );
+ $comment = self::cleanupForIRC( $rc_comment );
$flag = ($rc_new ? "N" : "") . ($rc_minor ? "M" : "") . ($rc_bot ? "B" : "");
}
+
+ if ( $wgRC2UDPInterwikiPrefix === true ) {
+ $prefix = $wgLocalInterwiki;
+ } elseif ( $wgRC2UDPInterwikiPrefix ) {
+ $prefix = $wgRC2UDPInterwikiPrefix;
+ } else {
+ $prefix = false;
+ }
+ if ( $prefix !== false ) {
+ $titleString = "\00314[[\00303$prefix:\00307$title\00314]]";
+ } else {
+ $titleString = "\00314[[\00307$title\00314]]";
+ }
+
# see http://www.irssi.org/documentation/formats for some colour codes. prefix is \003,
# no colour (\003) switches back to the term default
- $fullString = "\00314[[\00307$title\00314]]\0034 $flag\00310 " .
+ $fullString = "$titleString\0034 $flag\00310 " .
"\00302$url\003 \0035*\003 \00303$user\003 \0035*\003 $szdiff \00310$comment\003\n";
+
return $fullString;
}
@@ -620,32 +671,16 @@ class RecentChange
* Returns the change size (HTML).
* The lengths can be given optionally.
*/
- function getCharacterDifference( $old = 0, $new = 0 ) {
- global $wgRCChangedSizeThreshold, $wgLang;
-
+ public function getCharacterDifference( $old = 0, $new = 0 ) {
if( $old === 0 ) {
$old = $this->mAttribs['rc_old_len'];
}
if( $new === 0 ) {
$new = $this->mAttribs['rc_new_len'];
}
-
if( $old === NULL || $new === NULL ) {
return '';
}
-
- $szdiff = $new - $old;
- $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape'),
- $wgLang->formatNum($szdiff) );
-
- if( $szdiff < $wgRCChangedSizeThreshold ) {
- return '<strong class=\'mw-plusminus-neg\'>(' . $formatedSize . ')</strong>';
- } elseif( $szdiff === 0 ) {
- return '<span class=\'mw-plusminus-null\'>(' . $formatedSize . ')</span>';
- } elseif( $szdiff > 0 ) {
- return '<span class=\'mw-plusminus-pos\'>(+' . $formatedSize . ')</span>';
- } else {
- return '<span class=\'mw-plusminus-neg\'>(' . $formatedSize . ')</span>';
- }
+ return ChangesList::showCharacterDifference( $old, $new );
}
}
diff --git a/includes/RefreshLinksJob.php b/includes/RefreshLinksJob.php
index f95e5a50..1c119a8d 100644
--- a/includes/RefreshLinksJob.php
+++ b/includes/RefreshLinksJob.php
@@ -47,3 +47,87 @@ class RefreshLinksJob extends Job {
return true;
}
}
+
+/**
+ * Background job to update links for a given title.
+ * Newer version for high use templates.
+ *
+ * @ingroup JobQueue
+ */
+class RefreshLinksJob2 extends Job {
+
+ function __construct( $title, $params, $id = 0 ) {
+ parent::__construct( 'refreshLinks2', $title, $params, $id );
+ }
+
+ /**
+ * Run a refreshLinks2 job
+ * @return boolean success
+ */
+ function run() {
+ global $wgParser;
+
+ wfProfileIn( __METHOD__ );
+
+ $linkCache = LinkCache::singleton();
+ $linkCache->clear();
+
+ if( is_null( $this->title ) ) {
+ $this->error = "refreshLinks2: Invalid title";
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ if( !isset($this->params['start']) || !isset($this->params['end']) ) {
+ $this->error = "refreshLinks2: Invalid params";
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ $start = intval($this->params['start']);
+ $end = intval($this->params['end']);
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( array( 'templatelinks', 'page' ),
+ array( 'page_namespace', 'page_title' ),
+ array(
+ 'page_id=tl_from',
+ "tl_from >= '$start'",
+ "tl_from <= '$end'",
+ 'tl_namespace' => $this->title->getNamespace(),
+ 'tl_title' => $this->title->getDBkey()
+ ), __METHOD__
+ );
+
+ # Not suitable for page load triggered job running!
+ # Gracefully switch to refreshLinks jobs if this happens.
+ if( php_sapi_name() != 'cli' ) {
+ $jobs = array();
+ while( $row = $dbr->fetchObject( $res ) ) {
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $jobs[] = new RefreshLinksJob( $title, '' );
+ }
+ Job::batchInsert( $jobs );
+ return true;
+ }
+ # Re-parse each page that transcludes this page and update their tracking links...
+ while( $row = $dbr->fetchObject( $res ) ) {
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $revision = Revision::newFromTitle( $title );
+ if ( !$revision ) {
+ $this->error = 'refreshLinks: Article not found "' . $title->getPrefixedDBkey() . '"';
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ wfProfileIn( __METHOD__.'-parse' );
+ $options = new ParserOptions;
+ $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() );
+ wfProfileOut( __METHOD__.'-parse' );
+ wfProfileIn( __METHOD__.'-update' );
+ $update = new LinksUpdate( $title, $parserOutput, false );
+ $update->doUpdate();
+ wfProfileOut( __METHOD__.'-update' );
+ wfProfileOut( __METHOD__ );
+ }
+
+ return true;
+ }
+}
diff --git a/includes/Revision.php b/includes/Revision.php
index d0ccb46d..7938d88a 100644
--- a/includes/Revision.php
+++ b/includes/Revision.php
@@ -13,6 +13,11 @@ class Revision {
const DELETED_USER = 4;
const DELETED_RESTRICTED = 8;
+ // Audience options for Revision::getText()
+ const FOR_PUBLIC = 1;
+ const FOR_THIS_USER = 2;
+ const RAW = 3;
+
/**
* Load a page revision from a given revision ID number.
* Returns null if no such revision can be found.
@@ -37,16 +42,24 @@ class Revision {
* @return Revision
*/
public static function newFromTitle( $title, $id = 0 ) {
- if( $id ) {
- $matchId = intval( $id );
+ $conds = array(
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
+ );
+ if ( $id ) {
+ // Use the specified ID
+ $conds['rev_id'] = $id;
+ } elseif ( wfGetLB()->getServerCount() > 1 ) {
+ // Get the latest revision ID from the master
+ $dbw = wfGetDB( DB_MASTER );
+ $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
+ $conds['rev_id'] = $latest;
} else {
- $matchId = 'page_latest';
+ // Use a join to get the latest revision
+ $conds[] = 'rev_id=page_latest';
}
- return Revision::newFromConds(
- array( "rev_id=$matchId",
- 'page_id=rev_page',
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey() ) );
+ $conds[] = 'page_id=rev_page';
+ return Revision::newFromConds( $conds );
}
/**
@@ -144,7 +157,7 @@ class Revision {
private static function newFromConds( $conditions ) {
$db = wfGetDB( DB_SLAVE );
$row = Revision::loadFromConds( $db, $conditions );
- if( is_null( $row ) ) {
+ if( is_null( $row ) && wfGetLB()->getServerCount() > 1 ) {
$dbw = wfGetDB( DB_MASTER );
$row = Revision::loadFromConds( $dbw, $conditions );
}
@@ -232,7 +245,7 @@ class Revision {
array( 'page', 'revision' ),
$fields,
$conditions,
- 'Revision::fetchRow',
+ __METHOD__,
array( 'LIMIT' => 1 ) );
$ret = $db->resultObject( $res );
return $ret;
@@ -306,9 +319,9 @@ class Revision {
$this->mSize = intval( $row->rev_len );
if( isset( $row->page_latest ) ) {
- $this->mCurrent = ( $row->rev_id == $row->page_latest );
- $this->mTitle = Title::makeTitle( $row->page_namespace,
- $row->page_title );
+ $this->mCurrent = ( $row->rev_id == $row->page_latest );
+ $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $this->mTitle->resetArticleID( $this->mPage );
} else {
$this->mCurrent = false;
$this->mTitle = null;
@@ -427,11 +440,22 @@ class Revision {
}
/**
- * Fetch revision's user id if it's available to all users
+ * Fetch revision's user id if it's available to the specified audience.
+ * If the specified audience does not have access to it, zero will be
+ * returned.
+ *
+ * @param integer $audience One of:
+ * Revision::FOR_PUBLIC to be displayed to all users
+ * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::RAW get the ID regardless of permissions
+ *
+ *
* @return int
*/
- public function getUser() {
- if( $this->isDeleted( self::DELETED_USER ) ) {
+ public function getUser( $audience = self::FOR_PUBLIC ) {
+ if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
+ return 0;
+ } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER ) ) {
return 0;
} else {
return $this->mUser;
@@ -447,11 +471,21 @@ class Revision {
}
/**
- * Fetch revision's username if it's available to all users
+ * Fetch revision's username if it's available to the specified audience.
+ * If the specified audience does not have access to the username, an
+ * empty string will be returned.
+ *
+ * @param integer $audience One of:
+ * Revision::FOR_PUBLIC to be displayed to all users
+ * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::RAW get the text regardless of permissions
+ *
* @return string
*/
- public function getUserText() {
- if( $this->isDeleted( self::DELETED_USER ) ) {
+ public function getUserText( $audience = self::FOR_PUBLIC ) {
+ if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
+ return "";
+ } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER ) ) {
return "";
} else {
return $this->mUserText;
@@ -467,11 +501,21 @@ class Revision {
}
/**
- * Fetch revision comment if it's available to all users
+ * Fetch revision comment if it's available to the specified audience.
+ * If the specified audience does not have access to the comment, an
+ * empty string will be returned.
+ *
+ * @param integer $audience One of:
+ * Revision::FOR_PUBLIC to be displayed to all users
+ * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::RAW get the text regardless of permissions
+ *
* @return string
*/
- function getComment() {
- if( $this->isDeleted( self::DELETED_COMMENT ) ) {
+ function getComment( $audience = self::FOR_PUBLIC ) {
+ if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
+ return "";
+ } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT ) ) {
return "";
} else {
return $this->mComment;
@@ -500,13 +544,31 @@ class Revision {
public function isDeleted( $field ) {
return ($this->mDeleted & $field) == $field;
}
+
+ /**
+ * Get the deletion bitfield of the revision
+ */
+ public function getVisibility() {
+ return (int)$this->mDeleted;
+ }
/**
- * Fetch revision text if it's available to all users
+ * Fetch revision text if it's available to the specified audience.
+ * If the specified audience does not have the ability to view this
+ * revision, an empty string will be returned.
+ *
+ * @param integer $audience One of:
+ * Revision::FOR_PUBLIC to be displayed to all users
+ * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::RAW get the text regardless of permissions
+ *
+ *
* @return string
*/
- public function getText() {
- if( $this->isDeleted( self::DELETED_TEXT ) ) {
+ public function getText( $audience = self::FOR_PUBLIC ) {
+ if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
+ return "";
+ } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT ) ) {
return "";
} else {
return $this->getRawText();
@@ -514,6 +576,13 @@ class Revision {
}
/**
+ * Alias for getText(Revision::FOR_THIS_USER)
+ */
+ public function revText() {
+ return $this->getText( self::FOR_THIS_USER );
+ }
+
+ /**
* Fetch revision text without regard for view restrictions
* @return string
*/
@@ -526,18 +595,6 @@ class Revision {
}
/**
- * Fetch revision text if it's available to THIS user
- * @return string
- */
- public function revText() {
- if( !$this->userCan( self::DELETED_TEXT ) ) {
- return "";
- } else {
- return $this->getRawText();
- }
- }
-
- /**
* @return string
*/
public function getTimestamp() {
@@ -607,7 +664,7 @@ class Revision {
* $row is usually an object from wfFetchRow(), both the flags and the text
* field must be included
*
- * @param integer $row Id of a row
+ * @param object $row The text data
* @param string $prefix table prefix (default 'old_')
* @return string $text|false the text requested
*/
@@ -663,9 +720,11 @@ class Revision {
}
global $wgLegacyEncoding;
- if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) ) {
+ if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) ) {
# Old revisions kept around in a legacy encoding?
# Upconvert on demand.
+ # ("utf8" checked for compatibility with some broken
+ # conversion scripts 2008-12-30)
global $wgInputEncoding, $wgContLang;
$text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding, $text );
}
@@ -719,20 +778,13 @@ class Revision {
$flags = Revision::compressRevisionText( $data );
# Write to external storage if required
- if ( $wgDefaultExternalStore ) {
- if ( is_array( $wgDefaultExternalStore ) ) {
- // Distribute storage across multiple clusters
- $store = $wgDefaultExternalStore[mt_rand(0, count( $wgDefaultExternalStore ) - 1)];
- } else {
- $store = $wgDefaultExternalStore;
- }
+ if( $wgDefaultExternalStore ) {
// Store and get the URL
- $data = ExternalStore::insert( $store, $data );
- if ( !$data ) {
- # This should only happen in the case of a configuration error, where the external store is not valid
- throw new MWException( "Unable to store text to external storage $store" );
+ $data = ExternalStore::insertToDefault( $data );
+ if( !$data ) {
+ throw new MWException( "Unable to store text to external storage" );
}
- if ( $flags ) {
+ if( $flags ) {
$flags .= ',';
}
$flags .= 'external';
@@ -816,7 +868,7 @@ class Revision {
__METHOD__ );
}
- if( !$row ) {
+ if( !$row && wfGetLB()->getServerCount() > 1 ) {
// Possible slave lag!
$dbw = wfGetDB( DB_MASTER );
$row = $dbw->selectRow( 'text',
@@ -827,7 +879,8 @@ class Revision {
$text = self::getRevisionText( $row );
- if( $wgRevisionCacheExpiry ) {
+ # No negative caching -- negative hits on text rows may be due to corrupted slave servers
+ if( $wgRevisionCacheExpiry && $text !== false ) {
$wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
}
@@ -855,7 +908,7 @@ class Revision {
$current = $dbw->selectRow(
array( 'page', 'revision' ),
- array( 'page_latest', 'rev_text_id' ),
+ array( 'page_latest', 'rev_text_id', 'rev_len' ),
array(
'page_id' => $pageId,
'page_latest=rev_id',
@@ -868,7 +921,8 @@ class Revision {
'comment' => $summary,
'minor_edit' => $minor,
'text_id' => $current->rev_text_id,
- 'parent_id' => $current->page_latest
+ 'parent_id' => $current->page_latest,
+ 'len' => $current->rev_len
) );
} else {
$revision = null;
@@ -902,17 +956,15 @@ class Revision {
/**
* Get rev_timestamp from rev_id, without loading the rest of the row
+ * @param Title $title
* @param integer $id
- * @param integer $pageid, optional
*/
- static function getTimestampFromId( $id, $pageId = 0 ) {
+ static function getTimestampFromId( $title, $id ) {
$dbr = wfGetDB( DB_SLAVE );
$conds = array( 'rev_id' => $id );
- if( $pageId ) {
- $conds['rev_page'] = $pageId;
- }
+ $conds['rev_page'] = $title->getArticleId();
$timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
- if ( $timestamp === false ) {
+ if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) {
# Not in slave, try master
$dbw = wfGetDB( DB_MASTER );
$timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php
index 28b1c275..5d58b036 100644
--- a/includes/Sanitizer.php
+++ b/includes/Sanitizer.php
@@ -331,9 +331,6 @@ $wgHtmlEntityAliases = array(
* @ingroup Parser
*/
class Sanitizer {
- const NONE = 0;
- const INITIAL_NONLETTER = 1;
-
/**
* Cleans up HTML, removes dangerous tags and attributes, and
* removes HTML comments
@@ -616,8 +613,11 @@ class Sanitizer {
}
}
- if ( $attribute === 'id' )
- $value = Sanitizer::escapeId( $value );
+ if ( $attribute === 'id' ) {
+ global $wgEnforceHtmlIds;
+ $value = Sanitizer::escapeId( $value,
+ $wgEnforceHtmlIds ? 'noninitial' : 'xml' );
+ }
// If this attribute was previously set, override it.
// Output should only have one attribute of each name.
@@ -627,10 +627,9 @@ 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.
+ * 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 (if they're both strings).
*
* @todo implement merging for other attributes such as style
* @param array $a
@@ -639,16 +638,12 @@ class Sanitizer {
*/
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 ) ) );
+ if( isset( $a['class'] ) && isset( $b['class'] )
+ && is_string( $a['class'] ) && is_string( $b['class'] )
+ && $a['class'] !== $b['class'] ) {
+ $classes = preg_split( '/\s+/', "{$a['class']} {$b['class']}",
+ -1, PREG_SPLIT_NO_EMPTY );
+ $out['class'] = implode( ' ', array_unique( $classes ) );
}
return $out;
}
@@ -782,28 +777,55 @@ class Sanitizer {
* name attributes
* @see http://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with the id attribute
*
- * @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.
+ * @param string $id Id to validate
+ * @param mixed $options String or array of strings (default is array()):
+ * 'noninitial': This is a non-initial fragment of an id, not a full id,
+ * so don't pay attention if the first character isn't valid at the
+ * beginning of an id.
+ * 'xml': Don't restrict the id to be HTML4-compatible. This option
+ * allows any alphabetic character to be used, per the XML standard.
+ * Therefore, it also completely changes the type of escaping: instead
+ * of weird dot-encoding, runs of invalid characters (mostly
+ * whitespace) are just compressed into a single underscore.
* @return string
*/
- static function escapeId( $id, $flags = Sanitizer::INITIAL_NONLETTER ) {
- static $replace = array(
- '%3A' => ':',
- '%' => '.'
- );
-
- $id = urlencode( Sanitizer::decodeCharReferences( strtr( $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";
+ static function escapeId( $id, $options = array() ) {
+ $options = (array)$options;
+
+ if ( !in_array( 'xml', $options ) ) {
+ # HTML4-style escaping
+ static $replace = array(
+ '%3A' => ':',
+ '%' => '.'
+ );
+
+ $id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) );
+ $id = str_replace( array_keys( $replace ), array_values( $replace ), $id );
+
+ if ( !preg_match( '/^[a-zA-Z]/', $id )
+ && !in_array( 'noninitial', $options ) ) {
+ // Initial character must be a letter!
+ $id = "x$id";
+ }
+ return $id;
+ }
+
+ # XML-style escaping. For the patterns used, see the XML 1.0 standard,
+ # 5th edition, NameStartChar and NameChar: <http://www.w3.org/TR/REC-xml/>
+ $nameStartChar = ':a-zA-Z_\xC0-\xD6\xD8-\xF6\xF8-\x{2FF}\x{370}-\x{37D}'
+ . '\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}'
+ . '\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}';
+ $nameChar = $nameStartChar . '.\-0-9\xB7\x{0300}-\x{036F}'
+ . '\x{203F}-\x{2040}';
+ # Replace _ as well so we don't get multiple consecutive underscores
+ $id = preg_replace( "/([^$nameChar]|_)+/u", '_', $id );
+ $id = trim( $id, '_' );
+
+ if ( !preg_match( "/^[$nameStartChar]/u", $id )
+ && !in_array( 'noninitial', $options ) ) {
+ $id = "_$id";
}
+
return $id;
}
@@ -827,6 +849,22 @@ class Sanitizer {
}
/**
+ * Given HTML input, escape with htmlspecialchars but un-escape entites.
+ * This allows (generally harmless) entities like &nbsp; to survive.
+ *
+ * @param string $html String to escape
+ * @return string Escaped input
+ */
+ static function escapeHtmlAllowEntities( $html ) {
+ # It seems wise to escape ' as well as ", as a matter of course. Can't
+ # hurt.
+ $html = htmlspecialchars( $html, ENT_QUOTES );
+ $html = str_replace( '&amp;', '&', $html );
+ $html = Sanitizer::normalizeCharReferences( $html );
+ return $html;
+ }
+
+ /**
* Regex replace callback for armoring links against further processing.
* @param array $matches
* @return string
@@ -844,7 +882,7 @@ class Sanitizer {
* @param string
* @return array
*/
- static function decodeTagAttributes( $text ) {
+ public static function decodeTagAttributes( $text ) {
$attribs = array();
if( trim( $text ) == '' ) {
@@ -1111,7 +1149,8 @@ class Sanitizer {
}
/**
- * @todo Document it a bit
+ * Foreach array key (an allowed HTML element), return an array
+ * of allowed attributes
* @return array
*/
static function setupAttributeWhitelist() {
@@ -1301,7 +1340,7 @@ class Sanitizer {
return $out;
}
- static function cleanUrl( $url, $hostname=true ) {
+ static function cleanUrl( $url ) {
# Normalize any HTML entities in input. They will be
# re-escaped by makeExternalLink().
$url = Sanitizer::decodeCharReferences( $url );
diff --git a/includes/SearchEngine.php b/includes/SearchEngine.php
index edd93cce..3ea0341d 100644
--- a/includes/SearchEngine.php
+++ b/includes/SearchEngine.php
@@ -13,6 +13,7 @@
class SearchEngine {
var $limit = 10;
var $offset = 0;
+ var $prefix = '';
var $searchTerms = array();
var $namespaces = array( NS_MAIN );
var $showRedirects = false;
@@ -43,6 +44,19 @@ class SearchEngine {
return null;
}
+ /** If this search backend can list/unlist redirects */
+ function acceptListRedirects() {
+ return true;
+ }
+
+ /**
+ * Transform search term in cases when parts of the query came as different GET params (when supported)
+ * e.g. for prefix queries: search=test&prefix=Main_Page/Archive -> test prefix:Main Page/Archive
+ */
+ function transformSearchTerm( $term ) {
+ return $term;
+ }
+
/**
* If an exact title match can be find, or a very slightly close match,
* return the title. If no match, returns NULL.
@@ -98,19 +112,6 @@ class SearchEngine {
return $title;
}
- global $wgCapitalLinks, $wgContLang;
- if( !$wgCapitalLinks ) {
- // Catch differs-by-first-letter-case-only
- $title = Title::newFromText( $wgContLang->ucfirst( $term ) );
- if ( $title && $title->exists() ) {
- return $title;
- }
- $title = Title::newFromText( $wgContLang->lcfirst( $term ) );
- if ( $title && $title->exists() ) {
- return $title;
- }
- }
-
// Give hooks a chance at better match variants
$title = null;
if( !wfRunHooks( 'SearchGetNearMatch', array( $term, &$title ) ) ) {
@@ -135,7 +136,7 @@ class SearchEngine {
# Go to images that exist even if there's no local page.
# 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 ) {
+ if( $title->getNamespace() == NS_FILE ) {
$image = wfFindFile( $title );
if( $image ) {
return $title;
@@ -158,7 +159,7 @@ class SearchEngine {
}
public static function legalSearchChars() {
- return "A-Za-z_'0-9\\x80-\\xFF\\-";
+ return "A-Za-z_'.0-9\\x80-\\xFF\\-";
}
/**
@@ -275,7 +276,51 @@ class SearchEngine {
return array_keys($wgNamespacesToBeSearchedDefault, true);
}
-
+
+ /**
+ * Get a list of namespace names useful for showing in tooltips
+ * and preferences
+ *
+ * @param unknown_type $namespaces
+ */
+ public static function namespacesAsText( $namespaces ){
+ global $wgContLang;
+
+ $formatted = array_map( array($wgContLang,'getFormattedNsText'), $namespaces );
+ foreach( $formatted as $key => $ns ){
+ if ( empty($ns) )
+ $formatted[$key] = wfMsg( 'blanknamespace' );
+ }
+ return $formatted;
+ }
+
+ /**
+ * An array of "project" namespaces indexes typically searched
+ * by logged-in users
+ *
+ * @return array
+ * @static
+ */
+ public static function projectNamespaces() {
+ global $wgNamespacesToBeSearchedDefault, $wgNamespacesToBeSearchedProject;
+
+ return array_keys( $wgNamespacesToBeSearchedProject, true );
+ }
+
+ /**
+ * An array of "project" namespaces indexes typically searched
+ * by logged-in users in addition to the default namespaces
+ *
+ * @return array
+ * @static
+ */
+ public static function defaultAndProjectNamespaces() {
+ global $wgNamespacesToBeSearchedDefault, $wgNamespacesToBeSearchedProject;
+
+ return array_keys( $wgNamespacesToBeSearchedDefault +
+ $wgNamespacesToBeSearchedProject, true);
+ }
+
/**
* Return a 'cleaned up' search string
*
@@ -290,24 +335,17 @@ class SearchEngine {
* Load up the appropriate search engine class for the currently
* active database backend, and return a configured instance.
*
- * @fixme Ask the database class for his default search class
- * instead of knowing about every backend here.
* @return SearchEngine
*/
public static function create() {
- global $wgDBtype, $wgSearchType;
+ global $wgSearchType;
+ $dbr = wfGetDB( DB_SLAVE );
if( $wgSearchType ) {
$class = $wgSearchType;
- } elseif( $wgDBtype == 'mysql' ) {
- $class = 'SearchMySQL';
- } else if ( $wgDBtype == 'postgres' ) {
- $class = 'SearchPostgres';
- } else if ( $wgDBtype == 'oracle' ) {
- $class = 'SearchOracle';
} else {
- $class = 'SearchEngineDummy';
+ $class = $dbr->getSearchEngine();
}
- $search = new $class( wfGetDB( DB_SLAVE ) );
+ $search = new $class( $dbr );
$search->setLimitOffset(0,0);
return $search;
}
@@ -345,11 +383,11 @@ class SearchEngine {
*/
public static function getOpenSearchTemplate() {
global $wgOpenSearchTemplate, $wgServer, $wgScriptPath;
- if($wgOpenSearchTemplate)
+ if( $wgOpenSearchTemplate ) {
return $wgOpenSearchTemplate;
- else{
- $ns = implode(',',SearchEngine::defaultNamespaces());
- if(!$ns) $ns = "0";
+ } else {
+ $ns = implode( '|', SearchEngine::defaultNamespaces() );
+ if( !$ns ) $ns = "0";
return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace='.$ns;
}
}
@@ -432,7 +470,7 @@ class SearchResultSet {
}
/**
- * @return string highlighted suggested query, '' if none
+ * @return string HTML highlighted suggested query, '' if none
*/
function getSuggestionSnippet(){
return '';
@@ -503,11 +541,15 @@ class SearchResultTooMany {
*/
class SearchResult {
var $mRevision = null;
+ var $mImage = null;
- function SearchResult( $row ) {
+ function __construct( $row ) {
$this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title );
- if( !is_null($this->mTitle) )
+ if( !is_null($this->mTitle) ){
$this->mRevision = Revision::newFromTitle( $this->mTitle );
+ if( $this->mTitle->getNamespace() === NS_FILE )
+ $this->mImage = wfFindFile( $this->mTitle );
+ }
}
/**
@@ -529,9 +571,7 @@ class SearchResult {
* @access public
*/
function isMissingRevision(){
- if( !$this->mRevision )
- return true;
- return false;
+ return !$this->mRevision && !$this->mImage;
}
/**
@@ -554,7 +594,11 @@ class SearchResult {
*/
protected function initText(){
if( !isset($this->mText) ){
- $this->mText = $this->mRevision->getText();
+ if($this->mRevision != null)
+ $this->mText = $this->mRevision->getText();
+ else // TODO: can we fetch raw wikitext for commons images?
+ $this->mText = '';
+
}
}
@@ -614,7 +658,11 @@ class SearchResult {
* @return string timestamp
*/
function getTimestamp(){
- return $this->mRevision->getTimestamp();
+ if( $this->mRevision )
+ return $this->mRevision->getTimestamp();
+ else if( $this->mImage )
+ return $this->mImage->getTimestamp();
+ return '';
}
/**
@@ -706,7 +754,7 @@ class SearchHighlighter {
if($key == 2){
// see if this is an image link
$ns = substr($val[0],2,-1);
- if( $wgContLang->getNsIndex($ns) != NS_IMAGE )
+ if( $wgContLang->getNsIndex($ns) != NS_FILE )
break;
}
@@ -761,13 +809,12 @@ class SearchHighlighter {
// prepare regexps
foreach( $terms as $index => $term ) {
- $terms[$index] = preg_quote( $term, '/' );
// manually do upper/lowercase stuff for utf-8 since PHP won't do it
if(preg_match('/[\x80-\xff]/', $term) ){
$terms[$index] = preg_replace_callback('/./us',array($this,'caseCallback'),$terms[$index]);
+ } else {
+ $terms[$index] = $term;
}
-
-
}
$anyterm = implode( '|', $terms );
$phrase = implode("$wgSearchHighlightBoundaries+", $terms );
@@ -1077,7 +1124,7 @@ class SearchHighlighter {
global $wgContLang;
$ns = substr( $matches[1], 0, $colon );
$index = $wgContLang->getNsIndex($ns);
- if( $index !== false && ($index == NS_IMAGE || $index == NS_CATEGORY) )
+ if( $index !== false && ($index == NS_FILE || $index == NS_CATEGORY) )
return $matches[0]; // return the whole thing
else
return $matches[2];
@@ -1097,11 +1144,10 @@ class SearchHighlighter {
public function highlightSimple( $text, $terms, $contextlines, $contextchars ) {
global $wgLang, $wgContLang;
$fname = __METHOD__;
-
+
$lines = explode( "\n", $text );
$terms = implode( '|', $terms );
- $terms = str_replace( '/', "\\/", $terms);
$max = intval( $contextchars ) + 1;
$pat1 = "/(.*)($terms)(.{0,$max})/i";
diff --git a/includes/SearchMySQL.php b/includes/SearchMySQL.php
index f9b71c8e..5fc06790 100644
--- a/includes/SearchMySQL.php
+++ b/includes/SearchMySQL.php
@@ -34,7 +34,10 @@ class SearchMySQL extends SearchEngine {
$this->db = $db;
}
- /** @todo document */
+ /**
+ * Parse the user's query and transform it into an SQL fragment which will
+ * become part of a WHERE clause
+ */
function parseQuery( $filteredText, $fulltext ) {
global $wgContLang;
$lc = SearchEngine::legalSearchChars(); // Minus format chars
@@ -54,7 +57,11 @@ class SearchMySQL extends SearchEngine {
if( !empty( $terms[3] ) ) {
// Match individual terms in result highlighting...
$regexp = preg_quote( $terms[3], '/' );
- if( $terms[4] ) $regexp .= "[0-9A-Za-z_]+";
+ if( $terms[4] ) {
+ $regexp = "\b$regexp"; // foo*
+ } else {
+ $regexp = "\b$regexp\b";
+ }
} else {
// Match the quoted term in result highlighting...
$regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' );
@@ -122,9 +129,10 @@ class SearchMySQL extends SearchEngine {
function queryNamespaces() {
if( is_null($this->namespaces) )
return ''; # search all
- $namespaces = implode( ',', $this->namespaces );
- if ($namespaces == '') {
+ if ( !count( $this->namespaces ) ) {
$namespaces = '0';
+ } else {
+ $namespaces = $this->db->makeList( $this->namespaces );
}
return 'AND page_namespace IN (' . $namespaces . ')';
}
diff --git a/includes/SearchOracle.php b/includes/SearchOracle.php
index bf9368d1..b48d5e6e 100644
--- a/includes/SearchOracle.php
+++ b/includes/SearchOracle.php
@@ -77,9 +77,10 @@ class SearchOracle extends SearchEngine {
function queryNamespaces() {
if( is_null($this->namespaces) )
return '';
- $namespaces = implode(',', $this->namespaces);
- if ($namespaces == '') {
+ if ( !count( $this->namespaces ) ) {
$namespaces = '0';
+ } else {
+ $namespaces = $this->db->makeList( $this->namespaces );
}
return 'AND page_namespace IN (' . $namespaces . ')';
}
@@ -144,7 +145,10 @@ class SearchOracle extends SearchEngine {
'WHERE page_id=si_page AND ' . $match;
}
- /** @todo document */
+ /**
+ * Parse a user input search string, and return an SQL fragment to be used
+ * as part of a WHERE clause
+ */
function parseQuery($filteredText, $fulltext) {
global $wgContLang;
$lc = SearchEngine::legalSearchChars();
@@ -170,9 +174,9 @@ class SearchOracle extends SearchEngine {
}
}
- $searchon = $this->db->strencode(join(',', $q));
+ $searchon = $this->db->addQuotes(join(',', $q));
$field = $this->getIndexField($fulltext);
- return " CONTAINS($field, '$searchon', 1) > 0 ";
+ return " CONTAINS($field, $searchon, 1) > 0 ";
}
/**
diff --git a/includes/SearchPostgres.php b/includes/SearchPostgres.php
index 88e4a0da..4862a44e 100644
--- a/includes/SearchPostgres.php
+++ b/includes/SearchPostgres.php
@@ -66,6 +66,7 @@ class SearchPostgres extends SearchEngine {
/*
* Transform the user's search string into a better form for tsearch2
+ * Returns an SQL fragment consisting of quoted text to search for.
*/
function parseQuery( $term ) {
@@ -142,6 +143,7 @@ class SearchPostgres extends SearchEngine {
}
$prefix = $wgDBversion < 8.3 ? "'default'," : '';
+ # Get the SQL fragment for the given term
$searchstring = $this->parseQuery( $term );
## We need a separate query here so gin does not complain about empty searches
@@ -183,7 +185,7 @@ class SearchPostgres extends SearchEngine {
if ( count($this->namespaces) < 1)
$query .= ' AND page_namespace = 0';
else {
- $namespaces = implode( ',', $this->namespaces );
+ $namespaces = $this->db->makeList( $this->namespaces );
$query .= " AND page_namespace IN ($namespaces)";
}
}
@@ -201,9 +203,9 @@ class SearchPostgres extends SearchEngine {
function update( $pageid, $title, $text ) {
## We don't want to index older revisions
- $SQL = "UPDATE pagecontent SET textvector = NULL WHERE old_id = ".
- "(SELECT rev_text_id FROM revision WHERE rev_page = $pageid ".
- "ORDER BY rev_text_id DESC LIMIT 1 OFFSET 1)";
+ $SQL = "UPDATE pagecontent SET textvector = NULL WHERE old_id IN ".
+ "(SELECT rev_text_id FROM revision WHERE rev_page = " . intval( $pageid ) .
+ " ORDER BY rev_text_id DESC OFFSET 1)";
$this->db->doQuery($SQL);
return true;
}
diff --git a/includes/Setup.php b/includes/Setup.php
index 36c4d965..859ad008 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -59,6 +59,23 @@ if ( empty( $wgFileStore['deleted']['directory'] ) ) {
}
/**
+ * Unconditional protection for NS_MEDIAWIKI since otherwise it's too easy for a
+ * sysadmin to set $wgNamespaceProtection incorrectly and leave the wiki insecure.
+ *
+ * Note that this is the definition of editinterface and it can be granted to
+ * all users if desired.
+ */
+$wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface';
+
+/**
+ * The canonical names of namespaces 6 and 7 are, as of v1.14, "File"
+ * and "File_talk". The old names "Image" and "Image_talk" are
+ * retained as aliases for backwards compatibility.
+ */
+$wgNamespaceAliases['Image'] = NS_FILE;
+$wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
+
+/**
* Initialise $wgLocalFileRepo from backwards-compatible settings
*/
if ( !$wgLocalFileRepo ) {
@@ -137,12 +154,6 @@ wfProfileIn( $fname.'-misc1' );
$wgIP = false; # Load on demand
# Can't stub this one, it sets up $_GET and $_REQUEST in its constructor
$wgRequest = new WebRequest;
-if ( function_exists( 'posix_uname' ) ) {
- $wguname = posix_uname();
- $wgNodeName = $wguname['nodename'];
-} else {
- $wgNodeName = '';
-}
# Useful debug output
if ( $wgCommandLineMode ) {
@@ -198,15 +209,19 @@ wfDebug( 'Main cache: ' . get_class( $wgMemc ) .
"\nParser cache: " . get_class( $parserMemc ) . "\n" );
wfProfileOut( $fname.'-memcached' );
+
+## Most of the config is out, some might want to run hooks here.
+wfRunHooks( 'SetupAfterCache' );
+
wfProfileIn( $fname.'-SetupSession' );
# Set default shared prefix
if( $wgSharedPrefix === false ) $wgSharedPrefix = $wgDBprefix;
if( !$wgCookiePrefix ) {
- if ( in_array('user', $wgSharedTables) && $wgSharedDB && $wgSharedPrefix ) {
+ if ( $wgSharedDB && $wgSharedPrefix && in_array('user',$wgSharedTables) ) {
$wgCookiePrefix = $wgSharedDB . '_' . $wgSharedPrefix;
- } elseif ( in_array('user', $wgSharedTables) && $wgSharedDB ) {
+ } elseif ( $wgSharedDB && in_array('user',$wgSharedTables) ) {
$wgCookiePrefix = $wgSharedDB;
} elseif ( $wgDBprefix ) {
$wgCookiePrefix = $wgDBname . '_' . $wgDBprefix;
@@ -269,7 +284,6 @@ wfProfileIn( $fname.'-misc2' );
$wgDeferredUpdateList = array();
$wgPostCommitUpdateList = array();
-if ( $wgAjaxSearch ) $wgAjaxExportList[] = 'wfSajaxSearch';
if ( $wgAjaxWatch ) $wgAjaxExportList[] = 'wfAjaxWatch';
if ( $wgAjaxUploadDestCheck ) $wgAjaxExportList[] = 'UploadForm::ajaxGetExistsWarning';
if( $wgAjaxLicensePreview )
@@ -299,6 +313,16 @@ wfRunHooks( 'LogPageLogName', array( &$wgLogNames ) );
wfRunHooks( 'LogPageLogHeader', array( &$wgLogHeaders ) );
wfRunHooks( 'LogPageActionText', array( &$wgLogActions ) );
+if( !empty($wgNewUserLog) ) {
+ # Add a new log type
+ $wgLogTypes[] = 'newusers';
+ $wgLogNames['newusers'] = 'newuserlogpage';
+ $wgLogHeaders['newusers'] = 'newuserlogpagetext';
+ $wgLogActions['newusers/newusers'] = 'newuserlogentry'; // For compatibility with older log entries
+ $wgLogActions['newusers/create'] = 'newuserlog-create-entry';
+ $wgLogActions['newusers/create2'] = 'newuserlog-create2-entry';
+ $wgLogActions['newusers/autocreate'] = 'newuserlog-autocreate-entry';
+}
wfDebug( "Fully initialised\n" );
$wgFullyInitialised = true;
diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php
index 6cdd5082..2ed28139 100644
--- a/includes/SiteConfiguration.php
+++ b/includes/SiteConfiguration.php
@@ -5,57 +5,143 @@
* meaning that require_once() fails to detect that it is including the same
* file again. We use DIY C-style protection as a workaround.
*/
-if (!defined('SITE_CONFIGURATION')) {
-define('SITE_CONFIGURATION', 1);
+
+// Hide this pattern from Doxygen, which spazzes out at it
+/// @cond
+if( !defined( 'SITE_CONFIGURATION' ) ){
+define( 'SITE_CONFIGURATION', 1 );
+/// @endcond
/**
* This is a class used to hold configuration settings, particularly for multi-wiki sites.
- *
*/
class SiteConfiguration {
- var $suffixes = array();
- var $wikis = array();
- var $settings = array();
- var $localVHosts = array();
-
- /** */
- function get( $settingName, $wiki, $suffix, $params = array(), $wikiTags = array() ) {
- if ( array_key_exists( $settingName, $this->settings ) ) {
+
+ /**
+ * Array of suffixes, for self::siteFromDB()
+ */
+ public $suffixes = array();
+
+ /**
+ * Array of wikis, should be the same as $wgLocalDatabases
+ */
+ public $wikis = array();
+
+ /**
+ * The whole array of settings
+ */
+ public $settings = array();
+
+ /**
+ * Array of domains that are local and can be handled by the same server
+ */
+ public $localVHosts = array();
+
+ /**
+ * A callback function that returns an array with the following keys (all
+ * optional):
+ * - suffix: site's suffix
+ * - lang: site's lang
+ * - tags: array of wiki tags
+ * - params: array of parameters to be replaced
+ * The function will receive the SiteConfiguration instance in the first
+ * argument and the wiki in the second one.
+ * if suffix and lang are passed they will be used for the return value of
+ * self::siteFromDB() and self::$suffixes will be ignored
+ */
+ public $siteParamsCallback = null;
+
+ /**
+ * Retrieves a configuration setting for a given wiki.
+ * @param $settingName String ID of the setting name to retrieve
+ * @param $wiki String Wiki ID of the wiki in question.
+ * @param $suffix String The suffix of the wiki in question.
+ * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data.
+ * @param $wikiTags Array The tags assigned to the wiki.
+ * @return Mixed the value of the setting requested.
+ */
+ public function get( $settingName, $wiki, $suffix = null, $params = array(), $wikiTags = array() ) {
+ $params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags );
+ return $this->getSetting( $settingName, $wiki, $params );
+ }
+
+ /**
+ * Really retrieves a configuration setting for a given wiki.
+ *
+ * @param $settingName String ID of the setting name to retrieve.
+ * @param $wiki String Wiki ID of the wiki in question.
+ * @param $params Array: array of parameters.
+ * @return Mixed the value of the setting requested.
+ */
+ protected function getSetting( $settingName, $wiki, /*array*/ $params ){
+ $retval = null;
+ if( array_key_exists( $settingName, $this->settings ) ) {
$thisSetting =& $this->settings[$settingName];
do {
- if ( array_key_exists( $wiki, $thisSetting ) ) {
+ // Do individual wiki settings
+ if( array_key_exists( $wiki, $thisSetting ) ) {
$retval = $thisSetting[$wiki];
break;
+ } elseif( array_key_exists( "+$wiki", $thisSetting ) && is_array( $thisSetting["+$wiki"] ) ) {
+ $retval = $thisSetting["+$wiki"];
}
- foreach ( $wikiTags as $tag ) {
- if ( array_key_exists( $tag, $thisSetting ) ) {
- $retval = $thisSetting[$tag];
+
+ // Do tag settings
+ foreach( $params['tags'] as $tag ) {
+ if( array_key_exists( $tag, $thisSetting ) ) {
+ if ( isset( $retval ) && is_array( $retval ) && is_array( $thisSetting[$tag] ) ) {
+ $retval = self::arrayMerge( $retval, $thisSetting[$tag] );
+ } else {
+ $retval = $thisSetting[$tag];
+ }
break 2;
+ } elseif( array_key_exists( "+$tag", $thisSetting ) && is_array($thisSetting["+$tag"]) ) {
+ if( !isset( $retval ) )
+ $retval = array();
+ $retval = self::arrayMerge( $retval, $thisSetting["+$tag"] );
}
}
- if ( array_key_exists( $suffix, $thisSetting ) ) {
- $retval = $thisSetting[$suffix];
- break;
+ // Do suffix settings
+ $suffix = $params['suffix'];
+ if( !is_null( $suffix ) ) {
+ if( array_key_exists( $suffix, $thisSetting ) ) {
+ if ( isset($retval) && is_array($retval) && is_array($thisSetting[$suffix]) ) {
+ $retval = self::arrayMerge( $retval, $thisSetting[$suffix] );
+ } else {
+ $retval = $thisSetting[$suffix];
+ }
+ break;
+ } elseif( array_key_exists( "+$suffix", $thisSetting ) && is_array($thisSetting["+$suffix"]) ) {
+ if (!isset($retval))
+ $retval = array();
+ $retval = self::arrayMerge( $retval, $thisSetting["+$suffix"] );
+ }
}
- if ( array_key_exists( 'default', $thisSetting ) ) {
- $retval = $thisSetting['default'];
+
+ // Fall back to default.
+ if( array_key_exists( 'default', $thisSetting ) ) {
+ if( is_array( $retval ) && is_array( $thisSetting['default'] ) ) {
+ $retval = self::arrayMerge( $retval, $thisSetting['default'] );
+ } else {
+ $retval = $thisSetting['default'];
+ }
break;
}
- $retval = null;
} while ( false );
- } else {
- $retval = NULL;
}
- if ( !is_null( $retval ) && count( $params ) ) {
- foreach ( $params as $key => $value ) {
+ if( !is_null( $retval ) && count( $params['params'] ) ) {
+ foreach ( $params['params'] as $key => $value ) {
$retval = $this->doReplace( '$' . $key, $value, $retval );
}
}
return $retval;
}
- /** Type-safe string replace; won't do replacements on non-strings */
+ /**
+ * Type-safe string replace; won't do replacements on non-strings
+ * private?
+ */
function doReplace( $from, $to, $in ) {
if( is_string( $in ) ) {
return str_replace( $from, $to, $in );
@@ -69,62 +155,191 @@ class SiteConfiguration {
}
}
- /** */
- function getAll( $wiki, $suffix, $params, $wikiTags = array() ) {
+ /**
+ * Gets all settings for a wiki
+ * @param $wiki String Wiki ID of the wiki in question.
+ * @param $suffix String The suffix of the wiki in question.
+ * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data.
+ * @param $wikiTags Array The tags assigned to the wiki.
+ * @return Array Array of settings requested.
+ */
+ public function getAll( $wiki, $suffix = null, $params = array(), $wikiTags = array() ) {
+ $params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags );
$localSettings = array();
- foreach ( $this->settings as $varname => $stuff ) {
- $value = $this->get( $varname, $wiki, $suffix, $params, $wikiTags );
+ foreach( $this->settings as $varname => $stuff ) {
+ $append = false;
+ $var = $varname;
+ if ( substr( $varname, 0, 1 ) == '+' ) {
+ $append = true;
+ $var = substr( $varname, 1 );
+ }
+
+ $value = $this->getSetting( $varname, $wiki, $params );
+ if ( $append && is_array( $value ) && is_array( $GLOBALS[$var] ) )
+ $value = self::arrayMerge( $value, $GLOBALS[$var] );
if ( !is_null( $value ) ) {
- $localSettings[$varname] = $value;
+ $localSettings[$var] = $value;
}
}
return $localSettings;
}
- /** */
- function getBool( $setting, $wiki, $suffix, $wikiTags = array() ) {
+ /**
+ * Retrieves a configuration setting for a given wiki, forced to a boolean.
+ * @param $settingName String ID of the setting name to retrieve
+ * @param $wiki String Wiki ID of the wiki in question.
+ * @param $suffix String The suffix of the wiki in question.
+ * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data.
+ * @param $wikiTags Array The tags assigned to the wiki.
+ * @return bool The value of the setting requested.
+ */
+ public function getBool( $setting, $wiki, $suffix = null, $wikiTags = array() ) {
return (bool)($this->get( $setting, $wiki, $suffix, array(), $wikiTags ) );
}
- /** */
+ /** Retrieves an array of local databases */
function &getLocalDatabases() {
return $this->wikis;
}
- /** */
+ /** A no-op */
function initialise() {
}
- /** */
- function extractVar( $setting, $wiki, $suffix, &$var, $params, $wikiTags = array() ) {
+ /**
+ * Retrieves the value of a given setting, and places it in a variable passed by reference.
+ * @param $settingName String ID of the setting name to retrieve
+ * @param $wiki String Wiki ID of the wiki in question.
+ * @param $suffix String The suffix of the wiki in question.
+ * @param $var Reference The variable to insert the value into.
+ * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data.
+ * @param $wikiTags Array The tags assigned to the wiki.
+ */
+ public function extractVar( $setting, $wiki, $suffix, &$var, $params = array(), $wikiTags = array() ) {
$value = $this->get( $setting, $wiki, $suffix, $params, $wikiTags );
if ( !is_null( $value ) ) {
$var = $value;
}
}
- /** */
- function extractGlobal( $setting, $wiki, $suffix, $params, $wikiTags = array() ) {
- $value = $this->get( $setting, $wiki, $suffix, $params, $wikiTags );
+ /**
+ * Retrieves the value of a given setting, and places it in its corresponding global variable.
+ * @param $settingName String ID of the setting name to retrieve
+ * @param $wiki String Wiki ID of the wiki in question.
+ * @param $suffix String The suffix of the wiki in question.
+ * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data.
+ * @param $wikiTags Array The tags assigned to the wiki.
+ */
+ public function extractGlobal( $setting, $wiki, $suffix = null, $params = array(), $wikiTags = array() ) {
+ $params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags );
+ $this->extractGlobalSetting( $setting, $wiki, $params );
+ }
+
+ public function extractGlobalSetting( $setting, $wiki, $params ) {
+ $value = $this->getSetting( $setting, $wiki, $params );
if ( !is_null( $value ) ) {
- $GLOBALS[$setting] = $value;
+ if (substr($setting,0,1) == '+' && is_array($value)) {
+ $setting = substr($setting,1);
+ if ( is_array($GLOBALS[$setting]) ) {
+ $GLOBALS[$setting] = self::arrayMerge( $GLOBALS[$setting], $value );
+ } else {
+ $GLOBALS[$setting] = $value;
+ }
+ } else {
+ $GLOBALS[$setting] = $value;
+ }
}
}
- /** */
- function extractAllGlobals( $wiki, $suffix, $params, $wikiTags = array() ) {
+ /**
+ * Retrieves the values of all settings, and places them in their corresponding global variables.
+ * @param $wiki String Wiki ID of the wiki in question.
+ * @param $suffix String The suffix of the wiki in question.
+ * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data.
+ * @param $wikiTags Array The tags assigned to the wiki.
+ */
+ public function extractAllGlobals( $wiki, $suffix = null, $params = array(), $wikiTags = array() ) {
+ $params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags );
foreach ( $this->settings as $varName => $setting ) {
- $this->extractGlobal( $varName, $wiki, $suffix, $params, $wikiTags );
+ $this->extractGlobalSetting( $varName, $wiki, $params );
}
}
/**
+ * Return specific settings for $wiki
+ * See the documentation of self::$siteParamsCallback for more in-depth
+ * documentation about this function
+ *
+ * @param $wiki String
+ * @return array
+ */
+ protected function getWikiParams( $wiki ){
+ static $default = array(
+ 'suffix' => null,
+ 'lang' => null,
+ 'tags' => array(),
+ 'params' => array(),
+ );
+
+ if( !is_callable( $this->siteParamsCallback ) )
+ return $default;
+
+ $ret = call_user_func_array( $this->siteParamsCallback, array( $this, $wiki ) );
+ # Validate the returned value
+ if( !is_array( $ret ) )
+ return $default;
+
+ foreach( $default as $name => $def ){
+ if( !isset( $ret[$name] ) || ( is_array( $default[$name] ) && !is_array( $ret[$name] ) ) )
+ $ret[$name] = $default[$name];
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Merge params beetween the ones passed to the function and the ones given
+ * by self::$siteParamsCallback for backward compatibility
+ * Values returned by self::getWikiParams() have the priority.
+ *
+ * @param $wiki String Wiki ID of the wiki in question.
+ * @param $suffix String The suffix of the wiki in question.
+ * @param $params Array List of parameters. $.'key' is replaced by $value in
+ * all returned data.
+ * @param $wikiTags Array The tags assigned to the wiki.
+ * @return array
+ */
+ protected function mergeParams( $wiki, $suffix, /*array*/ $params, /*array*/ $wikiTags ){
+ $ret = $this->getWikiParams( $wiki );
+
+ if( is_null( $ret['suffix'] ) )
+ $ret['suffix'] = $suffix;
+
+ $ret['tags'] = array_unique( array_merge( $ret['tags'], $wikiTags ) );
+
+ $ret['params'] += $params;
+
+ // Automatically fill that ones if needed
+ if( !isset( $ret['params']['lang'] ) && !is_null( $ret['lang'] ) )
+ $ret['params']['lang'] = $ret['lang'];
+ if( !isset( $ret['params']['site'] ) && !is_null( $ret['suffix'] ) )
+ $ret['params']['site'] = $ret['suffix'];
+
+ return $ret;
+ }
+
+ /**
* Work out the site and language name from a database name
* @param $db
*/
- function siteFromDB( $db ) {
- $site = NULL;
- $lang = NULL;
+ public function siteFromDB( $db ) {
+ // Allow override
+ $def = $this->getWikiParams( $db );
+ if( !is_null( $def['suffix'] ) && !is_null( $def['lang'] ) )
+ return array( $def['suffix'], $def['lang'] );
+
+ $site = null;
+ $lang = null;
foreach ( $this->suffixes as $suffix ) {
if ( $suffix === '' ) {
$site = '';
@@ -140,9 +355,37 @@ class SiteConfiguration {
return array( $site, $lang );
}
- /** */
- function isLocalVHost( $vhost ) {
+ /**
+ * Returns true if the given vhost is handled locally.
+ * @param $vhost String
+ * @return bool
+ */
+ public function isLocalVHost( $vhost ) {
return in_array( $vhost, $this->localVHosts );
}
+
+ /**
+ * Merge multiple arrays together.
+ * On encountering duplicate keys, merge the two, but ONLY if they're arrays.
+ * PHP's array_merge_recursive() merges ANY duplicate values into arrays,
+ * which is not fun
+ */
+ static function arrayMerge( $array1/* ... */ ) {
+ $out = $array1;
+ for( $i=1; $i < func_num_args(); $i++ ) {
+ foreach( func_get_arg( $i ) as $key => $value ) {
+ if ( isset($out[$key]) && is_array($out[$key]) && is_array($value) ) {
+ $out[$key] = self::arrayMerge( $out[$key], $value );
+ } elseif ( !isset($out[$key]) || !$out[$key] && !is_numeric($key) ) {
+ // Values that evaluate to true given precedence, for the primary purpose of merging permissions arrays.
+ $out[$key] = $value;
+ } elseif ( is_numeric( $key ) ) {
+ $out[] = $value;
+ }
+ }
+ }
+
+ return $out;
+ }
}
}
diff --git a/includes/SiteStats.php b/includes/SiteStats.php
index 3b10f4a0..ab0caa7e 100644
--- a/includes/SiteStats.php
+++ b/includes/SiteStats.php
@@ -7,6 +7,7 @@ class SiteStats {
static $row, $loaded = false;
static $admins, $jobs;
static $pageCount = array();
+ static $groupMemberCounts = array();
static function recache() {
self::load( true );
@@ -92,18 +93,44 @@ class SiteStats {
self::load();
return self::$row->ss_users;
}
+
+ static function activeUsers() {
+ self::load();
+ return self::$row->ss_active_users;
+ }
static function images() {
self::load();
return self::$row->ss_images;
}
+ /**
+ * @deprecated Use self::numberingroup('sysop') instead
+ */
static function admins() {
- if ( !isset( self::$admins ) ) {
- $dbr = wfGetDB( DB_SLAVE );
- self::$admins = $dbr->selectField( 'user_groups', 'COUNT(*)', array( 'ug_group' => 'sysop' ), __METHOD__ );
+ wfDeprecated(__METHOD__);
+ return self::numberingroup('sysop');
+ }
+
+ /**
+ * Find the number of users in a given user group.
+ * @param string $group Name of group
+ * @return int
+ */
+ static function numberingroup($group) {
+ if ( !isset( self::$groupMemberCounts[$group] ) ) {
+ global $wgMemc;
+ $key = wfMemcKey( 'SiteStats', 'groupcounts', $group );
+ $hit = $wgMemc->get( $key );
+ if ( !$hit ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $hit = $dbr->selectField( 'user_groups', 'COUNT(*)',
+ array( 'ug_group' => $group ), __METHOD__ );
+ $wgMemc->set( $key, $hit, 3600 );
+ }
+ self::$groupMemberCounts[$group] = $hit;
}
- return self::$admins;
+ return self::$groupMemberCounts[$group];
}
static function jobs() {
@@ -185,54 +212,35 @@ class SiteStatsUpdate {
$fname = 'SiteStatsUpdate::doUpdate';
$dbw = wfGetDB( DB_MASTER );
- # First retrieve the row just to find out which schema we're in
- $row = $dbw->selectRow( 'site_stats', '*', false, $fname );
-
$updates = '';
$this->appendUpdate( $updates, 'ss_total_views', $this->mViews );
$this->appendUpdate( $updates, 'ss_total_edits', $this->mEdits );
$this->appendUpdate( $updates, 'ss_good_articles', $this->mGood );
+ $this->appendUpdate( $updates, 'ss_total_pages', $this->mPages );
+ $this->appendUpdate( $updates, 'ss_users', $this->mUsers );
- if ( isset( $row->ss_total_pages ) ) {
- # Update schema if required
- if ( $row->ss_total_pages == -1 && !$this->mViews ) {
- $dbr = wfGetDB( DB_SLAVE, array( 'SpecialStatistics', 'vslow') );
- list( $page, $user ) = $dbr->tableNamesN( 'page', 'user' );
-
- $sql = "SELECT COUNT(page_namespace) AS total FROM $page";
- $res = $dbr->query( $sql, $fname );
- $pageRow = $dbr->fetchObject( $res );
- $pages = $pageRow->total + $this->mPages;
-
- $sql = "SELECT COUNT(user_id) AS total FROM $user";
- $res = $dbr->query( $sql, $fname );
- $userRow = $dbr->fetchObject( $res );
- $users = $userRow->total + $this->mUsers;
-
- if ( $updates ) {
- $updates .= ',';
- }
- $updates .= "ss_total_pages=$pages, ss_users=$users";
- } else {
- $this->appendUpdate( $updates, 'ss_total_pages', $this->mPages );
- $this->appendUpdate( $updates, 'ss_users', $this->mUsers );
- }
- }
if ( $updates ) {
$site_stats = $dbw->tableName( 'site_stats' );
$sql = $dbw->limitResultForUpdate("UPDATE $site_stats SET $updates", 1);
+
+ # Need a separate transaction because this a global lock
$dbw->begin();
$dbw->query( $sql, $fname );
$dbw->commit();
}
-
- /*
- global $wgDBname, $wgTitle;
- if ( $this->mGood && $wgDBname == 'enwiki' ) {
- $good = $dbw->selectField( 'site_stats', 'ss_good_articles', '', $fname );
- error_log( $good . ' ' . $wgTitle->getPrefixedDBkey() . "\n", 3, '/home/wikipedia/logs/million.log' );
- }
- */
+ }
+
+ public static function cacheUpdate( $dbw ) {
+ $dbr = wfGetDB( DB_SLAVE, array( 'SpecialStatistics', 'vslow') );
+ # Get non-bot users than did some recent action other than making accounts.
+ # If account creation is included, the number gets inflated ~20+ fold on enwiki.
+ $activeUsers = $dbr->selectField( 'recentchanges', 'COUNT( DISTINCT rc_user_text )',
+ array( 'rc_user != 0', 'rc_bot' => 0, "rc_log_type != 'newusers' OR rc_log_type IS NULL" ),
+ __METHOD__ );
+ $dbw->update( 'site_stats',
+ array( 'ss_active_users' => intval($activeUsers) ),
+ array( 'ss_row_id' => 1 ), __METHOD__, array( 'LIMIT' => 1 )
+ );
}
}
diff --git a/includes/Skin.php b/includes/Skin.php
index a9e44ab4..636b96bf 100644
--- a/includes/Skin.php
+++ b/includes/Skin.php
@@ -60,6 +60,21 @@ class Skin extends Linker {
}
return $wgValidSkinNames;
}
+
+ /**
+ * Fetch the list of usable skins in regards to $wgSkipSkins.
+ * Useful for Special:Preferences and other places where you
+ * only want to show skins users _can_ use.
+ * @return array of strings
+ */
+ public static function getUsableSkins() {
+ global $wgSkipSkins;
+ $usableSkins = self::getSkinNames();
+ foreach ( $wgSkipSkins as $skip ) {
+ unset( $usableSkins[$skip] );
+ }
+ return $usableSkins;
+ }
/**
* Normalize a skin preference value to a form that can be loaded.
@@ -156,24 +171,28 @@ class Skin extends Linker {
return $q;
}
- function initPage( &$out ) {
- global $wgFavicon, $wgAppleTouchIcon, $wgScriptPath, $wgScriptExtension;
+ function initPage( OutputPage $out ) {
+ global $wgFavicon, $wgAppleTouchIcon;
wfProfileIn( __METHOD__ );
- if( false !== $wgFavicon ) {
- $out->addLink( array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) );
- }
-
+ # Generally the order of the favicon and apple-touch-icon links
+ # should not matter, but Konqueror (3.5.9 at least) incorrectly
+ # uses whichever one appears later in the HTML source. Make sure
+ # apple-touch-icon is specified first to avoid this.
if( false !== $wgAppleTouchIcon ) {
$out->addLink( array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) );
}
+ if( false !== $wgFavicon ) {
+ $out->addLink( array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) );
+ }
+
# OpenSearch description link
$out->addLink( array(
'rel' => 'search',
'type' => 'application/opensearchdescription+xml',
- 'href' => "$wgScriptPath/opensearch_desc{$wgScriptExtension}",
+ 'href' => wfScript( 'opensearch_desc' ),
'title' => wfMsgForContent( 'opensearch-desc' ),
));
@@ -208,7 +227,7 @@ class Skin extends Linker {
$lb->execute();
}
- function addMetadataLinks( &$out ) {
+ function addMetadataLinks( OutputPage $out ) {
global $wgTitle, $wgEnableDublinCoreRdf, $wgEnableCreativeCommonsRdf;
global $wgRightsPage, $wgRightsUrl;
@@ -244,13 +263,25 @@ class Skin extends Linker {
}
}
- function outputPage( &$out ) {
- global $wgDebugComments;
+ function setMembers(){
+ global $wgTitle, $wgUser;
+ $this->mTitle = $wgTitle;
+ $this->mUser = $wgUser;
+ $this->userpage = $wgUser->getUserPage()->getPrefixedText();
+ $this->usercss = false;
+ }
+ function outputPage( OutputPage $out ) {
+ global $wgDebugComments;
wfProfileIn( __METHOD__ );
+
+ $this->setMembers();
$this->initPage( $out );
- $out->out( $out->headElement() );
+ // See self::afterContentHook() for documentation
+ $afterContent = $this->afterContentHook();
+
+ $out->out( $out->headElement( $this ) );
$out->out( "\n<body" );
$ops = $this->getBodyOptions();
@@ -268,6 +299,8 @@ class Skin extends Linker {
$out->out( $out->mBodytext . "\n" );
$out->out( $this->afterContent() );
+
+ $out->out( $afterContent );
$out->out( $this->bottomScripts() );
@@ -280,14 +313,14 @@ class Skin extends Linker {
static function makeVariablesScript( $data ) {
global $wgJsMimeType;
- $r = "<script type= \"$wgJsMimeType\">/*<![CDATA[*/\n";
+ $r = array( "<script type= \"$wgJsMimeType\">/*<![CDATA[*/" );
foreach ( $data as $name => $value ) {
$encValue = Xml::encodeJsVar( $value );
- $r .= "var $name = $encValue;\n";
+ $r[] = "var $name = $encValue;";
}
- $r .= "/*]]>*/</script>\n";
+ $r[] = "/*]]>*/</script>\n";
- return $r;
+ return implode( "\n\t\t", $r );
}
/**
@@ -308,6 +341,18 @@ class Skin extends Linker {
$ns = $wgTitle->getNamespace();
$nsname = isset( $wgCanonicalNamespaceNames[ $ns ] ) ? $wgCanonicalNamespaceNames[ $ns ] : $wgTitle->getNsText();
+ $separatorTransTable = $wgContLang->separatorTransformTable();
+ $separatorTransTable = $separatorTransTable ? $separatorTransTable : array();
+ $compactSeparatorTransTable = array(
+ implode( "\t", array_keys( $separatorTransTable ) ),
+ implode( "\t", $separatorTransTable ),
+ );
+ $digitTransTable = $wgContLang->digitTransformTable();
+ $digitTransTable = $digitTransTable ? $digitTransTable : array();
+ $compactDigitTransTable = array(
+ implode( "\t", array_keys( $digitTransTable ) ),
+ implode( "\t", $digitTransTable ),
+ );
$vars = array(
'skin' => $data['skinname'],
@@ -316,7 +361,7 @@ class Skin extends Linker {
'wgScriptPath' => $wgScriptPath,
'wgScript' => $wgScript,
'wgVariantArticlePath' => $wgVariantArticlePath,
- 'wgActionPaths' => $wgActionPaths,
+ 'wgActionPaths' => (object)$wgActionPaths,
'wgServer' => $wgServer,
'wgCanonicalNamespace' => $nsname,
'wgCanonicalSpecialPageName' => SpecialPage::resolveAlias( $wgTitle->getDBkey() ),
@@ -335,6 +380,8 @@ class Skin extends Linker {
'wgVersion' => $wgVersion,
'wgEnableAPI' => $wgEnableAPI,
'wgEnableWriteAPI' => $wgEnableWriteAPI,
+ 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
+ 'wgDigitTransformTable' => $compactDigitTransTable,
);
if( $wgUseAjax && $wgEnableMWSuggest && !$wgUser->getOption( 'disablesuggest', false )){
@@ -362,32 +409,34 @@ class Skin extends Linker {
$vars['wgAjaxWatch'] = $msgs;
}
+ wfRunHooks('MakeGlobalVariablesScript', array(&$vars));
+
return self::makeVariablesScript( $vars );
}
function getHeadScripts( $allowUserJs ) {
global $wgStylePath, $wgUser, $wgJsMimeType, $wgStyleVersion;
- $r = self::makeGlobalVariablesScript( array( 'skinname' => $this->getSkinName() ) );
+ $vars = self::makeGlobalVariablesScript( array( 'skinname' => $this->getSkinName() ) );
- $r .= "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/wikibits.js?$wgStyleVersion\"></script>\n";
+ $r = array( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/wikibits.js?$wgStyleVersion\"></script>" );
global $wgUseSiteJs;
if ($wgUseSiteJs) {
$jsCache = $wgUser->isLoggedIn() ? '&smaxage=0' : '';
- $r .= "<script type=\"$wgJsMimeType\" src=\"".
+ $r[] = "<script type=\"$wgJsMimeType\" src=\"".
htmlspecialchars(self::makeUrl('-',
"action=raw$jsCache&gen=js&useskin=" .
urlencode( $this->getSkinName() ) ) ) .
- "\"><!-- site js --></script>\n";
+ "\"><!-- site js --></script>";
}
if( $allowUserJs && $wgUser->isLoggedIn() ) {
$userpage = $wgUser->getUserPage();
$userjs = htmlspecialchars( self::makeUrl(
$userpage->getPrefixedText().'/'.$this->getSkinName().'.js',
'action=raw&ctype='.$wgJsMimeType));
- $r .= '<script type="'.$wgJsMimeType.'" src="'.$userjs."\"></script>\n";
+ $r[] = '<script type="'.$wgJsMimeType.'" src="'.$userjs."\"></script>";
}
- return $r;
+ return $vars . "\t\t" . implode ( "\n\t\t", $r );
}
/**
@@ -414,38 +463,24 @@ class Skin extends Linker {
$wgRequest->getVal( 'wpEditToken' ) );
}
- # get the user/site-specific stylesheet, SkinTemplate loads via RawPage.php (settings are cached that way)
- function getUserStylesheet() {
- global $wgStylePath, $wgRequest, $wgContLang, $wgSquidMaxage, $wgStyleVersion;
- $sheet = $this->getStylesheet();
- $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";
-
- $query = "usemsgcache=yes&action=raw&ctype=text/css&smaxage=$wgSquidMaxage";
- $s .= '@import "' . self::makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI ) . "\";\n" .
- '@import "' . self::makeNSUrl( ucfirst( $this->getSkinName() . '.css' ), $query, NS_MEDIAWIKI ) . "\";\n";
-
- $s .= $this->doGetUserStyles();
- return $s."\n";
- }
-
/**
- * 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.
+ * generated JavaScript action=raw&gen=js
+ * This returns MediaWiki:Common.js and MediaWiki:[Skinname].js concate-
+ * nated together. For some bizarre reason, it does *not* return any
+ * custom user JS from subpages. Huh?
+ *
+ * There's absolutely no reason to have separate Monobook/Common JSes.
+ * Any JS that cares can just check the skin variable generated at the
+ * top. For now Monobook.js will be maintained, but it should be consi-
+ * dered deprecated.
*
- * @return string Raw JavaScript to be returned
+ * @return string
*/
- public function getUserJs() {
+ public function generateUserJs() {
+ global $wgStylePath;
+
wfProfileIn( __METHOD__ );
- global $wgStylePath;
$s = "/* generated javascript */\n";
$s .= "var skin = '" . Xml::escapeJsString( $this->getSkinName() ) . "';\n";
$s .= "var stylepath = '" . Xml::escapeJsString( $wgStylePath ) . "';";
@@ -454,45 +489,35 @@ class Skin extends Linker {
if ( !wfEmptyMsg ( 'common.js', $commonJs ) ) {
$s .= $commonJs;
}
+
+ $s .= "\n\n/* MediaWiki:".ucfirst( $this->getSkinName() ).".js */\n";
+ // avoid inclusion of non defined user JavaScript (with custom skins only)
+ // by checking for default message content
+ $msgKey = ucfirst( $this->getSkinName() ).'.js';
+ $userJS = wfMsgForContent($msgKey);
+ if ( !wfEmptyMsg( $msgKey, $userJS ) ) {
+ $s .= $userJS;
+ }
+
wfProfileOut( __METHOD__ );
return $s;
}
/**
- * Return html code that include User stylesheets
+ * generate user stylesheet for action=raw&gen=css
*/
- function getUserStyles() {
- $s = "<style type='text/css'>\n";
- $s .= "/*/*/ /*<![CDATA[*/\n"; # <-- Hide the styles from Netscape 4 without hiding them from IE/Mac
- $s .= $this->getUserStylesheet();
- $s .= "/*]]>*/ /* */\n";
- $s .= "</style>\n";
+ public function generateUserStylesheet() {
+ wfProfileIn( __METHOD__ );
+ $s = "/* generated user stylesheet */\n" .
+ $this->reallyGenerateUserStylesheet();
+ wfProfileOut( __METHOD__ );
return $s;
}
-
+
/**
- * Some styles that are set by user through the user settings interface.
+ * Split for easier subclassing in SkinSimple, SkinStandard and SkinCologneBlue
*/
- function doGetUserStyles() {
- global $wgUser, $wgUser, $wgRequest, $wgTitle, $wgAllowUserCss;
-
- $s = '';
-
- if( $wgAllowUserCss && $wgUser->isLoggedIn() ) { # logged in
- if($wgTitle->isCssSubpage() && $this->userCanPreview( $wgRequest->getText( 'action' ) ) ) {
- $s .= $wgRequest->getText('wpTextbox1');
- } else {
- $userpage = $wgUser->getUserPage();
- $s.= '@import "'.self::makeUrl(
- $userpage->getPrefixedText().'/'.$this->getSkinName().'.css',
- 'action=raw&ctype=text/css').'";'."\n";
- }
- }
-
- return $s . $this->reallyDoGetUserStyles();
- }
-
- function reallyDoGetUserStyles() {
+ protected function reallyGenerateUserStylesheet(){
global $wgUser;
$s = '';
if (($undopt = $wgUser->getOption("underline")) < 2) {
@@ -529,6 +554,86 @@ END;
return $s;
}
+ /**
+ * @private
+ */
+ function setupUserCss( OutputPage $out ) {
+ global $wgRequest, $wgContLang, $wgUser;
+ global $wgAllowUserCss, $wgUseSiteCss, $wgSquidMaxage, $wgStylePath;
+
+ wfProfileIn( __METHOD__ );
+
+ $this->setupSkinUserCss( $out );
+
+ $siteargs = array(
+ 'action' => 'raw',
+ 'maxage' => $wgSquidMaxage,
+ );
+
+ // Add any extension CSS
+ foreach( $out->getExtStyle() as $tag ) {
+ $out->addStyle( $tag['href'] );
+ }
+
+ // If we use the site's dynamic CSS, throw that in, too
+ // Per-site custom styles
+ if( $wgUseSiteCss ) {
+ global $wgHandheldStyle;
+ $query = wfArrayToCGI( array(
+ 'usemsgcache' => 'yes',
+ 'ctype' => 'text/css',
+ 'smaxage' => $wgSquidMaxage
+ ) + $siteargs );
+ # Site settings must override extension css! (bug 15025)
+ $out->addStyle( self::makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI ) );
+ $out->addStyle( self::makeNSUrl( 'Print.css', $query, NS_MEDIAWIKI ), 'print' );
+ if( $wgHandheldStyle ) {
+ $out->addStyle( self::makeNSUrl( 'Handheld.css', $query, NS_MEDIAWIKI ), 'handheld' );
+ }
+ $out->addStyle( self::makeNSUrl( $this->getSkinName() . '.css', $query, NS_MEDIAWIKI ) );
+ }
+
+ if( $wgUser->isLoggedIn() ) {
+ // Ensure that logged-in users' generated CSS isn't clobbered
+ // by anons' publicly cacheable generated CSS.
+ $siteargs['smaxage'] = '0';
+ $siteargs['ts'] = $wgUser->mTouched;
+ }
+ // Per-user styles based on preferences
+ $siteargs['gen'] = 'css';
+ if( ( $us = $wgRequest->getVal( 'useskin', '' ) ) !== '' ) {
+ $siteargs['useskin'] = $us;
+ }
+ $out->addStyle( self::makeUrl( '-', wfArrayToCGI( $siteargs ) ) );
+
+ // Per-user custom style pages
+ if( $wgAllowUserCss && $wgUser->isLoggedIn() ) {
+ $action = $wgRequest->getVal('action');
+ # If we're previewing the CSS page, use it
+ if( $this->mTitle->isCssSubpage() && $this->userCanPreview( $action ) ) {
+ $previewCss = $wgRequest->getText('wpTextbox1');
+ // @FIXME: properly escape the cdata!
+ $this->usercss = "/*<![CDATA[*/\n" . $previewCss . "/*]]>*/";
+ } else {
+ $out->addStyle( self::makeUrl($this->userpage . '/' . $this->getSkinName() .'.css',
+ 'action=raw&ctype=text/css' ) );
+ }
+ }
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Add skin specific stylesheets
+ * @param $out OutputPage
+ */
+ function setupSkinUserCss( OutputPage $out ) {
+ $out->addStyle( 'common/shared.css' );
+ $out->addStyle( 'common/oldshared.css' );
+ $out->addStyle( $this->getStylesheet() );
+ $out->addStyle( 'common/common_rtl.css', '', '', 'rtl' );
+ }
+
function getBodyOptions() {
global $wgUser, $wgTitle, $wgOut, $wgRequest, $wgContLang;
@@ -539,19 +644,33 @@ END;
}
else $a = array( 'bgcolor' => '#FFFFFF' );
if($wgOut->isArticle() && $wgUser->getOption('editondblclick') &&
- $wgTitle->userCan( 'edit' ) ) {
+ $wgTitle->quickUserCan( 'edit' ) ) {
$s = $wgTitle->getFullURL( $this->editUrlOptions() );
- $s = 'document.location = "' .wfEscapeJSString( $s ) .'";';
+ $s = 'document.location = "' .Xml::escapeJsString( $s ) .'";';
$a += array ('ondblclick' => $s);
}
$a['onload'] = $wgOut->getOnloadHandler();
$a['class'] =
- 'mediawiki ns-'.$wgTitle->getNamespace().
- ' '.($wgContLang->isRTL() ? "rtl" : "ltr").
- ' '.Sanitizer::escapeClass( 'page-'.$wgTitle->getPrefixedText() );
+ 'mediawiki' .
+ ' '.( $wgContLang->isRTL() ? "rtl" : "ltr" ).
+ ' '.$this->getPageClasses( $wgTitle ) .
+ ' skin-'. Sanitizer::escapeClass( $this->getSkinName( ) );
return $a;
}
+
+ function getPageClasses( $title ) {
+ $numeric = 'ns-'.$title->getNamespace();
+ if( $title->getNamespace() == NS_SPECIAL ) {
+ $type = "ns-special";
+ } elseif( $title->isTalkPage() ) {
+ $type = "ns-talk";
+ } else {
+ $type = "ns-subject";
+ }
+ $name = Sanitizer::escapeClass( 'page-'.$title->getPrefixedText() );
+ return "$numeric $type $name";
+ }
/**
* URL to the logo
@@ -589,11 +708,11 @@ END;
$s .= "\n<div id='content'>\n<div id='topbar'>\n" .
"<table border='0' cellspacing='0' width='98%'>\n<tr>\n";
- $shove = ($qb != 0);
- $left = ($qb == 1 || $qb == 3);
- if($wgContLang->isRTL()) $left = !$left;
+ $shove = ( $qb != 0 );
+ $left = ( $qb == 1 || $qb == 3 );
+ if( $wgContLang->isRTL() ) $left = !$left;
- if ( !$shove ) {
+ if( !$shove ) {
$s .= "<td class='top' align='left' valign='top' rowspan='{$rows}'>\n" .
$this->logoText() . '</td>';
} elseif( $left ) {
@@ -655,7 +774,7 @@ END;
$msg = wfMsgExt( 'pagecategories', array( 'parsemag', 'escapenoentities' ), count( $allCats['normal'] ) );
$s .= '<div id="mw-normal-catlinks">' .
- $this->makeLinkObj( Title::newFromText( wfMsgForContent('pagecategorieslink') ), $msg )
+ $this->link( Title::newFromText( wfMsgForContent('pagecategorieslink') ), $msg )
. $colon . $t . '</div>';
}
@@ -676,7 +795,7 @@ END;
# optional 'dmoz-like' category browser. Will be shown under the list
# of categories an article belong to
- if($wgUseCategoryBrowser) {
+ if( $wgUseCategoryBrowser ){
$s .= '<br /><hr />';
# get a big array of the parents tree
@@ -699,7 +818,7 @@ END;
* @param &skin Object: skin passed by reference
* @return String separated by &gt;, terminate with "\n"
*/
- function drawCategoryBrowser($tree, &$skin) {
+ function drawCategoryBrowser( $tree, &$skin ){
$return = '';
foreach ($tree as $element => $parent) {
if (empty($parent)) {
@@ -710,8 +829,8 @@ END;
$return .= Skin::drawCategoryBrowser($parent, $skin) . ' &gt; ';
}
# add our current element to the list
- $eltitle = Title::NewFromText($element);
- $return .= $skin->makeLinkObj( $eltitle, $eltitle->getText() ) ;
+ $eltitle = Title::newFromText($element);
+ $return .= $skin->link( $eltitle, $eltitle->getText() ) ;
}
return $return;
}
@@ -736,8 +855,43 @@ END;
}
/**
- * This gets called shortly before the \</body\> tag.
- * @return String HTML to be put before \</body\>
+ * This runs a hook to allow extensions placing their stuff after content
+ * and article metadata (e.g. categories).
+ * Note: This function has nothing to do with afterContent().
+ *
+ * This hook is placed here in order to allow using the same hook for all
+ * skins, both the SkinTemplate based ones and the older ones, which directly
+ * use this class to get their data.
+ *
+ * The output of this function gets processed in SkinTemplate::outputPage() for
+ * the SkinTemplate based skins, all other skins should directly echo it.
+ *
+ * Returns an empty string by default, if not changed by any hook function.
+ */
+ protected function afterContentHook() {
+ $data = "";
+
+ if( wfRunHooks( 'SkinAfterContent', array( &$data ) ) ){
+ // adding just some spaces shouldn't toggle the output
+ // of the whole <div/>, so we use trim() here
+ if( trim( $data ) != '' ){
+ // Doing this here instead of in the skins to
+ // ensure that the div has the same ID in all
+ // skins
+ $data = "<div id='mw-data-after-content'>\n" .
+ "\t$data\n" .
+ "</div>\n";
+ }
+ } else {
+ wfDebug( "Hook SkinAfterContent changed output processing.\n" );
+ }
+
+ return $data;
+ }
+
+ /**
+ * This gets called shortly before the </body> tag.
+ * @return String HTML to be put before </body>
*/
function afterContent() {
$printfooter = "<div class=\"printfooter\">\n" . $this->printFooter() . "</div>\n";
@@ -745,8 +899,8 @@ END;
}
/**
- * This gets called shortly before the \</body\> tag.
- * @return String HTML-wrapped JS code to be put before \</body\>
+ * This gets called shortly before the </body> tag.
+ * @return String HTML-wrapped JS code to be put before </body>
*/
function bottomScripts() {
global $wgJsMimeType;
@@ -768,7 +922,7 @@ END;
}
/** overloaded by derived classes */
- function doAfterContent() { }
+ function doAfterContent() { return "</div></div>"; }
function pageTitleLinks() {
global $wgOut, $wgTitle, $wgUser, $wgRequest;
@@ -788,7 +942,7 @@ END;
}
if ( $wgOut->isArticleRelated() ) {
- if ( $wgTitle->getNamespace() == NS_IMAGE ) {
+ if ( $wgTitle->getNamespace() == NS_FILE ) {
$name = $wgTitle->getDBkey();
$image = wfFindFile( $wgTitle );
if( $image ) {
@@ -859,7 +1013,7 @@ END;
function pageTitle() {
global $wgOut;
- $s = '<h1 class="pagetitle">' . htmlspecialchars( $wgOut->getPageTitle() ) . '</h1>';
+ $s = '<h1 class="pagetitle">' . $wgOut->getPageTitle() . '</h1>';
return $s;
}
@@ -869,7 +1023,7 @@ END;
$sub = $wgOut->getSubtitle();
if ( '' == $sub ) {
global $wgExtraSubtitle;
- $sub = wfMsg( 'tagline' ) . $wgExtraSubtitle;
+ $sub = wfMsgExt( 'tagline', 'parsemag' ) . $wgExtraSubtitle;
}
$subpages = $this->subPageSubtitle();
$sub .= !empty($subpages)?"</p><p class='subpages'>$subpages":'';
@@ -926,50 +1080,54 @@ END;
function nameAndLogin() {
global $wgUser, $wgTitle, $wgLang, $wgContLang;
- $lo = $wgContLang->specialPage( 'Userlogout' );
+ $logoutPage = $wgContLang->specialPage( 'Userlogout' );
- $s = '';
+ $ret = '';
if ( $wgUser->isAnon() ) {
if( $this->showIPinHeader() ) {
- $n = wfGetIP();
+ $name = wfGetIP();
- $tl = $this->makeKnownLinkObj( $wgUser->getTalkPage(),
- $wgLang->getNsText( NS_TALK ) );
+ $talkLink = $this->link( $wgUser->getTalkPage(),
+ $wgLang->getNsText( NS_TALK ) );
- $s .= $n . ' ('.$tl.')';
+ $ret .= "$name ($talkLink)";
} else {
- $s .= wfMsg('notloggedin');
+ $ret .= wfMsg( 'notloggedin' );
}
- $rt = $wgTitle->getPrefixedURL();
- if ( 0 == strcasecmp( urlencode( $lo ), $rt ) ) {
- $q = '';
- } else { $q = "returnto={$rt}"; }
+ $returnTo = $wgTitle->getPrefixedDBkey();
+ $query = array();
+ if ( $logoutPage != $returnTo ) {
+ $query['returnto'] = $returnTo;
+ }
$loginlink = $wgUser->isAllowed( 'createaccount' )
? 'nav-login-createaccount'
: 'login';
- $s .= "\n<br />" . $this->makeKnownLinkObj(
+ $ret .= "\n<br />" . $this->link(
SpecialPage::getTitleFor( 'Userlogin' ),
- wfMsg( $loginlink ), $q );
+ wfMsg( $loginlink ), array(), $query
+ );
} else {
- $n = $wgUser->getName();
- $rt = $wgTitle->getPrefixedURL();
- $tl = $this->makeKnownLinkObj( $wgUser->getTalkPage(),
- $wgLang->getNsText( NS_TALK ) );
-
- $tl = " ({$tl})";
-
- $s .= $this->makeKnownLinkObj( $wgUser->getUserPage(),
- $n ) . "{$tl}<br />" .
- $this->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogout' ), wfMsg( 'logout' ),
- "returnto={$rt}" ) . ' | ' .
- $this->specialLink( 'preferences' );
+ $returnTo = $wgTitle->getPrefixedDBkey();
+ $talkLink = $this->link( $wgUser->getTalkPage(),
+ $wgLang->getNsText( NS_TALK ) );
+
+ $ret .= $this->link( $wgUser->getUserPage(),
+ htmlspecialchars( $wgUser->getName() ) );
+ $ret .= " ($talkLink)<br />";
+ $ret .= $this->link(
+ SpecialPage::getTitleFor( 'Userlogout' ), wfMsg( 'logout' ),
+ array(), array( 'returnto' => $returnTo )
+ );
+ $ret .= ' | ' . $this->specialLink( 'preferences' );
}
- $s .= ' | ' . $this->makeKnownLink( wfMsgForContent( 'helppage' ),
- wfMsg( 'help' ) );
+ $ret .= ' | ' . $this->link(
+ Title::newFromText( wfMsgForContent( 'helppage' ) ),
+ wfMsg( 'help' )
+ );
- return $s;
+ return $ret;
}
function getSearchLink() {
@@ -1107,6 +1265,7 @@ END;
$oldid = $wgRequest->getVal( 'oldid' );
$diff = $wgRequest->getVal( 'diff' );
if ( ! $wgOut->isArticle() ) { return ''; }
+ if( !$wgArticle instanceOf Article ) { return ''; }
if ( isset( $oldid ) || isset( $diff ) ) { return ''; }
if ( 0 == $wgArticle->getID() ) { return ''; }
@@ -1118,14 +1277,13 @@ END;
}
}
- if (isset($wgMaxCredits) && $wgMaxCredits != 0) {
- require_once('Credits.php');
- $s .= ' ' . getCredits($wgArticle, $wgMaxCredits, $wgShowCreditsIfMax);
+ if( $wgMaxCredits != 0 ){
+ $s .= ' ' . Credits::getCredits( $wgArticle, $wgMaxCredits, $wgShowCreditsIfMax );
} else {
- $s .= $this->lastModified();
+ $s .= $this->lastModified();
}
- if ($wgPageShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
+ if( $wgPageShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' ) ) {
$dbr = wfGetDB( DB_SLAVE );
$watchlist = $dbr->tableName( 'watchlist' );
$sql = "SELECT COUNT(*) AS n FROM $watchlist
@@ -1143,13 +1301,12 @@ END;
}
function getCopyright( $type = 'detect' ) {
- global $wgRightsPage, $wgRightsUrl, $wgRightsText, $wgRequest;
+ global $wgRightsPage, $wgRightsUrl, $wgRightsText, $wgRequest, $wgArticle;
if ( $type == 'detect' ) {
- $oldid = $wgRequest->getVal( 'oldid' );
$diff = $wgRequest->getVal( 'diff' );
-
- if ( !is_null( $oldid ) && is_null( $diff ) && wfMsgForContent( 'history_copyright' ) !== '-' ) {
+ $isCur = $wgArticle && $wgArticle->isCurrent();
+ if ( is_null( $diff ) && !$isCur && wfMsgForContent( 'history_copyright' ) !== '-' ) {
$type = 'history';
} else {
$type = 'normal';
@@ -1167,6 +1324,8 @@ END;
$link = $this->makeKnownLink( $wgRightsPage, $wgRightsText );
} elseif( $wgRightsUrl ) {
$link = $this->makeExternalLink( $wgRightsUrl, $wgRightsText );
+ } elseif( $wgRightsText ) {
+ $link = $wgRightsText;
} else {
# Give up now
return $out;
@@ -1205,7 +1364,7 @@ END;
function lastModified() {
global $wgLang, $wgArticle;
if( $this->mRevisionId ) {
- $timestamp = Revision::getTimestampFromId( $this->mRevisionId, $wgArticle->getId() );
+ $timestamp = Revision::getTimestampFromId( $wgArticle->getTitle(), $this->mRevisionId );
} else {
$timestamp = $wgArticle->getTimestamp();
}
@@ -1249,7 +1408,7 @@ END;
$sp = wfMsg( 'specialpages' );
$spp = $wgContLang->specialPage( 'Specialpages' );
- $s = '<form id="specialpages" method="get" class="inline" ' .
+ $s = '<form id="specialpages" method="get" ' .
'action="' . htmlspecialchars( "{$wgServer}{$wgRedirectScript}" ) . "\">\n";
$s .= "<select name=\"wpDropdown\">\n";
$s .= "<option value=\"{$spp}\">{$sp}</option>\n";
@@ -1308,9 +1467,9 @@ END;
if ( !$wgOut->isArticleRelated() ) {
$s = wfMsg( 'protectedpage' );
} else {
- if( $wgTitle->userCan( 'edit' ) && $wgTitle->exists() ) {
+ if( $wgTitle->quickUserCan( 'edit' ) && $wgTitle->exists() ) {
$t = wfMsg( 'editthispage' );
- } elseif( $wgTitle->userCan( 'create' ) && !$wgTitle->exists() ) {
+ } elseif( $wgTitle->quickUserCan( 'create' ) && !$wgTitle->exists() ) {
$t = wfMsg( 'create-this-page' );
} else {
$t = wfMsg( 'viewsource' );
@@ -1395,7 +1554,7 @@ END;
function moveThisPage() {
global $wgTitle;
- if ( $wgTitle->userCan( 'move' ) ) {
+ if ( $wgTitle->quickUserCan( 'move' ) ) {
return $this->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
wfMsg( 'movethispage' ), 'target=' . $wgTitle->getPrefixedURL() );
} else {
@@ -1428,14 +1587,10 @@ END;
}
function showEmailUser( $id ) {
- global $wgEnableEmail, $wgEnableUserEmail, $wgUser;
- return $wgEnableEmail &&
- $wgEnableUserEmail &&
- $wgUser->isLoggedIn() && # show only to signed in users
- 0 != $id; # we can only email to non-anons ..
-# '' != $id->getEmail() && # who must have an email address stored ..
-# 0 != $id->getEmailauthenticationtimestamp() && # .. which is authenticated
-# 1 != $wgUser->getOption('disablemail'); # and not disabled
+ global $wgUser;
+ $targetUser = User::newFromId( $id );
+ return $wgUser->canSendEmail() && # the sending user must have a confirmed email address
+ $targetUser->canReceiveEmail(); # the target user must have a confirmed email address and allow emails from users
}
function emailUserLink() {
@@ -1496,12 +1651,6 @@ END;
return $s;
}
- function bugReportsLink() {
- $s = $this->makeKnownLink( wfMsgForContent( 'bugreportspage' ),
- wfMsg( 'bugreports' ) );
- return $s;
- }
-
function talkLink() {
global $wgTitle;
@@ -1510,6 +1659,8 @@ END;
return '';
}
+ $linkOptions = array();
+
if( $wgTitle->isTalkPage() ) {
$link = $wgTitle->getSubjectPage();
switch( $link->getNamespace() ) {
@@ -1522,8 +1673,11 @@ END;
case NS_PROJECT:
$text = wfMsg( 'projectpage' );
break;
- case NS_IMAGE:
+ case NS_FILE:
$text = wfMsg( 'imagepage' );
+ # Make link known if image exists, even if the desc. page doesn't.
+ if( wfFindFile( $link ) )
+ $linkOptions[] = 'known';
break;
case NS_MEDIAWIKI:
$text = wfMsg( 'mediawikipage' );
@@ -1545,7 +1699,7 @@ END;
$text = wfMsg( 'talkpage' );
}
- $s = $this->makeLinkObj( $link, $text );
+ $s = $this->link( $link, $text, array(), array(), $linkOptions );
return $s;
}
@@ -1677,19 +1831,17 @@ END;
continue;
if (strpos($line, '**') !== 0) {
$line = trim($line, '* ');
- if ( $line == 'SEARCH' || $line == 'TOOLBOX' || $line == 'LANGUAGES' ) {
- # Special box type
- $bar[$line] = array();
- } else {
- $heading = $line;
- }
+ $heading = $line;
+ if( !array_key_exists($heading, $bar) ) $bar[$heading] = array();
} else {
if (strpos($line, '|') !== false) { // sanity check
$line = array_map('trim', explode( '|' , trim($line, '* '), 2 ) );
$link = wfMsgForContent( $line[0] );
if ($link == '-')
continue;
- if (wfEmptyMsg($line[1], $text = wfMsg($line[1])))
+
+ $text = wfMsgExt($line[1], 'parsemag');
+ if (wfEmptyMsg($line[1], $text))
$text = $line[1];
if (wfEmptyMsg($line[0], $link))
$link = $line[0];
@@ -1715,6 +1867,7 @@ END;
} else { continue; }
}
}
+ wfRunHooks('SkinBuildSidebar', array($this, &$bar));
if ( $wgEnableSidebarCache ) $parserMemc->set( $key, $bar, $wgSidebarCacheExpiry );
wfProfileOut( __METHOD__ );
return $bar;
diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php
index 506d1024..4f13571a 100644
--- a/includes/SkinTemplate.php
+++ b/includes/SkinTemplate.php
@@ -87,12 +87,6 @@ class SkinTemplate extends Skin {
*/
var $template;
- /**
- * An array of strings representing extra CSS files to load. May include:
- * 'IE', 'IE50', 'IE55', 'IE60', 'IE70', 'rtl'.
- */
- var $cssfiles;
-
/**#@-*/
/**
@@ -100,14 +94,23 @@ class SkinTemplate extends Skin {
* Child classes should override this to set the name,
* style subdirectory, and template filler callback.
*
- * @param OutputPage $out
+ * @param $out OutputPage
*/
- function initPage( &$out ) {
+ function initPage( OutputPage $out ) {
parent::initPage( $out );
$this->skinname = 'monobook';
$this->stylename = 'monobook';
$this->template = 'QuickTemplate';
- $this->cssfiles = array();
+ }
+
+ /**
+ * Add specific styles for this skin
+ *
+ * @param $out OutputPage
+ */
+ function setupSkinUserCss( OutputPage $out ){
+ $out->addStyle( 'common/shared.css', 'screen' );
+ $out->addStyle( 'common/commonPrint.css', 'print' );
}
/**
@@ -115,9 +118,9 @@ class SkinTemplate extends Skin {
* and eventually it spits out some HTML. Should have interface
* roughly equivalent to PHPTAL 0.7.
*
- * @param string $callback (or file)
- * @param string $repository subdirectory where we keep template files
- * @param string $cache_dir
+ * @param $callback string (or file)
+ * @param $repository string: subdirectory where we keep template files
+ * @param $cache_dir string
* @return object
* @private
*/
@@ -128,18 +131,17 @@ class SkinTemplate extends Skin {
/**
* initialize various variables and generate the template
*
- * @param OutputPage $out
- * @public
+ * @param $out OutputPage
*/
- function outputPage( &$out ) {
- global $wgTitle, $wgArticle, $wgUser, $wgLang, $wgContLang, $wgOut;
+ function outputPage( OutputPage $out ) {
+ global $wgTitle, $wgArticle, $wgUser, $wgLang, $wgContLang;
global $wgScript, $wgStylePath, $wgContLanguageCode;
global $wgMimeType, $wgJsMimeType, $wgOutputEncoding, $wgRequest;
global $wgXhtmlDefaultNamespace, $wgXhtmlNamespaces;
global $wgDisableCounters, $wgLogo, $action, $wgFeedClasses, $wgHideInterlanguageLinks;
global $wgMaxCredits, $wgShowCreditsIfMax;
global $wgPageShowWatchingUsers;
- global $wgUseTrackbacks;
+ global $wgUseTrackbacks, $wgUseSiteJs;
global $wgArticlePath, $wgScriptPath, $wgServer, $wgLang, $wgCanonicalNamespaceNames;
wfProfileIn( __METHOD__ );
@@ -150,9 +152,7 @@ class SkinTemplate extends Skin {
wfProfileIn( __METHOD__."-init" );
$this->initPage( $out );
- $this->mTitle =& $wgTitle;
- $this->mUser =& $wgUser;
-
+ $this->setMembers();
$tpl = $this->setupTemplate( $this->template, 'skins' );
#if ( $wgUseDatabaseMessages ) { // uncomment this to fall back to GetText
@@ -167,8 +167,6 @@ class SkinTemplate extends Skin {
$this->iscontent = ($this->mTitle->getNamespace() != NS_SPECIAL );
$this->iseditable = ($this->iscontent and !($action == 'edit' or $action == 'submit'));
$this->username = $wgUser->getName();
- $userPage = $wgUser->getUserPage();
- $this->userpage = $userPage->getPrefixedText();
if ( $wgUser->isLoggedIn() || $this->showIPinHeader() ) {
$this->userpageUrlDetails = self::makeUrlDetails( $this->userpage );
@@ -178,17 +176,18 @@ class SkinTemplate extends Skin {
$this->userpageUrlDetails = self::makeKnownUrlDetails( $this->userpage );
}
- $this->usercss = $this->userjs = $this->userjsprev = false;
- $this->setupUserCss();
+ $this->userjs = $this->userjsprev = false;
+ $this->setupUserCss( $out );
$this->setupUserJs( $out->isUserJsAllowed() );
$this->titletxt = $this->mTitle->getPrefixedText();
wfProfileOut( __METHOD__."-stuff" );
wfProfileIn( __METHOD__."-stuff2" );
- $tpl->set( 'title', $wgOut->getPageTitle() );
- $tpl->set( 'pagetitle', $wgOut->getHTMLTitle() );
- $tpl->set( 'displaytitle', $wgOut->mPageLinkTitle );
- $tpl->set( 'pageclass', Sanitizer::escapeClass( 'page-'.$this->mTitle->getPrefixedText() ) );
+ $tpl->set( 'title', $out->getPageTitle() );
+ $tpl->set( 'pagetitle', $out->getHTMLTitle() );
+ $tpl->set( 'displaytitle', $out->mPageLinkTitle );
+ $tpl->set( 'pageclass', $this->getPageClasses( $this->mTitle ) );
+ $tpl->set( 'skinnameclass', ( "skin-" . Sanitizer::escapeClass( $this->getSkinName ( ) ) ) );
$nsname = isset( $wgCanonicalNamespaceNames[ $this->mTitle->getNamespace() ] ) ?
$wgCanonicalNamespaceNames[ $this->mTitle->getNamespace() ] :
@@ -201,7 +200,7 @@ class SkinTemplate extends Skin {
$tpl->set( 'articleid', $this->mTitle->getArticleId() );
$tpl->set( 'currevisionid', isset( $wgArticle ) ? $wgArticle->getLatest() : 0 );
- $tpl->set( 'isarticle', $wgOut->isArticle() );
+ $tpl->set( 'isarticle', $out->isArticle() );
$tpl->setRef( "thispage", $this->thispage );
$subpagestr = $this->subPageSubtitle();
@@ -218,9 +217,9 @@ class SkinTemplate extends Skin {
);
$tpl->set( 'catlinks', $this->getCategories());
- if( $wgOut->isSyndicated() ) {
+ if( $out->isSyndicated() ) {
$feeds = array();
- foreach( $wgOut->getSyndicationLinks() as $format => $link ) {
+ foreach( $out->getSyndicationLinks() as $format => $link ) {
$feeds[$format] = array(
'text' => wfMsg( "feed-$format" ),
'href' => $link );
@@ -241,14 +240,15 @@ class SkinTemplate extends Skin {
$tpl->setRef( 'jsmimetype', $wgJsMimeType );
$tpl->setRef( 'charset', $wgOutputEncoding );
$tpl->set( 'headlinks', $out->getHeadLinks() );
- $tpl->set('headscripts', $out->getScript() );
+ $tpl->set( 'headscripts', $out->getScript() );
+ $tpl->set( 'csslinks', $out->buildCssLinks() );
$tpl->setRef( 'wgScript', $wgScript );
$tpl->setRef( 'skinname', $this->skinname );
$tpl->set( 'skinclass', get_class( $this ) );
$tpl->setRef( 'stylename', $this->stylename );
$tpl->set( 'printable', $wgRequest->getBool( 'printable' ) );
+ $tpl->set( 'handheld', $wgRequest->getBool( 'handheld' ) );
$tpl->setRef( 'loggedin', $this->loggedin );
- $tpl->set('nsclass', 'ns-'.$this->mTitle->getNamespace());
$tpl->set('notspecialpage', $this->mTitle->getNamespace() != NS_SPECIAL);
/* XXX currently unused, might get useful later
$tpl->set( "editable", ($this->mTitle->getNamespace() != NS_SPECIAL ) );
@@ -274,12 +274,10 @@ class SkinTemplate extends Skin {
$tpl->setRef( 'userpageurl', $this->userpageUrlDetails['href']);
$tpl->set( 'userlang', $wgLang->getCode() );
$tpl->set( 'pagecss', $this->setupPageCss() );
- $tpl->set( 'printcss', $this->getPrintCss() );
$tpl->setRef( 'usercss', $this->usercss);
$tpl->setRef( 'userjs', $this->userjs);
$tpl->setRef( 'userjsprev', $this->userjsprev);
- global $wgUseSiteJs;
- if ($wgUseSiteJs) {
+ if( $wgUseSiteJs ) {
$jsCache = $this->loggedin ? '&smaxage=0' : '';
$tpl->set( 'jsvarurl',
self::makeUrl('-',
@@ -307,18 +305,18 @@ class SkinTemplate extends Skin {
)
);
# Disable Cache
- $wgOut->setSquidMaxage(0);
+ $out->setSquidMaxage(0);
}
} else if (count($newtalks)) {
- $sep = str_replace("_", " ", wfMsgHtml("newtalkseperator"));
+ $sep = str_replace("_", " ", wfMsgHtml("newtalkseparator"));
$msgs = array();
foreach ($newtalks as $newtalk) {
- $msgs[] = wfElement("a",
+ $msgs[] = Xml::element("a",
array('href' => $newtalk["link"]), $newtalk["wiki"]);
}
$parts = implode($sep, $msgs);
$ntl = wfMsgHtml('youhavenewmessagesmulti', $parts);
- $wgOut->setSquidMaxage(0);
+ $out->setSquidMaxage(0);
} else {
$ntl = '';
}
@@ -326,9 +324,9 @@ class SkinTemplate extends Skin {
wfProfileIn( __METHOD__."-stuff3" );
$tpl->setRef( 'newtalk', $ntl );
- $tpl->setRef( 'skin', $this);
+ $tpl->setRef( 'skin', $this );
$tpl->set( 'logo', $this->logoText() );
- if ( $wgOut->isArticle() and (!isset( $oldid ) or isset( $diff )) and
+ if ( $out->isArticle() and (!isset( $oldid ) or isset( $diff )) and
$wgArticle and 0 != $wgArticle->getID() )
{
if ( !$wgDisableCounters ) {
@@ -367,11 +365,10 @@ class SkinTemplate extends Skin {
$this->credits = false;
- if (isset($wgMaxCredits) && $wgMaxCredits != 0) {
- require_once("Credits.php");
- $this->credits = getCredits($wgArticle, $wgMaxCredits, $wgShowCreditsIfMax);
+ if( $wgMaxCredits != 0 ){
+ $this->credits = Credits::getCredits( $wgArticle, $wgMaxCredits, $wgShowCreditsIfMax );
} else {
- $tpl->set('lastmod', $this->lastModified());
+ $tpl->set( 'lastmod', $this->lastModified() );
}
$tpl->setRef( 'credits', $this->credits );
@@ -411,16 +408,18 @@ class SkinTemplate extends Skin {
$language_urls = array();
if ( !$wgHideInterlanguageLinks ) {
- foreach( $wgOut->getLanguageLinks() as $l ) {
+ foreach( $out->getLanguageLinks() as $l ) {
$tmp = explode( ':', $l, 2 );
$class = 'interwiki-' . $tmp[0];
unset($tmp);
$nt = Title::newFromText( $l );
- $language_urls[] = array(
- 'href' => $nt->getFullURL(),
- 'text' => ($wgContLang->getLanguageName( $nt->getInterwiki()) != ''?$wgContLang->getLanguageName( $nt->getInterwiki()) : $l),
- 'class' => $class
- );
+ if ( $nt ) {
+ $language_urls[] = array(
+ 'href' => $nt->getFullURL(),
+ 'text' => ($wgContLang->getLanguageName( $nt->getInterwiki()) != ''?$wgContLang->getLanguageName( $nt->getInterwiki()) : $l),
+ 'class' => $class
+ );
+ }
}
}
if(count($language_urls)) {
@@ -430,6 +429,7 @@ class SkinTemplate extends Skin {
}
wfProfileOut( __METHOD__."-stuff4" );
+ wfProfileIn( __METHOD__."-stuff5" );
# Personal toolbar
$tpl->set('personal_urls', $this->buildPersonalUrls());
$content_actions = $this->buildContentActionUrls();
@@ -438,7 +438,7 @@ class SkinTemplate extends Skin {
// XXX: attach this from javascript, same with section editing
if($this->iseditable && $wgUser->getOption("editondblclick") )
{
- $encEditUrl = wfEscapeJsString( $this->mTitle->getLocalUrl( $this->editUrlOptions() ) );
+ $encEditUrl = Xml::escapeJsString( $this->mTitle->getLocalUrl( $this->editUrlOptions() ) );
$tpl->set('body_ondblclick', 'document.location = "' . $encEditUrl . '";');
} else {
$tpl->set('body_ondblclick', false);
@@ -452,6 +452,11 @@ class SkinTemplate extends Skin {
wfDebug( __METHOD__ . ': Hook SkinTemplateOutputPageBeforeExec broke outputPage execution!' );
}
+ // allow extensions adding stuff after the page content.
+ // See Skin::afterContentHook() for further documentation.
+ $tpl->set ('dataAfterContent', $this->afterContentHook());
+ wfProfileOut( __METHOD__."-stuff5" );
+
// execute template
wfProfileIn( __METHOD__."-execute" );
$res = $tpl->execute();
@@ -589,9 +594,9 @@ class SkinTemplate extends Skin {
if( $selected ) {
$classes[] = 'selected';
}
- if( $checkEdit && !$title->isAlwaysKnown() && $title->getArticleId() == 0 ) {
+ if( $checkEdit && !$title->isKnown() ) {
$classes[] = 'new';
- $query = 'action=edit';
+ $query = 'action=edit&redlink=1';
}
$text = wfMsg( $message );
@@ -641,7 +646,7 @@ class SkinTemplate extends Skin {
* @return array
* @private
*/
- function buildContentActionUrls () {
+ function buildContentActionUrls() {
global $wgContLang, $wgLang, $wgOut;
wfProfileIn( __METHOD__ );
@@ -690,7 +695,7 @@ class SkinTemplate extends Skin {
'href' => $this->mTitle->getLocalUrl( 'action=edit&section=new' )
);
}
- } elseif ( $this->mTitle->exists() || $this->mTitle->isAlwaysKnown() ) {
+ } elseif ( $this->mTitle->isKnown() ) {
$content_actions['viewsource'] = array(
'class' => ($action == 'edit') ? 'selected' : false,
'text' => wfMsg('viewsource'),
@@ -700,7 +705,7 @@ class SkinTemplate extends Skin {
wfProfileOut( __METHOD__."-edit" );
wfProfileIn( __METHOD__."-live" );
- if ( $this->mTitle->getArticleId() ) {
+ if ( $this->mTitle->exists() ) {
$content_actions['history'] = array(
'class' => ($action == 'history') ? 'selected' : false,
@@ -708,7 +713,7 @@ class SkinTemplate extends Skin {
'href' => $this->mTitle->getLocalUrl( 'action=history')
);
- if($wgUser->isAllowed('delete')){
+ if( $wgUser->isAllowed('delete') ) {
$content_actions['delete'] = array(
'class' => ($action == 'delete') ? 'selected' : false,
'text' => wfMsg('delete'),
@@ -725,7 +730,7 @@ class SkinTemplate extends Skin {
}
if ( $this->mTitle->getNamespace() !== NS_MEDIAWIKI && $wgUser->isAllowed( 'protect' ) ) {
- if(!$this->mTitle->isProtected()){
+ if( !$this->mTitle->isProtected() ){
$content_actions['protect'] = array(
'class' => ($action == 'protect') ? 'selected' : false,
'text' => wfMsg('protect'),
@@ -837,7 +842,7 @@ class SkinTemplate extends Skin {
* @return array
* @private
*/
- function buildNavUrls () {
+ function buildNavUrls() {
global $wgUseTrackbacks, $wgTitle, $wgUser, $wgRequest;
global $wgEnableUploads, $wgUploadNavigationUrl;
@@ -847,7 +852,7 @@ class SkinTemplate extends Skin {
$nav_urls = array();
$nav_urls['mainpage'] = array( 'href' => self::makeMainPageUrl() );
- if( $wgEnableUploads ) {
+ if( $wgEnableUploads && $wgUser->isAllowed( 'upload' ) ) {
if ($wgUploadNavigationUrl) {
$nav_urls['upload'] = array( 'href' => $wgUploadNavigationUrl );
} else {
@@ -952,70 +957,13 @@ class SkinTemplate extends Skin {
* @return string
* @private
*/
- function getNameSpaceKey () {
+ function getNameSpaceKey() {
return $this->mTitle->getNamespaceKey();
}
/**
* @private
*/
- function setupUserCss() {
- wfProfileIn( __METHOD__ );
-
- global $wgRequest, $wgAllowUserCss, $wgUseSiteCss, $wgContLang, $wgSquidMaxage, $wgStylePath, $wgUser;
-
- $sitecss = '';
- $usercss = '';
- $siteargs = '&maxage=' . $wgSquidMaxage;
- if( $this->loggedin ) {
- // Ensure that logged-in users' generated CSS isn't clobbered
- // by anons' publicly cacheable generated CSS.
- $siteargs .= '&smaxage=0';
- }
-
- # Add user-specific code if this is a user and we allow that kind of thing
-
- if ( $wgAllowUserCss && $this->loggedin ) {
- $action = $wgRequest->getText('action');
-
- # if we're previewing the CSS page, use it
- if( $this->mTitle->isCssSubpage() and $this->userCanPreview( $action ) ) {
- $siteargs = "&smaxage=0&maxage=0";
- $usercss = $wgRequest->getText('wpTextbox1');
- } else {
- $usercss = '@import "' .
- self::makeUrl($this->userpage . '/'.$this->skinname.'.css',
- 'action=raw&ctype=text/css') . '";' ."\n";
- }
-
- $siteargs .= '&ts=' . $wgUser->mTouched;
- }
-
- if( $wgContLang->isRTL() && in_array( 'rtl', $this->cssfiles ) ) {
- global $wgStyleVersion;
- $sitecss .= "@import \"$wgStylePath/$this->stylename/rtl.css?$wgStyleVersion\";\n";
- }
-
- # 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 = "&useskin=" . urlencode( $this->getSkinName() );
- $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$skinquery" ) . '";' . "\n";
- }
-
- # If we use any dynamic CSS, make a little CDATA block out of it.
-
- if ( !empty($sitecss) || !empty($usercss) ) {
- $this->usercss = "/*<![CDATA[*/\n" . $sitecss . $usercss . '/*]]>*/';
- }
- wfProfileOut( __METHOD__ );
- }
-
- /**
- * @private
- */
function setupUserJs( $allowUserJs ) {
wfProfileIn( __METHOD__ );
@@ -1043,63 +991,9 @@ class SkinTemplate extends Skin {
wfProfileIn( __METHOD__ );
$out = false;
wfRunHooks( 'SkinTemplateSetupPageCss', array( &$out ) );
-
wfProfileOut( __METHOD__ );
return $out;
}
-
- /**
- * returns css with user-specific options
- */
- public function getUserStylesheet() {
- wfProfileIn( __METHOD__ );
-
- $s = "/* generated user stylesheet */\n";
- $s .= $this->reallyDoGetUserStyles();
- wfProfileOut( __METHOD__ );
- return $s;
- }
-
- /**
- * Returns the print stylesheet for this skin. In all default skins this
- * is just commonPrint.css, but third-party skins may want to modify it.
- *
- * @return string
- */
- protected function getPrintCss() {
- global $wgStylePath;
- return $wgStylePath . "/common/commonPrint.css";
- }
-
- /**
- * This returns MediaWiki:Common.js and MediaWiki:[Skinname].js concate-
- * nated together. For some bizarre reason, it does *not* return any
- * custom user JS from subpages. Huh?
- *
- * There's absolutely no reason to have separate Monobook/Common JSes.
- * Any JS that cares can just check the skin variable generated at the
- * top. For now Monobook.js will be maintained, but it should be consi-
- * dered deprecated.
- *
- * @return string
- */
- public function getUserJs() {
- wfProfileIn( __METHOD__ );
-
- $s = parent::getUserJs();
- $s .= "\n\n/* MediaWiki:".ucfirst($this->skinname).".js */\n";
-
- // avoid inclusion of non defined user JavaScript (with custom skins only)
- // by checking for default message content
- $msgKey = ucfirst($this->skinname).'.js';
- $userJS = wfMsgForContent($msgKey);
- if ( !wfEmptyMsg( $msgKey, $userJS ) ) {
- $s .= $userJS;
- }
-
- wfProfileOut( __METHOD__ );
- return $s;
- }
}
/**
diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php
index d6ad6e6e..00eacd1e 100644
--- a/includes/SpecialPage.php
+++ b/includes/SpecialPage.php
@@ -89,14 +89,17 @@ class SpecialPage
'CreateAccount' => array( 'SpecialRedirectToSpecial', 'CreateAccount', 'Userlogin', 'signup', array( 'uselang' ) ),
'Preferences' => array( 'SpecialPage', 'Preferences' ),
'Watchlist' => array( 'SpecialPage', 'Watchlist' ),
+ 'Resetpass' => 'SpecialResetpass',
+
'Recentchanges' => 'SpecialRecentchanges',
'Upload' => array( 'SpecialPage', 'Upload' ),
- 'Imagelist' => array( 'SpecialPage', 'Imagelist' ),
+ 'Listfiles' => array( 'SpecialPage', 'Listfiles' ),
'Newimages' => array( 'IncludableSpecialPage', 'Newimages' ),
'Listusers' => array( 'SpecialPage', 'Listusers' ),
'Listgrouprights' => 'SpecialListGroupRights',
- 'Statistics' => array( 'SpecialPage', 'Statistics' ),
+ 'DeletedContributions' => 'DeletedContributionsPage',
+ 'Statistics' => 'SpecialStatistics',
'Randompage' => 'Randompage',
'Lonelypages' => array( 'SpecialPage', 'Lonelypages' ),
'Uncategorizedpages' => array( 'SpecialPage', 'Uncategorizedpages' ),
@@ -107,6 +110,8 @@ class SpecialPage
'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ),
'Wantedpages' => array( 'IncludableSpecialPage', 'Wantedpages' ),
'Wantedcategories' => array( 'SpecialPage', 'Wantedcategories' ),
+ 'Wantedfiles' => array( 'SpecialPage', 'Wantedfiles' ),
+ 'Wantedtemplates' => array( 'SpecialPage', 'Wantedtemplates' ),
'Mostlinked' => array( 'SpecialPage', 'Mostlinked' ),
'Mostlinkedcategories' => array( 'SpecialPage', 'Mostlinkedcategories' ),
'Mostlinkedtemplates' => array( 'SpecialPage', 'Mostlinkedtemplates' ),
@@ -121,27 +126,27 @@ class SpecialPage
'Deadendpages' => array( 'SpecialPage', 'Deadendpages' ),
'Protectedpages' => array( 'SpecialPage', 'Protectedpages' ),
'Protectedtitles' => array( 'SpecialPage', 'Protectedtitles' ),
- 'Allpages' => array( 'IncludableSpecialPage', 'Allpages' ),
- 'Prefixindex' => array( 'IncludableSpecialPage', 'Prefixindex' ) ,
+ 'Allpages' => 'SpecialAllpages',
+ 'Prefixindex' => 'SpecialPrefixindex',
'Ipblocklist' => array( 'SpecialPage', 'Ipblocklist' ),
'Specialpages' => array( 'UnlistedSpecialPage', 'Specialpages' ),
- 'Contributions' => array( 'SpecialPage', 'Contributions' ),
+ 'Contributions' => 'SpecialContributions',
'Emailuser' => array( 'UnlistedSpecialPage', 'Emailuser' ),
'Whatlinkshere' => array( 'SpecialPage', 'Whatlinkshere' ),
+ 'LinkSearch' => array( 'SpecialPage', 'LinkSearch' ),
'Recentchangeslinked' => 'SpecialRecentchangeslinked',
'Movepage' => array( 'UnlistedSpecialPage', 'Movepage' ),
'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ),
- 'Resetpass' => array( 'UnlistedSpecialPage', 'Resetpass' ),
'Booksources' => 'SpecialBookSources',
'Categories' => array( 'SpecialPage', 'Categories' ),
'Export' => array( 'SpecialPage', 'Export' ),
- 'Version' => array( 'SpecialPage', 'Version' ),
+ 'Version' => 'SpecialVersion',
'Blankpage' => array( 'UnlistedSpecialPage', 'Blankpage' ),
'Allmessages' => array( 'SpecialPage', 'Allmessages' ),
'Log' => array( 'SpecialPage', 'Log' ),
'Blockip' => array( 'SpecialPage', 'Blockip', 'block' ),
'Undelete' => array( 'SpecialPage', 'Undelete', 'deletedhistory' ),
- 'Import' => array( 'SpecialPage', 'Import', 'import' ),
+ 'Import' => 'SpecialImport',
'Lockdb' => array( 'SpecialPage', 'Lockdb', 'siteadmin' ),
'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ),
'Userrights' => 'UserrightsPage',
@@ -484,7 +489,7 @@ class SpecialPage
if ( !$page ) {
if ( !$including ) {
$wgOut->setArticleRelated( false );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setStatusCode( 404 );
$wgOut->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' );
}
@@ -576,7 +581,7 @@ class SpecialPage
if ( $subpage !== false && !is_null( $subpage ) ) {
$name = "$name/$subpage";
}
- return $name;
+ return ucfirst( $name );
}
/**
@@ -740,14 +745,8 @@ class SpecialPage
if(!is_callable($func) and $this->mFile) {
require_once( $this->mFile );
}
- # FIXME: these hooks are broken for extensions and anything else that subclasses SpecialPage.
- if ( wfRunHooks( 'SpecialPageExecuteBeforeHeader', array( &$this, &$par, &$func ) ) )
- $this->outputHeader();
- if ( ! wfRunHooks( 'SpecialPageExecuteBeforePage', array( &$this, &$par, &$func ) ) )
- return;
+ $this->outputHeader();
call_user_func( $func, $par, $this );
- if ( ! wfRunHooks( 'SpecialPageExecuteAfterPage', array( &$this, &$par, &$func ) ) )
- return;
} else {
$this->displayRestrictionError();
}
diff --git a/includes/SquidUpdate.php b/includes/SquidUpdate.php
index f69d1f0b..c8497a83 100644
--- a/includes/SquidUpdate.php
+++ b/includes/SquidUpdate.php
@@ -6,7 +6,7 @@
*/
/**
- * @todo document
+ * Handles purging appropriate Squid URLs given a title (or titles)
* @ingroup Cache
*/
class SquidUpdate {
diff --git a/includes/StringUtils.php b/includes/StringUtils.php
index 70d0bff1..c437b3c1 100644
--- a/includes/StringUtils.php
+++ b/includes/StringUtils.php
@@ -167,6 +167,18 @@ class StringUtils {
$string = str_replace( '$', '\\$', $string );
return $string;
}
+
+ /**
+ * Workalike for explode() with limited memory usage.
+ * Returns an Iterator
+ */
+ static function explode( $separator, $subject ) {
+ if ( substr_count( $subject, $separator ) > 1000 ) {
+ return new ExplodeIterator( $separator, $subject );
+ } else {
+ return new ArrayIterator( explode( $separator, $subject ) );
+ }
+ }
}
/**
@@ -310,3 +322,90 @@ class ReplacementArray {
return $result;
}
}
+
+/**
+ * An iterator which works exactly like:
+ *
+ * foreach ( explode( $delim, $s ) as $element ) {
+ * ...
+ * }
+ *
+ * Except it doesn't use 193 byte per element
+ */
+class ExplodeIterator implements Iterator {
+ // The subject string
+ var $subject, $subjectLength;
+
+ // The delimiter
+ var $delim, $delimLength;
+
+ // The position of the start of the line
+ var $curPos;
+
+ // The position after the end of the next delimiter
+ var $endPos;
+
+ // The current token
+ var $current;
+
+ /**
+ * Construct a DelimIterator
+ */
+ function __construct( $delim, $s ) {
+ $this->subject = $s;
+ $this->delim = $delim;
+
+ // Micro-optimisation (theoretical)
+ $this->subjectLength = strlen( $s );
+ $this->delimLength = strlen( $delim );
+
+ $this->rewind();
+ }
+
+ function rewind() {
+ $this->curPos = 0;
+ $this->endPos = strpos( $this->subject, $this->delim );
+ $this->refreshCurrent();
+ }
+
+
+ function refreshCurrent() {
+ if ( $this->curPos === false ) {
+ $this->current = false;
+ } elseif ( $this->curPos >= $this->subjectLength ) {
+ $this->current = '';
+ } elseif ( $this->endPos === false ) {
+ $this->current = substr( $this->subject, $this->curPos );
+ } else {
+ $this->current = substr( $this->subject, $this->curPos, $this->endPos - $this->curPos );
+ }
+ }
+
+ function current() {
+ return $this->current;
+ }
+
+ function key() {
+ return $this->curPos;
+ }
+
+ function next() {
+ if ( $this->endPos === false ) {
+ $this->curPos = false;
+ } else {
+ $this->curPos = $this->endPos + $this->delimLength;
+ if ( $this->curPos >= $this->subjectLength ) {
+ $this->endPos = false;
+ } else {
+ $this->endPos = strpos( $this->subject, $this->delim, $this->curPos );
+ }
+ }
+ $this->refreshCurrent();
+ return $this->current;
+ }
+
+ function valid() {
+ return $this->curPos !== false;
+ }
+}
+
diff --git a/includes/StubObject.php b/includes/StubObject.php
index ec52e7f4..e27f0b25 100644
--- a/includes/StubObject.php
+++ b/includes/StubObject.php
@@ -95,7 +95,7 @@ class StubObject {
if ( ++$recursionLevel > 2 ) {
throw new MWException( "Unstub loop detected on call of \${$this->mGlobal}->$name from $caller\n" );
}
- wfDebug( "Unstubbing \${$this->mGlobal} on call of \${$this->mGlobal}->$name from $caller\n" );
+ wfDebug( "Unstubbing \${$this->mGlobal} on call of \${$this->mGlobal}::$name from $caller\n" );
$GLOBALS[$this->mGlobal] = $this->_newObject();
--$recursionLevel;
wfProfileOut( $fname );
diff --git a/includes/Title.php b/includes/Title.php
index 6326240c..515a3b65 100644
--- a/includes/Title.php
+++ b/includes/Title.php
@@ -4,97 +4,86 @@
* @file
*/
-/** */
if ( !class_exists( 'UtfNormal' ) ) {
require_once( dirname(__FILE__) . '/normal/UtfNormal.php' );
}
define ( 'GAID_FOR_UPDATE', 1 );
-# Title::newFromTitle maintains a cache to avoid
-# expensive re-normalization of commonly used titles.
-# On a batch operation this can become a memory leak
-# if not bounded. After hitting this many titles,
-# reset the cache.
-define( 'MW_TITLECACHE_MAX', 1000 );
-# Constants for pr_cascade bitfield
+/**
+ * Constants for pr_cascade bitfield
+ */
define( 'CASCADE', 1 );
/**
- * Title class
- * - Represents a title, which may contain an interwiki designation or namespace
- * - Can fetch various kinds of data from the database, albeit inefficiently.
- *
+ * Represents a title within MediaWiki.
+ * Optionally may contain an interwiki designation or namespace.
+ * @note This class can fetch various kinds of data from the database;
+ * however, it does so inefficiently.
*/
class Title {
- /**
- * Static cache variables
- */
+ /** @name Static cache variables */
+ //@{
static private $titleCache=array();
static private $interwikiCache=array();
-
+ //@}
/**
- * All member variables should be considered private
- * Please use the accessor functions
+ * Title::newFromText maintains a cache to avoid expensive re-normalization of
+ * commonly used titles. On a batch operation this can become a memory leak
+ * if not bounded. After hitting this many titles reset the cache.
*/
+ const CACHE_MAX = 1000;
+
- /**#@+
+ /**
+ * @name Private member variables
+ * Please use the accessor functions instead.
* @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 #)
- var $mArticleID; # Article ID, fetched from the link cache on demand
- var $mLatestID; # ID of most recent revision
- var $mRestrictions; # Array of groups allowed to edit this article
- var $mCascadeRestriction; # Cascade restrictions on this page to included templates and images?
- var $mRestrictionsExpiry; # When do the restrictions on this page expire?
- var $mHasCascadingRestrictions; # Are cascading restrictions in effect on this page?
- var $mCascadeRestrictionSources;# Where are the cascading restrictions coming from on this page?
- var $mRestrictionsLoaded; # Boolean for initialisation on demand
- var $mPrefixedText; # Text form including namespace/interwiki, initialised on demand
- var $mDefaultNamespace; # Namespace index when there is no namespace
- # Zero except in {{transclusion}} tags
- var $mWatched; # Is $wgUser watching this page? NULL if unfilled, accessed through userIsWatching()
- var $mLength; # The page length, 0 for special pages
- var $mRedirect; # Is the article at this title a redirect?
- /**#@-*/
+ //@{
+
+ 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 = NS_MAIN; ///< 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 #)
+ var $mArticleID = -1; ///< Article ID, fetched from the link cache on demand
+ var $mLatestID = false; ///< ID of most recent revision
+ var $mRestrictions = array(); ///< Array of groups allowed to edit this article
+ var $mOldRestrictions = false;
+ var $mCascadeRestriction; ///< Cascade restrictions on this page to included templates and images?
+ var $mRestrictionsExpiry = array(); ///< When do the restrictions on this page expire?
+ var $mHasCascadingRestrictions; ///< Are cascading restrictions in effect on this page?
+ var $mCascadeSources; ///< Where are the cascading restrictions coming from on this page?
+ var $mRestrictionsLoaded = false; ///< Boolean for initialisation on demand
+ var $mPrefixedText; ///< Text form including namespace/interwiki, initialised on demand
+ # Don't change the following default, NS_MAIN is hardcoded in several
+ # places. See bug 696.
+ var $mDefaultNamespace = NS_MAIN; ///< Namespace index when there is no namespace
+ # Zero except in {{transclusion}} tags
+ var $mWatched = null; ///< Is $wgUser watching this page? null if unfilled, accessed through userIsWatching()
+ var $mLength = -1; ///< The page length, 0 for special pages
+ var $mRedirect = null; ///< Is the article at this title a redirect?
+ var $mNotificationTimestamp = array(); ///< Associative array of user ID -> timestamp/false
+ //@}
/**
* Constructor
* @private
*/
- /* private */ function __construct() {
- $this->mInterwiki = $this->mUrlform =
- $this->mTextform = $this->mDbkeyform = '';
- $this->mArticleID = -1;
- $this->mNamespace = NS_MAIN;
- $this->mRestrictionsLoaded = false;
- $this->mRestrictions = array();
- # Dont change the following, NS_MAIN is hardcoded in several place
- # See bug #696
- $this->mDefaultNamespace = NS_MAIN;
- $this->mWatched = NULL;
- $this->mLatestID = false;
- $this->mOldRestrictions = false;
- $this->mLength = -1;
- $this->mRedirect = NULL;
- }
+ /* private */ function __construct() {}
/**
* Create a new Title from a prefixed DB key
- * @param string $key The database key, which has underscores
+ * @param $key \type{\string} The database key, which has underscores
* instead of spaces, possibly including namespace and
* interwiki prefixes
- * @return Title the new object, or NULL on an error
+ * @return \type{Title} the new object, or NULL on an error
*/
public static function newFromDBkey( $key ) {
$t = new Title();
@@ -106,15 +95,16 @@ class Title {
}
/**
- * Create a new Title from text, such as what one would
- * find in a link. Decodes any HTML entities in the text.
+ * Create a new Title from text, such as what one would find in a link. De-
+ * codes any HTML entities in the text.
*
- * @param string $text the link text; spaces, prefixes,
- * and an initial ':' indicating the main namespace
- * are accepted
- * @param int $defaultNamespace the namespace to use if
- * none is specified by a prefix
- * @return Title the new object, or NULL on an error
+ * @param $text string The link text; spaces, prefixes, and an
+ * initial ':' indicating the main namespace are accepted.
+ * @param $defaultNamespace int The namespace to use if none is speci-
+ * fied by a prefix. If you want to force a specific namespace even if
+ * $text might begin with a namespace prefix, use makeTitle() or
+ * makeTitleSafe().
+ * @return Title The new object, or null on an error.
*/
public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
if( is_object( $text ) ) {
@@ -145,7 +135,7 @@ class Title {
static $cachedcount = 0 ;
if( $t->secureAndSplit() ) {
if( $defaultNamespace == NS_MAIN ) {
- if( $cachedcount >= MW_TITLECACHE_MAX ) {
+ if( $cachedcount >= self::CACHE_MAX ) {
# Avoid memory leaks on mass operations...
Title::$titleCache = array();
$cachedcount=0;
@@ -163,8 +153,8 @@ class Title {
/**
* Create a new Title from URL-encoded text. Ensures that
* the given title's length does not exceed the maximum.
- * @param string $url the title, as might be taken from a URL
- * @return Title the new object, or NULL on an error
+ * @param $url \type{\string} the title, as might be taken from a URL
+ * @return \type{Title} the new object, or NULL on an error
*/
public static function newFromURL( $url ) {
global $wgLegalTitleChars;
@@ -191,9 +181,9 @@ class Title {
* @todo This is inefficiently implemented, the page row is requested
* but not used for anything else
*
- * @param int $id the page_id corresponding to the Title to create
- * @param int $flags, use GAID_FOR_UPDATE to use master
- * @return Title the new object, or NULL on an error
+ * @param $id \type{\int} the page_id corresponding to the Title to create
+ * @param $flags \type{\int} use GAID_FOR_UPDATE to use master
+ * @return \type{Title} the new object, or NULL on an error
*/
public static function newFromID( $id, $flags = 0 ) {
$fname = 'Title::newFromID';
@@ -210,6 +200,8 @@ class Title {
/**
* Make an array of titles from an array of IDs
+ * @param $ids \type{\arrayof{\int}} Array of IDs
+ * @return \type{\arrayof{Title}} Array of Titles
*/
public static function newFromIDs( $ids ) {
if ( !count( $ids ) ) {
@@ -220,7 +212,7 @@ class Title {
'page_id IN (' . $dbr->makeList( $ids ) . ')', __METHOD__ );
$titles = array();
- while ( $row = $dbr->fetchObject( $res ) ) {
+ foreach( $res as $row ) {
$titles[] = Title::makeTitle( $row->page_namespace, $row->page_title );
}
return $titles;
@@ -228,7 +220,8 @@ class Title {
/**
* Make a Title object from a DB row
- * @param Row $row (needs at least page_title,page_namespace)
+ * @param $row \type{Row} (needs at least page_title,page_namespace)
+ * @return \type{Title} corresponding Title
*/
public static function newFromRow( $row ) {
$t = self::makeTitle( $row->page_namespace, $row->page_title );
@@ -248,10 +241,10 @@ class Title {
* For convenience, spaces are converted to underscores so that
* eg user_text fields can be used directly.
*
- * @param int $ns the namespace of the article
- * @param string $title the unprefixed database key form
- * @param string $fragment The link fragment (after the "#")
- * @return Title the new object
+ * @param $ns \type{\int} the namespace of the article
+ * @param $title \type{\string} the unprefixed database key form
+ * @param $fragment \type{\string} The link fragment (after the "#")
+ * @return \type{Title} the new object
*/
public static function &makeTitle( $ns, $title, $fragment = '' ) {
$t = new Title();
@@ -270,10 +263,10 @@ class Title {
* The parameters will be checked for validity, which is a bit slower
* than makeTitle() but safer for user-provided data.
*
- * @param int $ns the namespace of the article
- * @param string $title the database key form
- * @param string $fragment The link fragment (after the "#")
- * @return Title the new object, or NULL on an error
+ * @param $ns \type{\int} the namespace of the article
+ * @param $title \type{\string} the database key form
+ * @param $fragment \type{\string} The link fragment (after the "#")
+ * @return \type{Title} the new object, or NULL on an error
*/
public static function makeTitleSafe( $ns, $title, $fragment = '' ) {
$t = new Title();
@@ -287,7 +280,7 @@ class Title {
/**
* Create a new Title for the Main Page
- * @return Title the new object
+ * @return \type{Title} the new object
*/
public static function newMainPage() {
$title = Title::newFromText( wfMsgForContent( 'mainpage' ) );
@@ -302,15 +295,18 @@ class Title {
* 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
+ * @param $text \type{String} Text with possible redirect
+ * @return \type{Title} The corresponding Title
*/
public static function newFromRedirect( $text ) {
$redir = MagicWord::get( 'redirect' );
- if( $redir->matchStart( trim($text) ) ) {
+ $text = trim($text);
+ if( $redir->matchStartAndRemove( $text ) ) {
// Extract the first link and see if it's usable
+ // Ensure that it really does come directly after #REDIRECT
+ // Some older redirects included a colon, so don't freak about that!
$m = array();
- if( preg_match( '!\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
+ if( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
// Strip preceding colon used to "escape" categories, etc.
// and URL-decode links
if( strpos( $m[1], '%' ) !== false ) {
@@ -338,26 +334,26 @@ class Title {
/**
* Get the prefixed DB key associated with an ID
- * @param int $id the page_id of the article
- * @return Title an object representing the article, or NULL
+ * @param $id \type{\int} the page_id of the article
+ * @return \type{Title} an object representing the article, or NULL
* if no such article was found
- * @static
- * @access public
*/
- function nameOf( $id ) {
- $fname = 'Title::nameOf';
+ public static function nameOf( $id ) {
$dbr = wfGetDB( DB_SLAVE );
- $s = $dbr->selectRow( 'page', array( 'page_namespace','page_title' ), array( 'page_id' => $id ), $fname );
+ $s = $dbr->selectRow( 'page',
+ array( 'page_namespace','page_title' ),
+ array( 'page_id' => $id ),
+ __METHOD__ );
if ( $s === false ) { return NULL; }
- $n = Title::makeName( $s->page_namespace, $s->page_title );
+ $n = self::makeName( $s->page_namespace, $s->page_title );
return $n;
}
/**
* Get a regex character class describing the legal characters in a link
- * @return string the list of characters, not delimited
+ * @return \type{\string} the list of characters, not delimited
*/
public static function legalChars() {
global $wgLegalTitleChars;
@@ -368,9 +364,9 @@ class Title {
* Get a string representation of a title suitable for
* including in a search index
*
- * @param int $ns a namespace index
- * @param string $title text-form main part
- * @return string a stripped-down title string ready for the
+ * @param $ns \type{\int} a namespace index
+ * @param $title \type{\string} text-form main part
+ * @return \type{\string} a stripped-down title string ready for the
* search index
*/
public static function indexTitle( $ns, $title ) {
@@ -387,7 +383,7 @@ class Title {
$t = preg_replace( "/\\s+/", ' ', $t );
- if ( $ns == NS_IMAGE ) {
+ if ( $ns == NS_FILE ) {
$t = preg_replace( "/ (png|gif|jpg|jpeg|ogg)$/", "", $t );
}
return trim( $t );
@@ -395,10 +391,10 @@ class Title {
/*
* Make a prefixed DB key from a DB key and a namespace index
- * @param int $ns numerical representation of the namespace
- * @param string $title the DB key form the title
- * @param string $fragment The link fragment (after the "#")
- * @return string the prefixed form of the title
+ * @param $ns \type{\int} numerical representation of the namespace
+ * @param $title \type{\string} the DB key form the title
+ * @param $fragment \type{\string} The link fragment (after the "#")
+ * @return \type{\string} the prefixed form of the title
*/
public static function makeName( $ns, $title, $fragment = '' ) {
global $wgContLang;
@@ -413,111 +409,26 @@ class Title {
/**
* Returns the URL associated with an interwiki prefix
- * @param string $key the interwiki prefix (e.g. "MeatBall")
- * @return the associated URL, containing "$1", which should be
- * replaced by an article title
+ * @param $key \type{\string} the interwiki prefix (e.g. "MeatBall")
+ * @return \type{\string} the associated URL, containing "$1",
+ * which should be replaced by an article title
* @static (arguably)
+ * @deprecated See Interwiki class
*/
public function getInterwikiLink( $key ) {
- global $wgMemc, $wgInterwikiExpiry;
- global $wgInterwikiCache, $wgContLang;
- $fname = 'Title::getInterwikiLink';
-
- $key = $wgContLang->lc( $key );
-
- $k = wfMemcKey( 'interwiki', $key );
- if( array_key_exists( $k, Title::$interwikiCache ) ) {
- return Title::$interwikiCache[$k]->iw_url;
- }
-
- if ($wgInterwikiCache) {
- return Title::getInterwikiCached( $key );
- }
-
- $s = $wgMemc->get( $k );
- # Ignore old keys with no iw_local
- if( $s && isset( $s->iw_local ) && isset($s->iw_trans)) {
- Title::$interwikiCache[$k] = $s;
- return $s->iw_url;
- }
-
- $dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( 'interwiki',
- array( 'iw_url', 'iw_local', 'iw_trans' ),
- array( 'iw_prefix' => $key ), $fname );
- if( !$res ) {
- return '';
- }
-
- $s = $dbr->fetchObject( $res );
- if( !$s ) {
- # Cache non-existence: create a blank object and save it to memcached
- $s = (object)false;
- $s->iw_url = '';
- $s->iw_local = 0;
- $s->iw_trans = 0;
- }
- $wgMemc->set( $k, $s, $wgInterwikiExpiry );
- Title::$interwikiCache[$k] = $s;
-
- return $s->iw_url;
+ return Interwiki::fetch( $key )->getURL( );
}
/**
- * Fetch interwiki prefix data from local cache in constant database
- *
- * More logic is explained in DefaultSettings
- *
- * @return string URL of interwiki site
- */
- public static function getInterwikiCached( $key ) {
- global $wgInterwikiCache, $wgInterwikiScopes, $wgInterwikiFallbackSite;
- static $db, $site;
-
- if (!$db)
- $db=dba_open($wgInterwikiCache,'r','cdb');
- /* Resolve site name */
- if ($wgInterwikiScopes>=3 and !$site) {
- $site = dba_fetch('__sites:' . wfWikiID(), $db);
- if ($site=="")
- $site = $wgInterwikiFallbackSite;
- }
- $value = dba_fetch( wfMemcKey( $key ), $db);
- if ($value=='' and $wgInterwikiScopes>=3) {
- /* try site-level */
- $value = dba_fetch("_{$site}:{$key}", $db);
- }
- if ($value=='' and $wgInterwikiScopes>=2) {
- /* try globals */
- $value = dba_fetch("__global:{$key}", $db);
- }
- if ($value=='undef')
- $value='';
- $s = (object)false;
- $s->iw_url = '';
- $s->iw_local = 0;
- $s->iw_trans = 0;
- if ($value!='') {
- list($local,$url)=explode(' ',$value,2);
- $s->iw_url=$url;
- $s->iw_local=(int)$local;
- }
- Title::$interwikiCache[wfMemcKey( 'interwiki', $key )] = $s;
- return $s->iw_url;
- }
- /**
* Determine whether the object refers to a page within
* this project.
*
- * @return bool TRUE if this is an in-project interwiki link
+ * @return \type{\bool} TRUE if this is an in-project interwiki link
* or a wikilink, FALSE otherwise
*/
public function isLocal() {
if ( $this->mInterwiki != '' ) {
- # Make sure key is loaded into cache
- $this->getInterwikiLink( $this->mInterwiki );
- $k = wfMemcKey( 'interwiki', $this->mInterwiki );
- return (bool)(Title::$interwikiCache[$k]->iw_local);
+ return Interwiki::fetch( $this->mInterwiki )->isLocal();
} else {
return true;
}
@@ -527,28 +438,26 @@ class Title {
* Determine whether the object refers to a page within
* this project and is transcludable.
*
- * @return bool TRUE if this is transcludable
+ * @return \type{\bool} TRUE if this is transcludable
*/
public function isTrans() {
if ($this->mInterwiki == '')
return false;
- # Make sure key is loaded into cache
- $this->getInterwikiLink( $this->mInterwiki );
- $k = wfMemcKey( 'interwiki', $this->mInterwiki );
- return (bool)(Title::$interwikiCache[$k]->iw_trans);
+
+ return Interwiki::fetch( $this->mInterwiki )->isTranscludable();
}
/**
* Escape a text fragment, say from a link, for a URL
*/
static function escapeFragmentForURL( $fragment ) {
- $fragment = str_replace( ' ', '_', $fragment );
- $fragment = urlencode( Sanitizer::decodeCharReferences( $fragment ) );
- $replaceArray = array(
- '%3A' => ':',
- '%' => '.'
- );
- return strtr( $fragment, $replaceArray );
+ global $wgEnforceHtmlIds;
+ # Note that we don't urlencode the fragment. urlencoded Unicode
+ # fragments appear not to work in IE (at least up to 7) or in at least
+ # one version of Opera 9.x. The W3C validator, for one, doesn't seem
+ # to care if they aren't encoded.
+ return Sanitizer::escapeId( $fragment,
+ $wgEnforceHtmlIds ? 'noninitial' : 'xml' );
}
#----------------------------------------------------------------------------
@@ -558,27 +467,27 @@ class Title {
/** Simple accessors */
/**
* Get the text form (spaces not underscores) of the main part
- * @return string
+ * @return \type{\string} Main part of the title
*/
public function getText() { return $this->mTextform; }
/**
* Get the URL-encoded form of the main part
- * @return string
+ * @return \type{\string} Main part of the title, URL-encoded
*/
public function getPartialURL() { return $this->mUrlform; }
/**
* Get the main part with underscores
- * @return string
+ * @return \type{\string} Main part of the title, with underscores
*/
public function getDBkey() { return $this->mDbkeyform; }
/**
- * Get the namespace index, i.e. one of the NS_xxxx constants
- * @return int
+ * Get the namespace index, i.e.\ one of the NS_xxxx constants.
+ * @return \type{\int} Namespace index
*/
public function getNamespace() { return $this->mNamespace; }
/**
* Get the namespace text
- * @return string
+ * @return \type{\string} Namespace text
*/
public function getNsText() {
global $wgContLang, $wgCanonicalNamespaceNames;
@@ -598,49 +507,47 @@ class Title {
}
/**
* Get the DB key with the initial letter case as specified by the user
+ * @return \type{\string} DB key
*/
function getUserCaseDBKey() {
return $this->mUserCaseDBKey;
}
/**
* Get the namespace text of the subject (rather than talk) page
- * @return string
+ * @return \type{\string} Namespace text
*/
public function getSubjectNsText() {
global $wgContLang;
return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) );
}
-
/**
* Get the namespace text of the talk page
- * @return string
+ * @return \type{\string} Namespace text
*/
public function getTalkNsText() {
global $wgContLang;
return( $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ) );
}
-
/**
* Could this title have a corresponding talk page?
- * @return bool
+ * @return \type{\bool} TRUE or FALSE
*/
public function canTalk() {
return( MWNamespace::canTalk( $this->mNamespace ) );
}
-
/**
* Get the interwiki prefix (or null string)
- * @return string
+ * @return \type{\string} Interwiki prefix
*/
public function getInterwiki() { return $this->mInterwiki; }
/**
- * Get the Title fragment (i.e. the bit after the #) in text form
- * @return string
+ * Get the Title fragment (i.e.\ the bit after the #) in text form
+ * @return \type{\string} Title fragment
*/
public function getFragment() { return $this->mFragment; }
/**
* Get the fragment in URL form, including the "#" character if there is one
- * @return string
+ * @return \type{\string} Fragment in URL form
*/
public function getFragmentForURL() {
if ( $this->mFragment == '' ) {
@@ -651,13 +558,13 @@ class Title {
}
/**
* Get the default namespace index, for when there is no namespace
- * @return int
+ * @return \type{\int} Default namespace index
*/
public function getDefaultNamespace() { return $this->mDefaultNamespace; }
/**
* Get title for search index
- * @return string a stripped-down title string ready for the
+ * @return \type{\string} a stripped-down title string ready for the
* search index
*/
public function getIndexTitle() {
@@ -666,7 +573,7 @@ class Title {
/**
* Get the prefixed database key form
- * @return string the prefixed title, with underscores and
+ * @return \type{\string} the prefixed title, with underscores and
* any interwiki and namespace prefixes
*/
public function getPrefixedDBkey() {
@@ -678,7 +585,7 @@ class Title {
/**
* Get the prefixed title with spaces.
* This is the form usually used for display
- * @return string the prefixed title, with spaces
+ * @return \type{\string} the prefixed title, with spaces
*/
public function getPrefixedText() {
if ( empty( $this->mPrefixedText ) ) { // FIXME: bad usage of empty() ?
@@ -692,7 +599,7 @@ class Title {
/**
* Get the prefixed title with spaces, plus any fragment
* (part beginning with '#')
- * @return string the prefixed title, with spaces and
+ * @return \type{\string} the prefixed title, with spaces and
* the fragment, including '#'
*/
public function getFullText() {
@@ -705,7 +612,7 @@ class Title {
/**
* Get the base name, i.e. the leftmost parts before the /
- * @return string Base name
+ * @return \type{\string} Base name
*/
public function getBaseText() {
if( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
@@ -721,7 +628,7 @@ class Title {
/**
* Get the lowest-level subpage name, i.e. the rightmost part after /
- * @return string Subpage name
+ * @return \type{\string} Subpage name
*/
public function getSubpageText() {
if( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
@@ -733,29 +640,21 @@ class Title {
/**
* Get a URL-encoded form of the subpage text
- * @return string URL-encoded subpage name
+ * @return \type{\string} URL-encoded subpage name
*/
public function getSubpageUrlForm() {
$text = $this->getSubpageText();
$text = wfUrlencode( str_replace( ' ', '_', $text ) );
- $text = str_replace( '%28', '(', str_replace( '%29', ')', $text ) ); # Clean up the URL; per below, this might not be safe
return( $text );
}
/**
* Get a URL-encoded title (not an actual URL) including interwiki
- * @return string the URL-encoded form
+ * @return \type{\string} the URL-encoded form
*/
public function getPrefixedURL() {
$s = $this->prefix( $this->mDbkeyform );
- $s = str_replace( ' ', '_', $s );
-
- $s = wfUrlencode ( $s ) ;
-
- # Cleaning up URL to make it look nice -- is this safe?
- $s = str_replace( '%28', '(', $s );
- $s = str_replace( '%29', ')', $s );
-
+ $s = wfUrlencode( str_replace( ' ', '_', $s ) );
return $s;
}
@@ -763,15 +662,21 @@ class Title {
* Get a real URL referring to this title, with interwiki link and
* fragment
*
- * @param string $query an optional query string, not used
- * for interwiki links
- * @param string $variant language variant of url (for sr, zh..)
- * @return string the URL
+ * @param $query \twotypes{\string,\array} an optional query string, not used for interwiki
+ * links. Can be specified as an associative array as well, e.g.,
+ * array( 'action' => 'edit' ) (keys and values will be URL-escaped).
+ * @param $variant \type{\string} language variant of url (for sr, zh..)
+ * @return \type{\string} the URL
*/
public function getFullURL( $query = '', $variant = false ) {
global $wgContLang, $wgServer, $wgRequest;
- if ( '' == $this->mInterwiki ) {
+ if( is_array( $query ) ) {
+ $query = wfArrayToCGI( $query );
+ }
+
+ $interwiki = Interwiki::fetch( $this->mInterwiki );
+ if ( !$interwiki ) {
$url = $this->getLocalUrl( $query, $variant );
// Ugly quick hack to avoid duplicate prefixes (bug 4571 etc)
@@ -780,7 +685,7 @@ class Title {
$url = $wgServer . $url;
}
} else {
- $baseUrl = $this->getInterwikiLink( $this->mInterwiki );
+ $baseUrl = $interwiki->getURL( );
$namespace = wfUrlencode( $this->getNsText() );
if ( '' != $namespace ) {
@@ -802,15 +707,21 @@ class Title {
/**
* Get a URL with no fragment or server name. If this page is generated
* with action=render, $wgServer is prepended.
- * @param string $query an optional query string; if not specified,
- * $wgArticlePath will be used.
- * @param string $variant language variant of url (for sr, zh..)
- * @return string the URL
+ * @param mixed $query an optional query string; if not specified,
+ * $wgArticlePath will be used. Can be specified as an associative array
+ * as well, e.g., array( 'action' => 'edit' ) (keys and values will be
+ * URL-escaped).
+ * @param $variant \type{\string} language variant of url (for sr, zh..)
+ * @return \type{\string} the URL
*/
public function getLocalURL( $query = '', $variant = false ) {
global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
global $wgVariantArticlePath, $wgContLang, $wgUser;
+ if( is_array( $query ) ) {
+ $query = wfArrayToCGI( $query );
+ }
+
// internal links should point to same variant as current page (only anonymous users)
if($variant == false && $wgContLang->hasVariants() && !$wgUser->isLoggedIn()){
$pref = $wgContLang->getPreferredVariant(false);
@@ -853,7 +764,9 @@ class Title {
$query = $matches[1];
if( isset( $matches[4] ) ) $query .= $matches[4];
$url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
- if( $query != '' ) $url .= '?' . $query;
+ if( $query != '' ) {
+ $url = wfAppendQuery( $url, $query );
+ }
}
}
if ( $url === false ) {
@@ -875,10 +788,40 @@ class Title {
}
/**
+ * Get a URL that's the simplest URL that will be valid to link, locally,
+ * to the current Title. It includes the fragment, but does not include
+ * the server unless action=render is used (or the link is external). If
+ * there's a fragment but the prefixed text is empty, we just return a link
+ * to the fragment.
+ *
+ * @param $query \type{\arrayof{\string}} An associative array of key => value pairs for the
+ * query string. Keys and values will be escaped.
+ * @param $variant \type{\string} Language variant of URL (for sr, zh..). Ignored
+ * for external links. Default is "false" (same variant as current page,
+ * for anonymous users).
+ * @return \type{\string} the URL
+ */
+ public function getLinkUrl( $query = array(), $variant = false ) {
+ if( !is_array( $query ) ) {
+ throw new MWException( 'Title::getLinkUrl passed a non-array for '.
+ '$query' );
+ }
+ if( $this->isExternal() ) {
+ return $this->getFullURL( $query );
+ } elseif( $this->getPrefixedText() === ''
+ and $this->getFragment() !== '' ) {
+ return $this->getFragmentForURL();
+ } else {
+ return $this->getLocalURL( $query, $variant )
+ . $this->getFragmentForURL();
+ }
+ }
+
+ /**
* Get an HTML-escaped version of the URL form, suitable for
* using in a link, without a server name or fragment
- * @param string $query an optional query string
- * @return string the URL
+ * @param $query \type{\string} an optional query string
+ * @return \type{\string} the URL
*/
public function escapeLocalURL( $query = '' ) {
return htmlspecialchars( $this->getLocalURL( $query ) );
@@ -888,8 +831,8 @@ class Title {
* Get an HTML-escaped version of the URL form, suitable for
* using in a link, including the server name and fragment
*
- * @return string the URL
- * @param string $query an optional query string
+ * @param $query \type{\string} an optional query string
+ * @return \type{\string} the URL
*/
public function escapeFullURL( $query = '' ) {
return htmlspecialchars( $this->getFullURL( $query ) );
@@ -900,9 +843,9 @@ class Title {
* - Used in various Squid-related code, in case we have a different
* internal hostname for the server from the exposed one.
*
- * @param string $query an optional query string
- * @param string $variant language variant of url (for sr, zh..)
- * @return string the URL
+ * @param $query \type{\string} an optional query string
+ * @param $variant \type{\string} language variant of url (for sr, zh..)
+ * @return \type{\string} the URL
*/
public function getInternalURL( $query = '', $variant = false ) {
global $wgInternalServer;
@@ -913,7 +856,7 @@ class Title {
/**
* Get the edit URL for this Title
- * @return string the URL, or a null string if this is an
+ * @return \type{\string} the URL, or a null string if this is an
* interwiki link
*/
public function getEditURL() {
@@ -926,7 +869,7 @@ class Title {
/**
* Get the HTML-escaped displayable text form.
* Used for the title field in <a> tags.
- * @return string the text, including any prefixes
+ * @return \type{\string} the text, including any prefixes
*/
public function getEscapedText() {
return htmlspecialchars( $this->getPrefixedText() );
@@ -934,15 +877,15 @@ class Title {
/**
* Is this Title interwiki?
- * @return boolean
+ * @return \type{\bool}
*/
public function isExternal() { return ( '' != $this->mInterwiki ); }
/**
* Is this page "semi-protected" - the *only* protection is autoconfirm?
*
- * @param string Action to check (default: edit)
- * @return bool
+ * @param @action \type{\string} Action to check (default: edit)
+ * @return \type{\bool}
*/
public function isSemiProtected( $action = 'edit' ) {
if( $this->exists() ) {
@@ -965,9 +908,9 @@ class Title {
/**
* Does the title correspond to a protected article?
- * @param string $what the action the page is protected from,
+ * @param $what \type{\string} the action the page is protected from,
* by default checks move and edit
- * @return boolean
+ * @return \type{\bool}
*/
public function isProtected( $action = '' ) {
global $wgRestrictionLevels, $wgRestrictionTypes;
@@ -993,7 +936,7 @@ class Title {
/**
* Is $wgUser watching this page?
- * @return boolean
+ * @return \type{\bool}
*/
public function userIsWatching() {
global $wgUser;
@@ -1017,8 +960,8 @@ class Title {
*
* May provide false positives, but should never provide a false negative.
*
- * @param string $action action that permission needs to be checked for
- * @return boolean
+ * @param $action \type{\string} action that permission needs to be checked for
+ * @return \type{\bool}
*/
public function quickUserCan( $action ) {
return $this->userCan( $action, false );
@@ -1028,7 +971,7 @@ class Title {
* Determines if $wgUser is unable to edit this page because it has been protected
* by $wgNamespaceProtection.
*
- * @return boolean
+ * @return \type{\bool}
*/
public function isNamespaceProtected() {
global $wgNamespaceProtection, $wgUser;
@@ -1043,9 +986,9 @@ class Title {
/**
* Can $wgUser 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 boolean
+ * @param $action \type{\string} action that permission needs to be checked for
+ * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
+ * @return \type{\bool}
*/
public function userCan( $action, $doExpensiveQueries = true ) {
global $wgUser;
@@ -1057,11 +1000,11 @@ class Title {
*
* FIXME: This *does not* check throttles (User::pingLimiter()).
*
- * @param string $action action that permission needs to be checked for
- * @param User $user user to check
- * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries.
- * @param array $ignoreErrors Set this to a list of message keys whose corresponding errors may be ignored.
- * @return array Array of arrays of the arguments to wfMsg to explain permissions problems.
+ * @param $action \type{\string}action that permission needs to be checked for
+ * @param $user \type{User} user to check
+ * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
+ * @param $ignoreErrors \type{\arrayof{\string}} Set this to a list of message keys whose corresponding errors may be ignored.
+ * @return \type{\array} Array of arrays of the arguments to wfMsg to explain permissions problems.
*/
public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) {
if( !StubObject::isRealObject( $user ) ) {
@@ -1080,7 +1023,8 @@ class Title {
$errors[] = array( 'confirmedittext' );
}
- if ( $user->isBlockedFrom( $this ) && $action != 'createaccount' ) {
+ // Edit blocks should not affect reading. Account creation blocks handled at userlogin.
+ if ( $user->isBlockedFrom( $this ) && $action != 'read' && $action != 'createaccount' ) {
$block = $user->mBlock;
// This is from OutputPage::blockedPage
@@ -1147,10 +1091,10 @@ class Title {
* 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 User $user user to check
- * @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.
+ * @param $action \type{\string} action that permission needs to be checked for
+ * @param $user \type{User} user to check
+ * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
+ * @return \type{\array} Array of arrays of the arguments to wfMsg to explain permissions problems.
*/
private function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true ) {
wfProfileIn( __METHOD__ );
@@ -1158,61 +1102,55 @@ class Title {
$errors = array();
// Use getUserPermissionsErrors instead
- if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) {
+ if( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) {
wfProfileOut( __METHOD__ );
return $result ? array() : array( array( 'badaccess-group0' ) );
}
- if (!wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) {
- if ($result != array() && is_array($result) && !is_array($result[0]))
+ if( !wfRunHooks( 'getUserPermissionsErrors', array(&$this,&$user,$action,&$result) ) ) {
+ if( is_array($result) && count($result) && !is_array($result[0]) )
$errors[] = $result; # A single array representing an error
- else if (is_array($result) && is_array($result[0]))
+ else if( is_array($result) && is_array($result[0]) )
$errors = array_merge( $errors, $result ); # A nested array representing multiple errors
- else if ($result != '' && $result != null && $result !== true && $result !== false)
+ else if( $result !== '' && is_string($result) )
$errors[] = array($result); # A string representing a message-id
- else if ($result === false )
+ else if( $result === false )
$errors[] = array('badaccess-group0'); # a generic "We don't want them to do that"
}
- if ($doExpensiveQueries && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) ) {
- if ($result != array() && is_array($result) && !is_array($result[0]))
+ if( $doExpensiveQueries && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array(&$this,&$user,$action,&$result) ) ) {
+ if( is_array($result) && count($result) && !is_array($result[0]) )
$errors[] = $result; # A single array representing an error
- else if (is_array($result) && is_array($result[0]))
+ else if( is_array($result) && is_array($result[0]) )
$errors = array_merge( $errors, $result ); # A nested array representing multiple errors
- else if ($result != '' && $result != null && $result !== true && $result !== false)
+ else if( $result !== '' && is_string($result) )
$errors[] = array($result); # A string representing a message-id
- else if ($result === false )
+ else if( $result === false )
$errors[] = array('badaccess-group0'); # a generic "We don't want them to do that"
}
+ // TODO: document
$specialOKActions = array( 'createaccount', 'execute' );
if( NS_SPECIAL == $this->mNamespace && !in_array( $action, $specialOKActions) ) {
$errors[] = array('ns-specialprotected');
}
- 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...
- $errors[] = array('badaccess-group0');
+ if( $this->isNamespaceProtected() ) {
+ $ns = $this->getNamespace() == NS_MAIN ?
+ wfMsg( 'nstab-main' ) : $this->getNsText();
+ $errors[] = NS_MEDIAWIKI == $this->mNamespace ?
+ array('protectedinterface') : array( 'namespaceprotected', $ns );
}
# 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()
- && !$user->isAllowed('editusercssjs')
- && !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) ) {
+ if( $this->isCssJsSubpage() && !$user->isAllowed('editusercssjs')
+ && !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) )
+ {
$errors[] = array('customcssjsprotected');
}
- if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) {
+ if( $doExpensiveQueries && !$this->isCssJsSubpage() ) {
# We /could/ use the protection level on the source page, but it's fairly ugly
# as we have to establish a precedence hierarchy for pages included by multiple
# cascade-protected pages. So just restrict it to people with 'protect' permission,
@@ -1237,18 +1175,18 @@ class Title {
foreach( $this->getRestrictions($action) as $right ) {
// Backwards compatibility, rewrite sysop -> protect
- if ( $right == 'sysop' ) {
+ if( $right == 'sysop' ) {
$right = 'protect';
}
if( '' != $right && !$user->isAllowed( $right ) ) {
- //Users with 'editprotected' permission can edit protected pages
+ // Users with 'editprotected' permission can edit protected pages
if( $action=='edit' && $user->isAllowed( 'editprotected' ) ) {
- //Users with 'editprotected' permission cannot edit protected pages
- //with cascading option turned on.
- if($this->mCascadeRestriction) {
+ // Users with 'editprotected' permission cannot edit protected pages
+ // with cascading option turned on.
+ if( $this->mCascadeRestriction ) {
$errors[] = array( 'protectedpagetext', $right );
} else {
- //Nothing, user can edit!
+ // Nothing, user can edit!
}
} else {
$errors[] = array( 'protectedpagetext', $right );
@@ -1256,57 +1194,76 @@ class Title {
}
}
- if ($action == 'protect') {
- if ($this->getUserPermissionsErrors('edit', $user) != array()) {
+ if( $action == 'protect' ) {
+ if( $this->getUserPermissionsErrors('edit', $user) != array() ) {
$errors[] = array( 'protect-cantedit' ); // If they can't edit, they shouldn't protect.
}
}
- if ($action == 'create') {
+ if( $action == 'create' ) {
$title_protection = $this->getTitleProtection();
+ if( is_array($title_protection) ) {
+ extract($title_protection); // is this extract() really needed?
- if (is_array($title_protection)) {
- extract($title_protection);
-
- if ($pt_create_perm == 'sysop')
- $pt_create_perm = 'protect';
-
- if ($pt_create_perm == '' || !$user->isAllowed($pt_create_perm)) {
- $errors[] = array ( 'titleprotected', User::whoIs($pt_user), $pt_reason );
+ if( $pt_create_perm == 'sysop' ) {
+ $pt_create_perm = 'protect'; // B/C
+ }
+ if( $pt_create_perm == '' || !$user->isAllowed($pt_create_perm) ) {
+ $errors[] = array( 'titleprotected', User::whoIs($pt_user), $pt_reason );
}
}
- if( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
- ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) {
+ 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');
- } elseif ( !$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 ) {
- $groups[] = '[['.$groupPage->getPrefixedText().'|'.$groupName.']]';
- } else {
- $groups[] = $groupName;
- }
- }
+ } elseif( $action == 'move' ) {
+ if( !$user->isAllowed( 'move' ) ) {
+ // User can't move anything
+ $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed');
+ } elseif( !$user->isAllowed( 'move-rootuserpages' )
+ && $this->getNamespace() == NS_USER && !$this->isSubpage() )
+ {
+ // Show user page-specific message only if the user can move other pages
+ $errors[] = array( 'cant-move-user-page' );
+ }
+ // Check if user is allowed to move files if it's a file
+ if( $this->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
+ $errors[] = array( 'movenotallowedfile' );
+ }
+ // Check for immobile pages
+ if( !MWNamespace::isMovable( $this->getNamespace() ) ) {
+ // Specific message for this case
+ $errors[] = array( 'immobile-source-namespace', $this->getNsText() );
+ } elseif( !$this->isMovable() ) {
+ // Less specific message for rarer cases
+ $errors[] = array( 'immobile-page' );
}
- $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 );
+ } elseif( $action == 'move-target' ) {
+ if( !$user->isAllowed( 'move' ) ) {
+ // User can't move anything
+ $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed');
+ } elseif( !$user->isAllowed( 'move-rootuserpages' )
+ && $this->getNamespace() == NS_USER && !$this->isSubpage() )
+ {
+ // Show user page-specific message only if the user can move other pages
+ $errors[] = array( 'cant-move-to-user-page' );
+ }
+ if( !MWNamespace::isMovable( $this->getNamespace() ) ) {
+ $errors[] = array( 'immobile-target-namespace', $this->getNsText() );
+ } elseif( !$this->isMovable() ) {
+ $errors[] = array( 'immobile-target-page' );
+ }
+ } elseif( !$user->isAllowed( $action ) ) {
+ $return = null;
+ $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
+ User::getGroupsWithPermission( $action ) );
+ if( $groups ) {
+ $return = array( 'badaccess-groups',
+ array( implode( ', ', $groups ), count( $groups ) ) );
+ } else {
+ $return = array( "badaccess-group0" );
}
$errors[] = $return;
}
@@ -1317,7 +1274,7 @@ class Title {
/**
* Is this title subject to title protection?
- * @return mixed An associative array representing any existent title
+ * @return \type{\mixed} An associative array representing any existent title
* protection, or false if there's none.
*/
private function getTitleProtection() {
@@ -1328,7 +1285,8 @@ class Title {
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'protected_titles', '*',
- array ('pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey()) );
+ array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
+ __METHOD__ );
if ($row = $dbr->fetchRow( $res )) {
return $row;
@@ -1337,11 +1295,17 @@ class Title {
}
}
+ /**
+ * Update the title protection status
+ * @param $create_perm \type{\string} Permission required for creation
+ * @param $reason \type{\string} Reason for protection
+ * @param $expiry \type{\string} Expiry timestamp
+ */
public function updateTitleProtection( $create_perm, $reason, $expiry ) {
- global $wgGroupPermissions,$wgUser,$wgContLang;
+ global $wgUser,$wgContLang;
if ($create_perm == implode(',',$this->getRestrictions('create'))
- && $expiry == $this->mRestrictionsExpiry) {
+ && $expiry == $this->mRestrictionsExpiry['create']) {
// No change
return true;
}
@@ -1354,9 +1318,12 @@ class Title {
$expiry_description = '';
if ( $encodedExpiry != 'infinity' ) {
- $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) ).')';
+ $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) , $wgContLang->date( $expiry ) , $wgContLang->time( $expiry ) ).')';
}
-
+ else {
+ $expiry_description .= ' (' . wfMsgForContent( 'protect-expiry-indefinite' ).')';
+ }
+
# Update protection table
if ($create_perm != '' ) {
$dbw->replace( 'protected_titles', array(array('pt_namespace', 'pt_title')),
@@ -1373,7 +1340,8 @@ class Title {
$log = new LogPage( 'protect' );
if( $create_perm ) {
- $log->addEntry( $this->mRestrictions['create'] ? 'modify' : 'protect', $this, trim( $reason . " [create=$create_perm] $expiry_description" ) );
+ $params = array("[create=$create_perm] $expiry_description",'');
+ $log->addEntry( $this->mRestrictions['create'] ? 'modify' : 'protect', $this, trim( $reason ), $params );
} else {
$log->addEntry( 'unprotect', $this, $reason );
}
@@ -1382,18 +1350,19 @@ class Title {
}
/**
- * Remove any title protection (due to page existing
+ * Remove any title protection due to page existing
*/
public function deleteTitleProtection() {
$dbw = wfGetDB( DB_MASTER );
$dbw->delete( 'protected_titles',
- array ('pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey()), __METHOD__ );
+ array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
+ __METHOD__ );
}
/**
* Can $wgUser edit this page?
- * @return boolean
+ * @return \type{\bool} TRUE or FALSE
* @deprecated use userCan('edit')
*/
public function userCanEdit( $doExpensiveQueries = true ) {
@@ -1402,7 +1371,7 @@ class Title {
/**
* Can $wgUser create this page?
- * @return boolean
+ * @return \type{\bool} TRUE or FALSE
* @deprecated use userCan('create')
*/
public function userCanCreate( $doExpensiveQueries = true ) {
@@ -1411,7 +1380,7 @@ class Title {
/**
* Can $wgUser move this page?
- * @return boolean
+ * @return \type{\bool} TRUE or FALSE
* @deprecated use userCan('move')
*/
public function userCanMove( $doExpensiveQueries = true ) {
@@ -1422,16 +1391,15 @@ class Title {
* Would anybody with sufficient privileges be able to move this page?
* Some pages just aren't movable.
*
- * @return boolean
+ * @return \type{\bool} TRUE or FALSE
*/
public function isMovable() {
- return MWNamespace::isMovable( $this->getNamespace() )
- && $this->getInterwiki() == '';
+ return MWNamespace::isMovable( $this->getNamespace() ) && $this->getInterwiki() == '';
}
/**
* Can $wgUser read this page?
- * @return boolean
+ * @return \type{\bool} TRUE or FALSE
* @todo fold these checks into userCan()
*/
public function userCanRead() {
@@ -1508,7 +1476,7 @@ class Title {
/**
* Is this a talk page of some sort?
- * @return bool
+ * @return \type{\bool} TRUE or FALSE
*/
public function isTalkPage() {
return MWNamespace::isTalk( $this->getNamespace() );
@@ -1516,7 +1484,7 @@ class Title {
/**
* Is this a subpage?
- * @return bool
+ * @return \type{\bool} TRUE or FALSE
*/
public function isSubpage() {
return MWNamespace::hasSubpages( $this->mNamespace )
@@ -1526,7 +1494,7 @@ class Title {
/**
* Does this have subpages? (Warning, usually requires an extra DB query.)
- * @return bool
+ * @return \type{\bool} TRUE or FALSE
*/
public function hasSubpages() {
if( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
@@ -1554,7 +1522,7 @@ class Title {
* Could this page contain custom CSS or JavaScript, based
* on the title?
*
- * @return bool
+ * @return \type{\bool} TRUE or FALSE
*/
public function isCssOrJsPage() {
return $this->mNamespace == NS_MEDIAWIKI
@@ -1563,7 +1531,7 @@ class Title {
/**
* Is this a .css or .js subpage of a user page?
- * @return bool
+ * @return \type{\bool} TRUE or FALSE
*/
public function isCssJsSubpage() {
return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
@@ -1571,6 +1539,7 @@ class Title {
/**
* Is this a *valid* .css or .js subpage of a user page?
* Check that the corresponding skin exists
+ * @return \type{\bool} TRUE or FALSE
*/
public function isValidCssJsSubpage() {
if ( $this->isCssJsSubpage() ) {
@@ -1590,14 +1559,14 @@ class Title {
}
/**
* Is this a .css subpage of a user page?
- * @return bool
+ * @return \type{\bool} TRUE or FALSE
*/
public function isCssSubpage() {
return ( NS_USER == $this->mNamespace && preg_match("/\\/.*\\.css$/", $this->mTextform ) );
}
/**
* Is this a .js subpage of a user page?
- * @return bool
+ * @return \type{\bool} TRUE or FALSE
*/
public function isJsSubpage() {
return ( NS_USER == $this->mNamespace && preg_match("/\\/.*\\.js$/", $this->mTextform ) );
@@ -1606,7 +1575,7 @@ class Title {
* Protect css/js subpages of user pages: can $wgUser edit
* this page?
*
- * @return boolean
+ * @return \type{\bool} TRUE or FALSE
* @todo XXX: this might be better using restrictions
*/
public function userCanEditCssJsSubpage() {
@@ -1617,7 +1586,7 @@ class Title {
/**
* Cascading protection: Return true if cascading restrictions apply to this page, false if not.
*
- * @return bool If the page is subject to cascading restrictions.
+ * @return \type{\bool} If the page is subject to cascading restrictions.
*/
public function isCascadeProtected() {
list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
@@ -1627,10 +1596,10 @@ class Title {
/**
* Cascading protection: Get the source of any cascading restrictions on this page.
*
- * @param $get_pages bool Whether or not to retrieve the actual pages that the restrictions have come from.
- * @return array( mixed title array, restriction array)
- * Array of the Title objects of the pages from which cascading restrictions have come, false for none, or true if such restrictions exist, but $get_pages was not set.
- * The restriction array is an array of each type, each of which contains an array of unique groups
+ * @param $get_pages \type{\bool} Whether or not to retrieve the actual pages that the restrictions have come from.
+ * @return \type{\arrayof{mixed title array, restriction array}} Array of the Title objects of the pages from
+ * which cascading restrictions have come, false for none, or true if such restrictions exist, but $get_pages was not set.
+ * The restriction array is an array of each type, each of which contains an array of unique groups.
*/
public function getCascadeProtectionSources( $get_pages = true ) {
global $wgRestrictionTypes;
@@ -1648,9 +1617,9 @@ class Title {
wfProfileIn( __METHOD__ );
- $dbr = wfGetDb( DB_SLAVE );
+ $dbr = wfGetDB( DB_SLAVE );
- if ( $this->getNamespace() == NS_IMAGE ) {
+ if ( $this->getNamespace() == NS_FILE ) {
$tables = array ('imagelinks', 'page_restrictions');
$where_clauses = array(
'il_to' => $this->getDBkey(),
@@ -1679,7 +1648,7 @@ class Title {
$now = wfTimestampNow();
$purgeExpired = false;
- while( $row = $dbr->fetchObject( $res ) ) {
+ foreach( $res as $row ) {
$expiry = Block::decodeExpiry( $row->pr_expiry );
if( $expiry > $now ) {
if ($get_pages) {
@@ -1712,7 +1681,6 @@ class Title {
} else {
$this->mHasCascadingRestrictions = $sources;
}
-
return array( $sources, $pagerestrictions );
}
@@ -1726,7 +1694,7 @@ class Title {
/**
* Loads a string into mRestrictions array
- * @param resource $res restrictions as an SQL result.
+ * @param $res \type{Resource} restrictions as an SQL result.
*/
private function loadRestrictionsFromRow( $res, $oldFashionedRestrictions = NULL ) {
global $wgRestrictionTypes;
@@ -1734,10 +1702,10 @@ class Title {
foreach( $wgRestrictionTypes as $type ){
$this->mRestrictions[$type] = array();
+ $this->mRestrictionsExpiry[$type] = Block::decodeExpiry('');
}
$this->mCascadeRestriction = false;
- $this->mRestrictionsExpiry = Block::decodeExpiry('');
# Backwards-compatibility: also load the restrictions from the page record (old format).
@@ -1768,7 +1736,7 @@ class Title {
$now = wfTimestampNow();
$purgeExpired = false;
- while ($row = $dbr->fetchObject( $res ) ) {
+ foreach( $res as $row ) {
# Cycle through all the restrictions.
// Don't take care of restrictions types that aren't in $wgRestrictionTypes
@@ -1781,7 +1749,7 @@ class Title {
// Only apply the restrictions if they haven't expired!
if ( !$expiry || $expiry > $now ) {
- $this->mRestrictionsExpiry = $expiry;
+ $this->mRestrictionsExpiry[$row->pr_type] = $expiry;
$this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
$this->mCascadeRestriction |= $row->pr_cascade;
@@ -1799,6 +1767,9 @@ class Title {
$this->mRestrictionsLoaded = true;
}
+ /**
+ * Load restrictions from the page_restrictions table
+ */
public function loadRestrictions( $oldFashionedRestrictions = NULL ) {
if( !$this->mRestrictionsLoaded ) {
if ($this->exists()) {
@@ -1819,13 +1790,13 @@ class Title {
if (!$expiry || $expiry > $now) {
// Apply the restrictions
- $this->mRestrictionsExpiry = $expiry;
+ $this->mRestrictionsExpiry['create'] = $expiry;
$this->mRestrictions['create'] = explode(',', trim($pt_create_perm) );
} else { // Get rid of the old restrictions
Title::purgeExpiredRestrictions();
}
} else {
- $this->mRestrictionsExpiry = Block::decodeExpiry('');
+ $this->mRestrictionsExpiry['create'] = Block::decodeExpiry('');
}
$this->mRestrictionsLoaded = true;
}
@@ -1849,8 +1820,8 @@ class Title {
/**
* Accessor/initialisation for mRestrictions
*
- * @param string $action action that permission needs to be checked for
- * @return array the array of groups allowed to edit this article
+ * @param $action \type{\string} action that permission needs to be checked for
+ * @return \type{\arrayof{\string}} the array of groups allowed to edit this article
*/
public function getRestrictions( $action ) {
if( !$this->mRestrictionsLoaded ) {
@@ -1862,8 +1833,20 @@ class Title {
}
/**
+ * Get the expiry time for the restriction against a given action
+ * @return 14-char timestamp, or 'infinity' if the page is protected forever
+ * or not protected at all, or false if the action is not recognised.
+ */
+ public function getRestrictionExpiry( $action ) {
+ if( !$this->mRestrictionsLoaded ) {
+ $this->loadRestrictions();
+ }
+ return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
+ }
+
+ /**
* Is there a version of this page in the deletion archive?
- * @return int the number of archived revisions
+ * @return \type{\int} the number of archived revisions
*/
public function isDeleted() {
$fname = 'Title::isDeleted';
@@ -1873,7 +1856,7 @@ class Title {
$dbr = wfGetDB( DB_SLAVE );
$n = $dbr->selectField( 'archive', 'COUNT(*)', array( 'ar_namespace' => $this->getNamespace(),
'ar_title' => $this->getDBkey() ), $fname );
- if( $this->getNamespace() == NS_IMAGE ) {
+ if( $this->getNamespace() == NS_FILE ) {
$n += $dbr->selectField( 'filearchive', 'COUNT(*)',
array( 'fa_name' => $this->getDBkey() ), $fname );
}
@@ -1884,18 +1867,22 @@ class Title {
/**
* Get the article ID for this Title from the link cache,
* adding it if necessary
- * @param int $flags a bit field; may be GAID_FOR_UPDATE to select
+ * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select
* for update
- * @return int the ID
+ * @return \type{\int} the ID
*/
public function getArticleID( $flags = 0 ) {
+ if( $this->getNamespace() < 0 ) {
+ return $this->mArticleID = 0;
+ }
$linkCache = LinkCache::singleton();
- if ( $flags & GAID_FOR_UPDATE ) {
+ if( $flags & GAID_FOR_UPDATE ) {
$oldUpdate = $linkCache->forUpdate( true );
+ $linkCache->clearLink( $this );
$this->mArticleID = $linkCache->addLinkObj( $this );
$linkCache->forUpdate( $oldUpdate );
} else {
- if ( -1 == $this->mArticleID ) {
+ if( -1 == $this->mArticleID ) {
$this->mArticleID = $linkCache->addLinkObj( $this );
}
}
@@ -1905,16 +1892,15 @@ class Title {
/**
* Is this an article that is a redirect page?
* Uses link cache, adding it if necessary
- * @param int $flags a bit field; may be GAID_FOR_UPDATE to select for update
- * @return bool
+ * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update
+ * @return \type{\bool}
*/
public function isRedirect( $flags = 0 ) {
if( !is_null($this->mRedirect) )
return $this->mRedirect;
- # Zero for special pages.
- # Also, calling getArticleID() loads the field from cache!
- if( !$this->getArticleID($flags) || $this->getNamespace() == NS_SPECIAL ) {
- return false;
+ # Calling getArticleID() loads the field from cache as needed
+ if( !$this->getArticleID($flags) ) {
+ return $this->mRedirect = false;
}
$linkCache = LinkCache::singleton();
$this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
@@ -1925,16 +1911,15 @@ class Title {
/**
* What is the length of this page?
* Uses link cache, adding it if necessary
- * @param int $flags a bit field; may be GAID_FOR_UPDATE to select for update
- * @return bool
+ * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update
+ * @return \type{\bool}
*/
public function getLength( $flags = 0 ) {
if( $this->mLength != -1 )
return $this->mLength;
- # Zero for special pages.
- # Also, calling getArticleID() loads the field from cache!
- if( !$this->getArticleID($flags) || $this->getNamespace() == NS_SPECIAL ) {
- return 0;
+ # Calling getArticleID() loads the field from cache as needed
+ if( !$this->getArticleID($flags) ) {
+ return $this->mLength = 0;
}
$linkCache = LinkCache::singleton();
$this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) );
@@ -1944,18 +1929,16 @@ class Title {
/**
* What is the page_latest field for this page?
- * @param int $flags a bit field; may be GAID_FOR_UPDATE to select for update
- * @return int
+ * @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update
+ * @return \type{\int}
*/
public function getLatestRevID( $flags = 0 ) {
- if ($this->mLatestID !== false)
+ if( $this->mLatestID !== false )
return $this->mLatestID;
$db = ($flags & GAID_FOR_UPDATE) ? wfGetDB(DB_MASTER) : wfGetDB(DB_SLAVE);
- return $this->mLatestID = $db->selectField( 'revision',
- "max(rev_id)",
- array('rev_page' => $this->getArticleID($flags)),
- 'Title::getLatestRevID' );
+ $this->mLatestID = $db->selectField( 'page', 'page_latest', $this->pageCond(), __METHOD__ );
+ return $this->mLatestID;
}
/**
@@ -1966,7 +1949,7 @@ class Title {
* loading of the new page_id. It's also called from
* Article::doDeleteArticle()
*
- * @param int $newid the new Article ID
+ * @param $newid \type{\int} the new Article ID
*/
public function resetArticleID( $newid ) {
$linkCache = LinkCache::singleton();
@@ -1980,30 +1963,19 @@ class Title {
/**
* Updates page_touched for this page; called from LinksUpdate.php
- * @return bool true if the update succeded
+ * @return \type{\bool} true if the update succeded
*/
public function invalidateCache() {
- global $wgUseFileCache;
-
- if ( wfReadOnly() ) {
+ if( wfReadOnly() ) {
return;
}
-
$dbw = wfGetDB( DB_MASTER );
$success = $dbw->update( 'page',
- array( /* SET */
- 'page_touched' => $dbw->timestamp()
- ), array( /* WHERE */
- 'page_namespace' => $this->getNamespace() ,
- 'page_title' => $this->getDBkey()
- ), 'Title::invalidateCache'
+ array( 'page_touched' => $dbw->timestamp() ),
+ $this->pageCond(),
+ __METHOD__
);
-
- if ($wgUseFileCache) {
- $cache = new HTMLFileCache($this);
- @unlink($cache->fileCacheName());
- }
-
+ HTMLFileCache::clearFileCache( $this );
return $success;
}
@@ -2011,8 +1983,8 @@ class Title {
* Prefix some arbitrary text with the namespace or interwiki prefix
* of this object
*
- * @param string $name the text
- * @return string the prefixed text
+ * @param $name \type{\string} the text
+ * @return \type{\string} the prefixed text
* @private
*/
/* private */ function prefix( $name ) {
@@ -2034,7 +2006,7 @@ class Title {
* removes illegal characters, splits off the interwiki and
* namespace prefixes, sets the other forms, and canonicalizes
* everything.
- * @return bool true on success
+ * @return \type{\bool} true on success
*/
private function secureAndSplit() {
global $wgContLang, $wgLocalInterwiki, $wgCapitalLinks;
@@ -2064,8 +2036,7 @@ class Title {
# Strip Unicode bidi override characters.
# Sometimes they slip into cut-n-pasted page titles, where the
# override chars get included in list displays.
- $dbkey = str_replace( "\xE2\x80\x8E", '', $dbkey ); // 200E LEFT-TO-RIGHT MARK
- $dbkey = str_replace( "\xE2\x80\x8F", '', $dbkey ); // 200F RIGHT-TO-LEFT MARK
+ $dbkey = preg_replace( '/\xE2\x80[\x8E\x8F\xAA-\xAE]/S', '', $dbkey );
# Clean up whitespace
#
@@ -2101,7 +2072,7 @@ class Title {
# Ordinary namespace
$dbkey = $m[2];
$this->mNamespace = $ns;
- } elseif( $this->getInterwikiLink( $p ) ) {
+ } elseif( Interwiki::isValidInterwiki( $p ) ) {
if( !$firstPass ) {
# Can't make a local interwiki link to an interwiki link.
# That's just crazy!
@@ -2158,9 +2129,9 @@ class Title {
}
/**
- * Pages with "/./" or "/../" appearing in the URLs will
- * often be unreachable due to the way web browsers deal
- * with 'relative' URLs. Forbid them explicitly.
+ * Pages with "/./" or "/../" appearing in the URLs will often be un-
+ * reachable due to the way web browsers deal with 'relative' URLs.
+ * Also, they conflict with subpage syntax. Forbid them explicitly.
*/
if ( strpos( $dbkey, '.' ) !== false &&
( $dbkey === '.' || $dbkey === '..' ||
@@ -2240,13 +2211,14 @@ class Title {
}
/**
- * Set the fragment for this title
- * This is kind of bad, since except for this rarely-used function, Title objects
- * are immutable. The reason this is here is because it's better than setting the
- * members directly, which is what Linker::formatComment was doing previously.
+ * Set the fragment for this title. Removes the first character from the
+ * specified fragment before setting, so it assumes you're passing it with
+ * an initial "#".
+ *
+ * Deprecated for public use, use Title::makeTitle() with fragment parameter.
+ * Still in active use privately.
*
- * @param string $fragment text
- * @todo clarify whether access is supposed to be public (was marked as "kind of public")
+ * @param $fragment \type{\string} text
*/
public function setFragment( $fragment ) {
$this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) );
@@ -2254,7 +2226,7 @@ class Title {
/**
* Get a Title object associated with the talk page of this article
- * @return Title the object for the talk page
+ * @return \type{Title} the object for the talk page
*/
public function getTalkPage() {
return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
@@ -2264,10 +2236,15 @@ class Title {
* Get a title object associated with the subject page of this
* talk page
*
- * @return Title the object for the subject page
+ * @return \type{Title} the object for the subject page
*/
public function getSubjectPage() {
- return Title::makeTitle( MWNamespace::getSubject( $this->getNamespace() ), $this->getDBkey() );
+ // Is this the same title?
+ $subjectNS = MWNamespace::getSubject( $this->getNamespace() );
+ if( $this->getNamespace() == $subjectNS ) {
+ return $this;
+ }
+ return Title::makeTitle( $subjectNS, $this->getDBkey() );
}
/**
@@ -2277,8 +2254,8 @@ class Title {
* WARNING: do not use this function on arbitrary user-supplied titles!
* On heavily-used templates it will max out the memory.
*
- * @param string $options may be FOR UPDATE
- * @return array the Title objects linking here
+ * @param $options \type{\string} may be FOR UPDATE
+ * @return \type{\arrayof{Title}} the Title objects linking here
*/
public function getLinksTo( $options = '', $table = 'pagelinks', $prefix = 'pl' ) {
$linkCache = LinkCache::singleton();
@@ -2295,12 +2272,12 @@ class Title {
"{$prefix}_from=page_id",
"{$prefix}_namespace" => $this->getNamespace(),
"{$prefix}_title" => $this->getDBkey() ),
- 'Title::getLinksTo',
+ __METHOD__,
$options );
$retVal = array();
if ( $db->numRows( $res ) ) {
- while ( $row = $db->fetchObject( $res ) ) {
+ foreach( $res as $row ) {
if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) {
$linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect );
$retVal[] = $titleObj;
@@ -2318,8 +2295,8 @@ class Title {
* WARNING: do not use this function on arbitrary user-supplied titles!
* On heavily-used templates it will max out the memory.
*
- * @param string $options may be FOR UPDATE
- * @return array the Title objects linking here
+ * @param $options \type{\string} may be FOR UPDATE
+ * @return \type{\arrayof{Title}} the Title objects linking here
*/
public function getTemplateLinksTo( $options = '' ) {
return $this->getLinksTo( $options, 'templatelinks', 'tl' );
@@ -2329,8 +2306,8 @@ class Title {
* Get an array of Title objects referring to non-existent articles linked from this page
*
* @todo check if needed (used only in SpecialBrokenRedirects.php, and should use redirect table in this case)
- * @param string $options may be FOR UPDATE
- * @return array the Title objects
+ * @param $options \type{\string} may be FOR UPDATE
+ * @return \type{\arrayof{Title}} the Title objects
*/
public function getBrokenLinksFrom( $options = '' ) {
if ( $this->getArticleId() == 0 ) {
@@ -2360,7 +2337,7 @@ class Title {
$retVal = array();
if ( $db->numRows( $res ) ) {
- while ( $row = $db->fetchObject( $res ) ) {
+ foreach( $res as $row ) {
$retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
}
}
@@ -2373,7 +2350,7 @@ class Title {
* Get a list of URLs to purge from the Squid cache when this
* page changes
*
- * @return array the URLs
+ * @return \type{\arrayof{\string}} the URLs
*/
public function getSquidURLs() {
global $wgContLang;
@@ -2395,6 +2372,9 @@ class Title {
return $urls;
}
+ /**
+ * Purge all applicable Squid URLs
+ */
public function purgeSquid() {
global $wgUseSquid;
if ( $wgUseSquid ) {
@@ -2406,7 +2386,7 @@ class Title {
/**
* Move this page without authentication
- * @param Title &$nt the new page Title
+ * @param &$nt \type{Title} the new page Title
*/
public function moveNoAuth( &$nt ) {
return $this->moveTo( $nt, false );
@@ -2415,13 +2395,15 @@ class Title {
/**
* Check whether a given move operation would be valid.
* Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
- * @param Title &$nt the new title
- * @param bool $auth indicates whether $wgUser's permissions
+ * @param &$nt \type{Title} the new title
+ * @param $auth \type{\bool} indicates whether $wgUser's permissions
* should be checked
- * @param string $reason is the log summary of the move, used for spam checking
- * @return mixed True on success, getUserPermissionsErrors()-like array on failure
+ * @param $reason \type{\string} is the log summary of the move, used for spam checking
+ * @return \type{\mixed} True on success, getUserPermissionsErrors()-like array on failure
*/
public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
+ global $wgUser;
+
$errors = array();
if( !$nt ) {
// Normally we'd add this to $errors, but we'll get
@@ -2431,8 +2413,14 @@ class Title {
if( $this->equals( $nt ) ) {
$errors[] = array('selfmove');
}
- if( !$this->isMovable() || !$nt->isMovable() ) {
- $errors[] = array('immobile_namespace');
+ if( !$this->isMovable() ) {
+ $errors[] = array( 'immobile-source-namespace', $this->getNsText() );
+ }
+ if ( $nt->getInterwiki() != '' ) {
+ $errors[] = array( 'immobile-target-namespace-iw' );
+ }
+ if ( !$nt->isMovable() ) {
+ $errors[] = array('immobile-target-namespace', $nt->getNsText() );
}
$oldid = $this->getArticleID();
@@ -2448,31 +2436,35 @@ class Title {
}
// Image-specific checks
- if( $this->getNamespace() == NS_IMAGE ) {
+ if( $this->getNamespace() == NS_FILE ) {
$file = wfLocalFile( $this );
if( $file->exists() ) {
- if( $nt->getNamespace() != NS_IMAGE ) {
+ if( $nt->getNamespace() != NS_FILE ) {
$errors[] = array('imagenocrossnamespace');
}
if( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) {
$errors[] = array('imageinvalidfilename');
}
- if( !File::checkExtensionCompatibility( $file, $nt->getDbKey() ) ) {
+ if( !File::checkExtensionCompatibility( $file, $nt->getDBKey() ) ) {
$errors[] = array('imagetypemismatch');
}
}
}
if ( $auth ) {
- global $wgUser;
- $errors = array_merge($errors,
- $this->getUserPermissionsErrors('move', $wgUser),
- $this->getUserPermissionsErrors('edit', $wgUser),
- $nt->getUserPermissionsErrors('move', $wgUser),
- $nt->getUserPermissionsErrors('edit', $wgUser));
+ $errors = wfMergeErrorArrays( $errors,
+ $this->getUserPermissionsErrors('move', $wgUser),
+ $this->getUserPermissionsErrors('edit', $wgUser),
+ $nt->getUserPermissionsErrors('move-target', $wgUser),
+ $nt->getUserPermissionsErrors('edit', $wgUser) );
}
- global $wgUser;
+ $match = EditPage::matchSpamRegex( $reason );
+ if( $match !== false ) {
+ // This is kind of lame, won't display nice
+ $errors[] = array('spamprotectiontext');
+ }
+
$err = null;
if( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) {
$errors[] = array('hookaborted', $err);
@@ -2500,13 +2492,13 @@ class Title {
/**
* Move a title to a new location
- * @param Title &$nt the new title
- * @param bool $auth indicates whether $wgUser's permissions
+ * @param &$nt \type{Title} the new title
+ * @param $auth \type{\bool} indicates whether $wgUser's permissions
* should be checked
- * @param string $reason The reason for the move
- * @param bool $createRedirect Whether to create a redirect from the old title to the new title.
+ * @param $reason \type{\string} The reason for the move
+ * @param $createRedirect \type{\bool} Whether to create a redirect from the old title to the new title.
* Ignored if the user doesn't have the suppressredirect right.
- * @return mixed true on success, getUserPermissionsErrors()-like array on failure
+ * @return \type{\mixed} true on success, getUserPermissionsErrors()-like array on failure
*/
public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
$err = $this->isValidMoveOperation( $nt, $auth, $reason );
@@ -2515,6 +2507,7 @@ class Title {
}
$pageid = $this->getArticleID();
+ $protected = $this->isProtected();
if( $nt->exists() ) {
$err = $this->moveOverExistingRedirect( $nt, $reason, $createRedirect );
$pageCountChange = ($createRedirect ? 0 : -1);
@@ -2549,8 +2542,29 @@ class Title {
'cl_sortkey' => $this->getPrefixedText() ),
__METHOD__ );
- # Update watchlists
+ if( $protected ) {
+ # Protect the redirect title as the title used to be...
+ $dbw->insertSelect( 'page_restrictions', 'page_restrictions',
+ array(
+ 'pr_page' => $redirid,
+ 'pr_type' => 'pr_type',
+ 'pr_level' => 'pr_level',
+ 'pr_cascade' => 'pr_cascade',
+ 'pr_user' => 'pr_user',
+ 'pr_expiry' => 'pr_expiry'
+ ),
+ array( 'pr_page' => $pageid ),
+ __METHOD__,
+ array( 'IGNORE' )
+ );
+ # Update the protection log
+ $log = new LogPage( 'protect' );
+ $comment = wfMsgForContent('prot_1movedto2',$this->getPrefixedText(), $nt->getPrefixedText() );
+ if( $reason ) $comment .= ': ' . $reason;
+ $log->addEntry( 'move_prot', $nt, $comment, array($this->getPrefixedText()) ); // FIXME: $params?
+ }
+ # Update watchlists
$oldnamespace = $this->getNamespace() & ~1;
$newnamespace = $nt->getNamespace() & ~1;
$oldtitle = $this->getDBkey();
@@ -2602,10 +2616,10 @@ class Title {
* Move page to a title which is at present a redirect to the
* source page
*
- * @param Title &$nt the page to move to, which should currently
+ * @param &$nt \type{Title} the page to move to, which should currently
* be a redirect
- * @param string $reason The reason for the move
- * @param bool $createRedirect Whether to leave a redirect at the old title.
+ * @param $reason \type{\string} The reason for the move
+ * @param $createRedirect \type{\bool} Whether to leave a redirect at the old title.
* Ignored if the user doesn't have the suppressredirect right
*/
private function moveOverExistingRedirect( &$nt, $reason = '', $createRedirect = true ) {
@@ -2620,9 +2634,9 @@ class Title {
$now = wfTimestampNow();
$newid = $nt->getArticleID();
$oldid = $this->getArticleID();
+ $latest = $this->getLatestRevID();
$dbw = wfGetDB( DB_MASTER );
- $dbw->begin();
# Delete the old redirect. We don't save it to history since
# by definition if we've got here it's rather uninteresting.
@@ -2648,7 +2662,7 @@ class Title {
$nullRevId = $nullRevision->insertOn( $dbw );
$article = new Article( $this );
- wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $wgUser) );
# Change the name of the target page:
$dbw->update( 'page',
@@ -2676,7 +2690,7 @@ class Title {
$redirectRevision->insertOn( $dbw );
$redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
- wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false, $wgUser) );
# Now, we record the link from the redirect to the new title.
# It should have no other outgoing links...
@@ -2687,12 +2701,14 @@ class Title {
'pl_namespace' => $nt->getNamespace(),
'pl_title' => $nt->getDBkey() ),
$fname );
+ $redirectSuppressed = false;
} else {
$this->resetArticleID( 0 );
+ $redirectSuppressed = true;
}
-
+
# Move an image if this is a file
- if( $this->getNamespace() == NS_IMAGE ) {
+ if( $this->getNamespace() == NS_FILE ) {
$file = wfLocalFile( $this );
if( $file->exists() ) {
$status = $file->move( $nt );
@@ -2702,11 +2718,10 @@ class Title {
}
}
}
- $dbw->commit();
# Log the move
$log = new LogPage( 'move' );
- $log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText() ) );
+ $log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) );
# Purge squid
if ( $wgUseSquid ) {
@@ -2719,9 +2734,9 @@ class Title {
/**
* Move page to non-existing title.
- * @param Title &$nt the new Title
- * @param string $reason The reason for the move
- * @param bool $createRedirect Whether to create a redirect from the old title to the new title
+ * @param &$nt \type{Title} the new Title
+ * @param $reason \type{\string} The reason for the move
+ * @param $createRedirect \type{\bool} Whether to create a redirect from the old title to the new title
* Ignored if the user doesn't have the suppressredirect right
*/
private function moveToNewTitle( &$nt, $reason = '', $createRedirect = true ) {
@@ -2729,14 +2744,16 @@ class Title {
$fname = 'MovePageForm::moveToNewTitle';
$comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() );
if ( $reason ) {
- $comment .= ": $reason";
+ $comment .= wfMsgExt( 'colon-separator',
+ array( 'escapenoentities', 'content' ) );
+ $comment .= $reason;
}
$newid = $nt->getArticleID();
$oldid = $this->getArticleID();
+ $latest = $this->getLatestRevId();
$dbw = wfGetDB( DB_MASTER );
- $dbw->begin();
$now = $dbw->timestamp();
# Save a null revision in the page's history notifying of the move
@@ -2744,7 +2761,7 @@ class Title {
$nullRevId = $nullRevision->insertOn( $dbw );
$article = new Article( $this );
- wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $wgUser) );
# Rename page entry
$dbw->update( 'page',
@@ -2772,7 +2789,7 @@ class Title {
$redirectRevision->insertOn( $dbw );
$redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
- wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false, $wgUser) );
# Record the just-created redirect's linking to the page
$dbw->insert( 'pagelinks',
@@ -2781,12 +2798,14 @@ class Title {
'pl_namespace' => $nt->getNamespace(),
'pl_title' => $nt->getDBkey() ),
$fname );
+ $redirectSuppressed = false;
} else {
$this->resetArticleID( 0 );
+ $redirectSuppressed = true;
}
-
+
# Move an image if this is a file
- if( $this->getNamespace() == NS_IMAGE ) {
+ if( $this->getNamespace() == NS_FILE ) {
$file = wfLocalFile( $this );
if( $file->exists() ) {
$status = $file->move( $nt );
@@ -2796,11 +2815,10 @@ class Title {
}
}
}
- $dbw->commit();
# Log the move
$log = new LogPage( 'move' );
- $log->addEntry( 'move', $this, $reason, array( 1 => $nt->getPrefixedText()) );
+ $log->addEntry( 'move', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) );
# Purge caches as per article creation
Article::onArticleCreate( $nt );
@@ -2810,41 +2828,69 @@ class Title {
$this->purgeSquid();
}
+
+ /**
+ * Checks if this page is just a one-rev redirect.
+ * Adds lock, so don't use just for light purposes.
+ *
+ * @return \type{\bool} TRUE or FALSE
+ */
+ public function isSingleRevRedirect() {
+ $dbw = wfGetDB( DB_MASTER );
+ # Is it a redirect?
+ $row = $dbw->selectRow( 'page',
+ array( 'page_is_redirect', 'page_latest', 'page_id' ),
+ $this->pageCond(),
+ __METHOD__,
+ 'FOR UPDATE'
+ );
+ # Cache some fields we may want
+ $this->mArticleID = $row ? intval($row->page_id) : 0;
+ $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
+ $this->mLatestID = $row ? intval($row->page_latest) : false;
+ if( !$this->mRedirect ) {
+ return false;
+ }
+ # Does the article have a history?
+ $row = $dbw->selectField( array( 'page', 'revision'),
+ 'rev_id',
+ array( 'page_namespace' => $this->getNamespace(),
+ 'page_title' => $this->getDBkey(),
+ 'page_id=rev_page',
+ 'page_latest != rev_id'
+ ),
+ __METHOD__,
+ 'FOR UPDATE'
+ );
+ # Return true if there was no history
+ return ($row === false);
+ }
/**
* Checks if $this can be moved to a given Title
* - Selects for update, so don't call it unless you mean business
*
- * @param Title &$nt the new title to check
+ * @param &$nt \type{Title} the new title to check
+ * @return \type{\bool} TRUE or FALSE
*/
public function isValidMoveTarget( $nt ) {
-
- $fname = 'Title::isValidMoveTarget';
$dbw = wfGetDB( DB_MASTER );
-
# Is it an existsing file?
- if( $nt->getNamespace() == NS_IMAGE ) {
+ if( $nt->getNamespace() == NS_FILE ) {
$file = wfLocalFile( $nt );
if( $file->exists() ) {
wfDebug( __METHOD__ . ": file exists\n" );
return false;
}
}
-
- # Is it a redirect?
- $id = $nt->getArticleID();
- $obj = $dbw->selectRow( array( 'page', 'revision', 'text'),
- array( 'page_is_redirect','old_text','old_flags' ),
- array( 'page_id' => $id, 'page_latest=rev_id', 'rev_text_id=old_id' ),
- $fname, 'FOR UPDATE' );
-
- if ( !$obj || 0 == $obj->page_is_redirect ) {
- # Not a redirect
- wfDebug( __METHOD__ . ": not a redirect\n" );
+ # Is it a redirect with no history?
+ if( !$nt->isSingleRevRedirect() ) {
+ wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
return false;
}
- $text = Revision::getRevisionText( $obj );
-
+ # Get the article text
+ $rev = Revision::newFromTitle( $nt );
+ $text = $rev->getText();
# Does the redirect point to the source?
# Or is it a broken self-redirect, usually caused by namespace collisions?
$m = array();
@@ -2861,35 +2907,23 @@ class Title {
wfDebug( __METHOD__ . ": failsafe\n" );
return false;
}
-
- # Does the article have a history?
- $row = $dbw->selectRow( array( 'page', 'revision'),
- array( 'rev_id' ),
- array( 'page_namespace' => $nt->getNamespace(),
- 'page_title' => $nt->getDBkey(),
- 'page_id=rev_page AND page_latest != rev_id'
- ), $fname, 'FOR UPDATE'
- );
-
- # Return true if there was no history
- return $row === false;
+ return true;
}
/**
* Can this title be added to a user's watchlist?
*
- * @return bool
+ * @return \type{\bool} TRUE or FALSE
*/
public function isWatchable() {
- return !$this->isExternal()
- && MWNamespace::isWatchable( $this->getNamespace() );
+ return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
}
/**
* Get categories to which this Title belongs and return an array of
* categories' names.
*
- * @return array an array of parents in the form:
+ * @return \type{\array} array an array of parents in the form:
* $parent => $currentarticle
*/
public function getParentCategories() {
@@ -2908,9 +2942,9 @@ class Title {
$res = $dbr->query( $sql );
if( $dbr->numRows( $res ) > 0 ) {
- while( $x = $dbr->fetchObject( $res ) )
- //$data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$x->cl_to);
- $data[$wgContLang->getNSText( NS_CATEGORY ).':'.$x->cl_to] = $this->getFullText();
+ foreach( $res as $row )
+ //$data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$row->cl_to);
+ $data[$wgContLang->getNSText( NS_CATEGORY ).':'.$row->cl_to] = $this->getFullText();
$dbr->freeResult( $res );
} else {
$data = array();
@@ -2920,8 +2954,8 @@ class Title {
/**
* Get a tree of parent categories
- * @param array $children an array with the children in the keys, to check for circular refs
- * @return array
+ * @param $children \type{\array} an array with the children in the keys, to check for circular refs
+ * @return \type{\array} Tree of parent categories
*/
public function getParentCategoryTree( $children = array() ) {
$stack = array();
@@ -2950,25 +2984,30 @@ class Title {
* Get an associative array for selecting this title from
* the "page" table
*
- * @return array
+ * @return \type{\array} Selection array
*/
public function pageCond() {
- return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform );
+ if( $this->mArticleID > 0 ) {
+ // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
+ return array( 'page_id' => $this->mArticleID );
+ } else {
+ return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform );
+ }
}
/**
* Get the revision ID of the previous revision
*
- * @param integer $revision Revision ID. Get the revision that was before this one.
- * @param integer $flags, GAID_FOR_UPDATE
- * @return integer $oldrevision|false
+ * @param $revId \type{\int} Revision ID. Get the revision that was before this one.
+ * @param $flags \type{\int} GAID_FOR_UPDATE
+ * @return \twotypes{\int,\bool} Old revision ID, or FALSE if none exists
*/
- public function getPreviousRevisionID( $revision, $flags=0 ) {
+ public function getPreviousRevisionID( $revId, $flags=0 ) {
$db = ($flags & GAID_FOR_UPDATE) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
return $db->selectField( 'revision', 'rev_id',
array(
'rev_page' => $this->getArticleId($flags),
- 'rev_id < ' . intval( $revision )
+ 'rev_id < ' . intval( $revId )
),
__METHOD__,
array( 'ORDER BY' => 'rev_id DESC' )
@@ -2978,29 +3017,56 @@ class Title {
/**
* Get the revision ID of the next revision
*
- * @param integer $revision Revision ID. Get the revision that was after this one.
- * @param integer $flags, GAID_FOR_UPDATE
- * @return integer $oldrevision|false
+ * @param $revId \type{\int} Revision ID. Get the revision that was after this one.
+ * @param $flags \type{\int} GAID_FOR_UPDATE
+ * @return \twotypes{\int,\bool} Next revision ID, or FALSE if none exists
*/
- public function getNextRevisionID( $revision, $flags=0 ) {
+ public function getNextRevisionID( $revId, $flags=0 ) {
$db = ($flags & GAID_FOR_UPDATE) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
return $db->selectField( 'revision', 'rev_id',
array(
'rev_page' => $this->getArticleId($flags),
- 'rev_id > ' . intval( $revision )
+ 'rev_id > ' . intval( $revId )
),
__METHOD__,
array( 'ORDER BY' => 'rev_id' )
);
}
+
+ /**
+ * Check if this is a new page
+ *
+ * @return bool
+ */
+ public function isNewPage() {
+ $dbr = wfGetDB( DB_SLAVE );
+ return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
+ }
+
+ /**
+ * Get the oldest revision timestamp of this page
+ *
+ * @return string, MW timestamp
+ */
+ public function getEarliestRevTime() {
+ $dbr = wfGetDB( DB_SLAVE );
+ if( $this->exists() ) {
+ $min = $dbr->selectField( 'revision',
+ 'MIN(rev_timestamp)',
+ array( 'rev_page' => $this->getArticleId() ),
+ __METHOD__ );
+ return wfTimestampOrNull( TS_MW, $min );
+ }
+ return null;
+ }
/**
* Get the number of revisions between the given revision IDs.
* Used for diffs and other things that really need it.
*
- * @param integer $old Revision ID.
- * @param integer $new Revision ID.
- * @return integer Number of revisions between these IDs.
+ * @param $old \type{\int} Revision ID.
+ * @param $new \type{\int} Revision ID.
+ * @return \type{\int} Number of revisions between these IDs.
*/
public function countRevisionsBetween( $old, $new ) {
$dbr = wfGetDB( DB_SLAVE );
@@ -3015,10 +3081,10 @@ class Title {
/**
* Compare with another title.
*
- * @param Title $title
- * @return bool
+ * @param \type{Title} $title
+ * @return \type{\bool} TRUE or FALSE
*/
- public function equals( $title ) {
+ public function equals( Title $title ) {
// Note: === is necessary for proper matching of number-like titles.
return $this->getInterwiki() === $title->getInterwiki()
&& $this->getNamespace() == $title->getNamespace()
@@ -3039,36 +3105,85 @@ class Title {
/**
* Return a string representation of this title
*
- * @return string
+ * @return \type{\string} String representation of this title
*/
public function __toString() {
return $this->getPrefixedText();
}
/**
- * Check if page exists
- * @return bool
+ * Check if page exists. For historical reasons, this function simply
+ * checks for the existence of the title in the page table, and will
+ * thus return false for interwiki links, special pages and the like.
+ * If you want to know if a title can be meaningfully viewed, you should
+ * probably call the isKnown() method instead.
+ *
+ * @return \type{\bool} TRUE or FALSE
*/
public function exists() {
return $this->getArticleId() != 0;
}
/**
- * Do we know that this title definitely exists, or should we otherwise
- * consider that it exists?
+ * Should links to this title be shown as potentially viewable (i.e. as
+ * "bluelinks"), even if there's no record by this title in the page
+ * table?
*
- * @return bool
+ * This function is semi-deprecated for public use, as well as somewhat
+ * misleadingly named. You probably just want to call isKnown(), which
+ * calls this function internally.
+ *
+ * (ISSUE: Most of these checks are cheap, but the file existence check
+ * can potentially be quite expensive. Including it here fixes a lot of
+ * existing code, but we might want to add an optional parameter to skip
+ * it and any other expensive checks.)
+ *
+ * @return \type{\bool} TRUE or FALSE
*/
public function isAlwaysKnown() {
- // If the page is form Mediawiki:message/lang, calling wfMsgWeirdKey causes
- // the full l10n of that language to be loaded. That takes much memory and
- // isn't needed. So we strip the language part away.
- // Also, extension messages which are not loaded, are shown as red, because
- // we don't call MessageCache::loadAllMessages.
- list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 );
- return $this->isExternal()
- || ( $this->mNamespace == NS_MAIN && $this->mDbkeyform == '' )
- || ( $this->mNamespace == NS_MEDIAWIKI && wfMsgWeirdKey( $basename ) );
+ if( $this->mInterwiki != '' ) {
+ return true; // any interwiki link might be viewable, for all we know
+ }
+ switch( $this->mNamespace ) {
+ case NS_MEDIA:
+ case NS_FILE:
+ return wfFindFile( $this ); // file exists, possibly in a foreign repo
+ case NS_SPECIAL:
+ return SpecialPage::exists( $this->getDBKey() ); // valid special page
+ case NS_MAIN:
+ return $this->mDbkeyform == ''; // selflink, possibly with fragment
+ case NS_MEDIAWIKI:
+ // If the page is form Mediawiki:message/lang, calling wfMsgWeirdKey causes
+ // the full l10n of that language to be loaded. That takes much memory and
+ // isn't needed. So we strip the language part away.
+ // Also, extension messages which are not loaded, are shown as red, because
+ // we don't call MessageCache::loadAllMessages.
+ list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 );
+ return wfMsgWeirdKey( $basename ); // known system message
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Does this title refer to a page that can (or might) be meaningfully
+ * viewed? In particular, this function may be used to determine if
+ * links to the title should be rendered as "bluelinks" (as opposed to
+ * "redlinks" to non-existent pages).
+ *
+ * @return \type{\bool} TRUE or FALSE
+ */
+ public function isKnown() {
+ return $this->exists() || $this->isAlwaysKnown();
+ }
+
+ /**
+ * Is this in a namespace that allows actual pages?
+ *
+ * @return \type{\bool} TRUE or FALSE
+ */
+ public function canExist() {
+ return $this->mNamespace >= 0 && $this->mNamespace != NS_MEDIA;
}
/**
@@ -3088,25 +3203,63 @@ class Title {
/**
* Get the last touched timestamp
+ * @param Database $db, optional db
+ * @return \type{\string} Last touched timestamp
*/
- public function getTouched() {
+ public function getTouched( $db = NULL ) {
+ $db = isset($db) ? $db : wfGetDB( DB_SLAVE );
+ $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
+ return $touched;
+ }
+
+ /**
+ * Get the timestamp when this page was updated since the user last saw it.
+ * @param User $user
+ * @return mixed string/NULL
+ */
+ public function getNotificationTimestamp( $user = NULL ) {
+ global $wgUser, $wgShowUpdatedMarker;
+ // Assume current user if none given
+ if( !$user ) $user = $wgUser;
+ // Check cache first
+ $uid = $user->getId();
+ if( isset($this->mNotificationTimestamp[$uid]) ) {
+ return $this->mNotificationTimestamp[$uid];
+ }
+ if( !$uid || !$wgShowUpdatedMarker ) {
+ return $this->mNotificationTimestamp[$uid] = false;
+ }
+ // Don't cache too much!
+ if( count($this->mNotificationTimestamp) >= self::CACHE_MAX ) {
+ $this->mNotificationTimestamp = array();
+ }
$dbr = wfGetDB( DB_SLAVE );
- $touched = $dbr->selectField( 'page', 'page_touched',
- array(
- 'page_namespace' => $this->getNamespace(),
- 'page_title' => $this->getDBkey()
- ), __METHOD__
+ $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist',
+ 'wl_notificationtimestamp',
+ array( 'wl_namespace' => $this->getNamespace(),
+ 'wl_title' => $this->getDBkey(),
+ 'wl_user' => $user->getId()
+ ),
+ __METHOD__
);
- return $touched;
+ return $this->mNotificationTimestamp[$uid];
}
+ /**
+ * Get the trackback URL for this page
+ * @return \type{\string} Trackback URL
+ */
public function trackbackURL() {
- global $wgTitle, $wgScriptPath, $wgServer;
+ global $wgScriptPath, $wgServer;
return "$wgServer$wgScriptPath/trackback.php?article="
- . htmlspecialchars(urlencode($wgTitle->getPrefixedDBkey()));
+ . htmlspecialchars(urlencode($this->getPrefixedDBkey()));
}
+ /**
+ * Get the trackback RDF for this page
+ * @return \type{\string} Trackback RDF
+ */
public function trackbackRDF() {
$url = htmlspecialchars($this->getFullURL());
$title = htmlspecialchars($this->getText());
@@ -3132,7 +3285,7 @@ class Title {
/**
* Generate strings used for xml 'id' names in monobook tabs
- * @return string
+ * @return \type{\string} XML 'id' name
*/
public function getNamespaceKey() {
global $wgContLang;
@@ -3150,8 +3303,8 @@ class Title {
case NS_PROJECT:
case NS_PROJECT_TALK:
return 'nstab-project';
- case NS_IMAGE:
- case NS_IMAGE_TALK:
+ case NS_FILE:
+ case NS_FILE_TALK:
return 'nstab-image';
case NS_MEDIAWIKI:
case NS_MEDIAWIKI_TALK:
@@ -3172,7 +3325,7 @@ class Title {
/**
* Returns true if this title resolves to the named special page
- * @param string $name The special page name
+ * @param $name \type{\string} The special page name
*/
public function isSpecial( $name ) {
if ( $this->getNamespace() == NS_SPECIAL ) {
@@ -3186,7 +3339,7 @@ class Title {
/**
* If the Title refers to a special page alias which is not the local default,
- * returns a new Title which points to the local default. Otherwise, returns $this.
+ * @return \type{Title} A new Title which points to the local default. Otherwise, returns $this.
*/
public function fixSpecialName() {
if ( $this->getNamespace() == NS_SPECIAL ) {
@@ -3206,12 +3359,19 @@ class Title {
* In other words, is this a content page, for the purposes of calculating
* statistics, etc?
*
- * @return bool
+ * @return \type{\bool} TRUE or FALSE
*/
public function isContentPage() {
return MWNamespace::isContent( $this->getNamespace() );
}
+ /**
+ * Get all extant redirects to this Title
+ *
+ * @param $ns \twotypes{\int,\null} Single namespace to consider;
+ * NULL to consider all namespaces
+ * @return \type{\arrayof{Title}} Redirects to this title
+ */
public function getRedirectsHere( $ns = null ) {
$redirs = array();
@@ -3223,7 +3383,7 @@ class Title {
);
if ( !is_null($ns) ) $where['page_namespace'] = $ns;
- $result = $dbr->select(
+ $res = $dbr->select(
array( 'redirect', 'page' ),
array( 'page_namespace', 'page_title' ),
$where,
@@ -3231,7 +3391,7 @@ class Title {
);
- while( $row = $dbr->fetchObject( $result ) ) {
+ foreach( $res as $row ) {
$redirs[] = self::newFromRow( $row );
}
return $redirs;
diff --git a/includes/TitleArray.php b/includes/TitleArray.php
new file mode 100644
index 00000000..f7a9e1dc
--- /dev/null
+++ b/includes/TitleArray.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Note: this entire file is a byte-for-byte copy of UserArray.php with
+ * s/User/Title/. If anyone can figure out how to do this nicely with inheri-
+ * tance or something, please do so.
+ */
+
+/**
+ * The TitleArray class only exists to provide the newFromResult method at pre-
+ * sent.
+ */
+abstract class TitleArray implements Iterator {
+ /**
+ * @param $res result A MySQL result including at least page_namespace and
+ * page_title -- also can have page_id, page_len, page_is_redirect,
+ * page_latest (if those will be used). See Title::newFromRow.
+ * @return TitleArray
+ */
+ static function newFromResult( $res ) {
+ $array = null;
+ if ( !wfRunHooks( 'TitleArrayFromResult', array( &$array, $res ) ) ) {
+ return null;
+ }
+ if ( $array === null ) {
+ $array = self::newFromResult_internal( $res );
+ }
+ return $array;
+ }
+
+ protected static function newFromResult_internal( $res ) {
+ $array = new TitleArrayFromResult( $res );
+ return $array;
+ }
+}
+
+class TitleArrayFromResult extends TitleArray {
+ var $res;
+ var $key, $current;
+
+ function __construct( $res ) {
+ $this->res = $res;
+ $this->key = 0;
+ $this->setCurrent( $this->res->current() );
+ }
+
+ protected function setCurrent( $row ) {
+ if ( $row === false ) {
+ $this->current = false;
+ } else {
+ $this->current = Title::newFromRow( $row );
+ }
+ }
+
+ public function count() {
+ return $this->res->numRows();
+ }
+
+ function current() {
+ return $this->current;
+ }
+
+ function key() {
+ return $this->key;
+ }
+
+ function next() {
+ $row = $this->res->next();
+ $this->setCurrent( $row );
+ $this->key++;
+ }
+
+ function rewind() {
+ $this->res->rewind();
+ $this->key = 0;
+ $this->setCurrent( $this->res->current() );
+ }
+
+ function valid() {
+ return $this->current !== false;
+ }
+}
diff --git a/includes/UploadBase.php b/includes/UploadBase.php
new file mode 100644
index 00000000..91155a1b
--- /dev/null
+++ b/includes/UploadBase.php
@@ -0,0 +1,867 @@
+<?php
+
+class UploadBase {
+ var $mTempPath;
+ var $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType;
+ var $mTitle = false, $mTitleError = 0;
+ var $mFilteredName, $mFinalExtension;
+
+ const SUCCESS = 0;
+ const OK = 0;
+ const BEFORE_PROCESSING = 1;
+ const LARGE_FILE_SERVER = 2;
+ const EMPTY_FILE = 3;
+ const MIN_LENGTH_PARTNAME = 4;
+ const ILLEGAL_FILENAME = 5;
+ const PROTECTED_PAGE = 6;
+ const OVERWRITE_EXISTING_FILE = 7;
+ const FILETYPE_MISSING = 8;
+ const FILETYPE_BADTYPE = 9;
+ const VERIFICATION_ERROR = 10;
+ const UPLOAD_VERIFICATION_ERROR = 11;
+ const UPLOAD_WARNING = 12;
+ const INTERNAL_ERROR = 13;
+
+ const SESSION_VERSION = 2;
+
+ /**
+ * Returns true if uploads are enabled.
+ * Can be overriden by subclasses.
+ */
+ static function isEnabled() {
+ global $wgEnableUploads;
+ return $wgEnableUploads;
+ }
+ /**
+ * Returns true if the user can use this upload module or else a string
+ * identifying the missing permission.
+ * Can be overriden by subclasses.
+ */
+ static function isAllowed( $user ) {
+ if( !$user->isAllowed( 'upload' ) )
+ return 'upload';
+ return true;
+ }
+
+ // Upload handlers. Should probably just be a global
+ static $uploadHandlers = array( 'Stash', 'Upload', 'Url' );
+ /**
+ * Create a form of UploadBase depending on wpSourceType and initializes it
+ */
+ static function createFromRequest( &$request, $type = null ) {
+ $type = $type ? $type : $request->getVal( 'wpSourceType' );
+ if( !$type )
+ return null;
+ $type = ucfirst($type);
+ $className = 'UploadFrom'.$type;
+ if( !in_array( $type, self::$uploadHandlers ) )
+ return null;
+ if( !call_user_func( array( $className, 'isEnabled' ) ) )
+ return null;
+ if( !call_user_func( array( $className, 'isValidRequest' ), $request ) )
+ return null;
+
+ $handler = new $className;
+ $handler->initializeFromRequest( $request );
+ return $handler;
+ }
+
+ /**
+ * Check whether a request if valid for this handler
+ */
+ static function isValidRequest( $request ) {
+ return false;
+ }
+
+ function __construct() {}
+
+ /**
+ * Do the real variable initialization
+ */
+ function initialize( $name, $tempPath, $fileSize, $removeTempFile = false ) {
+ $this->mDesiredDestName = $name;
+ $this->mTempPath = $tempPath;
+ $this->mFileSize = $fileSize;
+ $this->mRemoveTempFile = $removeTempFile;
+ }
+
+ /**
+ * Fetch the file. Usually a no-op
+ */
+ function fetchFile() {
+ return self::OK;
+ }
+
+ /**
+ * Verify whether the upload is sane.
+ * Returns self::OK or else an array with error information
+ */
+ function verifyUpload() {
+ global $wgUser;
+
+ /**
+ * If there was no filename or a zero size given, give up quick.
+ */
+ if( empty( $this->mFileSize ) )
+ return array( 'status' => self::EMPTY_FILE );
+
+ $nt = $this->getTitle();
+ if( is_null( $nt ) ) {
+ $result = array( 'status' => $this->mTitleError );
+ if( $this->mTitleError == self::ILLEGAL_FILENAME )
+ $resul['filtered'] = $this->mFilteredName;
+ if ( $this->mTitleError == self::FILETYPE_BADTYPE )
+ $result['finalExt'] = $this->mFinalExtension;
+ return $result;
+ }
+ $this->mLocalFile = wfLocalFile( $nt );
+ $this->mDestName = $this->mLocalFile->getName();
+
+ /**
+ * In some cases we may forbid overwriting of existing files.
+ */
+ $overwrite = $this->checkOverwrite( $this->mDestName );
+ if( $overwrite !== true )
+ return array( 'status' => self::OVERWRITE_EXISTING_FILE, 'overwrite' => $overwrite );
+
+ /**
+ * Look at the contents of the file; if we can recognize the
+ * type but it's corrupt or data of the wrong type, we should
+ * probably not accept it.
+ */
+ $verification = $this->verifyFile( $this->mTempPath );
+
+ if( $verification !== true ) {
+ if( !is_array( $verification ) )
+ $verification = array( $verification );
+ $verification['status'] = self::VERIFICATION_ERROR;
+ return $verification;
+ }
+
+ $error = '';
+ if( !wfRunHooks( 'UploadVerification',
+ array( $this->mDestName, $this->mTempPath, &$error ) ) ) {
+ return array( 'status' => self::UPLOAD_VERIFICATION_ERROR, 'error' => $error );
+ }
+
+ return self::OK;
+ }
+
+ /**
+ * Verifies that it's ok to include the uploaded file
+ *
+ * @param string $tmpfile the full path of the temporary file to verify
+ * @return mixed true of the file is verified, a string or array otherwise.
+ */
+ protected function verifyFile( $tmpfile ) {
+ $this->mFileProps = File::getPropsFromPath( $this->mTempPath,
+ $this->mFinalExtension );
+ $this->checkMacBinary();
+
+ #magically determine mime type
+ $magic = MimeMagic::singleton();
+ $mime = $magic->guessMimeType( $tmpfile, false );
+
+ #check mime type, if desired
+ global $wgVerifyMimeType;
+ if ( $wgVerifyMimeType ) {
+
+ wfDebug ( "\n\nmime: <$mime> extension: <{$this->mFinalExtension}>\n\n");
+ #check mime type against file extension
+ if( !self::verifyExtension( $mime, $this->mFinalExtension ) ) {
+ return 'uploadcorrupt';
+ }
+
+ #check mime type blacklist
+ global $wgMimeTypeBlacklist;
+ if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist)
+ && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
+ return array( 'filetype-badmime', $mime );
+ }
+ }
+
+ #check for htmlish code and javascript
+ if( $this->detectScript ( $tmpfile, $mime, $this->mFinalExtension ) ) {
+ return 'uploadscripted';
+ }
+
+ /**
+ * Scan the uploaded file for viruses
+ */
+ $virus = $this->detectVirus($tmpfile);
+ if ( $virus ) {
+ return array( 'uploadvirus', $virus );
+ }
+
+ wfDebug( __METHOD__.": all clear; passing.\n" );
+ return true;
+ }
+
+ /**
+ * Check whether the user can edit, upload and create the image
+ */
+ function verifyPermissions( $user ) {
+ /**
+ * If the image is protected, non-sysop users won't be able
+ * to modify it by uploading a new revision.
+ */
+ $nt = $this->getTitle();
+ if( is_null( $nt ) )
+ return true;
+ $permErrors = $nt->getUserPermissionsErrors( 'edit', $user );
+ $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $user );
+ $permErrorsCreate = ( $nt->exists() ? array() : $nt->getUserPermissionsErrors( 'create', $user ) );
+ if( $permErrors || $permErrorsUpload || $permErrorsCreate ) {
+ $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) );
+ $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) );
+ return $permErrors;
+ }
+ return true;
+ }
+
+ /**
+ * Check for non fatal problems with the file
+ */
+ function checkWarnings() {
+ $warning = array();
+
+ $filename = $this->mLocalFile->getName();
+ $n = strrpos( $filename, '.' );
+ $partname = $n ? substr( $filename, 0, $n ) : $filename;
+
+ // Check whether the resulting filename is different from the desired one
+ if( $this->mDesiredDestName != $filename )
+ $warning['badfilename'] = $filename;
+
+ // Check whether the file extension is on the unwanted list
+ global $wgCheckFileExtensions, $wgFileExtensions;
+ if ( $wgCheckFileExtensions ) {
+ if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) )
+ $warning['filetype-unwanted-type'] = $this->mFinalExtension;
+ }
+
+ global $wgUploadSizeWarning;
+ if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) )
+ $warning['large-file'] = $wgUploadSizeWarning;
+
+ if ( $this->mFileSize == 0 )
+ $warning['emptyfile'] = true;
+
+ $exists = self::getExistsWarning( $this->mLocalFile );
+ if( $exists !== false )
+ $warning['exists'] = $exists;
+
+ // Check whether this may be a thumbnail
+ if( $exists !== false && $exists[0] != 'thumb'
+ && self::isThumbName( $this->mLocalFile->getName() ) )
+ $warning['file-thumbnail-no'] = substr( $filename , 0,
+ strpos( $nt->getText() , '-' ) +1 );
+
+ $hash = File::sha1Base36( $this->mTempPath );
+ $dupes = RepoGroup::singleton()->findBySha1( $hash );
+ if( $dupes )
+ $warning['duplicate'] = $dupes;
+
+ $filenamePrefixBlacklist = self::getFilenamePrefixBlacklist();
+ foreach( $filenamePrefixBlacklist as $prefix ) {
+ if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
+ $warning['filename-bad-prefix'] = $prefix;
+ break;
+ }
+ }
+
+ # If the file existed before and was deleted, warn the user of this
+ # Don't bother doing so if the file exists now, however
+ if( $this->mLocalFile->wasDeleted() && !$this->mLocalFile->exists() )
+ $warning['filewasdeleted'] = $this->mLocalFile->getTitle();
+
+ return $warning;
+ }
+
+ /**
+ * Really perform the upload.
+ */
+ function performUpload( $comment, $pageText, $watch, $user ) {
+ $status = $this->mLocalFile->upload( $this->mTempPath, $comment, $pageText,
+ File::DELETE_SOURCE, $this->mFileProps, false, $user );
+
+ if( $status->isGood() && $watch ) {
+ $user->addWatch( $this->mLocalFile->getTitle() );
+ }
+
+ if( $status->isGood() )
+ wfRunHooks( 'UploadComplete', array( &$this ) );
+
+ return $status;
+ }
+
+ /**
+ * Returns a title or null
+ */
+ function getTitle() {
+ if ( $this->mTitle !== false )
+ return $this->mTitle;
+
+ /**
+ * Chop off any directories in the given filename. Then
+ * filter out illegal characters, and try to make a legible name
+ * out of it. We'll strip some silently that Title would die on.
+ */
+
+ $basename = $this->mDesiredDestName;
+
+ $this->mFilteredName = wfStripIllegalFilenameChars( $basename );
+
+ /**
+ * We'll want to blacklist against *any* 'extension', and use
+ * only the final one for the whitelist.
+ */
+ list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName );
+
+ if( count( $ext ) ) {
+ $this->mFinalExtension = $ext[count( $ext ) - 1];
+ } else {
+ $this->mFinalExtension = '';
+ }
+
+ /* Don't allow users to override the blacklist (check file extension) */
+ global $wgCheckFileExtensions, $wgStrictFileExtensions;
+ global $wgFileExtensions, $wgFileBlacklist;
+ if ( $this->mFinalExtension == '' ) {
+ $this->mTitleError = self::FILETYPE_MISSING;
+ return $this->mTitle = null;
+ } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
+ ( $wgCheckFileExtensions && $wgStrictFileExtensions &&
+ !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) ) {
+ $this->mTitleError = self::FILETYPE_BADTYPE;
+ return $this->mTitle = null;
+ }
+
+ # If there was more than one "extension", reassemble the base
+ # filename to prevent bogus complaints about length
+ if( count( $ext ) > 1 ) {
+ for( $i = 0; $i < count( $ext ) - 1; $i++ )
+ $partname .= '.' . $ext[$i];
+ }
+
+ if( strlen( $partname ) < 1 ) {
+ $this->mTitleError = self::MIN_LENGTH_PARTNAME;
+ return $this->mTitle = null;
+ }
+
+ $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
+ if( is_null( $nt ) ) {
+ $this->mTitleError = self::ILLEGAL_FILENAME;
+ return $this->mTitle = null;
+ }
+ return $this->mTitle = $nt;
+ }
+
+ function getLocalFile() {
+ if( is_null( $this->mLocalFile ) ) {
+ $nt = $this->getTitle();
+ $this->mLocalFile = is_null( $nt ) ? null : wfLocalFile( $nt );
+ }
+ return $this->mLocalFile;
+ }
+
+ /**
+ * Stash a file in a temporary directory for later processing
+ * after the user has confirmed it.
+ *
+ * If the user doesn't explicitly cancel or accept, these files
+ * can accumulate in the temp directory.
+ *
+ * @param string $saveName - the destination filename
+ * @param string $tempName - the source temporary file to save
+ * @return string - full path the stashed file, or false on failure
+ * @access private
+ */
+ function saveTempUploadedFile( $saveName, $tempName ) {
+ global $wgOut;
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $status = $repo->storeTemp( $saveName, $tempName );
+ return $status;
+ }
+
+ /**
+ * Stash a file in a temporary directory for later processing,
+ * and save the necessary descriptive info into the session.
+ * Returns a key value which will be passed through a form
+ * to pick up the path info on a later invocation.
+ *
+ * @return int
+ * @access private
+ */
+ function stashSession() {
+ $status = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath );
+
+ if( !$status->isGood() ) {
+ # Couldn't save the file.
+ return false;
+ }
+
+ return array(
+ 'mTempPath' => $status->value,
+ 'mFileSize' => $this->mFileSize,
+ 'mFileProps' => $this->mFileProps,
+ 'version' => self::SESSION_VERSION,
+ );
+ }
+
+ /**
+ * Remove a temporarily kept file stashed by saveTempUploadedFile().
+ * @return success
+ */
+ function unsaveUploadedFile() {
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $success = $repo->freeTemp( $this->mTempPath );
+ return $success;
+ }
+
+ /**
+ * If we've modified the upload file we need to manually remove it
+ * on exit to clean up.
+ * @access private
+ */
+ function cleanupTempFile() {
+ if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) {
+ wfDebug( __METHOD__.": Removing temporary file {$this->mTempPath}\n" );
+ unlink( $this->mTempPath );
+ }
+ }
+
+ function getTempPath() {
+ return $this->mTempPath;
+ }
+
+
+ /**
+ * Split a file into a base name and all dot-delimited 'extensions'
+ * on the end. Some web server configurations will fall back to
+ * earlier pseudo-'extensions' to determine type and execute
+ * scripts, so the blacklist needs to check them all.
+ *
+ * @return array
+ */
+ function splitExtensions( $filename ) {
+ $bits = explode( '.', $filename );
+ $basename = array_shift( $bits );
+ return array( $basename, $bits );
+ }
+
+ /**
+ * Perform case-insensitive match against a list of file extensions.
+ * Returns true if the extension is in the list.
+ *
+ * @param string $ext
+ * @param array $list
+ * @return bool
+ */
+ function checkFileExtension( $ext, $list ) {
+ return in_array( strtolower( $ext ), $list );
+ }
+
+ /**
+ * Perform case-insensitive match against a list of file extensions.
+ * Returns true if any of the extensions are in the list.
+ *
+ * @param array $ext
+ * @param array $list
+ * @return bool
+ */
+ function checkFileExtensionList( $ext, $list ) {
+ foreach( $ext as $e ) {
+ if( in_array( strtolower( $e ), $list ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Checks if the mime type of the uploaded file matches the file extension.
+ *
+ * @param string $mime the mime type of the uploaded file
+ * @param string $extension The filename extension that the file is to be served with
+ * @return bool
+ */
+ public static function verifyExtension( $mime, $extension ) {
+ $magic = MimeMagic::singleton();
+
+ if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' )
+ if ( ! $magic->isRecognizableExtension( $extension ) ) {
+ wfDebug( __METHOD__.": passing file with unknown detected mime type; " .
+ "unrecognized extension '$extension', can't verify\n" );
+ return true;
+ } else {
+ 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( __METHOD__.": no file extension known for mime type $mime, passing file\n" );
+ return true;
+ } elseif ($match===true) {
+ 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( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" );
+ return false;
+ }
+ }
+
+ /**
+ * 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;
+
+ #ugly hack: for text files, always look at the entire file.
+ #For binary field, just check the first K.
+
+ if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file );
+ else {
+ $fp = fopen( $file, 'rb' );
+ $chunk = fread( $fp, 1024 );
+ fclose( $fp );
+ }
+
+ $chunk= strtolower( $chunk );
+
+ if (!$chunk) return false;
+
+ #decode from UTF-16 if needed (could be used for obfuscation).
+ if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE";
+ elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE";
+ else $enc= NULL;
+
+ if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk);
+
+ $chunk= trim($chunk);
+
+ #FIXME: convert from UTF-16 if necessarry!
+
+ wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n");
+
+ #check for HTML doctype
+ if (eregi("<!DOCTYPE *X?HTML",$chunk)) return true;
+
+ /**
+ * Internet Explorer for Windows performs some really stupid file type
+ * autodetection which can cause it to interpret valid image files as HTML
+ * and potentially execute JavaScript, creating a cross-site scripting
+ * attack vectors.
+ *
+ * Apple's Safari browser also performs some unsafe file type autodetection
+ * which can cause legitimate files to be interpreted as HTML if the
+ * web server is not correctly configured to send the right content-type
+ * (or if you're really uploading plain text and octet streams!)
+ *
+ * Returns true if IE is likely to mistake the given file for HTML.
+ * Also returns true if Safari would mistake the given file for HTML
+ * when served with a generic content-type.
+ */
+
+ $tags = array(
+ '<body',
+ '<head',
+ '<html', #also in safari
+ '<img',
+ '<pre',
+ '<script', #also in safari
+ '<table'
+ );
+ if( ! $wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) {
+ $tags[] = '<title';
+ }
+
+ foreach( $tags as $tag ) {
+ if( false !== strpos( $chunk, $tag ) ) {
+ return true;
+ }
+ }
+
+ /*
+ * look for javascript
+ */
+
+ #resolve entity-refs to look at attributes. may be harsh on big files... cache result?
+ $chunk = Sanitizer::decodeCharReferences( $chunk );
+
+ #look for script-types
+ if (preg_match('!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim',$chunk)) return true;
+
+ #look for html-style script-urls
+ if (preg_match('!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true;
+
+ #look for css-style script-urls
+ if (preg_match('!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true;
+
+ wfDebug("SpecialUpload::detectScript: no scripts found\n");
+ 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.
+ */
+ function detectVirus($file) {
+ global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut;
+
+ if ( !$wgAntivirus ) {
+ wfDebug( __METHOD__.": virus scanner disabled\n");
+ return NULL;
+ }
+
+ if ( !$wgAntivirusSetup[$wgAntivirus] ) {
+ wfDebug( __METHOD__.": unknown virus scanner: $wgAntivirus\n" );
+ $wgOut->wrapWikiMsg( '<div class="error">$1</div>', array( 'virus-badscanner', $wgAntivirus ) );
+ return wfMsg('virus-unknownscanner') . " $wgAntivirus";
+ }
+
+ # look up scanner configuration
+ $command = $wgAntivirusSetup[$wgAntivirus]["command"];
+ $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"];
+ $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ?
+ $wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null;
+
+ 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( __METHOD__.": running virus scan: $command \n" );
+
+ # 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( "$command", $output, $exitCode );
+ } else {
+ exec( "$command 2>&1", $output, $exitCode );
+ }
+
+ # map exit code to AV_xxx constants.
+ $mappedCode = $exitCode;
+ if ( $exitCodeMap ) {
+ if ( isset( $exitCodeMap[$exitCode] ) ) {
+ $mappedCode = $exitCodeMap[$exitCode];
+ } elseif ( isset( $exitCodeMap["*"] ) ) {
+ $mappedCode = $exitCodeMap["*"];
+ }
+ }
+
+ 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 wfMsg('virus-scanfailed', array( $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 ( $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( __METHOD__.": FOUND VIRUS! scanner feedback: $output" );
+ return $output;
+ }
+ }
+
+ /**
+ * Check if the temporary file is MacBinary-encoded, as some uploads
+ * from Internet Explorer on Mac OS Classic and Mac OS X will be.
+ * If so, the data fork will be extracted to a second temporary file,
+ * which will then be checked for validity and either kept or discarded.
+ *
+ * @access private
+ */
+ function checkMacBinary() {
+ $macbin = new MacBinary( $this->mTempPath );
+ if( $macbin->isValid() ) {
+ $dataFile = tempnam( wfTempDir(), "WikiMacBinary" );
+ $dataHandle = fopen( $dataFile, 'wb' );
+
+ wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" );
+ $macbin->extractData( $dataHandle );
+
+ $this->mTempPath = $dataFile;
+ $this->mFileSize = $macbin->dataForkLength();
+
+ // We'll have to manually remove the new file if it's not kept.
+ $this->mRemoveTempFile = true;
+ }
+ $macbin->close();
+ }
+
+ /**
+ * Check if there's an overwrite conflict and, if so, if restrictions
+ * forbid this user from performing the upload.
+ *
+ * @return mixed true on success, WikiError on failure
+ * @access private
+ */
+ function checkOverwrite() {
+ global $wgUser;
+ // First check whether the local file can be overwritten
+ if( $this->mLocalFile->exists() )
+ if( !self::userCanReUpload( $wgUser, $this->mLocalFile ) )
+ return 'fileexists-forbidden';
+
+ // Check shared conflicts
+ $file = wfFindFile( $this->mLocalFile->getName() );
+ if ( $file && ( !$wgUser->isAllowed( 'reupload' ) ||
+ !$wgUser->isAllowed( 'reupload-shared' ) ) )
+ return 'fileexists-shared-forbidden';
+
+ 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;
+ if( is_string( $img ) )
+ $img = wfLocalFile( $img );
+ if ( !( $img instanceof LocalFile ) )
+ return false;
+
+ return $user->getId() == $img->getUser( 'id' );
+ }
+
+ public static function getExistsWarning( $file ) {
+ if( $file->exists() )
+ return array( 'exists', $file );
+
+ if( $file->getTitle()->getArticleID() )
+ return array( 'page-exists', $file );
+
+ if( strpos( $file->getName(), '.' ) == false ) {
+ $partname = $file->getName();
+ $rawExtension = '';
+ } else {
+ $n = strrpos( $file->getName(), '.' );
+ $rawExtension = substr( $file->getName(), $n + 1 );
+ $partname = substr( $file->getName(), 0, $n );
+ }
+
+ 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::makeTitle( NS_FILE, $partname . '.' . $file->getExtension() );
+ $file_lc = wfLocalFile( $nt_lc );
+
+ if( $file_lc->exists() )
+ return array( 'exists-normalized', $file_lc );
+ }
+
+ if ( self::isThumbName( $file->getName() ) ) {
+ # 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() )
+ return array( 'thumb', $file_thb );
+ }
+
+ return false;
+ }
+
+ public static function isThumbName( $filename ) {
+ $n = strrpos( $filename, '.' );
+ $partname = $n ? substr( $filename, 0, $n ) : $filename;
+ return (
+ substr( $partname , 3, 3 ) == 'px-' ||
+ substr( $partname , 2, 3 ) == 'px-'
+ ) &&
+ ereg( "[0-9]{2}" , substr( $partname , 0, 2) );
+ }
+
+ /**
+ * Get a list of blacklisted filename prefixes from [[MediaWiki:filename-prefix-blacklist]]
+ *
+ * @return array list of prefixes
+ */
+ public static function getFilenamePrefixBlacklist() {
+ $blacklist = array();
+ $message = wfMsgForContent( 'filename-prefix-blacklist' );
+ if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) {
+ $lines = explode( "\n", $message );
+ foreach( $lines as $line ) {
+ // Remove comment lines
+ $comment = substr( trim( $line ), 0, 1 );
+ if ( $comment == '#' || $comment == '' ) {
+ continue;
+ }
+ // Remove additional comments after a prefix
+ $comment = strpos( $line, '#' );
+ if ( $comment > 0 ) {
+ $line = substr( $line, 0, $comment-1 );
+ }
+ $blacklist[] = trim( $line );
+ }
+ }
+ return $blacklist;
+ }
+
+
+}
diff --git a/includes/UploadFromStash.php b/includes/UploadFromStash.php
new file mode 100644
index 00000000..8bff3b49
--- /dev/null
+++ b/includes/UploadFromStash.php
@@ -0,0 +1,58 @@
+<?php
+
+class UploadFromStash extends UploadBase {
+ static function isValidSessionKey( $key, $sessionData ) {
+ return !empty( $key ) &&
+ is_array( $sessionData ) &&
+ isset( $sessionData[$key] ) &&
+ isset( $sessionData[$key]['version'] ) &&
+ $sessionData[$key]['version'] == self::SESSION_VERSION
+ ;
+ }
+ static function isValidRequest( $request ) {
+ $sessionData = $request->getSessionData('wsUploadData');
+ return self::isValidSessionKey(
+ $request->getInt( 'wpSessionKey' ),
+ $sessionData
+ );
+ }
+
+ function initialize( $name, $sessionData ) {
+ /**
+ * Confirming a temporarily stashed upload.
+ * We don't want path names to be forged, so we keep
+ * them in the session on the server and just give
+ * an opaque key to the user agent.
+ */
+ $this->initialize( $name,
+ $sessionData['mTempPath'],
+ $sessionData['mFileSize'],
+ false
+ );
+
+ $this->mFileProps = $sessionData['mFileProps'];
+ }
+ function initializeFromRequest( &$request ) {
+ $sessionKey = $request->getInt( 'wpSessionKey' );
+ $sessionData = $request->getSessionData('wsUploadData');
+
+ $desiredDestName = $request->getText( 'wpDestFile' );
+ if( !$desiredDestName )
+ $desiredDestName = $request->getText( 'wpUploadFile' );
+
+ return $this->initialize( $desiredDestName, $sessionData[$sessionKey] );
+ }
+
+ /**
+ * File has been previously verified so no need to do so again.
+ */
+ protected function verifyFile( $tmpfile ) {
+ return true;
+ }
+ /**
+ * We're here from "ignore warnings anyway" so return just OK
+ */
+ function checkWarnings() {
+ return array();
+ }
+}
diff --git a/includes/UploadFromUpload.php b/includes/UploadFromUpload.php
new file mode 100644
index 00000000..1b6762c6
--- /dev/null
+++ b/includes/UploadFromUpload.php
@@ -0,0 +1,20 @@
+<?php
+
+class UploadFromUpload extends UploadBase {
+
+ function initializeFromRequest( &$request ) {
+ $desiredDestName = $request->getText( 'wpDestFile' );
+ if( !$desiredDestName )
+ $desiredDestName = $request->getText( 'wpUploadFile' );
+
+ return $this->initialize(
+ $desiredDestName,
+ $request->getFileTempName( 'wpUploadFile' ),
+ $request->getFileSize( 'wpUploadFile' )
+ );
+ }
+
+ static function isValidRequest( $request ) {
+ return (bool)$request->getFileTempName( 'wpUploadFile' );
+ }
+}
diff --git a/includes/UploadFromUrl.php b/includes/UploadFromUrl.php
new file mode 100644
index 00000000..7e23b8cd
--- /dev/null
+++ b/includes/UploadFromUrl.php
@@ -0,0 +1,92 @@
+<?php
+
+
+class UploadFromUrl extends UploadBase {
+ static function isAllowed( $user ) {
+ if( !$user->isAllowed( 'upload_by_url' ) )
+ return 'upload_by_url';
+ return parent::isAllowed( $user );
+ }
+ static function isEnabled() {
+ global $wgAllowCopyUploads;
+ return $wgAllowCopyUploads && parent::isEnabled();
+ }
+
+ function initialize( $name, $url ) {
+ global $wgTmpDirectory;
+ $local_file = tempnam( $wgTmpDirectory, 'WEBUPLOAD' );
+ $this-initialize( $name, $local_file, 0, true );
+
+ $this->mUrl = trim( $url );
+ }
+
+ /**
+ * Do the real fetching stuff
+ */
+ function fetchFile() {
+ if( stripos($this->mUrl, 'http://') !== 0 && stripos($this->mUrl, 'ftp://') !== 0 ) {
+ return array(
+ 'status' => self::BEFORE_PROCESSING,
+ 'error' => 'upload-proto-error',
+ );
+ }
+ $res = $this->curlCopy();
+ if( $res !== true ) {
+ return array(
+ 'status' => self::BEFORE_PROCESSING,
+ 'error' => $res,
+ );
+ }
+ return self::OK;
+ }
+
+ /**
+ * Safe copy from URL
+ * Returns true if there was an error, false otherwise
+ */
+ private function curlCopy() {
+ global $wgUser, $wgOut;
+
+ # Open temporary file
+ $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" );
+ if( $this->mCurlDestHandle === false ) {
+ # Could not open temporary file to write in
+ return 'upload-file-error';
+ }
+
+ $ch = curl_init();
+ curl_setopt( $ch, CURLOPT_HTTP_VERSION, 1.0); # Probably not needed, but apparently can work around some bug
+ curl_setopt( $ch, CURLOPT_TIMEOUT, 10); # 10 seconds timeout
+ curl_setopt( $ch, CURLOPT_LOW_SPEED_LIMIT, 512); # 0.5KB per second minimum transfer speed
+ curl_setopt( $ch, CURLOPT_URL, $this->mUrl);
+ curl_setopt( $ch, CURLOPT_WRITEFUNCTION, array( $this, 'uploadCurlCallback' ) );
+ curl_exec( $ch );
+ $error = curl_errno( $ch );
+ curl_close( $ch );
+
+ fclose( $this->mCurlDestHandle );
+ unset( $this->mCurlDestHandle );
+
+ if( $error )
+ return "upload-curl-error$errornum";
+
+ return true;
+ }
+
+ /**
+ * Callback function for CURL-based web transfer
+ * Write data to file unless we've passed the length limit;
+ * if so, abort immediately.
+ * @access private
+ */
+ function uploadCurlCallback( $ch, $data ) {
+ global $wgMaxUploadSize;
+ $length = strlen( $data );
+ $this->mFileSize += $length;
+ if( $this->mFileSize > $wgMaxUploadSize ) {
+ return 0;
+ }
+ fwrite( $this->mCurlDestHandle, $data );
+ return $length;
+ }
+}
diff --git a/includes/User.php b/includes/User.php
index 4e39d678..9fee089c 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -1,20 +1,29 @@
<?php
/**
- * See user.txt
+ * Implements the User class for the %MediaWiki software.
* @file
*/
-# Number of characters in user_token field
+/**
+ * \int Number of characters in user_token field.
+ * @ingroup Constants
+ */
define( 'USER_TOKEN_LENGTH', 32 );
-# Serialized record version
+/**
+ * \int Serialized record version.
+ * @ingroup Constants
+ */
define( 'MW_USER_VERSION', 6 );
-# Some punctuation to prevent editing from broken text-mangling proxies.
+/**
+ * \string Some punctuation to prevent editing from broken text-mangling proxies.
+ * @ingroup Constants
+ */
define( 'EDIT_TOKEN_SUFFIX', '+\\' );
/**
- * Thrown by User::setPassword() on error
+ * Thrown by User::setPassword() on error.
* @ingroup Exception
*/
class PasswordError extends MWException {
@@ -34,11 +43,13 @@ 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
- * extended via the UserToggles hook or $wgContLang->getExtraUserToggles().
+ * \type{\arrayof{\string}} 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 by
+ * $wgContLang::getExtraUserToggles().
+ * @showinitializer
*/
- static public $mToggles = array(
+ public static $mToggles = array(
'highlightbroken',
'justify',
'hideminor',
@@ -71,21 +82,26 @@ class User {
'showjumplinks',
'uselivepreview',
'forceeditsummary',
- 'watchlisthideown',
- 'watchlisthidebots',
'watchlisthideminor',
+ 'watchlisthidebots',
+ 'watchlisthideown',
+ 'watchlisthideanons',
+ 'watchlisthideliu',
'ccmeonemails',
'diffonly',
'showhiddencats',
+ 'noconvertlink',
+ 'norollbackdiff',
);
/**
- * List of member variables which are saved to the shared cache (memcached).
- * Any operation which changes the corresponding database fields must
- * call a cache-clearing function.
+ * \type{\arrayof{\string}} List of member variables which are saved to the
+ * shared cache (memcached). Any operation which changes the
+ * corresponding database fields must call a cache-clearing function.
+ * @showinitializer
*/
static $mCacheVars = array(
- # user table
+ // user table
'mId',
'mName',
'mRealName',
@@ -101,13 +117,15 @@ class User {
'mEmailTokenExpires',
'mRegistration',
'mEditCount',
- # user_group table
+ // user_group table
'mGroups',
);
/**
- * Core rights
- * Each of these should have a corresponding message of the form "right-$right"
+ * \type{\arrayof{\string}} Core rights.
+ * Each of these should have a corresponding message of the form
+ * "right-$right".
+ * @showinitializer
*/
static $mCoreRights = array(
'apihighlimits',
@@ -132,6 +150,9 @@ class User {
'markbotedits',
'minoredit',
'move',
+ 'movefile',
+ 'move-rootuserpages',
+ 'move-subpages',
'nominornewtalk',
'noratelimit',
'patrol',
@@ -142,6 +163,7 @@ class User {
'reupload',
'reupload-shared',
'rollback',
+ 'siteadmin',
'suppressredirect',
'trackback',
'undelete',
@@ -150,47 +172,57 @@ class User {
'upload_by_url',
'userrights',
);
- static $mAllRights = false;
-
/**
- * The cache variable declarations
+ * \string Cached results of getAllRights()
*/
+ static $mAllRights = false;
+
+ /** @name Cache variables */
+ //@{
var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
$mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
$mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
+ //@}
/**
- * Whether the cache variables have been loaded
+ * \bool Whether the cache variables have been loaded.
*/
- var $mDataLoaded;
+ var $mDataLoaded, $mAuthLoaded;
/**
- * Initialisation data source if mDataLoaded==false. May be one of:
- * defaults anonymous user initialised from class defaults
- * name initialise from mName
- * id initialise from mId
- * session log in from cookies or session if possible
+ * \string Initialization data source if mDataLoaded==false. May be one of:
+ * - 'defaults' anonymous user initialised from class defaults
+ * - 'name' initialise from mName
+ * - 'id' initialise from mId
+ * - 'session' log in from cookies or session if possible
*
* Use the User::newFrom*() family of functions to set this.
*/
var $mFrom;
- /**
- * Lazy-initialised variables, invalidated with clearInstanceCache
- */
+ /** @name Lazy-initialized variables, invalidated with clearInstanceCache */
+ //@{
var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
- $mBlockreason, $mBlock, $mEffectiveGroups;
+ $mBlockreason, $mBlock, $mEffectiveGroups, $mBlockedGlobally,
+ $mLocked, $mHideName;
+ //@}
/**
- * Lightweight constructor for anonymous user
- * Use the User::newFrom* factory functions for other kinds of users
+ * Lightweight constructor for an anonymous user.
+ * Use the User::newFrom* factory functions for other kinds of users.
+ *
+ * @see newFromName()
+ * @see newFromId()
+ * @see newFromConfirmationCode()
+ * @see newFromSession()
+ * @see newFromRow()
*/
function User() {
$this->clearInstanceCache( 'defaults' );
}
/**
- * Load the user table data for this object from the source given by mFrom
+ * Load the user table data for this object from the source given by mFrom.
*/
function load() {
if ( $this->mDataLoaded ) {
@@ -219,6 +251,7 @@ class User {
break;
case 'session':
$this->loadFromSession();
+ wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
break;
default:
throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
@@ -227,8 +260,8 @@ class User {
}
/**
- * Load user table data given mId
- * @return false if the ID does not exist, true otherwise
+ * Load user table data, given mId has already been set.
+ * @return \bool false if the ID does not exist, true otherwise
* @private
*/
function loadFromId() {
@@ -283,6 +316,10 @@ class User {
global $wgMemc;
$wgMemc->set( $key, $data );
}
+
+
+ /** @name newFrom*() static factory methods */
+ //@{
/**
* Static factory method for creation from username.
@@ -290,14 +327,14 @@ class User {
* This is slightly less efficient than newFromId(), so use newFromId() if
* you have both an ID and a name handy.
*
- * @param $name String: username, validated by Title:newFromText()
- * @param $validate Mixed: validate username. Takes the same parameters as
+ * @param $name \string Username, validated by Title::newFromText()
+ * @param $validate \mixed 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
- * is not present in the database, the result will be a user object with
- * a name, zero user ID and default settings.
+ * @return \type{User} The 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.
*/
static function newFromName( $name, $validate = 'valid' ) {
if ( $validate === true ) {
@@ -315,6 +352,12 @@ class User {
}
}
+ /**
+ * Static factory method for creation from a given user ID.
+ *
+ * @param $id \int Valid user ID
+ * @return \type{User} The corresponding User object
+ */
static function newFromId( $id ) {
$u = new User;
$u->mId = $id;
@@ -329,8 +372,8 @@ class User {
*
* If the code is invalid or has expired, returns NULL.
*
- * @param $code string
- * @return User
+ * @param $code \string Confirmation code
+ * @return \type{User}
*/
static function newFromConfirmationCode( $code ) {
$dbr = wfGetDB( DB_SLAVE );
@@ -349,7 +392,7 @@ class User {
* Create a new user object using data from session or cookies. If the
* login credentials are invalid, the result is an anonymous user.
*
- * @return User
+ * @return \type{User}
*/
static function newFromSession() {
$user = new User;
@@ -360,17 +403,22 @@ class User {
/**
* Create a new user object from a user row.
* The row should have all fields from the user table in it.
+ * @param $row array A row from the user table
+ * @return \type{User}
*/
static function newFromRow( $row ) {
$user = new User;
$user->loadFromRow( $row );
return $user;
}
+
+ //@}
+
/**
- * Get username given an id.
- * @param $id Integer: database user id
- * @return string Nickname of a user
+ * Get the username corresponding to a given user ID
+ * @param $id \int User ID
+ * @return \string The corresponding username
*/
static function whoIs( $id ) {
$dbr = wfGetDB( DB_SLAVE );
@@ -378,10 +426,10 @@ class User {
}
/**
- * Get the real name of a user given their identifier
+ * Get the real name of a user given their user ID
*
- * @param $id Int: database user id
- * @return string Real name of a user
+ * @param $id \int User ID
+ * @return \string The corresponding user's real name
*/
static function whoIsReal( $id ) {
$dbr = wfGetDB( DB_SLAVE );
@@ -390,12 +438,11 @@ class User {
/**
* Get database id given a user name
- * @param $name String: nickname of a user
- * @return integer|null Database user id (null: if non existent
- * @static
+ * @param $name \string Username
+ * @return \types{\int,\null} The corresponding user's ID, or null if user is nonexistent
*/
static function idFromName( $name ) {
- $nt = Title::newFromText( $name );
+ $nt = Title::makeTitleSafe( NS_USER, $name );
if( is_null( $nt ) ) {
# Illegal name
return null;
@@ -423,8 +470,8 @@ class User {
* addresses like this, if we allowed accounts like this to be created
* new users could get the old edits of these anonymous users.
*
- * @param $name String: nickname of a user
- * @return bool
+ * @param $name \string String to match
+ * @return \bool True or false
*/
static function isIP( $name ) {
return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
@@ -438,8 +485,8 @@ class User {
* is longer than the maximum allowed username size or doesn't begin with
* a capital letter.
*
- * @param $name string
- * @return bool
+ * @param $name \string String to match
+ * @return \bool True or false
*/
static function isValidUserName( $name ) {
global $wgContLang, $wgMaxNameChars;
@@ -492,8 +539,8 @@ class User {
* If an account already exists in this form, login will be blocked
* by a failure to pass this function.
*
- * @param $name string
- * @return bool
+ * @param $name \string String to match
+ * @return \bool True or false
*/
static function isUsableName( $name ) {
global $wgReservedUsernames;
@@ -502,8 +549,14 @@ class User {
return false;
}
+ static $reservedUsernames = false;
+ if ( !$reservedUsernames ) {
+ $reservedUsernames = $wgReservedUsernames;
+ wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
+ }
+
// Certain names may be reserved for batch processes.
- foreach ( $wgReservedUsernames as $reserved ) {
+ foreach ( $reservedUsernames as $reserved ) {
if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
$reserved = wfMsgForContent( substr( $reserved, 4 ) );
}
@@ -524,8 +577,8 @@ class User {
* rather than in isValidUserName() to avoid disrupting
* existing accounts.
*
- * @param $name string
- * @return bool
+ * @param $name \string String to match
+ * @return \bool True or false
*/
static function isCreatableName( $name ) {
return
@@ -538,8 +591,8 @@ class User {
/**
* Is the input a valid password for this user?
*
- * @param $password String: desired password
- * @return bool
+ * @param $password \string Desired password
+ * @return \bool True or false
*/
function isValidPassword( $password ) {
global $wgMinimalPasswordLength, $wgContLang;
@@ -556,7 +609,7 @@ class User {
}
/**
- * Does a string look like an email address?
+ * Does a string look like an e-mail address?
*
* There used to be a regular expression here, it got removed because it
* rejected valid addresses. Actually just check if there is '@' somewhere
@@ -564,8 +617,8 @@ class User {
*
* @todo Check for RFC 2822 compilance (bug 959)
*
- * @param $addr String: email address
- * @return bool
+ * @param $addr \string E-mail address
+ * @return \bool True or false
*/
public static function isValidEmailAddr( $addr ) {
$result = null;
@@ -579,12 +632,12 @@ class User {
/**
* Given unvalidated user input, return a canonical username, or false if
* the username is invalid.
- * @param $name string
- * @param $validate Mixed: type of validation to use:
- * false No validation
- * 'valid' Valid for batch processes
- * 'usable' Valid for batch processes and login
- * 'creatable' Valid for batch processes, login and account creation
+ * @param $name \string User input
+ * @param $validate \types{\string,\bool} Type of validation to use:
+ * - false No validation
+ * - 'valid' Valid for batch processes
+ * - 'usable' Valid for batch processes and login
+ * - 'creatable' Valid for batch processes, login and account creation
*/
static function getCanonicalName( $name, $validate = 'valid' ) {
# Force usernames to capital
@@ -598,7 +651,9 @@ class User {
return false;
# Clean up name according to title rules
- $t = Title::newFromText( $name );
+ $t = ($validate === 'valid') ?
+ Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
+ # Check for invalid titles
if( is_null( $t ) ) {
return false;
}
@@ -634,11 +689,10 @@ class User {
/**
* Count the number of edits of a user
+ * @todo It should not be static and some day should be merged as proper member function / deprecated -- domas
*
- * It should not be static and some day should be merged as proper member function / deprecated -- domas
- *
- * @param $uid Int: the user ID to check
- * @return int
+ * @param $uid \int User ID to check
+ * @return \int The user's edit count
*/
static function edits( $uid ) {
wfProfileIn( __METHOD__ );
@@ -674,7 +728,7 @@ class User {
* Return a random password. Sourced from mt_rand, so it's not particularly secure.
* @todo hash random numbers to improve security, like generateToken()
*
- * @return string
+ * @return \string New random password
*/
static function randomPassword() {
global $wgMinimalPasswordLength;
@@ -691,9 +745,10 @@ class User {
}
/**
- * Set cached properties to default. Note: this no longer clears
- * uncached lazy-initialised properties. The constructor does that instead.
+ * Set cached properties to default.
*
+ * @note This no longer clears uncached lazy-initialised properties;
+ * the constructor does that instead.
* @private
*/
function loadDefaults( $name = false ) {
@@ -728,8 +783,7 @@ class User {
}
/**
- * Initialise php session
- * @deprecated use wfSetupSession()
+ * @deprecated Use wfSetupSession().
*/
function SetupSession() {
wfDeprecated( __METHOD__ );
@@ -738,8 +792,8 @@ class User {
/**
* Load user data from the session or login cookie. If there are no valid
- * credentials, initialises the user as an anon.
- * @return true if the user is logged in, false otherwise
+ * credentials, initialises the user as an anonymous user.
+ * @return \bool True if the user is logged in, false otherwise.
*/
private function loadFromSession() {
global $wgMemc, $wgCookiePrefix;
@@ -750,20 +804,27 @@ class User {
return $result;
}
- if ( isset( $_SESSION['wsUserID'] ) ) {
- if ( 0 != $_SESSION['wsUserID'] ) {
+ if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
+ $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
+ if( isset( $_SESSION['wsUserID'] ) && $sId != $_SESSION['wsUserID'] ) {
+ $this->loadDefaults(); // Possible collision!
+ wfDebugLog( 'loginSessions', "Session user ID ({$_SESSION['wsUserID']}) and
+ cookie user ID ($sId) don't match!" );
+ return false;
+ }
+ $_SESSION['wsUserID'] = $sId;
+ } else if ( isset( $_SESSION['wsUserID'] ) ) {
+ if ( $_SESSION['wsUserID'] != 0 ) {
$sId = $_SESSION['wsUserID'];
} else {
$this->loadDefaults();
return false;
}
- } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
- $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
- $_SESSION['wsUserID'] = $sId;
} else {
$this->loadDefaults();
return false;
}
+
if ( isset( $_SESSION['wsUserName'] ) ) {
$sName = $_SESSION['wsUserName'];
} else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
@@ -806,10 +867,10 @@ class User {
}
/**
- * Load user and user_group data from the database
- * $this->mId must be set, this is how the user is identified.
+ * 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
+ * @return \bool True if the user exists, false if the user is anonymous
* @private
*/
function loadFromDatabase() {
@@ -840,7 +901,9 @@ class User {
}
/**
- * Initialise the user object from a row from the user table
+ * Initialize this object from a row from the user table.
+ *
+ * @param $row \type{\arrayof{\mixed}} Row from the user table to load.
*/
function loadFromRow( $row ) {
$this->mDataLoaded = true;
@@ -865,7 +928,7 @@ class User {
}
/**
- * Load the groups from the database if they aren't already loaded
+ * Load the groups from the database if they aren't already loaded.
* @private
*/
function loadGroups() {
@@ -884,8 +947,8 @@ class User {
/**
* Clear various cached data stored in this object.
- * @param $reloadFrom String: reload user and user_groups table data from a
- * given source. May be "name", "id", "defaults", "session" or false for
+ * @param $reloadFrom \string 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 ) {
@@ -906,9 +969,8 @@ class User {
/**
* Combine the language default options with any site-specific options
* and add the default language variants.
- * Not really private cause it's called by Language class
- * @return array
- * @private
+ *
+ * @return \type{\arrayof{\string}} Array of options
*/
static function getDefaultOptions() {
global $wgNamespacesToBeSearchedDefault;
@@ -934,8 +996,8 @@ class User {
/**
* Get a given default option value.
*
- * @param $opt string
- * @return string
+ * @param $opt \string Name of option to retrieve
+ * @return \string Default option value
*/
public static function getDefaultOption( $opt ) {
$defOpts = self::getDefaultOptions();
@@ -948,7 +1010,7 @@ class User {
/**
* Get a list of user toggle names
- * @return array
+ * @return \type{\arrayof{\string}} Array of user toggle names
*/
static function getToggles() {
global $wgContLang;
@@ -961,7 +1023,7 @@ class User {
/**
* Get blocking information
* @private
- * @param $bFromSlave Bool: specify whether to check slave or master. To
+ * @param $bFromSlave \bool Whether to check the slave database first. To
* improve performance, non-critical checks are done
* against slaves. Check when actually saving should be
* done against master.
@@ -986,6 +1048,7 @@ class User {
$this->mBlockedby = 0;
$this->mHideName = 0;
+ $this->mAllowUsertalk = 0;
$ip = wfGetIP();
if ($this->isAllowed( 'ipblock-exempt' ) ) {
@@ -1001,12 +1064,14 @@ class User {
$this->mBlockedby = $this->mBlock->mBy;
$this->mBlockreason = $this->mBlock->mReason;
$this->mHideName = $this->mBlock->mHideName;
+ $this->mAllowUsertalk = $this->mBlock->mAllowUsertalk;
if ( $this->isLoggedIn() ) {
$this->spreadBlock();
}
} else {
- $this->mBlock = null;
- wfDebug( __METHOD__.": No block.\n" );
+ // Bug 13611: don't remove mBlock here, to allow account creation blocks to
+ // apply to users. Note that the existence of $this->mBlock is not used to
+ // check for edit blocks, $this->mBlockedby is instead.
}
# Proxy blocking
@@ -1032,6 +1097,12 @@ class User {
wfProfileOut( __METHOD__ );
}
+ /**
+ * Whether the given IP is in the SORBS blacklist.
+ *
+ * @param $ip \string IP to check
+ * @return \bool True if blacklisted.
+ */
function inSorbsBlacklist( $ip ) {
global $wgEnableSorbs, $wgSorbsUrl;
@@ -1039,24 +1110,27 @@ class User {
$this->inDnsBlacklist( $ip, $wgSorbsUrl );
}
+ /**
+ * Whether the given IP is in a given DNS blacklist.
+ *
+ * @param $ip \string IP to check
+ * @param $base \string URL of the DNS blacklist
+ * @return \bool True if blacklisted.
+ */
function inDnsBlacklist( $ip, $base ) {
wfProfileIn( __METHOD__ );
$found = false;
$host = '';
- // FIXME: IPv6 ???
- $m = array();
- if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
+ // FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170)
+ if( IP::isIPv4($ip) ) {
# Make hostname
- for ( $i=4; $i>=1; $i-- ) {
- $host .= $m[$i] . '.';
- }
- $host .= $base;
+ $host = "$ip.$base";
# Send query
$ipList = gethostbynamel( $host );
- if ( $ipList ) {
+ if( $ipList ) {
wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
$found = true;
} else {
@@ -1071,7 +1145,7 @@ class User {
/**
* Is this user subject to rate limiting?
*
- * @return bool
+ * @return \bool True if rate limited
*/
public function isPingLimitable() {
global $wgRateLimitsExcludedGroups;
@@ -1086,10 +1160,11 @@ class User {
* Primitive rate limits: enforce maximum actions per time period
* to put a brake on flooding.
*
- * Note: when using a shared cache like memcached, IP-address
+ * @note When using a shared cache like memcached, IP-address
* last-hit counters will be shared across wikis.
*
- * @return bool true if a rate limiter was tripped
+ * @param $action \string Action to enforce; 'edit' if unspecified
+ * @return \bool True if a rate limiter was tripped
*/
function pingLimiter( $action='edit' ) {
@@ -1180,7 +1255,9 @@ class User {
/**
* Check if user is blocked
- * @return bool True if blocked, false otherwise
+ *
+ * @param $bFromSlave \bool Whether to check the slave database instead of the master
+ * @return \bool True if blocked, false otherwise
*/
function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
wfDebug( "User::isBlocked: enter\n" );
@@ -1190,6 +1267,10 @@ class User {
/**
* Check if user is blocked from editing a particular article
+ *
+ * @param $title \string Title to check
+ * @param $bFromSlave \bool Whether to check the slave database instead of the master
+ * @return \bool True if blocked, false otherwise
*/
function isBlockedFrom( $title, $bFromSlave = false ) {
global $wgBlockAllowsUTEdit;
@@ -1198,8 +1279,9 @@ class User {
wfDebug( __METHOD__.": asking isBlocked()\n" );
$blocked = $this->isBlocked( $bFromSlave );
+ $allowUsertalk = ($wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false);
# If a user's name is suppressed, they cannot make edits anywhere
- if ( !$this->mHideName && $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
+ if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
$title->getNamespace() == NS_USER_TALK ) {
$blocked = false;
wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
@@ -1209,8 +1291,8 @@ class User {
}
/**
- * Get name of blocker
- * @return string name of blocker
+ * If user is blocked, return the name of the user who placed the block
+ * @return \string name of blocker
*/
function blockedBy() {
$this->getBlockedStatus();
@@ -1218,16 +1300,74 @@ class User {
}
/**
- * Get blocking reason
- * @return string Blocking reason
+ * If user is blocked, return the specified reason for the block
+ * @return \string Blocking reason
*/
function blockedFor() {
$this->getBlockedStatus();
return $this->mBlockreason;
}
+
+ /**
+ * Check if user is blocked on all wikis.
+ * Do not use for actual edit permission checks!
+ * This is intented for quick UI checks.
+ *
+ * @param $ip \type{\string} IP address, uses current client if none given
+ * @return \type{\bool} True if blocked, false otherwise
+ */
+ function isBlockedGlobally( $ip = '' ) {
+ if( $this->mBlockedGlobally !== null ) {
+ return $this->mBlockedGlobally;
+ }
+ // User is already an IP?
+ if( IP::isIPAddress( $this->getName() ) ) {
+ $ip = $this->getName();
+ } else if( !$ip ) {
+ $ip = wfGetIP();
+ }
+ $blocked = false;
+ wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
+ $this->mBlockedGlobally = (bool)$blocked;
+ return $this->mBlockedGlobally;
+ }
+
+ /**
+ * Check if user account is locked
+ *
+ * @return \type{\bool} True if locked, false otherwise
+ */
+ function isLocked() {
+ if( $this->mLocked !== null ) {
+ return $this->mLocked;
+ }
+ global $wgAuth;
+ $authUser = $wgAuth->getUserInstance( $this );
+ $this->mLocked = (bool)$authUser->isLocked();
+ return $this->mLocked;
+ }
+
+ /**
+ * Check if user account is hidden
+ *
+ * @return \type{\bool} True if hidden, false otherwise
+ */
+ function isHidden() {
+ if( $this->mHideName !== null ) {
+ return $this->mHideName;
+ }
+ $this->getBlockedStatus();
+ if( !$this->mHideName ) {
+ global $wgAuth;
+ $authUser = $wgAuth->getUserInstance( $this );
+ $this->mHideName = (bool)$authUser->isHidden();
+ }
+ return $this->mHideName;
+ }
/**
- * Get the user ID. Returns 0 if the user is anonymous or nonexistent.
+ * Get the user's ID.
+ * @return \int The user's ID; 0 if the user is anonymous or nonexistent
*/
function getId() {
if( $this->mId === null and $this->mName !== null
@@ -1242,7 +1382,8 @@ class User {
}
/**
- * Set the user and reload all fields according to that ID
+ * Set the user and reload all fields according to a given ID
+ * @param $v \int User ID to reload
*/
function setId( $v ) {
$this->mId = $v;
@@ -1250,7 +1391,8 @@ class User {
}
/**
- * Get the user name, or the IP for anons
+ * Get the user name, or the IP of an anonymous user
+ * @return \string User's name or IP address
*/
function getName() {
if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
@@ -1275,8 +1417,9 @@ class User {
* address for an anonymous user to something other than the current
* remote IP.
*
- * User::newFromName() has rougly the same function, when the named user
+ * @note User::newFromName() has rougly the same function, when the named user
* does not exist.
+ * @param $str \string New user name to set
*/
function setName( $str ) {
$this->load();
@@ -1284,13 +1427,17 @@ class User {
}
/**
- * Return the title dbkey form of the name, for eg user pages.
- * @return string
+ * Get the user's name escaped by underscores.
+ * @return \string Username escaped by underscores.
*/
function getTitleKey() {
return str_replace( ' ', '_', $this->getName() );
}
+ /**
+ * Check if the user has new messages.
+ * @return \bool True if the user has new messages
+ */
function getNewtalk() {
$this->load();
@@ -1322,6 +1469,7 @@ class User {
/**
* Return the talk page(s) this user has new messages on.
+ * @return \type{\arrayof{\string}} Array of page URLs
*/
function getNewMessageLinks() {
$talks = array();
@@ -1337,13 +1485,13 @@ class User {
/**
- * Perform a user_newtalk check, uncached.
- * Use getNewtalk for a cached check.
+ * Internal uncached check for new messages
*
- * @param $field string
- * @param $id mixed
- * @param $fromMaster Bool: true to fetch from the master, false for a slave
- * @return bool
+ * @see getNewtalk()
+ * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
+ * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
+ * @param $fromMaster \bool true to fetch from the master, false for a slave
+ * @return \bool True if the user has new messages
* @private
*/
function checkNewtalk( $field, $id, $fromMaster = false ) {
@@ -1358,9 +1506,10 @@ class User {
}
/**
- * Add or update the
- * @param $field string
- * @param $id mixed
+ * Add or update the new messages flag
+ * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
+ * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
+ * @return \bool True if successful, false otherwise
* @private
*/
function updateNewtalk( $field, $id ) {
@@ -1380,8 +1529,9 @@ class User {
/**
* Clear the new messages flag for the given user
- * @param $field string
- * @param $id mixed
+ * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
+ * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
+ * @return \bool True if successful, false otherwise
* @private
*/
function deleteNewtalk( $field, $id ) {
@@ -1400,7 +1550,7 @@ class User {
/**
* Update the 'You have new messages!' status.
- * @param $val bool
+ * @param $val \bool Whether the user has new messages
*/
function setNewtalk( $val ) {
if( wfReadOnly() ) {
@@ -1439,6 +1589,7 @@ class User {
/**
* Generate a current or new-future timestamp to be stored in the
* user_touched field when we update things.
+ * @return \string Timestamp in TS_MW format
*/
private static function newTouchedTimestamp() {
global $wgClockSkewFudge;
@@ -1453,6 +1604,7 @@ class User {
* Called implicitly from invalidateCache() and saveSettings().
*/
private function clearSharedCache() {
+ $this->load();
if( $this->mId ) {
global $wgMemc;
$wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
@@ -1479,13 +1631,25 @@ class User {
}
}
+ /**
+ * Validate the cache for this account.
+ * @param $timestamp \string A timestamp in TS_MW format
+ */
function validateCache( $timestamp ) {
$this->load();
return ($timestamp >= $this->mTouched);
}
/**
- * Set the password and reset the random token
+ * Get the user touched timestamp
+ */
+ function getTouched() {
+ $this->load();
+ return $this->mTouched;
+ }
+
+ /**
+ * Set the password and reset the random token.
* Calls through to authentication plugin if necessary;
* will have no effect if the auth plugin refuses to
* pass the change through or if the legal password
@@ -1495,7 +1659,7 @@ class User {
* wipes it, so the account cannot be logged in until
* a new password is set, for instance via e-mail.
*
- * @param $str string
+ * @param $str \string New password to set
* @throws PasswordError on failure
*/
function setPassword( $str ) {
@@ -1523,10 +1687,9 @@ class User {
}
/**
- * Set the password and reset the random token no matter
- * what.
+ * Set the password and reset the random token unconditionally.
*
- * @param $str string
+ * @param $str \string New password to set
*/
function setInternalPassword( $str ) {
$this->load();
@@ -1542,6 +1705,10 @@ class User {
$this->mNewpassTime = null;
}
+ /**
+ * Get the user's current token.
+ * @return \string Token
+ */
function getToken() {
$this->load();
return $this->mToken;
@@ -1550,6 +1717,8 @@ class User {
/**
* Set the random token (used for persistent authentication)
* Called from loadDefaults() among other places.
+ *
+ * @param $token \string If specified, set the token to this value
* @private
*/
function setToken( $token = false ) {
@@ -1568,7 +1737,13 @@ class User {
$this->mToken = $token;
}
}
-
+
+ /**
+ * Set the cookie password
+ *
+ * @param $str \string New cookie password
+ * @private
+ */
function setCookiePassword( $str ) {
$this->load();
$this->mCookiePassword = md5( $str );
@@ -1576,7 +1751,9 @@ class User {
/**
* Set the password for a password reminder or new account email
- * Sets the user_newpass_time field if $throttle is true
+ *
+ * @param $str \string New password to set
+ * @param $throttle \bool If true, reset the throttle timestamp to the present
*/
function setNewpassword( $str, $throttle = true ) {
$this->load();
@@ -1587,8 +1764,9 @@ class User {
}
/**
- * Returns true if a password reminder email has already been sent within
- * the last $wgPasswordReminderResendTime hours
+ * Has password reminder email been sent within the last
+ * $wgPasswordReminderResendTime hours?
+ * @return \bool True or false
*/
function isPasswordReminderThrottled() {
global $wgPasswordReminderResendTime;
@@ -1600,38 +1778,62 @@ class User {
return time() < $expiry;
}
+ /**
+ * Get the user's e-mail address
+ * @return \string User's email address
+ */
function getEmail() {
$this->load();
wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
return $this->mEmail;
}
+ /**
+ * Get the timestamp of the user's e-mail authentication
+ * @return \string TS_MW timestamp
+ */
function getEmailAuthenticationTimestamp() {
$this->load();
wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
return $this->mEmailAuthenticated;
}
+ /**
+ * Set the user's e-mail address
+ * @param $str \string New e-mail address
+ */
function setEmail( $str ) {
$this->load();
$this->mEmail = $str;
wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
}
+ /**
+ * Get the user's real name
+ * @return \string User's real name
+ */
function getRealName() {
$this->load();
return $this->mRealName;
}
+ /**
+ * Set the user's real name
+ * @param $str \string New real name
+ */
function setRealName( $str ) {
$this->load();
$this->mRealName = $str;
}
/**
- * @param $oname String: the option to check
- * @param $defaultOverride String: A default value returned if the option does not exist
- * @return string
+ * Get the user's current setting for a given option.
+ *
+ * @param $oname \string The option to check
+ * @param $defaultOverride \string A default value returned if the option does not exist
+ * @return \string User's current value for the option
+ * @see getBoolOption()
+ * @see getIntOption()
*/
function getOption( $oname, $defaultOverride = '' ) {
$this->load();
@@ -1649,46 +1851,41 @@ class User {
return $defaultOverride;
}
}
-
- /**
- * Get the user's date preference, including some important migration for
- * old user rows.
- */
- function getDatePreference() {
- if ( is_null( $this->mDatePreference ) ) {
- global $wgLang;
- $value = $this->getOption( 'date' );
- $map = $wgLang->getDatePreferenceMigrationMap();
- if ( isset( $map[$value] ) ) {
- $value = $map[$value];
- }
- $this->mDatePreference = $value;
- }
- return $this->mDatePreference;
- }
-
+
/**
- * @param $oname String: the option to check
- * @return bool False if the option is not selected, true if it is
+ * Get the user's current setting for a given option, as a boolean value.
+ *
+ * @param $oname \string The option to check
+ * @return \bool User's current value for the option
+ * @see getOption()
*/
function getBoolOption( $oname ) {
return (bool)$this->getOption( $oname );
}
+
/**
- * Get an option as an integer value from the source string.
- * @param $oname String: the option to check
- * @param $default Int: optional value to return if option is unset/blank.
- * @return int
+ * Get the user's current setting for a given option, as a boolean value.
+ *
+ * @param $oname \string The option to check
+ * @param $defaultOverride \int A default value returned if the option does not exist
+ * @return \int User's current value for the option
+ * @see getOption()
*/
- function getIntOption( $oname, $default=0 ) {
+ function getIntOption( $oname, $defaultOverride=0 ) {
$val = $this->getOption( $oname );
if( $val == '' ) {
- $val = $default;
+ $val = $defaultOverride;
}
return intval( $val );
}
+ /**
+ * Set the given option for a user.
+ *
+ * @param $oname \string The option to set
+ * @param $val \mixed New value to set
+ */
function setOption( $oname, $val ) {
$this->load();
if ( is_null( $this->mOptions ) ) {
@@ -1713,6 +1910,28 @@ class User {
$this->mOptions[$oname] = $val;
}
+ /**
+ * Get the user's preferred date format.
+ * @return \string User's preferred date format
+ */
+ function getDatePreference() {
+ // Important migration for old data rows
+ if ( is_null( $this->mDatePreference ) ) {
+ global $wgLang;
+ $value = $this->getOption( 'date' );
+ $map = $wgLang->getDatePreferenceMigrationMap();
+ if ( isset( $map[$value] ) ) {
+ $value = $map[$value];
+ }
+ $this->mDatePreference = $value;
+ }
+ return $this->mDatePreference;
+ }
+
+ /**
+ * Get the permissions this user has.
+ * @return \type{\arrayof{\string}} Array of permission names
+ */
function getRights() {
if ( is_null( $this->mRights ) ) {
$this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
@@ -1726,7 +1945,7 @@ class User {
/**
* Get the list of explicit group memberships this user has.
* The implicit * and user groups are not included.
- * @return array of strings
+ * @return \type{\arrayof{\string}} Array of internal group names
*/
function getGroups() {
$this->load();
@@ -1737,8 +1956,8 @@ class User {
* Get the list of implicit group memberships this user has.
* This includes all explicit groups, plus 'user' if logged in,
* '*' for all accounts and autopromoted groups
- * @param $recache Boolean: don't use the cache
- * @return array of strings
+ * @param $recache \bool Whether to avoid the cache
+ * @return \type{\arrayof{\string}} Array of internal group names
*/
function getEffectiveGroups( $recache = false ) {
if ( $recache || is_null( $this->mEffectiveGroups ) ) {
@@ -1759,7 +1978,10 @@ class User {
return $this->mEffectiveGroups;
}
- /* Return the edit count for the user. This is where User::edits should have been */
+ /**
+ * Get the user's edit count.
+ * @return \int User'e edit count
+ */
function getEditCount() {
if ($this->mId) {
if ( !isset( $this->mEditCount ) ) {
@@ -1776,7 +1998,7 @@ class User {
/**
* Add the user to the given group.
* This takes immediate effect.
- * @param $group string
+ * @param $group \string Name of the group to add
*/
function addGroup( $group ) {
$dbw = wfGetDB( DB_MASTER );
@@ -1800,7 +2022,7 @@ class User {
/**
* Remove the user from the given group.
* This takes immediate effect.
- * @param $group string
+ * @param $group \string Name of the group to remove
*/
function removeGroup( $group ) {
$this->load();
@@ -1821,27 +2043,24 @@ class User {
/**
- * A more legible check for non-anonymousness.
- * Returns true if the user is not an anonymous visitor.
- *
- * @return bool
+ * Get whether the user is logged in
+ * @return \bool True or false
*/
function isLoggedIn() {
return $this->getID() != 0;
}
/**
- * A more legible check for anonymousness.
- * Returns true if the user is an anonymous visitor.
- *
- * @return bool
+ * Get whether the user is anonymous
+ * @return \bool True or false
*/
function isAnon() {
return !$this->isLoggedIn();
}
/**
- * Whether the user is a bot
+ * Get whether the user is a bot
+ * @return \bool True or false
* @deprecated
*/
function isBot() {
@@ -1851,8 +2070,8 @@ class User {
/**
* Check if user is allowed to access a feature / make an action
- * @param $action String: action to be checked
- * @return boolean True: action is allowed, False: action should not be allowed
+ * @param $action \string action to be checked
+ * @return \bool True if action is allowed, else false
*/
function isAllowed($action='') {
if ( $action === '' )
@@ -1866,7 +2085,7 @@ class User {
/**
* Check whether to enable recent changes patrol features for this user
- * @return bool
+ * @return \bool True or false
*/
public function useRCPatrol() {
global $wgUseRCPatrol;
@@ -1874,8 +2093,8 @@ class User {
}
/**
- * Check whether to enable recent changes patrol features for this user
- * @return bool
+ * Check whether to enable new pages patrol features for this user
+ * @return \bool True or false
*/
public function useNPPatrol() {
global $wgUseRCPatrol, $wgUseNPPatrol;
@@ -1883,31 +2102,34 @@ class User {
}
/**
- * Load a skin if it doesn't exist or return it
+ * Get the current skin, loading it if required
+ * @return \type{Skin} Current skin
* @todo FIXME : need to check the old failback system [AV]
*/
function &getSkin() {
- global $wgRequest;
+ global $wgRequest, $wgAllowUserSkin, $wgDefaultSkin;
if ( ! isset( $this->mSkin ) ) {
wfProfileIn( __METHOD__ );
- # get the user skin
- $userSkin = $this->getOption( 'skin' );
- $userSkin = $wgRequest->getVal('useskin', $userSkin);
-
+ if( $wgAllowUserSkin ) {
+ # get the user skin
+ $userSkin = $this->getOption( 'skin' );
+ $userSkin = $wgRequest->getVal('useskin', $userSkin);
+ } else {
+ # if we're not allowing users to override, then use the default
+ $userSkin = $wgDefaultSkin;
+ }
+
$this->mSkin =& Skin::newFromKey( $userSkin );
wfProfileOut( __METHOD__ );
}
return $this->mSkin;
}
- /**#@+
- * @param $title Title: article title to look at
- */
-
/**
- * Check watched status of an article
- * @return bool True if article is watched
+ * Check the watched status of an article.
+ * @param $title \type{Title} Title of the article to look at
+ * @return \bool True if article is watched
*/
function isWatched( $title ) {
$wl = WatchedItem::fromUserTitle( $this, $title );
@@ -1915,7 +2137,8 @@ class User {
}
/**
- * Watch an article
+ * Watch an article.
+ * @param $title \type{Title} Title of the article to look at
*/
function addWatch( $title ) {
$wl = WatchedItem::fromUserTitle( $this, $title );
@@ -1924,7 +2147,8 @@ class User {
}
/**
- * Stop watching an article
+ * Stop watching an article.
+ * @param $title \type{Title} Title of the article to look at
*/
function removeWatch( $title ) {
$wl = WatchedItem::fromUserTitle( $this, $title );
@@ -1936,6 +2160,7 @@ class User {
* Clear the user's notification timestamp for the given title.
* If e-notif e-mails are on, they will receive notification mails on
* the next change of the page if it's watched etc.
+ * @param $title \type{Title} Title of the article to look at
*/
function clearNotification( &$title ) {
global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker;
@@ -1991,14 +2216,12 @@ class User {
}
}
- /**#@-*/
-
/**
* Resets all of the given user's page-change notification timestamps.
* If e-notif e-mails are on, they will receive notification mails on
* the next change of any watched page.
*
- * @param $currentUser Int: user ID number
+ * @param $currentUser \int User ID
*/
function clearAllNotifications( $currentUser ) {
global $wgUseEnotif, $wgShowUpdatedMarker;
@@ -2021,8 +2244,9 @@ class User {
}
/**
+ * Encode this user's options as a string
+ * @return \string Encoded options
* @private
- * @return string Encoding options
*/
function encodeOptions() {
$this->load();
@@ -2038,6 +2262,8 @@ class User {
}
/**
+ * Set this user's options from an encoded string
+ * @param $str \string Encoded options to import
* @private
*/
function decodeOptions( $str ) {
@@ -2051,46 +2277,30 @@ class User {
}
}
+ /**
+ * Set a cookie on the user's client. Wrapper for
+ * WebResponse::setCookie
+ * @param $name \string Name of the cookie to set
+ * @param $value \string Value to set
+ * @param $exp \int Expiration time, as a UNIX time value;
+ * if 0 or not specified, use the default $wgCookieExpiration
+ */
protected function setCookie( $name, $value, $exp=0 ) {
- global $wgCookiePrefix,$wgCookieDomain,$wgCookieSecure,$wgCookieExpiration, $wgCookieHttpOnly;
- if( $exp == 0 ) {
- $exp = time() + $wgCookieExpiration;
- }
- $httpOnlySafe = wfHttpOnlySafe();
- wfDebugLog( 'cookie',
- 'setcookie: "' . implode( '", "',
- array(
- $wgCookiePrefix . $name,
- $value,
- $exp,
- '/',
- $wgCookieDomain,
- $wgCookieSecure,
- $httpOnlySafe && $wgCookieHttpOnly ) ) . '"' );
- if( $httpOnlySafe && isset( $wgCookieHttpOnly ) ) {
- setcookie( $wgCookiePrefix . $name,
- $value,
- $exp,
- '/',
- $wgCookieDomain,
- $wgCookieSecure,
- $wgCookieHttpOnly );
- } else {
- // setcookie() fails on PHP 5.1 if you give it future-compat paramters.
- // stab stab!
- setcookie( $wgCookiePrefix . $name,
- $value,
- $exp,
- '/',
- $wgCookieDomain,
- $wgCookieSecure );
- }
+ global $wgRequest;
+ $wgRequest->response()->setcookie( $name, $value, $exp );
}
+ /**
+ * Clear a cookie on the user's client
+ * @param $name \string Name of the cookie to clear
+ */
protected function clearCookie( $name ) {
$this->setCookie( $name, '', time() - 86400 );
}
+ /**
+ * Set the default cookies for this session on the user's client.
+ */
function setCookies() {
$this->load();
if ( 0 == $this->mId ) return;
@@ -2110,7 +2320,10 @@ class User {
}
wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
- $_SESSION = $session + $_SESSION;
+ #check for null, since the hook could cause a null value
+ if ( !is_null( $session ) && isset( $_SESSION ) ){
+ $_SESSION = $session + $_SESSION;
+ }
foreach ( $cookies as $name => $value ) {
if ( $value === false ) {
$this->clearCookie( $name );
@@ -2121,7 +2334,7 @@ class User {
}
/**
- * Logout user.
+ * Log this user out.
*/
function logout() {
global $wgUser;
@@ -2131,8 +2344,9 @@ class User {
}
/**
- * Really logout user
- * Clears the cookies and session, resets the instance cache
+ * Clear the user's cookies and session, and reset the instance cache.
+ * @private
+ * @see logout()
*/
function doLogout() {
$this->clearInstanceCache( 'defaults' );
@@ -2147,7 +2361,7 @@ class User {
}
/**
- * Save object settings into database
+ * Save this user's settings into the database.
* @todo Only rarely do all these fields need to be set!
*/
function saveSettings() {
@@ -2178,10 +2392,11 @@ class User {
);
wfRunHooks( 'UserSaveSettings', array( $this ) );
$this->clearSharedCache();
+ $this->getUserPage()->invalidateCache();
}
/**
- * Checks if a user with the given name exists, returns the ID.
+ * If only this user's username is known, and it exists, return the user ID.
*/
function idForName() {
$s = trim( $this->getName() );
@@ -2198,18 +2413,18 @@ class User {
/**
* Add a user to the database, return the user object
*
- * @param $name String: the user's name
- * @param $params Associative array of non-default parameters to save to the database:
- * password The user's password. Password logins will be disabled if this is omitted.
- * newpassword A temporary password mailed to the user
- * email The user's email address
- * email_authenticated The email authentication timestamp
- * real_name The user's real name
- * options An associative array of non-default options
- * token Random authentication token. Do not set.
- * registration Registration timestamp. Do not set.
+ * @param $name \string Username to add
+ * @param $params \type{\arrayof{\string}} Non-default parameters to save to the database:
+ * - password The user's password. Password logins will be disabled if this is omitted.
+ * - newpassword A temporary password mailed to the user
+ * - email The user's email address
+ * - email_authenticated The email authentication timestamp
+ * - real_name The user's real name
+ * - options An associative array of non-default options
+ * - token Random authentication token. Do not set.
+ * - registration Registration timestamp. Do not set.
*
- * @return User object, or null if the username already exists
+ * @return \type{User} A new User object, or null if the username already exists
*/
static function createNew( $name, $params = array() ) {
$user = new User;
@@ -2247,7 +2462,7 @@ class User {
}
/**
- * Add an existing user object to the database
+ * Add this existing user object to the database
*/
function addToDatabase() {
$this->load();
@@ -2271,13 +2486,13 @@ class User {
);
$this->mId = $dbw->insertId();
- # Clear instance cache other than user table data, which is already accurate
+ // Clear instance cache other than user table data, which is already accurate
$this->clearInstanceCache();
}
/**
- * If the (non-anonymous) user is blocked, this function will block any IP address
- * that they successfully log on from.
+ * If this (non-anonymous) user is blocked, block any IP address
+ * they've successfully logged in from.
*/
function spreadBlock() {
wfDebug( __METHOD__."()\n" );
@@ -2306,10 +2521,10 @@ class User {
* which will give them a chance to modify this key based on their own
* settings.
*
- * @return string
+ * @return \string Page rendering hash
*/
function getPageRenderingHash() {
- global $wgContLang, $wgUseDynamicDates, $wgLang;
+ global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
if( $this->mHash ){
return $this->mHash;
}
@@ -2329,6 +2544,8 @@ class User {
$extra = $wgContLang->getExtraHashOptions();
$confstr .= $extra;
+ $confstr .= $wgRenderHashAppend;
+
// Give a chance for extensions to modify the hash, if they have
// extra options or other effects on the parser cache.
wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
@@ -2339,21 +2556,28 @@ class User {
return $confstr;
}
+ /**
+ * Get whether the user is explicitly blocked from account creation.
+ * @return \bool True if blocked
+ */
function isBlockedFromCreateAccount() {
$this->getBlockedStatus();
return $this->mBlock && $this->mBlock->mCreateAccount;
}
/**
- * Determine if the user is blocked from using Special:Emailuser.
- *
- * @return boolean
+ * Get whether the user is blocked from using Special:Emailuser.
+ * @return \bool True if blocked
*/
function isBlockedFromEmailuser() {
$this->getBlockedStatus();
return $this->mBlock && $this->mBlock->mBlockEmail;
}
+ /**
+ * Get whether the user is allowed to create an account.
+ * @return \bool True if allowed
+ */
function isAllowedToCreateAccount() {
return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
}
@@ -2368,7 +2592,7 @@ class User {
/**
* Get this user's personal page title.
*
- * @return Title
+ * @return \type{Title} User's personal page title
*/
function getUserPage() {
return Title::makeTitle( NS_USER, $this->getName() );
@@ -2377,7 +2601,7 @@ class User {
/**
* Get this user's talk page title.
*
- * @return Title
+ * @return \type{Title} User's talk page title
*/
function getTalkPage() {
$title = $this->getUserPage();
@@ -2385,6 +2609,8 @@ class User {
}
/**
+ * Get the maximum valid user ID.
+ * @return \int User ID
* @static
*/
function getMaxID() {
@@ -2401,7 +2627,7 @@ class User {
/**
* Determine whether the user is a newbie. Newbies are either
* anonymous IPs, or the most recently created accounts.
- * @return bool True if it is a newbie.
+ * @return \bool True if the user is a newbie
*/
function isNewbie() {
return !$this->isAllowed( 'autoconfirmed' );
@@ -2411,7 +2637,7 @@ class User {
* Is the user active? We check to see if they've made at least
* X number of edits in the last Y days.
*
- * @return bool true if the user is active, false if not
+ * @return \bool True if the user is active, false if not.
*/
public function isActiveEditor() {
global $wgActiveUserEditCount, $wgActiveUserDays;
@@ -2435,8 +2661,8 @@ class User {
/**
* Check to see if the given clear-text password is one of the accepted passwords
- * @param $password String: user password.
- * @return bool True if the given password is correct otherwise False.
+ * @param $password \string user password.
+ * @return \bool True if the given password is correct, otherwise False.
*/
function checkPassword( $password ) {
global $wgAuth;
@@ -2476,7 +2702,7 @@ class User {
/**
* Check if the given clear-text password matches the temporary password
* sent by e-mail for password reset operations.
- * @return bool
+ * @return \bool True if matches, false otherwise
*/
function checkTemporaryPassword( $plaintext ) {
return self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() );
@@ -2488,9 +2714,8 @@ class User {
* login credentials aren't being hijacked with a foreign form
* submission.
*
- * @param $salt Mixed: optional function-specific data for hash.
- * Use a string or an array of strings.
- * @return string
+ * @param $salt \types{\string,\arrayof{\string}} Optional function-specific data for hashing
+ * @return \string The new edit token
*/
function editToken( $salt = '' ) {
if ( $this->isAnon() ) {
@@ -2510,9 +2735,10 @@ class User {
}
/**
- * Generate a hex-y looking random token for various uses.
- * Could be made more cryptographically sure if someone cares.
- * @return string
+ * Generate a looking random token for various uses.
+ *
+ * @param $salt \string Optional salt value
+ * @return \string The new random token
*/
function generateToken( $salt = '' ) {
$token = dechex( mt_rand() ) . dechex( mt_rand() );
@@ -2525,9 +2751,9 @@ class User {
* user's own login session, not a form submission from a third-party
* site.
*
- * @param $val String: the input value to compare
- * @param $salt String: optional function-specific data for hash
- * @return bool
+ * @param $val \string Input value to compare
+ * @param $salt \string Optional function-specific data for hashing
+ * @return \bool Whether the token matches
*/
function matchEditToken( $val, $salt = '' ) {
$sessionToken = $this->editToken( $salt );
@@ -2538,7 +2764,12 @@ class User {
}
/**
- * Check whether the edit token is fine except for the suffix
+ * Check given value against the token value stored in the session,
+ * ignoring the suffix.
+ *
+ * @param $val \string Input value to compare
+ * @param $salt \string Optional function-specific data for hashing
+ * @return \bool Whether the token matches
*/
function matchEditTokenNoSuffix( $val, $salt = '' ) {
$sessionToken = $this->editToken( $salt );
@@ -2549,10 +2780,7 @@ class User {
* Generate a new e-mail confirmation token and send a confirmation/invalidation
* mail to the user's given address.
*
- * Calls saveSettings() internally; as it has side effects, not committing changes
- * would be pretty silly.
- *
- * @return mixed True on success, a WikiError object on failure.
+ * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure.
*/
function sendConfirmationMail() {
global $wgLang;
@@ -2575,11 +2803,11 @@ class User {
* Send an e-mail to this user's account. Does not check for
* confirmed status or validity.
*
- * @param $subject string
- * @param $body string
- * @param $from string: optional from address; default $wgPasswordSender will be used otherwise.
- * @param $replyto string
- * @return mixed True on success, a WikiError object on failure.
+ * @param $subject \string Message subject
+ * @param $body \string Message body
+ * @param $from \string Optional From address; if unspecified, default $wgPasswordSender will be used
+ * @param $replyto \string Reply-To address
+ * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure
*/
function sendMail( $subject, $body, $from = null, $replyto = null ) {
if( is_null( $from ) ) {
@@ -2594,13 +2822,13 @@ class User {
/**
* Generate, store, and return a new e-mail confirmation code.
- * A hash (unsalted since it's used as a key) is stored.
+ * A hash (unsalted, since it's used as a key) is stored.
*
- * Call saveSettings() after calling this function to commit
+ * @note Call saveSettings() after calling this function to commit
* this change to the database.
*
- * @param &$expiration mixed output: accepts the expiration time
- * @return string
+ * @param[out] &$expiration \mixed Accepts the expiration time
+ * @return \string New token
* @private
*/
function confirmationToken( &$expiration ) {
@@ -2617,8 +2845,8 @@ class User {
/**
* Return a URL the user can use to confirm their email address.
- * @param $token accepts the email confirmation token
- * @return string
+ * @param $token \string Accepts the email confirmation token
+ * @return \string New token URL
* @private
*/
function confirmationTokenUrl( $token ) {
@@ -2626,8 +2854,8 @@ class User {
}
/**
* Return a URL the user can use to invalidate their email address.
- * @param $token accepts the email confirmation token
- * @return string
+ * @param $token \string Accepts the email confirmation token
+ * @return \string New token URL
* @private
*/
function invalidationTokenUrl( $token ) {
@@ -2639,10 +2867,14 @@ class User {
* This uses $wgArticlePath directly as a quickie hack to use the
* hardcoded English names of the Special: pages, for ASCII safety.
*
- * Since these URLs get dropped directly into emails, using the
+ * @note Since these URLs get dropped directly into emails, using the
* short English names avoids insanely long URL-encoded links, which
* also sometimes can get corrupted in some browsers/mailers
* (bug 6957 with Gmail and Internet Explorer).
+ *
+ * @param $page \string Special page
+ * @param $token \string Token
+ * @return \string Formatted URL
*/
protected function getTokenUrl( $page, $token ) {
global $wgArticlePath;
@@ -2656,7 +2888,7 @@ class User {
/**
* Mark the e-mail address confirmed.
*
- * Call saveSettings() after calling this function to commit the change.
+ * @note Call saveSettings() after calling this function to commit the change.
*/
function confirmEmail() {
$this->setEmailAuthenticationTimestamp( wfTimestampNow() );
@@ -2664,10 +2896,10 @@ class User {
}
/**
- * Invalidate the user's email confirmation, unauthenticate the email
- * if it was already confirmed.
+ * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
+ * address if it was already confirmed.
*
- * Call saveSettings() after calling this function to commit the change.
+ * @note Call saveSettings() after calling this function to commit the change.
*/
function invalidateEmail() {
$this->load();
@@ -2677,6 +2909,10 @@ class User {
return true;
}
+ /**
+ * Set the e-mail authentication timestamp.
+ * @param $timestamp \string TS_MW timestamp
+ */
function setEmailAuthenticationTimestamp( $timestamp ) {
$this->load();
$this->mEmailAuthenticated = $timestamp;
@@ -2686,9 +2922,13 @@ class User {
/**
* Is this user allowed to send e-mails within limits of current
* site configuration?
- * @return bool
+ * @return \bool True if allowed
*/
function canSendEmail() {
+ global $wgEnableEmail, $wgEnableUserEmail;
+ if( !$wgEnableEmail || !$wgEnableUserEmail ) {
+ return false;
+ }
$canSend = $this->isEmailConfirmed();
wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
return $canSend;
@@ -2697,7 +2937,7 @@ class User {
/**
* Is this user allowed to receive e-mails within limits of current
* site configuration?
- * @return bool
+ * @return \bool True if allowed
*/
function canReceiveEmail() {
return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
@@ -2707,11 +2947,11 @@ class User {
* Is this user's e-mail address valid-looking and confirmed within
* limits of the current site configuration?
*
- * If $wgEmailAuthentication is on, this may require the user to have
+ * @note If $wgEmailAuthentication is on, this may require the user to have
* confirmed their address by returning a code or using a password
* sent to the address from the wiki.
*
- * @return bool
+ * @return \bool True if confirmed
*/
function isEmailConfirmed() {
global $wgEmailAuthentication;
@@ -2731,8 +2971,8 @@ class User {
}
/**
- * Return true if there is an outstanding request for e-mail confirmation.
- * @return bool
+ * Check whether there is an outstanding request for e-mail confirmation.
+ * @return \bool True if pending
*/
function isEmailConfirmationPending() {
global $wgEmailAuthentication;
@@ -2743,20 +2983,40 @@ class User {
}
/**
- * Get the timestamp of account creation, or false for
- * non-existent/anonymous user accounts
+ * Get the timestamp of account creation.
*
- * @return mixed
+ * @return \types{\string,\bool} string Timestamp of account creation, or false for
+ * non-existent/anonymous user accounts.
*/
public function getRegistration() {
- return $this->mId > 0
+ return $this->getId() > 0
? $this->mRegistration
: false;
}
+
+ /**
+ * Get the timestamp of the first edit
+ *
+ * @return \types{\string,\bool} string Timestamp of first edit, or false for
+ * non-existent/anonymous user accounts.
+ */
+ public function getFirstEditTimestamp() {
+ if( $this->getId() == 0 ) return false; // anons
+ $dbr = wfGetDB( DB_SLAVE );
+ $time = $dbr->selectField( 'revision', 'rev_timestamp',
+ array( 'rev_user' => $this->getId() ),
+ __METHOD__,
+ array( 'ORDER BY' => 'rev_timestamp ASC' )
+ );
+ if( !$time ) return false; // no edits
+ return wfTimestamp( TS_MW, $time );
+ }
/**
- * @param $groups Array: list of groups
- * @return array list of permission key names for given groups combined
+ * Get the permissions associated with a given list of groups
+ *
+ * @param $groups \type{\arrayof{\string}} List of internal group names
+ * @return \type{\arrayof{\string}} List of permission key names for given groups combined
*/
static function getGroupPermissions( $groups ) {
global $wgGroupPermissions;
@@ -2764,15 +3024,35 @@ class User {
foreach( $groups as $group ) {
if( isset( $wgGroupPermissions[$group] ) ) {
$rights = array_merge( $rights,
+ // array_filter removes empty items
array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
}
}
- return $rights;
+ return array_unique($rights);
+ }
+
+ /**
+ * Get all the groups who have a given permission
+ *
+ * @param $role \string Role to check
+ * @return \type{\arrayof{\string}} List of internal group names with the given permission
+ */
+ static function getGroupsWithPermission( $role ) {
+ global $wgGroupPermissions;
+ $allowedGroups = array();
+ foreach ( $wgGroupPermissions as $group => $rights ) {
+ if ( isset( $rights[$role] ) && $rights[$role] ) {
+ $allowedGroups[] = $group;
+ }
+ }
+ return $allowedGroups;
}
/**
- * @param $group String: key name
- * @return string localized descriptive name for group, if provided
+ * Get the localized descriptive name for a group, if it exists
+ *
+ * @param $group \string Internal group name
+ * @return \string Localized descriptive group name
*/
static function getGroupName( $group ) {
global $wgMessageCache;
@@ -2785,8 +3065,10 @@ class User {
}
/**
- * @param $group String: key name
- * @return string localized descriptive name for member of a group, if provided
+ * Get the localized descriptive name for a member of a group, if it exists
+ *
+ * @param $group \string Internal group name
+ * @return \string Localized name for group member
*/
static function getGroupMember( $group ) {
global $wgMessageCache;
@@ -2801,9 +3083,8 @@ class User {
/**
* Return the set of defined explicit groups.
* The implicit groups (by default *, 'user' and 'autoconfirmed')
- * are not included, as they are defined automatically,
- * not in the database.
- * @return array
+ * are not included, as they are defined automatically, not in the database.
+ * @return \type{\arrayof{\string}} Array of internal group names
*/
static function getAllGroups() {
global $wgGroupPermissions;
@@ -2814,7 +3095,8 @@ class User {
}
/**
- * Get a list of all available permissions
+ * Get a list of all available permissions.
+ * @return \type{\arrayof{\string}} Array of permission names
*/
static function getAllRights() {
if ( self::$mAllRights === false ) {
@@ -2831,8 +3113,7 @@ class User {
/**
* Get a list of implicit groups
- *
- * @return array
+ * @return \type{\arrayof{\string}} Array of internal group names
*/
public static function getImplicitGroups() {
global $wgImplicitGroups;
@@ -2844,8 +3125,8 @@ class User {
/**
* Get the title of a page describing a particular group
*
- * @param $group Name of the group
- * @return mixed
+ * @param $group \string Internal group name
+ * @return \types{\type{Title},\bool} Title of the page if it exists, false otherwise
*/
static function getGroupPage( $group ) {
global $wgMessageCache;
@@ -2860,11 +3141,12 @@ class User {
}
/**
- * Create a link to the group in HTML, if available
+ * Create a link to the group in HTML, if available;
+ * else return the group name.
*
- * @param $group Name of the group
- * @param $text The text of the link
- * @return mixed
+ * @param $group \string Internal name of the group
+ * @param $text \string The text of the link
+ * @return \string HTML link to the group
*/
static function makeGroupLinkHTML( $group, $text = '' ) {
if( $text == '' ) {
@@ -2881,11 +3163,12 @@ class User {
}
/**
- * Create a link to the group in Wikitext, if available
+ * Create a link to the group in Wikitext, if available;
+ * else return the group name.
*
- * @param $group Name of the group
- * @param $text The text of the link (by default, the name of the group)
- * @return mixed
+ * @param $group \string Internal name of the group
+ * @param $text \string The text of the link
+ * @return \string Wikilink to the group
*/
static function makeGroupLinkWiki( $group, $text = '' ) {
if( $text == '' ) {
@@ -2944,6 +3227,12 @@ class User {
$this->invalidateCache();
}
+ /**
+ * Get the description of a given right
+ *
+ * @param $right \string Right to query
+ * @return \string Localized description of the right
+ */
static function getRightDescription( $right ) {
global $wgMessageCache;
$wgMessageCache->loadAllMessages();
@@ -2957,8 +3246,9 @@ class User {
/**
* Make an old-style password hash
*
- * @param $password String: plain-text password
- * @param $userId String: user ID
+ * @param $password \string Plain-text password
+ * @param $userId \string User ID
+ * @return \string Password hash
*/
static function oldCrypt( $password, $userId ) {
global $wgPasswordSalt;
@@ -2972,19 +3262,26 @@ class User {
/**
* Make a new-style password hash
*
- * @param $password String: plain-text password
- * @param $salt String: salt, may be random or the user ID. False to generate a salt.
+ * @param $password \string Plain-text password
+ * @param $salt \string Optional salt, may be random or the user ID.
+ * If unspecified or false, will generate one automatically
+ * @return \string Password hash
*/
static function crypt( $password, $salt = false ) {
global $wgPasswordSalt;
- if($wgPasswordSalt) {
+ $hash = '';
+ if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
+ return $hash;
+ }
+
+ if( $wgPasswordSalt ) {
if ( $salt === false ) {
$salt = substr( wfGenerateToken(), 0, 8 );
}
return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
} else {
- return ':A:' . md5( $password);
+ return ':A:' . md5( $password );
}
}
@@ -2992,13 +3289,20 @@ class User {
* Compare a password hash with a plain-text password. Requires the user
* ID if there's a chance that the hash is an old-style hash.
*
- * @param $hash String: password hash
- * @param $password String: plain-text password to compare
- * @param $userId String: user ID for old-style password salt
+ * @param $hash \string Password hash
+ * @param $password \string Plain-text password to compare
+ * @param $userId \string User ID for old-style password salt
+ * @return \bool
*/
static function comparePasswords( $hash, $password, $userId = false ) {
$m = false;
$type = substr( $hash, 0, 3 );
+
+ $result = false;
+ if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
+ return $result;
+ }
+
if ( $type == ':A:' ) {
# Unsalted
return md5( $password ) === substr( $hash, 3 );
@@ -3011,4 +3315,41 @@ class User {
return self::oldCrypt( $password, $userId ) === $hash;
}
}
+
+ /**
+ * Add a newuser log entry for this user
+ * @param $byEmail Boolean: account made by email?
+ */
+ public function addNewUserLogEntry( $byEmail = false ) {
+ global $wgUser, $wgContLang, $wgNewUserLog;
+ if( empty($wgNewUserLog) ) {
+ return true; // disabled
+ }
+ $talk = $wgContLang->getFormattedNsText( NS_TALK );
+ if( $this->getName() == $wgUser->getName() ) {
+ $action = 'create';
+ $message = '';
+ } else {
+ $action = 'create2';
+ $message = $byEmail ? wfMsgForContent( 'newuserlog-byemail' ) : '';
+ }
+ $log = new LogPage( 'newusers' );
+ $log->addEntry( $action, $this->getUserPage(), $message, array( $this->getId() ) );
+ return true;
+ }
+
+ /**
+ * Add an autocreate newuser log entry for this user
+ * Used by things like CentralAuth and perhaps other authplugins.
+ */
+ public function addNewUserLogEntryAutoCreate() {
+ global $wgNewUserLog;
+ if( empty($wgNewUserLog) ) {
+ return true; // disabled
+ }
+ $log = new LogPage( 'newusers', false );
+ $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) );
+ return true;
+ }
+
}
diff --git a/includes/UserArray.php b/includes/UserArray.php
index 27847e6f..a2f54b7f 100644
--- a/includes/UserArray.php
+++ b/includes/UserArray.php
@@ -36,6 +36,10 @@ class UserArrayFromResult extends UserArray {
}
}
+ public function count() {
+ return $this->res->numRows();
+ }
+
function current() {
return $this->current;
}
diff --git a/includes/UserMailer.php b/includes/UserMailer.php
index 0bc4268f..ab1a740b 100644
--- a/includes/UserMailer.php
+++ b/includes/UserMailer.php
@@ -32,13 +32,15 @@ class MailAddress {
* @param $address Mixed: string with an email address, or a User object
* @param $name String: human-readable name if a string address is given
*/
- function __construct( $address, $name=null ) {
+ function __construct( $address, $name = null, $realName = null ) {
if( is_object( $address ) && $address instanceof User ) {
$this->address = $address->getEmail();
$this->name = $address->getName();
+ $this->realName = $address->getRealName();
} else {
$this->address = strval( $address );
$this->name = strval( $name );
+ $this->reaName = strval( $realName );
}
}
@@ -51,7 +53,9 @@ class MailAddress {
# can't handle "Joe Bloggs <joe@bloggs.com>" format email addresses,
# so don't bother generating them
if( $this->name != '' && !wfIsWindows() ) {
- $quoted = wfQuotedPrintable( $this->name );
+ global $wgEnotifUseRealName;
+ $name = ( $wgEnotifUseRealName && $this->realName ) ? $this->realName : $this->name;
+ $quoted = wfQuotedPrintable( $name );
if( strpos( $quoted, '.' ) !== false || strpos( $quoted, ',' ) !== false ) {
$quoted = '"' . $quoted . '"';
}
@@ -98,9 +102,10 @@ class UserMailer {
* @param $subject String: email's subject.
* @param $body String: email's text.
* @param $replyto String: optional reply-to email (default: null).
+ * @param $contentType String: optional custom Content-Type
* @return mixed True on success, a WikiError object on failure.
*/
- static function send( $to, $from, $subject, $body, $replyto=null ) {
+ static function send( $to, $from, $subject, $body, $replyto=null, $contentType=null ) {
global $wgSMTP, $wgOutputEncoding, $wgErrorString, $wgEnotifImpersonal;
global $wgEnotifMaxRecips;
@@ -139,7 +144,8 @@ class UserMailer {
$headers['Subject'] = wfQuotedPrintable( $subject );
$headers['Date'] = date( 'r' );
$headers['MIME-Version'] = '1.0';
- $headers['Content-type'] = 'text/plain; charset='.$wgOutputEncoding;
+ $headers['Content-type'] = (is_null($contentType) ?
+ 'text/plain; charset='.$wgOutputEncoding : $contentType);
$headers['Content-transfer-encoding'] = '8bit';
$headers['Message-ID'] = "<$msgid@" . $wgSMTP['IDHost'] . '>'; // FIXME
$headers['X-Mailer'] = 'MediaWiki mailer';
@@ -170,9 +176,11 @@ class UserMailer {
} else {
$endl = "\n";
}
+ $ctype = (is_null($contentType) ?
+ 'text/plain; charset='.$wgOutputEncoding : $contentType);
$headers =
"MIME-Version: 1.0$endl" .
- "Content-type: text/plain; charset={$wgOutputEncoding}$endl" .
+ "Content-type: $ctype$endl" .
"Content-Transfer-Encoding: 8bit$endl" .
"X-Mailer: MediaWiki mailer$endl".
'From: ' . $from->toString();
@@ -255,14 +263,9 @@ class UserMailer {
*
*/
class EmailNotification {
- /**@{{
- * @private
- */
- var $to, $subject, $body, $replyto, $from;
- var $user, $title, $timestamp, $summary, $minorEdit, $oldid, $composed_common, $editor;
- var $mailTargets = array();
-
- /**@}}*/
+ private $to, $subject, $body, $replyto, $from;
+ private $user, $title, $timestamp, $summary, $minorEdit, $oldid, $composed_common, $editor;
+ private $mailTargets = array();
/**
* Send emails corresponding to the user $editor editing the page $title.
@@ -339,7 +342,7 @@ class EmailNotification {
$userTalkId = false;
- if ( (!$minorEdit || $wgEnotifMinorEdits) ) {
+ if ( !$minorEdit || ($wgEnotifMinorEdits && !$editor->isAllowed('nominornewtalk') ) ) {
if ( $wgEnotifUserTalk && $isUserTalkPage ) {
$targetUser = User::newFromName( $title->getText() );
if ( !$targetUser || $targetUser->isAnon() ) {
@@ -347,9 +350,13 @@ class EmailNotification {
} elseif ( $targetUser->getId() == $editor->getId() ) {
wfDebug( __METHOD__.": user edited their own talk page, no notification sent\n" );
} elseif( $targetUser->getOption( 'enotifusertalkpages' ) ) {
- wfDebug( __METHOD__.": sending talk page update notification\n" );
- $this->compose( $targetUser );
- $userTalkId = $targetUser->getId();
+ if( $targetUser->isEmailConfirmed() ) {
+ wfDebug( __METHOD__.": sending talk page update notification\n" );
+ $this->compose( $targetUser );
+ $userTalkId = $targetUser->getId();
+ } else {
+ wfDebug( __METHOD__.": talk page owner doesn't have validated email\n" );
+ }
} else {
wfDebug( __METHOD__.": talk page owner doesn't want notifications\n" );
}
@@ -396,7 +403,9 @@ class EmailNotification {
$this->sendMails();
- if ( $wgShowUpdatedMarker || $wgEnotifWatchlist ) {
+ $latestTimestamp = Revision::getTimestampFromId( $title, $title->getLatestRevID() );
+ // Do not update watchlists if something else already did.
+ if ( $timestamp >= $latestTimestamp && ($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. Do
# not do this to users for their own edits.
@@ -422,7 +431,7 @@ class EmailNotification {
function composeCommonMailtext() {
global $wgPasswordSender, $wgNoReplyAddress;
global $wgEnotifFromEditor, $wgEnotifRevealEditorAddress;
- global $wgEnotifImpersonal;
+ global $wgEnotifImpersonal, $wgEnotifUseRealName;
$this->composed_common = true;
@@ -439,9 +448,6 @@ class EmailNotification {
$replyto = ''; /* fail safe */
$keys = array();
- # regarding the use of oldid as an indicator for the last visited version, see also
- # http://bugzilla.wikipeda.org/show_bug.cgi?id=603 "Delete + undelete cycle doesn't preserve old_id"
- # However, in the case of a new page which is already watched, we have no previous version to compare
if( $this->oldid ) {
$difflink = $this->title->getFullUrl( 'diff=0&oldid=' . $this->oldid );
$keys['$NEWPAGE'] = wfMsgForContent( 'enotif_lastvisited', $difflink );
@@ -476,7 +482,7 @@ class EmailNotification {
# the user has not opted-out and the option is enabled at the
# global configuration level.
$editor = $this->editor;
- $name = $editor->getName();
+ $name = $wgEnotifUseRealName ? $editor->getRealName() : $editor->getName();
$adminAddress = new MailAddress( $wgPasswordSender, 'WikiAdmin' );
$editorAddress = new MailAddress( $editor );
if( $wgEnotifRevealEditorAddress
@@ -557,12 +563,13 @@ class EmailNotification {
* @private
*/
function sendPersonalised( $watchingUser ) {
- global $wgLang;
+ global $wgLang, $wgEnotifUseRealName;
// From the PHP manual:
// Note: The to parameter cannot be an address in the form of "Something <someone@example.com>".
// The mail command will not parse this properly while talking with the MTA.
$to = new MailAddress( $watchingUser );
- $body = str_replace( '$WATCHINGUSERNAME', $watchingUser->getName() , $this->body );
+ $name = $wgEnotifUseRealName ? $watchingUser->getRealName() : $watchingUser->getName();
+ $body = str_replace( '$WATCHINGUSERNAME', $name , $this->body );
$timecorrection = $watchingUser->getOption( 'timecorrection' );
diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php
index 23fc6a74..2d2d34f1 100644
--- a/includes/WatchedItem.php
+++ b/includes/WatchedItem.php
@@ -8,22 +8,23 @@
* @ingroup Watchlist
*/
class WatchedItem {
- var $mTitle, $mUser;
+ var $mTitle, $mUser, $id, $ns, $ti;
/**
* Create a WatchedItem object with the given user and title
- * @todo document
- * @access private
+ * @param $user User: the user to use for (un)watching
+ * @param $title Title: the title we're going to (un)watch
+ * @return WatchedItem object
*/
- static function fromUserTitle( $user, $title ) {
+ public static function fromUserTitle( $user, $title ) {
$wl = new WatchedItem;
$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 ?
-# The change results in talk-pages not automatically included in watchlists, when their parent page is included
-# $wl->ns = $title->getNamespace() & ~1;
+ # 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 ?
+ # The change results in talk-pages not automatically included in watchlists, when their parent page is included
+ # $wl->ns = $title->getNamespace() & ~1;
$wl->ns = $title->getNamespace();
$wl->ti = $title->getDBkey();
@@ -32,8 +33,9 @@ class WatchedItem {
/**
* Is mTitle being watched by mUser?
+ * @return bool
*/
- function isWatched() {
+ public function isWatched() {
# Pages and their talk pages are considered equivalent for watching;
# remember that talk namespaces are numbered as page namespace+1.
$fname = 'WatchedItem::isWatched';
@@ -46,9 +48,11 @@ class WatchedItem {
}
/**
- * @todo document
+ * Given a title and user (assumes the object is setup), add the watch to the
+ * database.
+ * @return bool (always true)
*/
- function addWatch() {
+ public function addWatch() {
$fname = 'WatchedItem::addWatch';
wfProfileIn( $fname );
@@ -77,7 +81,11 @@ class WatchedItem {
return true;
}
- function removeWatch() {
+ /**
+ * Same as addWatch, only the opposite.
+ * @return bool
+ */
+ public function removeWatch() {
$fname = 'WatchedItem::removeWatch';
$success = false;
@@ -118,11 +126,14 @@ class WatchedItem {
* @param $ot Title: page title to duplicate entries from, if present
* @param $nt Title: page title to add watches on
*/
- static function duplicateEntries( $ot, $nt ) {
+ public static function duplicateEntries( $ot, $nt ) {
WatchedItem::doDuplicateEntries( $ot->getSubjectPage(), $nt->getSubjectPage() );
WatchedItem::doDuplicateEntries( $ot->getTalkPage(), $nt->getTalkPage() );
}
+ /**
+ * Handle duplicate entries. Backend for duplicateEntries().
+ */
private static function doDuplicateEntries( $ot, $nt ) {
$fname = "WatchedItem::duplicateEntries";
$oldnamespace = $ot->getNamespace();
diff --git a/includes/WatchlistEditor.php b/includes/WatchlistEditor.php
index fcfdb782..e49851bd 100644
--- a/includes/WatchlistEditor.php
+++ b/includes/WatchlistEditor.php
@@ -46,19 +46,19 @@ class WatchlistEditor {
$this->unwatchTitles( $toUnwatch, $user );
$user->invalidateCache();
if( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 )
- $output->addHtml( wfMsgExt( 'watchlistedit-raw-done', 'parse' ) );
+ $output->addHTML( wfMsgExt( 'watchlistedit-raw-done', 'parse' ) );
if( ( $count = count( $toWatch ) ) > 0 ) {
- $output->addHtml( wfMsgExt( 'watchlistedit-raw-added', 'parse', $count ) );
+ $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 ) );
+ $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 ) ) );
+ $output->addHTML( wfMsgExt( 'watchlistedit-raw-removed', 'parse', count( $current ) ) );
$this->showTitles( $current, $output, $wgUser->getSkin() );
}
}
@@ -70,7 +70,7 @@ class WatchlistEditor {
$titles = $this->extractTitles( $request->getArray( 'titles' ) );
$this->unwatchTitles( $titles, $user );
$user->invalidateCache();
- $output->addHtml( wfMsgExt( 'watchlistedit-normal-done', 'parse',
+ $output->addHTML( wfMsgExt( 'watchlistedit-normal-done', 'parse',
$GLOBALS['wgLang']->formatNum( count( $titles ) ) ) );
$this->showTitles( $titles, $output, $wgUser->getSkin() );
}
@@ -138,16 +138,16 @@ class WatchlistEditor {
}
$batch->execute();
// Print out the list
- $output->addHtml( "<ul>\n" );
+ $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 )
+ $output->addHTML( "<li>" . $skin->makeLinkObj( $title )
. ' (' . $skin->makeLinkObj( $title->getTalkPage(), $talk ) . ")</li>\n" );
}
}
- $output->addHtml( "</ul>\n" );
+ $output->addHTML( "</ul>\n" );
}
/**
@@ -239,10 +239,10 @@ class WatchlistEditor {
*/
private function showItemCount( $output, $user ) {
if( ( $count = $this->countWatchlist( $user ) ) > 0 ) {
- $output->addHtml( wfMsgExt( 'watchlistedit-numitems', 'parse',
+ $output->addHTML( wfMsgExt( 'watchlistedit-numitems', 'parse',
$GLOBALS['wgLang']->formatNum( $count ) ) );
} else {
- $output->addHtml( wfMsgExt( 'watchlistedit-noitems', 'parse' ) );
+ $output->addHTML( wfMsgExt( 'watchlistedit-noitems', 'parse' ) );
}
return $count;
}
@@ -323,6 +323,8 @@ class WatchlistEditor {
),
__METHOD__
);
+ $article = new Article($title);
+ wfRunHooks('UnwatchArticleComplete',array(&$user,&$article));
}
}
}
@@ -340,21 +342,47 @@ class WatchlistEditor {
$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 .= "<fieldset>\n<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 .= $this->buildRemoveList( $user, $wgUser->getSkin() );
$form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-normal-submit' ) ) . '</p>';
$form .= '</fieldset></form>';
- $output->addHtml( $form );
+ $output->addHTML( $form );
+ }
+ }
+
+ /**
+ * Build the part of the standard watchlist editing form with the actual
+ * title selection checkboxes and stuff. Also generates a table of
+ * contents if there's more than one heading.
+ *
+ * @param $user User
+ * @param $skin Skin (really, Linker)
+ */
+ private function buildRemoveList( $user, $skin ) {
+ $list = "";
+ $toc = $skin->tocIndent();
+ $tocLength = 0;
+ foreach( $this->getWatchlistInfo( $user ) as $namespace => $pages ) {
+ $tocLength++;
+ $heading = htmlspecialchars( $this->getNamespaceHeading( $namespace ) );
+ $anchor = "editwatchlist-ns" . $namespace;
+
+ $list .= $skin->makeHeadLine( 2, ">", $anchor, $heading, "" );
+ $toc .= $skin->tocLine( $anchor, $heading, $tocLength, 1 ) . $skin->tocLineEnd();
+
+ $list .= "<ul>\n";
+ foreach( $pages as $dbkey => $redirect ) {
+ $title = Title::makeTitleSafe( $namespace, $dbkey );
+ $list .= $this->buildRemoveLine( $title, $redirect, $skin );
+ }
+ $list .= "</ul>\n";
+ }
+ // ISSUE: omit the TOC if the total number of titles is low?
+ if( $tocLength > 1 ) {
+ $list = $skin->tocList( $toc ) . $list;
}
+ return $list;
}
/**
@@ -389,9 +417,9 @@ class WatchlistEditor {
if( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
$tools[] = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $title->getText() ), wfMsgHtml( 'contributions' ) );
}
- return '<li>'
+ return "<li>"
. Xml::check( 'titles[]', false, array( 'value' => $title->getPrefixedText() ) )
- . $link . ' (' . implode( ' | ', $tools ) . ')' . '</li>';
+ . $link . " (" . implode( ' | ', $tools ) . ")" . "</li>\n";
}
/**
@@ -419,7 +447,7 @@ class WatchlistEditor {
$form .= '</textarea>';
$form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-raw-submit' ) ) . '</p>';
$form .= '</fieldset></form>';
- $output->addHtml( $form );
+ $output->addHTML( $form );
}
/**
diff --git a/includes/WebRequest.php b/includes/WebRequest.php
index 3fce5845..46747125 100644
--- a/includes/WebRequest.php
+++ b/includes/WebRequest.php
@@ -39,7 +39,8 @@ if ( !function_exists( '__autoload' ) ) {
* not create a second WebRequest object; make a FauxRequest object if
* you want to pass arbitrary data to some function in place of the web
* input.
- *
+ *
+ * @ingroup HTTP
*/
class WebRequest {
var $data = array();
@@ -54,7 +55,7 @@ class WebRequest {
// POST overrides GET data
// We don't use $_REQUEST here to avoid interference from cookies...
- $this->data = wfArrayMerge( $_GET, $_POST );
+ $this->data = $_POST + $_GET;
}
/**
@@ -255,6 +256,18 @@ class WebRequest {
return (string)$val;
}
}
+
+ /**
+ * Set an aribtrary value into our get/post data.
+ * @param $key string Key name to use
+ * @param $value mixed Value to set
+ * @return mixed old value if one was present, null otherwise
+ */
+ function setVal( $key, $value ) {
+ $ret = isset( $this->data[$key] ) ? $this->data[$key] : null;
+ $this->data[$key] = $value;
+ return $ret;
+ }
/**
* Fetch an array from the input or return $default if it's not set.
@@ -507,7 +520,7 @@ class WebRequest {
unset( $newquery['title'] );
$newquery = array_merge( $newquery, $array );
$query = wfArrayToCGI( $newquery );
- return $onlyquery ? $query : $wgTitle->getLocalURL( $basequery );
+ return $onlyquery ? $query : $wgTitle->getLocalURL( $query );
}
/**
@@ -636,11 +649,24 @@ class WebRequest {
}
}
}
+
+ /*
+ * Get data from $_SESSION
+ */
+ function getSessionData( $key ) {
+ if( !isset( $_SESSION[$key] ) )
+ return null;
+ return $_SESSION[$key];
+ }
+ function setSessionData( $key, $data ) {
+ $_SESSION[$key] = $data;
+ }
}
/**
* WebRequest clone which takes values from a provided array.
*
+ * @ingroup HTTP
*/
class FauxRequest extends WebRequest {
var $wasPosted = false;
@@ -650,7 +676,7 @@ class FauxRequest extends WebRequest {
* fake GET/POST values
* @param $wasPosted Bool: whether to treat the data as POST
*/
- function FauxRequest( $data, $wasPosted = false ) {
+ function FauxRequest( $data, $wasPosted = false, $session = null ) {
if( is_array( $data ) ) {
$this->data = $data;
} else {
@@ -658,6 +684,11 @@ class FauxRequest extends WebRequest {
}
$this->wasPosted = $wasPosted;
$this->headers = array();
+ $this->session = $session ? $session : array();
+ }
+
+ function notImplemented( $method ) {
+ throw new MWException( "{$method}() not implemented" );
}
function getText( $name, $default = '' ) {
@@ -678,15 +709,24 @@ class FauxRequest extends WebRequest {
}
function getRequestURL() {
- throw new MWException( 'FauxRequest::getRequestURL() not implemented' );
+ $this->notImplemented( __METHOD__ );
}
function appendQuery( $query ) {
- throw new MWException( 'FauxRequest::appendQuery() not implemented' );
+ $this->notImplemented( __METHOD__ );
}
function getHeader( $name ) {
return isset( $this->headers[$name] ) ? $this->headers[$name] : false;
}
+ function getSessionData( $key ) {
+ if( !isset( $this->session[$key] ) )
+ return null;
+ return $this->session[$key];
+ }
+ function setSessionData( $key, $data ) {
+ $this->notImplemented( __METHOD__ );
+ }
+
}
diff --git a/includes/WebResponse.php b/includes/WebResponse.php
index 05023e15..09d37385 100644
--- a/includes/WebResponse.php
+++ b/includes/WebResponse.php
@@ -2,17 +2,59 @@
/**
* Allow programs to request this object from WebRequest::response()
* and handle all outputting (or lack of outputting) via it.
+ * @ingroup HTTP
*/
class WebResponse {
- /** Output a HTTP header */
- function header($string, $replace=true) {
+ /**
+ * Output a HTTP header, wrapper for PHP's
+ * header()
+ * @param $string String: header to output
+ * @param $replace Bool: replace current similar header
+ */
+ public function header($string, $replace=true) {
header($string,$replace);
}
- /** Set the browser cookie */
- function setcookie($name, $value, $expire) {
- global $wgCookiePath, $wgCookieDomain, $wgCookieSecure;
- setcookie($name,$value,$expire, $wgCookiePath, $wgCookieDomain, $wgCookieSecure);
+ /** Set the browser cookie
+ * @param $name String: name of cookie
+ * @param $value String: value to give cookie
+ * @param $expire Int: number of seconds til cookie expires
+ */
+ public function setcookie( $name, $value, $expire = 0 ) {
+ global $wgCookiePath, $wgCookiePrefix, $wgCookieDomain;
+ global $wgCookieSecure,$wgCookieExpiration, $wgCookieHttpOnly;
+ if ( $expire == 0 ) {
+ $expire = time() + $wgCookieExpiration;
+ }
+ $httpOnlySafe = wfHttpOnlySafe();
+ wfDebugLog( 'cookie',
+ 'setcookie: "' . implode( '", "',
+ array(
+ $wgCookiePrefix . $name,
+ $value,
+ $expire,
+ $wgCookiePath,
+ $wgCookieDomain,
+ $wgCookieSecure,
+ $httpOnlySafe && $wgCookieHttpOnly ) ) . '"' );
+ if( $httpOnlySafe && isset( $wgCookieHttpOnly ) ) {
+ setcookie( $wgCookiePrefix . $name,
+ $value,
+ $expire,
+ $wgCookiePath,
+ $wgCookieDomain,
+ $wgCookieSecure,
+ $wgCookieHttpOnly );
+ } else {
+ // setcookie() fails on PHP 5.1 if you give it future-compat paramters.
+ // stab stab!
+ setcookie( $wgCookiePrefix . $name,
+ $value,
+ $expire,
+ $wgCookiePath,
+ $wgCookieDomain,
+ $wgCookieSecure );
+ }
}
}
diff --git a/includes/WebStart.php b/includes/WebStart.php
index 411c211c..edc58cb3 100644
--- a/includes/WebStart.php
+++ b/includes/WebStart.php
@@ -4,16 +4,6 @@
# starts the profiler and loads the configuration, and optionally loads
# Setup.php depending on whether MW_NO_SETUP is defined.
-# Test for PHP bug which breaks PHP 5.0.x on 64-bit...
-# As of 1.8 this breaks lots of common operations instead
-# of just some rare ones like export.
-$borked = str_replace( 'a', 'b', array( -1 => -1 ) );
-if( !isset( $borked[-1] ) ) {
- echo "PHP 5.0.x is buggy on your 64-bit system; you must upgrade to PHP 5.1.x\n" .
- "or higher. ABORTING. (http://bugs.php.net/bug.php?id=34879 for details)\n";
- die( -1 );
-}
-
# Protect against register_globals
# This must be done before any globals are set by the code
if ( ini_get( 'register_globals' ) ) {
@@ -74,6 +64,7 @@ if ( $IP === false ) {
$IP = realpath( '.' );
}
+
# Start profiler
require_once( "$IP/StartProfiler.php" );
wfProfileIn( 'WebStart.php-conf' );
@@ -81,20 +72,46 @@ wfProfileIn( 'WebStart.php-conf' );
# Load up some global defines.
require_once( "$IP/includes/Defines.php" );
-# LocalSettings.php is the per site customization file. If it does not exit
-# the wiki installer need to be launched or the generated file moved from
-# ./config/ to ./
-if( !file_exists( "$IP/LocalSettings.php" ) ) {
- require_once( "$IP/includes/DefaultSettings.php" ); # used for printing the version
- require_once( "$IP/includes/templates/NoLocalSettings.php" );
- die();
+# Check for PHP 5
+if ( !function_exists( 'version_compare' )
+ || version_compare( phpversion(), '5.0.0' ) < 0
+) {
+ define( 'MW_PHP4', '1' );
+ require( "$IP/includes/DefaultSettings.php" );
+ require( "$IP/includes/templates/PHP4.php" );
+ exit;
+}
+
+# Test for PHP bug which breaks PHP 5.0.x on 64-bit...
+# As of 1.8 this breaks lots of common operations instead
+# of just some rare ones like export.
+$borked = str_replace( 'a', 'b', array( -1 => -1 ) );
+if( !isset( $borked[-1] ) ) {
+ echo "PHP 5.0.x is buggy on your 64-bit system; you must upgrade to PHP 5.1.x\n" .
+ "or higher. ABORTING. (http://bugs.php.net/bug.php?id=34879 for details)\n";
+ exit;
}
# Start the autoloader, so that extensions can derive classes from core files
require_once( "$IP/includes/AutoLoader.php" );
-# Include site settings. $IP may be changed (hopefully before the AutoLoader is invoked)
-require_once( "$IP/LocalSettings.php" );
+if ( defined( 'MW_CONFIG_CALLBACK' ) ) {
+ # Use a callback function to configure MediaWiki
+ require_once( "$IP/includes/DefaultSettings.php" );
+ call_user_func( MW_CONFIG_CALLBACK );
+} else {
+ # LocalSettings.php is the per site customization file. If it does not exit
+ # the wiki installer need to be launched or the generated file moved from
+ # ./config/ to ./
+ if( !file_exists( "$IP/LocalSettings.php" ) ) {
+ require_once( "$IP/includes/DefaultSettings.php" ); # used for printing the version
+ require_once( "$IP/includes/templates/NoLocalSettings.php" );
+ die();
+ }
+
+ # Include site settings. $IP may be changed (hopefully before the AutoLoader is invoked)
+ require_once( "$IP/LocalSettings.php" );
+}
wfProfileOut( 'WebStart.php-conf' );
wfProfileIn( 'WebStart.php-ob_start' );
diff --git a/includes/Wiki.php b/includes/Wiki.php
index fa49290a..ce4ce67e 100644
--- a/includes/Wiki.php
+++ b/includes/Wiki.php
@@ -42,6 +42,7 @@ class MediaWiki {
/**
* Initialization of ... everything
* Performs the request too
+ * FIXME: why is this crap called "initialize" when it performs everything?
*
* @param $title Title ($wgTitle)
* @param $article Article
@@ -51,8 +52,11 @@ class MediaWiki {
*/
function initialize( &$title, &$article, &$output, &$user, $request ) {
wfProfileIn( __METHOD__ );
- $this->preliminaryChecks( $title, $output, $request ) ;
- if ( !$this->initializeSpecialCases( $title, $output, $request ) ) {
+ if( !$this->preliminaryChecks( $title, $output, $request ) ) {
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+ if( !$this->initializeSpecialCases( $title, $output, $request ) ) {
$new_article = $this->initializeArticle( $title, $request );
if( is_object( $new_article ) ) {
$article = $new_article;
@@ -60,6 +64,7 @@ class MediaWiki {
} elseif( is_string( $new_article ) ) {
$output->redirect( $new_article );
} else {
+ wfProfileOut( __METHOD__ );
throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle() returned neither an object nor a URL" );
}
}
@@ -76,7 +81,7 @@ class MediaWiki {
*/
function checkMaxLag( $maxLag ) {
list( $host, $lag ) = wfGetLB()->getMaxLag();
- if ( $lag > $maxLag ) {
+ if( $lag > $maxLag ) {
wfMaxlagError( $host, $lag, $maxLag );
return false;
} else {
@@ -84,7 +89,6 @@ class MediaWiki {
}
}
-
/**
* Checks some initial queries
* Note that $title here is *not* a Title object, but a string!
@@ -95,26 +99,23 @@ class MediaWiki {
*/
function checkInitialQueries( $title, $action ) {
global $wgOut, $wgRequest, $wgContLang;
- if( $wgRequest->getVal( 'printable' ) == 'yes' ){
+ if( $wgRequest->getVal( 'printable' ) === 'yes' ) {
$wgOut->setPrintable();
}
-
$ret = NULL;
-
- if ( '' == $title && 'delete' != $action ) {
- $ret = Title::newMainPage();
- } elseif ( $curid = $wgRequest->getInt( 'curid' ) ) {
+ if( $curid = $wgRequest->getInt( 'curid' ) ) {
# URLs like this are generated by RC, because rc_title isn't always accurate
$ret = Title::newFromID( $curid );
+ } elseif( '' == $title && 'delete' != $action ) {
+ $ret = Title::newMainPage();
} else {
$ret = Title::newFromURL( $title );
// check variant links so that interwiki links don't have to worry
// about the possible different language variants
if( count( $wgContLang->getVariants() ) > 1 && !is_null( $ret ) && $ret->getArticleID() == 0 )
$wgContLang->findVariantLink( $title, $ret );
-
}
- if ( ( $oldid = $wgRequest->getInt( 'oldid' ) )
+ if( ( $oldid = $wgRequest->getInt( 'oldid' ) )
&& ( is_null( $ret ) || $ret->getNamespace() != NS_SPECIAL ) ) {
// Allow oldid to override a changed or missing title.
$rev = Revision::newFromId( $oldid );
@@ -133,7 +134,6 @@ class MediaWiki {
* @param $request WebRequest
*/
function preliminaryChecks( &$title, &$output, $request ) {
-
if( $request->getCheck( 'search' ) ) {
// Compatibility with old search URLs which didn't use Special:Search
// Just check for presence here, so blank requests still
@@ -142,16 +142,16 @@ class MediaWiki {
// Do this above the read whitelist check for security...
$title = SpecialPage::getTitleFor( '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
# catch special pages etc. We check again in Article::view())
- if ( !is_null( $title ) && !$title->userCanRead() ) {
+ if( !is_null( $title ) && !$title->userCanRead() ) {
$output->loginToUse();
$output->output();
- exit;
+ $output->disable();
+ return false;
}
-
+ return true;
}
/**
@@ -161,6 +161,8 @@ class MediaWiki {
* - redirect loop
* - special pages
*
+ * FIXME: why is this crap called "initialize" when it performs everything?
+ *
* @param $title Title
* @param $output OutputPage
* @param $request WebRequest
@@ -170,25 +172,25 @@ class MediaWiki {
wfProfileIn( __METHOD__ );
$action = $this->getVal( 'Action' );
- if( !$title || $title->getDBkey() == '' ) {
+ if( is_null($title) || $title->getDBkey() == '' ) {
$title = SpecialPage::getTitleFor( 'Badtitle' );
# Die now before we mess up $wgArticle and the skin stops working
throw new ErrorPageError( 'badtitle', 'badtitletext' );
- } else if ( $title->getInterwiki() != '' ) {
+ } else if( $title->getInterwiki() != '' ) {
if( $rdfrom = $request->getVal( 'rdfrom' ) ) {
$url = $title->getFullURL( 'rdfrom=' . urlencode( $rdfrom ) );
} else {
$url = $title->getFullURL();
}
/* Check for a redirect loop */
- if ( !preg_match( '/^' . preg_quote( $this->getVal('Server'), '/' ) . '/', $url ) && $title->isLocal() ) {
+ if( !preg_match( '/^' . preg_quote( $this->getVal('Server'), '/' ) . '/', $url ) && $title->isLocal() ) {
$output->redirect( $url );
} else {
$title = SpecialPage::getTitleFor( 'Badtitle' );
throw new ErrorPageError( 'badtitle', 'badtitletext' );
}
- } else if ( ( $action == 'view' ) && !$request->wasPosted() &&
- (!isset( $this->GET['title'] ) || $title->getPrefixedDBKey() != $this->GET['title'] ) &&
+ } else if( $action == 'view' && !$request->wasPosted() &&
+ ( !isset($this->GET['title']) || $title->getPrefixedDBKey() != $this->GET['title'] ) &&
!count( array_diff( array_keys( $this->GET ), array( 'action', 'title' ) ) ) )
{
$targetUrl = $title->getFullURL();
@@ -219,7 +221,7 @@ class MediaWiki {
$output->setSquidMaxage( 1200 );
$output->redirect( $targetUrl, '301' );
}
- } else if ( NS_SPECIAL == $title->getNamespace() ) {
+ } else if( NS_SPECIAL == $title->getNamespace() ) {
/* actions that need to be made when we have a special pages */
SpecialPage::executePath( $title );
} else {
@@ -241,7 +243,7 @@ class MediaWiki {
static function articleFromTitle( &$title ) {
if( NS_MEDIA == $title->getNamespace() ) {
// FIXME: where should this go?
- $title = Title::makeTitle( NS_IMAGE, $title->getDBkey() );
+ $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
}
$article = null;
@@ -251,12 +253,12 @@ class MediaWiki {
}
switch( $title->getNamespace() ) {
- case NS_IMAGE:
- return new ImagePage( $title );
- case NS_CATEGORY:
- return new CategoryPage( $title );
- default:
- return new Article( $title );
+ case NS_FILE:
+ return new ImagePage( $title );
+ case NS_CATEGORY:
+ return new CategoryPage( $title );
+ default:
+ return new Article( $title );
}
}
@@ -271,27 +273,32 @@ class MediaWiki {
function initializeArticle( &$title, $request ) {
wfProfileIn( __METHOD__ );
- $action = $this->getVal( 'action' );
+ $action = $this->getVal( 'action', 'view' );
$article = self::articleFromTitle( $title );
-
- wfDebug("Article: ".$title->getPrefixedText()."\n");
-
+ # NS_MEDIAWIKI has no redirects.
+ # It is also used for CSS/JS, so performance matters here...
+ if( $title->getNamespace() == NS_MEDIAWIKI ) {
+ wfProfileOut( __METHOD__ );
+ return $article;
+ }
// Namespace might change when using redirects
// Check for redirects ...
- $file = $title->getNamespace() == NS_IMAGE ? $article->getFile() : null;
+ $file = ($title->getNamespace() == NS_FILE) ? $article->getFile() : null;
if( ( $action == 'view' || $action == 'render' ) // ... for actions that show content
- && !$request->getVal( 'oldid' ) && // ... and are not old revisions
- $request->getVal( 'redirect' ) != 'no' && // ... unless explicitly told not to
- // ... and the article is not a non-redirect image page with associated file
- !( is_object( $file ) && $file->exists() && !$file->getRedirected() ) ) {
-
+ && !$request->getVal( 'oldid' ) && // ... and are not old revisions
+ $request->getVal( 'redirect' ) != 'no' && // ... unless explicitly told not to
+ // ... and the article is not a non-redirect image page with associated file
+ !( is_object( $file ) && $file->exists() && !$file->getRedirected() ) )
+ {
# Give extensions a change to ignore/handle redirects as needed
$ignoreRedirect = $target = false;
- wfRunHooks( 'InitializeArticleMaybeRedirect', array( &$title, &$request, &$ignoreRedirect, &$target ) );
-
+
$dbr = wfGetDB( DB_SLAVE );
$article->loadPageData( $article->pageDataFromTitle( $dbr, $title ) );
+ wfRunHooks( 'InitializeArticleMaybeRedirect',
+ array(&$title,&$request,&$ignoreRedirect,&$target,&$article) );
+
// Follow redirects only for... redirects
if( !$ignoreRedirect && $article->isRedirect() ) {
# Is the target already set by an extension?
@@ -302,12 +309,11 @@ class MediaWiki {
return $target;
}
}
-
- if( is_object( $target ) ) {
+ if( is_object($target) ) {
// Rewrite environment to redirected article
$rarticle = self::articleFromTitle( $target );
$rarticle->loadPageData( $rarticle->pageDataFromTitle( $dbr, $target ) );
- if ( $rarticle->exists() || ( is_object( $file ) && !$file->isLocal() ) ) {
+ if( $rarticle->exists() || ( is_object( $file ) && !$file->isLocal() ) ) {
$rarticle->setRedirectedFrom( $title );
$article = $rarticle;
$title = $target;
@@ -327,14 +333,18 @@ class MediaWiki {
* @param $deferredUpdates array of updates to do
* @param $output OutputPage
*/
- function finalCleanup ( &$deferredUpdates, &$output ) {
+ function finalCleanup( &$deferredUpdates, &$output ) {
wfProfileIn( __METHOD__ );
- $this->doUpdates( $deferredUpdates );
- $this->doJobs();
# Now commit any transactions, so that unreported errors after output() don't roll back the whole thing
$factory = wfGetLBFactory();
- $factory->shutdown();
+ $factory->commitMasterChanges();
+ # Output everything!
$output->output();
+ # Do any deferred jobs
+ $this->doUpdates( $deferredUpdates );
+ $this->doJobs();
+ # Commit and close up!
+ $factory->shutdown();
wfProfileOut( __METHOD__ );
}
@@ -359,7 +369,7 @@ class MediaWiki {
$up->doUpdate();
# Commit after every update to prevent lock contention
- if ( $dbw->trxLevel() ) {
+ if( $dbw->trxLevel() ) {
$dbw->commit();
}
}
@@ -372,12 +382,12 @@ class MediaWiki {
function doJobs() {
$jobRunRate = $this->getVal( 'JobRunRate' );
- if ( $jobRunRate <= 0 || wfReadOnly() ) {
+ if( $jobRunRate <= 0 || wfReadOnly() ) {
return;
}
- if ( $jobRunRate < 1 ) {
+ if( $jobRunRate < 1 ) {
$max = mt_getrandmax();
- if ( mt_rand( 0, $max ) > $max * $jobRunRate ) {
+ if( mt_rand( 0, $max ) > $max * $jobRunRate ) {
return;
}
$n = 1;
@@ -391,7 +401,7 @@ class MediaWiki {
$success = $job->run();
$t += wfTime();
$t = round( $t*1000 );
- if ( !$success ) {
+ if( !$success ) {
$output .= "Error: " . $job->getLastError() . ", Time: $t ms\n";
} else {
$output .= "Success, Time: $t ms\n";
@@ -420,7 +430,7 @@ class MediaWiki {
function performAction( &$output, &$article, &$title, &$user, &$request ) {
wfProfileIn( __METHOD__ );
- if ( !wfRunHooks( 'MediaWikiPerformAction', array( $output, $article, $title, $user, $request, $this ) ) ) {
+ if( !wfRunHooks( 'MediaWikiPerformAction', array( $output, $article, $title, $user, $request, $this ) ) ) {
wfProfileOut( __METHOD__ );
return;
}
@@ -436,6 +446,10 @@ class MediaWiki {
$output->setSquidMaxage( $this->getVal( 'SquidMaxage' ) );
$article->view();
break;
+ case 'raw': // includes JS/CSS
+ $raw = new RawPage( $article );
+ $raw->view();
+ break;
case 'watch':
case 'unwatch':
case 'delete':
@@ -457,21 +471,20 @@ class MediaWiki {
if( !$this->getVal( 'EnableDublinCoreRdf' ) ) {
wfHttpError( 403, 'Forbidden', wfMsg( 'nodublincore' ) );
} else {
- require_once( 'includes/Metadata.php' );
- wfDublinCoreRdf( $article );
+ $rdf = new DublinCoreRdf( $article );
+ $rdf->show();
}
break;
case 'creativecommons':
if( !$this->getVal( 'EnableCreativeCommonsRdf' ) ) {
wfHttpError( 403, 'Forbidden', wfMsg( 'nocreativecommons' ) );
} else {
- require_once( 'includes/Metadata.php' );
- wfCreativeCommonsRdf( $article );
+ $rdf = new CreativeCommonsRdf( $article );
+ $rdf->show();
}
break;
case 'credits':
- require_once( 'includes/Credits.php' );
- showCreditsPage( $article );
+ Credits::showPage( $article );
break;
case 'submit':
if( session_id() == '' ) {
@@ -504,10 +517,6 @@ class MediaWiki {
$history = new PageHistory( $article );
$history->history();
break;
- case 'raw':
- $raw = new RawPage( $article );
- $raw->view();
- break;
default:
if( wfRunHooks( 'UnknownAction', array( $action, $article ) ) ) {
$output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
diff --git a/includes/WikiError.php b/includes/WikiError.php
index c5082004..41edb2f3 100644
--- a/includes/WikiError.php
+++ b/includes/WikiError.php
@@ -79,7 +79,8 @@ class WikiErrorMsg extends WikiError {
}
/**
- * @todo document
+ * Error class designed to handle errors involved with
+ * XML parsing
* @ingroup Exception
*/
class WikiXmlError extends WikiError {
diff --git a/includes/Xml.php b/includes/Xml.php
index 32a68251..68990d86 100644
--- a/includes/Xml.php
+++ b/includes/Xml.php
@@ -112,11 +112,11 @@ class Xml {
*
* @param $selected Mixed: Namespace which should be pre-selected
* @param $all Mixed: Value of an item denoting all namespaces, or null to omit
- * @param $hidden Mixed: Include hidden namespaces? [WTF? --RC]
* @param $element_name String: value of the "name" attribute of the select tag
+ * @param $label String: optional label to add to the field
* @return string
*/
- public static function namespaceSelector( $selected = '', $all = null, $hidden = false, $element_name = 'namespace' ) {
+ public static function namespaceSelector( $selected = '', $all = null, $element_name = 'namespace', $label = null ) {
global $wgContLang;
$namespaces = $wgContLang->getFormattedNamespaces();
$options = array();
@@ -139,12 +139,16 @@ class Xml {
$options[] = self::option( $name, $index, $index === $selected );
}
- return Xml::openElement( 'select', array( 'id' => 'namespace', 'name' => $element_name,
+ $ret = Xml::openElement( 'select', array( 'id' => 'namespace', 'name' => $element_name,
'class' => 'namespaceselector' ) )
. "\n"
. implode( "\n", $options )
. "\n"
. Xml::closeElement( 'select' );
+ if ( !is_null( $label ) ) {
+ $ret = Xml::label( $label, $element_name ) . '&nbsp;' . $ret;
+ }
+ return $ret;
}
/**
@@ -640,18 +644,63 @@ class Xml {
$form .= Xml::openElement( 'tr', array( 'id' => $id ) );
$form .= Xml::tags( 'td', array('class' => 'mw-label'), wfMsgExt( $labelmsg, array('parseinline') ) );
- $form .= Xml::openElement( 'td' ) . $input . Xml::closeElement( 'td' );
+ $form .= Xml::openElement( 'td', array( 'class' => 'mw-input' ) ) . $input . Xml::closeElement( 'td' );
+ $form .= Xml::closeElement( 'tr' );
+ }
+
+ if( $submitLabel ) {
+ $form .= Xml::openElement( 'tr', array( 'id' => $id ) );
+ $form .= Xml::tags( 'td', array(), '' );
+ $form .= Xml::openElement( 'td', array( 'class' => 'mw-submit' ) ) . Xml::submitButton( wfMsg( $submitLabel ) ) . Xml::closeElement( 'td' );
$form .= Xml::closeElement( 'tr' );
}
$form .= "</tbody></table>";
-
- if ($submitLabel) {
- $form .= Xml::submitButton( wfMsg($submitLabel) );
- }
+
return $form;
}
+
+ /**
+ * Build a table of data
+ * @param array $rows An array of arrays of strings, each to be a row in a table
+ * @param array $attribs Attributes to apply to the table tag [optional]
+ * @param array $headers An array of strings to use as table headers [optional]
+ * @return string
+ */
+ public static function buildTable( $rows, $attribs = array(), $headers = null ) {
+ $s = Xml::openElement( 'table', $attribs );
+ if ( is_array( $headers ) ) {
+ foreach( $headers as $id => $header ) {
+ $attribs = array();
+ if ( is_string( $id ) ) $attribs['id'] = $id;
+ $s .= Xml::element( 'th', $attribs, $header );
+ }
+ }
+ foreach( $rows as $id => $row ) {
+ $attribs = array();
+ if ( is_string( $id ) ) $attribs['id'] = $id;
+ $s .= Xml::buildTableRow( $attribs, $row );
+ }
+ $s .= Xml::closeElement( 'table' );
+ return $s;
+ }
+
+ /**
+ * Build a row for a table
+ * @param array $cells An array of strings to put in <td>
+ * @return string
+ */
+ public static function buildTableRow( $attribs, $cells ) {
+ $s = Xml::openElement( 'tr', $attribs );
+ foreach( $cells as $id => $cell ) {
+ $attribs = array();
+ if ( is_string( $id ) ) $attribs['id'] = $id;
+ $s .= Xml::element( 'td', $attribs, $cell );
+ }
+ $s .= Xml::closeElement( 'tr' );
+ return $s;
+ }
}
class XmlSelect {
@@ -674,7 +723,8 @@ class XmlSelect {
}
public function addOption( $name, $value = false ) {
- $value = $value ? $value : $name;
+ // Stab stab stab
+ $value = ($value !== false) ? $value : $name;
$this->options[] = Xml::option( $name, $value, $value === $this->default );
}
@@ -682,4 +732,4 @@ class XmlSelect {
return Xml::tags( 'select', $this->attributes, implode( "\n", $this->options ) );
}
-} \ No newline at end of file
+}
diff --git a/includes/XmlFunctions.php b/includes/XmlFunctions.php
index bc18a2cd..8cb8f3f5 100644
--- a/includes/XmlFunctions.php
+++ b/includes/XmlFunctions.php
@@ -4,63 +4,83 @@
* Look at the Xml class (Xml.php) for the implementations.
*/
function wfElement( $element, $attribs = null, $contents = '') {
+ wfDeprecated(__FUNCTION__);
return Xml::element( $element, $attribs, $contents );
}
function wfElementClean( $element, $attribs = array(), $contents = '') {
+ wfDeprecated(__FUNCTION__);
return Xml::elementClean( $element, $attribs, $contents );
}
function wfOpenElement( $element, $attribs = null ) {
+ wfDeprecated(__FUNCTION__);
return Xml::openElement( $element, $attribs );
}
function wfCloseElement( $element ) {
+ wfDeprecated(__FUNCTION__);
return "</$element>";
}
-function HTMLnamespaceselector($selected = '', $allnamespaces = null, $includehidden=false) {
- return Xml::namespaceSelector( $selected, $allnamespaces, $includehidden );
+function HTMLnamespaceselector($selected = '', $allnamespaces = null ) {
+ wfDeprecated(__FUNCTION__);
+ return Xml::namespaceSelector( $selected, $allnamespaces );
}
function wfSpan( $text, $class, $attribs=array() ) {
+ wfDeprecated(__FUNCTION__);
return Xml::span( $text, $class, $attribs );
}
function wfInput( $name, $size=false, $value=false, $attribs=array() ) {
+ wfDeprecated(__FUNCTION__);
return Xml::input( $name, $size, $value, $attribs );
}
function wfAttrib( $name, $present = true ) {
+ wfDeprecated(__FUNCTION__);
return Xml::attrib( $name, $present );
}
function wfCheck( $name, $checked=false, $attribs=array() ) {
+ wfDeprecated(__FUNCTION__);
return Xml::check( $name, $checked, $attribs );
}
function wfRadio( $name, $value, $checked=false, $attribs=array() ) {
+ wfDeprecated(__FUNCTION__);
return Xml::radio( $name, $value, $checked, $attribs );
}
function wfLabel( $label, $id ) {
+ wfDeprecated(__FUNCTION__);
return Xml::label( $label, $id );
}
function wfInputLabel( $label, $name, $id, $size=false, $value=false, $attribs=array() ) {
+ wfDeprecated(__FUNCTION__);
return Xml::inputLabel( $label, $name, $id, $size, $value, $attribs );
}
function wfCheckLabel( $label, $name, $id, $checked=false, $attribs=array() ) {
+ wfDeprecated(__FUNCTION__);
return Xml::checkLabel( $label, $name, $id, $checked, $attribs );
}
function wfRadioLabel( $label, $name, $value, $id, $checked=false, $attribs=array() ) {
+ wfDeprecated(__FUNCTION__);
return Xml::radioLabel( $label, $name, $value, $id, $checked, $attribs );
}
function wfSubmitButton( $value, $attribs=array() ) {
+ wfDeprecated(__FUNCTION__);
return Xml::submitButton( $value, $attribs );
}
function wfHidden( $name, $value, $attribs=array() ) {
+ wfDeprecated(__FUNCTION__);
return Xml::hidden( $name, $value, $attribs );
}
function wfEscapeJsString( $string ) {
+ wfDeprecated(__FUNCTION__);
return Xml::escapeJsString( $string );
}
function wfIsWellFormedXml( $text ) {
+ wfDeprecated(__FUNCTION__);
return Xml::isWellFormed( $text );
}
function wfIsWellFormedXmlFragment( $text ) {
+ wfDeprecated(__FUNCTION__);
return Xml::isWellFormedXmlFragment( $text );
}
function wfBuildForm( $fields, $submitLabel ) {
+ wfDeprecated(__FUNCTION__);
return Xml::buildForm( $fields, $submitLabel );
}
diff --git a/includes/XmlTypeCheck.php b/includes/XmlTypeCheck.php
index 8ee211e1..a004ef4d 100644
--- a/includes/XmlTypeCheck.php
+++ b/includes/XmlTypeCheck.php
@@ -31,6 +31,13 @@ class XmlTypeCheck {
$this->filterCallback = $filterCallback;
$this->run( $file );
}
+
+ /**
+ * Get the root element. Simple accessor to $rootElement
+ */
+ public function getRootElement() {
+ return $this->rootElement;
+ }
private function run( $fname ) {
$parser = xml_parser_create_ns( 'UTF-8' );
diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php
index 1cae8463..4c1e0ae8 100644
--- a/includes/ZhConversion.php
+++ b/includes/ZhConversion.php
@@ -2574,6 +2574,10 @@ $zh2Hant = array(
"三只" => "三隻",
"三余" => "三餘",
"上梁" => "上樑",
+"上签名" => "上簽名",
+"上签字" => "上簽字",
+"上签写" => "上簽寫",
+"上签收" => "上簽收",
"上签" => "上籤",
"上药" => "上藥",
"下于" => "下於",
@@ -3482,6 +3486,7 @@ $zh2Hant = array(
"外强中干" => "外強中乾",
"外制" => "外製",
"多划" => "多劃",
+"多只是" => "多只是",
"多天后" => "多天後",
"多于" => "多於",
"多冲" => "多衝",
@@ -4776,7 +4781,6 @@ $zh2Hant = array(
"水准" => "水準",
"水里" => "水裡",
"水里乡" => "水里鄉",
-"水表" => "水錶",
"水硷" => "水鹼",
"永历" => "永曆",
"求助于" => "求助於",
@@ -6228,7 +6232,6 @@ $zh2Hant = array(
"退烧药" => "退燒藥",
"逋发" => "逋髮",
"透辟" => "透闢",
-"这么着" => "這么著",
"这里" => "這裏",
"这里" => "這裡",
"这只" => "這隻",
@@ -6922,7 +6925,6 @@ $zh2Hant = array(
"斗斗" => "鬥鬥",
"斗鱼" => "鬥魚",
"斗鹌鹑" => "鬥鵪鶉",
-"闹着玩儿" => "鬧著玩儿",
"闹着玩儿" => "鬧著玩兒",
"闹钟" => "鬧鐘",
"哄动" => "鬨動",
@@ -9910,22 +9912,22 @@ $zh2Hans = array(
"乘著" => "乘着",
"書畫" => "书画",
"乾乾" => "乾乾",
-"乾元;" => "乾元;",
-"乾卦;" => "乾卦;",
+"乾元" => "乾元",
+"乾卦" => "乾卦",
"乾縣" => "乾县",
"乾嘉" => "乾嘉",
"乾圖" => "乾图",
"乾坤 " => "乾坤 ",
-"乾宅;" => "乾宅;",
+"乾宅" => "乾宅",
"乾斷" => "乾断",
"乾旦" => "乾旦",
-"乾曜;" => "乾曜;",
+"乾曜" => "乾曜",
"乾清宮" => "乾清宫",
"乾盛世" => "乾盛世",
"乾紅" => "乾红",
"乾綱" => "乾纲",
-"乾象;" => "乾象;",
-"乾造;" => "乾造;",
+"乾象" => "乾象",
+"乾造" => "乾造",
"乾陵" => "乾陵",
"乾隆" => "乾隆",
"爭著" => "争着",
@@ -10220,8 +10222,12 @@ $zh2Hans = array(
"本著名" => "本著名",
"本著者" => "本著者",
"機械畫" => "机械画",
+"殺著" => "杀着",
+"殺著作" => "杀著作",
+"殺著名" => "杀著名",
+"殺著者" => "杀著者",
"雜著" => "杂着",
-"李乾德;" => "李乾德;",
+"李乾德" => "李乾德",
"來著" => "来着",
"板著臉" => "板着脸",
"枕著" => "枕着",
@@ -10631,6 +10637,7 @@ $zh2TW = array(
"復蘇" => "復甦",
"缺省" => "預設",
"串行" => "串列",
+"串列加速器" => "串列加速器",
"以太网" => "乙太網",
"位图" => "點陣圖",
"例程" => "常式",
@@ -10695,7 +10702,6 @@ $zh2TW = array(
"服务器" => "伺服器",
"等于" => "等於",
"局域网" => "區域網",
-"计算机" => "電腦",
"扫瞄仪" => "掃瞄器",
"宽带" => "寬頻",
"数据库" => "資料庫",
@@ -10753,7 +10759,6 @@ $zh2TW = array(
"伯利兹" => "貝里斯",
"伯利茲" => "貝里斯",
"佛得角" => "維德角",
-"佛得角" => "維德角",
"克罗地亚" => "克羅埃西亞",
"克羅地亞" => "克羅埃西亞",
"冈比亚" => "甘比亞",
@@ -10811,10 +10816,11 @@ $zh2TW = array(
"塞浦路斯" => "塞普勒斯",
"塞舌尔" => "塞席爾",
"塞舌爾" => "塞席爾",
-"多米尼加" => "多明尼加",
+"多米尼加共和国" => "多明尼加",
+"多米尼加共和國" => "多明尼加",
"多明尼加共和國" => "多明尼加",
-"多米尼加联邦" => "多米尼克",
-"多明尼加聯邦" => "多米尼克",
+"多米尼加国" => "多米尼克",
+"多明尼加國" => "多米尼克",
"安提瓜和巴布达" => "安地卡及巴布達",
"安提瓜和巴布達" => "安地卡及巴布達",
"尼日利亚" => "奈及利亞",
@@ -10822,17 +10828,14 @@ $zh2TW = array(
"尼日尔" => "尼日",
"尼日爾" => "尼日",
"巴巴多斯" => "巴貝多",
-"巴巴多斯" => "巴貝多",
"巴布亚新几内亚" => "巴布亞紐幾內亞",
"巴布亞新畿內亞" => "巴布亞紐幾內亞",
"布基纳法索" => "布吉納法索",
"布基納法索" => "布吉納法索",
"布隆迪" => "蒲隆地",
"布隆迪" => "蒲隆地",
-"希腊" => "希臘",
"帕劳" => "帛琉",
"意大利" => "義大利",
-"意大利" => "義大利",
"所罗门群岛" => "索羅門群島",
"所羅門群島" => "索羅門群島",
"文莱" => "汶萊",
@@ -10886,7 +10889,6 @@ $zh2TW = array(
"赞比亚" => "尚比亞",
"贊比亞" => "尚比亞",
"阿塞拜疆" => "亞塞拜然",
-"阿塞拜疆" => "亞塞拜然",
"阿拉伯联合酋长国" => "阿拉伯聯合大公國",
"阿拉伯聯合酋長國" => "阿拉伯聯合大公國",
"马尔代夫" => "馬爾地夫",
@@ -10917,7 +10919,6 @@ $zh2TW = array(
"積架" => "捷豹",
"福士" => "福斯",
"雪铁龙" => "雪鐵龍",
-"马自达" => "馬自達",
"萬事得" => "馬自達",
"拿破仑" => "拿破崙",
"拿破侖" => "拿破崙",
@@ -10941,25 +10942,17 @@ $zh2HK = array(
"凶殘" => "兇殘",
"緝凶" => "緝兇",
"買凶" => "買兇",
-"打印机" => "打印機",
"印表機" => "打印機",
"字节" => "位元組",
"字節" => "位元組",
-"打印" => "打印",
"列印" => "打印",
"硬件" => "硬件",
"硬體" => "硬件",
-"二极管" => "二極管",
"二極體" => "二極管",
-"三极管" => "三極管",
"三極體" => "三極管",
-"数码" => "數碼",
"數位" => "數碼",
-"软件" => "軟件",
"軟體" => "軟件",
-"网络" => "網絡",
"網路" => "網絡",
-"人工智能" => "人工智能",
"人工智慧" => "人工智能",
"航天飞机" => "穿梭機",
"太空梭" => "穿梭機",
@@ -10969,141 +10962,85 @@ $zh2HK = array(
"機器人" => "機械人",
"移动电话" => "流動電話",
"行動電話" => "流動電話",
-"调制解调器" => "調制解調器",
"數據機" => "調制解調器",
"短信" => "短訊",
"簡訊" => "短訊",
-"乍得" => "乍得",
"查德" => "乍得",
-"也门" => "也門",
"葉門" => "也門",
-"伯利兹" => "伯利茲",
"貝里斯" => "伯利茲",
-"佛得角" => "佛得角",
"維德角" => "佛得角",
-"克罗地亚" => "克羅地亞",
"克羅埃西亞" => "克羅地亞",
-"冈比亚" => "岡比亞",
"甘比亞" => "岡比亞",
-"几内亚比绍" => "幾內亞比紹",
"幾內亞比索" => "幾內亞比紹",
-"列支敦士登" => "列支敦士登",
"列支敦斯登" => "列支敦士登",
-"利比里亚" => "利比里亞",
"賴比瑞亞" => "利比里亞",
-"加纳" => "加納",
"迦納" => "加納",
-"加蓬" => "加蓬",
"加彭" => "加蓬",
-"博茨瓦纳" => "博茨瓦納",
"波札那" => "博茨瓦納",
-"卡塔尔" => "卡塔爾",
"卡達" => "卡塔爾",
-"卢旺达" => "盧旺達",
"盧安達" => "盧旺達",
-"危地马拉" => "危地馬拉",
"瓜地馬拉" => "危地馬拉",
"厄瓜多尔" => "厄瓜多爾",
+"厄瓜多爾" => "厄瓜多爾",
"厄瓜多" => "厄瓜多爾",
-"厄立特里亚" => "厄立特里亞",
"厄利垂亞" => "厄立特里亞",
-"吉布提" => "吉布堤",
"吉布地" => "吉布堤",
-"哥斯达黎加" => "哥斯達黎加",
"哥斯大黎加" => "哥斯達黎加",
-"图瓦卢" => "圖瓦盧",
"吐瓦魯" => "圖瓦盧",
-"圣卢西亚" => "聖盧西亞",
"聖露西亞" => "聖盧西亞",
"圣基茨和尼维斯" => "聖吉斯納域斯",
"聖克里斯多福及尼維斯" => "聖吉斯納域斯",
-"圣文森特和格林纳丁斯" => "聖文森特和格林納丁斯",
"聖文森及格瑞那丁" => "聖文森特和格林納丁斯",
-"圣马力诺" => "聖馬力諾",
"聖馬利諾" => "聖馬力諾",
-"圭亚那" => "圭亞那",
"蓋亞那" => "圭亞那",
-"坦桑尼亚" => "坦桑尼亞",
"坦尚尼亞" => "坦桑尼亞",
-"埃塞俄比亚" => "埃塞俄比亞",
"衣索匹亞" => "埃塞俄比亞",
"衣索比亞" => "埃塞俄比亞",
-"基里巴斯" => "基里巴斯",
"吉里巴斯" => "基里巴斯",
-"狮子山" => "獅子山",
"塞普勒斯" => "塞浦路斯",
-"塞舌尔" => "塞舌爾",
"塞席爾" => "塞舌爾",
-"多米尼加" => "多明尼加共和國",
-"多明尼加" => "多明尼加共和國",
-"多米尼加联邦" => "多明尼加聯邦",
-"多米尼克" => "多明尼加聯邦",
-"安提瓜和巴布达" => "安提瓜和巴布達",
+"多米尼克" => "多明尼加國",
"安地卡及巴布達" => "安提瓜和巴布達",
"尼日利亚" => "尼日利亞",
+"尼日利亞" => "尼日利亞",
"奈及利亞" => "尼日利亞",
"尼日尔" => "尼日爾",
+"尼日爾" => "尼日爾",
"尼日" => "尼日爾",
-"巴巴多斯" => "巴巴多斯",
"巴貝多" => "巴巴多斯",
-"巴布亚新几内亚" => "巴布亞新畿內亞",
"巴布亞紐幾內亞" => "巴布亞新畿內亞",
-"布基纳法索" => "布基納法索",
"布吉納法索" => "布基納法索",
-"布隆迪" => "布隆迪",
"蒲隆地" => "布隆迪",
+"帕劳" => "帛琉",
"義大利" => "意大利",
-"所罗门群岛" => "所羅門群島",
"索羅門群島" => "所羅門群島",
-"斯威士兰" => "斯威士蘭",
+"文莱" => "汶萊",
"史瓦濟蘭" => "斯威士蘭",
-"斯洛文尼亚" => "斯洛文尼亞",
"斯洛維尼亞" => "斯洛文尼亞",
-"新西兰" => "新西蘭",
"紐西蘭" => "新西蘭",
-"格林纳达" => "格林納達",
"格瑞那達" => "格林納達",
-"格鲁吉亚" => "喬治亞",
-"格魯吉亞" => "喬治亞",
-"梵蒂冈" => "梵蒂岡",
-"毛里塔尼亚" => "毛里塔尼亞",
"茅利塔尼亞" => "毛里塔尼亞",
"毛里求斯" => "毛里裘斯",
"模里西斯" => "毛里裘斯",
+"沙地阿拉伯" => "沙特阿拉伯",
"沙烏地阿拉伯" => "沙特阿拉伯",
-"波斯尼亚和黑塞哥维那" => "波斯尼亞黑塞哥維那",
"波士尼亞赫塞哥維納" => "波斯尼亞黑塞哥維那",
-"津巴布韦" => "津巴布韋",
"辛巴威" => "津巴布韋",
-"洪都拉斯" => "洪都拉斯",
"宏都拉斯" => "洪都拉斯",
-"特立尼达和托巴哥" => "特立尼達和多巴哥",
"千里達托貝哥" => "特立尼達和多巴哥",
-"瑙鲁" => "瑙魯",
"諾魯" => "瑙魯",
-"瓦努阿图" => "瓦努阿圖",
"萬那杜" => "瓦努阿圖",
-"科摩罗" => "科摩羅",
"葛摩" => "科摩羅",
-"索马里" => "索馬里",
"索馬利亞" => "索馬里",
-"老挝" => "老撾",
"寮國" => "老撾",
"肯尼亚" => "肯雅",
"肯亞" => "肯雅",
-"莫桑比克" => "莫桑比克",
"莫三比克" => "莫桑比克",
-"莱索托" => "萊索托",
"賴索托" => "萊索托",
-"贝宁" => "貝寧",
"貝南" => "貝寧",
-"赞比亚" => "贊比亞",
"尚比亞" => "贊比亞",
-"阿塞拜疆" => "阿塞拜疆",
"亞塞拜然" => "阿塞拜疆",
-"阿拉伯联合酋长国" => "阿拉伯聯合酋長國",
"阿拉伯聯合大公國" => "阿拉伯聯合酋長國",
-"马尔代夫" => "馬爾代夫",
"馬爾地夫" => "馬爾代夫",
"馬利共和國" => "馬里共和國",
"方便面" => "即食麵",
@@ -11135,11 +11072,9 @@ $zh2HK = array(
"拿破崙" => "拿破侖",
"布什" => "布殊",
"布希" => "布殊",
-"克林顿" => "克林頓",
"柯林頓" => "克林頓",
"萨达姆" => "薩達姆",
"海珊" => "侯賽因",
-"侯赛因" => "侯賽因",
"大卫·贝克汉姆" => "大衛碧咸",
"迈克尔·欧文" => "米高奧雲",
"珍妮弗·卡普里亚蒂" => "卡佩雅蒂",
@@ -11158,6 +11093,7 @@ $zh2CN = array(
"記憶體" => "内存",
"預設" => "默认",
"串列" => "串行",
+"串列加速器" => "串列加速器",
"乙太網" => "以太网",
"點陣圖" => "位图",
"常式" => "例程",
@@ -11262,150 +11198,92 @@ $zh2CN = array(
"簡訊" => "短信",
"烏茲別克" => "乌兹别克斯坦",
"查德" => "乍得",
-"乍得" => "乍得",
-"也門" => "",
"葉門" => "也门",
"伯利茲" => "伯利兹",
"貝里斯" => "伯利兹",
"維德角" => "佛得角",
-"佛得角" => "佛得角",
-"克羅地亞" => "克罗地亚",
"克羅埃西亞" => "克罗地亚",
-"岡比亞" => "冈比亚",
"甘比亞" => "冈比亚",
-"幾內亞比紹" => "几内亚比绍",
"幾內亞比索" => "几内亚比绍",
"列支敦斯登" => "列支敦士登",
-"列支敦士登" => "列支敦士登",
-"利比里亞" => "利比里亚",
"賴比瑞亞" => "利比里亚",
-"加納" => "加纳",
"迦納" => "加纳",
"加彭" => "加蓬",
-"加蓬" => "加蓬",
-"博茨瓦納" => "博茨瓦纳",
"波札那" => "博茨瓦纳",
-"卡塔爾" => "卡塔尔",
"卡達" => "卡塔尔",
-"盧旺達" => "卢旺达",
"盧安達" => "卢旺达",
-"危地馬拉" => "危地马拉",
"瓜地馬拉" => "危地马拉",
"厄瓜多爾" => "厄瓜多尔",
+"厄瓜多尔" => "厄瓜多尔",
"厄瓜多" => "厄瓜多尔",
-"厄立特里亞" => "厄立特里亚",
"厄利垂亞" => "厄立特里亚",
-"吉布堤" => "吉布提",
"吉布地" => "吉布提",
"哈薩克" => "哈萨克斯坦",
-"哥斯達黎加" => "哥斯达黎加",
"哥斯大黎加" => "哥斯达黎加",
-"圖瓦盧" => "图瓦卢",
"吐瓦魯" => "图瓦卢",
"土庫曼" => "土库曼斯坦",
-"聖盧西亞" => "圣卢西亚",
"聖露西亞" => "圣卢西亚",
"聖吉斯納域斯" => "圣基茨和尼维斯",
"聖克里斯多福及尼維斯" => "圣基茨和尼维斯",
-"聖文森特和格林納丁斯" => "圣文森特和格林纳丁斯",
"聖文森及格瑞那丁" => "圣文森特和格林纳丁斯",
-"聖馬力諾" => "圣马力诺",
"聖馬利諾" => "圣马力诺",
-"圭亞那" => "圭亚那",
"蓋亞那" => "圭亚那",
-"坦桑尼亞" => "坦桑尼亚",
"坦尚尼亞" => "坦桑尼亚",
-"埃塞俄比亞" => "埃塞俄比亚",
"衣索匹亞" => "埃塞俄比亚",
"衣索比亞" => "埃塞俄比亚",
"吉里巴斯" => "基里巴斯",
-"基里巴斯" => "基里巴斯",
"塔吉克" => "塔吉克斯坦",
"塞拉利昂" => "塞拉利昂",
"塞普勒斯" => "塞浦路斯",
-"塞浦路斯" => "塞浦路斯",
-"塞舌爾" => "塞舌尔",
"塞席爾" => "塞舌尔",
-"多明尼加共和國" => "多米尼加",
-"多明尼加" => "多米尼加",
-"多明尼加聯邦" => "多米尼加联邦",
-"多米尼克" => "多米尼加联邦",
-"安提瓜和巴布達" => "安提瓜和巴布达",
+"多米尼克" => "多米尼加国",
"安地卡及巴布達" => "安提瓜和巴布达",
"尼日利亞" => "尼日利亚",
+"尼日利亚" => "尼日利亚",
"奈及利亞" => "尼日利亚",
"尼日爾" => "尼日尔",
+"尼日尔" => "尼日尔",
"尼日" => "尼日尔",
"巴貝多" => "巴巴多斯",
-"巴巴多斯" => "巴巴多斯",
-"巴布亞新畿內亞" => "巴布亚新几内亚",
"巴布亞紐幾內亞" => "巴布亚新几内亚",
"布基納法索" => "布基纳法索",
"布吉納法索" => "布基纳法索",
"蒲隆地" => "布隆迪",
-"布隆迪" => "布隆迪",
-"希臘" => "希腊",
"帛琉" => "帕劳",
"義大利" => "意大利",
-"意大利" => "意大利",
-"所羅門群島" => "所罗门群岛",
"索羅門群島" => "所罗门群岛",
"汶萊" => "文莱",
-"斯威士蘭" => "斯威士兰",
"史瓦濟蘭" => "斯威士兰",
-"斯洛文尼亞" => "斯洛文尼亚",
"斯洛維尼亞" => "斯洛文尼亚",
-"新西蘭" => "新西兰",
"紐西蘭" => "新西兰",
-"格林納達" => "格林纳达",
"格瑞那達" => "格林纳达",
-"格魯吉亞" => "乔治亚",
-"喬治亞" => "乔治亚",
-"梵蒂岡" => "梵蒂冈",
-"毛里塔尼亞" => "毛里塔尼亚",
"茅利塔尼亞" => "毛里塔尼亚",
"毛里裘斯" => "毛里求斯",
"模里西斯" => "毛里求斯",
"沙地阿拉伯" => "沙特阿拉伯",
"沙烏地阿拉伯" => "沙特阿拉伯",
-"波斯尼亞黑塞哥維那" => "波斯尼亚和黑塞哥维那",
"波士尼亞赫塞哥維納" => "波斯尼亚和黑塞哥维那",
-"津巴布韋" => "津巴布韦",
"辛巴威" => "津巴布韦",
"宏都拉斯" => "洪都拉斯",
-"洪都拉斯" => "洪都拉斯",
-"特立尼達和多巴哥" => "特立尼达和托巴哥",
"千里達托貝哥" => "特立尼达和托巴哥",
-"瑙魯" => "瑙鲁",
"諾魯" => "瑙鲁",
-"瓦努阿圖" => "瓦努阿图",
"萬那杜" => "瓦努阿图",
"溫納圖" => "瓦努阿图",
-"科摩羅" => "科摩罗",
"葛摩" => "科摩罗",
"象牙海岸" => "科特迪瓦",
"突尼西亞" => "突尼斯",
-"索馬里" => "索马里",
"索馬利亞" => "索马里",
-"老撾" => "老挝",
"寮國" => "老挝",
"肯雅" => "肯尼亚",
"肯亞" => "肯尼亚",
"蘇利南" => "苏里南",
"莫三比克" => "莫桑比克",
-"莫桑比克" => "莫桑比克",
-"萊索托" => "莱索托",
"賴索托" => "莱索托",
-"貝寧" => "贝宁",
"貝南" => "贝宁",
-"贊比亞" => "赞比亚",
"尚比亞" => "赞比亚",
"亞塞拜然" => "阿塞拜疆",
-"阿塞拜疆" => "阿塞拜疆",
-"阿拉伯聯合酋長國" => "阿拉伯联合酋长国",
"阿拉伯聯合大公國" => "阿拉伯联合酋长国",
"南韓" => "韩国",
-"馬爾代夫" => "马尔代夫",
"馬爾地夫" => "马尔代夫",
"馬爾他" => "马耳他",
"馬利共和國" => "马里共和国",
@@ -11415,7 +11293,7 @@ $zh2CN = array(
"泡麵" => "方便面",
"笨豬跳" => "蹦极跳",
"绑紧跳" => "蹦极跳",
-"冷盤  " => "凉菜",
+"冷盤" => "凉菜",
"冷菜" => "凉菜",
"散钱" => "零钱",
"谐星" => "笑星",
@@ -11443,16 +11321,12 @@ $zh2CN = array(
"積架" => "捷豹",
"福斯" => "大众",
"福士" => "大众",
-"雪鐵龍" => "雪铁龙",
"萬事得" => "马自达",
-"馬自達" => "马自达",
"寶獅" => "标志",
"拿破崙" => "拿破仑",
"布殊" => "布什",
"布希" => "布什",
"柯林頓" => "克林顿",
-"克林頓" => "克林顿",
-"薩達姆" => "萨达姆",
"海珊" => "萨达姆",
"梵谷" => "凡高",
"大衛碧咸" => "大卫·贝克汉姆",
@@ -11472,6 +11346,7 @@ $zh2SG = array(
"方便面" => "快速面",
"速食麵" => "快速面",
"即食麵" => "快速面",
+"泡麵" => "快速面",
"蹦极跳" => "绑紧跳",
"笨豬跳" => "绑紧跳",
"凉菜" => "冷菜",
@@ -11483,6 +11358,5 @@ $zh2SG = array(
"民乐" => "华乐",
"住房" => "住屋",
"房价" => "屋价",
-"泡麵" => "快速面",
); \ No newline at end of file
diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php
index 732adae1..22144333 100644
--- a/includes/api/ApiBase.php
+++ b/includes/api/ApiBase.php
@@ -38,14 +38,15 @@
*/
abstract class ApiBase {
- // These constants allow modules to specify exactly how to treat incomming parameters.
+ // These constants allow modules to specify exactly how to treat incoming parameters.
- const PARAM_DFLT = 0;
- const PARAM_ISMULTI = 1;
- const PARAM_TYPE = 2;
- const PARAM_MAX = 3;
- const PARAM_MAX2 = 4;
- const PARAM_MIN = 5;
+ const PARAM_DFLT = 0; // Default value of the parameter
+ const PARAM_ISMULTI = 1; // Boolean, do we accept more than one item for this parameter (e.g.: titles)?
+ const PARAM_TYPE = 2; // Can be either a string type (e.g.: 'integer') or an array of allowed values
+ const PARAM_MAX = 3; // Max value allowed for a parameter. Only applies if TYPE='integer'
+ const PARAM_MAX2 = 4; // Max value allowed for a parameter for bots and sysops. Only applies if TYPE='integer'
+ const PARAM_MIN = 5; // Lowest value allowed for a parameter. Only applies if TYPE='integer'
+ const PARAM_ALLOW_DUPLICATES = 6; // Boolean, do we allow the same value to be set more than once when ISMULTI=true
const LIMIT_BIG1 = 500; // Fast query, std user limit
const LIMIT_BIG2 = 5000; // Fast query, bot/sysop limit
@@ -159,6 +160,10 @@ abstract class ApiBase {
$data =& $this->getResult()->getData();
if(isset($data['warnings'][$this->getModuleName()]))
{
+ # Don't add duplicate warnings
+ $warn_regex = preg_quote($warning, '/');
+ if(preg_match("/{$warn_regex}(\\n|$)/", $data['warnings'][$this->getModuleName()]['*']))
+ return;
$warning = "{$data['warnings'][$this->getModuleName()]['*']}\n$warning";
unset($data['warnings'][$this->getModuleName()]);
}
@@ -238,10 +243,10 @@ abstract class ApiBase {
* module's help.
*/
public function makeHelpMsgParameters() {
- $params = $this->getAllowedParams();
+ $params = $this->getFinalParams();
if ($params !== false) {
- $paramsDescription = $this->getParamDescription();
+ $paramsDescription = $this->getFinalParamDescription();
$msg = '';
$paramPrefix = "\n" . str_repeat(' ', 19);
foreach ($params as $paramName => $paramSettings) {
@@ -260,7 +265,7 @@ abstract class ApiBase {
$choices = array();
$nothingPrompt = false;
foreach ($type as $t)
- if ($t=='')
+ if ($t === '')
$nothingPrompt = 'Can be empty, or ';
else
$choices[] = $t;
@@ -319,18 +324,39 @@ abstract class ApiBase {
}
/**
- * Returns an array of allowed parameters (keys) => default value for that parameter
+ * Returns an array of allowed parameters (keys) => default value for that parameter.
+ * Don't call this function directly: use getFinalParams() to allow hooks
+ * to modify parameters as needed.
*/
protected function getAllowedParams() {
return false;
}
/**
- * Returns the description string for the given parameter.
+ * Returns an array of parameter descriptions.
+ * Don't call this functon directly: use getFinalParamDescription() to allow
+ * hooks to modify descriptions as needed.
*/
protected function getParamDescription() {
return false;
}
+
+ /**
+ * Get final list of parameters, after hooks have had
+ * a chance to tweak it as needed.
+ */
+ public function getFinalParams() {
+ $params = $this->getAllowedParams();
+ wfRunHooks('APIGetAllowedParams', array(&$this, &$params));
+ return $params;
+ }
+
+
+ public function getFinalParamDescription() {
+ $desc = $this->getParamDescription();
+ wfRunHooks('APIGetParamDescription', array(&$this, &$desc));
+ return $desc;
+ }
/**
* This method mangles parameter name based on the prefix supplied to the constructor.
@@ -343,12 +369,11 @@ abstract class ApiBase {
/**
* Using getAllowedParams(), makes an array of the values provided by the user,
* with key being the name of the variable, and value - validated value from user or default.
- * This method can be used to generate local variables using extract().
* limit=max will not be parsed if $parseMaxLimit is set to false; use this
* when the max limit is not definite, e.g. when getting revisions.
*/
public function extractRequestParams($parseMaxLimit = true) {
- $params = $this->getAllowedParams();
+ $params = $this->getFinalParams();
$results = array ();
foreach ($params as $paramName => $paramSettings)
@@ -361,10 +386,27 @@ abstract class ApiBase {
* Get a value for the given parameter
*/
protected function getParameter($paramName, $parseMaxLimit = true) {
- $params = $this->getAllowedParams();
+ $params = $this->getFinalParams();
$paramSettings = $params[$paramName];
return $this->getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit);
}
+
+ /**
+ * Die if none or more than one of a certain set of parameters is set
+ */
+ public function requireOnlyOneParameter($params) {
+ $required = func_get_args();
+ array_shift($required);
+
+ $intersection = array_intersect(array_keys(array_filter($params,
+ create_function('$x', 'return !is_null($x);')
+ )), $required);
+ if (count($intersection) > 1) {
+ $this->dieUsage('The parameters '.implode(', ', $intersection).' can not be used together', 'invalidparammix');
+ } elseif (count($intersection) == 0) {
+ $this->dieUsage('One of the parameters '.implode(', ', $required).' is required', 'missingparam');
+ }
+ }
/**
* Returns an array of the namespaces (by integer id) that exist on the
@@ -400,10 +442,12 @@ abstract class ApiBase {
$default = $paramSettings;
$multi = false;
$type = gettype($paramSettings);
+ $dupes = false;
} else {
$default = isset ($paramSettings[self :: PARAM_DFLT]) ? $paramSettings[self :: PARAM_DFLT] : null;
$multi = isset ($paramSettings[self :: PARAM_ISMULTI]) ? $paramSettings[self :: PARAM_ISMULTI] : false;
$type = isset ($paramSettings[self :: PARAM_TYPE]) ? $paramSettings[self :: PARAM_TYPE] : null;
+ $dupes = isset ($paramSettings[self:: PARAM_ALLOW_DUPLICATES]) ? $paramSettings[self :: PARAM_ALLOW_DUPLICATES] : false;
// When type is not given, and no choices, the type is the same as $default
if (!isset ($type)) {
@@ -494,8 +538,8 @@ abstract class ApiBase {
}
}
- // There should never be any duplicate values in a list
- if (is_array($value))
+ // Throw out duplicates if requested
+ if (is_array($value) && !$dupes)
$value = array_unique($value);
}
@@ -515,10 +559,10 @@ abstract class ApiBase {
protected function parseMultiValue($valueName, $value, $allowMultiple, $allowedValues) {
if( trim($value) === "" )
return array();
- $sizeLimit = $this->mMainModule->canApiHighLimits() ? 501 : 51;
- $valuesList = explode('|', $value,$sizeLimit);
- if( count($valuesList) == $sizeLimit ) {
- $junk = array_pop($valuesList); // kill last jumbled param
+ $sizeLimit = $this->mMainModule->canApiHighLimits() ? self::LIMIT_SML2 : self::LIMIT_SML1;
+ $valuesList = explode('|', $value, $sizeLimit + 1);
+ if( self::truncateArray($valuesList, $sizeLimit) ) {
+ $this->setWarning("Too many values supplied for parameter '$valueName': the limit is $sizeLimit");
}
if (!$allowMultiple && count($valuesList) != 1) {
$possibleValues = is_array($allowedValues) ? "of '" . implode("', '", $allowedValues) . "'" : '';
@@ -527,7 +571,7 @@ abstract class ApiBase {
if (is_array($allowedValues)) {
# Check for unknown values
$unknown = array_diff($valuesList, $allowedValues);
- if(!empty($unknown))
+ if(count($unknown))
{
if($allowMultiple)
{
@@ -569,6 +613,23 @@ abstract class ApiBase {
}
}
}
+
+ /**
+ * Truncate an array to a certain length.
+ * @param $arr array Array to truncate
+ * @param $limit int Maximum length
+ * @return bool True if the array was truncated, false otherwise
+ */
+ public static function truncateArray(&$arr, $limit)
+ {
+ $modified = false;
+ while(count($arr) > $limit)
+ {
+ $junk = array_pop($arr);
+ $modified = true;
+ }
+ return $modified;
+ }
/**
* Call main module's error handler
@@ -594,8 +655,6 @@ abstract class ApiBase {
'protectedpagetext' => array('code' => 'protectedpage', 'info' => "The ``\$1'' right is required to edit this page"),
'protect-cantedit' => array('code' => 'cantedit', 'info' => "You can't protect this page because you can't edit it"),
'badaccess-group0' => array('code' => 'permissiondenied', 'info' => "Permission denied"), // Generic permission denied message
- 'badaccess-group1' => array('code' => 'permissiondenied', 'info' => "Permission denied"), // Can't use the parameter 'cause it's wikilinked
- 'badaccess-group2' => array('code' => 'permissiondenied', 'info' => "Permission denied"),
'badaccess-groups' => array('code' => 'permissiondenied', 'info' => "Permission denied"),
'titleprotected' => array('code' => 'protectedtitle', 'info' => "This title has been protected from creation"),
'nocreate-loggedin' => array('code' => 'cantcreate', 'info' => "You don't have permission to create new pages"),
@@ -632,13 +691,21 @@ abstract class ApiBase {
'ipb_already_blocked' => array('code' => 'alreadyblocked', 'info' => "The user you tried to block was already blocked"),
'ipb_blocked_as_range' => array('code' => 'blockedasrange', 'info' => "IP address ``\$1'' was blocked as part of range ``\$2''. You can't unblock the IP invidually, but you can unblock the range as a whole."),
'ipb_cant_unblock' => array('code' => 'cantunblock', 'info' => "The block you specified was not found. It may have been unblocked already"),
+ 'mailnologin' => array('code' => 'cantsend', 'info' => "You're not logged in or you don't have a confirmed e-mail address, so you can't send e-mail"),
+ 'usermaildisabled' => array('code' => 'usermaildisabled', 'info' => "User email has been disabled"),
+ 'blockedemailuser' => array('code' => 'blockedfrommail', 'info' => "You have been blocked from sending e-mail"),
+ 'notarget' => array('code' => 'notarget', 'info' => "You have not specified a valid target for this action"),
+ 'noemail' => array('code' => 'noemail', 'info' => "The user has not specified a valid e-mail address, or has chosen not to receive e-mail from other users"),
+ 'rcpatroldisabled' => array('code' => 'patroldisabled', 'info' => "Patrolling is disabled on this wiki"),
+ 'markedaspatrollederror-noautopatrol' => array('code' => 'noautopatrol', 'info' => "You don't have permission to patrol your own changes"),
// API-specific messages
'missingparam' => array('code' => 'no$1', 'info' => "The \$1 parameter must be set"),
'invalidtitle' => array('code' => 'invalidtitle', 'info' => "Bad title ``\$1''"),
+ 'nosuchpageid' => array('code' => 'nosuchpageid', 'info' => "There is no page with ID \$1"),
'invaliduser' => array('code' => 'invaliduser', 'info' => "Invalid username ``\$1''"),
- 'invalidexpiry' => array('code' => 'invalidexpiry', 'info' => "Invalid expiry time"),
- 'pastexpiry' => array('code' => 'pastexpiry', 'info' => "Expiry time is in the past"),
+ 'invalidexpiry' => array('code' => 'invalidexpiry', 'info' => "Invalid expiry time ``\$1''"),
+ 'pastexpiry' => array('code' => 'pastexpiry', 'info' => "Expiry time ``\$1'' is in the past"),
'create-titleexists' => array('code' => 'create-titleexists', 'info' => "Existing titles can't be protected with 'create'"),
'missingtitle-createonly' => array('code' => 'missingtitle-createonly', 'info' => "Missing titles can only be protected with 'create'"),
'cantblock' => array('code' => 'cantblock', 'info' => "You don't have permission to block users"),
@@ -651,6 +718,12 @@ abstract class ApiBase {
'permdenied-undelete' => array('code' => 'permissiondenied', 'info' => "You don't have permission to restore deleted revisions"),
'createonly-exists' => array('code' => 'articleexists', 'info' => "The article you tried to create has been created already"),
'nocreate-missing' => array('code' => 'missingtitle', 'info' => "The article you tried to edit doesn't exist"),
+ 'nosuchrcid' => array('code' => 'nosuchrcid', 'info' => "There is no change with rcid ``\$1''"),
+ 'cantpurge' => array('code' => 'cantpurge', 'info' => "Only users with the 'purge' right can purge pages via the API"),
+ 'protect-invalidaction' => array('code' => 'protect-invalidaction', 'info' => "Invalid protection type ``\$1''"),
+ 'protect-invalidlevel' => array('code' => 'protect-invalidlevel', 'info' => "Invalid protection level ``\$1''"),
+ 'toofewexpiries' => array('code' => 'toofewexpiries', 'info' => "\$1 expiry timestamps were provided where \$2 were needed"),
+
// ApiEditPage messages
'noimageredirect-anon' => array('code' => 'noimageredirect-anon', 'info' => "Anonymous users can't create image redirects"),
@@ -665,18 +738,33 @@ abstract class ApiBase {
'editconflict' => array('code' => 'editconflict', 'info' => "Edit conflict detected"),
'hashcheckfailed' => array('code' => 'badmd5', 'info' => "The supplied MD5 hash was incorrect"),
'missingtext' => array('code' => 'notext', 'info' => "One of the text, appendtext and prependtext parameters must be set"),
+ 'emptynewsection' => array('code' => 'emptynewsection', 'info' => 'Creating empty new sections is not possible.'),
);
/**
* Output the error message related to a certain array
- * @param array $error Element of a getUserPermissionsErrors()
+ * @param array $error Element of a getUserPermissionsErrors()-style array
*/
public function dieUsageMsg($error) {
+ $parsed = $this->parseMsg($error);
+ $this->dieUsage($parsed['code'], $parsed['info']);
+ }
+
+ /**
+ * Return the error message related to a certain array
+ * @param array $error Element of a getUserPermissionsErrors()-style array
+ * @return array('code' => code, 'info' => info)
+ */
+ public function parseMsg($error) {
$key = array_shift($error);
if(isset(self::$messageMap[$key]))
- $this->dieUsage(wfMsgReplaceArgs(self::$messageMap[$key]['info'], $error), wfMsgReplaceArgs(self::$messageMap[$key]['code'], $error));
+ return array( 'code' =>
+ wfMsgReplaceArgs(self::$messageMap[$key]['code'], $error),
+ 'info' =>
+ wfMsgReplaceArgs(self::$messageMap[$key]['info'], $error)
+ );
// If the key isn't present, throw an "unknown error"
- $this->dieUsageMsg(array('unknownerror', $key));
+ return $this->parseMsg(array('unknownerror', $key));
}
/**
@@ -814,6 +902,6 @@ abstract class ApiBase {
* Returns a String that identifies the version of this class.
*/
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiBase.php 36309 2008-06-15 20:37:28Z catrope $';
+ return __CLASS__ . ': $Id: ApiBase.php 47041 2009-02-09 14:39:41Z catrope $';
}
}
diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php
index 34813bf7..dfb11061 100644
--- a/includes/api/ApiBlock.php
+++ b/includes/api/ApiBlock.php
@@ -49,7 +49,7 @@ class ApiBlock extends ApiBase {
* of success. If it fails, the result will specify the nature of the error.
*/
public function execute() {
- global $wgUser;
+ global $wgUser, $wgBlockAllowsUTEdit;
$this->getMain()->requestWriteMode();
$params = $this->extractRequestParams();
@@ -72,8 +72,6 @@ class ApiBlock extends ApiBase {
$this->dieUsageMsg(array('canthide'));
if($params['noemail'] && !$wgUser->isAllowed('blockemail'))
$this->dieUsageMsg(array('cantblock-email'));
- if(wfReadOnly())
- $this->dieUsageMsg(array('readonlytext'));
$form = new IPBlockForm('');
$form->BlockAddress = $params['user'];
@@ -83,13 +81,15 @@ class ApiBlock extends ApiBase {
$form->BlockOther = '';
$form->BlockAnonOnly = $params['anononly'];
$form->BlockCreateAccount = $params['nocreate'];
- $form->BlockEnableAutoBlock = $params['autoblock'];
+ $form->BlockEnableAutoblock = $params['autoblock'];
$form->BlockEmail = $params['noemail'];
$form->BlockHideName = $params['hidename'];
+ $form->BlockAllowUsertalk = $params['allowusertalk'] && $wgBlockAllowsUTEdit;
+ $form->BlockReblock = $params['reblock'];
$userID = $expiry = null;
$retval = $form->doBlock($userID, $expiry);
- if(!empty($retval))
+ if(count($retval))
// We don't care about multiple errors, just report one of them
$this->dieUsageMsg($retval);
@@ -107,6 +107,8 @@ class ApiBlock extends ApiBase {
$res['noemail'] = '';
if($params['hidename'])
$res['hidename'] = '';
+ if($params['allowusertalk'])
+ $res['allowusertalk'] = '';
$this->getResult()->addValue(null, $this->getModuleName(), $res);
}
@@ -125,13 +127,15 @@ class ApiBlock extends ApiBase {
'autoblock' => false,
'noemail' => false,
'hidename' => false,
+ 'allowusertalk' => false,
+ 'reblock' => false,
);
}
public function getParamDescription() {
return array (
'user' => 'Username, IP address or IP range you want to block',
- 'token' => 'A block token previously obtained through the gettoken parameter',
+ 'token' => 'A block token previously obtained through the gettoken parameter or prop=info',
'gettoken' => 'If set, a block token will be returned, and no other action will be taken',
'expiry' => 'Relative expiry time, e.g. \'5 months\' or \'2 weeks\'. If set to \'infinite\', \'indefinite\' or \'never\', the block will never expire.',
'reason' => 'Reason for block (optional)',
@@ -139,7 +143,9 @@ class ApiBlock extends ApiBase {
'nocreate' => 'Prevent account creation',
'autoblock' => 'Automatically block the last used IP address, and any subsequent IP addresses they try to login from',
'noemail' => 'Prevent user from sending e-mail through the wiki. (Requires the "blockemail" right.)',
- 'hidename' => 'Hide the username from the block log. (Requires the "hideuser" right.)'
+ 'hidename' => 'Hide the username from the block log. (Requires the "hideuser" right.)',
+ 'allowusertalk' => 'Allow the user to edit their own talk page (depends on $wgBlockAllowsUTEdit)',
+ 'reblock' => 'If the user is already blocked, overwrite the existing block',
);
}
@@ -157,6 +163,6 @@ class ApiBlock extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiBlock.php 35388 2008-05-27 10:18:28Z catrope $';
+ return __CLASS__ . ': $Id: ApiBlock.php 43677 2008-11-18 15:21:04Z catrope $';
}
}
diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php
index 06592d46..c0212924 100644
--- a/includes/api/ApiDelete.php
+++ b/includes/api/ApiDelete.php
@@ -52,29 +52,36 @@ class ApiDelete extends ApiBase {
$this->getMain()->requestWriteMode();
$params = $this->extractRequestParams();
- $titleObj = NULL;
- if(!isset($params['title']))
- $this->dieUsageMsg(array('missingparam', 'title'));
+ $this->requireOnlyOneParameter($params, 'title', 'pageid');
if(!isset($params['token']))
$this->dieUsageMsg(array('missingparam', 'token'));
- $titleObj = Title::newFromText($params['title']);
- if(!$titleObj)
- $this->dieUsageMsg(array('invalidtitle', $params['title']));
+ if(isset($params['title']))
+ {
+ $titleObj = Title::newFromText($params['title']);
+ if(!$titleObj)
+ $this->dieUsageMsg(array('invalidtitle', $params['title']));
+ }
+ else if(isset($params['pageid']))
+ {
+ $titleObj = Title::newFromID($params['pageid']);
+ if(!$titleObj)
+ $this->dieUsageMsg(array('nosuchpageid', $params['pageid']));
+ }
if(!$titleObj->exists())
$this->dieUsageMsg(array('notanarticle'));
$reason = (isset($params['reason']) ? $params['reason'] : NULL);
- if ($titleObj->getNamespace() == NS_IMAGE) {
- $retval = self::deletefile($params['token'], $titleObj, $params['oldimage'], $reason, false);
- if(!empty($retval))
+ if ($titleObj->getNamespace() == NS_FILE) {
+ $retval = self::deleteFile($params['token'], $titleObj, $params['oldimage'], $reason, false);
+ if(count($retval))
// We don't care about multiple errors, just report one of them
$this->dieUsageMsg(current($retval));
} else {
$articleObj = new Article($titleObj);
$retval = self::delete($articleObj, $params['token'], $reason);
- if(!empty($retval))
+ if(count($retval))
// We don't care about multiple errors, just report one of them
$this->dieUsageMsg(current($retval));
@@ -90,8 +97,6 @@ class ApiDelete extends ApiBase {
private static function getPermissionsError(&$title, $token) {
global $wgUser;
- // Check wiki readonly
- if (wfReadOnly()) return array(array('readonlytext'));
// Check permissions
$errors = $title->getUserPermissionsErrors('delete', $wgUser);
@@ -114,8 +119,8 @@ class ApiDelete extends ApiBase {
public static function delete(&$article, $token, &$reason = NULL)
{
global $wgUser;
-
- $errors = self::getPermissionsError($article->getTitle(), $token);
+ $title = $article->getTitle();
+ $errors = self::getPermissionsError($title, $token);
if (count($errors)) return $errors;
// Auto-generate a summary, if necessary
@@ -156,7 +161,8 @@ class ApiDelete extends ApiBase {
if( !FileDeleteForm::haveDeletableFile($file, $oldfile, $oldimage) )
return array(array('nofile'));
-
+ if (is_null($reason)) # Log and RC don't like null reasons
+ $reason = '';
$status = FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress );
if( !$status->isGood() )
@@ -170,6 +176,9 @@ class ApiDelete extends ApiBase {
public function getAllowedParams() {
return array (
'title' => null,
+ 'pageid' => array(
+ ApiBase::PARAM_TYPE => 'integer'
+ ),
'token' => null,
'reason' => null,
'watch' => false,
@@ -180,7 +189,8 @@ class ApiDelete extends ApiBase {
public function getParamDescription() {
return array (
- 'title' => 'Title of the page you want to delete.',
+ 'title' => 'Title of the page you want to delete. Cannot be used together with pageid',
+ 'pageid' => 'Page ID of the page you want to delete. Cannot be used together with title',
'token' => 'A delete token previously retrieved through prop=info',
'reason' => 'Reason for the deletion. If not set, an automatically generated reason will be used.',
'watch' => 'Add the page to your watchlist',
@@ -191,7 +201,7 @@ class ApiDelete extends ApiBase {
public function getDescription() {
return array(
- 'Deletes a page. You need to be logged in as a sysop to use this function, see also action=login.'
+ 'Delete a page.'
);
}
@@ -203,6 +213,6 @@ class ApiDelete extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiDelete.php 35350 2008-05-26 12:15:21Z simetrical $';
+ return __CLASS__ . ': $Id: ApiDelete.php 44541 2008-12-13 21:07:18Z mrzman $';
}
}
diff --git a/includes/api/ApiDisabled.php b/includes/api/ApiDisabled.php
new file mode 100644
index 00000000..40e38a0f
--- /dev/null
+++ b/includes/api/ApiDisabled.php
@@ -0,0 +1,72 @@
+<?php
+
+/*
+ * Created on Sep 25, 2008
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiBase.php");
+}
+
+
+/**
+ * API module that dies with an error immediately.
+ *
+ * Use this to disable core modules with
+ * $wgAPIModules['modulename'] = 'ApiDisabled';
+ *
+ * To disable submodules of action=query, use ApiQueryDisabled instead
+ *
+ * @ingroup API
+ */
+class ApiDisabled extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ public function execute() {
+ $this->dieUsage("The ``{$this->getModuleName()}'' module has been disabled.", 'moduledisabled');
+ }
+
+ public function getAllowedParams() {
+ return array ();
+ }
+
+ public function getParamDescription() {
+ return array ();
+ }
+
+ public function getDescription() {
+ return array(
+ 'This module has been disabled.'
+ );
+ }
+
+ protected function getExamples() {
+ return array ();
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiDisabled.php 41268 2008-09-25 20:50:50Z catrope $';
+ }
+}
diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php
index d10432f3..bc5dfa87 100644
--- a/includes/api/ApiEditPage.php
+++ b/includes/api/ApiEditPage.php
@@ -29,8 +29,10 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * A query module to list all external URLs found on a given set of pages.
+ * A module that allows for editing and creating pages.
*
+ * Currently, this wraps around the EditPage class in an ugly way,
+ * EditPage.php should be rewritten to provide a cleaner interface
* @ingroup API
*/
class ApiEditPage extends ApiBase {
@@ -66,7 +68,7 @@ class ApiEditPage extends ApiBase {
$errors = $titleObj->getUserPermissionsErrors('edit', $wgUser);
if(!$titleObj->exists())
$errors = array_merge($errors, $titleObj->getUserPermissionsErrors('create', $wgUser));
- if(!empty($errors))
+ if(count($errors))
$this->dieUsageMsg($errors[0]);
$articleObj = new Article($titleObj);
@@ -98,8 +100,11 @@ class ApiEditPage extends ApiBase {
$reqArr['wpEdittime'] = wfTimestamp(TS_MW, $params['basetimestamp']);
else
$reqArr['wpEdittime'] = $articleObj->getTimestamp();
- # Fake wpStartime
- $reqArr['wpStarttime'] = $reqArr['wpEdittime'];
+ if(!is_null($params['starttimestamp']) && $params['starttimestamp'] != '')
+ $reqArr['wpStarttime'] = wfTimestamp(TS_MW, $params['starttimestamp']);
+ else
+ # Fake wpStartime
+ $reqArr['wpStarttime'] = $reqArr['wpEdittime'];
if($params['minor'] || (!$params['notminor'] && $wgUser->getOption('minordefault')))
$reqArr['wpMinoredit'] = '';
if($params['recreate'])
@@ -111,6 +116,8 @@ class ApiEditPage extends ApiBase {
$this->dieUsage("The section parameter must be set to an integer or 'new'", "invalidsection");
$reqArr['wpSection'] = $params['section'];
}
+ else
+ $reqArr['wpSection'] = '';
if($params['watch'])
$watch = true;
@@ -134,13 +141,13 @@ class ApiEditPage extends ApiBase {
# Handle CAPTCHA parameters
global $wgRequest;
if(isset($params['captchaid']))
- $wgRequest->data['wpCaptchaId'] = $params['captchaid'];
+ $wgRequest->setVal( 'wpCaptchaId', $params['captchaid'] );
if(isset($params['captchaword']))
- $wgRequest->data['wpCaptchaWord'] = $params['captchaword'];
+ $wgRequest->setVal( 'wpCaptchaWord', $params['captchaword'] );
$r = array();
if(!wfRunHooks('APIEditBeforeSave', array(&$ep, $ep->textbox1, &$r)))
{
- if(!empty($r))
+ if(count($r))
{
$r['result'] = "Failure";
$this->getResult()->addValue(null, $this->getModuleName(), $r);
@@ -200,18 +207,24 @@ class ApiEditPage extends ApiBase {
case EditPage::AS_CONFLICT_DETECTED:
$this->dieUsageMsg(array('editconflict'));
#case EditPage::AS_SUMMARY_NEEDED: Can't happen since we set wpIgnoreBlankSummary
- #case EditPage::AS_TEXTBOX_EMPTY: Can't happen since we don't do sections
+ case EditPage::AS_TEXTBOX_EMPTY:
+ $this->dieUsageMsg(array('emptynewsection'));
case EditPage::AS_END:
# This usually means some kind of race condition
# or DB weirdness occurred. Throw an unknown error here.
- $this->dieUsageMsg(array('unknownerror', 'AS_END'));
+ $this->dieUsageMsg(array('unknownerror'));
case EditPage::AS_SUCCESS_NEW_ARTICLE:
$r['new'] = '';
case EditPage::AS_SUCCESS_UPDATE:
$r['result'] = "Success";
$r['pageid'] = $titleObj->getArticleID();
$r['title'] = $titleObj->getPrefixedText();
- $newRevId = $titleObj->getLatestRevId();
+ # HACK: We create a new Article object here because getRevIdFetched()
+ # refuses to be run twice, and because Title::getLatestRevId()
+ # won't fetch from the master unless we select for update, which we
+ # don't want to do.
+ $newArticle = new Article($titleObj);
+ $newRevId = $newArticle->getRevIdFetched();
if($newRevId == $oldRevId)
$r['nochange'] = '';
else
@@ -245,6 +258,7 @@ class ApiEditPage extends ApiBase {
'notminor' => false,
'bot' => false,
'basetimestamp' => null,
+ 'starttimestamp' => null,
'recreate' => false,
'createonly' => false,
'nocreate' => false,
@@ -271,6 +285,9 @@ class ApiEditPage extends ApiBase {
'basetimestamp' => array('Timestamp of the base revision (gotten through prop=revisions&rvprop=timestamp).',
'Used to detect edit conflicts; leave unset to ignore conflicts.'
),
+ 'starttimestamp' => array('Timestamp when you obtained the edit token.',
+ 'Used to detect edit conflicts; leave unset to ignore conflicts.'
+ ),
'recreate' => 'Override any errors about the article having been deleted in the meantime',
'createonly' => 'Don\'t edit the page if it exists already',
'nocreate' => 'Throw an error if the page doesn\'t exist',
@@ -294,6 +311,6 @@ class ApiEditPage extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiEditPage.php 36309 2008-06-15 20:37:28Z catrope $';
+ return __CLASS__ . ': $Id: ApiEditPage.php 44394 2008-12-10 14:12:54Z catrope $';
}
}
diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php
index 7e083536..fbdf495f 100644
--- a/includes/api/ApiEmailUser.php
+++ b/includes/api/ApiEmailUser.php
@@ -39,6 +39,11 @@ class ApiEmailUser extends ApiBase {
public function execute() {
global $wgUser;
+
+ // Check whether email is enabled
+ if ( !EmailUserForm::userEmailEnabled() )
+ $this->dieUsageMsg( array( 'usermaildisabled' ) );
+
$this->getMain()->requestWriteMode();
$params = $this->extractRequestParams();
@@ -53,12 +58,12 @@ class ApiEmailUser extends ApiBase {
// Validate target
$targetUser = EmailUserForm::validateEmailTarget( $params['target'] );
if ( !( $targetUser instanceof User ) )
- $this->dieUsageMsg( array( $targetUser[0] ) );
+ $this->dieUsageMsg( array( $targetUser ) );
// Check permissions
$error = EmailUserForm::getPermissionsError( $wgUser, $params['token'] );
if ( $error )
- $this->dieUsageMsg( array( $error[0] ) );
+ $this->dieUsageMsg( array( $error ) );
$form = new EmailUserForm( $targetUser, $params['text'], $params['subject'], $params['ccme'] );
@@ -89,7 +94,6 @@ class ApiEmailUser extends ApiBase {
'target' => 'User to send email to',
'subject' => 'Subject header',
'text' => 'Mail body',
- // FIXME: How to properly get a token?
'token' => 'A token previously acquired via prop=info',
'ccme' => 'Send a copy of this mail to me',
);
@@ -97,7 +101,7 @@ class ApiEmailUser extends ApiBase {
public function getDescription() {
return array(
- 'Emails a user.'
+ 'Email a user.'
);
}
@@ -108,7 +112,7 @@ class ApiEmailUser extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: $';
+ return __CLASS__ . ': $Id: ApiEmailUser.php 41269 2008-09-25 21:39:36Z catrope $';
}
}
\ No newline at end of file
diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php
index 397aece3..f4e6212a 100644
--- a/includes/api/ApiExpandTemplates.php
+++ b/includes/api/ApiExpandTemplates.php
@@ -43,23 +43,22 @@ class ApiExpandTemplates extends ApiBase {
public function execute() {
// Get parameters
- extract( $this->extractRequestParams() );
- $retval = '';
+ $params = $this->extractRequestParams();
//Create title for parser
- $title_obj = Title :: newFromText( $title );
+ $title_obj = Title :: newFromText( $params['title'] );
if(!$title_obj)
- $title_obj = Title :: newFromText( "API" ); // Default title is "API". For example, ExpandTemplates uses "ExpendTemplates" for it
+ $title_obj = Title :: newFromText( "API" ); // default
$result = $this->getResult();
// Parse text
global $wgParser;
$options = new ParserOptions();
- if ( $generatexml )
+ if ( $params['generatexml'] )
{
$wgParser->startExternalParse( $title_obj, $options, OT_PREPROCESS );
- $dom = $wgParser->preprocessToDom( $text );
+ $dom = $wgParser->preprocessToDom( $params['text'] );
if ( is_callable( array( $dom, 'saveXML' ) ) ) {
$xml = $dom->saveXML();
} else {
@@ -67,9 +66,9 @@ class ApiExpandTemplates extends ApiBase {
}
$xml_result = array();
$result->setContent( $xml_result, $xml );
- $result->addValue( null, 'parsetree', $xml_result);
+ $result->addValue( null, 'parsetree', $xml_result);
}
- $retval = $wgParser->preprocess( $text, $title_obj, $options );
+ $retval = $wgParser->preprocess( $params['text'], $title_obj, $options );
// Return result
$retval_array = array();
@@ -106,6 +105,6 @@ class ApiExpandTemplates extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiExpandTemplates.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiExpandTemplates.php 44719 2008-12-17 16:34:01Z catrope $';
}
}
diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php
index 8f08f4db..9efbbbe0 100644
--- a/includes/api/ApiFormatBase.php
+++ b/includes/api/ApiFormatBase.php
@@ -199,15 +199,17 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
* This method also replaces any '<' with &lt;
*/
protected function formatHTML($text) {
+ global $wgUrlProtocols;
+
// Escape everything first for full coverage
$text = htmlspecialchars($text);
// encode all comments or tags as safe blue strings
$text = preg_replace('/\&lt;(!--.*?--|.*?)\&gt;/', '<span style="color:blue;">&lt;\1&gt;</span>', $text);
// identify URLs
- $protos = "http|https|ftp|gopher";
+ $protos = implode("|", $wgUrlProtocols);
# This regex hacks around bug 13218 (&quot; included in the URL)
- $text = preg_replace("#(($protos)://.*?)(&quot;)?([ \\'\"()<\n])#", '<a href="\\1">\\1</a>\\3\\4', $text);
+ $text = preg_replace("#(($protos).*?)(&quot;)?([ \\'\"()<\n])#", '<a href="\\1">\\1</a>\\3\\4', $text);
// identify requests to api.php
$text = preg_replace("#api\\.php\\?[^ \\()<\n\t]+#", '<a href="\\0">\\0</a>', $text);
if( $this->mHelp ) {
@@ -239,7 +241,7 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
}
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiFormatBase.php 44569 2008-12-14 08:31:04Z tstarling $';
+ return __CLASS__ . ': $Id: ApiFormatBase.php 43470 2008-11-14 00:30:34Z tstarling $';
}
}
@@ -300,6 +302,6 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatBase.php 44569 2008-12-14 08:31:04Z tstarling $';
+ return __CLASS__ . ': $Id: ApiFormatBase.php 43470 2008-11-14 00:30:34Z tstarling $';
}
}
diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php
index 42156849..1d89eb18 100644
--- a/includes/api/ApiFormatJson.php
+++ b/includes/api/ApiFormatJson.php
@@ -58,7 +58,10 @@ class ApiFormatJson extends ApiFormatBase {
$suffix = ")";
}
- if (!function_exists('json_encode') || $this->getIsHtml()) {
+ // Some versions of PHP have a broken json_encode, see PHP bug
+ // 46944. Test encoding an affected character (U+20000) to
+ // avoid this.
+ if (!function_exists('json_encode') || $this->getIsHtml() || strtolower(json_encode("\xf0\xa0\x80\x80")) != '\ud840\udc00') {
$json = new Services_JSON();
$this->printText($prefix . $json->encode($this->getResultData(), $this->getIsHtml()) . $suffix);
} else {
@@ -86,6 +89,6 @@ class ApiFormatJson extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatJson.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiFormatJson.php 45682 2009-01-12 19:06:33Z raymond $';
}
}
diff --git a/includes/api/ApiFormatJson_json.php b/includes/api/ApiFormatJson_json.php
index 87d7086e..4b29ff56 100644
--- a/includes/api/ApiFormatJson_json.php
+++ b/includes/api/ApiFormatJson_json.php
@@ -50,7 +50,7 @@
* @author Matt Knapp <mdknapp[at]gmail[dot]com>
* @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
* @copyright 2005 Michal Migurski
-* @version CVS: $Id: ApiFormatJson_json.php 35098 2008-05-20 17:13:28Z ialex $
+* @version CVS: $Id: ApiFormatJson_json.php 45682 2009-01-12 19:06:33Z raymond $
* @license http://www.opensource.org/licenses/bsd-license.php
* @see http://pear.php.net/pepr/pepr-proposal-show.php?id=198
*/
@@ -168,6 +168,17 @@ class Services_JSON
return chr(0xC0 | (($bytes >> 6) & 0x1F))
. chr(0x80 | ($bytes & 0x3F));
+ case (0xFC00 & $bytes) == 0xD800 && strlen($utf16) >= 4 && (0xFC & ord($utf16{2})) == 0xDC:
+ // return a 4-byte UTF-8 character
+ $char = ((($bytes & 0x03FF) << 10)
+ | ((ord($utf16{2}) & 0x03) << 8)
+ | ord($utf16{3}));
+ $char += 0x10000;
+ return chr(0xF0 | (($char >> 18) & 0x07))
+ . chr(0x80 | (($char >> 12) & 0x3F))
+ . chr(0x80 | (($char >> 6) & 0x3F))
+ . chr(0x80 | ($char & 0x3F));
+
case (0xFFFF & $bytes) == $bytes:
// return a 3-byte UTF-8 character
// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
@@ -218,6 +229,20 @@ class Services_JSON
| (0x0F & (ord($utf8{1}) >> 2)))
. chr((0xC0 & (ord($utf8{1}) << 6))
| (0x7F & ord($utf8{2})));
+
+ case 4:
+ // return a UTF-16 surrogate pair from a 4-byte UTF-8 char
+ if(ord($utf8{0}) > 0xF4) return ''; # invalid
+ $char = ((0x1C0000 & (ord($utf8{0}) << 18))
+ | (0x03F000 & (ord($utf8{1}) << 12))
+ | (0x000FC0 & (ord($utf8{2}) << 6))
+ | (0x00003F & ord($utf8{3})));
+ if($char > 0x10FFFF) return ''; # invalid
+ $char -= 0x10000;
+ return chr(0xD8 | (($char >> 18) & 0x03))
+ . chr(($char >> 10) & 0xFF)
+ . chr(0xDC | (($char >> 8) & 0x03))
+ . chr($char & 0xFF);
}
// ignoring UTF-32 for now, sorry
@@ -346,40 +371,19 @@ class Services_JSON
case (($ord_var_c & 0xF8) == 0xF0):
// characters U-00010000 - U-001FFFFF, mask 11110XXX
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ // These will always return a surrogate pair
$char = pack('C*', $ord_var_c,
ord($var{$c + 1}),
ord($var{$c + 2}),
ord($var{$c + 3}));
$c += 3;
$utf16 = $this->utf82utf16($char);
- $ascii .= sprintf('\u%04s', bin2hex($utf16));
- break;
-
- case (($ord_var_c & 0xFC) == 0xF8):
- // characters U-00200000 - U-03FFFFFF, mask 111110XX
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
- $char = pack('C*', $ord_var_c,
- ord($var{$c + 1}),
- ord($var{$c + 2}),
- ord($var{$c + 3}),
- ord($var{$c + 4}));
- $c += 4;
- $utf16 = $this->utf82utf16($char);
- $ascii .= sprintf('\u%04s', bin2hex($utf16));
- break;
-
- case (($ord_var_c & 0xFE) == 0xFC):
- // characters U-04000000 - U-7FFFFFFF, mask 1111110X
- // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
- $char = pack('C*', $ord_var_c,
- ord($var{$c + 1}),
- ord($var{$c + 2}),
- ord($var{$c + 3}),
- ord($var{$c + 4}),
- ord($var{$c + 5}));
- $c += 5;
- $utf16 = $this->utf82utf16($char);
- $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ if($utf16 == '') {
+ $ascii .= '\ufffd';
+ } else {
+ $utf16 = str_split($utf16, 2);
+ $ascii .= sprintf('\u%04s\u%04s', bin2hex($utf16[0]), bin2hex($utf16[1]));
+ }
break;
}
}
@@ -591,6 +595,16 @@ class Services_JSON
}
break;
+ case preg_match('/\\\uD[89AB][0-9A-F]{2}\\\uD[C-F][0-9A-F]{2}/i', substr($chrs, $c, 12)):
+ // escaped unicode surrogate pair
+ $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2)))
+ . chr(hexdec(substr($chrs, ($c + 4), 2)))
+ . chr(hexdec(substr($chrs, ($c + 8), 2)))
+ . chr(hexdec(substr($chrs, ($c + 10), 2)));
+ $utf8 .= $this->utf162utf8($utf16);
+ $c += 11;
+ break;
+
case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)):
// single, escaped unicode character
$utf16 = chr(hexdec(substr($chrs, ($c + 2), 2)))
@@ -812,6 +826,9 @@ class Services_JSON
}
}
+
+// Hide the PEAR_Error variant from Doxygen
+/// @cond
if (class_exists('PEAR_Error')) {
/**
@@ -827,6 +844,7 @@ if (class_exists('PEAR_Error')) {
}
} else {
+/// @endcond
/**
* @todo Ultimately, this class shall be descended from PEAR_Error
diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php
index 0909539e..e741c16d 100644
--- a/includes/api/ApiFormatWddx.php
+++ b/includes/api/ApiFormatWddx.php
@@ -42,38 +42,62 @@ class ApiFormatWddx extends ApiFormatBase {
}
public function execute() {
- if (function_exists('wddx_serialize_value')) {
+ if (function_exists('wddx_serialize_value') && !$this->getIsHtml()) {
$this->printText(wddx_serialize_value($this->getResultData()));
} else {
- $this->printText('<?xml version="1.0" encoding="utf-8"?>');
- $this->printText('<wddxPacket version="1.0"><header/><data>');
- $this->slowWddxPrinter($this->getResultData());
- $this->printText('</data></wddxPacket>');
+ // Don't do newlines and indentation if we weren't asked
+ // for pretty output
+ $nl = ($this->getIsHtml() ? "" : "\n");
+ $indstr = " ";
+ $this->printText("<?xml version=\"1.0\"?>$nl");
+ $this->printText("<wddxPacket version=\"1.0\">$nl");
+ $this->printText("$indstr<header/>$nl");
+ $this->printText("$indstr<data>$nl");
+ $this->slowWddxPrinter($this->getResultData(), 4);
+ $this->printText("$indstr</data>$nl");
+ $this->printText("</wddxPacket>$nl");
}
}
/**
* Recursivelly go through the object and output its data in WDDX format.
*/
- function slowWddxPrinter($elemValue) {
+ function slowWddxPrinter($elemValue, $indent = 0) {
+ $indstr = ($this->getIsHtml() ? "" : str_repeat(' ', $indent));
+ $indstr2 = ($this->getIsHtml() ? "" : str_repeat(' ', $indent + 2));
+ $nl = ($this->getIsHtml() ? "" : "\n");
switch (gettype($elemValue)) {
case 'array' :
- $this->printText('<struct>');
- foreach ($elemValue as $subElemName => $subElemValue) {
- $this->printText(wfElement('var', array (
- 'name' => $subElemName
- ), null));
- $this->slowWddxPrinter($subElemValue);
- $this->printText('</var>');
+ // Check whether we've got an associative array (<struct>)
+ // or a regular array (<array>)
+ $cnt = count($elemValue);
+ if($cnt == 0 || array_keys($elemValue) === range(0, $cnt - 1)) {
+ // Regular array
+ $this->printText($indstr . Xml::element('array', array(
+ 'length' => $cnt
+ ), null) . $nl);
+ foreach($elemValue as $subElemValue)
+ $this->slowWddxPrinter($subElemValue, $indent + 2);
+ $this->printText("$indstr</array>$nl");
+ } else {
+ // Associative array (<struct>)
+ $this->printText("$indstr<struct>$nl");
+ foreach($elemValue as $subElemName => $subElemValue) {
+ $this->printText($indstr2 . Xml::element('var', array(
+ 'name' => $subElemName
+ ), null) . $nl);
+ $this->slowWddxPrinter($subElemValue, $indent + 4);
+ $this->printText("$indstr2</var>$nl");
+ }
+ $this->printText("$indstr</struct>$nl");
}
- $this->printText('</struct>');
break;
case 'integer' :
case 'double' :
- $this->printText(wfElement('number', null, $elemValue));
+ $this->printText($indstr . Xml::element('number', null, $elemValue) . $nl);
break;
case 'string' :
- $this->printText(wfElement('string', null, $elemValue));
+ $this->printText($indstr . Xml::element('string', null, $elemValue) . $nl);
break;
default :
ApiBase :: dieDebug(__METHOD__, 'Unknown type ' . gettype($elemValue));
@@ -85,6 +109,6 @@ class ApiFormatWddx extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatWddx.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiFormatWddx.php 44588 2008-12-14 19:14:21Z demon $';
}
}
diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php
index d35eb3e9..7ff57324 100644
--- a/includes/api/ApiFormatXml.php
+++ b/includes/api/ApiFormatXml.php
@@ -56,12 +56,12 @@ class ApiFormatXml extends ApiFormatBase {
$params = $this->extractRequestParams();
$this->mDoubleQuote = $params['xmldoublequote'];
- $this->printText('<?xml version="1.0" encoding="utf-8"?>');
+ $this->printText('<?xml version="1.0"?>');
$this->recXmlPrint($this->mRootElemName, $this->getResultData(), $this->getIsHtml() ? -2 : null);
}
/**
- * This method takes an array and converts it into an xml.
+ * This method takes an array and converts it to XML.
* There are several noteworthy cases:
*
* If array contains a key '_element', then the code assumes that ALL other keys are not important and replaces them with the value['_element'].
@@ -80,6 +80,7 @@ class ApiFormatXml extends ApiFormatBase {
} else {
$indstr = '';
}
+ $elemName = str_replace(' ', '_', $elemName);
switch (gettype($elemValue)) {
case 'array' :
@@ -104,6 +105,14 @@ class ApiFormatXml extends ApiFormatBase {
foreach ($elemValue as $subElemId => & $subElemValue) {
if (is_string($subElemValue) && $this->mDoubleQuote)
$subElemValue = $this->doubleQuote($subElemValue);
+
+ // Replace spaces with underscores
+ $newSubElemId = str_replace(' ', '_', $subElemId);
+ if($newSubElemId != $subElemId) {
+ $elemValue[$newSubElemId] = $subElemValue;
+ unset($elemValue[$subElemId]);
+ $subElemId = $newSubElemId;
+ }
if (gettype($subElemId) === 'integer') {
$indElements[] = $subElemValue;
@@ -114,18 +123,18 @@ class ApiFormatXml extends ApiFormatBase {
}
}
- if (is_null($subElemIndName) && !empty ($indElements))
+ if (is_null($subElemIndName) && count($indElements))
ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has integer keys without _element value. Use ApiResult::setIndexedTagName().");
- if (!empty ($subElements) && !empty ($indElements) && !is_null($subElemContent))
+ if (count($subElements) && count($indElements) && !is_null($subElemContent))
ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has content and subelements");
if (!is_null($subElemContent)) {
- $this->printText($indstr . wfElement($elemName, $elemValue, $subElemContent));
- } elseif (empty ($indElements) && empty ($subElements)) {
- $this->printText($indstr . wfElement($elemName, $elemValue));
+ $this->printText($indstr . Xml::element($elemName, $elemValue, $subElemContent));
+ } elseif (!count($indElements) && !count($subElements)) {
+ $this->printText($indstr . Xml::element($elemName, $elemValue));
} else {
- $this->printText($indstr . wfElement($elemName, $elemValue, null));
+ $this->printText($indstr . Xml::element($elemName, $elemValue, null));
foreach ($subElements as $subElemId => & $subElemValue)
$this->recXmlPrint($subElemId, $subElemValue, $indent);
@@ -133,14 +142,14 @@ class ApiFormatXml extends ApiFormatBase {
foreach ($indElements as $subElemId => & $subElemValue)
$this->recXmlPrint($subElemIndName, $subElemValue, $indent);
- $this->printText($indstr . wfCloseElement($elemName));
+ $this->printText($indstr . Xml::closeElement($elemName));
}
break;
case 'object' :
// ignore
break;
default :
- $this->printText($indstr . wfElement($elemName, null, $elemValue));
+ $this->printText($indstr . Xml::element($elemName, null, $elemValue));
break;
}
}
@@ -166,6 +175,6 @@ class ApiFormatXml extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatXml.php 37075 2008-07-04 22:44:57Z brion $';
+ return __CLASS__ . ': $Id: ApiFormatXml.php 44588 2008-12-14 19:14:21Z demon $';
}
}
diff --git a/includes/api/ApiFormatYaml_spyc.php b/includes/api/ApiFormatYaml_spyc.php
index c0d4093e..f16b2c8a 100644
--- a/includes/api/ApiFormatYaml_spyc.php
+++ b/includes/api/ApiFormatYaml_spyc.php
@@ -1,883 +1,234 @@
<?php
- /**
- * Spyc -- A Simple PHP YAML Class
- * @version 0.2.3 -- 2006-02-04
- * @author Chris Wanstrath <chris@ozmm.org>
- * @see http://spyc.sourceforge.net/
- * @copyright Copyright 2005-2006 Chris Wanstrath
- * @license http://www.opensource.org/licenses/mit-license.php MIT License
- */
-
- /**
- * A node, used by Spyc for parsing YAML.
- * @ingroup API
- */
- class YAMLNode {
- /**#@+
- * @access public
- * @var string
- */
- var $parent;
- var $id;
- /**#@-*/
- /**
- * @access public
- * @var mixed
- */
- var $data;
- /**
- * @access public
- * @var int
- */
- var $indent;
- /**
- * @access public
- * @var bool
- */
- var $children = false;
-
- /**
- * The constructor assigns the node a unique ID.
- * @access public
- * @return void
- */
- function YAMLNode() {
- $this->id = uniqid('');
- }
- }
-
- /**
- * The Simple PHP YAML Class.
- *
- * This class can be used to read a YAML file and convert its contents
- * into a PHP array. It currently supports a very limited subsection of
- * the YAML spec.
- *
- * Usage:
- * <code>
- * $parser = new Spyc;
- * $array = $parser->load($file);
- * </code>
- * @ingroup API
- */
- class Spyc {
-
- /**
- * Load YAML into a PHP array statically
- *
- * The load method, when supplied with a YAML stream (string or file),
- * will do its best to convert YAML in a file into a PHP array. Pretty
- * simple.
- * Usage:
- * <code>
- * $array = Spyc::YAMLLoad('lucky.yml');
- * print_r($array);
- * </code>
- * @access public
- * @return array
- * @param string $input Path of YAML file or string containing YAML
- */
- function YAMLLoad($input) {
- $spyc = new Spyc;
- return $spyc->load($input);
- }
-
- /**
- * Dump YAML from PHP array statically
- *
- * The dump method, when supplied with an array, will do its best
- * to convert the array into friendly YAML. Pretty simple. Feel free to
- * save the returned string as nothing.yml and pass it around.
- *
- * Oh, and you can decide how big the indent is and what the wordwrap
- * for folding is. Pretty cool -- just pass in 'false' for either if
- * you want to use the default.
- *
- * Indent's default is 2 spaces, wordwrap's default is 40 characters. And
- * you can turn off wordwrap by passing in 0.
- *
- * @access public
- * @static
- * @return string
- * @param array $array PHP array
- * @param int $indent Pass in false to use the default, which is 2
- * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40)
- */
- public static function YAMLDump($array,$indent = false,$wordwrap = false) {
- $spyc = new Spyc;
- return $spyc->dump($array,$indent,$wordwrap);
- }
-
- /**
- * Load YAML into a PHP array from an instantiated object
- *
- * The load method, when supplied with a YAML stream (string or file path),
- * will do its best to convert the YAML into a PHP array. Pretty simple.
- * Usage:
- * <code>
- * $parser = new Spyc;
- * $array = $parser->load('lucky.yml');
- * print_r($array);
- * </code>
- * @access public
- * @return array
- * @param string $input Path of YAML file or string containing YAML
- */
- function load($input) {
- // See what type of input we're talking about
- // If it's not a file, assume it's a string
- if (!empty($input) && (strpos($input, "\n") === false)
- && file_exists($input)) {
- $yaml = file($input);
- } else {
- $yaml = explode("\n",$input);
- }
- // Initiate some objects and values
- $base = new YAMLNode;
- $base->indent = 0;
- $this->_lastIndent = 0;
- $this->_lastNode = $base->id;
- $this->_inBlock = false;
- $this->_isInline = false;
-
- foreach ($yaml as $linenum => $line) {
- $ifchk = trim($line);
-
- // If the line starts with a tab (instead of a space), throw a fit.
- if (preg_match('/^(\t)+(\w+)/', $line)) {
- $err = 'ERROR: Line '. ($linenum + 1) .' in your input YAML begins'.
- ' with a tab. YAML only recognizes spaces. Please reformat.';
- die($err);
- }
-
- if ($this->_inBlock === false && empty($ifchk)) {
- continue;
- } elseif ($this->_inBlock == true && empty($ifchk)) {
- $last =& $this->_allNodes[$this->_lastNode];
- $last->data[key($last->data)] .= "\n";
- } elseif ($ifchk{0} != '#' && substr($ifchk,0,3) != '---') {
- // Create a new node and get its indent
- $node = new YAMLNode;
- $node->indent = $this->_getIndent($line);
-
- // Check where the node lies in the hierarchy
- if ($this->_lastIndent == $node->indent) {
- // If we're in a block, add the text to the parent's data
- if ($this->_inBlock === true) {
- $parent =& $this->_allNodes[$this->_lastNode];
- $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd;
- } else {
- // The current node's parent is the same as the previous node's
- if (isset($this->_allNodes[$this->_lastNode])) {
- $node->parent = $this->_allNodes[$this->_lastNode]->parent;
- }
- }
- } elseif ($this->_lastIndent < $node->indent) {
- if ($this->_inBlock === true) {
- $parent =& $this->_allNodes[$this->_lastNode];
- $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd;
- } elseif ($this->_inBlock === false) {
- // The current node's parent is the previous node
- $node->parent = $this->_lastNode;
-
- // If the value of the last node's data was > or | we need to
- // start blocking i.e. taking in all lines as a text value until
- // we drop our indent.
- $parent =& $this->_allNodes[$node->parent];
- $this->_allNodes[$node->parent]->children = true;
- if (is_array($parent->data)) {
- $chk = $parent->data[key($parent->data)];
- if ($chk === '>') {
- $this->_inBlock = true;
- $this->_blockEnd = ' ';
- $parent->data[key($parent->data)] =
- str_replace('>','',$parent->data[key($parent->data)]);
- $parent->data[key($parent->data)] .= trim($line).' ';
- $this->_allNodes[$node->parent]->children = false;
- $this->_lastIndent = $node->indent;
- } elseif ($chk === '|') {
- $this->_inBlock = true;
- $this->_blockEnd = "\n";
- $parent->data[key($parent->data)] =
- str_replace('|','',$parent->data[key($parent->data)]);
- $parent->data[key($parent->data)] .= trim($line)."\n";
- $this->_allNodes[$node->parent]->children = false;
- $this->_lastIndent = $node->indent;
- }
- }
- }
- } elseif ($this->_lastIndent > $node->indent) {
- // Any block we had going is dead now
- if ($this->_inBlock === true) {
- $this->_inBlock = false;
- if ($this->_blockEnd = "\n") {
- $last =& $this->_allNodes[$this->_lastNode];
- $last->data[key($last->data)] =
- trim($last->data[key($last->data)]);
- }
- }
-
- // We don't know the parent of the node so we have to find it
- // foreach ($this->_allNodes as $n) {
- foreach ($this->_indentSort[$node->indent] as $n) {
- if ($n->indent == $node->indent) {
- $node->parent = $n->parent;
- }
- }
- }
-
- if ($this->_inBlock === false) {
- // Set these properties with information from our current node
- $this->_lastIndent = $node->indent;
- // Set the last node
- $this->_lastNode = $node->id;
- // Parse the YAML line and return its data
- $node->data = $this->_parseLine($line);
- // Add the node to the master list
- $this->_allNodes[$node->id] = $node;
- // Add a reference to the node in an indent array
- $this->_indentSort[$node->indent][] =& $this->_allNodes[$node->id];
- // Add a reference to the node in a References array if this node
- // has a YAML reference in it.
- if (
- ( (is_array($node->data)) &&
- isset($node->data[key($node->data)]) &&
- (!is_array($node->data[key($node->data)])) )
- &&
- ( (preg_match('/^&([^ ]+)/',$node->data[key($node->data)]))
- ||
- (preg_match('/^\*([^ ]+)/',$node->data[key($node->data)])) )
- ) {
- $this->_haveRefs[] =& $this->_allNodes[$node->id];
- } elseif (
- ( (is_array($node->data)) &&
- isset($node->data[key($node->data)]) &&
- (is_array($node->data[key($node->data)])) )
- ) {
- // Incomplete reference making code. Ugly, needs cleaned up.
- foreach ($node->data[key($node->data)] as $d) {
- if ( !is_array($d) &&
- ( (preg_match('/^&([^ ]+)/',$d))
- ||
- (preg_match('/^\*([^ ]+)/',$d)) )
- ) {
- $this->_haveRefs[] =& $this->_allNodes[$node->id];
- }
- }
- }
- }
- }
- }
- unset($node);
-
- // Here we travel through node-space and pick out references (& and *)
- $this->_linkReferences();
-
- // Build the PHP array out of node-space
- $trunk = $this->_buildArray();
- return $trunk;
- }
-
- /**
- * Dump PHP array to YAML
- *
- * The dump method, when supplied with an array, will do its best
- * to convert the array into friendly YAML. Pretty simple. Feel free to
- * save the returned string as tasteful.yml and pass it around.
- *
- * Oh, and you can decide how big the indent is and what the wordwrap
- * for folding is. Pretty cool -- just pass in 'false' for either if
- * you want to use the default.
- *
- * Indent's default is 2 spaces, wordwrap's default is 40 characters. And
- * you can turn off wordwrap by passing in 0.
- *
- * @access public
- * @return string
- * @param array $array PHP array
- * @param int $indent Pass in false to use the default, which is 2
- * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40)
- */
- function dump($array,$indent = false,$wordwrap = false) {
- // Dumps to some very clean YAML. We'll have to add some more features
- // and options soon. And better support for folding.
-
- // New features and options.
- if ($indent === false or !is_numeric($indent)) {
- $this->_dumpIndent = 2;
- } else {
- $this->_dumpIndent = $indent;
- }
-
- if ($wordwrap === false or !is_numeric($wordwrap)) {
- $this->_dumpWordWrap = 40;
- } else {
- $this->_dumpWordWrap = $wordwrap;
- }
-
- // New YAML document
- $string = "---\n";
-
- // Start at the base of the array and move through it.
- foreach ($array as $key => $value) {
- $string .= $this->_yamlize($key,$value,0);
- }
- return $string;
- }
-
- /**** Private Properties ****/
-
- /**#@+
- * @access private
- * @var mixed
- */
- var $_haveRefs;
- var $_allNodes;
- var $_lastIndent;
- var $_lastNode;
- var $_inBlock;
- var $_isInline;
- var $_dumpIndent;
- var $_dumpWordWrap;
- /**#@-*/
-
- /**** Private Methods ****/
-
- /**
- * Attempts to convert a key / value array item to YAML
- * @access private
- * @return string
- * @param $key The name of the key
- * @param $value The value of the item
- * @param $indent The indent of the current node
- */
- function _yamlize($key,$value,$indent) {
- if (is_array($value)) {
- // It has children. What to do?
- // Make it the right kind of item
- $string = $this->_dumpNode($key,NULL,$indent);
- // Add the indent
- $indent += $this->_dumpIndent;
- // Yamlize the array
- $string .= $this->_yamlizeArray($value,$indent);
- } elseif (!is_array($value)) {
- // It doesn't have children. Yip.
- $string = $this->_dumpNode($key,$value,$indent);
- }
- return $string;
- }
-
- /**
- * Attempts to convert an array to YAML
- * @access private
- * @return string
- * @param $array The array you want to convert
- * @param $indent The indent of the current level
- */
- function _yamlizeArray($array,$indent) {
- if (is_array($array)) {
- $string = '';
- foreach ($array as $key => $value) {
- $string .= $this->_yamlize($key,$value,$indent);
- }
- return $string;
- } else {
- return false;
- }
- }
-
- /**
- * Find out whether a string needs to be output as a literal rather than in plain style.
- * Added by Roan Kattouw 13-03-2008
- * @param $value The string to check
- * @return bool
- */
- function _needLiteral($value) {
- # Check whether the string contains # or : or begins with any of:
- # [ - ? , [ ] { } ! * & | > ' " % @ ` ]
- # or is a number or contains newlines
- return (bool)(gettype($value) == "string" &&
- (is_numeric($value) ||
- strpos($value, "\n") ||
- preg_match("/[#:]/", $value) ||
- preg_match("/^[-?,[\]{}!*&|>'\"%@`]/", $value)));
-
- }
-
- /**
- * Returns YAML from a key and a value
- * @access private
- * @return string
- * @param $key The name of the key
- * @param $value The value of the item
- * @param $indent The indent of the current node
- */
- function _dumpNode($key,$value,$indent) {
- // do some folding here, for blocks
- if ($this->_needLiteral($value)) {
- $value = $this->_doLiteralBlock($value,$indent);
- } else {
- $value = $this->_doFolding($value,$indent);
- }
-
- $spaces = str_repeat(' ',$indent);
-
- if (is_int($key)) {
- // It's a sequence
- if ($value)
- $string = $spaces.'- '.$value."\n";
- else
- $string = $spaces . "-\n";
- } else {
- // It's mapped
- if ($value)
- $string = $spaces.$key.': '.$value."\n";
- else
- $string = $spaces . $key . ":\n";
- }
- return $string;
- }
-
- /**
- * Creates a literal block for dumping
- * @access private
- * @return string
- * @param $value
- * @param $indent int The value of the indent
- */
- function _doLiteralBlock($value,$indent) {
- $exploded = explode("\n",$value);
- $newValue = '|';
- $indent += $this->_dumpIndent;
- $spaces = str_repeat(' ',$indent);
- foreach ($exploded as $line) {
- $newValue .= "\n" . $spaces . trim($line);
- }
- return $newValue;
- }
-
- /**
- * Folds a string of text, if necessary
- * @access private
- * @return string
- * @param $value The string you wish to fold
- */
- function _doFolding($value,$indent) {
- // Don't do anything if wordwrap is set to 0
- if ($this->_dumpWordWrap === 0) {
- return $value;
- }
-
- if (strlen($value) > $this->_dumpWordWrap) {
- $indent += $this->_dumpIndent;
- $indent = str_repeat(' ',$indent);
- $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent");
- $value = ">\n".$indent.$wrapped;
- }
- return $value;
- }
-
- /* Methods used in loading */
-
- /**
- * Finds and returns the indentation of a YAML line
- * @access private
- * @return int
- * @param string $line A line from the YAML file
- */
- function _getIndent($line) {
- $match = array();
- preg_match('/^\s{1,}/',$line,$match);
- if (!empty($match[0])) {
- $indent = substr_count($match[0],' ');
- } else {
- $indent = 0;
- }
- return $indent;
- }
-
- /**
- * Parses YAML code and returns an array for a node
- * @access private
- * @return array
- * @param string $line A line from the YAML file
- */
- function _parseLine($line) {
- $line = trim($line);
-
- $array = array();
-
- if (preg_match('/^-(.*):$/',$line)) {
- // It's a mapped sequence
- $key = trim(substr(substr($line,1),0,-1));
- $array[$key] = '';
- } elseif ($line[0] == '-' && substr($line,0,3) != '---') {
- // It's a list item but not a new stream
- if (strlen($line) > 1) {
- $value = trim(substr($line,1));
- // Set the type of the value. Int, string, etc
- $value = $this->_toType($value);
- $array[] = $value;
- } else {
- $array[] = array();
- }
- } elseif (preg_match('/^(.+):/',$line,$key)) {
- // It's a key/value pair most likely
- // If the key is in double quotes pull it out
- $matches = array();
- if (preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) {
- $value = trim(str_replace($matches[1],'',$line));
- $key = $matches[2];
- } else {
- // Do some guesswork as to the key and the value
- $explode = explode(':',$line);
- $key = trim($explode[0]);
- array_shift($explode);
- $value = trim(implode(':',$explode));
- }
-
- // Set the type of the value. Int, string, etc
- $value = $this->_toType($value);
- if (empty($key)) {
- $array[] = $value;
- } else {
- $array[$key] = $value;
- }
- }
- return $array;
- }
-
- /**
- * Finds the type of the passed value, returns the value as the new type.
- * @access private
- * @param string $value
- * @return mixed
- */
- function _toType($value) {
- $matches = array();
- if (preg_match('/^("(.*)"|\'(.*)\')/',$value,$matches)) {
- $value = (string)preg_replace('/(\'\'|\\\\\')/',"'",end($matches));
- $value = preg_replace('/\\\\"/','"',$value);
- } elseif (preg_match('/^\\[(.+)\\]$/',$value,$matches)) {
- // Inline Sequence
-
- // Take out strings sequences and mappings
- $explode = $this->_inlineEscape($matches[1]);
-
- // Propogate value array
- $value = array();
- foreach ($explode as $v) {
- $value[] = $this->_toType($v);
- }
- } elseif (strpos($value,': ')!==false && !preg_match('/^{(.+)/',$value)) {
- // It's a map
- $array = explode(': ',$value);
- $key = trim($array[0]);
- array_shift($array);
- $value = trim(implode(': ',$array));
- $value = $this->_toType($value);
- $value = array($key => $value);
- } elseif (preg_match("/{(.+)}$/",$value,$matches)) {
- // Inline Mapping
-
- // Take out strings sequences and mappings
- $explode = $this->_inlineEscape($matches[1]);
-
- // Propogate value array
- $array = array();
- foreach ($explode as $v) {
- $array = $array + $this->_toType($v);
- }
- $value = $array;
- } elseif (strtolower($value) == 'null' or $value == '' or $value == '~') {
- $value = NULL;
- } elseif (ctype_digit($value)) {
- $value = (int)$value;
- } elseif (in_array(strtolower($value),
- array('true', 'on', '+', 'yes', 'y'))) {
- $value = TRUE;
- } elseif (in_array(strtolower($value),
- array('false', 'off', '-', 'no', 'n'))) {
- $value = FALSE;
- } elseif (is_numeric($value)) {
- $value = (float)$value;
- } else {
- // Just a normal string, right?
- $value = trim(preg_replace('/#(.+)$/','',$value));
- }
-
- return $value;
- }
-
- /**
- * Used in inlines to check for more inlines or quoted strings
- * @access private
- * @return array
- */
- function _inlineEscape($inline) {
- // There's gotta be a cleaner way to do this...
- // While pure sequences seem to be nesting just fine,
- // pure mappings and mappings with sequences inside can't go very
- // deep. This needs to be fixed.
-
- // Check for strings
- $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/';
- $strings = array();
- if (preg_match_all($regex,$inline,$strings)) {
- $saved_strings[] = $strings[0][0];
- $inline = preg_replace($regex,'YAMLString',$inline);
- }
- unset($regex);
-
- // Check for sequences
- $seqs = array();
- if (preg_match_all('/\[(.+)\]/U',$inline,$seqs)) {
- $inline = preg_replace('/\[(.+)\]/U','YAMLSeq',$inline);
- $seqs = $seqs[0];
- }
-
- // Check for mappings
- $maps = array();
- if (preg_match_all('/{(.+)}/U',$inline,$maps)) {
- $inline = preg_replace('/{(.+)}/U','YAMLMap',$inline);
- $maps = $maps[0];
- }
-
- $explode = explode(', ',$inline);
-
- // Re-add the strings
- if (!empty($saved_strings)) {
- $i = 0;
- foreach ($explode as $key => $value) {
- if (strpos($value,'YAMLString')) {
- $explode[$key] = str_replace('YAMLString',$saved_strings[$i],$value);
- ++$i;
- }
- }
- }
-
- // Re-add the sequences
- if (!empty($seqs)) {
- $i = 0;
- foreach ($explode as $key => $value) {
- if (strpos($value,'YAMLSeq') !== false) {
- $explode[$key] = str_replace('YAMLSeq',$seqs[$i],$value);
- ++$i;
- }
- }
- }
-
- // Re-add the mappings
- if (!empty($maps)) {
- $i = 0;
- foreach ($explode as $key => $value) {
- if (strpos($value,'YAMLMap') !== false) {
- $explode[$key] = str_replace('YAMLMap',$maps[$i],$value);
- ++$i;
- }
- }
- }
-
- return $explode;
- }
-
- /**
- * Builds the PHP array from all the YAML nodes we've gathered
- * @access private
- * @return array
- */
- function _buildArray() {
- $trunk = array();
-
- if (!isset($this->_indentSort[0])) {
- return $trunk;
- }
-
- foreach ($this->_indentSort[0] as $n) {
- if (empty($n->parent)) {
- $this->_nodeArrayizeData($n);
- // Check for references and copy the needed data to complete them.
- $this->_makeReferences($n);
- // Merge our data with the big array we're building
- $trunk = $this->_array_kmerge($trunk,$n->data);
- }
- }
-
- return $trunk;
- }
-
- /**
- * Traverses node-space and sets references (& and *) accordingly
- * @access private
- * @return bool
- */
- function _linkReferences() {
- if (is_array($this->_haveRefs)) {
- foreach ($this->_haveRefs as $node) {
- if (!empty($node->data)) {
- $key = key($node->data);
- // If it's an array, don't check.
- if (is_array($node->data[$key])) {
- foreach ($node->data[$key] as $k => $v) {
- $this->_linkRef($node,$key,$k,$v);
- }
- } else {
- $this->_linkRef($node,$key);
- }
- }
- }
- }
- return true;
- }
-
- function _linkRef(&$n,$key,$k = NULL,$v = NULL) {
- if (empty($k) && empty($v)) {
- // Look for &refs
- $matches = array();
- if (preg_match('/^&([^ ]+)/',$n->data[$key],$matches)) {
- // Flag the node so we know it's a reference
- $this->_allNodes[$n->id]->ref = substr($matches[0],1);
- $this->_allNodes[$n->id]->data[$key] =
- substr($n->data[$key],strlen($matches[0])+1);
- // Look for *refs
- } elseif (preg_match('/^\*([^ ]+)/',$n->data[$key],$matches)) {
- $ref = substr($matches[0],1);
- // Flag the node as having a reference
- $this->_allNodes[$n->id]->refKey = $ref;
- }
- } elseif (!empty($k) && !empty($v)) {
- if (preg_match('/^&([^ ]+)/',$v,$matches)) {
- // Flag the node so we know it's a reference
- $this->_allNodes[$n->id]->ref = substr($matches[0],1);
- $this->_allNodes[$n->id]->data[$key][$k] =
- substr($v,strlen($matches[0])+1);
- // Look for *refs
- } elseif (preg_match('/^\*([^ ]+)/',$v,$matches)) {
- $ref = substr($matches[0],1);
- // Flag the node as having a reference
- $this->_allNodes[$n->id]->refKey = $ref;
- }
- }
- }
-
- /**
- * Finds the children of a node and aids in the building of the PHP array
- * @access private
- * @param int $nid The id of the node whose children we're gathering
- * @return array
- */
- function _gatherChildren($nid) {
- $return = array();
- $node =& $this->_allNodes[$nid];
- foreach ($this->_allNodes as $z) {
- if ($z->parent == $node->id) {
- // We found a child
- $this->_nodeArrayizeData($z);
- // Check for references
- $this->_makeReferences($z);
- // Merge with the big array we're returning
- // The big array being all the data of the children of our parent node
- $return = $this->_array_kmerge($return,$z->data);
- }
- }
- return $return;
- }
-
- /**
- * Turns a node's data and its children's data into a PHP array
- *
- * @access private
- * @param array $node The node which you want to arrayize
- * @return boolean
- */
- function _nodeArrayizeData(&$node) {
- if (is_array($node->data) && $node->children == true) {
- // This node has children, so we need to find them
- $childs = $this->_gatherChildren($node->id);
- // We've gathered all our children's data and are ready to use it
- $key = key($node->data);
- $key = empty($key) ? 0 : $key;
- // If it's an array, add to it of course
- if (is_array($node->data[$key])) {
- $node->data[$key] = $this->_array_kmerge($node->data[$key],$childs);
- } else {
- $node->data[$key] = $childs;
- }
- } elseif (!is_array($node->data) && $node->children == true) {
- // Same as above, find the children of this node
- $childs = $this->_gatherChildren($node->id);
- $node->data = array();
- $node->data[] = $childs;
- }
-
- // We edited $node by reference, so just return true
- return true;
- }
-
- /**
- * Traverses node-space and copies references to / from this object.
- * @access private
- * @param object $z A node whose references we wish to make real
- * @return bool
- */
- function _makeReferences(&$z) {
- // It is a reference
- if (isset($z->ref)) {
- $key = key($z->data);
- // Copy the data to this object for easy retrieval later
- $this->ref[$z->ref] =& $z->data[$key];
- // It has a reference
- } elseif (isset($z->refKey)) {
- if (isset($this->ref[$z->refKey])) {
- $key = key($z->data);
- // Copy the data from this object to make the node a real reference
- $z->data[$key] =& $this->ref[$z->refKey];
- }
- }
- return true;
- }
-
-
- /**
- * Merges arrays and maintains numeric keys.
- *
- * An ever-so-slightly modified version of the array_kmerge() function posted
- * to php.net by mail at nospam dot iaindooley dot com on 2004-04-08.
- *
- * http://www.php.net/manual/en/function.array-merge.php#41394
- *
- * @access private
- * @param array $arr1
- * @param array $arr2
- * @return array
- */
- function _array_kmerge($arr1,$arr2) {
- if(!is_array($arr1))
- $arr1 = array();
-
- if(!is_array($arr2))
- $arr2 = array();
-
- $keys1 = array_keys($arr1);
- $keys2 = array_keys($arr2);
- $keys = array_merge($keys1,$keys2);
- $vals1 = array_values($arr1);
- $vals2 = array_values($arr2);
- $vals = array_merge($vals1,$vals2);
- $ret = array();
-
- foreach($keys as $key) {
- list( /* unused */ ,$val) = each($vals);
- // This is the good part! If a key already exists, but it's part of a
- // sequence (an int), just keep addin numbers until we find a fresh one.
- if (isset($ret[$key]) and is_int($key)) {
- while (array_key_exists($key, $ret)) {
- $key++;
- }
- }
- $ret[$key] = $val;
- }
-
- return $ret;
- }
- }
+/**
+ * Spyc -- A Simple PHP YAML Class
+ * @version 0.2.3 -- 2006-02-04
+ * @author Chris Wanstrath <chris@ozmm.org>
+ * @see http://spyc.sourceforge.net/
+ * @copyright Copyright 2005-2006 Chris Wanstrath
+ * @license http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+/**
+ * The Simple PHP YAML Class.
+ *
+ * This class can be used to read a YAML file and convert its contents
+ * into a PHP array. It currently supports a very limited subsection of
+ * the YAML spec.
+ *
+ * @ingroup API
+ */
+class Spyc {
+
+ /**
+ * Dump YAML from PHP array statically
+ *
+ * The dump method, when supplied with an array, will do its best
+ * to convert the array into friendly YAML. Pretty simple. Feel free to
+ * save the returned string as nothing.yml and pass it around.
+ *
+ * Oh, and you can decide how big the indent is and what the wordwrap
+ * for folding is. Pretty cool -- just pass in 'false' for either if
+ * you want to use the default.
+ *
+ * Indent's default is 2 spaces, wordwrap's default is 40 characters. And
+ * you can turn off wordwrap by passing in 0.
+ *
+ * @return string
+ * @param $array Array: PHP array
+ * @param $indent Integer: Pass in false to use the default, which is 2
+ * @param $wordwrap Integer: Pass in 0 for no wordwrap, false for default (40)
+ */
+ public static function YAMLDump($array,$indent = false,$wordwrap = false) {
+ $spyc = new Spyc;
+ return $spyc->dump($array,$indent,$wordwrap);
+ }
+
+ /**
+ * Dump PHP array to YAML
+ *
+ * The dump method, when supplied with an array, will do its best
+ * to convert the array into friendly YAML. Pretty simple. Feel free to
+ * save the returned string as tasteful.yml and pass it around.
+ *
+ * Oh, and you can decide how big the indent is and what the wordwrap
+ * for folding is. Pretty cool -- just pass in 'false' for either if
+ * you want to use the default.
+ *
+ * Indent's default is 2 spaces, wordwrap's default is 40 characters. And
+ * you can turn off wordwrap by passing in 0.
+ *
+ * @public
+ * @return string
+ * @param $array Array: PHP array
+ * @param $indent Integer: Pass in false to use the default, which is 2
+ * @param $wordwrap Integer: Pass in 0 for no wordwrap, false for default (40)
+ */
+ function dump($array,$indent = false,$wordwrap = false) {
+ // Dumps to some very clean YAML. We'll have to add some more features
+ // and options soon. And better support for folding.
+
+ // New features and options.
+ if ($indent === false or !is_numeric($indent)) {
+ $this->_dumpIndent = 2;
+ } else {
+ $this->_dumpIndent = $indent;
+ }
+
+ if ($wordwrap === false or !is_numeric($wordwrap)) {
+ $this->_dumpWordWrap = 40;
+ } else {
+ $this->_dumpWordWrap = $wordwrap;
+ }
+
+ // New YAML document
+ $string = "---\n";
+
+ // Start at the base of the array and move through it.
+ foreach ($array as $key => $value) {
+ $string .= $this->_yamlize($key,$value,0);
+ }
+ return $string;
+ }
+
+ /**** Private Properties ****/
+
+ private $_haveRefs;
+ private $_allNodes;
+ private $_lastIndent;
+ private $_lastNode;
+ private $_inBlock;
+ private $_isInline;
+ private $_dumpIndent;
+ private $_dumpWordWrap;
+
+ /**** Private Methods ****/
+
+ /**
+ * Attempts to convert a key / value array item to YAML
+ * @return string
+ * @param $key The name of the key
+ * @param $value The value of the item
+ * @param $indent The indent of the current node
+ */
+ private function _yamlize($key,$value,$indent) {
+ if (is_array($value)) {
+ // It has children. What to do?
+ // Make it the right kind of item
+ $string = $this->_dumpNode($key,NULL,$indent);
+ // Add the indent
+ $indent += $this->_dumpIndent;
+ // Yamlize the array
+ $string .= $this->_yamlizeArray($value,$indent);
+ } elseif (!is_array($value)) {
+ // It doesn't have children. Yip.
+ $string = $this->_dumpNode($key,$value,$indent);
+ }
+ return $string;
+ }
+
+ /**
+ * Attempts to convert an array to YAML
+ * @return string
+ * @param $array The array you want to convert
+ * @param $indent The indent of the current level
+ */
+ private function _yamlizeArray($array,$indent) {
+ if (is_array($array)) {
+ $string = '';
+ foreach ($array as $key => $value) {
+ $string .= $this->_yamlize($key,$value,$indent);
+ }
+ return $string;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Find out whether a string needs to be output as a literal rather than in plain style.
+ * Added by Roan Kattouw 13-03-2008
+ * @param $value The string to check
+ * @return bool
+ */
+ function _needLiteral($value) {
+ # Check whether the string contains # or : or begins with any of:
+ # [ - ? , [ ] { } ! * & | > ' " % @ ` ]
+ # or is a number or contains newlines
+ return (bool)(gettype($value) == "string" &&
+ (is_numeric($value) ||
+ strpos($value, "\n") ||
+ preg_match("/[#:]/", $value) ||
+ preg_match("/^[-?,[\]{}!*&|>'\"%@`]/", $value)));
+
+ }
+
+ /**
+ * Returns YAML from a key and a value
+ * @return string
+ * @param $key The name of the key
+ * @param $value The value of the item
+ * @param $indent The indent of the current node
+ */
+ private function _dumpNode($key,$value,$indent) {
+ // do some folding here, for blocks
+ if ($this->_needLiteral($value)) {
+ $value = $this->_doLiteralBlock($value,$indent);
+ } else {
+ $value = $this->_doFolding($value,$indent);
+ }
+
+ $spaces = str_repeat(' ',$indent);
+
+ if (is_int($key)) {
+ // It's a sequence
+ if ($value !== '' && !is_null($value))
+ $string = $spaces.'- '.$value."\n";
+ else
+ $string = $spaces . "-\n";
+ } else {
+ // It's mapped
+ if ($value !== '' && !is_null($value))
+ $string = $spaces . $key . ': ' . $value . "\n";
+ else
+ $string = $spaces . $key . ":\n";
+ }
+ return $string;
+ }
+
+ /**
+ * Creates a literal block for dumping
+ * @return string
+ * @param $value
+ * @param $indent int The value of the indent
+ */
+ private function _doLiteralBlock($value,$indent) {
+ $exploded = explode("\n",$value);
+ $newValue = '|';
+ $indent += $this->_dumpIndent;
+ $spaces = str_repeat(' ',$indent);
+ foreach ($exploded as $line) {
+ $newValue .= "\n" . $spaces . trim($line);
+ }
+ return $newValue;
+ }
+
+ /**
+ * Folds a string of text, if necessary
+ * @return string
+ * @param $value The string you wish to fold
+ */
+ private function _doFolding($value,$indent) {
+ // Don't do anything if wordwrap is set to 0
+ if ($this->_dumpWordWrap === 0) {
+ return $value;
+ }
+
+ if (strlen($value) > $this->_dumpWordWrap) {
+ $indent += $this->_dumpIndent;
+ $indent = str_repeat(' ',$indent);
+ $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent");
+ $value = ">\n".$indent.$wrapped;
+ }
+ return $value;
+ }
+}
diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php
index a45390c4..43b30f7c 100644
--- a/includes/api/ApiLogin.php
+++ b/includes/api/ApiLogin.php
@@ -36,23 +36,6 @@ if (!defined('MEDIAWIKI')) {
*/
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 = 5;
-
- /**
- * 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');
}
@@ -61,7 +44,7 @@ class ApiLogin extends ApiBase {
* 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
+ * log-in fails, as the result of a bad password, a nonexistent
* 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.
@@ -69,25 +52,14 @@ class ApiLogin extends ApiBase {
* @access public
*/
public function execute() {
- $name = $password = $domain = null;
- extract($this->extractRequestParams());
+ $params = $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,
- 'wpDomain' => $domain,
+ $req = new FauxRequest(array (
+ 'wpName' => $params['name'],
+ 'wpPassword' => $params['password'],
+ 'wpDomain' => $params['domain'],
'wpRemember' => ''
));
@@ -96,8 +68,8 @@ class ApiLogin extends ApiBase {
wfSetupSession();
}
- $loginForm = new LoginForm($params);
- switch ($loginForm->authenticateUserData()) {
+ $loginForm = new LoginForm($req);
+ switch ($authRes = $loginForm->authenticateUserData()) {
case LoginForm :: SUCCESS :
global $wgUser, $wgCookiePrefix;
@@ -139,95 +111,18 @@ class ApiLogin extends ApiBase {
$result['result'] = 'CreateBlocked';
$result['details'] = 'Your IP address is blocked from account creation';
break;
+ case LoginForm :: THROTTLED :
+ global $wgPasswordAttemptThrottle;
+ $result['result'] = 'Throttled';
+ $result['wait'] = $wgPasswordAttemptThrottle['seconds'];
+ break;
default :
- ApiBase :: dieDebug(__METHOD__, 'Unhandled case value');
- }
-
- if ($result['result'] != 'Success' && !isset( $result['details'] ) ) {
- $delay = $this->cacheBadLogin();
- $result['wait'] = $delay;
- $result['details'] = "Please wait " . $delay . " seconds before next log-in attempt";
+ ApiBase :: dieDebug(__METHOD__, "Unhandled case value: {$authRes}");
}
- // 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() );
- }
-
public function mustBePosted() { return true; }
public function getAllowedParams() {
@@ -263,6 +158,6 @@ class ApiLogin extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiLogin.php 35565 2008-05-29 19:23:37Z btongminh $';
+ return __CLASS__ . ': $Id: ApiLogin.php 45275 2009-01-01 02:02:03Z simetrical $';
}
}
diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php
index 694c9e3c..8b178f6a 100644
--- a/includes/api/ApiLogout.php
+++ b/includes/api/ApiLogout.php
@@ -42,11 +42,12 @@ class ApiLogout extends ApiBase {
public function execute() {
global $wgUser;
+ $oldName = $wgUser->getName();
$wgUser->logout();
// Give extensions to do something after user logout
$injected_html = '';
- wfRunHooks( 'UserLogoutComplete', array(&$wgUser, &$injected_html) );
+ wfRunHooks( 'UserLogoutComplete', array(&$wgUser, &$injected_html, $oldName) );
}
public function getAllowedParams() {
@@ -70,6 +71,6 @@ class ApiLogout extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiLogout.php 35294 2008-05-24 20:44:49Z btongminh $';
+ return __CLASS__ . ': $Id: ApiLogout.php 43522 2008-11-15 01:23:39Z brion $';
}
}
diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php
index 2d0e278c..60d932be 100644
--- a/includes/api/ApiMain.php
+++ b/includes/api/ApiMain.php
@@ -65,6 +65,7 @@ class ApiMain extends ApiBase {
'feedwatchlist' => 'ApiFeedWatchlist',
'help' => 'ApiHelp',
'paraminfo' => 'ApiParamInfo',
+ 'purge' => 'ApiPurge',
);
private static $WriteModules = array (
@@ -77,6 +78,8 @@ class ApiMain extends ApiBase {
'move' => 'ApiMove',
'edit' => 'ApiEditPage',
'emailuser' => 'ApiEmailUser',
+ 'watch' => 'ApiWatch',
+ 'patrol' => 'ApiPatrol',
);
/**
@@ -99,6 +102,23 @@ class ApiMain extends ApiBase {
'dbg' => 'ApiFormatDbg',
'dbgfm' => 'ApiFormatDbg'
);
+
+ /**
+ * List of user roles that are specifically relevant to the API.
+ * array( 'right' => array ( 'msg' => 'Some message with a $1',
+ * 'params' => array ( $someVarToSubst ) ),
+ * );
+ */
+ private static $mRights = array('writeapi' => array(
+ 'msg' => 'Use of the write API',
+ 'params' => array()
+ ),
+ 'apihighlimits' => array(
+ 'msg' => 'Use higher limits in API queries (Slow queries: $1 results; Fast queries: $2 results). The limits for slow queries also apply to multivalue parameters.',
+ 'params' => array (ApiMain::LIMIT_SML2, ApiMain::LIMIT_BIG2)
+ )
+ );
+
private $mPrinter, $mModules, $mModuleNames, $mFormats, $mFormatNames;
private $mResult, $mAction, $mShowVersions, $mEnableWrite, $mRequest, $mInternalMode, $mSquidMaxage;
@@ -144,9 +164,9 @@ class ApiMain extends ApiBase {
if($wgEnableWriteAPI)
$this->mModules += self::$WriteModules;
- $this->mModuleNames = array_keys($this->mModules); // todo: optimize
+ $this->mModuleNames = array_keys($this->mModules);
$this->mFormats = self :: $Formats;
- $this->mFormatNames = array_keys($this->mFormats); // todo: optimize
+ $this->mFormatNames = array_keys($this->mFormats);
$this->mResult = new ApiResult($this);
$this->mShowVersions = false;
@@ -193,6 +213,8 @@ class ApiMain extends ApiBase {
if (!$wgUser->isAllowed('writeapi'))
$this->dieUsage('You\'re not allowed to edit this ' .
'wiki through the API', 'writeapidenied');
+ if (wfReadOnly())
+ $this->dieUsageMsg(array('readonlytext'));
}
/**
@@ -206,6 +228,8 @@ class ApiMain extends ApiBase {
* Create an instance of an output formatter by its name
*/
public function createPrinterByName($format) {
+ if( !isset( $this->mFormats[$format] ) )
+ $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
return new $this->mFormats[$format] ($this, $format);
}
@@ -235,6 +259,11 @@ class ApiMain extends ApiBase {
try {
$this->executeAction();
} catch (Exception $e) {
+ // Log it
+ if ( $e instanceof MWException ) {
+ wfDebugLog( 'exception', $e->getLogMessage() );
+ }
+
//
// Handle any kind of exception by outputing properly formatted error message.
// If this fails, an unhandled exception should be thrown so that global error
@@ -248,7 +277,7 @@ class ApiMain extends ApiBase {
$headerStr = 'MediaWiki-API-Error: ' . $errCode;
if ($e->getCode() === 0)
- header($headerStr, true);
+ header($headerStr);
else
header($headerStr, true, $e->getCode());
@@ -260,12 +289,11 @@ class ApiMain extends ApiBase {
$this->printResult(true);
}
- global $wgRequest;
if($this->mSquidMaxage == -1)
{
# Nobody called setCacheMaxAge(), use the (s)maxage parameters
- $smaxage = $wgRequest->getVal('smaxage', 0);
- $maxage = $wgRequest->getVal('maxage', 0);
+ $smaxage = $this->getParameter('smaxage');
+ $maxage = $this->getParameter('maxage');
}
else
$smaxage = $maxage = $this->mSquidMaxage;
@@ -332,6 +360,9 @@ class ApiMain extends ApiBase {
}
$this->getResult()->reset();
+ // Re-add the id
+ if($this->mRequest->getCheck('requestid'))
+ $this->getResult()->addValue(null, 'requestid', $this->mRequest->getVal('requestid'));
$this->getResult()->addValue(null, 'error', $errMessage);
return $errMessage['code'];
@@ -341,12 +372,19 @@ class ApiMain extends ApiBase {
* Execute the actual module, without any error handling
*/
protected function executeAction() {
+ // First add the id to the top element
+ if($this->mRequest->getCheck('requestid'))
+ $this->getResult()->addValue(null, 'requestid', $this->mRequest->getVal('requestid'));
$params = $this->extractRequestParams();
$this->mShowVersions = $params['version'];
$this->mAction = $params['action'];
+ if( !is_string( $this->mAction ) ) {
+ $this->dieUsage( "The API requires a valid action parameter", 'unknown_action' );
+ }
+
// Instantiate the module requested by the user
$module = new $this->mModules[$this->mAction] ($this, $this->mAction);
@@ -356,6 +394,9 @@ class ApiMain extends ApiBase {
$maxLag = $params['maxlag'];
list( $host, $lag ) = wfGetLB()->getMaxLag();
if ( $lag > $maxLag ) {
+ header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
+ header( 'X-Database-Lag: ' . intval( $lag ) );
+ // XXX: should we return a 503 HTTP error code like wfMaxlagError() does?
if( $wgShowHostnames ) {
ApiBase :: dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
} else {
@@ -384,6 +425,7 @@ class ApiMain extends ApiBase {
// Execute
$module->profileIn();
$module->execute();
+ wfRunHooks('APIAfterExecute', array(&$module));
$module->profileOut();
if (!$this->mInternalMode) {
@@ -396,6 +438,7 @@ class ApiMain extends ApiBase {
* Print results using the current printer
*/
protected function printResult($isError) {
+ $this->getResult()->cleanupUTF8();
$printer = $this->mPrinter;
$printer->profileIn();
@@ -437,6 +480,7 @@ class ApiMain extends ApiBase {
ApiBase :: PARAM_TYPE => 'integer',
ApiBase :: PARAM_DFLT => 0
),
+ 'requestid' => null,
);
}
@@ -451,6 +495,7 @@ class ApiMain extends ApiBase {
'maxlag' => 'Maximum lag',
'smaxage' => 'Set the s-maxage header to this many seconds. Errors are never cached',
'maxage' => 'Set the max-age header to this many seconds. Errors are never cached',
+ 'requestid' => 'Request ID to distinguish requests. This will just be output back to you',
);
}
@@ -493,6 +538,7 @@ class ApiMain extends ApiBase {
'API developers:',
' Roan Kattouw <Firstname>.<Lastname>@home.nl (lead developer Sep 2007-present)',
' Victor Vasiliev - vasilvv at gee mail dot com',
+ ' Bryan Tong Minh - bryan . tongminh @ gmail . com',
' Yuri Astrakhan <Firstname><Lastname>@gmail.com (creator, lead developer Sep 2006-Sep 2007)',
'',
'Please send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org',
@@ -521,6 +567,14 @@ class ApiMain extends ApiBase {
$msg .= "\n";
}
+ $msg .= "\n$astriks Permissions $astriks\n\n";
+ foreach ( self :: $mRights as $right => $rightMsg ) {
+ $groups = User::getGroupsWithPermission( $right );
+ $msg .= "* " . $right . " *\n " . wfMsgReplaceArgs( $rightMsg[ 'msg' ], $rightMsg[ 'params' ] ) .
+ "\nGranted to:\n " . str_replace( "*", "all", implode( ", ", $groups ) ) . "\n";
+
+ }
+
$msg .= "\n$astriks Formats $astriks\n\n";
foreach( $this->mFormats as $formatName => $unused ) {
$module = $this->createPrinterByName($formatName);
@@ -539,7 +593,7 @@ class ApiMain extends ApiBase {
public static function makeHelpMsgHeader($module, $paramName) {
$modulePrefix = $module->getModulePrefix();
- if (!empty($modulePrefix))
+ if (strval($modulePrefix) !== '')
$modulePrefix = "($modulePrefix) ";
return "* $paramName={$module->getModuleName()} $modulePrefix*";
@@ -602,8 +656,8 @@ class ApiMain extends ApiBase {
*/
public function getVersion() {
$vers = array ();
- $vers[] = 'MediaWiki ' . SpecialVersion::getVersion();
- $vers[] = __CLASS__ . ': $Id: ApiMain.php 44569 2008-12-14 08:31:04Z tstarling $';
+ $vers[] = 'MediaWiki: ' . SpecialVersion::getVersion() . "\n http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/";
+ $vers[] = __CLASS__ . ': $Id: ApiMain.php 45752 2009-01-14 21:36:57Z catrope $';
$vers[] = ApiBase :: getBaseVersion();
$vers[] = ApiFormatBase :: getBaseVersion();
$vers[] = ApiQueryBase :: getBaseVersion();
diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php
index 8687bdcd..13b058c9 100644
--- a/includes/api/ApiMove.php
+++ b/includes/api/ApiMove.php
@@ -44,9 +44,7 @@ class ApiMove extends ApiBase {
if(is_null($params['reason']))
$params['reason'] = '';
- $titleObj = NULL;
- if(!isset($params['from']))
- $this->dieUsageMsg(array('missingparam', 'from'));
+ $this->requireOnlyOneParameter($params, 'from', 'fromid');
if(!isset($params['to']))
$this->dieUsageMsg(array('missingparam', 'to'));
if(!isset($params['token']))
@@ -54,9 +52,18 @@ class ApiMove extends ApiBase {
if(!$wgUser->matchEditToken($params['token']))
$this->dieUsageMsg(array('sessionfailure'));
- $fromTitle = Title::newFromText($params['from']);
- if(!$fromTitle)
- $this->dieUsageMsg(array('invalidtitle', $params['from']));
+ if(isset($params['from']))
+ {
+ $fromTitle = Title::newFromText($params['from']);
+ if(!$fromTitle)
+ $this->dieUsageMsg(array('invalidtitle', $params['from']));
+ }
+ else if(isset($params['fromid']))
+ {
+ $fromTitle = Title::newFromID($params['fromid']);
+ if(!$fromTitle)
+ $this->dieUsageMsg(array('nosuchpageid', $params['fromid']));
+ }
if(!$fromTitle->exists())
$this->dieUsageMsg(array('notanarticle'));
$fromTalk = $fromTitle->getTalkPage();
@@ -66,27 +73,10 @@ class ApiMove extends ApiBase {
$this->dieUsageMsg(array('invalidtitle', $params['to']));
$toTalk = $toTitle->getTalkPage();
- // Run getUserPermissionsErrors() here so we get message arguments too,
- // rather than just a message key. The latter is troublesome for messages
- // that use arguments.
- // FIXME: moveTo() should really return an array, requires some
- // refactoring of other code, though (mainly SpecialMovepage.php)
- $errors = array_merge($fromTitle->getUserPermissionsErrors('move', $wgUser),
- $fromTitle->getUserPermissionsErrors('edit', $wgUser),
- $toTitle->getUserPermissionsErrors('move', $wgUser),
- $toTitle->getUserPermissionsErrors('edit', $wgUser));
- if(!empty($errors))
- // We don't care about multiple errors, just report one of them
- $this->dieUsageMsg(current($errors));
-
$hookErr = null;
-
$retval = $fromTitle->moveTo($toTitle, true, $params['reason'], !$params['noredirect']);
if($retval !== true)
- {
- # FIXME: Title::moveTo() sometimes returns a string
$this->dieUsageMsg(reset($retval));
- }
$r = array('from' => $fromTitle->getPrefixedText(), 'to' => $toTitle->getPrefixedText(), 'reason' => $params['reason']);
if(!$params['noredirect'] || !$wgUser->isAllowed('suppressredirect'))
@@ -105,8 +95,9 @@ class ApiMove extends ApiBase {
// We're not gonna dieUsage() on failure, since we already changed something
else
{
- $r['talkmove-error-code'] = ApiBase::$messageMap[$retval]['code'];
- $r['talkmove-error-info'] = ApiBase::$messageMap[$retval]['info'];
+ $parsed = $this->parseMsg(reset($retval));
+ $r['talkmove-error-code'] = $parsed['code'];
+ $r['talkmove-error-info'] = $parsed['info'];
}
}
@@ -129,6 +120,9 @@ class ApiMove extends ApiBase {
public function getAllowedParams() {
return array (
'from' => null,
+ 'fromid' => array(
+ ApiBase::PARAM_TYPE => 'integer'
+ ),
'to' => null,
'token' => null,
'reason' => null,
@@ -141,7 +135,8 @@ class ApiMove extends ApiBase {
public function getParamDescription() {
return array (
- 'from' => 'Title of the page you want to move.',
+ 'from' => 'Title of the page you want to move. Cannot be used together with fromid.',
+ 'fromid' => 'Page ID of the page you want to move. Cannot be used together with from.',
'to' => 'Title you want to rename the page to.',
'token' => 'A move token previously retrieved through prop=info',
'reason' => 'Reason for the move (optional).',
@@ -154,7 +149,7 @@ class ApiMove extends ApiBase {
public function getDescription() {
return array(
- 'Moves a page.'
+ 'Move a page.'
);
}
@@ -165,6 +160,6 @@ class ApiMove extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiMove.php 35619 2008-05-30 19:59:47Z btongminh $';
+ return __CLASS__ . ': $Id: ApiMove.php 47041 2009-02-09 14:39:41Z catrope $';
}
}
diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php
index e09cb285..54482e4b 100644
--- a/includes/api/ApiPageSet.php
+++ b/includes/api/ApiPageSet.php
@@ -53,7 +53,7 @@ class ApiPageSet extends ApiQueryBase {
private $mRequestedPageFields;
public function __construct($query, $resolveRedirects = false) {
- parent :: __construct($query, __CLASS__);
+ parent :: __construct($query, 'query');
$this->mAllPages = array ();
$this->mTitles = array();
@@ -92,10 +92,11 @@ class ApiPageSet extends ApiQueryBase {
*/
public function getPageTableFields() {
// Ensure we get minimum required fields
+ // DON'T change this order
$pageFlds = array (
- 'page_id' => null,
'page_namespace' => null,
- 'page_title' => null
+ 'page_title' => null,
+ 'page_id' => null,
);
// only store non-default fields
@@ -227,19 +228,18 @@ class ApiPageSet extends ApiQueryBase {
*/
public function execute() {
$this->profileIn();
- $titles = $pageids = $revids = null;
- extract($this->extractRequestParams());
+ $params = $this->extractRequestParams();
// Only one of the titles/pageids/revids is allowed at the same time
$dataSource = null;
- if (isset ($titles))
+ if (isset ($params['titles']))
$dataSource = 'titles';
- if (isset ($pageids)) {
+ if (isset ($params['pageids'])) {
if (isset ($dataSource))
$this->dieUsage("Cannot use 'pageids' at the same time as '$dataSource'", 'multisource');
$dataSource = 'pageids';
}
- if (isset ($revids)) {
+ if (isset ($params['revids'])) {
if (isset ($dataSource))
$this->dieUsage("Cannot use 'revids' at the same time as '$dataSource'", 'multisource');
$dataSource = 'revids';
@@ -247,15 +247,17 @@ class ApiPageSet extends ApiQueryBase {
switch ($dataSource) {
case 'titles' :
- $this->initFromTitles($titles);
+ $this->initFromTitles($params['titles']);
break;
case 'pageids' :
- $this->initFromPageIds($pageids);
+ $this->initFromPageIds($params['pageids']);
break;
case 'revids' :
if($this->mResolveRedirects)
- $this->dieUsage('revids may not be used with redirect resolution', 'params');
- $this->initFromRevIDs($revids);
+ $this->setWarning('Redirect resolution cannot be used together with the revids= parameter. '.
+ 'Any redirects the revids= point to have not been resolved.');
+ $this->mResolveRedirects = false;
+ $this->initFromRevIDs($params['revids']);
break;
default :
// Do nothing - some queries do not need any of the data sources.
@@ -366,7 +368,7 @@ class ApiPageSet extends ApiQueryBase {
}
private function initFromPageIds($pageids) {
- if(empty($pageids))
+ if(!count($pageids))
return;
$pageids = array_map('intval', $pageids); // paranoia
@@ -424,7 +426,7 @@ class ApiPageSet extends ApiQueryBase {
if(isset($remaining)) {
// Any items left in the $remaining list are added as missing
if($processTitles) {
- // The remaining titles in $remaining are non-existant pages
+ // The remaining titles in $remaining are non-existent pages
foreach ($remaining as $ns => $dbkeys) {
foreach ( $dbkeys as $dbkey => $unused ) {
$title = Title :: makeTitle($ns, $dbkey);
@@ -438,7 +440,7 @@ class ApiPageSet extends ApiQueryBase {
else
{
// The remaining pageids do not exist
- if(empty($this->mMissingPageIDs))
+ if(!$this->mMissingPageIDs)
$this->mMissingPageIDs = array_keys($remaining);
else
$this->mMissingPageIDs = array_merge($this->mMissingPageIDs, array_keys($remaining));
@@ -448,16 +450,16 @@ class ApiPageSet extends ApiQueryBase {
private function initFromRevIDs($revids) {
- if(empty($revids))
+ if(!count($revids))
return;
$db = $this->getDB();
$pageids = array();
$remaining = array_flip($revids);
- $tables = array('revision');
+ $tables = array('revision','page');
$fields = array('rev_id','rev_page');
- $where = array('rev_deleted' => 0, 'rev_id' => $revids);
+ $where = array('rev_deleted' => 0, 'rev_id' => $revids,'rev_page = page_id');
// Get pageIDs data from the `page` table
$this->profileDBIn();
@@ -475,8 +477,6 @@ class ApiPageSet extends ApiQueryBase {
$this->mMissingRevIDs = array_keys($remaining);
// Populate all the page information
- if($this->mResolveRedirects)
- ApiBase :: dieDebug(__METHOD__, 'revids may not be used with redirect resolution');
$this->initFromPageIds(array_keys($pageids));
}
@@ -488,7 +488,7 @@ class ApiPageSet extends ApiQueryBase {
// Repeat until all redirects have been resolved
// The infinite loop is prevented by keeping all known pages in $this->mAllPages
- while (!empty ($this->mPendingRedirectIDs)) {
+ while ($this->mPendingRedirectIDs) {
// Resolve redirects by querying the pagelinks table, and repeat the process
// Create a new linkBatch object for the next pass
@@ -537,7 +537,7 @@ class ApiPageSet extends ApiQueryBase {
$this->mRedirectTitles[$from] = $to;
}
$db->freeResult($res);
- if(!empty($this->mPendingRedirectIDs))
+ if($this->mPendingRedirectIDs)
{
# We found pages that aren't in the redirect table
# Add them
@@ -580,16 +580,16 @@ class ApiPageSet extends ApiQueryBase {
continue; // There's nothing else we can do
}
$iw = $titleObj->getInterwiki();
- if (!empty($iw)) {
+ if (strval($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);
+ $this->setWarning("No support for special pages has been implemented");
+ else
+ $linkBatch->addObj($titleObj);
}
// Make sure we remember the original title that was given to us
@@ -628,6 +628,6 @@ class ApiPageSet extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiPageSet.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiPageSet.php 45275 2009-01-01 02:02:03Z simetrical $';
}
}
diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php
index 77ce514f..2cf044cf 100644
--- a/includes/api/ApiParamInfo.php
+++ b/includes/api/ApiParamInfo.php
@@ -86,12 +86,12 @@ class ApiParamInfo extends ApiBase {
$retval['classname'] = get_class($obj);
$retval['description'] = (is_array($obj->getDescription()) ? implode("\n", $obj->getDescription()) : $obj->getDescription());
$retval['prefix'] = $obj->getModulePrefix();
- $allowedParams = $obj->getAllowedParams();
+ $allowedParams = $obj->getFinalParams();
if(!is_array($allowedParams))
return $retval;
$retval['parameters'] = array();
- $paramDesc = $obj->getParamDescription();
- foreach($obj->getAllowedParams() as $n => $p)
+ $paramDesc = $obj->getFinalParamDescription();
+ foreach($allowedParams as $n => $p)
{
$a = array('name' => $n);
if(!is_array($p))
@@ -111,7 +111,15 @@ class ApiParamInfo extends ApiBase {
$a['default'] = $p[ApiBase::PARAM_DFLT];
if(isset($p[ApiBase::PARAM_ISMULTI]))
if($p[ApiBase::PARAM_ISMULTI])
+ {
$a['multi'] = '';
+ $a['limit'] = $this->getMain()->canApiHighLimits() ?
+ ApiBase::LIMIT_SML2 :
+ ApiBase::LIMIT_SML1;
+ }
+ if(isset($p[ApiBase::PARAM_ALLOW_DUPLICATES]))
+ if($p[ApiBase::PARAM_ALLOW_DUPLICATES])
+ $a['allowsduplicates'] = '';
if(isset($p[ApiBase::PARAM_TYPE]))
{
$a['type'] = $p[ApiBase::PARAM_TYPE];
@@ -161,6 +169,6 @@ class ApiParamInfo extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiParamInfo.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiParamInfo.php 41653 2008-10-04 15:03:03Z catrope $';
}
}
diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php
index 4dcc94b6..e221fb1d 100644
--- a/includes/api/ApiParse.php
+++ b/includes/api/ApiParse.php
@@ -49,10 +49,13 @@ class ApiParse extends ApiBase {
$prop = array_flip($params['prop']);
$revid = false;
- global $wgParser, $wgUser;
+ // The parser needs $wgTitle to be set, apparently the
+ // $title parameter in Parser::parse isn't enough *sigh*
+ global $wgParser, $wgUser, $wgTitle;
$popts = new ParserOptions();
$popts->setTidy(true);
$popts->enableLimitReport();
+ $redirValues = null;
if(!is_null($oldid) || !is_null($page))
{
if(!is_null($oldid))
@@ -63,23 +66,42 @@ class ApiParse extends ApiBase {
$this->dieUsage("There is no revision ID $oldid", 'missingrev');
if(!$rev->userCan(Revision::DELETED_TEXT))
$this->dieUsage("You don't have permission to view deleted revisions", 'permissiondenied');
- $text = $rev->getRawText();
+ $text = $rev->getText( Revision::FOR_THIS_USER );
$titleObj = $rev->getTitle();
+ $wgTitle = $titleObj;
$p_result = $wgParser->parse($text, $titleObj, $popts);
}
else
{
- $titleObj = Title::newFromText($page);
+ if($params['redirects'])
+ {
+ $req = new FauxRequest(array(
+ 'action' => 'query',
+ 'redirects' => '',
+ 'titles' => $page
+ ));
+ $main = new ApiMain($req);
+ $main->execute();
+ $data = $main->getResultData();
+ $redirValues = @$data['query']['redirects'];
+ $to = $page;
+ foreach((array)$redirValues as $r)
+ $to = $r['to'];
+ }
+ else
+ $to = $page;
+ $titleObj = Title::newFromText($to);
if(!$titleObj)
$this->dieUsage("The page you specified doesn't exist", 'missingtitle');
- // Try the parser cache first
$articleObj = new Article($titleObj);
if(isset($prop['revid']))
$oldid = $articleObj->getRevIdFetched();
+ // Try the parser cache first
$pcache = ParserCache::singleton();
$p_result = $pcache->get($articleObj, $wgUser);
- if(!$p_result) {
+ if(!$p_result)
+ {
$p_result = $wgParser->parse($articleObj->getContent(), $titleObj, $popts);
global $wgUseParserCache;
if($wgUseParserCache)
@@ -92,12 +114,25 @@ class ApiParse extends ApiBase {
$titleObj = Title::newFromText($title);
if(!$titleObj)
$titleObj = Title::newFromText("API");
+ $wgTitle = $titleObj;
+ if($params['pst'] || $params['onlypst'])
+ $text = $wgParser->preSaveTransform($text, $titleObj, $wgUser, $popts);
+ if($params['onlypst'])
+ {
+ // Build a result and bail out
+ $result_array['text'] = array();
+ $this->getResult()->setContent($result_array['text'], $text);
+ $this->getResult()->addValue(null, $this->getModuleName(), $result_array);
+ return;
+ }
$p_result = $wgParser->parse($text, $titleObj, $popts);
}
// Return result
$result = $this->getResult();
$result_array = array();
+ if($params['redirects'] && !is_null($redirValues))
+ $result_array['redirects'] = $redirValues;
if(isset($prop['text'])) {
$result_array['text'] = array();
$result->setContent($result_array['text'], $p_result->getText());
@@ -120,6 +155,7 @@ class ApiParse extends ApiBase {
$result_array['revid'] = $oldid;
$result_mapping = array(
+ 'redirects' => 'r',
'langlinks' => 'll',
'categories' => 'cl',
'links' => 'pl',
@@ -184,6 +220,7 @@ class ApiParse extends ApiBase {
),
'text' => null,
'page' => null,
+ 'redirects' => false,
'oldid' => null,
'prop' => array(
ApiBase :: PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid',
@@ -199,19 +236,28 @@ class ApiParse extends ApiBase {
'sections',
'revid'
)
- )
+ ),
+ 'pst' => false,
+ 'onlypst' => false,
);
}
public function getParamDescription() {
return array (
'text' => 'Wikitext to parse',
+ 'redirects' => 'If the page parameter is set to a redirect, resolve it',
'title' => 'Title of page the text belongs to',
'page' => 'Parse the content of this page. Cannot be used together with text and title',
'oldid' => 'Parse the content of this revision. Overrides page',
'prop' => array('Which pieces of information to get.',
'NOTE: Section tree is only generated if there are more than 4 sections, or if the __TOC__ keyword is present'
),
+ 'pst' => array( 'Do a pre-save transform on the input before parsing it.',
+ 'Ignored if page or oldid is used.'
+ ),
+ 'onlypst' => array('Do a PST on the input, but don\'t parse it.',
+ 'Returns PSTed wikitext. Ignored if page or oldid is used.'
+ ),
);
}
@@ -226,6 +272,6 @@ class ApiParse extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiParse.php 36983 2008-07-03 15:01:50Z catrope $';
+ return __CLASS__ . ': $Id: ApiParse.php 44858 2008-12-20 20:00:07Z catrope $';
}
}
diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php
new file mode 100644
index 00000000..08de87b0
--- /dev/null
+++ b/includes/api/ApiPatrol.php
@@ -0,0 +1,99 @@
+<?php
+
+/*
+ * Created on Sep 2, 2008
+ *
+ * API for MediaWiki 1.14+
+ *
+ * Copyright (C) 2008 Soxred93 soxred93@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')) {
+ require_once ('ApiBase.php');
+}
+
+/**
+ * Allows user to patrol pages
+ * @ingroup API
+ */
+class ApiPatrol extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ /**
+ * Patrols the article or provides the reason the patrol failed.
+ */
+ public function execute() {
+ global $wgUser, $wgUseRCPatrol, $wgUseNPPatrol;
+ $this->getMain()->requestWriteMode();
+ $params = $this->extractRequestParams();
+
+ if(!isset($params['token']))
+ $this->dieUsageMsg(array('missingparam', 'token'));
+ if(!isset($params['rcid']))
+ $this->dieUsageMsg(array('missingparam', 'rcid'));
+ if(!$wgUser->matchEditToken($params['token']))
+ $this->dieUsageMsg(array('sessionfailure'));
+
+ $rc = RecentChange::newFromID($params['rcid']);
+ if(!$rc instanceof RecentChange)
+ $this->dieUsageMsg(array('nosuchrcid', $params['rcid']));
+ $retval = RecentChange::markPatrolled($params['rcid']);
+
+ if($retval)
+ $this->dieUsageMsg(current($retval));
+
+ $result = array('rcid' => $rc->getAttribute('rc_id'));
+ ApiQueryBase::addTitleInfo($result, $rc->getTitle());
+ $this->getResult()->addValue(null, $this->getModuleName(), $result);
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'token' => null,
+ 'rcid' => array(
+ ApiBase :: PARAM_TYPE => 'integer'
+ ),
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'token' => 'Patrol token obtained from list=recentchanges',
+ 'rcid' => 'Recentchanges ID to patrol',
+ );
+ }
+
+ public function getDescription() {
+ return array (
+ 'Patrol a page or revision. '
+ );
+ }
+
+ protected function getExamples() {
+ return array(
+ 'api.php?action=patrol&token=123abc&rcid=230672766'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiPatrol.php 42548 2008-10-25 14:04:43Z tstarling $';
+ }
+}
diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php
index 30bcfdbc..522d02b2 100644
--- a/includes/api/ApiProtect.php
+++ b/includes/api/ApiProtect.php
@@ -37,7 +37,7 @@ class ApiProtect extends ApiBase {
}
public function execute() {
- global $wgUser;
+ global $wgUser, $wgRestrictionTypes, $wgRestrictionLevels;
$this->getMain()->requestWriteMode();
$params = $this->extractRequestParams();
@@ -46,7 +46,7 @@ class ApiProtect extends ApiBase {
$this->dieUsageMsg(array('missingparam', 'title'));
if(!isset($params['token']))
$this->dieUsageMsg(array('missingparam', 'token'));
- if(!isset($params['protections']) || empty($params['protections']))
+ if(empty($params['protections']))
$this->dieUsageMsg(array('missingparam', 'protections'));
if(!$wgUser->matchEditToken($params['token']))
@@ -57,25 +57,23 @@ class ApiProtect extends ApiBase {
$this->dieUsageMsg(array('invalidtitle', $params['title']));
$errors = $titleObj->getUserPermissionsErrors('protect', $wgUser);
- if(!empty($errors))
+ if($errors)
// We don't care about multiple errors, just report one of them
$this->dieUsageMsg(current($errors));
- if(in_array($params['expiry'], array('infinite', 'indefinite', 'never')))
- $expiry = Block::infinity();
- else
+ $expiry = (array)$params['expiry'];
+ if(count($expiry) != count($params['protections']))
{
- $expiry = strtotime($params['expiry']);
- if($expiry < 0 || $expiry == false)
- $this->dieUsageMsg(array('invalidexpiry'));
-
- $expiry = wfTimestamp(TS_MW, $expiry);
- if($expiry < wfTimestampNow())
- $this->dieUsageMsg(array('pastexpiry'));
+ if(count($expiry) == 1)
+ $expiry = array_fill(0, count($params['protections']), $expiry[0]);
+ else
+ $this->dieUsageMsg(array('toofewexpiries', count($expiry), count($params['protections'])));
}
-
+
$protections = array();
- foreach($params['protections'] as $prot)
+ $expiryarray = array();
+ $resultProtections = array();
+ foreach($params['protections'] as $i => $prot)
{
$p = explode('=', $prot);
$protections[$p[0]] = ($p[1] == 'all' ? '' : $p[1]);
@@ -83,26 +81,45 @@ class ApiProtect extends ApiBase {
$this->dieUsageMsg(array('create-titleexists'));
if(!$titleObj->exists() && $p[0] != 'create')
$this->dieUsageMsg(array('missingtitles-createonly'));
+ if(!in_array($p[0], $wgRestrictionTypes) && $p[0] != 'create')
+ $this->dieUsageMsg(array('protect-invalidaction', $p[0]));
+ if(!in_array($p[1], $wgRestrictionLevels) && $p[1] != 'all')
+ $this->dieUsageMsg(array('protect-invalidlevel', $p[1]));
+
+ if(in_array($expiry[$i], array('infinite', 'indefinite', 'never')))
+ $expiryarray[$p[0]] = Block::infinity();
+ else
+ {
+ $exp = strtotime($expiry[$i]);
+ if($exp < 0 || $exp == false)
+ $this->dieUsageMsg(array('invalidexpiry', $expiry[$i]));
+
+ $exp = wfTimestamp(TS_MW, $exp);
+ if($exp < wfTimestampNow())
+ $this->dieUsageMsg(array('pastexpiry', $expiry[$i]));
+ $expiryarray[$p[0]] = $exp;
+ }
+ $resultProtections[] = array($p[0] => $protections[$p[0]],
+ 'expiry' => ($expiryarray[$p[0]] == Block::infinity() ?
+ 'infinite' :
+ wfTimestamp(TS_ISO_8601, $expiryarray[$p[0]])));
}
+ $cascade = $params['cascade'];
if($titleObj->exists()) {
$articleObj = new Article($titleObj);
- $ok = $articleObj->updateRestrictions($protections, $params['reason'], $params['cascade'], $expiry);
+ $ok = $articleObj->updateRestrictions($protections, $params['reason'], $cascade, $expiryarray);
} else
- $ok = $titleObj->updateTitleProtection($protections['create'], $params['reason'], $expiry);
+ $ok = $titleObj->updateTitleProtection($protections['create'], $params['reason'], $expiryarray['create']);
if(!$ok)
// This is very weird. Maybe the article was deleted or the user was blocked/desysopped in the meantime?
// Just throw an unknown error in this case, as it's very likely to be a race condition
$this->dieUsageMsg(array());
$res = array('title' => $titleObj->getPrefixedText(), 'reason' => $params['reason']);
- if($expiry == Block::infinity())
- $res['expiry'] = 'infinity';
- else
- $res['expiry'] = wfTimestamp(TS_ISO_8601, $expiry);
-
- if($params['cascade'])
+ if($cascade)
$res['cascade'] = '';
- $res['protections'] = $protections;
+ $res['protections'] = $resultProtections;
+ $this->getResult()->setIndexedTagName($res['protections'], 'protection');
$this->getResult()->addValue(null, $this->getModuleName(), $res);
}
@@ -115,7 +132,11 @@ class ApiProtect extends ApiBase {
'protections' => array(
ApiBase :: PARAM_ISMULTI => true
),
- 'expiry' => 'infinite',
+ 'expiry' => array(
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_ALLOW_DUPLICATES => true,
+ ApiBase :: PARAM_DFLT => 'infinite',
+ ),
'reason' => '',
'cascade' => false
);
@@ -123,12 +144,14 @@ class ApiProtect extends ApiBase {
public function getParamDescription() {
return array (
- 'title' => 'Title of the page you want to restore.',
+ 'title' => 'Title of the page you want to (un)protect.',
'token' => 'A protect token previously retrieved through prop=info',
'protections' => 'Pipe-separated list of protection levels, formatted action=group (e.g. edit=sysop)',
- 'expiry' => 'Expiry timestamp. If set to \'infinite\', \'indefinite\' or \'never\', the protection will never expire.',
+ 'expiry' => array('Expiry timestamps. If only one timestamp is set, it\'ll be used for all protections.',
+ 'Use \'infinite\', \'indefinite\' or \'never\', for a neverexpiring protection.'),
'reason' => 'Reason for (un)protecting (optional)',
- 'cascade' => 'Enable cascading protection (i.e. protect pages included in this page)'
+ 'cascade' => array('Enable cascading protection (i.e. protect pages included in this page)',
+ 'Ignored if not all protection levels are \'sysop\' or \'protect\''),
);
}
@@ -140,12 +163,12 @@ class ApiProtect extends ApiBase {
protected function getExamples() {
return array (
- 'api.php?action=protect&title=Main%20Page&token=123ABC&protections=edit=sysop|move=sysop&cascade&expiry=20070901163000',
+ 'api.php?action=protect&title=Main%20Page&token=123ABC&protections=edit=sysop|move=sysop&cascade&expiry=20070901163000|never',
'api.php?action=protect&title=Main%20Page&token=123ABC&protections=edit=all|move=all&reason=Lifting%20restrictions'
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiProtect.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiProtect.php 44426 2008-12-10 22:39:41Z catrope $';
}
}
diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php
new file mode 100644
index 00000000..d7202a46
--- /dev/null
+++ b/includes/api/ApiPurge.php
@@ -0,0 +1,106 @@
+<?php
+
+/*
+ * Created on Sep 2, 2008
+ *
+ * API for MediaWiki 1.14+
+ *
+ * Copyright (C) 2008 Chad Horohoe
+ *
+ * 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')) {
+ require_once ('ApiBase.php');
+}
+
+/**
+ * API interface for page purging
+ * @ingroup API
+ */
+class ApiPurge extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ /**
+ * Purges the cache of a page
+ */
+ public function execute() {
+ global $wgUser;
+ $params = $this->extractRequestParams();
+ if(!$wgUser->isAllowed('purge'))
+ $this->dieUsageMsg(array('cantpurge'));
+ if(!isset($params['titles']))
+ $this->dieUsageMsg(array('missingparam', 'titles'));
+ $result = array();
+ foreach($params['titles'] as $t) {
+ $r = array();
+ $title = Title::newFromText($t);
+ if(!$title instanceof Title)
+ {
+ $r['title'] = $t;
+ $r['invalid'] = '';
+ $result[] = $r;
+ continue;
+ }
+ ApiQueryBase::addTitleInfo($r, $title);
+ if(!$title->exists())
+ {
+ $r['missing'] = '';
+ $result[] = $r;
+ continue;
+ }
+ $article = new Article($title);
+ $article->doPurge(); // Directly purge and skip the UI part of purge().
+ $r['purged'] = '';
+ $result[] = $r;
+ }
+ $this->getResult()->setIndexedTagName($result, 'page');
+ $this->getResult()->addValue(null, $this->getModuleName(), $result);
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'titles' => array(
+ ApiBase :: PARAM_ISMULTI => true
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'titles' => 'A list of titles',
+ );
+ }
+
+ public function getDescription() {
+ return array (
+ 'Purge the cache for the given titles.'
+ );
+ }
+
+ protected function getExamples() {
+ return array(
+ 'api.php?action=purge&titles=Main_Page|API'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiPurge.php 41020 2008-09-19 00:21:03Z demon $';
+ }
+}
diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php
index f4a2402f..45a5667a 100644
--- a/includes/api/ApiQuery.php
+++ b/includes/api/ApiQuery.php
@@ -56,6 +56,7 @@ class ApiQuery extends ApiBase {
'categories' => 'ApiQueryCategories',
'extlinks' => 'ApiQueryExternalLinks',
'categoryinfo' => 'ApiQueryCategoryInfo',
+ 'duplicatefiles' => 'ApiQueryDuplicateFiles',
);
private $mQueryListModules = array (
@@ -75,6 +76,7 @@ class ApiQuery extends ApiBase {
'search' => 'ApiQuerySearch',
'usercontribs' => 'ApiQueryContributions',
'watchlist' => 'ApiQueryWatchlist',
+ 'watchlistraw' => 'ApiQueryWatchlistRaw',
'exturlusage' => 'ApiQueryExtLinksUsage',
'users' => 'ApiQueryUsers',
'random' => 'ApiQueryRandom',
@@ -93,10 +95,10 @@ class ApiQuery extends ApiBase {
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);
+ global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules;
+ self :: appendUserModules($this->mQueryPropModules, $wgAPIPropModules);
+ self :: appendUserModules($this->mQueryListModules, $wgAPIListModules);
+ self :: appendUserModules($this->mQueryMetaModules, $wgAPIMetaModules);
$this->mPropModuleNames = array_keys($this->mQueryPropModules);
$this->mListModuleNames = array_keys($this->mQueryListModules);
@@ -209,6 +211,7 @@ class ApiQuery extends ApiBase {
foreach ($modules as $module) {
$module->profileIn();
$module->execute();
+ wfRunHooks('APIQueryAfterExecute', array(&$module));
$module->profileOut();
}
}
@@ -229,8 +232,8 @@ class ApiQuery extends ApiBase {
* Create instances of all modules requested by the client
*/
private function InstantiateModules(&$modules, $param, $moduleList) {
- $list = $this->params[$param];
- if (isset ($list))
+ $list = @$this->params[$param];
+ if (!is_null ($list))
foreach ($list as $moduleName)
$modules[] = new $moduleList[$moduleName] ($this, $moduleName);
}
@@ -253,7 +256,7 @@ class ApiQuery extends ApiBase {
);
}
- if (!empty ($normValues)) {
+ if (count($normValues)) {
$result->setIndexedTagName($normValues, 'n');
$result->addValue('query', 'normalized', $normValues);
}
@@ -267,7 +270,7 @@ class ApiQuery extends ApiBase {
);
}
- if (!empty ($intrwValues)) {
+ if (count($intrwValues)) {
$result->setIndexedTagName($intrwValues, 'i');
$result->addValue('query', 'interwiki', $intrwValues);
}
@@ -281,7 +284,7 @@ class ApiQuery extends ApiBase {
);
}
- if (!empty ($redirValues)) {
+ if (count($redirValues)) {
$result->setIndexedTagName($redirValues, 'r');
$result->addValue('query', 'redirects', $redirValues);
}
@@ -290,7 +293,7 @@ class ApiQuery extends ApiBase {
// Missing revision elements
//
$missingRevIDs = $pageSet->getMissingRevisionIDs();
- if (!empty ($missingRevIDs)) {
+ if (count($missingRevIDs)) {
$revids = array ();
foreach ($missingRevIDs as $revid) {
$revids[$revid] = array (
@@ -332,7 +335,7 @@ class ApiQuery extends ApiBase {
$pages[$pageid] = $vals;
}
- if (!empty ($pages)) {
+ if (count($pages)) {
if ($this->params['indexpageids']) {
$pageIDs = array_keys($pages);
@@ -381,6 +384,7 @@ class ApiQuery extends ApiBase {
// populate resultPageSet with the generator output
$generator->profileIn();
$generator->executeGenerator($resultPageSet);
+ wfRunHooks('APIQueryGeneratorAfterExecute', array(&$generator, &$resultPageSet));
$resultPageSet->finishPageSetGeneration();
$generator->profileOut();
@@ -476,7 +480,6 @@ class ApiQuery extends ApiBase {
return $psModule->makeHelpMsgParameters() . parent :: makeHelpMsgParameters();
}
- // @todo should work correctly
public function shouldCheckMaxlag() {
return true;
}
@@ -509,7 +512,7 @@ class ApiQuery extends ApiBase {
public function getVersion() {
$psModule = new ApiPageSet($this);
$vers = array ();
- $vers[] = __CLASS__ . ': $Id: ApiQuery.php 35098 2008-05-20 17:13:28Z ialex $';
+ $vers[] = __CLASS__ . ': $Id: ApiQuery.php 42548 2008-10-25 14:04:43Z tstarling $';
$vers[] = $psModule->getVersion();
return $vers;
}
diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php
index 3ff42c88..e6287eea 100644
--- a/includes/api/ApiQueryAllCategories.php
+++ b/includes/api/ApiQueryAllCategories.php
@@ -56,17 +56,30 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
$this->addTables('category');
$this->addFields('cat_title');
- if (!is_null($params['from']))
- $this->addWhere('cat_title>=' . $db->addQuotes($this->titleToKey($params['from'])));
+ $dir = ($params['dir'] == 'descending' ? 'older' : 'newer');
+ $from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from']));
+ $this->addWhereRange('cat_title', $dir, $from, null);
if (isset ($params['prefix']))
- $this->addWhere("cat_title LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'");
+ $this->addWhere("cat_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'");
$this->addOption('LIMIT', $params['limit']+1);
$this->addOption('ORDER BY', 'cat_title' . ($params['dir'] == 'descending' ? ' DESC' : ''));
$prop = array_flip($params['prop']);
$this->addFieldsIf( array( 'cat_pages', 'cat_subcats', 'cat_files' ), isset($prop['size']) );
- $this->addFieldsIf( 'cat_hidden', isset($prop['hidden']) );
+ if(isset($prop['hidden']))
+ {
+ $this->addTables(array('page', 'page_props'));
+ $this->addJoinConds(array(
+ 'page' => array('LEFT JOIN', array(
+ 'page_namespace' => NS_CATEGORY,
+ 'page_title=cat_title')),
+ 'page_props' => array('LEFT JOIN', array(
+ 'pp_page=page_id',
+ 'pp_propname' => 'hiddencat')),
+ ));
+ $this->addFields('pp_propname AS cat_hidden');
+ }
$res = $this->select(__METHOD__);
@@ -158,6 +171,6 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllCategories.php 36790 2008-06-29 22:26:23Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryAllCategories.php 44590 2008-12-14 20:24:23Z catrope $';
}
}
diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php
index aefbb725..9ad34aa2 100644
--- a/includes/api/ApiQueryAllLinks.php
+++ b/includes/api/ApiQueryAllLinks.php
@@ -74,30 +74,30 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
$arr = explode('|', $params['continue']);
if(count($arr) != 2)
$this->dieUsage("Invalid continue parameter", 'badcontinue');
- $params['from'] = $arr[0]; // Handled later
+ $from = $this->getDB()->strencode($this->titleToKey($arr[0]));
$id = intval($arr[1]);
- $this->addWhere("pl_from >= $id");
+ $this->addWhere("pl_title > '$from' OR " .
+ "(pl_title = '$from' AND " .
+ "pl_from > $id)");
}
if (!is_null($params['from']))
- $this->addWhere('pl_title>=' . $db->addQuotes($this->titleToKey($params['from'])));
+ $this->addWhere('pl_title>=' . $db->addQuotes($this->titlePartToKey($params['from'])));
if (isset ($params['prefix']))
- $this->addWhere("pl_title LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'");
+ $this->addWhere("pl_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'");
$this->addFields(array (
- 'pl_namespace',
'pl_title',
- 'pl_from'
));
+ $this->addFieldsIf('pl_from', !$params['unique']);
$this->addOption('USE INDEX', 'pl_namespace');
$limit = $params['limit'];
$this->addOption('LIMIT', $limit+1);
- # Only order by pl_namespace if it isn't constant in the WHERE clause
- if(count($params['namespace']) != 1)
- $this->addOption('ORDER BY', 'pl_namespace, pl_title');
- else
+ if($params['unique'])
$this->addOption('ORDER BY', 'pl_title');
+ else
+ $this->addOption('ORDER BY', 'pl_title, pl_from');
$res = $this->select(__METHOD__);
@@ -107,7 +107,10 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
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->keyToTitle($row->pl_title) . "|" . $row->pl_from);
+ if($params['unique'])
+ $this->setContinueEnumParameter('from', $this->keyToTitle($row->pl_title));
+ else
+ $this->setContinueEnumParameter('continue', $this->keyToTitle($row->pl_title) . "|" . $row->pl_from);
break;
}
@@ -116,7 +119,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
if ($fld_ids)
$vals['fromid'] = intval($row->pl_from);
if ($fld_title) {
- $title = Title :: makeTitle($row->pl_namespace, $row->pl_title);
+ $title = Title :: makeTitle($params['namespace'], $row->pl_title);
$vals['ns'] = intval($title->getNamespace());
$vals['title'] = $title->getPrefixedText();
}
@@ -187,6 +190,6 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllLinks.php 37258 2008-07-07 14:48:40Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryAllLinks.php 45850 2009-01-17 20:03:25Z catrope $';
}
}
diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php
index dd0e98a8..8395808b 100644
--- a/includes/api/ApiQueryAllUsers.php
+++ b/includes/api/ApiQueryAllUsers.php
@@ -121,7 +121,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
$row = $db->fetchObject($res);
$count++;
- if (!$row || $lastUser != $row->user_name) {
+ if (!$row || $lastUser !== $row->user_name) {
// Save the last pass's user data
if (is_array($lastUserData))
$data[] = $lastUserData;
@@ -219,6 +219,6 @@ class ApiQueryAllUsers extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllUsers.php 36790 2008-06-29 22:26:23Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryAllUsers.php 44472 2008-12-11 21:51:01Z catrope $';
}
}
diff --git a/includes/api/ApiQueryAllimages.php b/includes/api/ApiQueryAllimages.php
index 26cbc368..ea83c667 100644
--- a/includes/api/ApiQueryAllimages.php
+++ b/includes/api/ApiQueryAllimages.php
@@ -61,10 +61,11 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase {
$params = $this->extractRequestParams();
// Image filters
- if (!is_null($params['from']))
- $this->addWhere('img_name>=' . $db->addQuotes($this->titleToKey($params['from'])));
+ $dir = ($params['dir'] == 'descending' ? 'older' : 'newer');
+ $from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from']));
+ $this->addWhereRange('img_name', $dir, $from, null);
if (isset ($params['prefix']))
- $this->addWhere("img_name LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'");
+ $this->addWhere("img_name LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'");
if (isset ($params['minsize'])) {
$this->addWhere('img_size>=' . intval($params['minsize']));
@@ -109,10 +110,10 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase {
if (is_null($resultPageSet)) {
$file = $repo->newFileFromRow( $row );
-
- $data[] = ApiQueryImageInfo::getInfo( $file, $prop, $result );
+ $data[] = array_merge(array('name' => $row->img_name),
+ ApiQueryImageInfo::getInfo($file, $prop, $result));
} else {
- $data[] = Title::makeTitle( NS_IMAGE, $row->img_name );
+ $data[] = Title::makeTitle(NS_FILE, $row->img_name);
}
}
$db->freeResult($res);
@@ -162,7 +163,8 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase {
'dimensions', // Obsolete
'mime',
'sha1',
- 'metadata'
+ 'metadata',
+ 'bitdepth',
),
ApiBase :: PARAM_DFLT => 'timestamp|url',
ApiBase :: PARAM_ISMULTI => true
@@ -200,6 +202,6 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllimages.php 37909 2008-07-22 13:26:15Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryAllimages.php 44121 2008-12-01 17:14:30Z vyznev $';
}
}
diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php
index 39490fe7..531fa02a 100644
--- a/includes/api/ApiQueryAllpages.php
+++ b/includes/api/ApiQueryAllpages.php
@@ -62,11 +62,21 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
$this->addWhereIf('page_is_redirect = 0', $params['filterredir'] === 'nonredirects');
$this->addWhereFld('page_namespace', $params['namespace']);
$dir = ($params['dir'] == 'descending' ? 'older' : 'newer');
- $from = (is_null($params['from']) ? null : $this->titleToKey($params['from']));
+ $from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from']));
$this->addWhereRange('page_title', $dir, $from, null);
if (isset ($params['prefix']))
- $this->addWhere("page_title LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'");
+ $this->addWhere("page_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'");
+ if (is_null($resultPageSet)) {
+ $selectFields = array (
+ 'page_namespace',
+ 'page_title',
+ 'page_id'
+ );
+ } else {
+ $selectFields = $resultPageSet->getPageTableFields();
+ }
+ $this->addFields($selectFields);
$forceNameTitleIndex = true;
if (isset ($params['minsize'])) {
$this->addWhere('page_len>=' . intval($params['minsize']));
@@ -79,15 +89,20 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
}
// Page protection filtering
- if (isset ($params['prtype'])) {
+ if (!empty ($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 != '*')
+ // Remove the empty string and '*' from the prlevel array
+ $prlevel = array_diff($params['prlevel'], array('', '*'));
+ if (!empty($prlevel))
$this->addWhereFld('pr_level', $prlevel);
+ if ($params['prfiltercascade'] == 'cascading')
+ $this->addWhereFld('pr_cascade', 1);
+ if ($params['prfiltercascade'] == 'noncascading')
+ $this->addWhereFld('pr_cascade', 0);
$this->addOption('DISTINCT');
@@ -105,20 +120,16 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
} else if($params['filterlanglinks'] == 'withlanglinks') {
$this->addTables('langlinks');
$this->addWhere('page_id=ll_from');
+ $this->addOption('STRAIGHT_JOIN');
+ // We have to GROUP BY all selected fields to stop
+ // PostgreSQL from whining
+ $this->addOption('GROUP BY', implode(', ', $selectFields));
$forceNameTitleIndex = false;
}
if ($forceNameTitleIndex)
$this->addOption('USE INDEX', 'name_title');
- if (is_null($resultPageSet)) {
- $this->addFields(array (
- 'page_id',
- 'page_namespace',
- 'page_title'
- ));
- } else {
- $this->addFields($resultPageSet->getPageTableFields());
- }
+
$limit = $params['limit'];
$this->addOption('LIMIT', $limit+1);
@@ -185,6 +196,14 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
ApiBase :: PARAM_TYPE => $wgRestrictionLevels,
ApiBase :: PARAM_ISMULTI => true
),
+ 'prfiltercascade' => array (
+ ApiBase :: PARAM_DFLT => 'all',
+ ApiBase :: PARAM_TYPE => array (
+ 'cascading',
+ 'noncascading',
+ 'all'
+ ),
+ ),
'limit' => array (
ApiBase :: PARAM_DFLT => 10,
ApiBase :: PARAM_TYPE => 'limit',
@@ -221,6 +240,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
'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)',
+ 'prfiltercascade' => 'Filter protections based on cascadingness (ignored when apprtype isn\'t set)',
'filterlanglinks' => 'Filter based on whether a page has langlinks',
'limit' => 'How many total pages to return.'
);
@@ -244,6 +264,6 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllpages.php 37775 2008-07-17 09:26:01Z brion $';
+ return __CLASS__ . ': $Id: ApiQueryAllpages.php 44863 2008-12-20 23:54:04Z catrope $';
}
}
diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php
index fea058f3..f67e0044 100644
--- a/includes/api/ApiQueryBacklinks.php
+++ b/includes/api/ApiQueryBacklinks.php
@@ -60,7 +60,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
);
public function __construct($query, $moduleName) {
- $code = $prefix = $linktbl = null;
extract($this->backlinksSettings[$moduleName]);
parent :: __construct($query, $moduleName, $code);
@@ -100,7 +99,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
* AND pl_title='Foo' AND pl_namespace=0
* LIMIT 11 ORDER BY pl_from
*/
- $db = $this->getDb();
+ $db = $this->getDB();
$this->addTables(array('page', $this->bl_table));
$this->addWhere("{$this->bl_from}=page_id");
if(is_null($resultPageSet))
@@ -108,12 +107,12 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
else
$this->addFields($resultPageSet->getPageTableFields());
$this->addFields('page_is_redirect');
- $this->addWhereFld($this->bl_title, $this->rootTitle->getDbKey());
+ $this->addWhereFld($this->bl_title, $this->rootTitle->getDBKey());
if($this->hasNS)
$this->addWhereFld($this->bl_ns, $this->rootTitle->getNamespace());
$this->addWhereFld('page_namespace', $this->params['namespace']);
if(!is_null($this->contID))
- $this->addWhere("page_id>={$this->contID}");
+ $this->addWhere("{$this->bl_from}>={$this->contID}");
if($this->params['filterredir'] == 'redirects')
$this->addWhereFld('page_is_redirect', 1);
if($this->params['filterredir'] == 'nonredirects')
@@ -124,11 +123,11 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
private function prepareSecondQuery($resultPageSet = null) {
/* SELECT page_id, page_title, page_namespace, page_is_redirect, pl_title, pl_namespace
- * FROM pagelinks, page WHERE pl_from=page_id
- * AND (pl_title='Foo' AND pl_namespace=0) OR (pl_title='Bar' AND pl_namespace=1)
- * LIMIT 11 ORDER BY pl_namespace, pl_title, pl_from
+ FROM pagelinks, page WHERE pl_from=page_id
+ AND (pl_title='Foo' AND pl_namespace=0) OR (pl_title='Bar' AND pl_namespace=1)
+ ORDER BY pl_namespace, pl_title, pl_from LIMIT 11
*/
- $db = $this->getDb();
+ $db = $this->getDB();
$this->addTables(array('page', $this->bl_table));
$this->addWhere("{$this->bl_from}=page_id");
if(is_null($resultPageSet))
@@ -138,16 +137,31 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$this->addFields($this->bl_title);
if($this->hasNS)
$this->addFields($this->bl_ns);
- $titleWhere = '';
+ $titleWhere = array();
foreach($this->redirTitles as $t)
- $titleWhere .= ($titleWhere != '' ? " OR " : '') .
- "({$this->bl_title} = ".$db->addQuotes($t->getDBKey()).
+ $titleWhere[] = "({$this->bl_title} = ".$db->addQuotes($t->getDBKey()).
($this->hasNS ? " AND {$this->bl_ns} = '{$t->getNamespace()}'" : "") .
")";
- $this->addWhere($titleWhere);
+ $this->addWhere($db->makeList($titleWhere, LIST_OR));
$this->addWhereFld('page_namespace', $this->params['namespace']);
if(!is_null($this->redirID))
- $this->addWhere("page_id>={$this->redirID}");
+ {
+ $first = $this->redirTitles[0];
+ $title = $db->strencode($first->getDBKey());
+ $ns = $first->getNamespace();
+ $from = $this->redirID;
+ if($this->hasNS)
+ $this->addWhere("{$this->bl_ns} > $ns OR ".
+ "({$this->bl_ns} = $ns AND ".
+ "({$this->bl_title} > '$title' OR ".
+ "({$this->bl_title} = '$title' AND ".
+ "{$this->bl_from} >= $from)))");
+ else
+ $this->addWhere("{$this->bl_title} > '$title' OR ".
+ "({$this->bl_title} = '$title' AND ".
+ "{$this->bl_from} >= $from)");
+
+ }
if($this->params['filterredir'] == 'redirects')
$this->addWhereFld('page_is_redirect', 1);
if($this->params['filterredir'] == 'nonredirects')
@@ -170,7 +184,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$this->prepareFirstQuery($resultPageSet);
$db = $this->getDB();
- $res = $this->select(__METHOD__);
+ $res = $this->select(__METHOD__.'::firstQuery');
$count = 0;
$this->data = array ();
@@ -195,11 +209,11 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
}
$db->freeResult($res);
- if($this->redirect && !empty($this->redirTitles))
+ if($this->redirect && count($this->redirTitles))
{
$this->resetQueryParams();
$this->prepareSecondQuery($resultPageSet);
- $res = $this->select(__METHOD__);
+ $res = $this->select(__METHOD__.'::secondQuery');
$count = 0;
while($row = $db->fetchObject($res))
{
@@ -210,7 +224,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
if($this->hasNS)
$contTitle = Title::makeTitle($row->{$this->bl_ns}, $row->{$this->bl_title});
else
- $contTitle = Title::makeTitle(NS_IMAGE, $row->{$this->bl_title});
+ $contTitle = Title::makeTitle(NS_FILE, $row->{$this->bl_title});
$this->continueStr = $this->getContinueRedirStr($contTitle->getArticleID(), $row->page_id);
break;
}
@@ -229,7 +243,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$resultData = array();
foreach($this->data as $ns => $a)
foreach($a as $title => $arr)
- $resultData[$arr['pageid']] = $arr;
+ $resultData[] = $arr;
$result = $this->getResult();
$result->setIndexedTagName($resultData, $this->bl_code);
$result->addValue('query', $this->getModuleName(), $resultData);
@@ -254,7 +268,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
ApiQueryBase::addTitleInfo($a, Title::makeTitle($row->page_namespace, $row->page_title));
if($row->page_is_redirect)
$a['redirect'] = '';
- $ns = $this->hasNS ? $row->{$this->bl_ns} : NS_IMAGE;
+ $ns = $this->hasNS ? $row->{$this->bl_ns} : NS_FILE;
$this->data[$ns][$row->{$this->bl_title}]['redirlinks'][] = $a;
$this->getResult()->setIndexedTagName($this->data[$ns][$row->{$this->bl_title}]['redirlinks'], $this->bl_code);
}
@@ -276,7 +290,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
}
// only image titles are allowed for the root in imageinfo mode
- if (!$this->hasNS && $this->rootTitle->getNamespace() !== NS_IMAGE)
+ if (!$this->hasNS && $this->rootTitle->getNamespace() !== NS_FILE)
$this->dieUsage("The title for {$this->getModuleName()} query must be an image", 'bad_image_title');
}
@@ -399,6 +413,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryBacklinks.php 37504 2008-07-10 14:28:09Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryBacklinks.php 46135 2009-01-24 13:03:40Z catrope $';
}
}
diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php
index f392186b..896dd00c 100644
--- a/includes/api/ApiQueryBase.php
+++ b/includes/api/ApiQueryBase.php
@@ -126,13 +126,19 @@ abstract class ApiQueryBase extends ApiBase {
* Clauses can be formatted as 'foo=bar' or array('foo' => 'bar'),
* the latter only works if the value is a constant (i.e. not another field)
*
+ * If $value is an empty array, this function does nothing.
+ *
* For example, array('foo=bar', 'baz' => 3, 'bla' => 'foo') translates
* to "foo=bar AND baz='3' AND bla='foo'"
* @param mixed $value String or array
*/
protected function addWhere($value) {
- if (is_array($value))
- $this->where = array_merge($this->where, $value);
+ if (is_array($value)) {
+ // Sanity check: don't insert empty arrays,
+ // Database::makeList() chokes on them
+ if ( count( $value ) )
+ $this->where = array_merge($this->where, $value);
+ }
else
$this->where[] = $value;
}
@@ -154,10 +160,12 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Equivalent to addWhere(array($field => $value))
* @param string $field Field name
- * @param string $value Value; ignored if nul;
+ * @param string $value Value; ignored if null or empty array;
*/
protected function addWhereFld($field, $value) {
- if (!is_null($value))
+ // Use count() to its full documented capabilities to simultaneously
+ // test for null, empty array or empty countable object
+ if ( count( $value ) )
$this->where[$field] = $value;
}
@@ -236,7 +244,7 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Add information (title and namespace) about a Title object to a result array
- * @param array $arr Result array la ApiResult
+ * @param array $arr Result array à la ApiResult
* @param Title $title Title object
* @param string $prefix Module prefix
*/
@@ -264,7 +272,7 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Add a sub-element under the page element with the given page ID
* @param int $pageId Page ID
- * @param array $data Data array la ApiResult
+ * @param array $data Data array à la ApiResult
*/
protected function addPageSubItems($pageId, $data) {
$result = $this->getResult();
@@ -324,10 +332,13 @@ abstract class ApiQueryBase extends ApiBase {
* @return string Page title with underscores
*/
public function titleToKey($title) {
+ # Don't throw an error if we got an empty string
+ if(trim($title) == '')
+ return '';
$t = Title::newFromText($title);
if(!$t)
$this->dieUsageMsg(array('invalidtitle', $title));
- return $t->getDbKey();
+ return $t->getPrefixedDbKey();
}
/**
@@ -336,19 +347,40 @@ abstract class ApiQueryBase extends ApiBase {
* @return string Page title with spaces
*/
public function keyToTitle($key) {
+ # Don't throw an error if we got an empty string
+ if(trim($key) == '')
+ return '';
$t = Title::newFromDbKey($key);
# This really shouldn't happen but we gotta check anyway
if(!$t)
$this->dieUsageMsg(array('invalidtitle', $key));
return $t->getPrefixedText();
}
+
+ /**
+ * An alternative to titleToKey() that doesn't trim trailing spaces
+ * @param string $titlePart Title part with spaces
+ * @return string Title part with underscores
+ */
+ public function titlePartToKey($titlePart) {
+ return substr($this->titleToKey($titlePart . 'x'), 0, -1);
+ }
+
+ /**
+ * An alternative to keyToTitle() that doesn't trim trailing spaces
+ * @param string $keyPart Key part with spaces
+ * @return string Key part with underscores
+ */
+ public function keyPartToTitle($keyPart) {
+ return substr($this->keyToTitle($keyPart . 'x'), 0, -1);
+ }
/**
* Get version string for use in the API help output
* @return string
*/
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiQueryBase.php 37083 2008-07-05 11:18:50Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryBase.php 44461 2008-12-11 19:11:11Z ialex $';
}
}
diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php
index ebe87908..6f356cea 100644
--- a/includes/api/ApiQueryBlocks.php
+++ b/includes/api/ApiQueryBlocks.php
@@ -42,10 +42,6 @@ class ApiQueryBlocks extends ApiQueryBase {
}
public function execute() {
- $this->run();
- }
-
- private function run() {
global $wgUser;
$params = $this->extractRequestParams();
@@ -87,17 +83,17 @@ class ApiQueryBlocks extends ApiQueryBase {
if($fld_range)
$this->addFields(array('ipb_range_start', 'ipb_range_end'));
if($fld_flags)
- $this->addFields(array('ipb_auto', 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock', 'ipb_block_email', 'ipb_deleted'));
+ $this->addFields(array('ipb_auto', 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock', 'ipb_block_email', 'ipb_deleted', 'ipb_allow_usertalk'));
$this->addOption('LIMIT', $params['limit'] + 1);
$this->addWhereRange('ipb_timestamp', $params['dir'], $params['start'], $params['end']);
if(isset($params['ids']))
- $this->addWhere(array('ipb_id' => $params['ids']));
+ $this->addWhereFld('ipb_id', $params['ids']);
if(isset($params['users']))
{
foreach((array)$params['users'] as $u)
$this->prepareUsername($u);
- $this->addWhere(array('ipb_address' => $this->usernames));
+ $this->addWhereFld('ipb_address', $this->usernames);
}
if(isset($params['ip']))
{
@@ -120,19 +116,18 @@ class ApiQueryBlocks extends ApiQueryBase {
));
}
if(!$wgUser->isAllowed('suppress'))
- $this->addWhere(array('ipb_deleted' => 0));
+ $this->addWhereFld('ipb_deleted', 0);
// Purge expired entries on one in every 10 queries
if(!mt_rand(0, 10))
Block::purgeExpired();
$res = $this->select(__METHOD__);
- $db = wfGetDB();
$count = 0;
- while($row = $db->fetchObject($res))
+ while($row = $res->fetchObject())
{
- if($count++ == $params['limit'])
+ if(++$count > $params['limit'])
{
// We've had enough
$this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ipb_timestamp));
@@ -142,13 +137,9 @@ class ApiQueryBlocks extends ApiQueryBase {
if($fld_id)
$block['id'] = $row->ipb_id;
if($fld_user && !$row->ipb_auto)
- {
$block['user'] = $row->ipb_address;
- }
if($fld_by)
- {
$block['by'] = $row->user_name;
- }
if($fld_timestamp)
$block['timestamp'] = wfTimestamp(TS_ISO_8601, $row->ipb_timestamp);
if($fld_expiry)
@@ -157,8 +148,8 @@ class ApiQueryBlocks extends ApiQueryBase {
$block['reason'] = $row->ipb_reason;
if($fld_range)
{
- $block['rangestart'] = $this->convertHexIP($row->ipb_range_start);
- $block['rangeend'] = $this->convertHexIP($row->ipb_range_end);
+ $block['rangestart'] = IP::hexToQuad($row->ipb_range_start);
+ $block['rangeend'] = IP::hexToQuad($row->ipb_range_end);
}
if($fld_flags)
{
@@ -175,6 +166,8 @@ class ApiQueryBlocks extends ApiQueryBase {
$block['noemail'] = '';
if($row->ipb_deleted)
$block['hidden'] = '';
+ if($row->ipb_allow_usertalk)
+ $block['allowusertalk'] = '';
}
$data[] = $block;
}
@@ -194,19 +187,6 @@ class ApiQueryBlocks extends ApiQueryBase {
$this->usernames[] = $name;
}
- protected function convertHexIP($ip)
- {
- // Converts a hexadecimal IP to nnn.nnn.nnn.nnn format
- $dec = wfBaseConvert($ip, 16, 10);
- $parts[0] = (int)($dec / (256*256*256));
- $dec %= 256*256*256;
- $parts[1] = (int)($dec / (256*256));
- $dec %= 256*256;
- $parts[2] = (int)($dec / 256);
- $parts[3] = $dec % 256;
- return implode('.', $parts);
- }
-
public function getAllowedParams() {
return array (
'start' => array(
@@ -279,6 +259,6 @@ class ApiQueryBlocks extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryBlocks.php 37892 2008-07-21 21:37:11Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryBlocks.php 43676 2008-11-18 15:11:11Z catrope $';
}
}
diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php
index 51492d63..9c4e9b41 100644
--- a/includes/api/ApiQueryCategories.php
+++ b/includes/api/ApiQueryCategories.php
@@ -54,6 +54,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
$params = $this->extractRequestParams();
$prop = $params['prop'];
+ $show = array_flip((array)$params['show']);
$this->addFields(array (
'cl_from',
@@ -86,11 +87,31 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
$this->dieUsage("Invalid continue param. You should pass the " .
"original value returned by the previous query", "_badcontinue");
$clfrom = intval($cont[0]);
- $clto = $this->getDb()->strencode($this->titleToKey($cont[1]));
+ $clto = $this->getDB()->strencode($this->titleToKey($cont[1]));
$this->addWhere("cl_from > $clfrom OR ".
"(cl_from = $clfrom AND ".
"cl_to >= '$clto')");
}
+ if(isset($show['hidden']) && isset($show['!hidden']))
+ $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show');
+ if(isset($show['hidden']) || isset($show['!hidden']))
+ {
+ $this->addOption('STRAIGHT_JOIN');
+ $this->addTables(array('page', 'page_props'));
+ $this->addJoinConds(array(
+ 'page' => array('LEFT JOIN', array(
+ 'page_namespace' => NS_CATEGORY,
+ 'page_title = cl_to')),
+ 'page_props' => array('LEFT JOIN', array(
+ 'pp_page=page_id',
+ 'pp_propname' => 'hiddencat'))
+ ));
+ if(isset($show['hidden']))
+ $this->addWhere(array('pp_propname IS NOT NULL'));
+ else
+ $this->addWhere(array('pp_propname IS NULL'));
+ }
+
# Don't order by cl_from if it's constant in the WHERE clause
if(count($this->getPageSet()->getGoodTitles()) == 1)
$this->addOption('ORDER BY', 'cl_to');
@@ -128,7 +149,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
if ($fld_sortkey)
$vals['sortkey'] = $row->cl_sortkey;
if ($fld_timestamp)
- $vals['timestamp'] = $row->cl_timestamp;
+ $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->cl_timestamp);
$data[] = $vals;
}
@@ -166,6 +187,13 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
'timestamp',
)
),
+ 'show' => array(
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array(
+ 'hidden',
+ '!hidden',
+ )
+ ),
'limit' => array(
ApiBase :: PARAM_DFLT => 10,
ApiBase :: PARAM_TYPE => 'limit',
@@ -181,6 +209,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
return array (
'prop' => 'Which additional properties to get for each category.',
'limit' => 'How many categories to return',
+ 'show' => 'Which kind of categories to show',
'continue' => 'When more results are available, use this to continue',
);
}
@@ -199,6 +228,6 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryCategories.php 37909 2008-07-22 13:26:15Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryCategories.php 44585 2008-12-14 17:39:50Z catrope $';
}
}
diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php
index f809bb15..f83c4a5b 100644
--- a/includes/api/ApiQueryCategoryInfo.php
+++ b/includes/api/ApiQueryCategoryInfo.php
@@ -41,9 +41,10 @@ class ApiQueryCategoryInfo extends ApiQueryBase {
public function execute() {
$alltitles = $this->getPageSet()->getAllTitlesByNamespace();
- $categories = $alltitles[NS_CATEGORY];
- if(empty($categories))
+ if ( empty( $alltitles[NS_CATEGORY] ) ) {
return;
+ }
+ $categories = $alltitles[NS_CATEGORY];
$titles = $this->getPageSet()->getGoodTitles() +
$this->getPageSet()->getMissingTitles();
@@ -51,11 +52,19 @@ class ApiQueryCategoryInfo extends ApiQueryBase {
foreach($categories as $c)
{
$t = $titles[$c];
- $cattitles[$c] = $t->getDbKey();
+ $cattitles[$c] = $t->getDBKey();
}
- $this->addTables('category');
- $this->addFields(array('cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'cat_hidden'));
+ $this->addTables(array('category', 'page', 'page_props'));
+ $this->addJoinConds(array(
+ 'page' => array('LEFT JOIN', array(
+ 'page_namespace' => NS_CATEGORY,
+ 'page_title=cat_title')),
+ 'page_props' => array('LEFT JOIN', array(
+ 'pp_page=page_id',
+ 'pp_propname' => 'hiddencat')),
+ ));
+ $this->addFields(array('cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'pp_propname AS cat_hidden'));
$this->addWhere(array('cat_title' => $cattitles));
$db = $this->getDB();
@@ -86,6 +95,6 @@ class ApiQueryCategoryInfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryCategoryInfo.php 37504 2008-07-10 14:28:09Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryCategoryInfo.php 44590 2008-12-14 20:24:23Z catrope $';
}
}
diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php
index 3909b213..e2f577a2 100644
--- a/includes/api/ApiQueryCategoryMembers.php
+++ b/includes/api/ApiQueryCategoryMembers.php
@@ -76,17 +76,9 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
$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');
- // cl_timestamp will be added by addWhereRange() later
- $this->addOption('ORDER BY', 'cl_to');
- }
else
- {
- $dir = ($params['dir'] == 'desc' ? ' DESC' : '');
$this->addOption('USE INDEX', 'cl_sortkey');
- $this->addOption('ORDER BY', 'cl_to, cl_sortkey' . $dir . ', cl_from' . $dir);
- }
$this->addWhere('cl_from=page_id');
$this->setContinuation($params['continue'], $params['dir']);
@@ -94,6 +86,11 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
$this->addWhereFld('page_namespace', $params['namespace']);
if($params['sort'] == 'timestamp')
$this->addWhereRange('cl_timestamp', ($params['dir'] == 'asc' ? 'newer' : 'older'), $params['start'], $params['end']);
+ else
+ {
+ $this->addWhereRange('cl_sortkey', ($params['dir'] == 'asc' ? 'newer' : 'older'), $params['startsortkey'], $params['endsortkey']);
+ $this->addWhereRange('cl_from', ($params['dir'] == 'asc' ? 'newer' : 'older'), null, null);
+ }
$limit = $params['limit'];
$this->addOption('LIMIT', $limit +1);
@@ -157,18 +154,15 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
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);
- }
+ $pos = strrpos($continue, '|');
+ $sortkey = substr($continue, 0, $pos);
+ $fromstr = substr($continue, $pos + 1);
+ $from = intval($fromstr);
- if ($hasError)
+ if ($from == 0 && strlen($fromstr) > 0)
$this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "badcontinue");
- $encSortKey = $this->getDB()->addQuotes($continueList[0]);
+ $encSortKey = $this->getDB()->addQuotes($sortkey);
$encFrom = $this->getDB()->addQuotes($from);
$op = ($dir == 'desc' ? '<' : '>');
@@ -225,7 +219,9 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
),
'end' => array(
ApiBase :: PARAM_TYPE => 'timestamp'
- )
+ ),
+ 'startsortkey' => null,
+ 'endsortkey' => null,
);
}
@@ -238,6 +234,8 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
'dir' => 'In which direction to sort',
'start' => 'Timestamp to start listing from. Can only be used with cmsort=timestamp',
'end' => 'Timestamp to end listing at. Can only be used with cmsort=timestamp',
+ 'startsortkey' => 'Sortkey to start listing from. Can only be used with cmsort=sortkey',
+ 'endsortkey' => 'Sortkey to end listing at. Can only be used with cmsort=sortkey',
'continue' => 'For large categories, give the value retured from previous query',
'limit' => 'The maximum number of pages to return.',
);
@@ -257,6 +255,6 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 42197 2008-10-18 10:09:19Z ialex $';
}
}
diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php
index 8368896d..408421c4 100644
--- a/includes/api/ApiQueryDeletedrevs.php
+++ b/includes/api/ApiQueryDeletedrevs.php
@@ -107,6 +107,8 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$lb = new LinkBatch($titles);
$where = $lb->constructSet('ar', $db);
$this->addWhere($where);
+ } else {
+ $this->dieUsage('You have to specify a page title or titles');
}
$this->addOption('LIMIT', $limit + 1);
@@ -228,6 +230,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 37502 2008-07-10 14:13:11Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 40798 2008-09-13 20:41:58Z aaron $';
}
}
diff --git a/includes/api/ApiQueryDisabled.php b/includes/api/ApiQueryDisabled.php
new file mode 100644
index 00000000..50825464
--- /dev/null
+++ b/includes/api/ApiQueryDisabled.php
@@ -0,0 +1,72 @@
+<?php
+
+/*
+ * Created on Sep 25, 2008
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiBase.php");
+}
+
+
+/**
+ * API module that does nothing
+ *
+ * Use this to disable core modules with e.g.
+ * $wgAPIPropModules['modulename'] = 'ApiQueryDisabled';
+ *
+ * To disable top-level modules, use ApiDisabled instead
+ *
+ * @ingroup API
+ */
+class ApiQueryDisabled extends ApiQueryBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ public function execute() {
+ $this->setWarning("The ``{$this->getModuleName()}'' module has been disabled.");
+ }
+
+ public function getAllowedParams() {
+ return array ();
+ }
+
+ public function getParamDescription() {
+ return array ();
+ }
+
+ public function getDescription() {
+ return array(
+ 'This module has been disabled.'
+ );
+ }
+
+ protected function getExamples() {
+ return array ();
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryDisabled.php 41268 2008-09-25 20:50:50Z catrope $';
+ }
+}
diff --git a/includes/api/ApiQueryDuplicateFiles.php b/includes/api/ApiQueryDuplicateFiles.php
new file mode 100644
index 00000000..5f7d7ee0
--- /dev/null
+++ b/includes/api/ApiQueryDuplicateFiles.php
@@ -0,0 +1,164 @@
+<?php
+
+/*
+ * Created on Sep 27, 2008
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2008 Roan Kattow <Firstname>,<Lastname>@home.nl
+ *
+ * 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 duplicates of the given file(s)
+ *
+ * @ingroup API
+ */
+class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'df');
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function executeGenerator($resultPageSet) {
+ $this->run($resultPageSet);
+ }
+
+ private function run($resultPageSet = null) {
+ $params = $this->extractRequestParams();
+ $namespaces = $this->getPageSet()->getAllTitlesByNamespace();
+ if ( empty( $namespaces[NS_FILE] ) ) {
+ return;
+ }
+ $images = $namespaces[NS_FILE];
+
+ $this->addTables('image', 'i1');
+ $this->addTables('image', 'i2');
+ $this->addFields(array(
+ 'i1.img_name AS orig_name',
+ 'i2.img_name AS dup_name',
+ 'i2.img_user_text AS dup_user_text',
+ 'i2.img_timestamp AS dup_timestamp'
+ ));
+ $this->addWhere(array(
+ 'i1.img_name' => array_keys($images),
+ 'i1.img_sha1 = i2.img_sha1',
+ 'i1.img_name != i2.img_name',
+ ));
+ if(isset($params['continue']))
+ {
+ $cont = explode('|', $params['continue']);
+ if(count($cont) != 2)
+ $this->dieUsage("Invalid continue param. You should pass the " .
+ "original value returned by the previous query", "_badcontinue");
+ $orig = $this->getDB()->strencode($this->titleTokey($cont[0]));
+ $dup = $this->getDB()->strencode($this->titleToKey($cont[1]));
+ $this->addWhere("i1.img_name > '$orig' OR ".
+ "(i1.img_name = '$orig' AND ".
+ "i2.img_name >= '$dup')");
+ }
+ $this->addOption('ORDER BY', 'i1.img_name');
+ $this->addOption('LIMIT', $params['limit'] + 1);
+
+ $res = $this->select(__METHOD__);
+ $db = $this->getDB();
+ $count = 0;
+ $data = array();
+ $titles = array();
+ $lastName = '';
+ while($row = $db->fetchObject($res))
+ {
+ if(++$count > $params['limit'])
+ {
+ // We've reached the one extra which shows that
+ // there are additional pages to be had. Stop here...
+ $this->setContinueEnumParameter('continue',
+ $this->keyToTitle($row->orig_name) . '|' .
+ $this->keyToTitle($row->dup_name));
+ break;
+ }
+ if(!is_null($resultPageSet))
+ $titles[] = Title::makeTitle(NS_FILE, $row->dup_name);
+ else
+ {
+ if($row->orig_name != $lastName)
+ {
+ if($lastName != '')
+ {
+ $this->addPageSubItems($images[$lastName], $data);
+ $data = array();
+ }
+ $lastName = $row->orig_name;
+ }
+
+ $data[] = array(
+ 'name' => $row->dup_name,
+ 'user' => $row->dup_user_text,
+ 'timestamp' => wfTimestamp(TS_ISO_8601, $row->dup_timestamp)
+ );
+ }
+ }
+ if(!is_null($resultPageSet))
+ $resultPageSet->populateFromTitles($titles);
+ else if($lastName != '')
+ $this->addPageSubItems($images[$lastName], $data);
+ $db->freeResult($res);
+ }
+
+ public function getAllowedParams() {
+ return array (
+ '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
+ ),
+ 'continue' => null,
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'limit' => 'How many files to return',
+ 'continue' => 'When more results are available, use this to continue',
+ );
+ }
+
+ public function getDescription() {
+ return 'List all files that are duplicates of the given file(s).';
+ }
+
+ protected function getExamples() {
+ return array ( 'api.php?action=query&titles=Image:Albert_Einstein_Head.jpg&prop=duplicatefiles',
+ 'api.php?action=query&generator=allimages&prop=duplicatefiles',
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryDuplicateFiles.php 44121 2008-12-01 17:14:30Z vyznev $';
+ }
+}
diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php
index 8ffb7246..85e21f42 100644
--- a/includes/api/ApiQueryExtLinksUsage.php
+++ b/includes/api/ApiQueryExtLinksUsage.php
@@ -54,7 +54,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
// Find the right prefix
global $wgUrlProtocols;
- if(!is_null($protocol) && !empty($protocol) && !in_array($protocol, $wgUrlProtocols))
+ if($protocol && !in_array($protocol, $wgUrlProtocols))
{
foreach ($wgUrlProtocols as $p) {
if( substr( $p, 0, strlen( $protocol ) ) === $protocol ) {
@@ -66,7 +66,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
else
$protocol = null;
- $db = $this->getDb();
+ $db = $this->getDB();
$this->addTables(array('page','externallinks')); // must be in this order for 'USE INDEX'
$this->addOption('USE INDEX', 'el_index');
$this->addWhere('page_id=el_from');
@@ -206,6 +206,6 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 37909 2008-07-22 13:26:15Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 43271 2008-11-06 22:38:42Z siebrand $';
}
}
diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php
index 33ff1d3f..612d5cc9 100644
--- a/includes/api/ApiQueryImageInfo.php
+++ b/includes/api/ApiQueryImageInfo.php
@@ -56,10 +56,10 @@ class ApiQueryImageInfo extends ApiQueryBase {
}
$pageIds = $this->getPageSet()->getAllTitlesByNamespace();
- if (!empty($pageIds[NS_IMAGE])) {
+ if (!empty($pageIds[NS_FILE])) {
$result = $this->getResult();
- $images = RepoGroup::singleton()->findFiles( array_keys( $pageIds[NS_IMAGE] ) );
+ $images = RepoGroup::singleton()->findFiles( array_keys( $pageIds[NS_FILE] ) );
foreach ( $images as $img ) {
$data = array();
@@ -78,14 +78,14 @@ class ApiQueryImageInfo extends ApiQueryBase {
if(++$count > $params['limit']) {
// We've reached the extra one which shows that there are additional pages to be had. Stop here...
// Only set a query-continue if there was only one title
- if(count($pageIds[NS_IMAGE]) == 1)
+ if(count($pageIds[NS_FILE]) == 1)
$this->setContinueEnumParameter('start', $oldie->getTimestamp());
break;
}
$data[] = self::getInfo( $oldie, $prop, $result );
}
- $pageId = $pageIds[NS_IMAGE][ $img->getOriginalTitle()->getDBkey() ];
+ $pageId = $pageIds[NS_FILE][ $img->getOriginalTitle()->getDBkey() ];
$result->addValue(
array( 'query', 'pages', intval( $pageId ) ),
'imagerepository', $img->getRepoName()
@@ -93,10 +93,10 @@ class ApiQueryImageInfo extends ApiQueryBase {
$this->addPageSubItems($pageId, $data);
}
- $missing = array_diff( array_keys( $pageIds[NS_IMAGE] ), array_keys( $images ) );
+ $missing = array_diff( array_keys( $pageIds[NS_FILE] ), array_keys( $images ) );
foreach ( $missing as $title )
$result->addValue(
- array( 'query', 'pages', intval( $pageIds[NS_IMAGE][$title] ) ),
+ array( 'query', 'pages', intval( $pageIds[NS_FILE][$title] ) ),
'imagerepository', ''
);
}
@@ -123,12 +123,12 @@ class ApiQueryImageInfo extends ApiQueryBase {
}
if( isset( $prop['url'] ) ) {
if( !is_null( $scale ) && !$file->isOld() ) {
- $thumb = $file->getThumbnail( $scale['width'], $scale['height'] );
- if( $thumb )
+ $mto = $file->transform( array( 'width' => $scale['width'], 'height' => $scale['height'] ) );
+ if( $mto && !$mto->isError() )
{
- $vals['thumburl'] = wfExpandUrl( $thumb->getURL() );
- $vals['thumbwidth'] = $thumb->getWidth();
- $vals['thumbheight'] = $thumb->getHeight();
+ $vals['thumburl'] = $mto->getUrl();
+ $vals['thumbwidth'] = $mto->getWidth();
+ $vals['thumbheight'] = $mto->getHeight();
}
}
$vals['url'] = $file->getFullURL();
@@ -148,6 +148,9 @@ class ApiQueryImageInfo extends ApiQueryBase {
if( isset( $prop['archivename'] ) && $file->isOld() )
$vals['archivename'] = $file->getArchiveName();
+
+ if( isset( $prop['bitdepth'] ) )
+ $vals['bitdepth'] = $file->getBitDepth();
return $vals;
}
@@ -166,7 +169,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
'sha1',
'mime',
'metadata',
- 'archivename'
+ 'archivename',
+ 'bitdepth',
)
),
'limit' => array(
@@ -219,6 +223,6 @@ class ApiQueryImageInfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryImageInfo.php 37504 2008-07-10 14:28:09Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryImageInfo.php 44121 2008-12-01 17:14:30Z vyznev $';
}
}
diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php
index 32c4e1b0..02fe24f1 100644
--- a/includes/api/ApiQueryImages.php
+++ b/includes/api/ApiQueryImages.php
@@ -66,7 +66,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
$this->dieUsage("Invalid continue param. You should pass the " .
"original value returned by the previous query", "_badcontinue");
$ilfrom = intval($cont[0]);
- $ilto = $this->getDb()->strencode($this->titleToKey($cont[1]));
+ $ilto = $this->getDB()->strencode($this->titleToKey($cont[1]));
$this->addWhere("il_from > $ilfrom OR ".
"(il_from = $ilfrom AND ".
"il_to >= '$ilto')");
@@ -103,7 +103,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
}
$vals = array();
- ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle(NS_IMAGE, $row->il_to));
+ ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle(NS_FILE, $row->il_to));
$data[] = $vals;
}
@@ -123,7 +123,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
'|' . $this->keyToTitle($row->il_to));
break;
}
- $titles[] = Title :: makeTitle(NS_IMAGE, $row->il_to);
+ $titles[] = Title :: makeTitle(NS_FILE, $row->il_to);
}
$resultPageSet->populateFromTitles($titles);
}
@@ -165,6 +165,6 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryImages.php 37535 2008-07-10 21:20:43Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryImages.php 44121 2008-12-01 17:14:30Z vyznev $';
}
}
diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php
index 9c6487b3..0c5c72fc 100644
--- a/includes/api/ApiQueryInfo.php
+++ b/includes/api/ApiQueryInfo.php
@@ -68,7 +68,8 @@ class ApiQueryInfo extends ApiQueryBase {
'protect' => array( 'ApiQueryInfo', 'getProtectToken' ),
'move' => array( 'ApiQueryInfo', 'getMoveToken' ),
'block' => array( 'ApiQueryInfo', 'getBlockToken' ),
- 'unblock' => array( 'ApiQueryInfo', 'getUnblockToken' )
+ 'unblock' => array( 'ApiQueryInfo', 'getUnblockToken' ),
+ 'email' => array( 'ApiQueryInfo', 'getEmailToken' ),
);
wfRunHooks('APIQueryInfoTokens', array(&$this->tokenFunctions));
return $this->tokenFunctions;
@@ -153,17 +154,33 @@ class ApiQueryInfo extends ApiQueryBase {
return self::getBlockToken($pageid, $title);
}
+ public static function getEmailToken($pageid, $title)
+ {
+ global $wgUser;
+ if(!$wgUser->canSendEmail() || $wgUser->isBlockedFromEmailUser())
+ return false;
+
+ static $cachedEmailToken = null;
+ if(!is_null($cachedEmailToken))
+ return $cachedEmailToken;
+
+ $cachedEmailToken = $wgUser->editToken();
+ return $cachedEmailToken;
+ }
+
public function execute() {
global $wgUser;
$params = $this->extractRequestParams();
- $fld_protection = $fld_talkid = $fld_subjectid = false;
+ $fld_protection = $fld_talkid = $fld_subjectid = $fld_url = $fld_readable = false;
if(!is_null($params['prop'])) {
$prop = array_flip($params['prop']);
$fld_protection = isset($prop['protection']);
$fld_talkid = isset($prop['talkid']);
$fld_subjectid = isset($prop['subjectid']);
+ $fld_url = isset($prop['url']);
+ $fld_readable = isset($prop['readable']);
}
$pageSet = $this->getPageSet();
@@ -180,7 +197,7 @@ class ApiQueryInfo extends ApiQueryBase {
$pageLength = $pageSet->getCustomField('page_len');
$db = $this->getDB();
- if ($fld_protection && !empty($titles)) {
+ if ($fld_protection && count($titles)) {
$this->addTables('page_restrictions');
$this->addFields(array('pr_page', 'pr_type', 'pr_level', 'pr_expiry', 'pr_cascade'));
$this->addWhereFld('pr_page', array_keys($titles));
@@ -195,12 +212,44 @@ class ApiQueryInfo extends ApiQueryBase {
if($row->pr_cascade)
$a['cascade'] = '';
$protections[$row->pr_page][] = $a;
+
+ # Also check old restrictions
+ if($pageRestrictions[$row->pr_page]) {
+ foreach(explode(':', trim($pageRestrictions[$pageid])) as $restrict) {
+ $temp = explode('=', trim($restrict));
+ if(count($temp) == 1) {
+ // old old format should be treated as edit/move restriction
+ $restriction = trim( $temp[0] );
+ if($restriction == '')
+ continue;
+ $protections[$row->pr_page][] = array(
+ 'type' => 'edit',
+ 'level' => $restriction,
+ 'expiry' => 'infinity',
+ );
+ $protections[$row->pr_page][] = array(
+ 'type' => 'move',
+ 'level' => $restriction,
+ 'expiry' => 'infinity',
+ );
+ } else {
+ $restriction = trim( $temp[1] );
+ if($restriction == '')
+ continue;
+ $protections[$row->pr_page][] = array(
+ 'type' => $temp[0],
+ 'level' => $restriction,
+ 'expiry' => 'infinity',
+ );
+ }
+ }
+ }
}
$db->freeResult($res);
$imageIds = array();
foreach ($titles as $id => $title)
- if ($title->getNamespace() == NS_IMAGE)
+ if ($title->getNamespace() == NS_FILE)
$imageIds[] = $id;
// To avoid code duplication
$cascadeTypes = array(
@@ -214,7 +263,7 @@ class ApiQueryInfo extends ApiQueryBase {
array(
'prefix' => 'il',
'table' => 'imagelinks',
- 'ns' => NS_IMAGE,
+ 'ns' => NS_FILE,
'title' => 'il_to',
'ids' => $imageIds
)
@@ -256,7 +305,7 @@ class ApiQueryInfo extends ApiQueryBase {
}
// We don't need to check for pt stuff if there are no nonexistent titles
- if($fld_protection && !empty($missing))
+ if($fld_protection && count($missing))
{
$this->resetQueryParams();
// Construct a custom WHERE clause that matches all titles in $missing
@@ -278,8 +327,8 @@ class ApiQueryInfo extends ApiQueryBase {
$images = array();
$others = array();
foreach ($missing as $title)
- if ($title->getNamespace() == NS_IMAGE)
- $images[] = $title->getDbKey();
+ if ($title->getNamespace() == NS_FILE)
+ $images[] = $title->getDBKey();
else
$others[] = $title;
@@ -328,7 +377,7 @@ class ApiQueryInfo extends ApiQueryBase {
'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ),
'source' => $source->getPrefixedText()
);
- $prottitles[NS_IMAGE][$row->il_to][] = $a;
+ $prottitles[NS_FILE][$row->il_to][] = $a;
}
$db->freeResult($res);
}
@@ -350,7 +399,7 @@ class ApiQueryInfo extends ApiQueryBase {
else if($fld_talkid)
$talktitles[] = $t->getTalkPage();
}
- if(!empty($talktitles) || !empty($subjecttitles))
+ if(count($talktitles) || count($subjecttitles))
{
// Construct a custom WHERE clause that matches
// all titles in $talktitles and $subjecttitles
@@ -386,6 +435,7 @@ class ApiQueryInfo extends ApiQueryBase {
if (!is_null($params['token'])) {
$tokenFunctions = $this->getTokenFunctions();
+ $pageInfo['starttimestamp'] = wfTimestamp(TS_ISO_8601, time());
foreach($params['token'] as $t)
{
$val = call_user_func($tokenFunctions[$t], $pageid, $title);
@@ -397,46 +447,23 @@ class ApiQueryInfo extends ApiQueryBase {
}
if($fld_protection) {
+ $pageInfo['protection'] = array();
if (isset($protections[$pageid])) {
$pageInfo['protection'] = $protections[$pageid];
$result->setIndexedTagName($pageInfo['protection'], 'pr');
- } else {
- # Also check old restrictions
- if( $pageRestrictions[$pageid] ) {
- foreach( explode( ':', trim( $pageRestrictions[$pageid] ) ) as $restrict ) {
- $temp = explode( '=', trim( $restrict ) );
- if(count($temp) == 1) {
- // old old format should be treated as edit/move restriction
- $restriction = trim( $temp[0] );
- $pageInfo['protection'][] = array(
- 'type' => 'edit',
- 'level' => $restriction,
- 'expiry' => 'infinity',
- );
- $pageInfo['protection'][] = array(
- 'type' => 'move',
- 'level' => $restriction,
- 'expiry' => 'infinity',
- );
- } else {
- $restriction = trim( $temp[1] );
- $pageInfo['protection'][] = array(
- 'type' => $temp[0],
- 'level' => $restriction,
- 'expiry' => 'infinity',
- );
- }
- }
- $result->setIndexedTagName($pageInfo['protection'], 'pr');
- } else {
- $pageInfo['protection'] = array();
- }
}
}
- if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDbKey()]))
- $pageInfo['talkid'] = $talkids[$title->getNamespace()][$title->getDbKey()];
- if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDbKey()]))
- $pageInfo['subjectid'] = $subjectids[$title->getNamespace()][$title->getDbKey()];
+ if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDBKey()]))
+ $pageInfo['talkid'] = $talkids[$title->getNamespace()][$title->getDBKey()];
+ if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDBKey()]))
+ $pageInfo['subjectid'] = $subjectids[$title->getNamespace()][$title->getDBKey()];
+ if($fld_url) {
+ $pageInfo['fullurl'] = $title->getFullURL();
+ $pageInfo['editurl'] = $title->getFullURL('action=edit');
+ }
+ if($fld_readable)
+ if($title->userCanRead())
+ $pageInfo['readable'] = '';
$result->addValue(array (
'query',
@@ -444,19 +471,22 @@ class ApiQueryInfo extends ApiQueryBase {
), $pageid, $pageInfo);
}
- // Get edit/protect tokens and protection data for missing titles if requested
- // Delete and move tokens are N/A for missing titles anyway
- if(!is_null($params['token']) || $fld_protection || $fld_talkid || $fld_subjectid)
+ // Get properties for missing titles if requested
+ if(!is_null($params['token']) || $fld_protection || $fld_talkid || $fld_subjectid ||
+ $fld_url || $fld_readable)
{
$res = &$result->getData();
foreach($missing as $pageid => $title) {
if(!is_null($params['token']))
{
$tokenFunctions = $this->getTokenFunctions();
+ $res['query']['pages'][$pageid]['starttimestamp'] = wfTimestamp(TS_ISO_8601, time());
foreach($params['token'] as $t)
{
$val = call_user_func($tokenFunctions[$t], $pageid, $title);
- if($val !== false)
+ if($val === false)
+ $this->setWarning("Action '$t' is not allowed for the current user");
+ else
$res['query']['pages'][$pageid][$t . 'token'] = $val;
}
}
@@ -470,10 +500,17 @@ class ApiQueryInfo extends ApiQueryBase {
$res['query']['pages'][$pageid]['protection'] = array();
$result->setIndexedTagName($res['query']['pages'][$pageid]['protection'], 'pr');
}
- if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDbKey()]))
- $res['query']['pages'][$pageid]['talkid'] = $talkids[$title->getNamespace()][$title->getDbKey()];
- if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDbKey()]))
- $res['query']['pages'][$pageid]['subjectid'] = $subjectids[$title->getNamespace()][$title->getDbKey()];
+ if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDBKey()]))
+ $res['query']['pages'][$pageid]['talkid'] = $talkids[$title->getNamespace()][$title->getDBKey()];
+ if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDBKey()]))
+ $res['query']['pages'][$pageid]['subjectid'] = $subjectids[$title->getNamespace()][$title->getDBKey()];
+ if($fld_url) {
+ $res['query']['pages'][$pageid]['fullurl'] = $title->getFullURL();
+ $res['query']['pages'][$pageid]['editurl'] = $title->getFullURL('action=edit');
+ }
+ if($fld_readable)
+ if($title->userCanRead())
+ $res['query']['pages'][$pageid]['readable'] = '';
}
}
}
@@ -486,7 +523,9 @@ class ApiQueryInfo extends ApiQueryBase {
ApiBase :: PARAM_TYPE => array (
'protection',
'talkid',
- 'subjectid'
+ 'subjectid',
+ 'url',
+ 'readable',
)),
'token' => array (
ApiBase :: PARAM_DFLT => NULL,
@@ -521,6 +560,6 @@ class ApiQueryInfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryInfo.php 37191 2008-07-06 18:43:06Z brion $';
+ return __CLASS__ . ': $Id: ApiQueryInfo.php 45683 2009-01-12 19:10:42Z raymond $';
}
}
diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php
index e7d84fc3..8eaf8d02 100644
--- a/includes/api/ApiQueryLangLinks.php
+++ b/includes/api/ApiQueryLangLinks.php
@@ -58,7 +58,7 @@ class ApiQueryLangLinks extends ApiQueryBase {
$this->dieUsage("Invalid continue param. You should pass the " .
"original value returned by the previous query", "_badcontinue");
$llfrom = intval($cont[0]);
- $lllang = $this->getDb()->strencode($cont[1]);
+ $lllang = $this->getDB()->strencode($cont[1]);
$this->addWhere("ll_from > $llfrom OR ".
"(ll_from = $llfrom AND ".
"ll_lang >= '$lllang')");
@@ -134,6 +134,6 @@ class ApiQueryLangLinks extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryLangLinks.php 37534 2008-07-10 21:08:37Z brion $';
+ return __CLASS__ . ': $Id: ApiQueryLangLinks.php 43271 2008-11-06 22:38:42Z siebrand $';
}
}
diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php
index 546a599d..91b5b529 100644
--- a/includes/api/ApiQueryLinks.php
+++ b/includes/api/ApiQueryLinks.php
@@ -76,9 +76,9 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
$params = $this->extractRequestParams();
$this->addFields(array (
- $this->prefix . '_from pl_from',
- $this->prefix . '_namespace pl_namespace',
- $this->prefix . '_title pl_title'
+ $this->prefix . '_from AS pl_from',
+ $this->prefix . '_namespace AS pl_namespace',
+ $this->prefix . '_title AS pl_title'
));
$this->addTables($this->table);
@@ -92,7 +92,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
"original value returned by the previous query", "_badcontinue");
$plfrom = intval($cont[0]);
$plns = intval($cont[1]);
- $pltitle = $this->getDb()->strencode($this->titleToKey($cont[2]));
+ $pltitle = $this->getDB()->strencode($this->titleToKey($cont[2]));
$this->addWhere("{$this->prefix}_from > $plfrom OR ".
"({$this->prefix}_from = $plfrom AND ".
"({$this->prefix}_namespace > $plns OR ".
@@ -213,6 +213,6 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryLinks.php 37909 2008-07-22 13:26:15Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryLinks.php 43271 2008-11-06 22:38:42Z siebrand $';
}
}
diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php
index 47a526bb..83c73b83 100644
--- a/includes/api/ApiQueryLogEvents.php
+++ b/includes/api/ApiQueryLogEvents.php
@@ -93,16 +93,15 @@ class ApiQueryLogEvents extends ApiQueryBase {
$limit = $params['limit'];
$this->addOption('LIMIT', $limit +1);
-
+
+ $index = false;
$user = $params['user'];
if (!is_null($user)) {
- $userid = $db->selectField('user', 'user_id', array (
- 'user_name' => $user
- ));
+ $userid = User::idFromName($user);
if (!$userid)
$this->dieUsage("User name $user not found", 'param_user');
$this->addWhereFld('log_user', $userid);
- $this->addOption('USE INDEX', array('logging' => array('user_time','page_time')));
+ $index = 'user_time';
}
$title = $params['title'];
@@ -112,8 +111,14 @@ class ApiQueryLogEvents extends ApiQueryBase {
$this->dieUsage("Bad title value '$title'", 'param_title');
$this->addWhereFld('log_namespace', $titleObj->getNamespace());
$this->addWhereFld('log_title', $titleObj->getDBkey());
- $this->addOption('USE INDEX', array('logging' => array('user_time','page_time')));
+
+ // Use the title index in preference to the user index if there is a conflict
+ $index = 'page_time';
}
+ if ( $index ) {
+ $this->addOption( 'USE INDEX', array( 'logging' => $index ) );
+ }
+
$data = array ();
$count = 0;
@@ -134,6 +139,48 @@ class ApiQueryLogEvents extends ApiQueryBase {
$this->getResult()->setIndexedTagName($data, 'item');
$this->getResult()->addValue('query', $this->getModuleName(), $data);
}
+
+ public static function addLogParams($result, &$vals, $params, $type, $ts) {
+ $params = explode("\n", $params);
+ switch ($type) {
+ case 'move':
+ if (isset ($params[0])) {
+ $title = Title :: newFromText($params[0]);
+ if ($title) {
+ $vals2 = array();
+ ApiQueryBase :: addTitleInfo($vals2, $title, "new_");
+ $vals[$type] = $vals2;
+ $params = null;
+ }
+ }
+ break;
+ case 'patrol':
+ $vals2 = array();
+ list( $vals2['cur'], $vals2['prev'], $vals2['auto'] ) = $params;
+ $vals[$type] = $vals2;
+ $params = null;
+ break;
+ case 'rights':
+ $vals2 = array();
+ list( $vals2['old'], $vals2['new'] ) = $params;
+ $vals[$type] = $vals2;
+ $params = null;
+ break;
+ case 'block':
+ $vals2 = array();
+ list( $vals2['duration'], $vals2['flags'] ) = $params;
+ $vals2['expiry'] = wfTimestamp(TS_ISO_8601,
+ strtotime($params[0], wfTimestamp(TS_UNIX, $ts)));
+ $vals[$type] = $vals2;
+ $params = null;
+ break;
+ }
+ if (!is_null($params)) {
+ $result->setIndexedTagName($params, 'param');
+ $vals = array_merge($vals, $params);
+ }
+ return $vals;
+ }
private function extractRowInfo($row) {
$vals = array();
@@ -154,43 +201,9 @@ class ApiQueryLogEvents extends ApiQueryBase {
}
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);
- }
+ self::addLogParams($this->getResult(), $vals,
+ $row->log_params, $row->log_type,
+ $row->log_timestamp);
}
if ($this->fld_user) {
@@ -201,7 +214,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
if ($this->fld_timestamp) {
$vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->log_timestamp);
}
- if ($this->fld_comment && !empty ($row->log_comment)) {
+ if ($this->fld_comment && isset($row->log_comment)) {
$vals['comment'] = $row->log_comment;
}
@@ -277,6 +290,6 @@ class ApiQueryLogEvents extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryLogEvents.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiQueryLogEvents.php 44234 2008-12-04 15:59:26Z catrope $';
}
}
diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php
index 046157a6..e7b8bf46 100644
--- a/includes/api/ApiQueryRandom.php
+++ b/includes/api/ApiQueryRandom.php
@@ -48,13 +48,13 @@ if (!defined('MEDIAWIKI')) {
$this->run($resultPageSet);
}
- protected function prepareQuery($randstr, $limit, $namespace, &$resultPageSet) {
+ protected function prepareQuery($randstr, $limit, $namespace, &$resultPageSet, $redirect) {
$this->resetQueryParams();
$this->addTables('page');
$this->addOption('LIMIT', $limit);
$this->addWhereFld('page_namespace', $namespace);
$this->addWhereRange('page_random', 'newer', $randstr, null);
- $this->addWhere(array('page_is_redirect' => 0));
+ $this->addWhereFld('page_is_redirect', $redirect);
$this->addOption('USE INDEX', 'page_random');
if(is_null($resultPageSet))
$this->addFields(array('page_id', 'page_title', 'page_namespace'));
@@ -89,7 +89,8 @@ if (!defined('MEDIAWIKI')) {
$result = $this->getResult();
$data = array();
$this->pageIDs = array();
- $this->prepareQuery(wfRandom(), $params['limit'], $params['namespace'], $resultPageSet);
+
+ $this->prepareQuery(wfRandom(), $params['limit'], $params['namespace'], $resultPageSet, $params['redirect']);
$count = $this->runQuery($data, $resultPageSet);
if($count < $params['limit'])
{
@@ -97,7 +98,7 @@ if (!defined('MEDIAWIKI')) {
* for page_random. We'll just take the lowest ones, see
* also the comment in Title::getRandomTitle()
*/
- $this->prepareQuery(0, $params['limit'] - $count, $params['namespace'], $resultPageSet);
+ $this->prepareQuery(0, $params['limit'] - $count, $params['namespace'], $resultPageSet, $params['redirect']);
$this->runQuery($data, $resultPageSet);
}
@@ -129,13 +130,15 @@ if (!defined('MEDIAWIKI')) {
ApiBase :: PARAM_MAX => 10,
ApiBase :: PARAM_MAX2 => 20
),
+ 'redirect' => false,
);
}
public function getParamDescription() {
return array (
'namespace' => 'Return pages in these namespaces only',
- 'limit' => 'Limit how many random pages will be returned'
+ 'limit' => 'Limit how many random pages will be returned',
+ 'redirect' => 'Load a random redirect instead of a random page'
);
}
diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php
index 2b8c6a92..04eb910f 100644
--- a/includes/api/ApiQueryRecentChanges.php
+++ b/includes/api/ApiQueryRecentChanges.php
@@ -43,16 +43,48 @@ class ApiQueryRecentChanges extends ApiQueryBase {
private $fld_comment = false, $fld_user = false, $fld_flags = false,
$fld_timestamp = false, $fld_title = false, $fld_ids = false,
$fld_sizes = false;
+
+ protected function getTokenFunctions() {
+ // tokenname => function
+ // function prototype is func($pageid, $title, $rev)
+ // should return token or false
+
+ // Don't call the hooks twice
+ if(isset($this->tokenFunctions))
+ return $this->tokenFunctions;
+
+ // If we're in JSON callback mode, no tokens can be obtained
+ if(!is_null($this->getMain()->getRequest()->getVal('callback')))
+ return array();
+
+ $this->tokenFunctions = array(
+ 'patrol' => array( 'ApiQueryRecentChanges', 'getPatrolToken' )
+ );
+ wfRunHooks('APIQueryRecentChangesTokens', array(&$this->tokenFunctions));
+ return $this->tokenFunctions;
+ }
+
+ public static function getPatrolToken($pageid, $title, $rc)
+ {
+ global $wgUser;
+ if(!$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
+ return false;
+
+ // The patrol token is always the same, let's exploit that
+ static $cachedPatrolToken = null;
+ if(!is_null($cachedPatrolToken))
+ return $cachedPatrolToken;
+
+ $cachedPatrolToken = $wgUser->editToken();
+ return $cachedPatrolToken;
+ }
/**
* Generates and outputs the result of this query based upon the provided parameters.
*/
public function execute() {
- /* Initialize vars */
- $limit = $prop = $namespace = $titles = $show = $type = $dir = $start = $end = null;
-
/* Get the parameters of the request. */
- extract($this->extractRequestParams());
+ $params = $this->extractRequestParams();
/* Build our basic query. Namely, something along the lines of:
* SELECT * FROM recentchanges WHERE rc_timestamp > $start
@@ -62,13 +94,13 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$db = $this->getDB();
$this->addTables('recentchanges');
$this->addOption('USE INDEX', array('recentchanges' => 'rc_timestamp'));
- $this->addWhereRange('rc_timestamp', $dir, $start, $end);
- $this->addWhereFld('rc_namespace', $namespace);
+ $this->addWhereRange('rc_timestamp', $params['dir'], $params['start'], $params['end']);
+ $this->addWhereFld('rc_namespace', $params['namespace']);
$this->addWhereFld('rc_deleted', 0);
- if(!empty($titles))
+ if($params['titles'])
{
$lb = new LinkBatch;
- foreach($titles as $t)
+ foreach($params['titles'] as $t)
{
$obj = Title::newFromText($t);
$lb->addObj($obj);
@@ -77,19 +109,19 @@ class ApiQueryRecentChanges extends ApiQueryBase {
// LinkBatch refuses these, but we need them anyway
if(!array_key_exists($obj->getNamespace(), $lb->data))
$lb->data[$obj->getNamespace()] = array();
- $lb->data[$obj->getNamespace()][$obj->getDbKey()] = 1;
+ $lb->data[$obj->getNamespace()][$obj->getDBKey()] = 1;
}
}
- $where = $lb->constructSet('rc', $this->getDb());
+ $where = $lb->constructSet('rc', $this->getDB());
if($where != '')
$this->addWhere($where);
}
- if(!is_null($type))
- $this->addWhereFld('rc_type', $this->parseRCType($type));
+ if(!is_null($params['type']))
+ $this->addWhereFld('rc_type', $this->parseRCType($params['type']));
- if (!is_null($show)) {
- $show = array_flip($show);
+ if (!is_null($params['show'])) {
+ $show = array_flip($params['show']);
/* Check for conflicting parameters. */
if ((isset ($show['minor']) && isset ($show['!minor']))
@@ -103,7 +135,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
// Check permissions
global $wgUser;
- if((isset($show['patrolled']) || isset($show['!patrolled'])) && !$wgUser->isAllowed('patrol'))
+ if((isset($show['patrolled']) || isset($show['!patrolled'])) && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
$this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied');
/* Add additional conditions to query depending upon parameters. */
@@ -125,14 +157,15 @@ class ApiQueryRecentChanges extends ApiQueryBase {
'rc_timestamp',
'rc_namespace',
'rc_title',
+ 'rc_cur_id',
'rc_type',
'rc_moved_to_ns',
'rc_moved_to_title'
));
/* Determine what properties we need to display. */
- if (!is_null($prop)) {
- $prop = array_flip($prop);
+ if (!is_null($params['prop'])) {
+ $prop = array_flip($params['prop']);
/* Set up internal members based upon params. */
$this->fld_comment = isset ($prop['comment']);
@@ -144,14 +177,14 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$this->fld_sizes = isset ($prop['sizes']);
$this->fld_redirect = isset($prop['redirect']);
$this->fld_patrolled = isset($prop['patrolled']);
+ $this->fld_loginfo = isset($prop['loginfo']);
global $wgUser;
- if($this->fld_patrolled && !$wgUser->isAllowed('patrol'))
+ if($this->fld_patrolled && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
$this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied');
/* Add fields to our query if they are specified as a needed parameter. */
$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);
@@ -163,6 +196,10 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$this->addFieldsIf('rc_old_len', $this->fld_sizes);
$this->addFieldsIf('rc_new_len', $this->fld_sizes);
$this->addFieldsIf('rc_patrolled', $this->fld_patrolled);
+ $this->addFieldsIf('rc_logid', $this->fld_loginfo);
+ $this->addFieldsIf('rc_log_type', $this->fld_loginfo);
+ $this->addFieldsIf('rc_log_action', $this->fld_loginfo);
+ $this->addFieldsIf('rc_params', $this->fld_loginfo);
if($this->fld_redirect || isset($show['redirect']) || isset($show['!redirect']))
{
$this->addTables('page');
@@ -170,9 +207,8 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$this->addFields('page_is_redirect');
}
}
- /* Specify the limit for our query. It's $limit+1 because we (possibly) need to
- * generate a "continue" parameter, to allow paging. */
- $this->addOption('LIMIT', $limit +1);
+ $this->token = $params['token'];
+ $this->addOption('LIMIT', $params['limit'] +1);
$data = array ();
$count = 0;
@@ -183,7 +219,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
/* Iterate through the rows, adding data extracted from them to our query result. */
while ($row = $db->fetchObject($res)) {
- if (++ $count > $limit) {
+ if (++ $count > $params['limit']) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
$this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rc_timestamp));
break;
@@ -215,7 +251,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
private function extractRowInfo($row) {
/* If page was moved somewhere, get the title of the move target. */
$movedToTitle = false;
- if (!empty($row->rc_moved_to_title))
+ if (isset($row->rc_moved_to_title) && $row->rc_moved_to_title !== '')
$movedToTitle = Title :: makeTitle($row->rc_moved_to_ns, $row->rc_moved_to_title);
/* Determine the title of the page that has been changed. */
@@ -228,11 +264,11 @@ class ApiQueryRecentChanges extends ApiQueryBase {
/* Determine what kind of change this was. */
switch ( $type ) {
- case RC_EDIT: $vals['type'] = 'edit'; break;
- case RC_NEW: $vals['type'] = 'new'; break;
- case RC_MOVE: $vals['type'] = 'move'; break;
- case RC_LOG: $vals['type'] = 'log'; break;
- case RC_MOVE_OVER_REDIRECT: $vals['type'] = 'move over redirect'; break;
+ case RC_EDIT: $vals['type'] = 'edit'; break;
+ case RC_NEW: $vals['type'] = 'new'; break;
+ case RC_MOVE: $vals['type'] = 'move'; break;
+ case RC_LOG: $vals['type'] = 'log'; break;
+ case RC_MOVE_OVER_REDIRECT: $vals['type'] = 'move over redirect'; break;
default: $vals['type'] = $type;
}
@@ -279,7 +315,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rc_timestamp);
/* Add edit summary / log summary. */
- if ($this->fld_comment && !empty ($row->rc_comment)) {
+ if ($this->fld_comment && isset($row->rc_comment)) {
$vals['comment'] = $row->rc_comment;
}
@@ -290,6 +326,29 @@ class ApiQueryRecentChanges extends ApiQueryBase {
/* Add the patrolled flag */
if ($this->fld_patrolled && $row->rc_patrolled == 1)
$vals['patrolled'] = '';
+
+ if ($this->fld_loginfo && $row->rc_type == RC_LOG) {
+ $vals['logid'] = $row->rc_logid;
+ $vals['logtype'] = $row->rc_log_type;
+ $vals['logaction'] = $row->rc_log_action;
+ ApiQueryLogEvents::addLogParams($this->getResult(),
+ $vals, $row->rc_params,
+ $row->rc_log_type, $row->rc_timestamp);
+ }
+
+ if(!is_null($this->token))
+ {
+ $tokenFunctions = $this->getTokenFunctions();
+ foreach($this->token as $t)
+ {
+ $val = call_user_func($tokenFunctions[$t], $row->rc_cur_id,
+ $title, RecentChange::newFromRow($row));
+ if($val === false)
+ $this->setWarning("Action '$t' is not allowed for the current user");
+ else
+ $vals[$t . 'token'] = $val;
+ }
+ }
return $vals;
}
@@ -345,9 +404,14 @@ class ApiQueryRecentChanges extends ApiQueryBase {
'ids',
'sizes',
'redirect',
- 'patrolled'
+ 'patrolled',
+ 'loginfo',
)
),
+ 'token' => array(
+ ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions()),
+ ApiBase :: PARAM_ISMULTI => true
+ ),
'show' => array (
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => array (
@@ -389,6 +453,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
'namespace' => 'Filter log entries to only this namespace(s)',
'titles' => 'Filter log entries to only these page titles',
'prop' => 'Include additional pieces of information',
+ 'token' => 'Which tokens to obtain for each change',
'show' => array (
'Show only items that meet this criteria.',
'For example, to see only minor edits done by logged-in users, set show=minor|!anon'
@@ -409,6 +474,6 @@ class ApiQueryRecentChanges extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 37909 2008-07-22 13:26:15Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 44719 2008-12-17 16:34:01Z catrope $';
}
}
diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php
index 1fd2d7c6..977e792b 100644
--- a/includes/api/ApiQueryRevisions.php
+++ b/includes/api/ApiQueryRevisions.php
@@ -58,7 +58,7 @@ class ApiQueryRevisions extends ApiQueryBase {
return array();
$this->tokenFunctions = array(
- 'rollback' => array( 'ApiQueryRevisions','getRollbackToken' )
+ 'rollback' => array( 'ApiQueryRevisions', 'getRollbackToken' )
);
wfRunHooks('APIQueryRevisionsTokens', array(&$this->tokenFunctions));
return $this->tokenFunctions;
@@ -74,14 +74,16 @@ class ApiQueryRevisions extends ApiQueryBase {
}
public function execute() {
- $limit = $startid = $endid = $start = $end = $dir = $prop = $user = $excludeuser = $expandtemplates = $section = $token = null;
- extract($this->extractRequestParams(false));
+ $params = $this->extractRequestParams(false);
// 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 extremely
// difficult to manage continuations and require additional SQL indexes
- $enumRevMode = (!is_null($user) || !is_null($excludeuser) || !is_null($limit) || !is_null($startid) || !is_null($endid) || $dir === 'newer' || !is_null($start) || !is_null($end));
+ $enumRevMode = (!is_null($params['user']) || !is_null($params['excludeuser']) ||
+ !is_null($params['limit']) || !is_null($params['startid']) ||
+ !is_null($params['endid']) || $params['dir'] === 'newer' ||
+ !is_null($params['start']) || !is_null($params['end']));
$pageSet = $this->getPageSet();
@@ -100,8 +102,10 @@ class ApiQueryRevisions extends ApiQueryBase {
$this->addTables('revision');
$this->addFields( Revision::selectFields() );
+ $this->addTables( 'page' );
+ $this->addWhere('page_id = rev_page');
- $prop = array_flip($prop);
+ $prop = array_flip($params['prop']);
// Optional fields
$this->fld_ids = isset ($prop['ids']);
@@ -111,11 +115,9 @@ class ApiQueryRevisions extends ApiQueryBase {
$this->fld_comment = isset ($prop['comment']);
$this->fld_size = isset ($prop['size']);
$this->fld_user = isset ($prop['user']);
- $this->token = $token;
+ $this->token = $params['token'];
- if ( !is_null($this->token) || ( $this->fld_content && $this->expandTemplates ) || $pageCount > 0) {
- $this->addTables( 'page' );
- $this->addWhere('page_id=rev_page');
+ if ( !is_null($this->token) || $pageCount > 0) {
$this->addFields( Revision::selectPageFields() );
}
@@ -136,15 +138,17 @@ class ApiQueryRevisions extends ApiQueryBase {
$this->fld_content = true;
- $this->expandTemplates = $expandtemplates;
- if(isset($section))
- $this->section = $section;
+ $this->expandTemplates = $params['expandtemplates'];
+ $this->generateXML = $params['generatexml'];
+ if(isset($params['section']))
+ $this->section = $params['section'];
else
$this->section = false;
}
$userMax = ( $this->fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 );
$botMax = ( $this->fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 );
+ $limit = $params['limit'];
if( $limit == 'max' ) {
$limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
$this->getResult()->addValue( 'limits', $this->getModuleName(), $limit );
@@ -153,13 +157,13 @@ class ApiQueryRevisions extends ApiQueryBase {
if ($enumRevMode) {
// This is mostly to prevent parameter errors (and optimize SQL?)
- if (!is_null($startid) && !is_null($start))
+ if (!is_null($params['startid']) && !is_null($params['start']))
$this->dieUsage('start and startid cannot be used together', 'badparams');
- if (!is_null($endid) && !is_null($end))
+ if (!is_null($params['endid']) && !is_null($params['end']))
$this->dieUsage('end and endid cannot be used together', 'badparams');
- if(!is_null($user) && !is_null( $excludeuser))
+ if(!is_null($params['user']) && !is_null($params['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
@@ -169,10 +173,12 @@ class ApiQueryRevisions extends ApiQueryBase {
// one row with the same timestamp for the same page.
// The order needs to be the same as start parameter to avoid SQL filesort.
- if (is_null($startid) && is_null($endid))
- $this->addWhereRange('rev_timestamp', $dir, $start, $end);
+ if (is_null($params['startid']) && is_null($params['endid']))
+ $this->addWhereRange('rev_timestamp', $params['dir'],
+ $params['start'], $params['end']);
else
- $this->addWhereRange('rev_id', $dir, $startid, $endid);
+ $this->addWhereRange('rev_id', $params['dir'],
+ $params['startid'], $params['endid']);
// must manually initialize unset limit
if (is_null($limit))
@@ -182,30 +188,38 @@ class ApiQueryRevisions extends ApiQueryBase {
// 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));
+ if(!is_null($params['user'])) {
+ $this->addWhereFld('rev_user_text', $params['user']);
+ } elseif (!is_null( $params['excludeuser'])) {
+ $this->addWhere('rev_user_text != ' .
+ $this->getDB()->addQuotes($params['excludeuser']));
}
}
elseif ($revCount > 0) {
- $this->validateLimit('rev_count', $revCount, 1, $userMax, $botMax);
+ $max = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
+ $revs = $pageSet->getRevisionIDs();
+ if(self::truncateArray($revs, $max))
+ $this->setWarning("Too many values supplied for parameter 'revids': the limit is $max");
// Get all revision IDs
- $this->addWhereFld('rev_id', array_keys($pageSet->getRevisionIDs()));
+ $this->addWhereFld('rev_id', array_keys($revs));
// assumption testing -- we should never get more then $revCount rows.
$limit = $revCount;
}
elseif ($pageCount > 0) {
+ $max = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
+ $titles = $pageSet->getGoodTitles();
+ if(self::truncateArray($titles, $max))
+ $this->setWarning("Too many values supplied for parameter 'titles': the limit is $max");
+
// When working in multi-page non-enumeration mode,
// limit to the latest revision only
$this->addWhere('page_id=rev_page');
$this->addWhere('page_latest=rev_id');
- $this->validateLimit('page_count', $pageCount, 1, $userMax, $botMax);
-
+
// Get all page IDs
- $this->addWhereFld('page_id', array_keys($pageSet->getGoodTitles()));
+ $this->addWhereFld('page_id', array_keys($titles));
// assumption testing -- we should never get more then $pageCount rows.
$limit = $pageCount;
@@ -281,7 +295,7 @@ class ApiQueryRevisions extends ApiQueryBase {
if ($this->fld_comment) {
$comment = $revision->getComment();
- if (!empty($comment))
+ if (strval($comment) !== '')
$vals['comment'] = $comment;
}
@@ -312,6 +326,17 @@ class ApiQueryRevisions extends ApiQueryBase {
if($text === false)
$this->dieUsage("There is no section {$this->section} in r".$revision->getId(), 'nosuchsection');
}
+ if ($this->generateXML) {
+ $wgParser->startExternalParse( $title, new ParserOptions(), OT_PREPROCESS );
+ $dom = $wgParser->preprocessToDom( $text );
+ if ( is_callable( array( $dom, 'saveXML' ) ) ) {
+ $xml = $dom->saveXML();
+ } else {
+ $xml = $dom->__toString();
+ }
+ $vals['parsetree'] = $xml;
+
+ }
if ($this->expandTemplates) {
$text = $wgParser->preprocess( $text, $title, new ParserOptions() );
}
@@ -366,11 +391,9 @@ class ApiQueryRevisions extends ApiQueryBase {
'excludeuser' => array(
ApiBase :: PARAM_TYPE => 'user'
),
-
'expandtemplates' => false,
- 'section' => array(
- ApiBase :: PARAM_TYPE => 'integer'
- ),
+ 'generatexml' => false,
+ 'section' => null,
'token' => array(
ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions()),
ApiBase :: PARAM_ISMULTI => true
@@ -390,6 +413,7 @@ class ApiQueryRevisions extends ApiQueryBase {
'user' => 'only include revisions made by user',
'excludeuser' => 'exclude revisions made by user',
'expandtemplates' => 'expand templates in revision content',
+ 'generatexml' => 'generate XML parse tree for revision content',
'section' => 'only retrieve the content of this section',
'token' => 'Which tokens to obtain for each revision',
);
@@ -424,6 +448,6 @@ class ApiQueryRevisions extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryRevisions.php 37300 2008-07-08 08:42:27Z btongminh $';
+ return __CLASS__ . ': $Id: ApiQueryRevisions.php 44719 2008-12-17 16:34:01Z catrope $';
}
}
diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php
index 84a2ec63..cb020fff 100644
--- a/includes/api/ApiQuerySearch.php
+++ b/includes/api/ApiQuerySearch.php
@@ -53,7 +53,8 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
$limit = $params['limit'];
$query = $params['search'];
- if (is_null($query) || empty($query))
+ $what = $params['what'];
+ if (strval($query) === '')
$this->dieUsage("empty search string is not allowed", 'param-search');
$search = SearchEngine::create();
@@ -61,13 +62,30 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
$search->setNamespaces( $params['namespace'] );
$search->showRedirects = $params['redirects'];
- if ($params['what'] == 'text')
+ if ($what == 'text') {
$matches = $search->searchText( $query );
- else
+ } elseif( $what == 'title' ) {
$matches = $search->searchTitle( $query );
+ } else {
+ // We default to title searches; this is a terrible legacy
+ // of the way we initially set up the MySQL fulltext-based
+ // search engine with separate title and text fields.
+ // In the future, the default should be for a combined index.
+ $what = 'title';
+ $matches = $search->searchTitle( $query );
+
+ // Not all search engines support a separate title search,
+ // for instance the Lucene-based engine we use on Wikipedia.
+ // In this case, fall back to full-text search (which will
+ // include titles in it!)
+ if( is_null( $matches ) ) {
+ $what = 'text';
+ $matches = $search->searchText( $query );
+ }
+ }
if (is_null($matches))
- $this->dieUsage("{$params['what']} search is disabled",
- "search-{$params['what']}-disabled");
+ $this->dieUsage("{$what} search is disabled",
+ "search-{$what}-disabled");
$data = array ();
$count = 0;
@@ -78,8 +96,9 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
break;
}
- // Silently skip broken titles
- if ($result->isBrokenTitle()) continue;
+ // Silently skip broken and missing titles
+ if ($result->isBrokenTitle() || $result->isMissingRevision())
+ continue;
$title = $result->getTitle();
if (is_null($resultPageSet)) {
@@ -109,7 +128,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
ApiBase :: PARAM_ISMULTI => true,
),
'what' => array (
- ApiBase :: PARAM_DFLT => 'title',
+ ApiBase :: PARAM_DFLT => null,
ApiBase :: PARAM_TYPE => array (
'title',
'text',
@@ -151,6 +170,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQuerySearch.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiQuerySearch.php 44186 2008-12-03 19:33:57Z catrope $';
}
}
diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php
index 1fd3b888..84757f7f 100644
--- a/includes/api/ApiQuerySiteinfo.php
+++ b/includes/api/ApiQuerySiteinfo.php
@@ -57,6 +57,9 @@ class ApiQuerySiteinfo extends ApiQueryBase {
case 'specialpagealiases':
$this->appendSpecialPageAliases( $p );
break;
+ case 'magicwords':
+ $this->appendMagicWords( $p );
+ break;
case 'interwikimap':
$filteriw = isset( $params['filteriw'] ) ? $params['filteriw'] : false;
$this->appendInterwikiMap( $p, $filteriw );
@@ -70,6 +73,9 @@ class ApiQuerySiteinfo extends ApiQueryBase {
case 'usergroups':
$this->appendUserGroups( $p );
break;
+ case 'extensions':
+ $this->appendExtensions( $p );
+ break;
default :
ApiBase :: dieDebug( __METHOD__, "Unknown prop=$p" );
}
@@ -129,8 +135,13 @@ class ApiQuerySiteinfo extends ApiQueryBase {
'id' => $ns
);
ApiResult :: setContent( $data[$ns], $title );
- if( MWNamespace::hasSubpages($ns) )
+ $canonical = MWNamespace::getCanonicalName( $ns );
+
+ if( MWNamespace::hasSubpages( $ns ) )
$data[$ns]['subpages'] = '';
+
+ if( $canonical )
+ $data[$ns]['canonical'] = strtr($canonical, '_', ' ');
}
$this->getResult()->setIndexedTagName( $data, 'ns' );
@@ -138,9 +149,11 @@ class ApiQuerySiteinfo extends ApiQueryBase {
}
protected function appendNamespaceAliases( $property ) {
- global $wgNamespaceAliases;
+ global $wgNamespaceAliases, $wgContLang;
+ $wgContLang->load();
+ $aliases = array_merge($wgNamespaceAliases, $wgContLang->namespaceAliases);
$data = array();
- foreach( $wgNamespaceAliases as $title => $ns ) {
+ foreach( $aliases as $title => $ns ) {
$item = array(
'id' => $ns
);
@@ -164,6 +177,22 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$this->getResult()->setIndexedTagName( $data, 'specialpage' );
$this->getResult()->addValue( 'query', $property, $data );
}
+
+ protected function appendMagicWords( $property ) {
+ global $wgContLang;
+ $data = array();
+ foreach($wgContLang->getMagicWords() as $magicword => $aliases)
+ {
+ $caseSensitive = array_shift($aliases);
+ $arr = array('name' => $magicword, 'aliases' => $aliases);
+ if($caseSensitive)
+ $arr['case-sensitive'] = '';
+ $this->getResult()->setIndexedTagName($arr['aliases'], 'alias');
+ $data[] = $arr;
+ }
+ $this->getResult()->setIndexedTagName($data, 'magicword');
+ $this->getResult()->addValue('query', $property, $data);
+ }
protected function appendInterwikiMap( $property, $filter ) {
$this->resetQueryParams();
@@ -174,7 +203,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$this->addWhere( 'iw_local = 1' );
elseif( $filter === '!local' )
$this->addWhere( 'iw_local = 0' );
- elseif( $filter !== false )
+ elseif( $filter )
ApiBase :: dieDebug( __METHOD__, "Unknown filter=$filter" );
$this->addOption( 'ORDER BY', 'iw_prefix' );
@@ -239,7 +268,8 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$data['edits'] = intval( SiteStats::edits() );
$data['images'] = intval( SiteStats::images() );
$data['users'] = intval( SiteStats::users() );
- $data['admins'] = intval( SiteStats::admins() );
+ $data['activeusers'] = intval( SiteStats::activeUsers() );
+ $data['admins'] = intval( SiteStats::numberingroup('sysop') );
$data['jobs'] = intval( SiteStats::jobs() );
$this->getResult()->addValue( 'query', $property, $data );
}
@@ -257,6 +287,40 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$this->getResult()->addValue( 'query', $property, $data );
}
+ protected function appendExtensions( $property ) {
+ global $wgExtensionCredits;
+ $data = array();
+ foreach ( $wgExtensionCredits as $type => $extensions ) {
+ foreach ( $extensions as $ext ) {
+ $ret = array();
+ $ret['type'] = $type;
+ if ( isset( $ext['name'] ) )
+ $ret['name'] = $ext['name'];
+ if ( isset( $ext['description'] ) )
+ $ret['description'] = $ext['description'];
+ if ( isset( $ext['descriptionmsg'] ) )
+ $ret['descriptionmsg'] = $ext['descriptionmsg'];
+ if ( isset( $ext['author'] ) ) {
+ $ret['author'] = is_array( $ext['author'] ) ?
+ implode( ', ', $ext['author' ] ) : $ext['author'];
+ }
+ if ( isset( $ext['version'] ) ) {
+ $ret['version'] = $ext['version'];
+ } elseif ( isset( $ext['svn-revision'] ) &&
+ preg_match( '/\$(?:Rev|LastChangedRevision|Revision): *(\d+)/',
+ $ext['svn-revision'], $m ) )
+ {
+ $ret['version'] = 'r' . $m[1];
+ }
+ $data[] = $ret;
+ }
+ }
+
+ $this->getResult()->setIndexedTagName( $data, 'ext' );
+ $this->getResult()->addValue( 'query', $property, $data );
+ }
+
+
public function getAllowedParams() {
return array(
'prop' => array(
@@ -267,10 +331,12 @@ class ApiQuerySiteinfo extends ApiQueryBase {
'namespaces',
'namespacealiases',
'specialpagealiases',
+ 'magicwords',
'interwikimap',
'dbrepllag',
'statistics',
'usergroups',
+ 'extensions',
)
),
'filteriw' => array(
@@ -288,13 +354,15 @@ class ApiQuerySiteinfo extends ApiQueryBase {
'prop' => array(
'Which sysinfo properties to get:',
' "general" - Overall system information',
- ' "namespaces" - List of registered namespaces (localized)',
+ ' "namespaces" - List of registered namespaces and their canonical names',
' "namespacealiases" - List of registered namespace aliases',
' "specialpagealiases" - List of special page aliases',
+ ' "magicwords" - List of magic words and their aliases',
' "statistics" - Returns site statistics',
' "interwikimap" - Returns interwiki map (optionally filtered)',
' "dbrepllag" - Returns database server with the highest replication lag',
' "usergroups" - Returns user groups and the associated permissions',
+ ' "extensions" - Returns extensions installed on the wiki',
),
'filteriw' => 'Return only local or only nonlocal entries of the interwiki map',
'showalldb' => 'List all database servers, not just the one lagging the most',
@@ -314,6 +382,6 @@ class ApiQuerySiteinfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 37034 2008-07-04 09:21:11Z vasilievvv $';
+ return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 44862 2008-12-20 23:49:16Z catrope $';
}
}
diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php
index c477acdb..be6c8bc4 100644
--- a/includes/api/ApiQueryUserContributions.php
+++ b/includes/api/ApiQueryUserContributions.php
@@ -62,6 +62,7 @@ class ApiQueryContributions extends ApiQueryBase {
if(isset($this->params['userprefix']))
{
$this->prefixMode = true;
+ $this->multiUserMode = true;
$this->userprefix = $this->params['userprefix'];
}
else
@@ -72,6 +73,7 @@ class ApiQueryContributions extends ApiQueryBase {
foreach($this->params['user'] as $u)
$this->prepareUsername($u);
$this->prefixMode = false;
+ $this->multiUserMode = (count($this->params['user']) > 1);
}
$this->prepareQuery();
@@ -87,7 +89,10 @@ class ApiQueryContributions 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', wfTimestamp(TS_ISO_8601, $row->rev_timestamp));
+ if($this->multiUserMode)
+ $this->setContinueEnumParameter('continue', $this->continueStr($row));
+ else
+ $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rev_timestamp));
break;
}
@@ -132,13 +137,28 @@ class ApiQueryContributions extends ApiQueryBase {
//anything we retrieve.
$this->addTables(array('revision', 'page'));
$this->addWhere('page_id=rev_page');
+
+ // Handle continue parameter
+ if($this->multiUserMode && !is_null($this->params['continue']))
+ {
+ $continue = explode('|', $this->params['continue']);
+ if(count($continue) != 2)
+ $this->dieUsage("Invalid continue param. You should pass the original " .
+ "value returned by the previous query", "_badcontinue");
+ $encUser = $this->getDB()->strencode($continue[0]);
+ $encTS = wfTimestamp(TS_MW, $continue[1]);
+ $op = ($this->params['dir'] == 'older' ? '<' : '>');
+ $this->addWhere("rev_user_text $op '$encUser' OR " .
+ "(rev_user_text = '$encUser' AND " .
+ "rev_timestamp $op= '$encTS')");
+ }
$this->addWhereFld('rev_deleted', 0);
// We only want pages by the specified users.
if($this->prefixMode)
- $this->addWhere("rev_user_text LIKE '" . $this->getDb()->escapeLike($this->userprefix) . "%'");
+ $this->addWhere("rev_user_text LIKE '" . $this->getDB()->escapeLike($this->userprefix) . "%'");
else
- $this->addWhereFld( 'rev_user_text', $this->usernames );
+ $this->addWhereFld('rev_user_text', $this->usernames);
// ... and in the specified timeframe.
// Ensure the same sort order for rev_user_text and rev_timestamp
// so our query is indexed
@@ -157,6 +177,7 @@ class ApiQueryContributions extends ApiQueryBase {
$this->addWhereIf('rev_minor_edit != 0', isset ($show['minor']));
}
$this->addOption('LIMIT', $this->params['limit'] + 1);
+ $this->addOption( 'USE INDEX', array( 'revision' => 'usertext_timestamp' ) );
// Mandatory fields: timestamp allows request continuation
// ns+title checks if the user has access rights for this page
@@ -207,11 +228,17 @@ class ApiQueryContributions extends ApiQueryBase {
$vals['top'] = '';
}
- if ($this->fld_comment && !empty ($row->rev_comment))
+ if ($this->fld_comment && isset( $row->rev_comment ) )
$vals['comment'] = $row->rev_comment;
return $vals;
}
+
+ private function continueStr($row)
+ {
+ return $row->rev_user_text . '|' .
+ wfTimestamp(TS_ISO_8601, $row->rev_timestamp);
+ }
public function getAllowedParams() {
return array (
@@ -228,6 +255,7 @@ class ApiQueryContributions extends ApiQueryBase {
'end' => array (
ApiBase :: PARAM_TYPE => 'timestamp'
),
+ 'continue' => null,
'user' => array (
ApiBase :: PARAM_ISMULTI => true
),
@@ -269,6 +297,7 @@ class ApiQueryContributions extends ApiQueryBase {
'limit' => 'The maximum number of contributions to return.',
'start' => 'The start timestamp to return from.',
'end' => 'The end timestamp to return to.',
+ 'continue' => 'When more results are available, use this to continue.',
'user' => 'The user to retrieve contributions for.',
'userprefix' => 'Retrieve contibutions for all users whose names begin with this value. Overrides ucuser.',
'dir' => 'The direction to search (older or newer).',
@@ -290,6 +319,6 @@ class ApiQueryContributions extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryUserContributions.php 37383 2008-07-09 11:44:49Z btongminh $';
+ return __CLASS__ . ': $Id: ApiQueryUserContributions.php 43271 2008-11-06 22:38:42Z siebrand $';
}
}
diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php
index 2d55a352..203b7e25 100644
--- a/includes/api/ApiQueryUserInfo.php
+++ b/includes/api/ApiQueryUserInfo.php
@@ -76,12 +76,16 @@ class ApiQueryUserInfo extends ApiQueryBase {
$result->setIndexedTagName($vals['groups'], 'g'); // even if empty
}
if (isset($this->prop['rights'])) {
- $vals['rights'] = $wgUser->getRights();
+ // User::getRights() may return duplicate values, strip them
+ $vals['rights'] = array_values(array_unique($wgUser->getRights()));
$result->setIndexedTagName($vals['rights'], 'r'); // even if empty
}
if (isset($this->prop['options'])) {
$vals['options'] = (is_null($wgUser->mOptions) ? User::getDefaultOptions() : $wgUser->mOptions);
}
+ if (isset($this->prop['preferencestoken']) && is_null($this->getMain()->getRequest()->getVal('callback'))) {
+ $vals['preferencestoken'] = $wgUser->editToken();
+ }
if (isset($this->prop['editcount'])) {
$vals['editcount'] = $wgUser->getEditCount();
}
@@ -110,6 +114,7 @@ class ApiQueryUserInfo extends ApiQueryBase {
if(!$wgUser->isAnon())
$categories[] = 'newbie';
}
+ $categories = array_merge($categories, $wgUser->getGroups());
// Now get the actual limits
$retval = array();
@@ -134,6 +139,7 @@ class ApiQueryUserInfo extends ApiQueryBase {
'groups',
'rights',
'options',
+ 'preferencestoken',
'editcount',
'ratelimits'
)
@@ -168,6 +174,6 @@ class ApiQueryUserInfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryUserInfo.php 35186 2008-05-22 16:39:43Z brion $';
+ return __CLASS__ . ': $Id: ApiQueryUserInfo.php 43764 2008-11-20 15:15:00Z catrope $';
}
}
diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php
index a8147567..e50d8d82 100644
--- a/includes/api/ApiQueryUsers.php
+++ b/includes/api/ApiQueryUsers.php
@@ -68,15 +68,13 @@ if (!defined('MEDIAWIKI')) {
else
$goodNames[] = $n;
}
- if(empty($goodNames))
+ if(!count($goodNames))
return $retval;
- $db = $this->getDb();
+ $db = $this->getDB();
$this->addTables('user', 'u1');
- $this->addFields('u1.user_name');
+ $this->addFields('u1.*');
$this->addWhereFld('u1.user_name', $goodNames);
- $this->addFieldsIf('u1.user_editcount', isset($this->prop['editcount']));
- $this->addFieldsIf('u1.user_registration', isset($this->prop['registration']));
if(isset($this->prop['groups'])) {
$this->addTables('user_groups');
@@ -96,20 +94,26 @@ if (!defined('MEDIAWIKI')) {
$data = array();
$res = $this->select(__METHOD__);
while(($r = $db->fetchObject($res))) {
- $data[$r->user_name]['name'] = $r->user_name;
+ $user = User::newFromRow($r);
+ $name = $user->getName();
+ $data[$name]['name'] = $name;
if(isset($this->prop['editcount']))
- $data[$r->user_name]['editcount'] = $r->user_editcount;
+ // No proper member function in User class for this
+ $data[$name]['editcount'] = $r->user_editcount;
if(isset($this->prop['registration']))
- $data[$r->user_name]['registration'] = wfTimestampOrNull(TS_ISO_8601, $r->user_registration);
+ // Nor for this one
+ $data[$name]['registration'] = wfTimestampOrNull(TS_ISO_8601, $r->user_registration);
if(isset($this->prop['groups']))
// This row contains only one group, others will be added from other rows
if(!is_null($r->ug_group))
- $data[$r->user_name]['groups'][] = $r->ug_group;
+ $data[$name]['groups'][] = $r->ug_group;
if(isset($this->prop['blockinfo']))
if(!is_null($r->blocker_name)) {
- $data[$r->user_name]['blockedby'] = $r->blocker_name;
- $data[$r->user_name]['blockreason'] = $r->ipb_reason;
+ $data[$name]['blockedby'] = $r->blocker_name;
+ $data[$name]['blockreason'] = $r->ipb_reason;
}
+ if(isset($this->prop['emailable']) && $user->canReceiveEmail())
+ $data[$name]['emailable'] = '';
}
// Second pass: add result data to $retval
@@ -134,7 +138,8 @@ if (!defined('MEDIAWIKI')) {
'blockinfo',
'groups',
'editcount',
- 'registration'
+ 'registration',
+ 'emailable',
)
),
'users' => array(
@@ -147,9 +152,11 @@ if (!defined('MEDIAWIKI')) {
return array (
'prop' => array(
'What pieces of information to include',
- ' blockinfo - tags if the user is blocked, by whom, and for what reason',
- ' groups - lists all the groups the user belongs to',
- ' editcount - adds the user\'s edit count'
+ ' blockinfo - tags if the user is blocked, by whom, and for what reason',
+ ' groups - lists all the groups the user belongs to',
+ ' editcount - adds the user\'s edit count',
+ ' registration - adds the user\'s registration timestamp',
+ ' emailable - tags if the user can and wants to receive e-mail through [[Special:Emailuser]]',
),
'users' => 'A list of users to obtain the same information for'
);
@@ -164,6 +171,6 @@ if (!defined('MEDIAWIKI')) {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryUsers.php 38183 2008-07-29 12:58:04Z rotem $';
+ return __CLASS__ . ': $Id: ApiQueryUsers.php 44231 2008-12-04 14:42:30Z catrope $';
}
}
diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php
index d17e83f6..ed3482fb 100644
--- a/includes/api/ApiQueryWatchlist.php
+++ b/includes/api/ApiQueryWatchlist.php
@@ -59,12 +59,11 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
if (!$wgUser->isLoggedIn())
$this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin');
- $allrev = $start = $end = $namespace = $dir = $limit = $prop = $show = null;
- extract($this->extractRequestParams());
+ $params = $this->extractRequestParams();
- if (!is_null($prop) && is_null($resultPageSet)) {
+ if (!is_null($params['prop']) && is_null($resultPageSet)) {
- $prop = array_flip($prop);
+ $prop = array_flip($params['prop']);
$this->fld_ids = isset($prop['ids']);
$this->fld_title = isset($prop['title']);
@@ -76,8 +75,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$this->fld_patrol = isset($prop['patrol']);
if ($this->fld_patrol) {
- global $wgUseRCPatrol, $wgUser;
- if (!$wgUseRCPatrol || !$wgUser->isAllowed('patrol'))
+ global $wgUser;
+ if (!$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
$this->dieUsage('patrol property is not available', 'patrol');
}
}
@@ -100,7 +99,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$this->addFieldsIf('rc_old_len', $this->fld_sizes);
$this->addFieldsIf('rc_new_len', $this->fld_sizes);
}
- elseif ($allrev) {
+ elseif ($params['allrev']) {
$this->addFields(array (
'rc_this_oldid',
'rc_namespace',
@@ -131,20 +130,26 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'rc_deleted' => 0,
));
- $this->addWhereRange('rc_timestamp', $dir, $start, $end);
- $this->addWhereFld('wl_namespace', $namespace);
- $this->addWhereIf('rc_this_oldid=page_latest', !$allrev);
+ $this->addWhereRange('rc_timestamp', $params['dir'], $params['start'], $params['end']);
+ $this->addWhereFld('wl_namespace', $params['namespace']);
+ $this->addWhereIf('rc_this_oldid=page_latest', !$params['allrev']);
- if (!is_null($show)) {
- $show = array_flip($show);
+ if (!is_null($params['show'])) {
+ $show = array_flip($params['show']);
/* Check for conflicting parameters. */
if ((isset ($show['minor']) && isset ($show['!minor']))
|| (isset ($show['bot']) && isset ($show['!bot']))
- || (isset ($show['anon']) && isset ($show['!anon']))) {
+ || (isset ($show['anon']) && isset ($show['!anon']))
+ || (isset ($show['patrolled']) && isset ($show['!patrolled']))) {
$this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show');
}
+
+ // Check permissions
+ global $wgUser;
+ if((isset($show['patrolled']) || isset($show['!patrolled'])) && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
+ $this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied');
/* Add additional conditions to query depending upon parameters. */
$this->addWhereIf('rc_minor = 0', isset ($show['!minor']));
@@ -153,13 +158,15 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$this->addWhereIf('rc_bot != 0', isset ($show['bot']));
$this->addWhereIf('rc_user = 0', isset ($show['anon']));
$this->addWhereIf('rc_user != 0', isset ($show['!anon']));
+ $this->addWhereIf('rc_patrolled = 0', isset($show['!patrolled']));
+ $this->addWhereIf('rc_patrolled != 0', isset($show['patrolled']));
}
# This is an index optimization for mysql, as done in the Special:Watchlist page
- $this->addWhereIf("rc_timestamp > ''", !isset ($start) && !isset ($end) && $wgDBtype == 'mysql');
+ $this->addWhereIf("rc_timestamp > ''", !isset ($params['start']) && !isset ($params['end']) && $wgDBtype == 'mysql');
- $this->addOption('LIMIT', $limit +1);
+ $this->addOption('LIMIT', $params['limit'] +1);
$data = array ();
$count = 0;
@@ -167,7 +174,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$db = $this->getDB();
while ($row = $db->fetchObject($res)) {
- if (++ $count > $limit) {
+ if (++ $count > $params['limit']) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
$this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rc_timestamp));
break;
@@ -178,7 +185,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
if ($vals)
$data[] = $vals;
} else {
- if ($allrev) {
+ if ($params['allrev']) {
$data[] = intval($row->rc_this_oldid);
} else {
$data[] = intval($row->rc_cur_id);
@@ -192,7 +199,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$this->getResult()->setIndexedTagName($data, 'item');
$this->getResult()->addValue('query', $this->getModuleName(), $data);
}
- elseif ($allrev) {
+ elseif ($params['allrev']) {
$resultPageSet->populateFromRevisionIDs($data);
} else {
$resultPageSet->populateFromPageIDs($data);
@@ -237,7 +244,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$vals['newlen'] = intval($row->rc_new_len);
}
- if ($this->fld_comment && !empty ($row->rc_comment))
+ if ($this->fld_comment && isset( $row->rc_comment ))
$vals['comment'] = $row->rc_comment;
return $vals;
@@ -292,7 +299,9 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'bot',
'!bot',
'anon',
- '!anon'
+ '!anon',
+ 'patrolled',
+ '!patrolled',
)
)
);
@@ -329,6 +338,6 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryWatchlist.php 37909 2008-07-22 13:26:15Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryWatchlist.php 44719 2008-12-17 16:34:01Z catrope $';
}
}
diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php
new file mode 100644
index 00000000..e9951b42
--- /dev/null
+++ b/includes/api/ApiQueryWatchlistRaw.php
@@ -0,0 +1,179 @@
+<?php
+
+/*
+ * Created on Oct 4, 2008
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * 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 action allows clients to retrieve a list of pages
+ * on the logged-in user's watchlist.
+ *
+ * @ingroup API
+ */
+class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'wr');
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function executeGenerator($resultPageSet) {
+ $this->run($resultPageSet);
+ }
+
+ private function run($resultPageSet = null) {
+ global $wgUser;
+
+ $this->selectNamedDB('watchlist', DB_SLAVE, 'watchlist');
+
+ if (!$wgUser->isLoggedIn())
+ $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin');
+ $params = $this->extractRequestParams();
+ $prop = array_flip((array)$params['prop']);
+ $show = array_flip((array)$params['show']);
+ if(isset($show['changed']) && isset($show['!changed']))
+ $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show');
+
+ $this->addTables('watchlist');
+ $this->addFields(array('wl_namespace', 'wl_title'));
+ $this->addFieldsIf('wl_notificationtimestamp', isset($prop['changed']));
+ $this->addWhereFld('wl_user', $wgUser->getId());
+ $this->addWhereFld('wl_namespace', $params['namespace']);
+ $this->addWhereIf('wl_notificationtimestamp IS NOT NULL', isset($show['changed']));
+ $this->addWhereIf('wl_notificationtimestamp IS NULL', isset($show['!changed']));
+ if(isset($params['continue']))
+ {
+ $cont = explode('|', $params['continue']);
+ if(count($cont) != 2)
+ $this->dieUsage("Invalid continue param. You should pass the " .
+ "original value returned by the previous query", "_badcontinue");
+ $ns = intval($cont[0]);
+ $title = $this->getDB()->strencode($this->titleToKey($cont[1]));
+ $this->addWhere("wl_namespace > '$ns' OR ".
+ "(wl_namespace = '$ns' AND ".
+ "wl_title >= '$title')");
+ }
+ // Don't ORDER BY wl_namespace if it's constant in the WHERE clause
+ if(count($params['namespace']) == 1)
+ $this->addOption('ORDER BY', 'wl_title');
+ else
+ $this->addOption('ORDER BY', 'wl_namespace, wl_title');
+ $this->addOption('LIMIT', $params['limit'] + 1);
+ $res = $this->select(__METHOD__);
+
+ $db = $this->getDB();
+ $data = array();
+ $titles = array();
+ $count = 0;
+ while($row = $db->fetchObject($res))
+ {
+ if(++$count > $params['limit'])
+ {
+ // We've reached the one extra which shows that there are additional pages to be had. Stop here...
+ $this->setContinueEnumParameter('continue', $row->wl_namespace . '|' .
+ $this->keyToTitle($row->wl_title));
+ break;
+ }
+ $t = Title::makeTitle($row->wl_namespace, $row->wl_title);
+ if(is_null($resultPageSet))
+ {
+ $vals = array();
+ ApiQueryBase::addTitleInfo($vals, $t);
+ if(isset($prop['changed']) && !is_null($row->wl_notificationtimestamp))
+ $vals['changed'] = wfTimestamp(TS_ISO_8601, $row->wl_notificationtimestamp);
+ $data[] = $vals;
+ }
+ else
+ $titles[] = $t;
+ }
+ if(is_null($resultPageSet))
+ {
+ $this->getResult()->setIndexedTagName($data, 'wr');
+ $this->getResult()->addValue(null, $this->getModuleName(), $data);
+ }
+ else
+ $resultPageSet->populateFromTitles($titles);
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'continue' => 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
+ ),
+ 'prop' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array (
+ 'changed',
+ )
+ ),
+ 'show' => array (
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array (
+ 'changed',
+ '!changed',
+ )
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'continue' => 'When more results are available, use this to continue',
+ 'namespace' => 'Only list pages in the given namespace(s).',
+ 'limit' => 'How many total results to return per request.',
+ 'prop' => 'Which additional properties to get (non-generator mode only).',
+ 'show' => 'Only list items that meet these criteria.',
+ );
+ }
+
+ public function getDescription() {
+ return "Get all pages on the logged in user's watchlist";
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=query&list=watchlistraw',
+ 'api.php?action=query&generator=watchlistraw&gwrshow=changed&prop=revisions',
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryWatchlistRaw.php 41651 2008-10-04 14:30:33Z catrope $';
+ }
+}
diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php
index 9e798d35..900953e0 100644
--- a/includes/api/ApiResult.php
+++ b/includes/api/ApiResult.php
@@ -100,7 +100,7 @@ class ApiResult extends ApiBase {
}
elseif (is_array($arr[$name]) && is_array($value)) {
$merged = array_intersect_key($arr[$name], $value);
- if (empty ($merged))
+ if (!count($merged))
$arr[$name] += $value;
else
ApiBase :: dieDebug(__METHOD__, "Attempting to merge element $name");
@@ -180,18 +180,27 @@ class ApiResult extends ApiBase {
}
}
- if (empty($name))
+ if (!$name)
$data[] = $value; // Add list element
else
ApiResult :: setElement($data, $name, $value); // Add named element
}
+ /**
+ * Ensure all values in this result are valid UTF-8.
+ */
+ public function cleanUpUTF8()
+ {
+ $data = & $this->getData();
+ array_walk_recursive($data, array('UtfNormal', 'cleanUp'));
+ }
+
public function execute() {
ApiBase :: dieDebug(__METHOD__, 'execute() is not supported on Result object');
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiResult.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiResult.php 45752 2009-01-14 21:36:57Z catrope $';
}
}
@@ -201,7 +210,7 @@ if (!function_exists('array_intersect_key')) {
$argc = func_num_args();
if ($argc > 2) {
- for ($i = 1; !empty($isec) && $i < $argc; $i++) {
+ for ($i = 1; $isec && $i < $argc; $i++) {
$arr = func_get_arg($i);
foreach (array_keys($isec) as $key) {
diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php
index 3739f694..653dca9e 100644
--- a/includes/api/ApiRollback.php
+++ b/includes/api/ApiRollback.php
@@ -37,7 +37,6 @@ class ApiRollback extends ApiBase {
}
public function execute() {
- global $wgUser;
$this->getMain()->requestWriteMode();
$params = $this->extractRequestParams();
@@ -55,7 +54,10 @@ class ApiRollback extends ApiBase {
if(!$titleObj->exists())
$this->dieUsageMsg(array('notanarticle'));
- $username = User::getCanonicalName($params['user']);
+ #We need to be able to revert IPs, but getCanonicalName rejects them
+ $username = User::isIP($params['user'])
+ ? $params['user']
+ : User::getCanonicalName($params['user']);
if(!$username)
$this->dieUsageMsg(array('invaliduser', $params['user']));
@@ -64,20 +66,17 @@ class ApiRollback extends ApiBase {
$details = null;
$retval = $articleObj->doRollback($username, $summary, $params['token'], $params['markbot'], $details);
- if(!empty($retval))
+ if($retval)
// We don't care about multiple errors, just report one of them
$this->dieUsageMsg(current($retval));
- $current = $target = $summary = NULL;
- extract($details);
-
$info = array(
'title' => $titleObj->getPrefixedText(),
- 'pageid' => $current->getPage(),
- 'summary' => $summary,
+ 'pageid' => $details['current']->getPage(),
+ 'summary' => $details['summary'],
'revid' => $titleObj->getLatestRevID(),
- 'old_revid' => $current->getID(),
- 'last_revid' => $target->getID()
+ 'old_revid' => $details['current']->getID(),
+ 'last_revid' => $details['target']->getID()
);
$this->getResult()->addValue(null, $this->getModuleName(), $info);
@@ -99,7 +98,7 @@ class ApiRollback extends ApiBase {
return array (
'title' => 'Title of the page you want to rollback.',
'user' => 'Name of the user whose edits are to be rolled back. If set incorrectly, you\'ll get a badtoken error.',
- 'token' => 'A rollback token previously retrieved through prop=info',
+ 'token' => 'A rollback token previously retrieved through prop=revisions',
'summary' => 'Custom edit summary. If not set, default summary will be used.',
'markbot' => 'Mark the reverted edits and the revert as bot edits'
);
@@ -107,8 +106,8 @@ class ApiRollback extends ApiBase {
public function getDescription() {
return array(
- 'Undoes the last edit to the page. If the last user who edited the page made multiple edits in a row,',
- 'they will all be rolled back. You need to be logged in as a sysop to use this function, see also action=login.'
+ 'Undo the last edit to the page. If the last user who edited the page made multiple edits in a row,',
+ 'they will all be rolled back.'
);
}
@@ -120,6 +119,6 @@ class ApiRollback extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiRollback.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiRollback.php 45043 2008-12-26 04:13:47Z mrzman $';
}
}
diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php
index d6a02a2a..cd52c518 100644
--- a/includes/api/ApiUnblock.php
+++ b/includes/api/ApiUnblock.php
@@ -64,14 +64,12 @@ class ApiUnblock extends ApiBase {
$this->dieUsageMsg(array('sessionfailure'));
if(!$wgUser->isAllowed('block'))
$this->dieUsageMsg(array('cantunblock'));
- if(wfReadOnly())
- $this->dieUsageMsg(array('readonlytext'));
$id = $params['id'];
$user = $params['user'];
$reason = (is_null($params['reason']) ? '' : $params['reason']);
$retval = IPUnblockForm::doUnblock($id, $user, $reason, $range);
- if(!empty($retval))
+ if($retval)
$this->dieUsageMsg($retval);
$res['id'] = $id;
@@ -96,7 +94,7 @@ class ApiUnblock extends ApiBase {
return array (
'id' => 'ID of the block you want to unblock (obtained through list=blocks). Cannot be used together with user',
'user' => 'Username, IP address or IP range you want to unblock. Cannot be used together with id',
- 'token' => 'An unblock token previously obtained through the gettoken parameter',
+ 'token' => 'An unblock token previously obtained through the gettoken parameter or prop=info',
'gettoken' => 'If set, an unblock token will be returned, and no other action will be taken',
'reason' => 'Reason for unblock (optional)',
);
@@ -116,6 +114,6 @@ class ApiUnblock extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiUnblock.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiUnblock.php 42651 2008-10-27 12:06:49Z catrope $';
}
}
diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php
index e054a70e..7ae9a3c0 100644
--- a/includes/api/ApiUndelete.php
+++ b/includes/api/ApiUndelete.php
@@ -51,8 +51,6 @@ class ApiUndelete extends ApiBase {
$this->dieUsageMsg(array('permdenied-undelete'));
if($wgUser->isBlocked())
$this->dieUsageMsg(array('blockedtext'));
- if(wfReadOnly())
- $this->dieUsageMsg(array('readonlytext'));
if(!$wgUser->matchEditToken($params['token']))
$this->dieUsageMsg(array('sessionfailure'));
@@ -69,7 +67,7 @@ class ApiUndelete extends ApiBase {
$params['timestamps'][$i] = wfTimestamp(TS_MW, $ts);
$pa = new PageArchive($titleObj);
- $dbw = wfGetDb(DB_MASTER);
+ $dbw = wfGetDB(DB_MASTER);
$dbw->begin();
$retval = $pa->undelete((isset($params['timestamps']) ? $params['timestamps'] : array()), $params['reason']);
if(!is_array($retval))
@@ -123,6 +121,6 @@ class ApiUndelete extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiUndelete.php 35348 2008-05-26 10:51:31Z catrope $';
+ return __CLASS__ . ': $Id: ApiUndelete.php 43270 2008-11-06 22:30:55Z siebrand $';
}
}
diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php
new file mode 100644
index 00000000..ab122fea
--- /dev/null
+++ b/includes/api/ApiWatch.php
@@ -0,0 +1,99 @@
+<?php
+
+/*
+ * Created on Jan 4, 2008
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2008 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 ('ApiBase.php');
+}
+
+/**
+ * API module to allow users to log out of the wiki. API equivalent of
+ * Special:Userlogout.
+ *
+ * @ingroup API
+ */
+class ApiWatch extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ public function execute() {
+ global $wgUser;
+ $this->getMain()->requestWriteMode();
+ if(!$wgUser->isLoggedIn())
+ $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin');
+ $params = $this->extractRequestParams();
+ $title = Title::newFromText($params['title']);
+ if(!$title)
+ $this->dieUsageMsg(array('invalidtitle', $params['title']));
+ $article = new Article($title);
+ $res = array('title' => $title->getPrefixedText());
+ if($params['unwatch'])
+ {
+ $res['unwatched'] = '';
+ $success = $article->doUnwatch();
+ }
+ else
+ {
+ $res['watched'] = '';
+ $success = $article->doWatch();
+ }
+ if(!$success)
+ $this->dieUsageMsg(array('hookaborted'));
+ $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'title' => null,
+ 'unwatch' => false,
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'title' => 'The page to (un)watch',
+ 'unwatch' => 'If set the page will be unwatched rather than watched',
+ );
+ }
+
+ public function getDescription() {
+ return array (
+ 'Add or remove a page from/to the current user\'s watchlist'
+ );
+ }
+
+ protected function getExamples() {
+ return array(
+ 'api.php?action=watch&title=Main_Page',
+ 'api.php?action=watch&title=Main_Page&unwatch',
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiWatch.php 40460 2008-09-04 22:20:32Z ialex $';
+ }
+}
diff --git a/includes/db/Database.php b/includes/db/Database.php
index 885ede54..84b88643 100644
--- a/includes/db/Database.php
+++ b/includes/db/Database.php
@@ -205,12 +205,17 @@ class Database {
return false;
}
- /**#@+
- * Get function
+ /**
+ * Return the last query that went through Database::query()
+ * @return String
*/
function lastQuery() { return $this->mLastQuery; }
+
+ /**
+ * Is a connection to the database open?
+ * @return Boolean
+ */
function isOpen() { return $this->mOpened; }
- /**#@-*/
function setFlag( $flag ) {
$this->mFlags |= $flag;
@@ -243,13 +248,13 @@ class Database {
# Other functions
#------------------------------------------------------------------------------
- /**@{{
+ /**
* Constructor.
- * @param string $server database server host
- * @param string $user database user name
- * @param string $password database user password
- * @param string $dbname database name
- * @param failFunction
+ * @param $server String: database server host
+ * @param $user String: database user name
+ * @param $password String: database user password
+ * @param $dbName String: database name
+ * @param $failFunction
* @param $flags
* @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
*/
@@ -293,7 +298,11 @@ class Database {
}
/**
- * @static
+ * Same as new Database( ... ), kept for backward compatibility
+ * @param $server String: database server host
+ * @param $user String: database user name
+ * @param $password String: database user password
+ * @param $dbName String: database name
* @param failFunction
* @param $flags
*/
@@ -305,9 +314,13 @@ class Database {
/**
* Usually aborts on failure
* If the failFunction is set to a non-zero integer, returns success
+ * @param $server String: database server host
+ * @param $user String: database user name
+ * @param $password String: database user password
+ * @param $dbName String: database name
*/
function open( $server, $user, $password, $dbName ) {
- global $wguname, $wgAllDBsAreLocalhost;
+ global $wgAllDBsAreLocalhost;
wfProfileIn( __METHOD__ );
# Test for missing mysql.so
@@ -338,13 +351,17 @@ class Database {
wfProfileIn("dbconnect-$server");
- # Try to connect up to three times
# The kernel's default SYN retransmission period is far too slow for us,
- # so we use a short timeout plus a manual retry.
+ # so we use a short timeout plus a manual retry. Retrying means that a small
+ # but finite rate of SYN packet loss won't cause user-visible errors.
$this->mConn = false;
- $max = 3;
+ if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) {
+ $numAttempts = 2;
+ } else {
+ $numAttempts = 1;
+ }
$this->installErrorHandler();
- for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
+ for ( $i = 0; $i < $numAttempts && !$this->mConn; $i++ ) {
if ( $i > 1 ) {
usleep( 1000 );
}
@@ -360,23 +377,28 @@ class Database {
}
}
$phpError = $this->restoreErrorHandler();
+ # Always log connection errors
+ if ( !$this->mConn ) {
+ $error = $this->lastError();
+ if ( !$error ) {
+ $error = $phpError;
+ }
+ wfLogDBError( "Error connecting to {$this->mServer}: $error\n" );
+ wfDebug( "DB connection error\n" );
+ wfDebug( "Server: $server, User: $user, Password: " .
+ substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
+ $success = false;
+ }
wfProfileOut("dbconnect-$server");
- if ( $dbName != '' ) {
- if ( $this->mConn !== false ) {
- $success = @/**/mysql_select_db( $dbName, $this->mConn );
- if ( !$success ) {
- $error = "Error selecting database $dbName on server {$this->mServer} " .
- "from client host {$wguname['nodename']}\n";
- wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
- wfDebug( $error );
- }
- } else {
- wfDebug( "DB connection error\n" );
- wfDebug( "Server: $server, User: $user, Password: " .
- substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
- $success = false;
+ if ( $dbName != '' && $this->mConn !== false ) {
+ $success = @/**/mysql_select_db( $dbName, $this->mConn );
+ if ( !$success ) {
+ $error = "Error selecting database $dbName on server {$this->mServer} " .
+ "from client host " . wfHostname() . "\n";
+ wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
+ wfDebug( $error );
}
} else {
# Delay USE query
@@ -405,16 +427,25 @@ class Database {
wfProfileOut( __METHOD__ );
return $success;
}
- /**@}}*/
protected function installErrorHandler() {
$this->mPHPError = false;
+ $this->htmlErrors = ini_set( 'html_errors', '0' );
set_error_handler( array( $this, 'connectionErrorHandler' ) );
}
protected function restoreErrorHandler() {
restore_error_handler();
- return $this->mPHPError;
+ if ( $this->htmlErrors !== false ) {
+ ini_set( 'html_errors', $this->htmlErrors );
+ }
+ if ( $this->mPHPError ) {
+ $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
+ $error = preg_replace( '!^.*?:(.*)$!', '$1', $error );
+ return $error;
+ } else {
+ return false;
+ }
}
protected function connectionErrorHandler( $errno, $errstr ) {
@@ -425,7 +456,7 @@ class Database {
* Closes a database connection.
* if it is open : commits any open transactions
*
- * @return bool operation success. true if already closed.
+ * @return Bool operation success. true if already closed.
*/
function close()
{
@@ -441,7 +472,7 @@ class Database {
}
/**
- * @param string $error fallback error message, used if none is given by MySQL
+ * @param $error String: fallback error message, used if none is given by MySQL
*/
function reportConnectionError( $error = 'Unknown error' ) {
$myError = $this->lastError();
@@ -457,7 +488,6 @@ class Database {
}
} else {
# New method
- wfLogDBError( "Connection error: $error\n" );
throw new DBConnectionError( $this, $error );
}
}
@@ -468,7 +498,7 @@ class Database {
* @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...
+ * @param $tempIgnore Boolean: 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
@@ -572,7 +602,7 @@ class Database {
* The DBMS-dependent part of query()
* @param $sql String: SQL query.
* @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
- * @access private
+ * @private
*/
/*private*/ function doQuery( $sql ) {
if( $this->bufferResults() ) {
@@ -584,11 +614,11 @@ class Database {
}
/**
- * @param $error
- * @param $errno
- * @param $sql
- * @param string $fname
- * @param bool $tempIgnore
+ * @param $error String
+ * @param $errno Integer
+ * @param $sql String
+ * @param $fname String
+ * @param $tempIgnore Boolean
*/
function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
global $wgCommandLineMode;
@@ -630,8 +660,8 @@ class Database {
/**
* Execute a prepared query with the various arguments
- * @param string $prepared the prepared sql
- * @param mixed $args Either an array here, or put scalars as varargs
+ * @param $prepared String: the prepared sql
+ * @param $args Mixed: Either an array here, or put scalars as varargs
*/
function execute( $prepared, $args = null ) {
if( !is_array( $args ) ) {
@@ -646,8 +676,8 @@ class Database {
/**
* Prepare & execute an SQL statement, quoting and inserting arguments
* in the appropriate places.
- * @param string $query
- * @param string $args ...
+ * @param $query String
+ * @param $args ...
*/
function safeQuery( $query, $args = null ) {
$prepared = $this->prepare( $query, 'Database::safeQuery' );
@@ -664,8 +694,8 @@ class Database {
/**
* For faking prepared SQL statements on DBs that don't support
* it directly.
- * @param string $preparedSql - a 'preparable' SQL statement
- * @param array $args - array of arguments to fill it with
+ * @param $preparedQuery String: a 'preparable' SQL statement
+ * @param $args Array of arguments to fill it with
* @return string executable SQL
*/
function fillPrepared( $preparedQuery, $args ) {
@@ -680,8 +710,8 @@ class Database {
* The arguments should be in $this->preparedArgs and must not be touched
* while we're doing this.
*
- * @param array $matches
- * @return string
+ * @param $matches Array
+ * @return String
* @private
*/
function fillPreparedArg( $matches ) {
@@ -702,11 +732,9 @@ class Database {
}
}
- /**#@+
- * @param mixed $res A SQL result
- */
/**
* Free a result object
+ * @param $res Mixed: A SQL result
*/
function freeResult( $res ) {
if ( $res instanceof ResultWrapper ) {
@@ -758,6 +786,7 @@ class Database {
/**
* Get the number of rows in a result object
+ * @param $res Mixed: A SQL result
*/
function numRows( $res ) {
if ( $res instanceof ResultWrapper ) {
@@ -773,6 +802,7 @@ class Database {
/**
* Get the number of fields in a result object
* See documentation for mysql_num_fields()
+ * @param $res Mixed: A SQL result
*/
function numFields( $res ) {
if ( $res instanceof ResultWrapper ) {
@@ -785,6 +815,8 @@ class Database {
* Get a field name in a result object
* See documentation for mysql_field_name():
* http://www.php.net/mysql_field_name
+ * @param $res Mixed: A SQL result
+ * @param $n Integer
*/
function fieldName( $res, $n ) {
if ( $res instanceof ResultWrapper ) {
@@ -808,6 +840,8 @@ class Database {
/**
* Change the position of the cursor in a result object
* See mysql_data_seek()
+ * @param $res Mixed: A SQL result
+ * @param $row Mixed: Either MySQL row or ResultWrapper
*/
function dataSeek( $res, $row ) {
if ( $res instanceof ResultWrapper ) {
@@ -854,7 +888,6 @@ class Database {
* See mysql_affected_rows() for more details
*/
function affectedRows() { return mysql_affected_rows( $this->mConn ); }
- /**#@-*/ // end of template : @param $result
/**
* Simple UPDATE wrapper
@@ -864,8 +897,7 @@ class Database {
* This function exists for historical reasons, Database::update() has a more standard
* calling convention and feature set
*/
- function set( $table, $var, $value, $cond, $fname = 'Database::set' )
- {
+ function set( $table, $var, $value, $cond, $fname = 'Database::set' ) {
$table = $this->tableName( $table );
$sql = "UPDATE $table SET $var = '" .
$this->strencode( $value ) . "' WHERE ($cond)";
@@ -890,7 +922,7 @@ class Database {
$row = $this->fetchRow( $res );
if ( $row !== false ) {
$this->freeResult( $res );
- return $row[0];
+ return reset( $row );
} else {
return false;
}
@@ -902,9 +934,9 @@ class Database {
*
* @private
*
- * @param array $options an associative array of options to be turned into
+ * @param $options Array: associative array of options to be turned into
* an SQL query, valid keys are listed in the function.
- * @return array
+ * @return Array
*/
function makeSelectOptions( $options ) {
$preLimitTail = $postLimitTail = '';
@@ -953,14 +985,14 @@ class Database {
/**
* SELECT wrapper
*
- * @param mixed $table Array or string, table name(s) (prefix auto-added)
- * @param mixed $vars Array or string, field name(s) to be retrieved
- * @param mixed $conds Array or string, condition(s) for WHERE
- * @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
- * see Database::makeSelectOptions code for list of supported stuff
- * @param array $join_conds Associative array of table join conditions (optional)
- * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
+ * @param $table Mixed: Array or string, table name(s) (prefix auto-added)
+ * @param $vars Mixed: Array or string, field name(s) to be retrieved
+ * @param $conds Mixed: Array or string, condition(s) for WHERE
+ * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
+ * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+ * see Database::makeSelectOptions code for list of supported stuff
+ * @param $join_conds Array: Associative array of table join conditions (optional)
+ * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
* @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
*/
function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() )
@@ -972,14 +1004,14 @@ class Database {
/**
* SELECT wrapper
*
- * @param mixed $table Array or string, table name(s) (prefix auto-added)
- * @param mixed $vars Array or string, field name(s) to be retrieved
- * @param mixed $conds Array or string, condition(s) for WHERE
- * @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
- * see Database::makeSelectOptions code for list of supported stuff
- * @param array $join_conds Associative array of table join conditions (optional)
- * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
+ * @param $table Mixed: Array or string, table name(s) (prefix auto-added)
+ * @param $vars Mixed: Array or string, field name(s) to be retrieved
+ * @param $conds Mixed: Array or string, condition(s) for WHERE
+ * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
+ * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+ * see Database::makeSelectOptions code for list of supported stuff
+ * @param $join_conds Array: Associative array of table join conditions (optional)
+ * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
* @return string, the SQL text
*/
function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) {
@@ -1030,13 +1062,17 @@ class Database {
* Single row SELECT wrapper
* Aborts or returns FALSE on error
*
- * $vars: the selected variables
- * $conds: a condition map, terms are ANDed together.
+ * @param $table String: table name
+ * @param $vars String: the selected variables
+ * @param $conds Array: a condition map, terms are ANDed together.
* Items with numeric keys are taken to be literal conditions
* Takes an array of selected variables, and a condition map, which is ANDed
* e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" =>
* NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where
* $obj- >page_id is the ID of the Astronomy article
+ * @param $fname String: Calling functio name
+ * @param $options Array
+ * @param $join_conds Array
*
* @todo migrate documentation to phpdocumentor format
*/
@@ -1086,8 +1122,7 @@ class Database {
* Removes most variables from an SQL query and replaces them with X or N for numbers.
* It's only slightly flawed. Don't use for anything important.
*
- * @param string $sql A SQL Query
- * @static
+ * @param $sql String: A SQL Query
*/
static function generalizeSQL( $sql ) {
# This does the same as the regexp below would do, but in such a way
@@ -1280,7 +1315,7 @@ class Database {
* Make UPDATE options for the Database::update function
*
* @private
- * @param array $options The options passed to Database::update
+ * @param $options Array: The options passed to Database::update
* @return string
*/
function makeUpdateOptions( $options ) {
@@ -1298,14 +1333,14 @@ class Database {
/**
* UPDATE wrapper, takes a condition array and a SET array
*
- * @param string $table The table to UPDATE
- * @param array $values An array of values to SET
- * @param array $conds An array of conditions (WHERE). Use '*' to update all rows.
- * @param string $fname The Class::Function calling this function
- * (for the log)
- * @param array $options An array of UPDATE options, can be one or
+ * @param $table String: The table to UPDATE
+ * @param $values Array: An array of values to SET
+ * @param $conds Array: An array of conditions (WHERE). Use '*' to update all rows.
+ * @param $fname String: The Class::Function calling this function
+ * (for the log)
+ * @param $options Array: An array of UPDATE options, can be one or
* more of IGNORE, LOW_PRIORITY
- * @return bool
+ * @return Boolean
*/
function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
$table = $this->tableName( $table );
@@ -1410,8 +1445,8 @@ class Database {
* themselves. Pass the canonical name to such functions. This is only needed
* when calling query() directly.
*
- * @param string $name database table name
- * @return string full database name
+ * @param $name String: database table name
+ * @return String: full database name
*/
function tableName( $name ) {
global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
@@ -1540,8 +1575,8 @@ class Database {
/**
* Wrapper for addslashes()
- * @param string $s String to be slashed.
- * @return string slashed string.
+ * @param $s String: to be slashed.
+ * @return String: slashed string.
*/
function strencode( $s ) {
return mysql_real_escape_string( $s, $this->mConn );
@@ -1632,11 +1667,12 @@ class Database {
*
* DO NOT put the join condition in $conds
*
- * @param string $delTable The table to delete from.
- * @param string $joinTable The other table.
- * @param string $delVar The variable to join on, in the first table.
- * @param string $joinVar The variable to join on, in the second table.
- * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
+ * @param $delTable String: The table to delete from.
+ * @param $joinTable String: The other table.
+ * @param $delVar String: The variable to join on, in the first table.
+ * @param $joinVar String: The variable to join on, in the second table.
+ * @param $conds Array: Condition array of field names mapped to variables, ANDed together in the WHERE clause
+ * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
*/
function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
if ( !$conds ) {
@@ -1732,9 +1768,9 @@ class Database {
/**
* Construct a LIMIT query with optional offset
* This is used for query pages
- * $sql string SQL query we will append the limit too
- * $limit integer the SQL limit
- * $offset integer the SQL offset (default false)
+ * @param $sql String: SQL query we will append the limit too
+ * @param $limit Integer: the SQL limit
+ * @param $offset Integer the SQL offset (default false)
*/
function limitResult($sql, $limit, $offset=false) {
if( !is_numeric($limit) ) {
@@ -1752,10 +1788,10 @@ class Database {
* Returns an SQL expression for a simple conditional.
* Uses IF on MySQL.
*
- * @param string $cond SQL expression which will result in a boolean value
- * @param string $trueVal SQL expression to return if true
- * @param string $falseVal SQL expression to return if false
- * @return string SQL fragment
+ * @param $cond String: SQL expression which will result in a boolean value
+ * @param $trueVal String: SQL expression to return if true
+ * @param $falseVal String: SQL expression to return if false
+ * @return String: SQL fragment
*/
function conditional( $cond, $trueVal, $falseVal ) {
return " IF($cond, $trueVal, $falseVal) ";
@@ -1765,9 +1801,9 @@ class Database {
* Returns a comand for str_replace function in SQL query.
* Uses REPLACE() in MySQL
*
- * @param string $orig String or column to modify
- * @param string $old String or column to seek
- * @param string $new String or column to replace with
+ * @param $orig String: column to modify
+ * @param $old String: column to seek
+ * @param $new String: column to replace with
*/
function strreplace( $orig, $old, $new ) {
return "REPLACE({$orig}, {$old}, {$new})";
@@ -1838,9 +1874,8 @@ class Database {
/**
* Do a SELECT MASTER_POS_WAIT()
*
- * @param string $file the binlog file
- * @param string $pos the binlog position
- * @param integer $timeout the maximum number of seconds to wait for synchronisation
+ * @param $pos MySQLMasterPos object
+ * @param $timeout Integer: the maximum number of seconds to wait for synchronisation
*/
function masterPosWait( MySQLMasterPos $pos, $timeout ) {
$fname = 'Database::masterPosWait';
@@ -1896,7 +1931,8 @@ class Database {
$res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' );
$row = $this->fetchObject( $res );
if ( $row ) {
- return new MySQLMasterPos( $row->Master_Log_File, $row->Read_Master_Log_Pos );
+ $pos = isset($row->Exec_master_log_pos) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos;
+ return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos );
} else {
return false;
}
@@ -2001,14 +2037,14 @@ class Database {
}
/**
- * @return string wikitext of a link to the server software's web site
+ * @return String: wikitext of a link to the server software's web site
*/
function getSoftwareLink() {
return "[http://www.mysql.com/ MySQL]";
}
/**
- * @return string Version information from the database
+ * @return String: Version information from the database
*/
function getServerVersion() {
return mysql_get_server_info( $this->mConn );
@@ -2106,7 +2142,7 @@ class Database {
* May be useful for very long batch queries such as
* full-wiki dumps, where a single query reads out
* over hours or days.
- * @param int $timeout in seconds
+ * @param $timeout Integer in seconds
*/
public function setTimeout( $timeout ) {
$this->query( "SET net_read_timeout=$timeout" );
@@ -2116,9 +2152,9 @@ class Database {
/**
* Read and execute SQL commands from a file.
* Returns true on success, error string or exception on failure (depending on object's error ignore settings)
- * @param string $filename File name to open
- * @param callback $lineCallback Optional function called before reading each line
- * @param callback $resultCallback Optional function called for each MySQL result
+ * @param $filename String: File name to open
+ * @param $lineCallback Callback: Optional function called before reading each line
+ * @param $resultCallback Callback: Optional function called for each MySQL result
*/
function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
$fp = fopen( $filename, 'r' );
@@ -2133,9 +2169,9 @@ class Database {
/**
* Read and execute commands from an open file handle
* Returns true on success, error string or exception on failure (depending on object's error ignore settings)
- * @param string $fp File handle
- * @param callback $lineCallback Optional function called before reading each line
- * @param callback $resultCallback Optional function called for each MySQL result
+ * @param $fp String: File handle
+ * @param $lineCallback Callback: Optional function called before reading each line
+ * @param $resultCallback Callback: Optional function called for each MySQL result
*/
function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
$cmd = "";
@@ -2177,7 +2213,7 @@ class Database {
$cmd = $this->replaceVars( $cmd );
$res = $this->query( $cmd, __METHOD__ );
if ( $resultCallback ) {
- call_user_func( $resultCallback, $res );
+ call_user_func( $resultCallback, $res, $this );
}
if ( false === $res ) {
@@ -2240,8 +2276,8 @@ class Database {
* Abstracted from Filestore::lock() so child classes can implement for
* their own needs.
*
- * @param string $lockName Name of lock to aquire
- * @param string $method Name of method calling us
+ * @param $lockName String: Name of lock to aquire
+ * @param $method String: Name of method calling us
* @return bool
*/
public function lock( $lockName, $method ) {
@@ -2263,14 +2299,24 @@ class Database {
* @todo fixme - Figure out a way to return a bool
* based on successful lock release.
*
- * @param string $lockName Name of lock to release
- * @param string $method Name of method calling us
+ * @param $lockName String: Name of lock to release
+ * @param $method String: Name of method calling us
*/
public function unlock( $lockName, $method ) {
$lockName = $this->addQuotes( $lockName );
$result = $this->query( "SELECT RELEASE_LOCK($lockName)", $method );
$this->freeResult( $result );
}
+
+ /**
+ * Get search engine class. All subclasses of this
+ * need to implement this if they wish to use searching.
+ *
+ * @return String
+ */
+ public function getSearchEngine() {
+ return "SearchMySQL";
+ }
}
/**
@@ -2390,8 +2436,8 @@ class DBError extends MWException {
/**
* Construct a database error
- * @param Database $db The database object which threw the error
- * @param string $error A simple error message to be used for debugging
+ * @param $db Database object which threw the error
+ * @param $error A simple error message to be used for debugging
*/
function __construct( Database &$db, $error ) {
$this->db =& $db;
@@ -2483,7 +2529,13 @@ border=\"0\" ALT=\"Google\"></A>
}
$text = str_replace( '$1', $this->error, $noconnect );
- $text .= wfGetSiteNotice();
+
+ /*
+ if ( $GLOBALS['wgShowExceptionDetails'] ) {
+ $text .= '</p><p>Backtrace:</p><p>' .
+ nl2br( htmlspecialchars( $this->getTraceAsString() ) ) .
+ "</p>\n";
+ }*/
if($wgUseFileCache) {
if($wgTitle) {
@@ -2504,13 +2556,13 @@ border=\"0\" ALT=\"Google\"></A>
$cache = new HTMLFileCache( $t );
if( $cache->isFileCached() ) {
// @todo, FIXME: $msg is not defined on the next line.
- $msg = '<p style="color: red"><b>'.$msg."<br />\n" .
+ $msg = '<p style="color: red"><b>'.$text."<br />\n" .
$cachederror . "</b></p>\n";
$tag = '<div id="article">';
$text = str_replace(
$tag,
- $tag . $msg,
+ $tag . $text,
$cache->fetchPageText() );
}
}
diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php
index 32fe28b1..28ccab2d 100644
--- a/includes/db/DatabaseMssql.php
+++ b/includes/db/DatabaseMssql.php
@@ -105,7 +105,7 @@ class DatabaseMssql extends Database {
$success = @/**/mssql_select_db($dbName, $this->mConn);
if (!$success) {
$error = "Error selecting database $dbName on server {$this->mServer} " .
- "from client host {$wguname['nodename']}\n";
+ "from client host " . wfHostname() . "\n";
wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
wfDebug( $error );
}
@@ -154,9 +154,6 @@ class DatabaseMssql extends Database {
return $ret;
}
- /**#@+
- * @param mixed $res A SQL result
- */
/**
* Free a result object
*/
@@ -225,6 +222,7 @@ class DatabaseMssql extends Database {
/**
* Get the number of fields in a result object
* See documentation for mysql_num_fields()
+ * @param $res SQL result object as returned from Database::query(), etc.
*/
function numFields( $res ) {
if ( $res instanceof ResultWrapper ) {
@@ -237,6 +235,8 @@ class DatabaseMssql extends Database {
* Get a field name in a result object
* See documentation for mysql_field_name():
* http://www.php.net/mysql_field_name
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @param $n Int
*/
function fieldName( $res, $n ) {
if ( $res instanceof ResultWrapper ) {
@@ -263,6 +263,8 @@ class DatabaseMssql extends Database {
/**
* Change the position of the cursor in a result object
* See mysql_data_seek()
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @param $row Database row
*/
function dataSeek( $res, $row ) {
if ( $res instanceof ResultWrapper ) {
@@ -339,7 +341,7 @@ class DatabaseMssql extends Database {
*
* @private
*
- * @param array $options an associative array of options to be turned into
+ * @param $options Array: an associative array of options to be turned into
* an SQL query, valid keys are listed in the function.
* @return array
*/
@@ -390,11 +392,11 @@ class DatabaseMssql extends Database {
/**
* SELECT wrapper
*
- * @param mixed $table Array or string, table name(s) (prefix auto-added)
- * @param mixed $vars Array or string, field name(s) to be retrieved
- * @param mixed $conds Array or string, condition(s) for WHERE
- * @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+ * @param $table Mixed: Array or string, table name(s) (prefix auto-added)
+ * @param $vars Mixed: Array or string, field name(s) to be retrieved
+ * @param $conds Mixed: Array or string, condition(s) for WHERE
+ * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
+ * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')),
* see Database::makeSelectOptions code for list of supported stuff
* @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
*/
@@ -643,12 +645,12 @@ class DatabaseMssql extends Database {
/**
* UPDATE wrapper, takes a condition array and a SET array
*
- * @param string $table The table to UPDATE
- * @param array $values An array of values to SET
- * @param array $conds An array of conditions (WHERE). Use '*' to update all rows.
- * @param string $fname The Class::Function calling this function
- * (for the log)
- * @param array $options An array of UPDATE options, can be one or
+ * @param $table String: The table to UPDATE
+ * @param $values Array: An array of values to SET
+ * @param $conds Array: An array of conditions (WHERE). Use '*' to update all rows.
+ * @param $fname String: The Class::Function calling this function
+ * (for the log)
+ * @param $options Array: An array of UPDATE options, can be one or
* more of IGNORE, LOW_PRIORITY
* @return bool
*/
@@ -666,7 +668,7 @@ class DatabaseMssql extends Database {
* Make UPDATE options for the Database::update function
*
* @private
- * @param array $options The options passed to Database::update
+ * @param $options Array: The options passed to Database::update
* @return string
*/
function makeUpdateOptions( $options ) {
@@ -698,7 +700,7 @@ class DatabaseMssql extends Database {
/**
* MSSQL doubles quotes instead of escaping them
- * @param string $s String to be slashed.
+ * @param $s String to be slashed.
* @return string slashed string.
*/
function strencode($s) {
@@ -755,11 +757,12 @@ class DatabaseMssql extends Database {
*
* DO NOT put the join condition in $conds
*
- * @param string $delTable The table to delete from.
- * @param string $joinTable The other table.
- * @param string $delVar The variable to join on, in the first table.
- * @param string $joinVar The variable to join on, in the second table.
- * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
+ * @param $delTable String: The table to delete from.
+ * @param $joinTable String: The other table.
+ * @param $delVar String: The variable to join on, in the first table.
+ * @param $joinVar String: The variable to join on, in the second table.
+ * @param $conds Array: Condition array of field names mapped to variables, ANDed together in the WHERE clause
+ * @param $fname String: Calling function name
*/
function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
if ( !$conds ) {
@@ -857,9 +860,9 @@ class DatabaseMssql extends Database {
/**
* Returns an SQL expression for a simple conditional.
*
- * @param string $cond SQL expression which will result in a boolean value
- * @param string $trueVal SQL expression to return if true
- * @param string $falseVal SQL expression to return if false
+ * @param $cond String: SQL expression which will result in a boolean value
+ * @param $trueVal String: SQL expression to return if true
+ * @param $falseVal String: SQL expression to return if false
* @return string SQL fragment
*/
function conditional( $cond, $trueVal, $falseVal ) {
@@ -1007,6 +1010,10 @@ class DatabaseMssql extends Database {
public function unlock( $lockName, $method ) {
return true;
}
+
+ public function getSearchEngine() {
+ return "SearchEngineDummy";
+ }
}
diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php
index f4dbac71..4c37a507 100644
--- a/includes/db/DatabaseOracle.php
+++ b/includes/db/DatabaseOracle.php
@@ -509,10 +509,10 @@ class DatabaseOracle extends Database {
* Returns an SQL expression for a simple conditional.
* Uses CASE on Oracle
*
- * @param string $cond SQL expression which will result in a boolean value
- * @param string $trueVal SQL expression to return if true
- * @param string $falseVal SQL expression to return if false
- * @return string SQL fragment
+ * @param $cond String: SQL expression which will result in a boolean value
+ * @param $trueVal String: SQL expression to return if true
+ * @param $falseVal String: SQL expression to return if false
+ * @return String: SQL fragment
*/
function conditional( $cond, $trueVal, $falseVal ) {
return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
@@ -640,7 +640,7 @@ echo "error!\n";
*
* @private
*
- * @param array $options an associative array of options to be turned into
+ * @param $options Array: an associative array of options to be turned into
* an SQL query, valid keys are listed in the function.
* @return array
*/
@@ -716,5 +716,9 @@ echo "error!\n";
public function unlock( $lockName, $method ) {
return true;
}
+
+ public function getSearchEngine() {
+ return "SearchOracle";
+ }
} // end DatabaseOracle class
diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php
index 8fd04cb6..16a74b53 100644
--- a/includes/db/DatabasePostgres.php
+++ b/includes/db/DatabasePostgres.php
@@ -78,12 +78,6 @@ class DatabasePostgres extends Database {
$failFunction = false, $flags = 0 )
{
- global $wgOut;
- # Can't get a reference if it hasn't been set yet
- if ( !isset( $wgOut ) ) {
- $wgOut = NULL;
- }
- $this->mOut =& $wgOut;
$this->mFailFunction = $failFunction;
$this->mFlags = $flags;
$this->open( $server, $user, $password, $dbName);
@@ -138,10 +132,9 @@ class DatabasePostgres extends Database {
global $wgDBport;
- if (!strlen($user)) { ## e.g. the class is being loaded
- return;
+ if (!strlen($user)) { ## e.g. the class is being loaded
+ return;
}
-
$this->close();
$this->mServer = $server;
$this->mPort = $port = $wgDBport;
@@ -149,22 +142,31 @@ class DatabasePostgres extends Database {
$this->mPassword = $password;
$this->mDBname = $dbName;
- $hstring="";
+ $connectVars = array(
+ 'dbname' => $dbName,
+ 'user' => $user,
+ 'password' => $password );
if ($server!=false && $server!="") {
- $hstring="host=$server ";
+ $connectVars['host'] = $server;
}
if ($port!=false && $port!="") {
- $hstring .= "port=$port ";
+ $connectVars['port'] = $port;
}
+ $connectString = $this->makeConnectionString( $connectVars );
- error_reporting( E_ALL );
- @$this->mConn = pg_connect("$hstring dbname=$dbName user=$user password=$password");
+ $this->installErrorHandler();
+ $this->mConn = pg_connect( $connectString );
+ $phpError = $this->restoreErrorHandler();
if ( $this->mConn == false ) {
wfDebug( "DB connection error\n" );
wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
wfDebug( $this->lastError()."\n" );
- return false;
+ if ( !$this->mFailFunction ) {
+ throw new DBConnectionError( $this, $phpError );
+ } else {
+ return false;
+ }
}
$this->mOpened = true;
@@ -189,6 +191,14 @@ class DatabasePostgres extends Database {
return $this->mConn;
}
+ function makeConnectionString( $vars ) {
+ $s = '';
+ foreach ( $vars as $name => $value ) {
+ $s .= "$name='" . str_replace( "'", "\\'", $value ) . "' ";
+ }
+ return $s;
+ }
+
function initial_setup($password, $dbName) {
// If this is the initial connection, setup the schema stuff and possibly create the user
@@ -197,9 +207,8 @@ class DatabasePostgres extends Database {
print "<li>Checking the version of Postgres...";
$version = $this->getServerVersion();
$PGMINVER = '8.1';
- if ($this->numeric_version < $PGMINVER) {
- print "<b>FAILED</b>. Required version is $PGMINVER. You have " .
- htmlspecialchars( $this->numeric_version ) . " (" . htmlspecialchars( $version ) . ")</li>\n";
+ if ($version < $PGMINVER) {
+ print "<b>FAILED</b>. Required version is $PGMINVER. You have " . htmlspecialchars( $version ) . "</li>\n";
dieout("</ul>");
}
print "version " . htmlspecialchars( $this->numeric_version ) . " is OK.</li>\n";
@@ -730,10 +739,10 @@ class DatabasePostgres extends Database {
* $args may be a single associative array, or an array of these with numeric keys,
* for multi-row insert (Postgres version 8.2 and above only).
*
- * @param array $table String: Name of the table to insert to.
- * @param array $args Array: Items to insert into the table.
- * @param array $fname String: Name of the function, for profiling
- * @param mixed $options String or Array. Valid options: IGNORE
+ * @param $table String: Name of the table to insert to.
+ * @param $args Array: Items to insert into the table.
+ * @param $fname String: Name of the function, for profiling
+ * @param $options String or Array. Valid options: IGNORE
*
* @return bool Success of insert operation. IGNORE always returns true.
*/
@@ -746,8 +755,7 @@ class DatabasePostgres extends Database {
$table = $this->tableName( $table );
if (! isset( $wgDBversion ) ) {
- $this->getServerVersion();
- $wgDBversion = $this->numeric_version;
+ $wgDBversion = $this->getServerVersion();
}
if ( !is_array( $options ) )
@@ -1009,10 +1017,10 @@ class DatabasePostgres extends Database {
* Returns an SQL expression for a simple conditional.
* Uses CASE on Postgres
*
- * @param string $cond SQL expression which will result in a boolean value
- * @param string $trueVal SQL expression to return if true
- * @param string $falseVal SQL expression to return if false
- * @return string SQL fragment
+ * @param $cond String: SQL expression which will result in a boolean value
+ * @param $trueVal String: SQL expression to return if true
+ * @param $falseVal String: SQL expression to return if false
+ * @return String: SQL fragment
*/
function conditional( $cond, $trueVal, $falseVal ) {
return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
@@ -1055,7 +1063,7 @@ class DatabasePostgres extends Database {
/**
* @return string wikitext of a link to the server software's web site
*/
- function getSoftwareLink() {
+ function getSoftwareLink() {
return "[http://www.postgresql.org/ PostgreSQL]";
}
@@ -1063,16 +1071,17 @@ class DatabasePostgres extends Database {
* @return string Version information from the database
*/
function getServerVersion() {
- $version = pg_fetch_result($this->doQuery("SELECT version()"),0,0);
- $thisver = array();
- if (!preg_match('/PostgreSQL (\d+\.\d+)(\S+)/', $version, $thisver)) {
- die("Could not determine the numeric version from $version!");
+ $versionInfo = pg_version( $this->mConn );
+ if ( isset( $versionInfo['server'] ) ) {
+ $this->numeric_version = $versionInfo['server'];
+ } else {
+ // There's no way to identify the precise version before 7.4, but
+ // it doesn't matter anyway since we're just going to give an error.
+ $this->numeric_version = '7.3 or earlier';
}
- $this->numeric_version = $thisver[1];
- return $version;
+ return $this->numeric_version;
}
-
/**
* Query whether a given relation exists (in the given schema, or the
* default mw one if not given)
@@ -1319,7 +1328,7 @@ END;
*
* @private
*
- * @param string $com SQL string, read from a stream (usually tables.sql)
+ * @param $ins String: SQL string, read from a stream (usually tables.sql)
*
* @return string SQL string
*/
@@ -1344,7 +1353,7 @@ END;
*
* @private
*
- * @param array $options an associative array of options to be turned into
+ * @param $options Array: an associative array of options to be turned into
* an SQL query, valid keys are listed in the function.
* @return array
*/
@@ -1417,5 +1426,9 @@ END;
public function unlock( $lockName, $method ) {
return true;
}
+
+ public function getSearchEngine() {
+ return "SearchPostgres";
+ }
} // end DatabasePostgres class
diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php
index 112c417b..dfc506cc 100644
--- a/includes/db/DatabaseSqlite.php
+++ b/includes/db/DatabaseSqlite.php
@@ -20,11 +20,9 @@ class DatabaseSqlite extends Database {
* Constructor
*/
function __construct($server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0) {
- global $wgOut,$wgSQLiteDataDir;
+ global $wgOut,$wgSQLiteDataDir, $wgSQLiteDataDirMode;
if ("$wgSQLiteDataDir" == '') $wgSQLiteDataDir = dirname($_SERVER['DOCUMENT_ROOT']).'/data';
- if (!is_dir($wgSQLiteDataDir)) mkdir($wgSQLiteDataDir,0700);
- if (!isset($wgOut)) $wgOut = NULL; # Can't get a reference if it hasn't been set yet
- $this->mOut =& $wgOut;
+ if (!is_dir($wgSQLiteDataDir)) wfMkdirParents( $wgSQLiteDataDir, $wgSQLiteDataDirMode );
$this->mFailFunction = $failFunction;
$this->mFlags = $flags;
$this->mDatabaseFile = "$wgSQLiteDataDir/$dbName.sqlite";
@@ -48,11 +46,28 @@ class DatabaseSqlite extends Database {
$this->mConn = false;
if ($dbName) {
$file = $this->mDatabaseFile;
- if ($this->mFlags & DBO_PERSISTENT) $this->mConn = new PDO("sqlite:$file",$user,$pass,array(PDO::ATTR_PERSISTENT => true));
- else $this->mConn = new PDO("sqlite:$file",$user,$pass);
- if ($this->mConn === false) wfDebug("DB connection error: $err\n");;
+ try {
+ if ( $this->mFlags & DBO_PERSISTENT ) {
+ $this->mConn = new PDO( "sqlite:$file", $user, $pass,
+ array( PDO::ATTR_PERSISTENT => true ) );
+ } else {
+ $this->mConn = new PDO( "sqlite:$file", $user, $pass );
+ }
+ } catch ( PDOException $e ) {
+ $err = $e->getMessage();
+ }
+ if ( $this->mConn === false ) {
+ wfDebug( "DB connection error: $err\n" );
+ if ( !$this->mFailFunction ) {
+ throw new DBConnectionError( $this, $err );
+ } else {
+ return false;
+ }
+
+ }
$this->mOpened = $this->mConn;
- $this->mConn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_SILENT); # set error codes only, dont raise exceptions
+ # set error codes only, don't raise exceptions
+ $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
}
return $this->mConn;
}
@@ -390,7 +405,19 @@ class DatabaseSqlite extends Database {
public function unlock( $lockName, $method ) {
return true;
}
+
+ public function getSearchEngine() {
+ return "SearchEngineDummy";
+ }
+ /**
+ * No-op version of deadlockLoop
+ */
+ public function deadlockLoop( /*...*/ ) {
+ $args = func_get_args();
+ $function = array_shift( $args );
+ return call_user_func_array( $function, $args );
+ }
}
/**
diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php
index 48c2d99b..820aa2ea 100644
--- a/includes/db/LBFactory_Multi.php
+++ b/includes/db/LBFactory_Multi.php
@@ -36,6 +36,8 @@
*
* masterTemplateOverrides An override array for all master servers.
*
+ * readOnlyBySection A map of section name to read-only message. Missing or false for read/write.
+ *
* @ingroup Database
*/
class LBFactory_Multi extends LBFactory {
@@ -44,7 +46,7 @@ class LBFactory_Multi extends LBFactory {
// Optional settings
var $groupLoadsBySection = array(), $groupLoadsByDB = array(), $hostsByName = array();
var $externalLoads = array(), $externalTemplateOverrides, $templateOverridesByServer;
- var $templateOverridesByCluster, $masterTemplateOverrides;
+ var $templateOverridesByCluster, $masterTemplateOverrides, $readOnlyBySection = array();
// Other stuff
var $conf, $mainLBs = array(), $extLBs = array();
var $lastWiki, $lastSection;
@@ -55,7 +57,8 @@ class LBFactory_Multi extends LBFactory {
$required = array( 'sectionsByDB', 'sectionLoads', 'serverTemplate' );
$optional = array( 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
- 'templateOverridesByCluster', 'masterTemplateOverrides' );
+ 'templateOverridesByCluster', 'masterTemplateOverrides',
+ 'readOnlyBySection' );
foreach ( $required as $key ) {
if ( !isset( $conf[$key] ) ) {
@@ -69,6 +72,13 @@ class LBFactory_Multi extends LBFactory {
$this->$key = $conf[$key];
}
}
+
+ // Check for read-only mode
+ $section = $this->getSectionForWiki();
+ if ( !empty( $this->readOnlyBySection[$section] ) ) {
+ global $wgReadOnly;
+ $wgReadOnly = $this->readOnlyBySection[$section];
+ }
}
function getSectionForWiki( $wiki = false ) {
diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php
index 42c4044d..f847fe22 100644
--- a/includes/db/LoadBalancer.php
+++ b/includes/db/LoadBalancer.php
@@ -13,7 +13,7 @@
class LoadBalancer {
/* private */ var $mServers, $mConns, $mLoads, $mGroupLoads;
/* private */ var $mFailFunction, $mErrorConnection;
- /* private */ var $mReadIndex, $mLastIndex, $mAllowLagged;
+ /* private */ var $mReadIndex, $mAllowLagged;
/* private */ var $mWaitForPos, $mWaitTimeout;
/* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error';
/* private */ var $mParentInfo, $mLagTimes;
@@ -50,7 +50,6 @@ class LoadBalancer {
'local' => array(),
'foreignUsed' => array(),
'foreignFree' => array() );
- $this->mLastIndex = -1;
$this->mLoads = array();
$this->mWaitForPos = false;
$this->mLaggedSlaveMode = false;
@@ -399,11 +398,20 @@ class LoadBalancer {
/**
* Get a connection by index
* This is the main entry point for this class.
+ * @param int $i Database
+ * @param array $groups Query groups
+ * @param string $wiki Wiki ID
*/
public function &getConnection( $i, $groups = array(), $wiki = false ) {
global $wgDBtype;
wfProfileIn( __METHOD__ );
+ if ( $i == DB_LAST ) {
+ throw new MWException( 'Attempt to call ' . __METHOD__ . ' with deprecated server index DB_LAST' );
+ } elseif ( $i === null || $i === false ) {
+ throw new MWException( 'Attempt to call ' . __METHOD__ . ' with invalid server index' );
+ }
+
if ( $wiki === wfWikiID() ) {
$wiki = false;
}
@@ -433,19 +441,13 @@ class LoadBalancer {
# Operation-based index
if ( $i == DB_SLAVE ) {
$i = $this->getReaderIndex( false, $wiki );
- } elseif ( $i == DB_LAST ) {
- # Just use $this->mLastIndex, which should already be set
- $i = $this->mLastIndex;
- if ( $i === -1 ) {
- # Oh dear, not set, best to use the writer for safety
- wfDebug( "Warning: DB_LAST used when there was no previous index\n" );
- $i = $this->getWriterIndex();
+ # Couldn't find a working server in getReaderIndex()?
+ if ( $i === false ) {
+ $this->mLastError = 'No working slave server: ' . $this->mLastError;
+ $this->reportConnectionError( $this->mErrorConnection );
+ return false;
}
}
- # Couldn't find a working server in getReaderIndex()?
- if ( $i === false ) {
- $this->reportConnectionError( $this->mErrorConnection );
- }
# Now we have an explicit index into the servers array
$conn = $this->openConnection( $i, $wiki );
@@ -525,7 +527,7 @@ class LoadBalancer {
} else {
$server = $this->mServers[$i];
$server['serverIndex'] = $i;
- $conn = $this->reallyOpenConnection( $server );
+ $conn = $this->reallyOpenConnection( $server, false );
if ( $conn->isOpen() ) {
$this->mConns['local'][$i][0] = $conn;
} else {
@@ -534,7 +536,6 @@ class LoadBalancer {
$conn = false;
}
}
- $this->mLastIndex = $i;
wfProfileOut( __METHOD__ );
return $conn;
}
@@ -576,9 +577,8 @@ class LoadBalancer {
$oldWiki = key( $this->mConns['foreignFree'][$i] );
if ( !$conn->selectDB( $dbName ) ) {
- global $wguname;
$this->mLastError = "Error selecting database $dbName on server " .
- $conn->getServer() . " from client host {$wguname['nodename']}\n";
+ $conn->getServer() . " from client host " . wfHostname() . "\n";
$this->mErrorConnection = $conn;
$conn = false;
} else {
@@ -598,6 +598,7 @@ class LoadBalancer {
$this->mErrorConnection = $conn;
$conn = false;
} else {
+ $conn->tablePrefix( $prefix );
$this->mConns['foreignUsed'][$i][$wiki] = $conn;
wfDebug( __METHOD__.": opened new connection for $i/$wiki\n" );
}
@@ -661,31 +662,27 @@ class LoadBalancer {
function reportConnectionError( &$conn ) {
wfProfileIn( __METHOD__ );
- # Prevent infinite recursion
-
- static $reporting = false;
- if ( !$reporting ) {
- $reporting = true;
- if ( !is_object( $conn ) ) {
- // No last connection, probably due to all servers being too busy
- $conn = new Database;
- if ( $this->mFailFunction ) {
- $conn->failFunction( $this->mFailFunction );
- $conn->reportConnectionError( $this->mLastError );
- } else {
- // If all servers were busy, mLastError will contain something sensible
- throw new DBConnectionError( $conn, $this->mLastError );
- }
+
+ if ( !is_object( $conn ) ) {
+ // No last connection, probably due to all servers being too busy
+ wfLogDBError( "LB failure with no last connection\n" );
+ $conn = new Database;
+ if ( $this->mFailFunction ) {
+ $conn->failFunction( $this->mFailFunction );
+ $conn->reportConnectionError( $this->mLastError );
} else {
- if ( $this->mFailFunction ) {
- $conn->failFunction( $this->mFailFunction );
- } else {
- $conn->failFunction( false );
- }
- $server = $conn->getProperty( 'mServer' );
- $conn->reportConnectionError( "{$this->mLastError} ({$server})" );
+ // If all servers were busy, mLastError will contain something sensible
+ throw new DBConnectionError( $conn, $this->mLastError );
+ }
+ } else {
+ if ( $this->mFailFunction ) {
+ $conn->failFunction( $this->mFailFunction );
+ } else {
+ $conn->failFunction( false );
}
- $reporting = false;
+ $server = $conn->getProperty( 'mServer' );
+ wfLogDBError( "Connection error: {$this->mLastError} ({$server})\n" );
+ $conn->reportConnectionError( "{$this->mLastError} ({$server})" );
}
wfProfileOut( __METHOD__ );
}
diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php
index 8e16f1a1..929ab2b9 100644
--- a/includes/db/LoadMonitor.php
+++ b/includes/db/LoadMonitor.php
@@ -64,6 +64,9 @@ class LoadMonitor_MySQL implements LoadMonitor {
$requestRate = 10;
global $wgMemc;
+ if ( empty( $wgMemc ) )
+ $wgMemc = wfGetMainCache();
+
$masterName = $this->parent->getServerName( 0 );
$memcKey = wfMemcKey( 'lag_times', $masterName );
$times = $wgMemc->get( $memcKey );
diff --git a/includes/diff/Diff.php b/includes/diff/Diff.php
new file mode 100644
index 00000000..538c2d83
--- /dev/null
+++ b/includes/diff/Diff.php
@@ -0,0 +1,580 @@
+<?php
+/* Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu>
+ *
+ * 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.
+ * or see http://www.gnu.org/
+ */
+
+/**
+ * This diff implementation is mainly lifted from the LCS algorithm of the Eclipse project which
+ * in turn is based on Myers' "An O(ND) difference algorithm and its variations"
+ * (http://citeseer.ist.psu.edu/myers86ond.html) with range compression (see Wu et al.'s
+ * "An O(NP) Sequence Comparison Algorithm").
+ *
+ * This implementation supports an upper bound on the excution time.
+ *
+ * Complexity: O((M + N)D) worst case time, O(M + N + D^2) expected time, O(M + N) space
+ *
+ * @author Guy Van den Broeck
+ * @ingroup DifferenceEngine
+ */
+class WikiDiff3 {
+
+ //Input variables
+ private $from;
+ private $to;
+ private $m;
+ private $n;
+
+ private $tooLong;
+ private $powLimit;
+
+ //State variables
+ private $maxDifferences;
+ private $lcsLengthCorrectedForHeuristic = false;
+
+ //Output variables
+ public $length;
+ public $removed;
+ public $added;
+ public $heuristicUsed;
+
+ function __construct($tooLong = 2000000, $powLimit = 1.45){
+ $this->tooLong = $tooLong;
+ $this->powLimit = $powLimit;
+ }
+
+ public function diff(/*array*/ $from, /*array*/ $to){
+ //remember initial lengths
+ $m = sizeof($from);
+ $n = count($to);
+
+ $this->heuristicUsed = false;
+
+ //output
+ $removed = $m > 0 ? array_fill(0, $m, true) : array();
+ $added = $n > 0 ? array_fill(0, $n, true) : array();
+
+ //reduce the complexity for the next step (intentionally done twice)
+ //remove common tokens at the start
+ $i = 0;
+ while($i < $m && $i < $n && $from[$i] === $to[$i]) {
+ $removed[$i] = $added[$i] = false;
+ unset($from[$i], $to[$i]);
+ ++$i;
+ }
+
+ //remove common tokens at the end
+ $j = 1;
+ while($i + $j <= $m && $i + $j <= $n && $from[$m - $j] === $to[$n - $j]) {
+ $removed[$m - $j] = $added[$n - $j] = false;
+ unset($from[$m - $j], $to[$n - $j]);
+ ++$j;
+ }
+
+ $this->from = $newFromIndex = $this->to = $newToIndex = array();
+
+ //remove tokens not in both sequences
+ $shared = array();
+ foreach( $from as $key ) {
+ $shared[$key] = false;
+ }
+
+ foreach($to as $index => &$el) {
+ if(array_key_exists($el, $shared)) {
+ //keep it
+ $this->to[] = $el;
+ $shared[$el] = true;
+ $newToIndex[] = $index;
+ }
+ }
+ foreach($from as $index => &$el) {
+ if($shared[$el]) {
+ //keep it
+ $this->from[] = $el;
+ $newFromIndex[] = $index;
+ }
+ }
+
+ unset($shared, $from, $to);
+
+ $this->m = count($this->from);
+ $this->n = count($this->to);
+
+ $this->removed = $this->m > 0 ? array_fill(0, $this->m, true) : array();
+ $this->added = $this->n > 0 ? array_fill(0, $this->n, true) : array();
+
+ if ($this->m == 0 || $this->n == 0) {
+ $this->length = 0;
+ } else {
+ $this->maxDifferences = ceil(($this->m + $this->n) / 2.0);
+ if ($this->m * $this->n > $this->tooLong) {
+ // limit complexity to D^POW_LIMIT for long sequences
+ $this->maxDifferences = floor(pow($this->maxDifferences, $this->powLimit - 1.0));
+ wfDebug("Limiting max number of differences to $this->maxDifferences\n");
+ }
+
+ /*
+ * The common prefixes and suffixes are always part of some LCS, include
+ * them now to reduce our search space
+ */
+ $max = min($this->m, $this->n);
+ for ($forwardBound = 0; $forwardBound < $max
+ && $this->from[$forwardBound] === $this->to[$forwardBound];
+ ++$forwardBound) {
+ $this->removed[$forwardBound] = $this->added[$forwardBound] = false;
+ }
+
+ $backBoundL1 = $this->m - 1;
+ $backBoundL2 = $this->n - 1;
+
+ while ($backBoundL1 >= $forwardBound && $backBoundL2 >= $forwardBound
+ && $this->from[$backBoundL1] === $this->to[$backBoundL2]) {
+ $this->removed[$backBoundL1--] = $this->added[$backBoundL2--] = false;
+ }
+
+ $temp = array_fill(0, $this->m + $this->n + 1, 0);
+ $V = array($temp, $temp);
+ $snake = array(0, 0, 0);
+
+ $this->length = $forwardBound + $this->m - $backBoundL1 - 1
+ + $this->lcs_rec($forwardBound, $backBoundL1,
+ $forwardBound, $backBoundL2, $V, $snake);
+ }
+
+ $this->m = $m;
+ $this->n = $n;
+
+ $this->length += $i + $j - 1;
+
+ foreach($this->removed as $key => &$removed_elem) {
+ if(!$removed_elem) {
+ $removed[$newFromIndex[$key]] = false;
+ }
+ }
+ foreach($this->added as $key => &$added_elem) {
+ if(!$added_elem) {
+ $added[$newToIndex[$key]] = false;
+ }
+ }
+ $this->removed = $removed;
+ $this->added = $added;
+ }
+
+ function diff_range($from_lines, $to_lines) {
+ // Diff and store locally
+ $this->diff($from_lines, $to_lines);
+ unset($from_lines, $to_lines);
+
+ $ranges = array();
+ $xi = $yi = 0;
+ while ($xi < $this->m || $yi < $this->n) {
+ // Matching "snake".
+ while ($xi < $this->m && $yi < $this->n
+ && !$this->removed[$xi]
+ && !$this->added[$yi]) {
+ ++$xi;
+ ++$yi;
+ }
+ // Find deletes & adds.
+ $xstart = $xi;
+ while ($xi < $this->m && $this->removed[$xi]) {
+ ++$xi;
+ }
+
+ $ystart = $yi;
+ while ($yi < $this->n && $this->added[$yi]) {
+ ++$yi;
+ }
+
+ if ($xi > $xstart || $yi > $ystart) {
+ $ranges[] = new RangeDifference($xstart, $xi,
+ $ystart, $yi);
+ }
+ }
+ return $ranges;
+ }
+
+ private function lcs_rec($bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake) {
+ // check that both sequences are non-empty
+ if ($bottoml1 > $topl1 || $bottoml2 > $topl2) {
+ return 0;
+ }
+
+ $d = $this->find_middle_snake($bottoml1, $topl1, $bottoml2,
+ $topl2, $V, $snake);
+
+ // need to store these so we don't lose them when they're
+ // overwritten by the recursion
+ $len = $snake[2];
+ $startx = $snake[0];
+ $starty = $snake[1];
+
+ // the middle snake is part of the LCS, store it
+ for ($i = 0; $i < $len; ++$i) {
+ $this->removed[$startx + $i] = $this->added[$starty + $i] = false;
+ }
+
+ if ($d > 1) {
+ return $len
+ + $this->lcs_rec($bottoml1, $startx - 1, $bottoml2,
+ $starty - 1, $V, $snake)
+ + $this->lcs_rec($startx + $len, $topl1, $starty + $len,
+ $topl2, $V, $snake);
+ } else if ($d == 1) {
+ /*
+ * In this case the sequences differ by exactly 1 line. We have
+ * already saved all the lines after the difference in the for loop
+ * above, now we need to save all the lines before the difference.
+ */
+ $max = min($startx - $bottoml1, $starty - $bottoml2);
+ for ($i = 0; $i < $max; ++$i) {
+ $this->removed[$bottoml1 + $i] =
+ $this->added[$bottoml2 + $i] = false;
+ }
+ return $max + $len;
+ }
+ return $len;
+ }
+
+ private function find_middle_snake($bottoml1, $topl1, $bottoml2,$topl2, &$V, &$snake) {
+ $from = &$this->from;
+ $to = &$this->to;
+ $V0 = &$V[0];
+ $V1 = &$V[1];
+ $snake0 = &$snake[0];
+ $snake1 = &$snake[1];
+ $snake2 = &$snake[2];
+ $bottoml1_min_1 = $bottoml1-1;
+ $bottoml2_min_1 = $bottoml2-1;
+ $N = $topl1 - $bottoml1_min_1;
+ $M = $topl2 - $bottoml2_min_1;
+ $delta = $N - $M;
+ $maxabsx = $N+$bottoml1;
+ $maxabsy = $M+$bottoml2;
+ $limit = min($this->maxDifferences, ceil(($N + $M ) / 2));
+
+ //value_to_add_forward: a 0 or 1 that we add to the start
+ // offset to make it odd/even
+ if (($M & 1) == 1) {
+ $value_to_add_forward = 1;
+ } else {
+ $value_to_add_forward = 0;
+ }
+
+ if (($N & 1) == 1) {
+ $value_to_add_backward = 1;
+ } else {
+ $value_to_add_backward = 0;
+ }
+
+ $start_forward = -$M;
+ $end_forward = $N;
+ $start_backward = -$N;
+ $end_backward = $M;
+
+ $limit_min_1 = $limit - 1;
+ $limit_plus_1 = $limit + 1;
+
+ $V0[$limit_plus_1] = 0;
+ $V1[$limit_min_1] = $N;
+ $limit = min($this->maxDifferences, ceil(($N + $M ) / 2));
+
+ if (($delta & 1) == 1) {
+ for ($d = 0; $d <= $limit; ++$d) {
+ $start_diag = max($value_to_add_forward + $start_forward, -$d);
+ $end_diag = min($end_forward, $d);
+ $value_to_add_forward = 1 - $value_to_add_forward;
+
+ // compute forward furthest reaching paths
+ for ($k = $start_diag; $k <= $end_diag; $k += 2) {
+ if ($k == -$d || ($k < $d
+ && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k])) {
+ $x = $V0[$limit_plus_1 + $k];
+ } else {
+ $x = $V0[$limit_min_1 + $k] + 1;
+ }
+
+ $absx = $snake0 = $x + $bottoml1;
+ $absy = $snake1 = $x - $k + $bottoml2;
+
+ while ($absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy]) {
+ ++$absx;
+ ++$absy;
+ }
+ $x = $absx-$bottoml1;
+
+ $snake2 = $absx -$snake0;
+ $V0[$limit + $k] = $x;
+ if ($k >= $delta - $d + 1 && $k <= $delta + $d - 1
+ && $x >= $V1[$limit + $k - $delta]) {
+ return 2 * $d - 1;
+ }
+
+ // check to see if we can cut down the diagonal range
+ if ($x >= $N && $end_forward > $k - 1) {
+ $end_forward = $k - 1;
+ } else if ($absy - $bottoml2 >= $M) {
+ $start_forward = $k + 1;
+ $value_to_add_forward = 0;
+ }
+ }
+
+ $start_diag = max($value_to_add_backward + $start_backward, -$d);
+ $end_diag = min($end_backward, $d);
+ $value_to_add_backward = 1 - $value_to_add_backward;
+
+ // compute backward furthest reaching paths
+ for ($k = $start_diag; $k <= $end_diag; $k += 2) {
+ if ($k == $d
+ || ($k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k])) {
+ $x = $V1[$limit_min_1 + $k];
+ } else {
+ $x = $V1[$limit_plus_1 + $k] - 1;
+ }
+
+ $y = $x - $k - $delta;
+
+ $snake2 = 0;
+ while ($x > 0 && $y > 0
+ && $from[$x +$bottoml1_min_1] === $to[$y + $bottoml2_min_1]) {
+ --$x;
+ --$y;
+ ++$snake2;
+ }
+ $V1[$limit + $k] = $x;
+
+ // check to see if we can cut down our diagonal range
+ if ($x <= 0) {
+ $start_backward = $k + 1;
+ $value_to_add_backward = 0;
+ } else if ($y <= 0 && $end_backward > $k - 1) {
+ $end_backward = $k - 1;
+ }
+ }
+ }
+ } else {
+ for ($d = 0; $d <= $limit; ++$d) {
+ $start_diag = max($value_to_add_forward + $start_forward, -$d);
+ $end_diag = min($end_forward, $d);
+ $value_to_add_forward = 1 - $value_to_add_forward;
+
+ // compute forward furthest reaching paths
+ for ($k = $start_diag; $k <= $end_diag; $k += 2) {
+ if ($k == -$d
+ || ($k < $d && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k])) {
+ $x = $V0[$limit_plus_1 + $k];
+ } else {
+ $x = $V0[$limit_min_1 + $k] + 1;
+ }
+
+ $absx = $snake0 = $x + $bottoml1;
+ $absy = $snake1 = $x - $k + $bottoml2;
+
+ while ($absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy]) {
+ ++$absx;
+ ++$absy;
+ }
+ $x = $absx-$bottoml1;
+ $snake2 = $absx -$snake0;
+ $V0[$limit + $k] = $x;
+
+ // check to see if we can cut down the diagonal range
+ if ($x >= $N && $end_forward > $k - 1) {
+ $end_forward = $k - 1;
+ } else if ($absy-$bottoml2 >= $M) {
+ $start_forward = $k + 1;
+ $value_to_add_forward = 0;
+ }
+ }
+
+ $start_diag = max($value_to_add_backward + $start_backward, -$d);
+ $end_diag = min($end_backward, $d);
+ $value_to_add_backward = 1 - $value_to_add_backward;
+
+ // compute backward furthest reaching paths
+ for ($k = $start_diag; $k <= $end_diag; $k += 2) {
+ if ($k == $d
+ || ($k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k])) {
+ $x = $V1[$limit_min_1 + $k];
+ } else {
+ $x = $V1[$limit_plus_1 + $k] - 1;
+ }
+
+ $y = $x - $k - $delta;
+
+ $snake2 = 0;
+ while ($x > 0 && $y > 0
+ && $from[$x +$bottoml1_min_1] === $to[$y + $bottoml2_min_1]) {
+ --$x;
+ --$y;
+ ++$snake2;
+ }
+ $V1[$limit + $k] = $x;
+
+ if ($k >= -$delta - $d && $k <= $d - $delta
+ && $x <= $V0[$limit + $k + $delta]) {
+ $snake0 = $bottoml1 + $x;
+ $snake1 = $bottoml2 + $y;
+ return 2 * $d;
+ }
+
+ // check to see if we can cut down our diagonal range
+ if ($x <= 0) {
+ $start_backward = $k + 1;
+ $value_to_add_backward = 0;
+ } else if ($y <= 0 && $end_backward > $k - 1) {
+ $end_backward = $k - 1;
+ }
+ }
+ }
+ }
+ /*
+ * computing the true LCS is too expensive, instead find the diagonal
+ * with the most progress and pretend a midle snake of length 0 occurs
+ * there.
+ */
+
+ $most_progress = self::findMostProgress($M, $N, $limit, $V);
+
+ $snake0 = $bottoml1 + $most_progress[0];
+ $snake1 = $bottoml2 + $most_progress[1];
+ $snake2 = 0;
+ wfDebug("Computing the LCS is too expensive. Using a heuristic.\n");
+ $this->heuristicUsed = true;
+ return 5; /*
+ * HACK: since we didn't really finish the LCS computation
+ * we don't really know the length of the SES. We don't do
+ * anything with the result anyway, unless it's <=1. We know
+ * for a fact SES > 1 so 5 is as good a number as any to
+ * return here
+ */
+ }
+
+ private static function findMostProgress($M, $N, $limit, $V) {
+ $delta = $N - $M;
+
+ if (($M & 1) == ($limit & 1)) {
+ $forward_start_diag = max(-$M, -$limit);
+ } else {
+ $forward_start_diag = max(1 - $M, -$limit);
+ }
+
+ $forward_end_diag = min($N, $limit);
+
+ if (($N & 1) == ($limit & 1)) {
+ $backward_start_diag = max(-$N, -$limit);
+ } else {
+ $backward_start_diag = max(1 - $N, -$limit);
+ }
+
+ $backward_end_diag = -min($M, $limit);
+
+ $temp = array(0, 0, 0);
+
+
+ $max_progress = array_fill(0, ceil(max($forward_end_diag - $forward_start_diag,
+ $backward_end_diag - $backward_start_diag) / 2), $temp);
+ $num_progress = 0; // the 1st entry is current, it is initialized
+ // with 0s
+
+ // first search the forward diagonals
+ for ($k = $forward_start_diag; $k <= $forward_end_diag; $k += 2) {
+ $x = $V[0][$limit + $k];
+ $y = $x - $k;
+ if ($x > $N || $y > $M) {
+ continue;
+ }
+
+ $progress = $x + $y;
+ if ($progress > $max_progress[0][2]) {
+ $num_progress = 0;
+ $max_progress[0][0] = $x;
+ $max_progress[0][1] = $y;
+ $max_progress[0][2] = $progress;
+ } else if ($progress == $max_progress[0][2]) {
+ ++$num_progress;
+ $max_progress[$num_progress][0] = $x;
+ $max_progress[$num_progress][1] = $y;
+ $max_progress[$num_progress][2] = $progress;
+ }
+ }
+
+ $max_progress_forward = true; // initially the maximum
+ // progress is in the forward
+ // direction
+
+ // now search the backward diagonals
+ for ($k = $backward_start_diag; $k <= $backward_end_diag; $k += 2) {
+ $x = $V[1][$limit + $k];
+ $y = $x - $k - $delta;
+ if ($x < 0 || $y < 0) {
+ continue;
+ }
+
+ $progress = $N - $x + $M - $y;
+ if ($progress > $max_progress[0][2]) {
+ $num_progress = 0;
+ $max_progress_forward = false;
+ $max_progress[0][0] = $x;
+ $max_progress[0][1] = $y;
+ $max_progress[0][2] = $progress;
+ } else if ($progress == $max_progress[0][2] && !$max_progress_forward) {
+ ++$num_progress;
+ $max_progress[$num_progress][0] = $x;
+ $max_progress[$num_progress][1] = $y;
+ $max_progress[$num_progress][2] = $progress;
+ }
+ }
+
+ // return the middle diagonal with maximal progress.
+ return $max_progress[floor($num_progress / 2)];
+ }
+
+ public function getLcsLength(){
+ if($this->heuristicUsed && !$this->lcsLengthCorrectedForHeuristic){
+ $this->lcsLengthCorrectedForHeuristic = true;
+ $this->length = $this->m-array_sum($this->added);
+ }
+ return $this->length;
+ }
+
+}
+
+/**
+ * Alternative representation of a set of changes, by the index
+ * ranges that are changed.
+ *
+ * @ingroup DifferenceEngine
+ */
+class RangeDifference {
+
+ public $leftstart;
+ public $leftend;
+ public $leftlength;
+
+ public $rightstart;
+ public $rightend;
+ public $rightlength;
+
+ function __construct($leftstart, $leftend, $rightstart, $rightend){
+ $this->leftstart = $leftstart;
+ $this->leftend = $leftend;
+ $this->leftlength = $leftend - $leftstart;
+ $this->rightstart = $rightstart;
+ $this->rightend = $rightend;
+ $this->rightlength = $rightend - $rightstart;
+ }
+}
diff --git a/includes/DifferenceEngine.php b/includes/diff/DifferenceEngine.php
index 0b4028cb..b30ff190 100644
--- a/includes/DifferenceEngine.php
+++ b/includes/diff/DifferenceEngine.php
@@ -27,6 +27,7 @@ class DifferenceEngine {
var $mOldRev, $mNewRev;
var $mRevisionsLoaded = false; // Have the revisions been loaded
var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
+ var $htmldiff;
/**#@-*/
/**
@@ -36,8 +37,9 @@ class DifferenceEngine {
* @param $new String: either 'prev' or 'next'.
* @param $rcid Integer: ??? FIXME (default 0)
* @param $refreshCache boolean If set, refreshes the diff cache
+ * @param $htmldiff boolean If set, output using HTMLDiff instead of raw wikicode diff
*/
- function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false ) {
+ function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false , $htmldiff = false) {
$this->mTitle = $titleObj;
wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
@@ -67,6 +69,7 @@ class DifferenceEngine {
}
$this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer
$this->mRefreshCache = $refreshCache;
+ $this->htmldiff = $htmldiff;
}
function getTitle() {
@@ -74,10 +77,11 @@ class DifferenceEngine {
}
function showDiffPage( $diffOnly = false ) {
- global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol;
+ global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol, $wgEnableHtmlDiff;
wfProfileIn( __METHOD__ );
- # If external diffs are enabled both globally and for the user,
+
+ # If external diffs are enabled both globally and for the user,
# we'll use the application/x-external-editor interface to call
# an external diff tool like kompare, kdiff3, etc.
if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
@@ -88,19 +92,19 @@ class DifferenceEngine {
$url2=$this->mTitle->getFullURL("action=raw&oldid=".$this->mNewid);
$special=$wgLang->getNsText(NS_SPECIAL);
$control=<<<CONTROL
-[Process]
-Type=Diff text
-Engine=MediaWiki
-Script={$wgServer}{$wgScript}
-Special namespace={$special}
-
-[File]
-Extension=wiki
-URL=$url1
-
-[File 2]
-Extension=wiki
-URL=$url2
+ [Process]
+ Type=Diff text
+ Engine=MediaWiki
+ Script={$wgServer}{$wgScript}
+ Special namespace={$special}
+
+ [File]
+ Extension=wiki
+ URL=$url1
+
+ [File 2]
+ Extension=wiki
+ URL=$url2
CONTROL;
echo($control);
return;
@@ -141,14 +145,15 @@ CONTROL;
} else {
$wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
}
- $wgOut->setSubtitle( wfMsg( 'difference' ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
- if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) {
+ if ( !$this->mOldPage->userCanRead() || !$this->mNewPage->userCanRead() ) {
$wgOut->loginToUse();
$wgOut->output();
+ $wgOut->disable();
wfProfileOut( __METHOD__ );
- exit;
+ return;
}
$sk = $wgUser->getSkin();
@@ -162,7 +167,7 @@ CONTROL;
}
// Prepare a change patrol link, if applicable
- if( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) {
+ if( $wgUseRCPatrol && $this->mTitle->userCan('patrol') ) {
// If we've been given an explicit change identifier, use it; saves time
if( $this->mRcidMarkPatrolled ) {
$rcid = $this->mRcidMarkPatrolled;
@@ -170,15 +175,15 @@ CONTROL;
// Look for an unpatrolled change corresponding to this diff
$db = wfGetDB( DB_SLAVE );
$change = RecentChange::newFromConds(
- array(
- // Add redundant user,timestamp condition so we can use the existing index
- 'rc_user_text' => $this->mNewRev->getRawUserText(),
- 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
+ array(
+ // Add redundant user,timestamp condition so we can use the existing index
+ 'rc_user_text' => $this->mNewRev->getUserText( Revision::FOR_THIS_USER ),
+ 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
'rc_this_oldid' => $this->mNewid,
'rc_last_oldid' => $this->mOldid,
- 'rc_patrolled' => 0
- ),
- __METHOD__
+ 'rc_patrolled' => 0
+ ),
+ __METHOD__
);
if( $change instanceof RecentChange ) {
$rcid = $change->mAttribs['rc_id'];
@@ -192,8 +197,8 @@ CONTROL;
$patrol = ' <span class="patrollink">[' . $sk->makeKnownLinkObj(
$this->mTitle,
wfMsgHtml( 'markaspatrolleddiff' ),
- "action=markpatrolled&rcid={$rcid}"
- ) . ']</span>';
+ "action=markpatrolled&rcid={$rcid}"
+ ) . ']</span>';
} else {
$patrol = '';
}
@@ -201,57 +206,58 @@ CONTROL;
$patrol = '';
}
+ $htmldiffarg = $this->htmlDiffArgument();
$prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ),
- 'diff=prev&oldid='.$this->mOldid, '', '', 'id="differences-prevlink"' );
+ 'diff=prev&oldid='.$this->mOldid.$htmldiffarg, '', '', 'id="differences-prevlink"' );
if ( $this->mNewRev->isCurrent() ) {
$nextlink = '&nbsp;';
} else {
$nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ),
- 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' );
+ 'diff=next&oldid='.$this->mNewid.$htmldiffarg, '', '', 'id="differences-nextlink"' );
}
$oldminor = '';
$newminor = '';
if ($this->mOldRev->mMinorEdit == 1) {
- $oldminor = Xml::span( wfMsg( 'minoreditletter'), 'minor' ) . ' ';
+ $oldminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' ';
}
if ($this->mNewRev->mMinorEdit == 1) {
- $newminor = Xml::span( wfMsg( 'minoreditletter'), 'minor' ) . ' ';
+ $newminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' ';
}
$rdel = ''; $ldel = '';
if( $wgUser->isAllowed( 'deleterevision' ) ) {
$revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) {
- // If revision was hidden from sysops
- $ldel = wfMsgHtml('rev-delundel');
+ // If revision was hidden from sysops
+ $ldel = wfMsgHtml( 'rev-delundel' );
} else {
$ldel = $sk->makeKnownLinkObj( $revdel,
- wfMsgHtml('rev-delundel'),
+ wfMsgHtml( 'rev-delundel' ),
'target=' . urlencode( $this->mOldRev->mTitle->getPrefixedDbkey() ) .
'&oldid=' . urlencode( $this->mOldRev->getId() ) );
// Bolden oversighted content
if( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) )
- $ldel = "<strong>$ldel</strong>";
+ $ldel = "<strong>$ldel</strong>";
}
$ldel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$ldel</small>)</tt> ";
// We don't currently handle well changing the top revision's settings
if( $this->mNewRev->isCurrent() ) {
- // If revision was hidden from sysops
- $rdel = wfMsgHtml('rev-delundel');
+ // If revision was hidden from sysops
+ $rdel = wfMsgHtml( 'rev-delundel' );
} else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) {
- // If revision was hidden from sysops
- $rdel = wfMsgHtml('rev-delundel');
+ // If revision was hidden from sysops
+ $rdel = wfMsgHtml( 'rev-delundel' );
} else {
$rdel = $sk->makeKnownLinkObj( $revdel,
- wfMsgHtml('rev-delundel'),
+ wfMsgHtml( 'rev-delundel' ),
'target=' . urlencode( $this->mNewRev->mTitle->getPrefixedDbkey() ) .
'&oldid=' . urlencode( $this->mNewRev->getId() ) );
// Bolden oversighted content
if( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) )
- $rdel = "<strong>$rdel</strong>";
+ $rdel = "<strong>$rdel</strong>";
}
$rdel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$rdel</small>)</tt> ";
}
@@ -265,11 +271,22 @@ CONTROL;
'<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, true ) . $rdel . "</div>" .
'<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
- $this->showDiff( $oldHeader, $newHeader );
-
- if ( !$diffOnly )
- $this->renderNewRevision();
-
+ if( $wgEnableHtmlDiff && $this->htmldiff) {
+ $multi = $this->getMultiNotice();
+ $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'wikicodecomparison' ),
+ 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=0', '', '', 'id="differences-switchtype"' ).'</div>');
+ $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
+ $this->renderHtmlDiff();
+ } else {
+ if($wgEnableHtmlDiff){
+ $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'visualcomparison' ),
+ 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=1', '', '', 'id="differences-switchtype"' ).'</div>');
+ }
+ $this->showDiff( $oldHeader, $newHeader );
+ if( !$diffOnly ) {
+ $this->renderNewRevision();
+ }
+ }
wfProfileOut( __METHOD__ );
}
@@ -283,9 +300,9 @@ CONTROL;
$wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
#add deleted rev tag if needed
if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
- $wgOut->addWikiMsg( 'rev-deleted-text-permission' );
+ $wgOut->addWikiMsg( 'rev-deleted-text-permission' );
} else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
- $wgOut->addWikiMsg( 'rev-deleted-text-view' );
+ $wgOut->addWikiMsg( 'rev-deleted-text-view' );
}
if( !$this->mNewRev->isCurrent() ) {
@@ -305,12 +322,12 @@ CONTROL;
// 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->mNewtext ) );
- $wgOut->addHtml( "\n</pre>\n" );
+ $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
+ $wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
+ $wgOut->addHTML( "\n</pre>\n" );
}
} else
- $wgOut->addWikiTextTidy( $this->mNewtext );
+ $wgOut->addWikiTextTidy( $this->mNewtext );
if( !$this->mNewRev->isCurrent() ) {
$wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
@@ -319,6 +336,70 @@ CONTROL;
wfProfileOut( __METHOD__ );
}
+
+ function renderHtmlDiff() {
+ global $wgOut, $wgTitle, $wgParser, $wgDebugComments;
+ wfProfileIn( __METHOD__ );
+
+ $this->showDiffStyle();
+
+ $wgOut->addHTML( '<h2>'.wfMsgHtml( 'visual-comparison' )."</h2>\n" );
+ #add deleted rev tag if needed
+ if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
+ $wgOut->addWikiMsg( 'rev-deleted-text-permission' );
+ } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
+ $wgOut->addWikiMsg( 'rev-deleted-text-view' );
+ }
+
+ if( !$this->mNewRev->isCurrent() ) {
+ $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
+ }
+
+ $this->loadText();
+
+ // Old revision
+ if( is_object( $this->mOldRev ) ) {
+ $wgOut->setRevisionId( $this->mOldRev->getId() );
+ }
+
+ $popts = $wgOut->parserOptions();
+ $oldTidy = $popts->setTidy( true );
+ $popts->setEditSection( false );
+
+ $parserOutput = $wgParser->parse( $this->mOldtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
+ $popts->setTidy( $oldTidy );
+
+ //only for new?
+ //$wgOut->addParserOutputNoText( $parserOutput );
+ $oldHtml = $parserOutput->getText();
+ wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$oldHtml ) );
+
+ // New revision
+ if( is_object( $this->mNewRev ) ) {
+ $wgOut->setRevisionId( $this->mNewRev->getId() );
+ }
+
+ $popts = $wgOut->parserOptions();
+ $oldTidy = $popts->setTidy( true );
+
+ $parserOutput = $wgParser->parse( $this->mNewtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
+ $popts->setTidy( $oldTidy );
+
+ $wgOut->addParserOutputNoText( $parserOutput );
+ $newHtml = $parserOutput->getText();
+ wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$newHtml ) );
+
+ unset($parserOutput, $popts);
+
+ $differ = new HTMLDiffer(new DelegatingContentHandler($wgOut));
+ $differ->htmlDiff($oldHtml, $newHtml);
+ if ( $wgDebugComments ) {
+ $wgOut->addHTML( "\n<!-- HtmlDiff Debug Output:\n" . HTMLDiffer::getDebugOutput() . " End Debug -->" );
+ }
+
+ wfProfileOut( __METHOD__ );
+ }
+
/**
* Show the first revision of an article. Uses normal diff headers in
* contrast to normal "old revision" display style.
@@ -343,31 +424,44 @@ CONTROL;
# Check if user is allowed to look at this page. If not, bail out.
#
- if ( !( $this->mTitle->userCanRead() ) ) {
+ if ( !$this->mTitle->userCanRead() ) {
$wgOut->loginToUse();
$wgOut->output();
wfProfileOut( __METHOD__ );
- exit;
+ throw new MWException("Permission Error: you do not have access to view this page");
}
# Prepare the header box
#
$sk = $wgUser->getSkin();
- $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' );
+ $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid.$this->htmlDiffArgument(), '', '', 'id="differences-nextlink"' );
$header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />" .
- $sk->revUserTools( $this->mNewRev ) . "<br />" .
- $sk->revComment( $this->mNewRev ) . "<br />" .
- $nextlink . "</div>\n";
+ $sk->revUserTools( $this->mNewRev ) . "<br />" .
+ $sk->revComment( $this->mNewRev ) . "<br />" .
+ $nextlink . "</div>\n";
$wgOut->addHTML( $header );
- $wgOut->setSubtitle( wfMsg( 'difference' ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
wfProfileOut( __METHOD__ );
}
+ function htmlDiffArgument(){
+ global $wgEnableHtmlDiff;
+ if($wgEnableHtmlDiff){
+ if($this->htmldiff){
+ return '&htmldiff=1';
+ }else{
+ return '&htmldiff=0';
+ }
+ }else{
+ return '';
+ }
+ }
+
/**
* Get the diff text, send it to $wgOut
* Returns false if the diff could not be generated, otherwise returns true
@@ -423,9 +517,9 @@ CONTROL;
wfProfileIn( __METHOD__ );
// Check if the diff should be hidden from this user
if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
- return '';
+ return '';
} else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
- return '';
+ return '';
}
// Cacheable?
$key = false;
@@ -453,7 +547,9 @@ CONTROL;
$difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
// Save to cache for 7 days
- if ( $key !== false && $difftext !== false ) {
+ if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
+ wfIncrStats( 'diff_uncacheable' );
+ } else if ( $key !== false && $difftext !== false ) {
wfIncrStats( 'diff_cache_miss' );
$wgMemc->set( $key, $difftext, 7*86400 );
} else {
@@ -486,7 +582,7 @@ CONTROL;
dl('php_wikidiff.so');
}
return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
- $this->debug( 'wikidiff1' );
+ $this->debug( 'wikidiff1' );
}
if ( $wgExternalDiffEngine == 'wikidiff2' ) {
@@ -505,7 +601,7 @@ CONTROL;
return $text;
}
}
- if ( $wgExternalDiffEngine !== false ) {
+ if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
# Diff via the shell
global $wgTmpDirectory;
$tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
@@ -541,25 +637,25 @@ CONTROL;
$diffs = new Diff( $ota, $nta );
$formatter = new TableDiffFormatter();
return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
- $this->debug();
+ $this->debug();
}
-
+
/**
* Generate a debug comment indicating diff generating time,
* server node, and generator backend.
*/
protected function debug( $generator="internal" ) {
- global $wgShowHostnames, $wgNodeName;
+ global $wgShowHostnames;
$data = array( $generator );
if( $wgShowHostnames ) {
- $data[] = $wgNodeName;
+ $data[] = wfHostname();
}
$data[] = wfTimestamp( TS_DB );
return "<!-- diff generator: " .
- implode( " ",
- array_map(
+ implode( " ",
+ array_map(
"htmlspecialchars",
- $data ) ) .
+ $data ) ) .
" -->\n";
}
@@ -568,12 +664,12 @@ CONTROL;
*/
function localiseLineNumbers( $text ) {
return preg_replace_callback( '/<!--LINE (\d+)-->/',
- array( &$this, 'localiseLineNumbersCb' ), $text );
+ array( &$this, 'localiseLineNumbersCb' ), $text );
}
function localiseLineNumbersCb( $matches ) {
global $wgLang;
- return wfMsgExt( 'lineno', array('parseinline'), $wgLang->formatNum( $matches[1] ) );
+ return wfMsgExt( 'lineno', array( 'parseinline' ), $wgLang->formatNum( $matches[1] ) );
}
@@ -582,7 +678,7 @@ CONTROL;
*/
function getMultiNotice() {
if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
- return '';
+ return '';
if( !$this->mOldPage->equals( $this->mNewPage ) ) {
// Comparing two different pages? Count would be meaningless.
@@ -597,7 +693,7 @@ CONTROL;
$n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
if ( !$n )
- return '';
+ return '';
return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n );
}
@@ -607,22 +703,20 @@ CONTROL;
* Add the header to a diff body
*/
static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
- global $wgOut;
-
$header = "
- <table class='diff'>
- <col class='diff-marker' />
- <col class='diff-content' />
- <col class='diff-marker' />
- <col class='diff-content' />
- <tr valign='top'>
- <td colspan='2' class='diff-otitle'>{$otitle}</td>
- <td colspan='2' class='diff-ntitle'>{$ntitle}</td>
- </tr>
+ <table class='diff'>
+ <col class='diff-marker' />
+ <col class='diff-content' />
+ <col class='diff-marker' />
+ <col class='diff-content' />
+ <tr valign='top'>
+ <td colspan='2' class='diff-otitle'>{$otitle}</td>
+ <td colspan='2' class='diff-ntitle'>{$ntitle}</td>
+ </tr>
";
if ( $multi != '' )
- $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
+ $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
return $header . $diff . "</table>";
}
@@ -647,7 +741,7 @@ CONTROL;
* API convenience.
*/
function loadRevisionData() {
- global $wgLang;
+ global $wgLang, $wgUser;
if ( $this->mRevisionsLoaded ) {
return true;
} else {
@@ -657,10 +751,10 @@ CONTROL;
// Load the new revision object
$this->mNewRev = $this->mNewid
- ? Revision::newFromId( $this->mNewid )
- : Revision::newFromTitle( $this->mTitle );
+ ? Revision::newFromId( $this->mNewid )
+ : Revision::newFromTitle( $this->mTitle );
if( !$this->mNewRev instanceof Revision )
- return false;
+ return false;
// Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
$this->mNewid = $this->mNewRev->getId();
@@ -673,10 +767,10 @@ CONTROL;
$this->mNewPage = $this->mNewRev->getTitle();
if( $this->mNewRev->isCurrent() ) {
$newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
- $this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) );
+ $this->mPagetitle = wfMsgHTML( 'currentrev-asof', $timestamp );
$newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' );
- $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)";
+ $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
$this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
} else {
@@ -688,9 +782,9 @@ CONTROL;
$this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
}
if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
- $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
+ $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
} else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
- $this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>';
+ $this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>';
}
// Load the old revision object
@@ -722,17 +816,19 @@ CONTROL;
$this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) );
$this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
- . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
+ . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
// Add an "undo" link
$newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
+ $htmlLink = htmlspecialchars( wfMsg( 'editundo' ) );
+ $htmlTitle = $wgUser->getSkin()->tooltip( 'undo' );
if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
- $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)";
+ $this->mNewtitle .= " (<a href='$newUndo' $htmlTitle>" . $htmlLink . "</a>)";
}
if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) {
- $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>';
+ $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>';
} else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
- $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>';
+ $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>';
}
}
@@ -754,13 +850,13 @@ CONTROL;
return false;
}
if ( $this->mOldRev ) {
- $this->mOldtext = $this->mOldRev->revText();
+ $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
if ( $this->mOldtext === false ) {
return false;
}
}
if ( $this->mNewRev ) {
- $this->mNewtext = $this->mNewRev->revText();
+ $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
if ( $this->mNewtext === false ) {
return false;
}
@@ -828,7 +924,7 @@ class _DiffOp_Copy extends _DiffOp {
function _DiffOp_Copy ($orig, $closing = false) {
if (!is_array($closing))
- $closing = $orig;
+ $closing = $orig;
$this->orig = $orig;
$this->closing = $closing;
}
@@ -892,7 +988,6 @@ class _DiffOp_Change extends _DiffOp {
}
}
-
/**
* Class used internally by Diff to actually compute the diffs.
*
@@ -911,70 +1006,30 @@ class _DiffOp_Change extends _DiffOp {
* are my own.
*
* Line length limits for robustness added by Tim Starling, 2005-08-31
+ * Alternative implementation added by Guy Van den Broeck, 2008-07-30
*
- * @author Geoffrey T. Dairiki, Tim Starling
+ * @author Geoffrey T. Dairiki, Tim Starling, Guy Van den Broeck
* @private
* @ingroup DifferenceEngine
*/
class _DiffEngine {
+
const MAX_XREF_LENGTH = 10000;
- function diff ($from_lines, $to_lines) {
+ function diff ($from_lines, $to_lines){
wfProfileIn( __METHOD__ );
- $n_from = sizeof($from_lines);
- $n_to = sizeof($to_lines);
-
- $this->xchanged = $this->ychanged = array();
- $this->xv = $this->yv = array();
- $this->xind = $this->yind = array();
- unset($this->seq);
- unset($this->in_seq);
- unset($this->lcs);
-
- // Skip leading common lines.
- for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
- if ($from_lines[$skip] !== $to_lines[$skip])
- break;
- $this->xchanged[$skip] = $this->ychanged[$skip] = false;
- }
- // Skip trailing common lines.
- $xi = $n_from; $yi = $n_to;
- for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
- if ($from_lines[$xi] !== $to_lines[$yi])
- break;
- $this->xchanged[$xi] = $this->ychanged[$yi] = false;
- }
-
- // Ignore lines which do not exist in both files.
- for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
- $xhash[$this->_line_hash($from_lines[$xi])] = 1;
- }
-
- for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
- $line = $to_lines[$yi];
- if ( ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) )
- continue;
- $yhash[$this->_line_hash($line)] = 1;
- $this->yv[] = $line;
- $this->yind[] = $yi;
- }
- for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
- $line = $from_lines[$xi];
- if ( ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) )
- continue;
- $this->xv[] = $line;
- $this->xind[] = $xi;
- }
-
- // Find the LCS.
- $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+ // Diff and store locally
+ $this->diff_local($from_lines, $to_lines);
// Merge edits when possible
$this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
$this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
// Compute the edit operations.
+ $n_from = sizeof($from_lines);
+ $n_to = sizeof($to_lines);
+
$edits = array();
$xi = $yi = 0;
while ($xi < $n_from || $yi < $n_to) {
@@ -984,33 +1039,96 @@ class _DiffEngine {
// Skip matching "snake".
$copy = array();
while ( $xi < $n_from && $yi < $n_to
- && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+ && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
$copy[] = $from_lines[$xi++];
++$yi;
}
if ($copy)
- $edits[] = new _DiffOp_Copy($copy);
+ $edits[] = new _DiffOp_Copy($copy);
// Find deletes & adds.
$delete = array();
while ($xi < $n_from && $this->xchanged[$xi])
- $delete[] = $from_lines[$xi++];
+ $delete[] = $from_lines[$xi++];
$add = array();
while ($yi < $n_to && $this->ychanged[$yi])
- $add[] = $to_lines[$yi++];
+ $add[] = $to_lines[$yi++];
if ($delete && $add)
- $edits[] = new _DiffOp_Change($delete, $add);
+ $edits[] = new _DiffOp_Change($delete, $add);
elseif ($delete)
- $edits[] = new _DiffOp_Delete($delete);
+ $edits[] = new _DiffOp_Delete($delete);
elseif ($add)
- $edits[] = new _DiffOp_Add($add);
+ $edits[] = new _DiffOp_Add($add);
}
wfProfileOut( __METHOD__ );
return $edits;
}
+ function diff_local ($from_lines, $to_lines) {
+ global $wgExternalDiffEngine;
+ wfProfileIn( __METHOD__);
+
+ if($wgExternalDiffEngine == 'wikidiff3'){
+ // wikidiff3
+ $wikidiff3 = new WikiDiff3();
+ $wikidiff3->diff($from_lines, $to_lines);
+ $this->xchanged = $wikidiff3->removed;
+ $this->ychanged = $wikidiff3->added;
+ unset($wikidiff3);
+ }else{
+ // old diff
+ $n_from = sizeof($from_lines);
+ $n_to = sizeof($to_lines);
+ $this->xchanged = $this->ychanged = array();
+ $this->xv = $this->yv = array();
+ $this->xind = $this->yind = array();
+ unset($this->seq);
+ unset($this->in_seq);
+ unset($this->lcs);
+
+ // Skip leading common lines.
+ for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+ if ($from_lines[$skip] !== $to_lines[$skip])
+ break;
+ $this->xchanged[$skip] = $this->ychanged[$skip] = false;
+ }
+ // Skip trailing common lines.
+ $xi = $n_from; $yi = $n_to;
+ for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+ if ($from_lines[$xi] !== $to_lines[$yi])
+ break;
+ $this->xchanged[$xi] = $this->ychanged[$yi] = false;
+ }
+
+ // Ignore lines which do not exist in both files.
+ for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+ $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+ }
+
+ for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+ $line = $to_lines[$yi];
+ if ( ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) )
+ continue;
+ $yhash[$this->_line_hash($line)] = 1;
+ $this->yv[] = $line;
+ $this->yind[] = $yi;
+ }
+ for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+ $line = $from_lines[$xi];
+ if ( ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) )
+ continue;
+ $this->xv[] = $line;
+ $this->xind[] = $xi;
+ }
+
+ // Find the LCS.
+ $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+ }
+ wfProfileOut( __METHOD__ );
+ }
+
/**
* Returns the whole line if it's small enough, or the MD5 hash otherwise
*/
@@ -1022,7 +1140,6 @@ class _DiffEngine {
}
}
-
/* Divide the Largest Common Subsequence (LCS) of the sequences
* [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
* sized segments.
@@ -1040,23 +1157,22 @@ class _DiffEngine {
* of the portions it is going to specify.
*/
function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
- wfProfileIn( __METHOD__ );
$flip = false;
if ($xlim - $xoff > $ylim - $yoff) {
// Things seems faster (I'm not sure I understand why)
- // when the shortest sequence in X.
- $flip = true;
+ // when the shortest sequence in X.
+ $flip = true;
list ($xoff, $xlim, $yoff, $ylim)
= array( $yoff, $ylim, $xoff, $xlim);
}
if ($flip)
- for ($i = $ylim - 1; $i >= $yoff; $i--)
- $ymatches[$this->xv[$i]][] = $i;
+ for ($i = $ylim - 1; $i >= $yoff; $i--)
+ $ymatches[$this->xv[$i]][] = $i;
else
- for ($i = $ylim - 1; $i >= $yoff; $i--)
- $ymatches[$this->yv[$i]][] = $i;
+ for ($i = $ylim - 1; $i >= $yoff; $i--)
+ $ymatches[$this->yv[$i]][] = $i;
$this->lcs = 0;
$this->seq[0]= $yoff - 1;
@@ -1066,25 +1182,24 @@ class _DiffEngine {
$numer = $xlim - $xoff + $nchunks - 1;
$x = $xoff;
for ($chunk = 0; $chunk < $nchunks; $chunk++) {
- wfProfileIn( __METHOD__ . "-chunk" );
if ($chunk > 0)
- for ($i = 0; $i <= $this->lcs; $i++)
- $ymids[$i][$chunk-1] = $this->seq[$i];
+ for ($i = 0; $i <= $this->lcs; $i++)
+ $ymids[$i][$chunk-1] = $this->seq[$i];
$x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
for ( ; $x < $x1; $x++) {
$line = $flip ? $this->yv[$x] : $this->xv[$x];
- if (empty($ymatches[$line]))
- continue;
+ if (empty($ymatches[$line]))
+ continue;
$matches = $ymatches[$line];
reset($matches);
while (list ($junk, $y) = each($matches))
- if (empty($this->in_seq[$y])) {
- $k = $this->_lcs_pos($y);
- USE_ASSERTS && assert($k > 0);
- $ymids[$k] = $ymids[$k-1];
- break;
- }
+ if (empty($this->in_seq[$y])) {
+ $k = $this->_lcs_pos($y);
+ USE_ASSERTS && assert($k > 0);
+ $ymids[$k] = $ymids[$k-1];
+ break;
+ }
while (list ( /* $junk */, $y) = each($matches)) {
if ($y > $this->seq[$k-1]) {
USE_ASSERTS && assert($y < $this->seq[$k]);
@@ -1100,7 +1215,6 @@ class _DiffEngine {
}
}
}
- wfProfileOut( __METHOD__ . "-chunk" );
}
$seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
@@ -1112,18 +1226,14 @@ class _DiffEngine {
}
$seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
- wfProfileOut( __METHOD__ );
return array($this->lcs, $seps);
}
function _lcs_pos ($ypos) {
- wfProfileIn( __METHOD__ );
-
$end = $this->lcs;
if ($end == 0 || $ypos > $this->seq[$end]) {
$this->seq[++$this->lcs] = $ypos;
$this->in_seq[$ypos] = 1;
- wfProfileOut( __METHOD__ );
return $this->lcs;
}
@@ -1131,9 +1241,9 @@ class _DiffEngine {
while ($beg < $end) {
$mid = (int)(($beg + $end) / 2);
if ( $ypos > $this->seq[$mid] )
- $beg = $mid + 1;
+ $beg = $mid + 1;
else
- $end = $mid;
+ $end = $mid;
}
USE_ASSERTS && assert($ypos != $this->seq[$end]);
@@ -1141,7 +1251,6 @@ class _DiffEngine {
$this->in_seq[$this->seq[$end]] = false;
$this->seq[$end] = $ypos;
$this->in_seq[$ypos] = 1;
- wfProfileOut( __METHOD__ );
return $end;
}
@@ -1157,24 +1266,22 @@ class _DiffEngine {
* All line numbers are origin-0 and discarded lines are not counted.
*/
function _compareseq ($xoff, $xlim, $yoff, $ylim) {
- wfProfileIn( __METHOD__ );
-
// Slide down the bottom initial diagonal.
while ($xoff < $xlim && $yoff < $ylim
- && $this->xv[$xoff] == $this->yv[$yoff]) {
+ && $this->xv[$xoff] == $this->yv[$yoff]) {
++$xoff;
++$yoff;
}
// Slide up the top initial diagonal.
while ($xlim > $xoff && $ylim > $yoff
- && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+ && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
--$xlim;
--$ylim;
}
if ($xoff == $xlim || $yoff == $ylim)
- $lcs = 0;
+ $lcs = 0;
else {
// This is ad hoc but seems to work well.
//$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
@@ -1188,9 +1295,9 @@ class _DiffEngine {
// X and Y sequences have no common subsequence:
// mark all changed.
while ($yoff < $ylim)
- $this->ychanged[$this->yind[$yoff++]] = 1;
+ $this->ychanged[$this->yind[$yoff++]] = 1;
while ($xoff < $xlim)
- $this->xchanged[$this->xind[$xoff++]] = 1;
+ $this->xchanged[$this->xind[$xoff++]] = 1;
} else {
// Use the partitions to split this problem into subproblems.
reset($seps);
@@ -1200,7 +1307,6 @@ class _DiffEngine {
$pt1 = $pt2;
}
}
- wfProfileOut( __METHOD__ );
}
/* Adjust inserts/deletes of identical lines to join changes
@@ -1237,23 +1343,23 @@ class _DiffEngine {
* $other_changed[$j] == false.
*/
while ($j < $other_len && $other_changed[$j])
- $j++;
+ $j++;
while ($i < $len && ! $changed[$i]) {
USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
$i++; $j++;
while ($j < $other_len && $other_changed[$j])
- $j++;
+ $j++;
}
if ($i == $len)
- break;
+ break;
$start = $i;
// Find the end of this run of changes.
while (++$i < $len && $changed[$i])
- continue;
+ continue;
do {
/*
@@ -1271,10 +1377,10 @@ class _DiffEngine {
$changed[--$start] = 1;
$changed[--$i] = false;
while ($start > 0 && $changed[$start - 1])
- $start--;
+ $start--;
USE_ASSERTS && assert('$j > 0');
while ($other_changed[--$j])
- continue;
+ continue;
USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
}
@@ -1296,14 +1402,14 @@ class _DiffEngine {
$changed[$start++] = false;
$changed[$i++] = 1;
while ($i < $len && $changed[$i])
- $i++;
+ $i++;
USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
$j++;
if ($j < $other_len && $other_changed[$j]) {
$corresponding = $i;
while ($j < $other_len && $other_changed[$j])
- $j++;
+ $j++;
}
}
} while ($runlength != $i - $start);
@@ -1317,7 +1423,7 @@ class _DiffEngine {
$changed[--$i] = 0;
USE_ASSERTS && assert('$j > 0');
while ($other_changed[--$j])
- continue;
+ continue;
USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
}
}
@@ -1376,7 +1482,7 @@ class Diff
function isEmpty () {
foreach ($this->edits as $edit) {
if ($edit->type != 'copy')
- return false;
+ return false;
}
return true;
}
@@ -1392,7 +1498,7 @@ class Diff
$lcs = 0;
foreach ($this->edits as $edit) {
if ($edit->type == 'copy')
- $lcs += sizeof($edit->orig);
+ $lcs += sizeof($edit->orig);
}
return $lcs;
}
@@ -1410,7 +1516,7 @@ class Diff
foreach ($this->edits as $edit) {
if ($edit->orig)
- array_splice($lines, sizeof($lines), 0, $edit->orig);
+ array_splice($lines, sizeof($lines), 0, $edit->orig);
}
return $lines;
}
@@ -1428,7 +1534,7 @@ class Diff
foreach ($this->edits as $edit) {
if ($edit->closing)
- array_splice($lines, sizeof($lines), 0, $edit->closing);
+ array_splice($lines, sizeof($lines), 0, $edit->closing);
}
return $lines;
}
@@ -1441,21 +1547,21 @@ class Diff
function _check ($from_lines, $to_lines) {
wfProfileIn( __METHOD__ );
if (serialize($from_lines) != serialize($this->orig()))
- trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+ trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
if (serialize($to_lines) != serialize($this->closing()))
- trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+ trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
$rev = $this->reverse();
if (serialize($to_lines) != serialize($rev->orig()))
- trigger_error("Reversed original doesn't match", E_USER_ERROR);
+ trigger_error("Reversed original doesn't match", E_USER_ERROR);
if (serialize($from_lines) != serialize($rev->closing()))
- trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+ trigger_error("Reversed closing doesn't match", E_USER_ERROR);
$prevtype = 'none';
foreach ($this->edits as $edit) {
if ( $prevtype == $edit->type )
- trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+ trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
$prevtype = $edit->type;
}
@@ -1496,7 +1602,7 @@ class MappedDiff extends Diff
* have the same number of elements as $to_lines.
*/
function MappedDiff($from_lines, $to_lines,
- $mapped_from_lines, $mapped_to_lines) {
+ $mapped_from_lines, $mapped_to_lines) {
wfProfileIn( __METHOD__ );
assert(sizeof($from_lines) == sizeof($mapped_from_lines));
@@ -1579,8 +1685,8 @@ class DiffFormatter {
$block[] = new _DiffOp_Copy($context);
}
$this->_block($x0, $ntrail + $xi - $x0,
- $y0, $ntrail + $yi - $y0,
- $block);
+ $y0, $ntrail + $yi - $y0,
+ $block);
$block = false;
}
}
@@ -1593,21 +1699,21 @@ class DiffFormatter {
$y0 = $yi - sizeof($context);
$block = array();
if ($context)
- $block[] = new _DiffOp_Copy($context);
+ $block[] = new _DiffOp_Copy($context);
}
$block[] = $edit;
}
if ($edit->orig)
- $xi += sizeof($edit->orig);
+ $xi += sizeof($edit->orig);
if ($edit->closing)
- $yi += sizeof($edit->closing);
+ $yi += sizeof($edit->closing);
}
if (is_array($block))
- $this->_block($x0, $xi - $x0,
- $y0, $yi - $y0,
- $block);
+ $this->_block($x0, $xi - $x0,
+ $y0, $yi - $y0,
+ $block);
$end = $this->_end_diff();
wfProfileOut( __METHOD__ );
@@ -1619,15 +1725,15 @@ class DiffFormatter {
$this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
foreach ($edits as $edit) {
if ($edit->type == 'copy')
- $this->_context($edit->orig);
+ $this->_context($edit->orig);
elseif ($edit->type == 'add')
- $this->_added($edit->closing);
+ $this->_added($edit->closing);
elseif ($edit->type == 'delete')
- $this->_deleted($edit->orig);
+ $this->_deleted($edit->orig);
elseif ($edit->type == 'change')
- $this->_changed($edit->orig, $edit->closing);
+ $this->_changed($edit->orig, $edit->closing);
else
- trigger_error('Unknown edit type', E_USER_ERROR);
+ trigger_error('Unknown edit type', E_USER_ERROR);
}
$this->_end_block();
wfProfileOut( __METHOD__ );
@@ -1645,9 +1751,9 @@ class DiffFormatter {
function _block_header($xbeg, $xlen, $ybeg, $ylen) {
if ($xlen > 1)
- $xbeg .= "," . ($xbeg + $xlen - 1);
+ $xbeg .= "," . ($xbeg + $xlen - 1);
if ($ylen > 1)
- $ybeg .= "," . ($ybeg + $ylen - 1);
+ $ybeg .= "," . ($ybeg + $ylen - 1);
return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
}
@@ -1661,7 +1767,7 @@ class DiffFormatter {
function _lines($lines, $prefix = ' ') {
foreach ($lines as $line)
- echo "$prefix $line\n";
+ echo "$prefix $line\n";
}
function _context($lines) {
@@ -1716,40 +1822,40 @@ class ArrayDiffFormatter extends DiffFormatter {
$newline = 1;
$retval = array();
foreach($diff->edits as $edit)
- switch($edit->type) {
- case 'add':
- foreach($edit->closing as $l) {
- $retval[] = array(
+ switch($edit->type) {
+ case 'add':
+ foreach($edit->closing as $l) {
+ $retval[] = array(
'action' => 'add',
'new'=> $l,
'newline' => $newline++
- );
- }
- break;
- case 'delete':
- foreach($edit->orig as $l) {
- $retval[] = array(
+ );
+ }
+ break;
+ case 'delete':
+ foreach($edit->orig as $l) {
+ $retval[] = array(
'action' => 'delete',
'old' => $l,
'oldline' => $oldline++,
- );
- }
- break;
- case 'change':
- foreach($edit->orig as $i => $l) {
- $retval[] = array(
+ );
+ }
+ break;
+ case 'change':
+ foreach($edit->orig as $i => $l) {
+ $retval[] = array(
'action' => 'change',
'old' => $l,
'new' => @$edit->closing[$i],
'oldline' => $oldline++,
'newline' => $newline++,
- );
- }
- break;
- case 'copy':
- $oldline += count($edit->orig);
- $newline += count($edit->orig);
- }
+ );
+ }
+ break;
+ case 'copy':
+ $oldline += count($edit->orig);
+ $newline += count($edit->orig);
+ }
return $retval;
}
}
@@ -1777,13 +1883,13 @@ class _HWLDF_WordAccumulator {
function _flushGroup ($new_tag) {
if ($this->_group !== '') {
if ($this->_tag == 'ins')
- $this->_line .= '<ins class="diffchange diffchange-inline">' .
- htmlspecialchars ( $this->_group ) . '</ins>';
+ $this->_line .= '<ins class="diffchange diffchange-inline">' .
+ htmlspecialchars ( $this->_group ) . '</ins>';
elseif ($this->_tag == 'del')
- $this->_line .= '<del class="diffchange diffchange-inline">' .
- htmlspecialchars ( $this->_group ) . '</del>';
+ $this->_line .= '<del class="diffchange diffchange-inline">' .
+ htmlspecialchars ( $this->_group ) . '</del>';
else
- $this->_line .= htmlspecialchars ( $this->_group );
+ $this->_line .= htmlspecialchars ( $this->_group );
}
$this->_group = '';
$this->_tag = $new_tag;
@@ -1792,21 +1898,21 @@ class _HWLDF_WordAccumulator {
function _flushLine ($new_tag) {
$this->_flushGroup($new_tag);
if ($this->_line != '')
- array_push ( $this->_lines, $this->_line );
+ array_push ( $this->_lines, $this->_line );
else
- # make empty lines visible by inserting an NBSP
- array_push ( $this->_lines, NBSP );
+ # make empty lines visible by inserting an NBSP
+ array_push ( $this->_lines, NBSP );
$this->_line = '';
}
function addWords ($words, $tag = '') {
if ($tag != $this->_tag)
- $this->_flushGroup($tag);
+ $this->_flushGroup($tag);
foreach ($words as $word) {
// new-line should only come as first char of word.
if ($word == '')
- continue;
+ continue;
if ($word[0] == "\n") {
$this->_flushLine($tag);
$word = substr($word, 1);
@@ -1837,7 +1943,7 @@ class WordLevelDiff extends MappedDiff {
list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
$this->MappedDiff($orig_words, $closing_words,
- $orig_stripped, $closing_stripped);
+ $orig_stripped, $closing_stripped);
wfProfileOut( __METHOD__ );
}
@@ -1862,7 +1968,7 @@ class WordLevelDiff extends MappedDiff {
} else {
$m = array();
if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
- $line, $m))
+ $line, $m))
{
$words = array_merge( $words, $m[0] );
$stripped = array_merge( $stripped, $m[1] );
@@ -1879,9 +1985,9 @@ class WordLevelDiff extends MappedDiff {
foreach ($this->edits as $edit) {
if ($edit->type == 'copy')
- $orig->addWords($edit->orig);
+ $orig->addWords($edit->orig);
elseif ($edit->orig)
- $orig->addWords($edit->orig, 'del');
+ $orig->addWords($edit->orig, 'del');
}
$lines = $orig->getLines();
wfProfileOut( __METHOD__ );
@@ -1894,9 +2000,9 @@ class WordLevelDiff extends MappedDiff {
foreach ($this->edits as $edit) {
if ($edit->type == 'copy')
- $closing->addWords($edit->closing);
+ $closing->addWords($edit->closing);
elseif ($edit->closing)
- $closing->addWords($edit->closing, 'ins');
+ $closing->addWords($edit->closing, 'ins');
}
$lines = $closing->getLines();
wfProfileOut( __METHOD__ );
@@ -1969,24 +2075,24 @@ class TableDiffFormatter extends DiffFormatter {
function _added( $lines ) {
foreach ($lines as $line) {
echo '<tr>' . $this->emptyLine() .
- $this->addedLine( '<ins class="diffchange">' .
- htmlspecialchars ( $line ) . '</ins>' ) . "</tr>\n";
+ $this->addedLine( '<ins class="diffchange">' .
+ htmlspecialchars ( $line ) . '</ins>' ) . "</tr>\n";
}
}
function _deleted($lines) {
foreach ($lines as $line) {
echo '<tr>' . $this->deletedLine( '<del class="diffchange">' .
- htmlspecialchars ( $line ) . '</del>' ) .
- $this->emptyLine() . "</tr>\n";
+ htmlspecialchars ( $line ) . '</del>' ) .
+ $this->emptyLine() . "</tr>\n";
}
}
function _context( $lines ) {
foreach ($lines as $line) {
echo '<tr>' .
- $this->contextLine( htmlspecialchars ( $line ) ) .
- $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n";
+ $this->contextLine( htmlspecialchars ( $line ) ) .
+ $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n";
}
}
@@ -2003,11 +2109,11 @@ class TableDiffFormatter extends DiffFormatter {
while ( $line = array_shift( $del ) ) {
$aline = array_shift( $add );
echo '<tr>' . $this->deletedLine( $line ) .
- $this->addedLine( $aline ) . "</tr>\n";
+ $this->addedLine( $aline ) . "</tr>\n";
}
foreach ($add as $line) { # If any leftovers
echo '<tr>' . $this->emptyLine() .
- $this->addedLine( $line ) . "</tr>\n";
+ $this->addedLine( $line ) . "</tr>\n";
}
wfProfileOut( __METHOD__ );
}
diff --git a/includes/diff/HTMLDiff.php b/includes/diff/HTMLDiff.php
new file mode 100644
index 00000000..0698059f
--- /dev/null
+++ b/includes/diff/HTMLDiff.php
@@ -0,0 +1,1005 @@
+<?php
+
+/** Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu>
+ *
+ * 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.
+ * or see http://www.gnu.org/
+ *
+ * @ingroup DifferenceEngine
+ */
+
+/**
+ * When detecting the last common parent of two nodes, all results are stored as
+ * a LastCommonParentResult.
+ */
+class LastCommonParentResult {
+
+ // Parent
+ public $parent;
+
+ // Splitting
+ public $splittingNeeded = false;
+
+ // Depth
+ public $lastCommonParentDepth = -1;
+
+ // Index
+ public $indexInLastCommonParent = -1;
+}
+
+class Modification{
+
+ const NONE = 1;
+ const REMOVED = 2;
+ const ADDED = 4;
+ const CHANGED = 8;
+
+ public $type;
+
+ public $id = -1;
+
+ public $firstOfID = false;
+
+ public $changes;
+
+ function __construct($type) {
+ $this->type = $type;
+ }
+
+ public static function typeToString($type) {
+ switch($type) {
+ case self::NONE: return 'none';
+ case self::REMOVED: return 'removed';
+ case self::ADDED: return 'added';
+ case self::CHANGED: return 'changed';
+ }
+ }
+}
+
+class DomTreeBuilder {
+
+ public $textNodes = array();
+
+ public $bodyNode;
+
+ private $currentParent;
+
+ private $newWord = '';
+
+ protected $bodyStarted = false;
+
+ protected $bodyEnded = false;
+
+ private $whiteSpaceBeforeThis = false;
+
+ private $lastSibling;
+
+ private $notInPre = true;
+
+ function __construct() {
+ $this->bodyNode = $this->currentParent = new BodyNode();
+ $this->lastSibling = new DummyNode();
+ }
+
+ /**
+ * Must be called manually
+ */
+ public function endDocument() {
+ $this->endWord();
+ HTMLDiffer::diffDebug( count($this->textNodes) . " text nodes in document.\n" );
+ }
+
+ public function startElement($parser, $name, /*array*/ $attributes) {
+ if (strcasecmp($name, 'body') != 0) {
+ HTMLDiffer::diffDebug( "Starting $name node.\n" );
+ $this->endWord();
+
+ $newNode = new TagNode($this->currentParent, $name, $attributes);
+ $this->currentParent->children[] = $newNode;
+ $this->currentParent = $newNode;
+ $this->lastSibling = new DummyNode();
+ if ($this->whiteSpaceBeforeThis && !in_array(strtolower($this->currentParent->qName),TagNode::$blocks)) {
+ $this->currentParent->whiteBefore = true;
+ }
+ $this->whiteSpaceBeforeThis = false;
+ if(strcasecmp($name, 'pre') == 0) {
+ $this->notInPre = false;
+ }
+ }
+ }
+
+ public function endElement($parser, $name) {
+ if(strcasecmp($name, 'body') != 0) {
+ HTMLDiffer::diffDebug( "Ending $name node.\n");
+ if (0 == strcasecmp($name,'img')) {
+ // Insert a dummy leaf for the image
+ $img = new ImageNode($this->currentParent, $this->currentParent->attributes);
+ $this->currentParent->children[] = $img;
+ $img->whiteBefore = $this->whiteSpaceBeforeThis;
+ $this->lastSibling = $img;
+ $this->textNodes[] = $img;
+ }
+ $this->endWord();
+ if (!in_array(strtolower($this->currentParent->qName),TagNode::$blocks)) {
+ $this->lastSibling = $this->currentParent;
+ } else {
+ $this->lastSibling = new DummyNode();
+ }
+ $this->currentParent = $this->currentParent->parent;
+ $this->whiteSpaceBeforeThis = false;
+ if (!$this->notInPre && strcasecmp($name, 'pre') == 0) {
+ $this->notInPre = true;
+ }
+ } else {
+ $this->endDocument();
+ }
+ }
+
+ const regex = '/([\s\.\,\"\\\'\(\)\?\:\;\!\{\}\-\+\*\=\_\[\]\&\|\$]{1})/';
+ const whitespace = '/^[\s]{1}$/';
+ const delimiter = '/^[\s\.\,\"\\\'\(\)\?\:\;\!\{\}\-\+\*\=\_\[\]\&\|\$]{1}$/';
+
+ public function characters($parser, $data) {
+ $matches = preg_split(self::regex, $data, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+ foreach($matches as &$word) {
+ if (preg_match(self::whitespace, $word) && $this->notInPre) {
+ $this->endWord();
+ $this->lastSibling->whiteAfter = true;
+ $this->whiteSpaceBeforeThis = true;
+ } else if (preg_match(self::delimiter, $word)) {
+ $this->endWord();
+ $textNode = new TextNode($this->currentParent, $word);
+ $this->currentParent->children[] = $textNode;
+ $textNode->whiteBefore = $this->whiteSpaceBeforeThis;
+ $this->whiteSpaceBeforeThis = false;
+ $this->lastSibling = $textNode;
+ $this->textNodes[] = $textNode;
+ } else {
+ $this->newWord .= $word;
+ }
+ }
+ }
+
+ private function endWord() {
+ if ($this->newWord !== '') {
+ $node = new TextNode($this->currentParent, $this->newWord);
+ $this->currentParent->children[] = $node;
+ $node->whiteBefore = $this->whiteSpaceBeforeThis;
+ $this->whiteSpaceBeforeThis = false;
+ $this->lastSibling = $node;
+ $this->textNodes[] = $node;
+ $this->newWord = "";
+ }
+ }
+
+ public function getDiffLines() {
+ return array_map(array('TextNode','toDiffLine'), $this->textNodes);
+ }
+}
+
+class TextNodeDiffer {
+
+ private $textNodes;
+ public $bodyNode;
+
+ private $oldTextNodes;
+ private $oldBodyNode;
+
+ private $newID = 0;
+
+ private $changedID = 0;
+
+ private $changedIDUsed = false;
+
+ // used to remove the whitespace between a red and green block
+ private $whiteAfterLastChangedPart = false;
+
+ private $deletedID = 0;
+
+ function __construct(DomTreeBuilder $tree, DomTreeBuilder $oldTree) {
+ $this->textNodes = $tree->textNodes;
+ $this->bodyNode = $tree->bodyNode;
+ $this->oldTextNodes = $oldTree->textNodes;
+ $this->oldBodyNode = $oldTree->bodyNode;
+ }
+
+ public function markAsNew($start, $end) {
+ if ($end <= $start) {
+ return;
+ }
+
+ if ($this->whiteAfterLastChangedPart) {
+ $this->textNodes[$start]->whiteBefore = false;
+ }
+
+ for ($i = $start; $i < $end; ++$i) {
+ $mod = new Modification(Modification::ADDED);
+ $mod->id = $this->newID;
+ $this->textNodes[$i]->modification = $mod;
+ }
+ if ($start < $end) {
+ $this->textNodes[$start]->modification->firstOfID = true;
+ }
+ ++$this->newID;
+ }
+
+ public function handlePossibleChangedPart($leftstart, $leftend, $rightstart, $rightend) {
+ $i = $rightstart;
+ $j = $leftstart;
+
+ if ($this->changedIDUsed) {
+ ++$this->changedID;
+ $this->changedIDUsed = false;
+ }
+
+ $changes;
+ while ($i < $rightend) {
+ $acthis = new AncestorComparator($this->textNodes[$i]->getParentTree());
+ $acother = new AncestorComparator($this->oldTextNodes[$j]->getParentTree());
+ $result = $acthis->getResult($acother);
+ unset($acthis, $acother);
+
+ if ( $result ) {
+ $mod = new Modification(Modification::CHANGED);
+
+ if (!$this->changedIDUsed) {
+ $mod->firstOfID = true;
+ } else if (!is_null( $result ) && $result !== $this->changes) {
+ ++$this->changedID;
+ $mod->firstOfID = true;
+ }
+
+ $mod->changes = $result;
+ $mod->id = $this->changedID;
+
+ $this->textNodes[$i]->modification = $mod;
+ $this->changes = $result;
+ $this->changedIDUsed = true;
+ } else if ($this->changedIDUsed) {
+ ++$this->changedID;
+ $this->changedIDUsed = false;
+ }
+ ++$i;
+ ++$j;
+ }
+ }
+
+ public function markAsDeleted($start, $end, $before) {
+
+ if ($end <= $start) {
+ return;
+ }
+
+ if ($before > 0 && $this->textNodes[$before - 1]->whiteAfter) {
+ $this->whiteAfterLastChangedPart = true;
+ } else {
+ $this->whiteAfterLastChangedPart = false;
+ }
+
+ for ($i = $start; $i < $end; ++$i) {
+ $mod = new Modification(Modification::REMOVED);
+ $mod->id = $this->deletedID;
+
+ // oldTextNodes is used here because we're going to move its deleted
+ // elements to this tree!
+ $this->oldTextNodes[$i]->modification = $mod;
+ }
+ $this->oldTextNodes[$start]->modification->firstOfID = true;
+
+ $root = $this->oldTextNodes[$start]->getLastCommonParent($this->oldTextNodes[$end-1])->parent;
+
+ $junk1 = $junk2 = null;
+ $deletedNodes = $root->getMinimalDeletedSet($this->deletedID, $junk1, $junk2);
+
+ HTMLDiffer::diffDebug( "Minimal set of deleted nodes of size " . count($deletedNodes) . "\n" );
+
+ // Set prevLeaf to the leaf after which the old HTML needs to be
+ // inserted
+ if ($before > 0) {
+ $prevLeaf = $this->textNodes[$before - 1];
+ }
+ // Set nextLeaf to the leaf before which the old HTML needs to be
+ // inserted
+ if ($before < count($this->textNodes)) {
+ $nextLeaf = $this->textNodes[$before];
+ }
+
+ while (count($deletedNodes) > 0) {
+ if (isset($prevLeaf)) {
+ $prevResult = $prevLeaf->getLastCommonParent($deletedNodes[0]);
+ } else {
+ $prevResult = new LastCommonParentResult();
+ $prevResult->parent = $this->bodyNode;
+ $prevResult->indexInLastCommonParent = -1;
+ }
+ if (isset($nextleaf)) {
+ $nextResult = $nextLeaf->getLastCommonParent($deletedNodes[count($deletedNodes) - 1]);
+ } else {
+ $nextResult = new LastCommonParentResult();
+ $nextResult->parent = $this->bodyNode;
+ $nextResult->indexInLastCommonParent = $this->bodyNode->getNbChildren();
+ }
+
+ if ($prevResult->lastCommonParentDepth == $nextResult->lastCommonParentDepth) {
+ // We need some metric to choose which way to add-...
+ if ($deletedNodes[0]->parent === $deletedNodes[count($deletedNodes) - 1]->parent
+ && $prevResult->parent === $nextResult->parent) {
+ // The difference is not in the parent
+ $prevResult->lastCommonParentDepth = $prevResult->lastCommonParentDepth + 1;
+ } else {
+ // The difference is in the parent, so compare them
+ // now THIS is tricky
+ $distancePrev = $deletedNodes[0]->parent->getMatchRatio($prevResult->parent);
+ $distanceNext = $deletedNodes[count($deletedNodes) - 1]->parent->getMatchRatio($nextResult->parent);
+
+ if ($distancePrev <= $distanceNext) {
+ $prevResult->lastCommonParentDepth = $prevResult->lastCommonParentDepth + 1;
+ } else {
+ $nextResult->lastCommonParentDepth = $nextResult->lastCommonParentDepth + 1;
+ }
+ }
+
+ }
+
+ if ($prevResult->lastCommonParentDepth > $nextResult->lastCommonParentDepth) {
+ // Inserting at the front
+ if ($prevResult->splittingNeeded) {
+ $prevLeaf->parent->splitUntil($prevResult->parent, $prevLeaf, true);
+ }
+ $prevLeaf = $deletedNodes[0]->copyTree();
+ unset($deletedNodes[0]);
+ $deletedNodes = array_values($deletedNodes);
+ $prevLeaf->setParent($prevResult->parent);
+ $prevResult->parent->addChildAbsolute($prevLeaf,$prevResult->indexInLastCommonParent + 1);
+ } else if ($prevResult->lastCommonParentDepth < $nextResult->lastCommonParentDepth) {
+ // Inserting at the back
+ if ($nextResult->splittingNeeded) {
+ $splitOccured = $nextLeaf->parent->splitUntil($nextResult->parent, $nextLeaf, false);
+ if ($splitOccured) {
+ // The place where to insert is shifted one place to the
+ // right
+ $nextResult->indexInLastCommonParent = $nextResult->indexInLastCommonParent + 1;
+ }
+ }
+ $nextLeaf = $deletedNodes[count(deletedNodes) - 1]->copyTree();
+ unset($deletedNodes[count(deletedNodes) - 1]);
+ $deletedNodes = array_values($deletedNodes);
+ $nextLeaf->setParent($nextResult->parent);
+ $nextResult->parent->addChildAbsolute($nextLeaf,$nextResult->indexInLastCommonParent);
+ }
+ }
+ ++$this->deletedID;
+ }
+
+ public function expandWhiteSpace() {
+ $this->bodyNode->expandWhiteSpace();
+ }
+
+ public function lengthNew(){
+ return count($this->textNodes);
+ }
+
+ public function lengthOld(){
+ return count($this->oldTextNodes);
+ }
+}
+
+class HTMLDiffer {
+
+ private $output;
+ private static $debug = '';
+
+ function __construct($output) {
+ $this->output = $output;
+ }
+
+ function htmlDiff($from, $to) {
+ wfProfileIn( __METHOD__ );
+ // Create an XML parser
+ $xml_parser = xml_parser_create('');
+
+ $domfrom = new DomTreeBuilder();
+
+ // Set the functions to handle opening and closing tags
+ xml_set_element_handler($xml_parser, array($domfrom, "startElement"), array($domfrom, "endElement"));
+
+ // Set the function to handle blocks of character data
+ xml_set_character_data_handler($xml_parser, array($domfrom, "characters"));
+
+ HTMLDiffer::diffDebug( "Parsing " . strlen($from) . " characters worth of HTML\n" );
+ if (!xml_parse($xml_parser, '<?xml version="1.0" encoding="UTF-8"?>'.Sanitizer::hackDocType().'<body>', false)
+ || !xml_parse($xml_parser, $from, false)
+ || !xml_parse($xml_parser, '</body>', true)){
+ $error = xml_error_string(xml_get_error_code($xml_parser));
+ $line = xml_get_current_line_number($xml_parser);
+ HTMLDiffer::diffDebug( "XML error: $error at line $line\n" );
+ }
+ xml_parser_free($xml_parser);
+ unset($from);
+
+ $xml_parser = xml_parser_create('');
+
+ $domto = new DomTreeBuilder();
+
+ // Set the functions to handle opening and closing tags
+ xml_set_element_handler($xml_parser, array($domto, "startElement"), array($domto, "endElement"));
+
+ // Set the function to handle blocks of character data
+ xml_set_character_data_handler($xml_parser, array($domto, "characters"));
+
+ HTMLDiffer::diffDebug( "Parsing " . strlen($to) . " characters worth of HTML\n" );
+ if (!xml_parse($xml_parser, '<?xml version="1.0" encoding="UTF-8"?>'.Sanitizer::hackDocType().'<body>', false)
+ || !xml_parse($xml_parser, $to, false)
+ || !xml_parse($xml_parser, '</body>', true)){
+ $error = xml_error_string(xml_get_error_code($xml_parser));
+ $line = xml_get_current_line_number($xml_parser);
+ HTMLDiffer::diffDebug( "XML error: $error at line $line\n" );
+ }
+ xml_parser_free($xml_parser);
+ unset($to);
+
+ $diffengine = new WikiDiff3();
+ $differences = $this->preProcess($diffengine->diff_range($domfrom->getDiffLines(), $domto->getDiffLines()));
+ unset($xml_parser, $diffengine);
+
+ $domdiffer = new TextNodeDiffer($domto, $domfrom);
+
+ $currentIndexLeft = 0;
+ $currentIndexRight = 0;
+ foreach ($differences as &$d) {
+ if ($d->leftstart > $currentIndexLeft) {
+ $domdiffer->handlePossibleChangedPart($currentIndexLeft, $d->leftstart,
+ $currentIndexRight, $d->rightstart);
+ }
+ if ($d->leftlength > 0) {
+ $domdiffer->markAsDeleted($d->leftstart, $d->leftend, $d->rightstart);
+ }
+ $domdiffer->markAsNew($d->rightstart, $d->rightend);
+
+ $currentIndexLeft = $d->leftend;
+ $currentIndexRight = $d->rightend;
+ }
+ $oldLength = $domdiffer->lengthOld();
+ if ($currentIndexLeft < $oldLength) {
+ $domdiffer->handlePossibleChangedPart($currentIndexLeft, $oldLength, $currentIndexRight, $domdiffer->lengthNew());
+ }
+ $domdiffer->expandWhiteSpace();
+ $output = new HTMLOutput('htmldiff', $this->output);
+ $output->parse($domdiffer->bodyNode);
+ wfProfileOut( __METHOD__ );
+ }
+
+ private function preProcess(/*array*/ $differences) {
+ $newRanges = array();
+
+ $nbDifferences = count($differences);
+ for ($i = 0; $i < $nbDifferences; ++$i) {
+ $leftStart = $differences[$i]->leftstart;
+ $leftEnd = $differences[$i]->leftend;
+ $rightStart = $differences[$i]->rightstart;
+ $rightEnd = $differences[$i]->rightend;
+
+ $leftLength = $leftEnd - $leftStart;
+ $rightLength = $rightEnd - $rightStart;
+
+ while ($i + 1 < $nbDifferences && self::score($leftLength,
+ $differences[$i + 1]->leftlength,
+ $rightLength,
+ $differences[$i + 1]->rightlength)
+ > ($differences[$i + 1]->leftstart - $leftEnd)) {
+ $leftEnd = $differences[$i + 1]->leftend;
+ $rightEnd = $differences[$i + 1]->rightend;
+ $leftLength = $leftEnd - $leftStart;
+ $rightLength = $rightEnd - $rightStart;
+ ++$i;
+ }
+ $newRanges[] = new RangeDifference($leftStart, $leftEnd, $rightStart, $rightEnd);
+ }
+ return $newRanges;
+ }
+
+ /**
+ * Heuristic to merge differences for readability.
+ */
+ public static function score($ll, $nll, $rl, $nrl) {
+ if (($ll == 0 && $nll == 0)
+ || ($rl == 0 && $nrl == 0)) {
+ return 0;
+ }
+ $numbers = array($ll, $nll, $rl, $nrl);
+ $d = 0;
+ foreach ($numbers as &$number) {
+ while ($number > 3) {
+ $d += 3;
+ $number -= 3;
+ $number *= 0.5;
+ }
+ $d += $number;
+
+ }
+ return $d / (1.5 * count($numbers));
+ }
+
+ /**
+ * Add to debug output
+ * @param string $str Debug output
+ */
+ public static function diffDebug( $str ) {
+ self :: $debug .= $str;
+ }
+
+ /**
+ * Get debug output
+ * @return string
+ */
+ public static function getDebugOutput() {
+ return self :: $debug;
+ }
+
+}
+
+class TextOnlyComparator {
+
+ public $leafs = array();
+
+ function _construct(TagNode $tree) {
+ $this->addRecursive($tree);
+ $this->leafs = array_map(array('TextNode','toDiffLine'), $this->leafs);
+ }
+
+ private function addRecursive(TagNode $tree) {
+ foreach ($tree->children as &$child) {
+ if ($child instanceof TagNode) {
+ $this->addRecursive($child);
+ } else if ($child instanceof TextNode) {
+ $this->leafs[] = $node;
+ }
+ }
+ }
+
+ public function getMatchRatio(TextOnlyComparator $other) {
+ $nbOthers = count($other->leafs);
+ $nbThis = count($this->leafs);
+ if($nbOthers == 0 || $nbThis == 0){
+ return -log(0);
+ }
+
+ $diffengine = new WikiDiff3(25000, 1.35);
+ $diffengine->diff($this->leafs, $other->leafs);
+
+ $lcsLength = $diffengine->getLcsLength();
+
+ $distanceThis = $nbThis-$lcsLength;
+
+ return (2.0 - $lcsLength/$nbOthers - $lcsLength/$nbThis) / 2.0;
+ }
+}
+
+/**
+ * A comparator used when calculating the difference in ancestry of two Nodes.
+ */
+class AncestorComparator {
+
+ public $ancestors;
+ public $ancestorsText;
+
+ function __construct(/*array*/ $ancestors) {
+ $this->ancestors = $ancestors;
+ $this->ancestorsText = array_map(array('TagNode','toDiffLine'), $ancestors);
+ }
+
+ public $compareTxt = "";
+
+ public function getResult(AncestorComparator $other) {
+
+ $diffengine = new WikiDiff3(10000, 1.35);
+ $differences = $diffengine->diff_range($other->ancestorsText,$this->ancestorsText);
+
+ if (count($differences) == 0){
+ return null;
+ }
+ $changeTxt = new ChangeTextGenerator($this, $other);
+
+ return $changeTxt->getChanged($differences)->toString();;
+ }
+}
+
+class ChangeTextGenerator {
+
+ private $ancestorComparator;
+ private $other;
+
+ private $factory;
+
+ function __construct(AncestorComparator $ancestorComparator, AncestorComparator $other) {
+ $this->ancestorComparator = $ancestorComparator;
+ $this->other = $other;
+ $this->factory = new TagToStringFactory();
+ }
+
+ public function getChanged(/*array*/ $differences) {
+ $txt = new ChangeText;
+ $rootlistopened = false;
+ if (count($differences) > 1) {
+ $txt->addHtml('<ul class="changelist">');
+ $rootlistopened = true;
+ }
+ $nbDifferences = count($differences);
+ for ($j = 0; $j < $nbDifferences; ++$j) {
+ $d = $differences[$j];
+ $lvl1listopened = false;
+ if ($rootlistopened) {
+ $txt->addHtml('<li>');
+ }
+ if ($d->leftlength + $d->rightlength > 1) {
+ $txt->addHtml('<ul class="changelist">');
+ $lvl1listopened = true;
+ }
+ // left are the old ones
+ for ($i = $d->leftstart; $i < $d->leftend; ++$i) {
+ if ($lvl1listopened){
+ $txt->addHtml('<li>');
+ }
+ // add a bullet for a old tag
+ $this->addTagOld($txt, $this->other->ancestors[$i]);
+ if ($lvl1listopened){
+ $txt->addHtml('</li>');
+ }
+ }
+ // right are the new ones
+ for ($i = $d->rightstart; $i < $d->rightend; ++$i) {
+ if ($lvl1listopened){
+ $txt->addHtml('<li>');
+ }
+ // add a bullet for a new tag
+ $this->addTagNew($txt, $this->ancestorComparator->ancestors[$i]);
+
+ if ($lvl1listopened){
+ $txt->addHtml('</li>');
+ }
+ }
+ if ($lvl1listopened) {
+ $txt->addHtml('</ul>');
+ }
+ if ($rootlistopened) {
+ $txt->addHtml('</li>');
+ }
+ }
+ if ($rootlistopened) {
+ $txt->addHtml('</ul>');
+ }
+ return $txt;
+ }
+
+ private function addTagOld(ChangeText $txt, TagNode $ancestor) {
+ $this->factory->create($ancestor)->getRemovedDescription($txt);
+ }
+
+ private function addTagNew(ChangeText $txt, TagNode $ancestor) {
+ $this->factory->create($ancestor)->getAddedDescription($txt);
+ }
+}
+
+class ChangeText {
+
+ private $txt = "";
+
+ public function addHtml($s) {
+ $this->txt .= $s;
+ }
+
+ public function toString() {
+ return $this->txt;
+ }
+}
+
+class TagToStringFactory {
+
+ private static $containerTags = array('html', 'body', 'p', 'blockquote',
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'pre', 'div', 'ul', 'ol', 'li',
+ 'table', 'tbody', 'tr', 'td', 'th', 'br', 'hr', 'code', 'dl',
+ 'dt', 'dd', 'input', 'form', 'img', 'span', 'a');
+
+ private static $styleTags = array('i', 'b', 'strong', 'em', 'font',
+ 'big', 'del', 'tt', 'sub', 'sup', 'strike');
+
+ const MOVED = 1;
+ const STYLE = 2;
+ const UNKNOWN = 4;
+
+ public function create(TagNode $node) {
+ $sem = $this->getChangeSemantic($node->qName);
+ if (strcasecmp($node->qName,'a') == 0) {
+ return new AnchorToString($node, $sem);
+ }
+ if (strcasecmp($node->qName,'img') == 0) {
+ return new NoContentTagToString($node, $sem);
+ }
+ return new TagToString($node, $sem);
+ }
+
+ protected function getChangeSemantic($qname) {
+ if (in_array(strtolower($qname),self::$containerTags)) {
+ return self::MOVED;
+ }
+ if (in_array(strtolower($qname),self::$styleTags)) {
+ return self::STYLE;
+ }
+ return self::UNKNOWN;
+ }
+}
+
+class TagToString {
+
+ protected $node;
+
+ protected $sem;
+
+ function __construct(TagNode $node, $sem) {
+ $this->node = $node;
+ $this->sem = $sem;
+ }
+
+ public function getRemovedDescription(ChangeText $txt) {
+ $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' );
+ if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){
+ $tagDescription = "&lt;" . $this->node->qName . "&gt;";
+ }
+ if ($this->sem == TagToStringFactory::MOVED) {
+ $txt->addHtml( wfMsgExt( 'diff-movedoutof', 'parseinline', $tagDescription ) );
+ } else if ($this->sem == TagToStringFactory::STYLE) {
+ $txt->addHtml( wfMsgExt( 'diff-styleremoved' , 'parseinline', $tagDescription ) );
+ } else {
+ $txt->addHtml( wfMsgExt( 'diff-removed' , 'parseinline', $tagDescription ) );
+ }
+ $this->addAttributes($txt, $this->node->attributes);
+ $txt->addHtml('.');
+ }
+
+ public function getAddedDescription(ChangeText $txt) {
+ $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' );
+ if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){
+ $tagDescription = "&lt;" . $this->node->qName . "&gt;";
+ }
+ if ($this->sem == TagToStringFactory::MOVED) {
+ $txt->addHtml( wfMsgExt( 'diff-movedto' , 'parseinline', $tagDescription) );
+ } else if ($this->sem == TagToStringFactory::STYLE) {
+ $txt->addHtml( wfMsgExt( 'diff-styleadded', 'parseinline', $tagDescription ) );
+ } else {
+ $txt->addHtml( wfMsgExt( 'diff-added', 'parseinline', $tagDescription ) );
+ }
+ $this->addAttributes($txt, $this->node->attributes);
+ $txt->addHtml('.');
+ }
+
+ protected function addAttributes(ChangeText $txt, array $attributes) {
+ if (count($attributes) < 1) {
+ return;
+ }
+ $firstOne = true;
+ $nbAttributes_min_1 = count($attributes)-1;
+ $keys = array_keys($attributes);
+ for ($i=0;$i<$nbAttributes_min_1;$i++) {
+ $key = $keys[$i];
+ $attr = $attributes[$key];
+ if($firstOne) {
+ $firstOne = false;
+ $txt->addHtml( wfMsgExt('diff-with', 'escapenoentities', $this->translateArgument($key), htmlspecialchars($attr) ) );
+ continue;
+ }
+ $txt->addHtml( wfMsgExt( 'comma-separator', 'escapenoentities' ) .
+ wfMsgExt( 'diff-with-additional', 'escapenoentities',
+ $this->translateArgument( $key ), htmlspecialchars( $attr ) )
+ );
+ }
+
+ if ($nbAttributes_min_1 > 0) {
+ $txt->addHtml( wfMsgExt( 'diff-with-final', 'escapenoentities',
+ $this->translateArgument($keys[$nbAttributes_min_1]),
+ htmlspecialchars($attributes[$keys[$nbAttributes_min_1]]) ) );
+ }
+ }
+
+ protected function translateArgument($name) {
+ $translation = wfMsgExt('diff-' . $name, 'parseinline' );
+ if ( wfEmptyMsg( 'diff-' . $name, $translation ) ) {
+ $translation = "&lt;" . $name . "&gt;";;
+ }
+ return htmlspecialchars( $translation );
+ }
+}
+
+class NoContentTagToString extends TagToString {
+
+ function __construct(TagNode $node, $sem) {
+ parent::__construct($node, $sem);
+ }
+
+ public function getAddedDescription(ChangeText $txt) {
+ $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' );
+ if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){
+ $tagDescription = "&lt;" . $this->node->qName . "&gt;";
+ }
+ $txt->addHtml( wfMsgExt('diff-changedto', 'parseinline', $tagDescription ) );
+ $this->addAttributes($txt, $this->node->attributes);
+ $txt->addHtml('.');
+ }
+
+ public function getRemovedDescription(ChangeText $txt) {
+ $txt->addHtml( wfMsgExt('diff-changedfrom', 'parseinline', $tagDescription ) );
+ $this->addAttributes($txt, $this->node->attributes);
+ $txt->addHtml('.');
+ }
+}
+
+class AnchorToString extends TagToString {
+
+ function __construct(TagNode $node, $sem) {
+ parent::__construct($node, $sem);
+ }
+
+ protected function addAttributes(ChangeText $txt, array $attributes) {
+ if (array_key_exists('href', $attributes)) {
+ $txt->addHtml(' ' . wfMsgExt( 'diff-withdestination', 'parseinline', htmlspecialchars($attributes['href']) ) );
+ unset($attributes['href']);
+ }
+ parent::addAttributes($txt, $attributes);
+ }
+}
+
+/**
+ * Takes a branch root and creates an HTML file for it.
+ */
+class HTMLOutput{
+
+ private $prefix;
+ private $handler;
+
+ function __construct($prefix, $handler) {
+ $this->prefix = $prefix;
+ $this->handler = $handler;
+ }
+
+ public function parse(TagNode $node) {
+ $handler = &$this->handler;
+
+ if (strcasecmp($node->qName, 'img') != 0 && strcasecmp($node->qName, 'body') != 0) {
+ $handler->startElement($node->qName, $node->attributes);
+ }
+
+ $newStarted = false;
+ $remStarted = false;
+ $changeStarted = false;
+ $changeTXT = '';
+
+ foreach ($node->children as &$child) {
+ if ($child instanceof TagNode) {
+ if ($newStarted) {
+ $handler->endElement('span');
+ $newStarted = false;
+ } else if ($changeStarted) {
+ $handler->endElement('span');
+ $changeStarted = false;
+ } else if ($remStarted) {
+ $handler->endElement('span');
+ $remStarted = false;
+ }
+ $this->parse($child);
+ } else if ($child instanceof TextNode) {
+ $mod = $child->modification;
+
+ if ($newStarted && ($mod->type != Modification::ADDED || $mod->firstOfID)) {
+ $handler->endElement('span');
+ $newStarted = false;
+ } else if ($changeStarted && ($mod->type != Modification::CHANGED
+ || $mod->changes != $changeTXT || $mod->firstOfID)) {
+ $handler->endElement('span');
+ $changeStarted = false;
+ } else if ($remStarted && ($mod->type != Modification::REMOVED || $mod ->firstOfID)) {
+ $handler->endElement('span');
+ $remStarted = false;
+ }
+
+ // no else because a removed part can just be closed and a new
+ // part can start
+ if (!$newStarted && $mod->type == Modification::ADDED) {
+ $attrs = array('class' => 'diff-html-added');
+ if ($mod->firstOfID) {
+ $attrs['id'] = "added-{$this->prefix}-{$mod->id}";
+ }
+ $handler->startElement('span', $attrs);
+ $newStarted = true;
+ } else if (!$changeStarted && $mod->type == Modification::CHANGED) {
+ $attrs = array('class' => 'diff-html-changed');
+ if ($mod->firstOfID) {
+ $attrs['id'] = "changed-{$this->prefix}-{$mod->id}";
+ }
+ $handler->startElement('span', $attrs);
+
+ //tooltip
+ $handler->startElement('span', array('class' => 'tip'));
+ $handler->html($mod->changes);
+ $handler->endElement('span');
+
+ $changeStarted = true;
+ $changeTXT = $mod->changes;
+ } else if (!$remStarted && $mod->type == Modification::REMOVED) {
+ $attrs = array('class'=>'diff-html-removed');
+ if ($mod->firstOfID) {
+ $attrs['id'] = "removed-{$this->prefix}-{$mod->id}";
+ }
+ $handler->startElement('span', $attrs);
+ $remStarted = true;
+ }
+
+ $chars = $child->text;
+
+ if ($child instanceof ImageNode) {
+ $this->writeImage($child);
+ } else {
+ $handler->characters($chars);
+ }
+ }
+ }
+
+ if ($newStarted) {
+ $handler->endElement('span');
+ $newStarted = false;
+ } else if ($changeStarted) {
+ $handler->endElement('span');
+ $changeStarted = false;
+ } else if ($remStarted) {
+ $handler->endElement('span');
+ $remStarted = false;
+ }
+
+ if (strcasecmp($node->qName, 'img') != 0
+ && strcasecmp($node->qName, 'body') != 0) {
+ $handler->endElement($node->qName);
+ }
+ }
+
+ private function writeImage(ImageNode $imgNode) {
+ $attrs = $imgNode->attributes;
+ $this->handler->startElement('img', $attrs);
+ $this->handler->endElement('img');
+ }
+}
+
+class DelegatingContentHandler {
+
+ private $delegate;
+
+ function __construct($delegate) {
+ $this->delegate = $delegate;
+ }
+
+ function startElement($qname, /*array*/ $arguments) {
+ $this->delegate->addHtml(Xml::openElement($qname, $arguments));
+ }
+
+ function endElement($qname){
+ $this->delegate->addHtml(Xml::closeElement($qname));
+ }
+
+ function characters($chars){
+ $this->delegate->addHtml(htmlspecialchars($chars));
+ }
+
+ function html($html){
+ $this->delegate->addHtml($html);
+ }
+}
diff --git a/includes/diff/Nodes.php b/includes/diff/Nodes.php
new file mode 100644
index 00000000..1b1363d4
--- /dev/null
+++ b/includes/diff/Nodes.php
@@ -0,0 +1,439 @@
+<?php
+
+/** Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu>
+ *
+ * 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.
+ * or see http://www.gnu.org/
+ *
+ */
+
+/**
+ * Any element in the DOM tree of an HTML document.
+ * @ingroup DifferenceEngine
+ */
+class Node {
+
+ public $parent;
+
+ protected $parentTree;
+
+ public $whiteBefore = false;
+
+ public $whiteAfter = false;
+
+ function __construct($parent) {
+ $this->parent = $parent;
+ }
+
+ public function getParentTree() {
+ if (!isset($this->parentTree)) {
+ if (!is_null($this->parent)) {
+ $this->parentTree = $this->parent->getParentTree();
+ $this->parentTree[] = $this->parent;
+ } else {
+ $this->parentTree = array();
+ }
+ }
+ return $this->parentTree;
+ }
+
+ public function getLastCommonParent(Node $other) {
+ $result = new LastCommonParentResult();
+
+ $myParents = $this->getParentTree();
+ $otherParents = $other->getParentTree();
+
+ $i = 1;
+ $isSame = true;
+ $nbMyParents = count($myParents);
+ $nbOtherParents = count($otherParents);
+ while ($isSame && $i < $nbMyParents && $i < $nbOtherParents) {
+ if (!$myParents[$i]->openingTag === $otherParents[$i]->openingTag) {
+ $isSame = false;
+ } else {
+ // After a while, the index i-1 must be the last common parent
+ $i++;
+ }
+ }
+
+ $result->lastCommonParentDepth = $i - 1;
+ $result->parent = $myParents[$i - 1];
+
+ if (!$isSame || $nbMyParents > $nbOtherParents) {
+ // Not all tags matched, or all tags matched but
+ // there are tags left in this tree
+ $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($myParents[$i]);
+ $result->splittingNeeded = true;
+ } else if ($nbMyParents <= $nbOtherParents) {
+ $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($this);
+ }
+ return $result;
+ }
+
+ public function setParent($parent) {
+ $this->parent = $parent;
+ unset($this->parentTree);
+ }
+
+ public function inPre() {
+ $tree = $this->getParentTree();
+ foreach ($tree as &$ancestor) {
+ if ($ancestor->isPre()) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+/**
+ * Node that can contain other nodes. Represents an HTML tag.
+ * @ingroup DifferenceEngine
+ */
+class TagNode extends Node {
+
+ public $children = array();
+
+ public $qName;
+
+ public $attributes = array();
+
+ public $openingTag;
+
+ function __construct($parent, $qName, /*array*/ $attributes) {
+ parent::__construct($parent);
+ $this->qName = strtolower($qName);
+ foreach($attributes as $key => &$value){
+ $this->attributes[strtolower($key)] = $value;
+ }
+ return $this->openingTag = Xml::openElement($this->qName, $this->attributes);
+ }
+
+ public function addChildAbsolute(Node $node, $index) {
+ array_splice($this->children, $index, 0, array($node));
+ }
+
+ public function getIndexOf(Node $child) {
+ // don't trust array_search with objects
+ foreach ($this->children as $key => &$value){
+ if ($value === $child) {
+ return $key;
+ }
+ }
+ return null;
+ }
+
+ public function getNbChildren() {
+ return count($this->children);
+ }
+
+ public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
+ $nodes = array();
+
+ $allDeleted = false;
+ $somethingDeleted = false;
+ $hasNonDeletedDescendant = false;
+
+ if (empty($this->children)) {
+ return $nodes;
+ }
+
+ foreach ($this->children as &$child) {
+ $allDeleted_local = false;
+ $somethingDeleted_local = false;
+ $childrenChildren = $child->getMinimalDeletedSet($id, $allDeleted_local, $somethingDeleted_local);
+ if ($somethingDeleted_local) {
+ $nodes = array_merge($nodes, $childrenChildren);
+ $somethingDeleted = true;
+ }
+ if (!$allDeleted_local) {
+ $hasNonDeletedDescendant = true;
+ }
+ }
+ if (!$hasNonDeletedDescendant) {
+ $nodes = array($this);
+ $allDeleted = true;
+ }
+ return $nodes;
+ }
+
+ public function splitUntil(TagNode $parent, Node $split, $includeLeft) {
+ $splitOccured = false;
+ if ($parent !== $this) {
+ $part1 = new TagNode(null, $this->qName, $this->attributes);
+ $part2 = new TagNode(null, $this->qName, $this->attributes);
+ $part1->setParent($this->parent);
+ $part2->setParent($this->parent);
+
+ $onSplit = false;
+ $pastSplit = false;
+ foreach ($this->children as &$child)
+ {
+ if ($child === $split) {
+ $onSplit = true;
+ }
+ if(!$pastSplit || ($onSplit && $includeLeft)) {
+ $child->setParent($part1);
+ $part1->children[] = $child;
+ } else {
+ $child->setParent($part2);
+ $part2->children[] = $child;
+ }
+ if ($onSplit) {
+ $onSplit = false;
+ $pastSplit = true;
+ }
+ }
+ $myindexinparent = $this->parent->getIndexOf($this);
+ if (!empty($part1->children)) {
+ $this->parent->addChildAbsolute($part1, $myindexinparent);
+ }
+ if (!empty($part2->children)) {
+ $this->parent->addChildAbsolute($part2, $myindexinparent);
+ }
+ if (!empty($part1->children) && !empty($part2->children)) {
+ $splitOccured = true;
+ }
+
+ $this->parent->removeChild($myindexinparent);
+
+ if ($includeLeft) {
+ $this->parent->splitUntil($parent, $part1, $includeLeft);
+ } else {
+ $this->parent->splitUntil($parent, $part2, $includeLeft);
+ }
+ }
+ return $splitOccured;
+
+ }
+
+ private function removeChild($index) {
+ unset($this->children[$index]);
+ $this->children = array_values($this->children);
+ }
+
+ public static $blocks = array('html', 'body','p','blockquote', 'h1',
+ 'h2', 'h3', 'h4', 'h5', 'pre', 'div', 'ul', 'ol', 'li', 'table',
+ 'tbody', 'tr', 'td', 'th', 'br');
+
+ public function copyTree() {
+ $newThis = new TagNode(null, $this->qName, $this->attributes);
+ $newThis->whiteBefore = $this->whiteBefore;
+ $newThis->whiteAfter = $this->whiteAfter;
+ foreach ($this->children as &$child) {
+ $newChild = $child->copyTree();
+ $newChild->setParent($newThis);
+ $newThis->children[] = $newChild;
+ }
+ return $newThis;
+ }
+
+ public function getMatchRatio(TagNode $other) {
+ $txtComp = new TextOnlyComparator($other);
+ return $txtComp->getMatchRatio(new TextOnlyComparator($this));
+ }
+
+ public function expandWhiteSpace() {
+ $shift = 0;
+ $spaceAdded = false;
+
+ $nbOriginalChildren = $this->getNbChildren();
+ for ($i = 0; $i < $nbOriginalChildren; ++$i) {
+ $child = $this->children[$i + $shift];
+
+ if ($child instanceof TagNode) {
+ if (!$child->isPre()) {
+ $child->expandWhiteSpace();
+ }
+ }
+ if (!$spaceAdded && $child->whiteBefore) {
+ $ws = new WhiteSpaceNode(null, ' ', $child->getLeftMostChild());
+ $ws->setParent($this);
+ $this->addChildAbsolute($ws,$i + ($shift++));
+ }
+ if ($child->whiteAfter) {
+ $ws = new WhiteSpaceNode(null, ' ', $child->getRightMostChild());
+ $ws->setParent($this);
+ $this->addChildAbsolute($ws,$i + 1 + ($shift++));
+ $spaceAdded = true;
+ } else {
+ $spaceAdded = false;
+ }
+
+ }
+ }
+
+ public function getLeftMostChild() {
+ if (empty($this->children)) {
+ return $this;
+ }
+ return $this->children[0]->getLeftMostChild();
+ }
+
+ public function getRightMostChild() {
+ if (empty($this->children)) {
+ return $this;
+ }
+ return $this->children[$this->getNbChildren() - 1]->getRightMostChild();
+ }
+
+ public function isPre() {
+ return 0 == strcasecmp($this->qName,'pre');
+ }
+
+ public static function toDiffLine(TagNode $node) {
+ return $node->openingTag;
+ }
+}
+
+/**
+ * Represents a piece of text in the HTML file.
+ * @ingroup DifferenceEngine
+ */
+class TextNode extends Node {
+
+ public $text;
+
+ public $modification;
+
+ function __construct($parent, $text) {
+ parent::__construct($parent);
+ $this->modification = new Modification(Modification::NONE);
+ $this->text = $text;
+ }
+
+ public function copyTree() {
+ $clone = clone $this;
+ $clone->setParent(null);
+ return $clone;
+ }
+
+ public function getLeftMostChild() {
+ return $this;
+ }
+
+ public function getRightMostChild() {
+ return $this;
+ }
+
+ public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
+ if ($this->modification->type == Modification::REMOVED
+ && $this->modification->id == $id){
+ $somethingDeleted = true;
+ $allDeleted = true;
+ return array($this);
+ }
+ return array();
+ }
+
+ public function isSameText($other) {
+ if (is_null($other) || ! $other instanceof TextNode) {
+ return false;
+ }
+ return str_replace('\n', ' ',$this->text) === str_replace('\n', ' ',$other->text);
+ }
+
+ public static function toDiffLine(TextNode $node) {
+ return str_replace('\n', ' ',$node->text);
+ }
+}
+
+/**
+ * @todo Document
+ * @ingroup DifferenceEngine
+ */
+class WhiteSpaceNode extends TextNode {
+
+ function __construct($parent, $s, Node $like = null) {
+ parent::__construct($parent, $s);
+ if(!is_null($like) && $like instanceof TextNode) {
+ $newModification = clone $like->modification;
+ $newModification->firstOfID = false;
+ $this->modification = $newModification;
+ }
+ }
+}
+
+/**
+ * Represents the root of a HTML document.
+ * @ingroup DifferenceEngine
+ */
+class BodyNode extends TagNode {
+
+ function __construct() {
+ parent::__construct(null, 'body', array());
+ }
+
+ public function copyTree() {
+ $newThis = new BodyNode();
+ foreach ($this->children as &$child) {
+ $newChild = $child->copyTree();
+ $newChild->setParent($newThis);
+ $newThis->children[] = $newChild;
+ }
+ return $newThis;
+ }
+
+ public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
+ $nodes = array();
+ foreach ($this->children as &$child) {
+ $childrenChildren = $child->getMinimalDeletedSet($id,
+ $allDeleted, $somethingDeleted);
+ $nodes = array_merge($nodes, $childrenChildren);
+ }
+ return $nodes;
+ }
+
+}
+
+/**
+ * Represents an image in HTML. Even though images do not contain any text they
+ * are independent visible objects on the page. They are logically a TextNode.
+ * @ingroup DifferenceEngine
+ */
+class ImageNode extends TextNode {
+
+ public $attributes;
+
+ function __construct(TagNode $parent, /*array*/ $attrs) {
+ if(!array_key_exists('src', $attrs)) {
+ HTMLDiffer::diffDebug( "Image without a source\n" );
+ parent::__construct($parent, '<img></img>');
+ }else{
+ parent::__construct($parent, '<img>' . strtolower($attrs['src']) . '</img>');
+ }
+ $this->attributes = $attrs;
+ }
+
+ public function isSameText($other) {
+ if (is_null($other) || ! $other instanceof ImageNode) {
+ return false;
+ }
+ return $this->text === $other->text;
+ }
+
+}
+
+/**
+ * No-op node
+ * @ingroup DifferenceEngine
+ */
+class DummyNode extends Node {
+
+ function __construct() {
+ // no op
+ }
+
+}
diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php
index 646256bb..3919cfbc 100644
--- a/includes/filerepo/ArchivedFile.php
+++ b/includes/filerepo/ArchivedFile.php
@@ -30,12 +30,9 @@ class ArchivedFile
/**#@-*/
function ArchivedFile( $title, $id=0, $key='' ) {
- if( !is_object($title) ) {
- throw new MWException( 'ArchivedFile constructor given bogus title.' );
- }
$this->id = -1;
- $this->title = $title;
- $this->name = $title->getDBkey();
+ $this->title = false;
+ $this->name = false;
$this->group = '';
$this->key = '';
$this->size = 0;
@@ -51,6 +48,20 @@ class ArchivedFile
$this->timestamp = NULL;
$this->deleted = 0;
$this->dataLoaded = false;
+
+ if( is_object($title) ) {
+ $this->title = $title;
+ $this->name = $title->getDBkey();
+ }
+
+ if ($id)
+ $this->id = $id;
+
+ if ($key)
+ $this->key = $key;
+
+ if (!$id && !$key && !is_object($title))
+ throw new MWException( "No specifications provided to ArchivedFile constructor." );
}
/**
@@ -61,8 +72,19 @@ class ArchivedFile
if ( $this->dataLoaded ) {
return true;
}
- $conds = ($this->id) ? "fa_id = {$this->id}" : "fa_storage_key = '{$this->key}'";
- if( $this->title->getNamespace() == NS_IMAGE ) {
+ $conds = array();
+
+ if ($this->id>0)
+ $conds['fa_id'] = $this->id;
+ if ($this->key)
+ $conds['fa_storage_key'] = $this->key;
+ if ($this->title)
+ $conds['fa_name'] = $this->title->getDBkey();
+
+ if (!count($conds))
+ throw new MWException( "No specific information for retrieving archived file" );
+
+ if( !$this->title || $this->title->getNamespace() == NS_FILE ) {
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'filearchive',
array(
@@ -84,9 +106,7 @@ class ArchivedFile
'fa_user_text',
'fa_timestamp',
'fa_deleted' ),
- array(
- 'fa_name' => $this->title->getDBkey(),
- $conds ),
+ $conds,
__METHOD__,
array( 'ORDER BY' => 'fa_timestamp DESC' ) );
@@ -129,7 +149,7 @@ class ArchivedFile
* @return ResultWrapper
*/
public static function newFromRow( $row ) {
- $file = new ArchivedFile( Title::makeTitle( NS_IMAGE, $row->fa_name ) );
+ $file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) );
$file->id = intval($row->fa_id);
$file->name = $row->fa_name;
@@ -251,7 +271,7 @@ class ArchivedFile
*/
public function getTimestamp() {
$this->load();
- return $this->timestamp;
+ return wfTimestamp( TS_MW, $this->timestamp );
}
/**
diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php
index eb8df0f5..d561e61b 100644
--- a/includes/filerepo/FSRepo.php
+++ b/includes/filerepo/FSRepo.php
@@ -6,7 +6,7 @@
* @ingroup FileRepo
*/
class FSRepo extends FileRepo {
- var $directory, $deletedDir, $url, $hashLevels, $deletedHashLevels;
+ var $directory, $deletedDir, $url, $deletedHashLevels;
var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
var $oldFileFactory = false;
var $pathDisclosureProtection = 'simple';
@@ -452,14 +452,6 @@ class FSRepo extends FileRepo {
}
/**
- * 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
*/
diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php
index 64b48e0a..4f0990af 100644
--- a/includes/filerepo/File.php
+++ b/includes/filerepo/File.php
@@ -264,7 +264,14 @@ abstract class File {
* Overridden by LocalFile, UnregisteredLocalFile
* STUB
*/
- function getMetadata() { return false; }
+ public function getMetadata() { return false; }
+
+ /**
+ * Return the bit depth of the file
+ * Overridden by LocalFile
+ * STUB
+ */
+ public function getBitDepth() { return 0; }
/**
* Return the size of the image file, in bytes
@@ -499,8 +506,7 @@ abstract class File {
*
* @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
+ * @param boolean $render Deprecated
*
* @return ThumbnailImage or null on failure
*
@@ -511,8 +517,7 @@ abstract class File {
if ( $height != -1 ) {
$params['height'] = $height;
}
- $flags = $render ? self::RENDER_NOW : 0;
- return $this->transform( $params, $flags );
+ return $this->transform( $params, 0 );
}
/**
@@ -575,7 +580,7 @@ abstract class File {
// Purge. Useful in the event of Core -> Squid connection failure or squid
// purge collisions from elsewhere during failure. Don't keep triggering for
// "thumbs" which have the main image URL though (bug 13776)
- if ( $wgUseSquid && ($thumb->isError() || $thumb->getUrl() != $this->getURL()) ) {
+ if ( $wgUseSquid && ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL()) ) {
SquidUpdate::purge( array( $thumbUrl ) );
}
} while (false);
@@ -678,8 +683,9 @@ abstract class File {
* @param $limit integer Limit of rows to return
* @param $start timestamp Only revisions older than $start will be returned
* @param $end timestamp Only revisions newer than $end will be returned
+ * @param $inc bool Include the endpoints of the time range
*/
- function getHistory($limit = null, $start = null, $end = null) {
+ function getHistory($limit = null, $start = null, $end = null, $inc=true) {
return array();
}
@@ -1212,7 +1218,7 @@ abstract class File {
if ( $handler ) {
return $handler->getLongDesc( $this );
} else {
- return MediaHandler::getLongDesc( $this );
+ return MediaHandler::getGeneralLongDesc( $this );
}
}
@@ -1221,7 +1227,7 @@ abstract class File {
if ( $handler ) {
return $handler->getShortDesc( $this );
} else {
- return MediaHandler::getShortDesc( $this );
+ return MediaHandler::getGeneralShortDesc( $this );
}
}
@@ -1241,7 +1247,7 @@ abstract class File {
function getRedirectedTitle() {
if ( $this->redirected ) {
if ( !$this->redirectTitle )
- $this->redirectTitle = Title::makeTitle( NS_IMAGE, $this->redirected );
+ $this->redirectTitle = Title::makeTitle( NS_FILE, $this->redirected );
return $this->redirectTitle;
}
}
diff --git a/includes/filerepo/FileCache.php b/includes/filerepo/FileCache.php
new file mode 100644
index 00000000..7840d1a3
--- /dev/null
+++ b/includes/filerepo/FileCache.php
@@ -0,0 +1,156 @@
+<?php
+/**
+ * Cache of file objects, wrapping some RepoGroup functions to avoid redundant
+ * queries. Loosely inspired by the LinkCache / LinkBatch classes for titles.
+ *
+ * ISSUE: Merge with RepoGroup?
+ *
+ * @ingroup FileRepo
+ */
+class FileCache {
+ var $repoGroup;
+ var $cache = array(), $notFound = array();
+
+ protected static $instance;
+
+ /**
+ * Get a FileCache instance. Typically, only one instance of FileCache
+ * is needed in a MediaWiki invocation.
+ */
+ static function singleton() {
+ if ( self::$instance ) {
+ return self::$instance;
+ }
+ self::$instance = new FileCache( RepoGroup::singleton() );
+ 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;
+ }
+
+ /**
+ * Set the singleton instance to a given object
+ */
+ static function setSingleton( $instance ) {
+ self::$instance = $instance;
+ }
+
+ /**
+ * Construct a group of file repositories.
+ * @param RepoGroup $repoGroup
+ */
+ function __construct( $repoGroup ) {
+ $this->repoGroup = $repoGroup;
+ }
+
+
+ /**
+ * Add some files to the cache. This is a fairly low-level function,
+ * which most users should not need to call. Note that any existing
+ * entries for the same keys will not be replaced. Call clearFiles()
+ * first if you need that.
+ * @param array $files array of File objects, indexed by DB key
+ */
+ function addFiles( $files ) {
+ wfDebug( "FileCache adding ".count( $files )." files\n" );
+ $this->cache += $files;
+ }
+
+ /**
+ * Remove some files from the cache, so that their existence will be
+ * rechecked. This is a fairly low-level function, which most users
+ * should not need to call.
+ * @param array $remove array indexed by DB keys to remove (the values are ignored)
+ */
+ function clearFiles( $remove ) {
+ wfDebug( "FileCache clearing data for ".count( $remove )." files\n" );
+ $this->cache = array_diff_keys( $this->cache, $remove );
+ $this->notFound = array_diff_keys( $this->notFound, $remove );
+ }
+
+ /**
+ * Mark some DB keys as nonexistent. This is a fairly low-level
+ * function, which most users should not need to call.
+ * @param array $dbkeys array of DB keys
+ */
+ function markNotFound( $dbkeys ) {
+ wfDebug( "FileCache marking ".count( $dbkeys )." files as not found\n" );
+ $this->notFound += array_fill_keys( $dbkeys, true );
+ }
+
+
+ /**
+ * Search the cache for a file.
+ * @param mixed $title Title object or string
+ * @return File object or false if it is not found
+ * @todo Implement searching for old file versions(?)
+ */
+ function findFile( $title ) {
+ if( !( $title instanceof Title ) ) {
+ $title = Title::makeTitleSafe( NS_FILE, $title );
+ }
+ if( !$title ) {
+ return false; // invalid title?
+ }
+
+ $dbkey = $title->getDBkey();
+ if( array_key_exists( $dbkey, $this->cache ) ) {
+ wfDebug( "FileCache HIT for $dbkey\n" );
+ return $this->cache[$dbkey];
+ }
+ if( array_key_exists( $dbkey, $this->notFound ) ) {
+ wfDebug( "FileCache negative HIT for $dbkey\n" );
+ return false;
+ }
+
+ // Not in cache, fall back to a direct query
+ $file = $this->repoGroup->findFile( $title );
+ if( $file ) {
+ wfDebug( "FileCache MISS for $dbkey\n" );
+ $this->cache[$dbkey] = $file;
+ } else {
+ wfDebug( "FileCache negative MISS for $dbkey\n" );
+ $this->notFound[$dbkey] = true;
+ }
+ return $file;
+ }
+
+ /**
+ * Search the cache for multiple files.
+ * @param array $titles Title objects or strings to search for
+ * @return array of File objects, indexed by DB key
+ */
+ function findFiles( $titles ) {
+ $titleObjs = array();
+ foreach ( $titles as $title ) {
+ if ( !( $title instanceof Title ) ) {
+ $title = Title::makeTitleSafe( NS_FILE, $title );
+ }
+ if ( $title ) {
+ $titleObjs[$title->getDBkey()] = $title;
+ }
+ }
+
+ $result = array_intersect_key( $this->cache, $titleObjs );
+
+ $unsure = array_diff_key( $titleObjs, $result, $this->notFound );
+ if( $unsure ) {
+ wfDebug( "FileCache MISS for ".count( $unsure )." files out of ".count( $titleObjs )."...\n" );
+ // XXX: We assume the array returned by findFiles() is
+ // indexed by DBkey; this appears to be true, but should
+ // be explicitly documented.
+ $found = $this->repoGroup->findFiles( $unsure );
+ $result += $found;
+ $this->addFiles( $found );
+ $this->markNotFound( array_keys( array_diff_key( $unsure, $found ) ) );
+ }
+
+ wfDebug( "FileCache found ".count( $result )." files out of ".count( $titleObjs )."\n" );
+ return $result;
+ }
+}
diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php
index edfc2a99..5beac732 100644
--- a/includes/filerepo/FileRepo.php
+++ b/includes/filerepo/FileRepo.php
@@ -15,6 +15,7 @@ abstract class FileRepo {
var $thumbScriptUrl, $transformVia404;
var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital;
var $pathDisclosureProtection = 'paranoid';
+ var $descriptionCacheExpiry, $apiThumbCacheExpiry, $hashLevels;
/**
* Factory functions for creating new files
@@ -30,7 +31,8 @@ abstract class FileRepo {
// Optional settings
$this->initialCapital = true; // by default
foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
- 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection', 'descriptionCacheExpiry' ) as $var )
+ 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection',
+ 'descriptionCacheExpiry', 'apiThumbCacheExpiry', 'hashLevels' ) as $var )
{
if ( isset( $info[$var] ) ) {
$this->$var = $info[$var];
@@ -57,7 +59,7 @@ abstract class FileRepo {
*/
function newFile( $title, $time = false ) {
if ( !($title instanceof Title) ) {
- $title = Title::makeTitleSafe( NS_IMAGE, $title );
+ $title = Title::makeTitleSafe( NS_FILE, $title );
if ( !is_object( $title ) ) {
return null;
}
@@ -83,7 +85,7 @@ abstract class FileRepo {
*/
function findFile( $title, $time = false, $flags = 0 ) {
if ( !($title instanceof Title) ) {
- $title = Title::makeTitleSafe( NS_IMAGE, $title );
+ $title = Title::makeTitleSafe( NS_FILE, $title );
if ( !is_object( $title ) ) {
return false;
}
@@ -99,7 +101,7 @@ abstract class FileRepo {
# Now try an old version of the file
if ( $time !== false ) {
$img = $this->newFile( $title, $time );
- if ( $img->exists() ) {
+ if ( $img && $img->exists() ) {
if ( !$img->isDeleted(File::DELETED_FILE) ) {
return $img;
} else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) {
@@ -113,7 +115,7 @@ abstract class FileRepo {
return false;
}
$redir = $this->checkRedirect( $title );
- if( $redir && $redir->getNamespace() == NS_IMAGE) {
+ if( $redir && $redir->getNamespace() == NS_FILE) {
$img = $this->newFile( $redir );
if( !$img ) {
return false;
@@ -129,12 +131,12 @@ abstract class FileRepo {
/*
* Find many files at once.
* @param array $titles, an array of titles
- * @param int $flags
+ * @todo Think of a good way to optionally pass timestamps to this function.
*/
- function findFiles( $titles, $flags ) {
+ function findFiles( $titles ) {
$result = array();
foreach ( $titles as $index => $title ) {
- $file = $this->findFile( $title, $flags );
+ $file = $this->findFile( $title );
if ( $file )
$result[$file->getTitle()->getDBkey()] = $file;
}
@@ -236,6 +238,14 @@ abstract class FileRepo {
return $path;
}
}
+
+ /**
+ * 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 self::getHashPathForLevel( $name, $this->hashLevels );
+ }
/**
* Get the name of this repository, as specified by $info['name]' to the constructor
@@ -245,25 +255,6 @@ abstract class FileRepo {
}
/**
- * 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( MWNamespace::getCanonicalName( NS_IMAGE ) ) . ':', $this->articleUrl );
- } elseif ( !is_null( $this->scriptDirUrl ) ) {
- $this->descBaseUrl = $this->scriptDirUrl . '/index.php?title=' .
- wfUrlencode( MWNamespace::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
@@ -273,12 +264,29 @@ abstract class FileRepo {
* constructor, whereas local repositories use the local Title functions.
*/
function getDescriptionUrl( $name ) {
- $base = $this->getDescBaseUrl();
- if ( $base ) {
- return $base . wfUrlencode( $name );
- } else {
- return false;
+ $encName = wfUrlencode( $name );
+ if ( !is_null( $this->descBaseUrl ) ) {
+ # "http://example.com/wiki/Image:"
+ return $this->descBaseUrl . $encName;
+ }
+ if ( !is_null( $this->articleUrl ) ) {
+ # "http://example.com/wiki/$1"
+ #
+ # We use "Image:" as the canonical namespace for
+ # compatibility across all MediaWiki versions.
+ return str_replace( '$1',
+ "Image:$encName", $this->articleUrl );
}
+ if ( !is_null( $this->scriptDirUrl ) ) {
+ # "http://example.com/w"
+ #
+ # We use "Image:" as the canonical namespace for
+ # compatibility across all MediaWiki versions,
+ # and just sort of hope index.php is right. ;)
+ return $this->scriptDirUrl .
+ "/index.php?title=Image:$encName";
+ }
+ return false;
}
/**
@@ -290,12 +298,12 @@ abstract class FileRepo {
function getDescriptionRenderUrl( $name ) {
if ( isset( $this->scriptDirUrl ) ) {
return $this->scriptDirUrl . '/index.php?title=' .
- wfUrlencode( MWNamespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) .
+ wfUrlencode( 'Image:' . $name ) .
'&action=render';
} else {
- $descBase = $this->getDescBaseUrl();
- if ( $descBase ) {
- return wfAppendQuery( $descBase . wfUrlencode( $name ), 'action=render' );
+ $descUrl = $this->getDescriptionUrl( $name );
+ if ( $descUrl ) {
+ return wfAppendQuery( $descUrl, 'action=render' );
} else {
return false;
}
diff --git a/includes/filerepo/ForeignAPIFile.php b/includes/filerepo/ForeignAPIFile.php
index aaf92204..d9fb85d0 100644
--- a/includes/filerepo/ForeignAPIFile.php
+++ b/includes/filerepo/ForeignAPIFile.php
@@ -7,15 +7,19 @@
* @ingroup FileRepo
*/
class ForeignAPIFile extends File {
- function __construct( $title, $repo, $info ) {
+
+ private $mExists;
+
+ function __construct( $title, $repo, $info, $exists = false ) {
parent::__construct( $title, $repo );
$this->mInfo = $info;
+ $this->mExists = $exists;
}
static function newFromTitle( $title, $repo ) {
$info = $repo->getImageInfo( $title );
if( $info ) {
- return new ForeignAPIFile( $title, $repo, $info );
+ return new ForeignAPIFile( $title, $repo, $info, true );
} else {
return null;
}
@@ -23,7 +27,7 @@ class ForeignAPIFile extends File {
// Dummy functions...
public function exists() {
- return true;
+ return $this->mExists;
}
public function getPath() {
@@ -31,12 +35,15 @@ class ForeignAPIFile extends File {
}
function transform( $params, $flags = 0 ) {
- $thumbUrl = $this->repo->getThumbUrl(
- $this->getName(),
- isset( $params['width'] ) ? $params['width'] : -1,
- isset( $params['height'] ) ? $params['height'] : -1 );
+ if( !$this->canRender() ) {
+ // show icon
+ return parent::transform( $params, $flags );
+ }
+ $thumbUrl = $this->repo->getThumbUrlFromCache(
+ $this->getName(),
+ isset( $params['width'] ) ? $params['width'] : -1,
+ isset( $params['height'] ) ? $params['height'] : -1 );
if( $thumbUrl ) {
- wfDebug( __METHOD__ . " got remote thumb $thumbUrl\n" );
return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );;
}
return false;
@@ -98,4 +105,64 @@ class ForeignAPIFile extends File {
? $this->mInfo['descriptionurl']
: false;
}
+
+ /**
+ * Only useful if we're locally caching thumbs anyway...
+ */
+ function getThumbPath( $suffix = '' ) {
+ if ( $this->repo->canCacheThumbs() ) {
+ global $wgUploadDirectory;
+ $path = $wgUploadDirectory . '/thumb/' . $this->getHashPath( $this->getName() );
+ if ( $suffix ) {
+ $path = $path . $suffix . '/';
+ }
+ return $path;
+ }
+ else {
+ return null;
+ }
+ }
+
+ function getThumbnails() {
+ $files = array();
+ $dir = $this->getThumbPath( $this->getName() );
+ if ( is_dir( $dir ) ) {
+ $handle = opendir( $dir );
+ if ( $handle ) {
+ while ( false !== ( $file = readdir($handle) ) ) {
+ if ( $file{0} != '.' ) {
+ $files[] = $file;
+ }
+ }
+ closedir( $handle );
+ }
+ }
+ return $files;
+ }
+
+ function purgeCache() {
+ $this->purgeThumbnails();
+ $this->purgeDescriptionPage();
+ }
+
+ function purgeDescriptionPage() {
+ global $wgMemc;
+ $url = $this->repo->getDescriptionRenderUrl( $this->getName() );
+ $key = wfMemcKey( 'RemoteFileDescription', 'url', md5($url) );
+ $wgMemc->delete( $key );
+ }
+
+ function purgeThumbnails() {
+ global $wgMemc;
+ $key = wfMemcKey( 'ForeignAPIRepo', 'ThumbUrl', $this->getName() );
+ $wgMemc->delete( $key );
+ $files = $this->getThumbnails();
+ $dir = $this->getThumbPath( $this->getName() );
+ foreach ( $files as $file ) {
+ unlink( $dir . $file );
+ }
+ if ( is_dir( $dir ) ) {
+ rmdir( $dir ); // Might have already gone away, spews errors if we don't.
+ }
+ }
}
diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php
index 0dee699f..6fc9c465 100644
--- a/includes/filerepo/ForeignAPIRepo.php
+++ b/includes/filerepo/ForeignAPIRepo.php
@@ -19,6 +19,7 @@
*/
class ForeignAPIRepo extends FileRepo {
var $fileFactory = array( 'ForeignAPIFile', 'newFromTitle' );
+ var $apiThumbCacheExpiry = 0;
protected $mQueryCache = array();
function __construct( $info ) {
@@ -30,10 +31,12 @@ class ForeignAPIRepo extends FileRepo {
}
}
+/**
+ * No-ops
+ */
function storeBatch( $triplets, $flags = 0 ) {
return false;
}
-
function storeTemp( $originalName, $srcPath ) {
return false;
}
@@ -69,14 +72,16 @@ class ForeignAPIRepo extends FileRepo {
array_merge( $query,
array(
'format' => 'json',
- 'action' => 'query',
- 'prop' => 'imageinfo' ) ) );
+ 'action' => 'query' ) ) );
if( !isset( $this->mQueryCache[$url] ) ) {
- $key = wfMemcKey( 'ForeignAPIRepo', $url );
+ $key = wfMemcKey( 'ForeignAPIRepo', 'Metadata', md5( $url ) );
$data = $wgMemc->get( $key );
if( !$data ) {
$data = Http::get( $url );
+ if ( !$data ) {
+ return null;
+ }
$wgMemc->set( $key, $data, 3600 );
}
@@ -92,7 +97,22 @@ class ForeignAPIRepo extends FileRepo {
function getImageInfo( $title, $time = false ) {
return $this->queryImage( array(
'titles' => 'Image:' . $title->getText(),
- 'iiprop' => 'timestamp|user|comment|url|size|sha1|metadata|mime' ) );
+ 'iiprop' => 'timestamp|user|comment|url|size|sha1|metadata|mime',
+ 'prop' => 'imageinfo' ) );
+ }
+
+ function findBySha1( $hash ) {
+ $results = $this->fetchImageQuery( array(
+ 'aisha1base36' => $hash,
+ 'aiprop' => 'timestamp|user|comment|url|size|sha1|metadata|mime',
+ 'list' => 'allimages', ) );
+ $ret = array();
+ if ( isset( $results['query']['allimages'] ) ) {
+ foreach ( $results['query']['allimages'] as $img ) {
+ $ret[] = new ForeignAPIFile( Title::makeTitle( NS_IMAGE, $img['name'] ), $this, $img );
+ }
+ }
+ return $ret;
}
function getThumbUrl( $name, $width=-1, $height=-1 ) {
@@ -100,11 +120,56 @@ class ForeignAPIRepo extends FileRepo {
'titles' => 'Image:' . $name,
'iiprop' => 'url',
'iiurlwidth' => $width,
- 'iiurlheight' => $height ) );
+ 'iiurlheight' => $height,
+ 'prop' => 'imageinfo' ) );
if( $info ) {
+ wfDebug( __METHOD__ . " got remote thumb " . $info['thumburl'] . "\n" );
return $info['thumburl'];
} else {
return false;
}
}
+
+ function getThumbUrlFromCache( $name, $width, $height ) {
+ global $wgMemc, $wgUploadPath, $wgServer, $wgUploadDirectory;
+
+ if ( !$this->canCacheThumbs() ) {
+ return $this->getThumbUrl( $name, $width, $height );
+ }
+
+ $key = wfMemcKey( 'ForeignAPIRepo', 'ThumbUrl', $name );
+ if ( $thumbUrl = $wgMemc->get($key) ) {
+ wfDebug("Got thumb from local cache. $thumbUrl \n");
+ return $thumbUrl;
+ }
+ else {
+ $foreignUrl = $this->getThumbUrl( $name, $width, $height );
+
+ // We need the same filename as the remote one :)
+ $fileName = ltrim( substr( $foreignUrl, strrpos( $foreignUrl, '/' ) ), '/' );
+ $path = 'thumb/' . $this->getHashPath( $name ) . $name . "/";
+ if ( !is_dir($wgUploadDirectory . '/' . $path) ) {
+ wfMkdirParents($wgUploadDirectory . '/' . $path);
+ }
+ if ( !is_writable( $wgUploadDirectory . '/' . $path . $fileName ) ) {
+ wfDebug( __METHOD__ . " could not write to thumb path\n" );
+ return $foreignUrl;
+ }
+ $localUrl = $wgServer . $wgUploadPath . '/' . $path . $fileName;
+ $thumb = Http::get( $foreignUrl );
+ # FIXME: Delete old thumbs that aren't being used. Maintenance script?
+ file_put_contents($wgUploadDirectory . '/' . $path . $fileName, $thumb );
+ $wgMemc->set( $key, $localUrl, $this->apiThumbCacheExpiry );
+ wfDebug( __METHOD__ . " got local thumb $localUrl, saving to cache \n" );
+ return $localUrl;
+ }
+ }
+
+ /**
+ * Are we locally caching the thumbnails?
+ * @return bool
+ */
+ public function canCacheThumbs() {
+ return ( $this->apiThumbCacheExpiry > 0 );
+ }
}
diff --git a/includes/filerepo/ForeignDBFile.php b/includes/filerepo/ForeignDBFile.php
index eed26048..5fb432c8 100644
--- a/includes/filerepo/ForeignDBFile.php
+++ b/includes/filerepo/ForeignDBFile.php
@@ -13,7 +13,7 @@ class ForeignDBFile extends LocalFile {
* Do not call this except from inside a repo class.
*/
static function newFromRow( $row, $repo ) {
- $title = Title::makeTitle( NS_IMAGE, $row->img_name );
+ $title = Title::makeTitle( NS_FILE, $row->img_name );
$file = new self( $title, $repo );
$file->loadFromRow( $row );
return $file;
diff --git a/includes/filerepo/Image.php b/includes/filerepo/Image.php
index 665dd4bf..5207bb4b 100644
--- a/includes/filerepo/Image.php
+++ b/includes/filerepo/Image.php
@@ -36,7 +36,7 @@ class Image extends LocalFile {
*/
static function newFromName( $name ) {
wfDeprecated( __METHOD__ );
- $title = Title::makeTitleSafe( NS_IMAGE, $name );
+ $title = Title::makeTitleSafe( NS_FILE, $name );
if ( is_object( $title ) ) {
$img = wfFindFile( $title );
if ( !$img ) {
diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php
index 57c0703d..6fd6de72 100644
--- a/includes/filerepo/LocalFile.php
+++ b/includes/filerepo/LocalFile.php
@@ -68,7 +68,7 @@ class LocalFile extends File
* Do not call this except from inside a repo class.
*/
static function newFromRow( $row, $repo ) {
- $title = Title::makeTitle( NS_IMAGE, $row->img_name );
+ $title = Title::makeTitle( NS_FILE, $row->img_name );
$file = new self( $title, $repo );
$file->loadFromRow( $row );
return $file;
@@ -453,6 +453,11 @@ class LocalFile extends File
return $this->metadata;
}
+ function getBitDepth() {
+ $this->load();
+ return $this->bits;
+ }
+
/**
* Return the size of the image file, in bytes
* @public
@@ -619,31 +624,38 @@ class LocalFile extends File
/** purgeDescription inherited */
/** purgeEverything inherited */
- function getHistory($limit = null, $start = null, $end = null) {
+ function getHistory($limit = null, $start = null, $end = null, $inc = true) {
$dbr = $this->repo->getSlaveDB();
$tables = array('oldimage');
- $join_conds = array();
$fields = OldLocalFile::selectFields();
- $conds = $opts = array();
+ $conds = $opts = $join_conds = array();
+ $eq = $inc ? "=" : "";
$conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBKey() );
- if( $start !== null ) {
- $conds[] = "oi_timestamp <= " . $dbr->addQuotes( $dbr->timestamp( $start ) );
+ if( $start ) {
+ $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
}
- if( $end !== null ) {
- $conds[] = "oi_timestamp >= " . $dbr->addQuotes( $dbr->timestamp( $end ) );
+ if( $end ) {
+ $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
}
if( $limit ) {
$opts['LIMIT'] = $limit;
}
- $opts['ORDER BY'] = 'oi_timestamp DESC';
+ // Search backwards for time > x queries
+ $order = (!$start && $end !== null) ? "ASC" : "DESC";
+ $opts['ORDER BY'] = "oi_timestamp $order";
+ $opts['USE INDEX'] = array('oldimage' => 'oi_name_timestamp');
- wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields, &$conds, &$opts, &$join_conds ) );
+ wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
+ &$conds, &$opts, &$join_conds ) );
$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
$r = array();
while( $row = $dbr->fetchObject($res) ) {
$r[] = OldLocalFile::newFromRow($row, $this->repo);
}
+ if( $order == "ASC" ) {
+ $r = array_reverse( $r ); // make sure it ends up descending
+ }
return $r;
}
@@ -732,11 +744,11 @@ class LocalFile extends File
* @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 ) {
+ function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) {
$this->lock();
$status = $this->publish( $srcPath, $flags );
if ( $status->ok ) {
- if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp ) ) {
+ if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
$status->fatal( 'filenotfound', $srcPath );
}
}
@@ -766,18 +778,22 @@ class LocalFile extends File
/**
* Record a file upload in the upload log and the image table
*/
- function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false )
+ function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null )
{
- global $wgUser;
+ if( is_null( $user ) ) {
+ global $wgUser;
+ $user = $wgUser;
+ }
$dbw = $this->repo->getMasterDB();
+ $dbw->begin();
if ( !$props ) {
$props = $this->repo->getFileProps( $this->getVirtualUrl() );
}
$props['description'] = $comment;
- $props['user'] = $wgUser->getId();
- $props['user_text'] = $wgUser->getName();
+ $props['user'] = $user->getId();
+ $props['user_text'] = $user->getName();
$props['timestamp'] = wfTimestamp( TS_MW );
$this->setProps( $props );
@@ -812,8 +828,8 @@ class LocalFile extends File
'img_minor_mime' => $this->minor_mime,
'img_timestamp' => $timestamp,
'img_description' => $comment,
- 'img_user' => $wgUser->getId(),
- 'img_user_text' => $wgUser->getName(),
+ 'img_user' => $user->getId(),
+ 'img_user_text' => $user->getName(),
'img_metadata' => $this->metadata,
'img_sha1' => $this->sha1
),
@@ -858,8 +874,8 @@ class LocalFile extends File
'img_minor_mime' => $this->minor_mime,
'img_timestamp' => $timestamp,
'img_description' => $comment,
- 'img_user' => $wgUser->getId(),
- 'img_user_text' => $wgUser->getName(),
+ 'img_user' => $user->getId(),
+ 'img_user_text' => $user->getName(),
'img_metadata' => $this->metadata,
'img_sha1' => $this->sha1
), array( /* WHERE */
@@ -874,19 +890,22 @@ class LocalFile extends File
}
$descTitle = $this->getTitle();
- $article = new Article( $descTitle );
+ $article = new ImagePage( $descTitle );
+ $article->setFile( $this );
# Add the log entry
$log = new LogPage( 'upload' );
$action = $reupload ? 'overwrite' : 'upload';
- $log->addEntry( $action, $descTitle, $comment );
+ $log->addEntry( $action, $descTitle, $comment, array(), $user );
if( $descTitle->exists() ) {
# Create a null revision
- $nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(), $log->getRcComment(), false );
+ $latest = $descTitle->getLatestRevID();
+ $nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(),
+ $log->getRcComment(), false );
$nullRevision->insertOn( $dbw );
- wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $user) );
$article->updateRevisionOn( $dbw, $nullRevision );
# Invalidate the cache for the description page
@@ -1109,8 +1128,8 @@ class LocalFile extends File
if ( !$revision ) return false;
$text = $revision->getText();
if ( !$text ) return false;
- $html = $wgParser->parse( $text, new ParserOptions );
- return $html;
+ $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
+ return $pout->getText();
}
function getDescription() {
@@ -1128,7 +1147,7 @@ class LocalFile extends File
// Initialise now if necessary
if ( $this->sha1 == '' && $this->fileExists ) {
$this->sha1 = File::sha1Base36( $this->getPath() );
- if ( strval( $this->sha1 ) != '' ) {
+ if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
$dbw = $this->repo->getMasterDB();
$dbw->update( 'image',
array( 'img_sha1' => $this->sha1 ),
@@ -1355,7 +1374,7 @@ class LocalFileDeleteBatch {
$dbw->delete( 'oldimage',
array(
'oi_name' => $this->file->getName(),
- 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')'
+ 'oi_archive_name' => array_keys( $oldRels )
), __METHOD__ );
}
if ( $deleteCurrent ) {
@@ -1509,7 +1528,8 @@ class LocalFileRestoreBatch {
$result = $dbw->select( 'filearchive', '*',
$conditions,
__METHOD__,
- array( 'ORDER BY' => 'fa_timestamp DESC' ) );
+ array( 'ORDER BY' => 'fa_timestamp DESC' )
+ );
$idsPresent = array();
$storeBatch = array();
@@ -1554,15 +1574,11 @@ class LocalFileRestoreBatch {
'minor_mime' => $row->fa_minor_mime,
'major_mime' => $row->fa_major_mime,
'media_type' => $row->fa_media_type,
- 'metadata' => $row->fa_metadata );
+ 'metadata' => $row->fa_metadata
+ );
}
if ( $first && !$exists ) {
- // The live (current) version cannot be hidden!
- if( !$this->unsuppress && $row->fa_deleted ) {
- $this->file->unlock();
- return $status;
- }
// This revision will be published as the new current version
$destRel = $this->file->getRel();
$insertCurrent = array(
@@ -1579,7 +1595,13 @@ class LocalFileRestoreBatch {
'img_user' => $row->fa_user,
'img_user_text' => $row->fa_user_text,
'img_timestamp' => $row->fa_timestamp,
- 'img_sha1' => $sha1);
+ 'img_sha1' => $sha1
+ );
+ // The live (current) version cannot be hidden!
+ if( !$this->unsuppress && $row->fa_deleted ) {
+ $storeBatch[] = array( $deletedUrl, 'public', $destRel );
+ $this->cleanupBatch[] = $row->fa_storage_key;
+ }
} else {
$archiveName = $row->fa_archive_name;
if( $archiveName == '' ) {
@@ -1616,6 +1638,7 @@ class LocalFileRestoreBatch {
$deleteIds[] = $row->fa_id;
if( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
// private files can stay where they are
+ $status->successCount++;
} else {
$storeBatch[] = array( $deletedUrl, 'public', $destRel );
$this->cleanupBatch[] = $row->fa_storage_key;
@@ -1705,7 +1728,7 @@ class LocalFileMoveBatch {
$this->file = $file;
$this->target = $target;
$this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
- $this->newHash = $this->file->repo->getHashPath( $this->target->getDbKey() );
+ $this->newHash = $this->file->repo->getHashPath( $this->target->getDBKey() );
$this->oldName = $this->file->getName();
$this->newName = $this->file->repo->getNameFromTitle( $this->target );
$this->oldRel = $this->oldHash . $this->oldName;
@@ -1751,7 +1774,7 @@ class LocalFileMoveBatch {
continue;
}
$this->olds[] = array(
- "{$archiveBase}/{$this->oldHash}{$oldname}",
+ "{$archiveBase}/{$this->oldHash}{$oldName}",
"{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
);
}
diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php
index 90b198c8..5eb1a11c 100644
--- a/includes/filerepo/LocalRepo.php
+++ b/includes/filerepo/LocalRepo.php
@@ -94,7 +94,7 @@ class LocalRepo extends FSRepo {
'page_id', //Field
array( //Conditions
'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDbKey(),
+ 'page_title' => $title->getDBKey(),
),
__METHOD__ //Function name
);
@@ -108,7 +108,7 @@ class LocalRepo extends FSRepo {
$title = Title::newFromTitle( $title );
}
if( $title instanceof Title && $title->getNamespace() == NS_MEDIA ) {
- $title = Title::makeTitle( NS_IMAGE, $title->getText() );
+ $title = Title::makeTitle( NS_FILE, $title->getText() );
}
$memcKey = $this->getMemcKey( "image_redirect:" . md5( $title->getPrefixedDBkey() ) );
@@ -164,8 +164,7 @@ class LocalRepo extends FSRepo {
/*
* Find many files using one query
*/
- function findFiles( $titles, $flags ) {
- // FIXME: Comply with $flags
+ function findFiles( $titles ) {
// FIXME: Only accepts a $titles array where the keys are the sanitized
// file names.
diff --git a/includes/filerepo/OldLocalFile.php b/includes/filerepo/OldLocalFile.php
index 89e49c4c..46c35bd9 100644
--- a/includes/filerepo/OldLocalFile.php
+++ b/includes/filerepo/OldLocalFile.php
@@ -23,7 +23,7 @@ class OldLocalFile extends LocalFile {
}
static function newFromRow( $row, $repo ) {
- $title = Title::makeTitle( NS_IMAGE, $row->oi_name );
+ $title = Title::makeTitle( NS_FILE, $row->oi_name );
$file = new self( $title, $repo, null, $row->oi_archive_name );
$file->loadFromRow( $row, 'oi_' );
return $file;
@@ -64,6 +64,7 @@ class OldLocalFile extends LocalFile {
'oi_user',
'oi_user_text',
'oi_timestamp',
+ 'oi_deleted',
'oi_sha1',
);
}
diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php
index 7cb837b3..2303f581 100644
--- a/includes/filerepo/RepoGroup.php
+++ b/includes/filerepo/RepoGroup.php
@@ -82,7 +82,7 @@ class RepoGroup {
}
return false;
}
- function findFiles( $titles, $flags = 0 ) {
+ function findFiles( $titles ) {
if ( !$this->reposInitialised ) {
$this->initialiseRepos();
}
@@ -90,11 +90,12 @@ class RepoGroup {
$titleObjs = array();
foreach ( $titles as $title ) {
if ( !( $title instanceof Title ) )
- $title = Title::makeTitleSafe( NS_IMAGE, $title );
- $titleObjs[$title->getDBkey()] = $title;
+ $title = Title::makeTitleSafe( NS_FILE, $title );
+ if ( $title )
+ $titleObjs[$title->getDBkey()] = $title;
}
- $images = $this->localRepo->findFiles( $titleObjs, $flags );
+ $images = $this->localRepo->findFiles( $titleObjs );
foreach ( $this->foreignRepos as $repo ) {
// Remove found files from $titleObjs
@@ -102,7 +103,7 @@ class RepoGroup {
if ( isset( $titleObjs[$name] ) )
unset( $titleObjs[$name] );
- $images = array_merge( $images, $repo->findFiles( $titleObjs, $flags ) );
+ $images = array_merge( $images, $repo->findFiles( $titleObjs ) );
}
return $images;
}
@@ -176,6 +177,13 @@ class RepoGroup {
return $this->getRepo( 'local' );
}
+ /**
+ * Call a function for each foreign repo, with the repo object as the
+ * first parameter.
+ *
+ * @param $callback callback The function to call
+ * @param $params array Optional additional parameters to pass to the function
+ */
function forEachForeignRepo( $callback, $params = array() ) {
foreach( $this->foreignRepos as $repo ) {
$args = array_merge( array( $repo ), $params );
@@ -186,8 +194,12 @@ class RepoGroup {
return false;
}
+ /**
+ * Does the installation have any foreign repos set up?
+ * @return bool
+ */
function hasForeignRepos() {
- return !empty( $this->foreignRepos );
+ return (bool)$this->foreignRepos;
}
/**
diff --git a/includes/filerepo/UnregisteredLocalFile.php b/includes/filerepo/UnregisteredLocalFile.php
index c687ef6e..6f63cb0b 100644
--- a/includes/filerepo/UnregisteredLocalFile.php
+++ b/includes/filerepo/UnregisteredLocalFile.php
@@ -32,7 +32,7 @@ class UnregisteredLocalFile extends File {
$this->name = $repo->getNameFromTitle( $title );
} else {
$this->name = basename( $path );
- $this->title = Title::makeTitleSafe( NS_IMAGE, $this->name );
+ $this->title = Title::makeTitleSafe( NS_FILE, $this->name );
}
$this->repo = $repo;
if ( $path ) {
diff --git a/includes/media/BMP.php b/includes/media/BMP.php
index ce1b0362..39b29744 100644
--- a/includes/media/BMP.php
+++ b/includes/media/BMP.php
@@ -11,6 +11,15 @@
* @ingroup Media
*/
class BmpHandler extends BitmapHandler {
+ // We never want to use .bmp in an <img/> tag
+ function mustRender( $file ) {
+ return true;
+ }
+
+ // Render files as PNG
+ function getThumbType( $text, $mime ) {
+ return array( 'png', 'image/png' );
+ }
/*
* Get width and height from the bmp header.
diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php
index e01386e9..b949ae3d 100644
--- a/includes/media/Bitmap.php
+++ b/includes/media/Bitmap.php
@@ -41,9 +41,10 @@ class BitmapHandler extends ImageHandler {
}
function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
- global $wgUseImageMagick, $wgImageMagickConvertCommand;
+ global $wgUseImageMagick, $wgImageMagickConvertCommand, $wgImageMagickTempDir;
global $wgCustomConvertCommand;
global $wgSharpenParameter, $wgSharpenReductionThreshold;
+ global $wgMaxAnimatedGifArea;
if ( !$this->normaliseParams( $image, $params ) ) {
return new TransformParameterError( $params );
@@ -59,7 +60,7 @@ class BitmapHandler extends ImageHandler {
$retval = 0;
wfDebug( __METHOD__.": creating {$physicalWidth}x{$physicalHeight} thumbnail at $dstPath\n" );
- if ( $physicalWidth == $srcWidth && $physicalHeight == $srcHeight ) {
+ if ( !$image->mustRender() && $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, $image->getURL(), $clientWidth, $clientHeight, $srcPath );
@@ -77,6 +78,7 @@ class BitmapHandler extends ImageHandler {
} else {
$scaler = 'client';
}
+ wfDebug( __METHOD__.": scaler $scaler\n" );
if ( $scaler == 'client' ) {
# Client-side image scaling, use the source URL
@@ -85,18 +87,22 @@ class BitmapHandler extends ImageHandler {
}
if ( $flags & self::TRANSFORM_LATER ) {
+ wfDebug( __METHOD__.": Transforming later per flags.\n" );
return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
}
if ( !wfMkdirParents( dirname( $dstPath ) ) ) {
- wfDebug( "Unable to create thumbnail destination directory, falling back to client scaling\n" );
+ wfDebug( __METHOD__.": Unable to create thumbnail destination directory, falling back to client scaling\n" );
return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath );
}
if ( $scaler == 'im' ) {
# use ImageMagick
+ $quality = '';
$sharpen = '';
+ $frame = '';
+ $animation = '';
if ( $mimeType == 'image/jpeg' ) {
$quality = "-quality 80"; // 80%
# Sharpening, see bug 6193
@@ -105,8 +111,21 @@ class BitmapHandler extends ImageHandler {
}
} elseif ( $mimeType == 'image/png' ) {
$quality = "-quality 95"; // zlib 9, adaptive filtering
+ } elseif( $mimeType == 'image/gif' ) {
+ if( $srcWidth * $srcHeight > $wgMaxAnimatedGifArea ) {
+ // Extract initial frame only; we're so big it'll
+ // be a total drag. :P
+ $frame = '[0]';
+ } else {
+ // Coalesce is needed to scale animated GIFs properly (bug 1017).
+ $animation = ' -coalesce ';
+ }
+ }
+
+ if ( strval( $wgImageMagickTempDir ) !== '' ) {
+ $tempEnv = 'MAGICK_TMPDIR=' . wfEscapeShellArg( $wgImageMagickTempDir ) . ' ';
} else {
- $quality = ''; // default
+ $tempEnv = '';
}
# Specify white background color, will be used for transparent images
@@ -116,11 +135,12 @@ class BitmapHandler extends ImageHandler {
# It seems that ImageMagick has a bug wherein it produces thumbnails of
# the wrong size in the second case.
- $cmd = wfEscapeShellArg($wgImageMagickConvertCommand) .
+ $cmd =
+ $tempEnv .
+ wfEscapeShellArg($wgImageMagickConvertCommand) .
" {$quality} -background white -size {$physicalWidth} ".
- wfEscapeShellArg($srcPath) .
- // Coalesce is needed to scale animated GIFs properly (bug 1017).
- ' -coalesce ' .
+ wfEscapeShellArg($srcPath . $frame) .
+ $animation .
// For the -resize option a "!" is needed to force exact size,
// or ImageMagick may decide your ratio is wrong and slice off
// a pixel.
diff --git a/includes/media/Bitmap_ClientOnly.php b/includes/media/Bitmap_ClientOnly.php
new file mode 100644
index 00000000..9801f9be
--- /dev/null
+++ b/includes/media/Bitmap_ClientOnly.php
@@ -0,0 +1,15 @@
+<?php
+
+class BitmapHandler_ClientOnly extends BitmapHandler {
+ function normaliseParams( $image, &$params ) {
+ return ImageHandler::normaliseParams( $image, $params );
+ }
+
+ function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
+ if ( !$this->normaliseParams( $image, $params ) ) {
+ return new TransformParameterError( $params );
+ }
+ return new ThumbnailImage( $image, $image->getURL(), $params['width'],
+ $params['height'], $image->getPath() );
+ }
+}
diff --git a/includes/media/Generic.php b/includes/media/Generic.php
index b2cb70f6..a9c681e1 100644
--- a/includes/media/Generic.php
+++ b/includes/media/Generic.php
@@ -239,6 +239,21 @@ abstract class MediaHandler {
$sk->formatSize( $file->getSize() ),
$file->getMimeType() );
}
+
+ static function getGeneralShortDesc( $file ) {
+ global $wgLang;
+ $nbytes = '(' . wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $file->getSize() ) ) . ')';
+ return "$nbytes";
+ }
+
+ static function getGeneralLongDesc( $file ) {
+ global $wgUser;
+ $sk = $wgUser->getSkin();
+ return wfMsgExt( 'file-info', 'parseinline',
+ $sk->formatSize( $file->getSize() ),
+ $file->getMimeType() );
+ }
function getDimensionsString( $file ) {
return '';
diff --git a/includes/media/SVG.php b/includes/media/SVG.php
index 2604e3b4..f0519e89 100644
--- a/includes/media/SVG.php
+++ b/includes/media/SVG.php
@@ -27,7 +27,6 @@ class SvgHandler extends ImageHandler {
if ( !parent::normaliseParams( $image, $params ) ) {
return false;
}
-
# Don't make an image bigger than wgMaxSVGSize
$params['physicalWidth'] = $params['width'];
$params['physicalHeight'] = $params['height'];
@@ -60,32 +59,49 @@ class SvgHandler extends ImageHandler {
return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
wfMsg( 'thumbnail_dest_directory' ) );
}
-
+
+ $status = $this->rasterize( $srcPath, $dstPath, $physicalWidth, $physicalHeight );
+ if( $status === true ) {
+ return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
+ } else {
+ return $status; // MediaTransformError
+ }
+ }
+
+ /*
+ * Transform an SVG file to PNG
+ * This function can be called outside of thumbnail contexts
+ * @param string $srcPath
+ * @param string $dstPath
+ * @param string $width
+ * @param string $height
+ * @returns TRUE/MediaTransformError
+ */
+ public function rasterize( $srcPath, $dstPath, $width, $height ) {
+ global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath;
$err = false;
- if( isset( $wgSVGConverters[$wgSVGConverter] ) ) {
+ if ( isset( $wgSVGConverters[$wgSVGConverter] ) ) {
$cmd = str_replace(
array( '$path/', '$width', '$height', '$input', '$output' ),
array( $wgSVGConverterPath ? wfEscapeShellArg( "$wgSVGConverterPath/" ) : "",
- intval( $physicalWidth ),
- intval( $physicalHeight ),
+ intval( $width ),
+ intval( $height ),
wfEscapeShellArg( $srcPath ),
wfEscapeShellArg( $dstPath ) ),
- $wgSVGConverters[$wgSVGConverter] ) . " 2>&1";
+ $wgSVGConverters[$wgSVGConverter]
+ ) . " 2>&1";
wfProfileIn( 'rsvg' );
wfDebug( __METHOD__.": $cmd\n" );
$err = wfShellExec( $cmd, $retval );
wfProfileOut( 'rsvg' );
}
-
$removed = $this->removeBadFile( $dstPath, $retval );
if ( $retval != 0 || $removed ) {
- wfDebugLog( 'thumbnail',
- sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
+ wfDebugLog( 'thumbnail', sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
wfHostname(), $retval, trim($err), $cmd ) );
- return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
- } else {
- return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
+ return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
}
+ return true;
}
function getImageSize( $image, $path ) {
diff --git a/includes/memcached-client.php b/includes/memcached-client.php
index 6bd18387..79745309 100644
--- a/includes/memcached-client.php
+++ b/includes/memcached-client.php
@@ -454,7 +454,7 @@ class memcached
if (!$this->_active)
return false;
- $this->stats['get_multi']++;
+ @$this->stats['get_multi']++;
$sock_keys = array();
foreach ($keys as $key)
@@ -800,8 +800,8 @@ class memcached
if (is_resource($sock)) {
$this->_flush_read_buffer($sock);
return $sock;
- }
- $hv += $this->_hashfunc($tries . $realkey);
+ }
+ $hv = $this->_hashfunc( $hv . $realkey );
}
return false;
diff --git a/includes/mime.types b/includes/mime.types
index 6021e926..2b8cb9ab 100644
--- a/includes/mime.types
+++ b/includes/mime.types
@@ -59,6 +59,7 @@ application/xslt+xml xslt
application/xml xml xsl xsd
application/xml-dtd dtd
application/zip zip jar xpi sxc stc sxd std sxi sti sxm stm sxw stw
+application/x-rar rar
audio/basic au snd
audio/midi mid midi kar
audio/mpeg mpga mp2 mp3
diff --git a/includes/parser/CoreLinkFunctions.php b/includes/parser/CoreLinkFunctions.php
new file mode 100644
index 00000000..d6d11880
--- /dev/null
+++ b/includes/parser/CoreLinkFunctions.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * Various core link functions, registered in Parser::firstCallInit()
+ * @ingroup Parser
+ */
+class CoreLinkFunctions {
+ static function register( $parser ) {
+ $parser->setLinkHook( NS_CATEGORY, array( __CLASS__, 'categoryLinkHook' ) );
+ return true;
+ }
+
+ static function defaultLinkHook( $parser, $holders, $markers,
+ Title $title, $titleText, &$displayText = null, &$leadingColon = false ) {
+ if( isset($displayText) && $markers->findMarker( $displayText ) ) {
+ # There are links inside of the displayText
+ # For backwards compatibility the deepest links are dominant so this
+ # link should not be handled
+ $displayText = $markers->expand($displayText);
+ # Return false so that this link is reverted back to WikiText
+ return false;
+ }
+ return $holders->makeHolder( $title, isset($displayText) ? $displayText : $titleText, '', '', '' );
+ }
+
+ static function categoryLinkHook( $parser, $holders, $markers,
+ Title $title, $titleText, &$sortText = null, &$leadingColon = false ) {
+ global $wgContLang;
+ # When a category link starts with a : treat it as a normal link
+ if( $leadingColon ) return true;
+ if( isset($sortText) && $markers->findMarker( $sortText ) ) {
+ # There are links inside of the sortText
+ # For backwards compatibility the deepest links are dominant so this
+ # link should not be handled
+ $sortText = $markers->expand($sortText);
+ # Return false so that this link is reverted back to WikiText
+ return false;
+ }
+ if( !isset($sortText) ) $sortText = $parser->getDefaultSort();
+ $sortText = Sanitizer::decodeCharReferences( $sortText );
+ $sortText = str_replace( "\n", '', $sortText );
+ $sortText = $wgContLang->convertCategoryKey( $sortText );
+ $parser->mOutput->addCategory( $title->getDBkey(), $sortText );
+ return '';
+ }
+
+}
diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php
index d9072e93..a3b5189a 100644
--- a/includes/parser/CoreParserFunctions.php
+++ b/includes/parser/CoreParserFunctions.php
@@ -33,7 +33,9 @@ class CoreParserFunctions {
$parser->setFunctionHook( 'numberofarticles', array( __CLASS__, 'numberofarticles' ), SFH_NO_HASH );
$parser->setFunctionHook( 'numberoffiles', array( __CLASS__, 'numberoffiles' ), SFH_NO_HASH );
$parser->setFunctionHook( 'numberofadmins', array( __CLASS__, 'numberofadmins' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'numberingroup', array( __CLASS__, 'numberingroup' ), SFH_NO_HASH );
$parser->setFunctionHook( 'numberofedits', array( __CLASS__, 'numberofedits' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'numberofviews', array( __CLASS__, 'numberofviews' ), SFH_NO_HASH );
$parser->setFunctionHook( 'language', array( __CLASS__, 'language' ), SFH_NO_HASH );
$parser->setFunctionHook( 'padleft', array( __CLASS__, 'padleft' ), SFH_NO_HASH );
$parser->setFunctionHook( 'padright', array( __CLASS__, 'padright' ), SFH_NO_HASH );
@@ -56,7 +58,10 @@ class CoreParserFunctions {
static function intFunction( $parser, $part1 = '' /*, ... */ ) {
if ( strval( $part1 ) !== '' ) {
$args = array_slice( func_get_args(), 2 );
- return wfMsgReal( $part1, $args, true );
+ $message = wfMsgGetKey( $part1, true, false, false );
+ $message = wfMsgReplaceArgs( $message, $args );
+ $message = $parser->replaceVariables( $message ); // like $wgMessageCache->transform()
+ return $message;
} else {
return array( 'found' => false );
}
@@ -64,20 +69,13 @@ class CoreParserFunctions {
static function ns( $parser, $part1 = '' ) {
global $wgContLang;
- $found = false;
if ( intval( $part1 ) || $part1 == "0" ) {
- $text = $wgContLang->getNsText( intval( $part1 ) );
- $found = true;
+ $index = intval( $part1 );
} else {
- $param = str_replace( ' ', '_', strtolower( $part1 ) );
- $index = MWNamespace::getCanonicalIndex( strtolower( $param ) );
- if ( !is_null( $index ) ) {
- $text = $wgContLang->getNsText( $index );
- $found = true;
- }
+ $index = $wgContLang->getNsIndex( str_replace( ' ', '_', $part1 ) );
}
- if ( $found ) {
- return $text;
+ if ( $index !== false ) {
+ return $wgContLang->getFormattedNsText( $index );
} else {
return array( 'found' => false );
}
@@ -128,8 +126,12 @@ class CoreParserFunctions {
# attempt, url-decode and try for a second.
if( is_null( $title ) )
$title = Title::newFromUrl( urldecode( $s ) );
- if ( !is_null( $title ) ) {
- if ( !is_null( $arg ) ) {
+ if( !is_null( $title ) ) {
+ # Convert NS_MEDIA -> NS_FILE
+ if( $title->getNamespace() == NS_MEDIA ) {
+ $title = Title::makeTitle( NS_FILE, $title->getDBKey() );
+ }
+ if( !is_null( $arg ) ) {
$text = $title->$func( $arg );
} else {
$text = $title->$func();
@@ -167,10 +169,16 @@ class CoreParserFunctions {
* @return string
*/
static function displaytitle( $parser, $text = '' ) {
+ global $wgRestrictDisplayTitle;
$text = trim( Sanitizer::decodeCharReferences( $text ) );
- $title = Title::newFromText( $text );
- if( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) )
+
+ if ( !$wgRestrictDisplayTitle ) {
$parser->mOutput->setDisplayTitle( $text );
+ } else {
+ $title = Title::newFromText( $text );
+ if( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) )
+ $parser->mOutput->setDisplayTitle( $text );
+ }
return '';
}
@@ -207,14 +215,20 @@ class CoreParserFunctions {
return self::formatRaw( SiteStats::images(), $raw );
}
static function numberofadmins( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::admins(), $raw );
+ return self::formatRaw( SiteStats::numberingroup('sysop'), $raw );
}
static function numberofedits( $parser, $raw = null ) {
return self::formatRaw( SiteStats::edits(), $raw );
}
+ static function numberofviews( $parser, $raw = null ) {
+ return self::formatRaw( SiteStats::views(), $raw );
+ }
static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) {
return self::formatRaw( SiteStats::pagesInNs( intval( $namespace ) ), $raw );
}
+ static function numberingroup( $parser, $name = '', $raw = null) {
+ return self::formatRaw( SiteStats::numberingroup( strtolower( $name ) ), $raw );
+ }
/**
* Return the number of pages in the given category, or 0 if it's nonexis-
@@ -269,12 +283,12 @@ class CoreParserFunctions {
if( isset( $cache[$page] ) ) {
$length = $cache[$page];
} elseif( $parser->incrementExpensiveFunctionCount() ) {
- $length = $cache[$page] = $title->getLength();
+ $rev = Revision::newFromTitle($title);
+ $id = $rev ? $rev->getPage() : 0;
+ $length = $cache[$page] = $rev ? $rev->getSize() : 0;
// Register dependency in templatelinks
- $id = $title->getArticleId();
- $revid = Revision::newFromTitle($title);
- $parser->mOutput->addTemplate($title, $id, $revid);
+ $parser->mOutput->addTemplate( $title, $id, $rev ? $rev->getId() : 0 );
}
return self::formatRaw( $length, $raw );
}
@@ -320,9 +334,18 @@ class CoreParserFunctions {
public static function defaultsort( $parser, $text ) {
$text = trim( $text );
- if( strlen( $text ) > 0 )
- $parser->setDefaultSort( $text );
- return '';
+ if( strlen( $text ) == 0 )
+ return '';
+ $old = $parser->getCustomDefaultSort();
+ $parser->setDefaultSort( $text );
+ if( $old === false || $old == $text )
+ return '';
+ else
+ return( '<span class="error">' .
+ wfMsg( 'duplicate-defaultsort',
+ htmlspecialchars( $old ),
+ htmlspecialchars( $text ) ) .
+ '</span>' );
}
public static function filepath( $parser, $name='', $option='' ) {
@@ -330,7 +353,7 @@ class CoreParserFunctions {
if( $file ) {
$url = $file->getFullUrl();
if( $option == 'nowiki' ) {
- return "<nowiki>$url</nowiki>";
+ return array( $url, 'nowiki' => true );
}
return $url;
} else {
@@ -365,7 +388,7 @@ class CoreParserFunctions {
foreach ( $args as $arg ) {
$bits = $arg->splitArg();
if ( strval( $bits['index'] ) === '' ) {
- $name = $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS );
+ $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
$value = trim( $frame->expand( $bits['value'] ) );
if ( preg_match( '/^(?:["\'](.+)["\']|""|\'\')$/s', $value, $m ) ) {
$value = isset( $m[1] ) ? $m[1] : '';
diff --git a/includes/parser/LinkHolderArray.php b/includes/parser/LinkHolderArray.php
new file mode 100644
index 00000000..35b672b9
--- /dev/null
+++ b/includes/parser/LinkHolderArray.php
@@ -0,0 +1,438 @@
+<?php
+
+class LinkHolderArray {
+ var $internals = array(), $interwikis = array();
+ var $size = 0;
+ var $parent;
+
+ function __construct( $parent ) {
+ $this->parent = $parent;
+ }
+
+ /**
+ * Reduce memory usage to reduce the impact of circular references
+ */
+ function __destruct() {
+ foreach ( $this as $name => $value ) {
+ unset( $this->$name );
+ }
+ }
+
+ /**
+ * Merge another LinkHolderArray into this one
+ */
+ function merge( $other ) {
+ foreach ( $other->internals as $ns => $entries ) {
+ $this->size += count( $entries );
+ if ( !isset( $this->internals[$ns] ) ) {
+ $this->internals[$ns] = $entries;
+ } else {
+ $this->internals[$ns] += $entries;
+ }
+ }
+ $this->interwikis += $other->interwikis;
+ }
+
+ /**
+ * Returns true if the memory requirements of this object are getting large
+ */
+ function isBig() {
+ global $wgLinkHolderBatchSize;
+ return $this->size > $wgLinkHolderBatchSize;
+ }
+
+ /**
+ * Clear all stored link holders.
+ * Make sure you don't have any text left using these link holders, before you call this
+ */
+ function clear() {
+ $this->internals = array();
+ $this->interwikis = array();
+ $this->size = 0;
+ }
+
+ /**
+ * Make a link placeholder. The text returned can be later resolved to a real link with
+ * replaceLinkHolders(). This is done for two reasons: firstly to avoid further
+ * parsing of interwiki links, and secondly to allow all existence checks and
+ * article length checks (for stub links) to be bundled into a single query.
+ *
+ */
+ function makeHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ wfProfileIn( __METHOD__ );
+ if ( ! is_object($nt) ) {
+ # Fail gracefully
+ $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
+ } else {
+ # Separate the link trail from the rest of the link
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+
+ $entry = array(
+ 'title' => $nt,
+ 'text' => $prefix.$text.$inside,
+ 'pdbk' => $nt->getPrefixedDBkey(),
+ );
+ if ( $query !== '' ) {
+ $entry['query'] = $query;
+ }
+
+ if ( $nt->isExternal() ) {
+ // Use a globally unique ID to keep the objects mergable
+ $key = $this->parent->nextLinkID();
+ $this->interwikis[$key] = $entry;
+ $retVal = "<!--IWLINK $key-->{$trail}";
+ } else {
+ $key = $this->parent->nextLinkID();
+ $ns = $nt->getNamespace();
+ $this->internals[$ns][$key] = $entry;
+ $retVal = "<!--LINK $ns:$key-->{$trail}";
+ }
+ $this->size++;
+ }
+ wfProfileOut( __METHOD__ );
+ return $retVal;
+ }
+
+ /**
+ * Get the stub threshold
+ */
+ function getStubThreshold() {
+ global $wgUser;
+ if ( !isset( $this->stubThreshold ) ) {
+ $this->stubThreshold = $wgUser->getOption('stubthreshold');
+ }
+ return $this->stubThreshold;
+ }
+
+ /**
+ * Replace <!--LINK--> link placeholders with actual links, in the buffer
+ * Placeholders created in Skin::makeLinkObj()
+ * Returns an array of link CSS classes, indexed by PDBK.
+ */
+ function replace( &$text ) {
+ wfProfileIn( __METHOD__ );
+
+ $colours = $this->replaceInternal( $text );
+ $this->replaceInterwiki( $text );
+
+ wfProfileOut( __METHOD__ );
+ return $colours;
+ }
+
+ /**
+ * Replace internal links
+ */
+ protected function replaceInternal( &$text ) {
+ if ( !$this->internals ) {
+ return;
+ }
+
+ wfProfileIn( __METHOD__ );
+ global $wgContLang;
+
+ $colours = array();
+ $sk = $this->parent->getOptions()->getSkin();
+ $linkCache = LinkCache::singleton();
+ $output = $this->parent->getOutput();
+
+ wfProfileIn( __METHOD__.'-check' );
+ $dbr = wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $threshold = $this->getStubThreshold();
+
+ # Sort by namespace
+ ksort( $this->internals );
+
+ # Generate query
+ $query = false;
+ $current = null;
+ foreach ( $this->internals as $ns => $entries ) {
+ foreach ( $entries as $index => $entry ) {
+ $key = "$ns:$index";
+ $title = $entry['title'];
+ $pdbk = $entry['pdbk'];
+
+ # Skip invalid entries.
+ # Result will be ugly, but prevents crash.
+ if ( is_null( $title ) ) {
+ continue;
+ }
+
+ # Check if it's a static known link, e.g. interwiki
+ if ( $title->isAlwaysKnown() ) {
+ $colours[$pdbk] = '';
+ } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
+ $colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
+ $output->addLink( $title, $id );
+ } elseif ( $linkCache->isBadLink( $pdbk ) ) {
+ $colours[$pdbk] = 'new';
+ } else {
+ # Not in the link cache, add it to the query
+ if ( !isset( $current ) ) {
+ $current = $ns;
+ $query = "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len";
+ $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN(";
+ } elseif ( $current != $ns ) {
+ $current = $ns;
+ $query .= ")) OR (page_namespace=$ns AND page_title IN(";
+ } else {
+ $query .= ', ';
+ }
+
+ $query .= $dbr->addQuotes( $title->getDBkey() );
+ }
+ }
+ }
+ if ( $query ) {
+ $query .= '))';
+
+ $res = $dbr->query( $query, __METHOD__ );
+
+ # Fetch data and form into an associative array
+ # non-existent = broken
+ $linkcolour_ids = array();
+ while ( $s = $dbr->fetchObject($res) ) {
+ $title = Title::makeTitle( $s->page_namespace, $s->page_title );
+ $pdbk = $title->getPrefixedDBkey();
+ $linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect );
+ $output->addLink( $title, $s->page_id );
+ # FIXME: convoluted data flow
+ # The redirect status and length is passed to getLinkColour via the LinkCache
+ # Use formal parameters instead
+ $colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
+ //add id to the extension todolist
+ $linkcolour_ids[$s->page_id] = $pdbk;
+ }
+ unset( $res );
+ //pass an array of page_ids to an extension
+ wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
+ }
+ wfProfileOut( __METHOD__.'-check' );
+
+ # Do a second query for different language variants of links and categories
+ if($wgContLang->hasVariants()) {
+ $this->doVariants( $colours );
+ }
+
+ # Construct search and replace arrays
+ wfProfileIn( __METHOD__.'-construct' );
+ $replacePairs = array();
+ foreach ( $this->internals as $ns => $entries ) {
+ foreach ( $entries as $index => $entry ) {
+ $pdbk = $entry['pdbk'];
+ $title = $entry['title'];
+ $query = isset( $entry['query'] ) ? $entry['query'] : '';
+ $key = "$ns:$index";
+ $searchkey = "<!--LINK $key-->";
+ if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] == 'new' ) {
+ $linkCache->addBadLinkObj( $title );
+ $colours[$pdbk] = 'new';
+ $output->addLink( $title, 0 );
+ $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title,
+ $entry['text'],
+ $query );
+ } else {
+ $replacePairs[$searchkey] = $sk->makeColouredLinkObj( $title, $colours[$pdbk],
+ $entry['text'],
+ $query );
+ }
+ }
+ }
+ $replacer = new HashtableReplacer( $replacePairs, 1 );
+ wfProfileOut( __METHOD__.'-construct' );
+
+ # Do the thing
+ wfProfileIn( __METHOD__.'-replace' );
+ $text = preg_replace_callback(
+ '/(<!--LINK .*?-->)/',
+ $replacer->cb(),
+ $text);
+
+ wfProfileOut( __METHOD__.'-replace' );
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Replace interwiki links
+ */
+ protected function replaceInterwiki( &$text ) {
+ if ( empty( $this->interwikis ) ) {
+ return;
+ }
+
+ wfProfileIn( __METHOD__ );
+ # Make interwiki link HTML
+ $sk = $this->parent->getOptions()->getSkin();
+ $replacePairs = array();
+ foreach( $this->interwikis as $key => $link ) {
+ $replacePairs[$key] = $sk->link( $link['title'], $link['text'] );
+ }
+ $replacer = new HashtableReplacer( $replacePairs, 1 );
+
+ $text = preg_replace_callback(
+ '/<!--IWLINK (.*?)-->/',
+ $replacer->cb(),
+ $text );
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Modify $this->internals and $colours according to language variant linking rules
+ */
+ protected function doVariants( &$colours ) {
+ global $wgContLang;
+ $linkBatch = new LinkBatch();
+ $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders)
+ $output = $this->parent->getOutput();
+ $linkCache = LinkCache::singleton();
+ $sk = $this->parent->getOptions()->getSkin();
+ $threshold = $this->getStubThreshold();
+
+ // Add variants of links to link batch
+ foreach ( $this->internals as $ns => $entries ) {
+ foreach ( $entries as $index => $entry ) {
+ $key = "$ns:$index";
+ $pdbk = $entry['pdbk'];
+ $title = $entry['title'];
+ $titleText = $title->getText();
+
+ // generate all variants of the link title text
+ $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText);
+
+ // if link was not found (in first query), add all variants to query
+ if ( !isset($colours[$pdbk]) ){
+ foreach($allTextVariants as $textVariant){
+ if($textVariant != $titleText){
+ $variantTitle = Title::makeTitle( $ns, $textVariant );
+ if(is_null($variantTitle)) continue;
+ $linkBatch->addObj( $variantTitle );
+ $variantMap[$variantTitle->getPrefixedDBkey()][] = $key;
+ }
+ }
+ }
+ }
+ }
+
+ // process categories, check if a category exists in some variant
+ $categoryMap = array(); // maps $category_variant => $category (dbkeys)
+ $varCategories = array(); // category replacements oldDBkey => newDBkey
+ foreach( $output->getCategoryLinks() as $category ){
+ $variants = $wgContLang->convertLinkToAllVariants($category);
+ foreach($variants as $variant){
+ if($variant != $category){
+ $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) );
+ if(is_null($variantTitle)) continue;
+ $linkBatch->addObj( $variantTitle );
+ $categoryMap[$variant] = $category;
+ }
+ }
+ }
+
+
+ if(!$linkBatch->isEmpty()){
+ // construct query
+ $dbr = wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $titleClause = $linkBatch->constructSet('page', $dbr);
+ $variantQuery = "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len";
+ $variantQuery .= " FROM $page WHERE $titleClause";
+ $varRes = $dbr->query( $variantQuery, __METHOD__ );
+ $linkcolour_ids = array();
+
+ // for each found variants, figure out link holders and replace
+ while ( $s = $dbr->fetchObject($varRes) ) {
+
+ $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
+ $varPdbk = $variantTitle->getPrefixedDBkey();
+ $vardbk = $variantTitle->getDBkey();
+
+ $holderKeys = array();
+ if(isset($variantMap[$varPdbk])){
+ $holderKeys = $variantMap[$varPdbk];
+ $linkCache->addGoodLinkObj( $s->page_id, $variantTitle, $s->page_len, $s->page_is_redirect );
+ $output->addLink( $variantTitle, $s->page_id );
+ }
+
+ // loop over link holders
+ foreach($holderKeys as $key){
+ list( $ns, $index ) = explode( ':', $key, 2 );
+ $entry =& $this->internals[$ns][$index];
+ $pdbk = $entry['pdbk'];
+
+ if(!isset($colours[$pdbk])){
+ // found link in some of the variants, replace the link holder data
+ $entry['title'] = $variantTitle;
+ $entry['pdbk'] = $varPdbk;
+
+ // set pdbk and colour
+ # FIXME: convoluted data flow
+ # The redirect status and length is passed to getLinkColour via the LinkCache
+ # Use formal parameters instead
+ $colours[$varPdbk] = $sk->getLinkColour( $variantTitle, $threshold );
+ $linkcolour_ids[$s->page_id] = $pdbk;
+ }
+ }
+
+ // check if the object is a variant of a category
+ if(isset($categoryMap[$vardbk])){
+ $oldkey = $categoryMap[$vardbk];
+ if($oldkey != $vardbk)
+ $varCategories[$oldkey]=$vardbk;
+ }
+ }
+ wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
+
+ // rebuild the categories in original order (if there are replacements)
+ if(count($varCategories)>0){
+ $newCats = array();
+ $originalCats = $output->getCategories();
+ foreach($originalCats as $cat => $sortkey){
+ // make the replacement
+ if( array_key_exists($cat,$varCategories) )
+ $newCats[$varCategories[$cat]] = $sortkey;
+ else $newCats[$cat] = $sortkey;
+ }
+ $output->setCategoryLinks($newCats);
+ }
+ }
+ }
+
+ /**
+ * Replace <!--LINK--> link placeholders with plain text of links
+ * (not HTML-formatted).
+ * @param string $text
+ * @return string
+ */
+ function replaceText( $text ) {
+ wfProfileIn( __METHOD__ );
+
+ $text = preg_replace_callback(
+ '/<!--(LINK|IWLINK) (.*?)-->/',
+ array( &$this, 'replaceTextCallback' ),
+ $text );
+
+ wfProfileOut( __METHOD__ );
+ return $text;
+ }
+
+ /**
+ * @param array $matches
+ * @return string
+ * @private
+ */
+ function replaceTextCallback( $matches ) {
+ $type = $matches[1];
+ $key = $matches[2];
+ if( $type == 'LINK' ) {
+ list( $ns, $index ) = explode( ':', $key, 2 );
+ if( isset( $this->internals[$ns][$index]['text'] ) ) {
+ return $this->internals[$ns][$index]['text'];
+ }
+ } elseif( $type == 'IWLINK' ) {
+ if( isset( $this->interwikis[$key]['text'] ) ) {
+ return $this->interwikis[$key]['text'];
+ }
+ }
+ return $matches[0];
+ }
+}
diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php
index 3ff56a2b..7fcfb90a 100644
--- a/includes/parser/Parser.php
+++ b/includes/parser/Parser.php
@@ -92,17 +92,18 @@ class Parser
# Persistent:
var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
$mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex, $mPreprocessor,
- $mExtLinkBracketedRegex, $mDefaultStripList, $mVarCache, $mConf;
+ $mExtLinkBracketedRegex, $mUrlProtocols, $mDefaultStripList, $mVarCache, $mConf;
# Cleared with clearState():
var $mOutput, $mAutonumber, $mDTopen, $mStripState;
var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
- var $mInterwikiLinkHolders, $mLinkHolders;
+ var $mLinkHolders, $mLinkID;
var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
var $mTplExpandCache; // empty-frame expansion cache
var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
var $mExpensiveFunctionCount; // number of expensive parser function calls
+ var $mFileCache;
# Temporary
# These are variables reset at least once per parse regardless of $clearState
@@ -128,6 +129,7 @@ class Parser
$this->mFunctionHooks = array();
$this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
$this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' );
+ $this->mUrlProtocols = wfUrlProtocols();
$this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
'[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
$this->mVarCache = array();
@@ -147,6 +149,18 @@ class Parser
}
/**
+ * Reduce memory usage to reduce the impact of circular references
+ */
+ function __destruct() {
+ if ( isset( $this->mLinkHolders ) ) {
+ $this->mLinkHolders->__destruct();
+ }
+ foreach ( $this as $name => $value ) {
+ unset( $this->$name );
+ }
+ }
+
+ /**
* Do various kinds of initialisation on the first call of the parser
*/
function firstCallInit() {
@@ -183,17 +197,8 @@ class Parser
$this->mStripState = new StripState;
$this->mArgStack = false;
$this->mInPre = false;
- $this->mInterwikiLinkHolders = array(
- 'texts' => array(),
- 'titles' => array()
- );
- $this->mLinkHolders = array(
- 'namespaces' => array(),
- 'dbkeys' => array(),
- 'queries' => array(),
- 'texts' => array(),
- 'titles' => array()
- );
+ $this->mLinkHolders = new LinkHolderArray( $this );
+ $this->mLinkID = 0;
$this->mRevisionTimestamp = $this->mRevisionId = null;
/**
@@ -208,7 +213,7 @@ class Parser
*/
#$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
# Changed to \x7f to allow XML double-parsing -- TS
- $this->mUniqPrefix = "\x7fUNIQ" . Parser::getRandomString();
+ $this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString();
# Clear these on every parse, bug 4549
@@ -225,6 +230,7 @@ class Parser
$this->mHeadings = array();
$this->mDoubleUnderscores = array();
$this->mExpensiveFunctionCount = 0;
+ $this->mFileCache = array();
# Fix cloning
if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
@@ -283,22 +289,22 @@ class Parser
* Convert wikitext to HTML
* Do not call this function recursively.
*
- * @param string $text Text we want to parse
- * @param Title &$title A title object
- * @param array $options
- * @param boolean $linestart
- * @param boolean $clearState
- * @param int $revid number to pass in {{REVISIONID}}
+ * @param $text String: text we want to parse
+ * @param $title A title object
+ * @param $options ParserOptions
+ * @param $linestart boolean
+ * @param $clearState boolean
+ * @param $revid Int: number to pass in {{REVISIONID}}
* @return ParserOutput a ParserOutput
*/
- public function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) {
+ public function parse( $text, Title $title, ParserOptions $options, $linestart = true, $clearState = true, $revid = null ) {
/**
* First pass--just handle <nowiki> sections, pass the rest off
* to internalParse() which does all the real work.
*/
global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
- $fname = 'Parser::parse-' . wfGetCaller();
+ $fname = __METHOD__.'-' . wfGetCaller();
wfProfileIn( __METHOD__ );
wfProfileIn( $fname );
@@ -332,7 +338,6 @@ class Parser
);
$text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
- # only once and last
$text = $this->doBlockLevels( $text, $linestart );
$this->replaceLinkHolders( $text );
@@ -352,7 +357,7 @@ class Parser
$uniq_prefix = $this->mUniqPrefix;
$matches = array();
$elements = array_keys( $this->mTransparentTagHooks );
- $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+ $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
foreach( $matches as $marker => $data ) {
list( $element, $content, $params, $tag ) = $data;
@@ -370,7 +375,7 @@ class Parser
$text = Sanitizer::normalizeCharReferences( $text );
if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
- $text = Parser::tidy($text);
+ $text = self::tidy($text);
} else {
# attempt to sanitize at least some nesting problems
# (bug #2702 and quite a few others)
@@ -475,6 +480,8 @@ class Parser
function &getTitle() { return $this->mTitle; }
function getOptions() { return $this->mOptions; }
function getRevisionId() { return $this->mRevisionId; }
+ function getOutput() { return $this->mOutput; }
+ function nextLinkID() { return $this->mLinkID++; }
function getFunctionLang() {
global $wgLang, $wgContLang;
@@ -553,7 +560,7 @@ class Parser
$text = $inside;
$tail = null;
} else {
- if( $element == '!--' ) {
+ if( $element === '!--' ) {
$end = '/(-->)/';
} else {
$end = "/(<\\/$element\\s*>)/i";
@@ -658,18 +665,27 @@ class Parser
*/
function tidy( $text ) {
global $wgTidyInternal;
+
$wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'.
' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'.
'<head><title>test</title></head><body>'.$text.'</body></html>';
+
+ # Tidy is known to clobber tabs; convert 'em to entities
+ $wrappedtext = str_replace("\t", '&#9;', $wrappedtext);
+
if( $wgTidyInternal ) {
- $correctedtext = Parser::internalTidy( $wrappedtext );
+ $correctedtext = self::internalTidy( $wrappedtext );
} else {
- $correctedtext = Parser::externalTidy( $wrappedtext );
+ $correctedtext = self::externalTidy( $wrappedtext );
}
if( is_null( $correctedtext ) ) {
wfDebug( "Tidy error detected!\n" );
return $text . "\n<!-- Tidy found serious XHTML errors -->\n";
}
+
+ # Convert the tabs back from entities
+ $correctedtext = str_replace('&#9;', "\t", $correctedtext);
+
return $correctedtext;
}
@@ -681,8 +697,7 @@ class Parser
*/
function externalTidy( $text ) {
global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
- $fname = 'Parser::externalTidy';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$cleansource = '';
$opts = ' -utf8';
@@ -693,23 +708,25 @@ class Parser
2 => array('file', wfGetNull(), 'a')
);
$pipes = array();
- $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
- if (is_resource($process)) {
- // Theoretically, this style of communication could cause a deadlock
- // here. If the stdout buffer fills up, then writes to stdin could
- // block. This doesn't appear to happen with tidy, because tidy only
- // writes to stdout after it's finished reading from stdin. Search
- // for tidyParseStdin and tidySaveStdout in console/tidy.c
- fwrite($pipes[0], $text);
- fclose($pipes[0]);
- while (!feof($pipes[1])) {
- $cleansource .= fgets($pipes[1], 1024);
+ if( function_exists('proc_open') ) {
+ $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
+ if (is_resource($process)) {
+ // Theoretically, this style of communication could cause a deadlock
+ // here. If the stdout buffer fills up, then writes to stdin could
+ // block. This doesn't appear to happen with tidy, because tidy only
+ // writes to stdout after it's finished reading from stdin. Search
+ // for tidyParseStdin and tidySaveStdout in console/tidy.c
+ fwrite($pipes[0], $text);
+ fclose($pipes[0]);
+ while (!feof($pipes[1])) {
+ $cleansource .= fgets($pipes[1], 1024);
+ }
+ fclose($pipes[1]);
+ proc_close($process);
}
- fclose($pipes[1]);
- proc_close($process);
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
if( $cleansource == '' && $text != '') {
// Some kind of error happened, so we couldn't get the corrected text.
@@ -731,8 +748,7 @@ class Parser
*/
function internalTidy( $text ) {
global $wgTidyConf, $IP, $wgDebugTidy;
- $fname = 'Parser::internalTidy';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$tidy = new tidy;
$tidy->parseString( $text, $wgTidyConf, 'utf8' );
@@ -750,7 +766,7 @@ class Parser
"\n-->";
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $cleansource;
}
@@ -760,34 +776,35 @@ class Parser
* @private
*/
function doTableStuff ( $text ) {
- $fname = 'Parser::doTableStuff';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
- $lines = explode ( "\n" , $text );
+ $lines = StringUtils::explode( "\n", $text );
+ $out = '';
$td_history = array (); // Is currently a td tag open?
$last_tag_history = array (); // Save history of last lag activated (td, th or caption)
$tr_history = array (); // Is currently a tr tag open?
$tr_attributes = array (); // history of tr attributes
$has_opened_tr = array(); // Did this table open a <tr> element?
$indent_level = 0; // indent level of the table
- foreach ( $lines as $key => $line )
- {
- $line = trim ( $line );
+
+ foreach ( $lines as $outLine ) {
+ $line = trim( $outLine );
if( $line == '' ) { // empty line, go to next line
+ $out .= $outLine."\n";
continue;
}
- $first_character = $line{0};
+ $first_character = $line[0];
$matches = array();
- if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) {
+ if ( preg_match( '/^(:*)\{\|(.*)$/', $line , $matches ) ) {
// First check if we are starting a new table
$indent_level = strlen( $matches[1] );
$attributes = $this->mStripState->unstripBoth( $matches[2] );
$attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' );
- $lines[$key] = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
+ $outLine = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
array_push ( $td_history , false );
array_push ( $last_tag_history , '' );
array_push ( $tr_history , false );
@@ -795,8 +812,9 @@ class Parser
array_push ( $has_opened_tr , false );
} else if ( count ( $td_history ) == 0 ) {
// Don't do any of the following
+ $out .= $outLine."\n";
continue;
- } else if ( substr ( $line , 0 , 2 ) == '|}' ) {
+ } else if ( substr ( $line , 0 , 2 ) === '|}' ) {
// We are ending a table
$line = '</table>' . substr ( $line , 2 );
$last_tag = array_pop ( $last_tag_history );
@@ -813,8 +831,8 @@ class Parser
$line = "</{$last_tag}>{$line}";
}
array_pop ( $tr_attributes );
- $lines[$key] = $line . str_repeat( '</dd></dl>' , $indent_level );
- } else if ( substr ( $line , 0 , 2 ) == '|-' ) {
+ $outLine = $line . str_repeat( '</dd></dl>' , $indent_level );
+ } else if ( substr ( $line , 0 , 2 ) === '|-' ) {
// Now we have a table row
$line = preg_replace( '#^\|-+#', '', $line );
@@ -837,21 +855,21 @@ class Parser
$line = "</{$last_tag}>{$line}";
}
- $lines[$key] = $line;
+ $outLine = $line;
array_push ( $tr_history , false );
array_push ( $td_history , false );
array_push ( $last_tag_history , '' );
}
- else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 ) == '|+' ) {
+ else if ( $first_character === '|' || $first_character === '!' || substr ( $line , 0 , 2 ) === '|+' ) {
// This might be cell elements, td, th or captions
- if ( substr ( $line , 0 , 2 ) == '|+' ) {
+ if ( substr ( $line , 0 , 2 ) === '|+' ) {
$first_character = '+';
$line = substr ( $line , 1 );
}
$line = substr ( $line , 1 );
- if ( $first_character == '!' ) {
+ if ( $first_character === '!' ) {
$line = str_replace ( '!!' , '||' , $line );
}
@@ -861,13 +879,13 @@ class Parser
// attribute values containing literal "||".
$cells = StringUtils::explodeMarkup( '||' , $line );
- $lines[$key] = '';
+ $outLine = '';
// Loop through each table cell
foreach ( $cells as $cell )
{
$previous = '';
- if ( $first_character != '+' )
+ if ( $first_character !== '+' )
{
$tr_after = array_pop ( $tr_attributes );
if ( !array_pop ( $tr_history ) ) {
@@ -885,11 +903,11 @@ class Parser
$previous = "</{$last_tag}>{$previous}";
}
- if ( $first_character == '|' ) {
+ if ( $first_character === '|' ) {
$last_tag = 'td';
- } else if ( $first_character == '!' ) {
+ } else if ( $first_character === '!' ) {
$last_tag = 'th';
- } else if ( $first_character == '+' ) {
+ } else if ( $first_character === '+' ) {
$last_tag = 'caption';
} else {
$last_tag = '';
@@ -912,38 +930,42 @@ class Parser
$cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
}
- $lines[$key] .= $cell;
+ $outLine .= $cell;
array_push ( $td_history , true );
}
}
+ $out .= $outLine . "\n";
}
// Closing open td, tr && table
while ( count ( $td_history ) > 0 )
{
if ( array_pop ( $td_history ) ) {
- $lines[] = '</td>' ;
+ $out .= "</td>\n";
}
if ( array_pop ( $tr_history ) ) {
- $lines[] = '</tr>' ;
+ $out .= "</tr>\n";
}
if ( !array_pop ( $has_opened_tr ) ) {
- $lines[] = "<tr><td></td></tr>" ;
+ $out .= "<tr><td></td></tr>\n" ;
}
- $lines[] = '</table>' ;
+ $out .= "</table>\n";
}
- $output = implode ( "\n" , $lines ) ;
+ // Remove trailing line-ending (b/c)
+ if ( substr( $out, -1 ) === "\n" ) {
+ $out = substr( $out, 0, -1 );
+ }
// special case: don't return empty table
- if( $output == "<table>\n<tr><td></td></tr>\n</table>" ) {
- $output = '';
+ if( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
+ $out = '';
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
- return $output;
+ return $out;
}
/**
@@ -954,12 +976,11 @@ class Parser
*/
function internalParse( $text ) {
$isMain = true;
- $fname = 'Parser::internalParse';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
# Hook to suspend the parser in this state
if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $text ;
}
@@ -992,84 +1013,147 @@ class Parser
$text = $this->doMagicLinks( $text );
$text = $this->formatHeadings( $text, $isMain );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $text;
}
/**
* Replace special strings like "ISBN xxx" and "RFC xxx" with
* magic external links.
- *
+ *
+ * DML
* @private
*/
function doMagicLinks( $text ) {
wfProfileIn( __METHOD__ );
+ $prots = $this->mUrlProtocols;
+ $urlChar = self::EXT_LINK_URL_CLASS;
$text = preg_replace_callback(
'!(?: # Start cases
- <a.*?</a> | # Skip link text
- <.*?> | # Skip stuff inside HTML elements
- (?:RFC|PMID)\s+([0-9]+) | # RFC or PMID, capture number as m[1]
- ISBN\s+(\b # ISBN, capture number as m[2]
- (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix
- (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters
- [0-9Xx] # check digit
- \b)
+ (<a.*?</a>) | # m[1]: Skip link text
+ (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
+ (\\b(?:$prots)$urlChar+) | # m[3]: Free external links" . '
+ (?:RFC|PMID)\s+([0-9]+) | # m[4]: RFC or PMID, capture number
+ ISBN\s+(\b # m[5]: ISBN, capture number
+ (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix
+ (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters
+ [0-9Xx] # check digit
+ \b)
)!x', array( &$this, 'magicLinkCallback' ), $text );
wfProfileOut( __METHOD__ );
return $text;
}
function magicLinkCallback( $m ) {
- if ( substr( $m[0], 0, 1 ) == '<' ) {
+ if ( isset( $m[1] ) && strval( $m[1] ) !== '' ) {
+ # Skip anchor
+ return $m[0];
+ } elseif ( isset( $m[2] ) && strval( $m[2] ) !== '' ) {
# Skip HTML element
return $m[0];
- } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) {
- $isbn = $m[2];
- $num = strtr( $isbn, array(
- '-' => '',
- ' ' => '',
- 'x' => 'X',
- ));
- $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
- $text = '<a href="' .
- $titleObj->escapeLocalUrl() .
- "\" class=\"internal\">ISBN $isbn</a>";
- } else {
- if ( substr( $m[0], 0, 3 ) == 'RFC' ) {
+ } elseif ( isset( $m[3] ) && strval( $m[3] ) !== '' ) {
+ # Free external link
+ return $this->makeFreeExternalLink( $m[0] );
+ } elseif ( isset( $m[4] ) && strval( $m[4] ) !== '' ) {
+ # RFC or PMID
+ if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
$keyword = 'RFC';
$urlmsg = 'rfcurl';
- $id = $m[1];
- } elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) {
+ $id = $m[4];
+ } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
$keyword = 'PMID';
$urlmsg = 'pubmedurl';
- $id = $m[1];
+ $id = $m[4];
} else {
throw new MWException( __METHOD__.': unrecognised match type "' .
substr($m[0], 0, 20 ) . '"' );
}
-
$url = wfMsg( $urlmsg, $id);
$sk = $this->mOptions->getSkin();
$la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
- $text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
+ return "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
+ } elseif ( isset( $m[5] ) && strval( $m[5] ) !== '' ) {
+ # ISBN
+ $isbn = $m[5];
+ $num = strtr( $isbn, array(
+ '-' => '',
+ ' ' => '',
+ 'x' => 'X',
+ ));
+ $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
+ return'<a href="' .
+ $titleObj->escapeLocalUrl() .
+ "\" class=\"internal\">ISBN $isbn</a>";
+ } else {
+ return $m[0];
}
- return $text;
}
/**
+ * Make a free external link, given a user-supplied URL
+ * @return HTML
+ * @private
+ */
+ function makeFreeExternalLink( $url ) {
+ global $wgContLang;
+ wfProfileIn( __METHOD__ );
+
+ $sk = $this->mOptions->getSkin();
+ $trail = '';
+
+ # The characters '<' and '>' (which were escaped by
+ # removeHTMLtags()) should not be included in
+ # URLs, per RFC 2396.
+ $m2 = array();
+ if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
+ $trail = substr($url, $m2[0][1]) . $trail;
+ $url = substr($url, 0, $m2[0][1]);
+ }
+
+ # Move trailing punctuation to $trail
+ $sep = ',;\.:!?';
+ # If there is no left bracket, then consider right brackets fair game too
+ if ( strpos( $url, '(' ) === false ) {
+ $sep .= ')';
+ }
+
+ $numSepChars = strspn( strrev( $url ), $sep );
+ if ( $numSepChars ) {
+ $trail = substr( $url, -$numSepChars ) . $trail;
+ $url = substr( $url, 0, -$numSepChars );
+ }
+
+ $url = Sanitizer::cleanUrl( $url );
+
+ # Is this an external image?
+ $text = $this->maybeMakeExternalImage( $url );
+ if ( $text === false ) {
+ # Not an image, make a link
+ $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free',
+ $this->getExternalLinkAttribs() );
+ # Register it in the output object...
+ # Replace unnecessary URL escape codes with their equivalent characters
+ $pasteurized = self::replaceUnusualEscapes( $url );
+ $this->mOutput->addExternalLink( $pasteurized );
+ }
+ wfProfileOut( __METHOD__ );
+ return $text . $trail;
+ }
+
+
+ /**
* Parse headers and return html
*
* @private
*/
function doHeadings( $text ) {
- $fname = 'Parser::doHeadings';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
for ( $i = 6; $i >= 1; --$i ) {
$h = str_repeat( '=', $i );
$text = preg_replace( "/^$h(.+)$h\\s*$/m",
"<h$i>\\1</h$i>", $text );
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $text;
}
@@ -1079,15 +1163,14 @@ class Parser
* @return string the altered text
*/
function doAllQuotes( $text ) {
- $fname = 'Parser::doAllQuotes';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$outtext = '';
- $lines = explode( "\n", $text );
+ $lines = StringUtils::explode( "\n", $text );
foreach ( $lines as $line ) {
- $outtext .= $this->doQuotes ( $line ) . "\n";
+ $outtext .= $this->doQuotes( $line ) . "\n";
}
$outtext = substr($outtext, 0,-1);
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $outtext;
}
@@ -1149,9 +1232,9 @@ class Parser
{
$x1 = substr ($arr[$i-1], -1);
$x2 = substr ($arr[$i-1], -2, 1);
- if ($x1 == ' ') {
+ if ($x1 === ' ') {
if ($firstspace == -1) $firstspace = $i;
- } else if ($x2 == ' ') {
+ } else if ($x2 === ' ') {
if ($firstsingleletterword == -1) $firstsingleletterword = $i;
} else {
if ($firstmultiletterword == -1) $firstmultiletterword = $i;
@@ -1191,7 +1274,7 @@ class Parser
{
if (($i % 2) == 0)
{
- if ($state == 'both')
+ if ($state === 'both')
$buffer .= $r;
else
$output .= $r;
@@ -1200,41 +1283,41 @@ class Parser
{
if (strlen ($r) == 2)
{
- if ($state == 'i')
+ if ($state === 'i')
{ $output .= '</i>'; $state = ''; }
- else if ($state == 'bi')
+ else if ($state === 'bi')
{ $output .= '</i>'; $state = 'b'; }
- else if ($state == 'ib')
+ else if ($state === 'ib')
{ $output .= '</b></i><b>'; $state = 'b'; }
- else if ($state == 'both')
+ else if ($state === 'both')
{ $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; }
else # $state can be 'b' or ''
{ $output .= '<i>'; $state .= 'i'; }
}
else if (strlen ($r) == 3)
{
- if ($state == 'b')
+ if ($state === 'b')
{ $output .= '</b>'; $state = ''; }
- else if ($state == 'bi')
+ else if ($state === 'bi')
{ $output .= '</i></b><i>'; $state = 'i'; }
- else if ($state == 'ib')
+ else if ($state === 'ib')
{ $output .= '</b>'; $state = 'i'; }
- else if ($state == 'both')
+ else if ($state === 'both')
{ $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; }
else # $state can be 'i' or ''
{ $output .= '<b>'; $state .= 'b'; }
}
else if (strlen ($r) == 5)
{
- if ($state == 'b')
+ if ($state === 'b')
{ $output .= '</b><i>'; $state = 'i'; }
- else if ($state == 'i')
+ else if ($state === 'i')
{ $output .= '</i><b>'; $state = 'b'; }
- else if ($state == 'bi')
+ else if ($state === 'bi')
{ $output .= '</i></b>'; $state = ''; }
- else if ($state == 'ib')
+ else if ($state === 'ib')
{ $output .= '</b></i>'; $state = ''; }
- else if ($state == 'both')
+ else if ($state === 'both')
{ $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; }
else # ($state == '')
{ $buffer = ''; $state = 'both'; }
@@ -1243,21 +1326,21 @@ class Parser
$i++;
}
# Now close all remaining tags. Notice that the order is important.
- if ($state == 'b' || $state == 'ib')
+ if ($state === 'b' || $state === 'ib')
$output .= '</b>';
- if ($state == 'i' || $state == 'bi' || $state == 'ib')
+ if ($state === 'i' || $state === 'bi' || $state === 'ib')
$output .= '</i>';
- if ($state == 'bi')
+ if ($state === 'bi')
$output .= '</b>';
# There might be lonely ''''', so make sure we have a buffer
- if ($state == 'both' && $buffer)
+ if ($state === 'both' && $buffer)
$output .= '<b><i>'.$buffer.'</i></b>';
return $output;
}
}
/**
- * Replace external links
+ * Replace external links (REL)
*
* Note: this is all very hackish and the order of execution matters a lot.
* Make sure to run maintenance/parserTests.php if you change this code.
@@ -1266,14 +1349,12 @@ class Parser
*/
function replaceExternalLinks( $text ) {
global $wgContLang;
- $fname = 'Parser::replaceExternalLinks';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$sk = $this->mOptions->getSkin();
$bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
-
- $s = $this->replaceFreeExternalLinks( array_shift( $bits ) );
+ $s = array_shift( $bits );
$i = 0;
while ( $i<count( $bits ) ) {
@@ -1301,13 +1382,14 @@ class Parser
$dtrail = '';
# Set linktype for CSS - if URL==text, link is essentially free
- $linktype = ($text == $url) ? 'free' : 'text';
+ $linktype = ($text === $url) ? 'free' : 'text';
# No link text, e.g. [http://domain.tld/some.link]
if ( $text == '' ) {
# Autonumber if allowed. See bug #5918
if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) {
- $text = '[' . ++$this->mAutonumber . ']';
+ $langObj = $this->getFunctionLang();
+ $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
$linktype = 'autonumber';
} else {
# Otherwise just use the URL
@@ -1324,108 +1406,44 @@ class Parser
$url = Sanitizer::cleanUrl( $url );
- # Process the trail (i.e. everything after this link up until start of the next link),
- # replacing any non-bracketed links
- $trail = $this->replaceFreeExternalLinks( $trail );
+ if ( $this->mOptions->mExternalLinkTarget ) {
+ $attribs = array( 'target' => $this->mOptions->mExternalLinkTarget );
+ } else {
+ $attribs = array();
+ }
# Use the encoded URL
# This means that users can paste URLs directly into the text
# Funny characters like &ouml; aren't valid in URLs anyway
# This was changed in August 2004
- $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail;
+ $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->getExternalLinkAttribs() )
+ . $dtrail . $trail;
# Register link in the output object.
# Replace unnecessary URL escape codes with the referenced character
# This prevents spammers from hiding links from the filters
- $pasteurized = Parser::replaceUnusualEscapes( $url );
+ $pasteurized = self::replaceUnusualEscapes( $url );
$this->mOutput->addExternalLink( $pasteurized );
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $s;
}
- /**
- * Replace anything that looks like a URL with a link
- * @private
- */
- function replaceFreeExternalLinks( $text ) {
- global $wgContLang;
- $fname = 'Parser::replaceFreeExternalLinks';
- wfProfileIn( $fname );
-
- $bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
- $s = array_shift( $bits );
- $i = 0;
-
- $sk = $this->mOptions->getSkin();
-
- while ( $i < count( $bits ) ){
- $protocol = $bits[$i++];
- $remainder = $bits[$i++];
-
- $m = array();
- if ( preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) {
- # Found some characters after the protocol that look promising
- $url = $protocol . $m[1];
- $trail = $m[2];
-
- # special case: handle urls as url args:
- # http://www.example.com/foo?=http://www.example.com/bar
- if(strlen($trail) == 0 &&
- isset($bits[$i]) &&
- preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) &&
- preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m ))
- {
- # add protocol, arg
- $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link
- $i += 2;
- $trail = $m[2];
- }
-
- # The characters '<' and '>' (which were escaped by
- # removeHTMLtags()) should not be included in
- # URLs, per RFC 2396.
- $m2 = array();
- if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
- $trail = substr($url, $m2[0][1]) . $trail;
- $url = substr($url, 0, $m2[0][1]);
- }
-
- # Move trailing punctuation to $trail
- $sep = ',;\.:!?';
- # If there is no left bracket, then consider right brackets fair game too
- if ( strpos( $url, '(' ) === false ) {
- $sep .= ')';
- }
-
- $numSepChars = strspn( strrev( $url ), $sep );
- if ( $numSepChars ) {
- $trail = substr( $url, -$numSepChars ) . $trail;
- $url = substr( $url, 0, -$numSepChars );
- }
-
- $url = Sanitizer::cleanUrl( $url );
-
- # Is this an external image?
- $text = $this->maybeMakeExternalImage( $url );
- if ( $text === false ) {
- # Not an image, make a link
- $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
- # Register it in the output object...
- # Replace unnecessary URL escape codes with their equivalent characters
- $pasteurized = Parser::replaceUnusualEscapes( $url );
- $this->mOutput->addExternalLink( $pasteurized );
- }
- $s .= $text . $trail;
- } else {
- $s .= $protocol . $remainder;
- }
+ function getExternalLinkAttribs() {
+ $attribs = array();
+ global $wgNoFollowLinks, $wgNoFollowNsExceptions;
+ $ns = $this->mTitle->getNamespace();
+ if( $wgNoFollowLinks && !in_array($ns, $wgNoFollowNsExceptions) ) {
+ $attribs['rel'] = 'nofollow';
}
- wfProfileOut( $fname );
- return $s;
+ if ( $this->mOptions->getExternalLinkTarget() ) {
+ $attribs['target'] = $this->mOptions->getExternalLinkTarget();
+ }
+ return $attribs;
}
+
/**
* Replace unusual URL escape codes with their equivalent characters
* @param string
@@ -1438,7 +1456,7 @@ class Parser
*/
static function replaceUnusualEscapes( $url ) {
return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
- array( 'Parser', 'replaceUnusualEscapesCallback' ), $url );
+ array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url );
}
/**
@@ -1462,7 +1480,7 @@ class Parser
/**
* make an image if it's allowed, either through the global
- * option or through the exception
+ * option, through the exception, or through the on-wiki whitelist
* @private
*/
function maybeMakeExternalImage( $url ) {
@@ -1470,47 +1488,88 @@ class Parser
$imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
$imagesexception = !empty($imagesfrom);
$text = false;
+ # $imagesfrom could be either a single string or an array of strings, parse out the latter
+ if( $imagesexception && is_array( $imagesfrom ) ) {
+ $imagematch = false;
+ foreach( $imagesfrom as $match ) {
+ if( strpos( $url, $match ) === 0 ) {
+ $imagematch = true;
+ break;
+ }
+ }
+ } elseif( $imagesexception ) {
+ $imagematch = (strpos( $url, $imagesfrom ) === 0);
+ } else {
+ $imagematch = false;
+ }
if ( $this->mOptions->getAllowExternalImages()
- || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) {
+ || ( $imagesexception && $imagematch ) ) {
if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
# Image found
$text = $sk->makeExternalImage( $url );
}
}
+ if( !$text && $this->mOptions->getEnableImageWhitelist()
+ && preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
+ $whitelist = explode( "\n", wfMsgForContent( 'external_image_whitelist' ) );
+ foreach( $whitelist as $entry ) {
+ # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
+ if( strpos( $entry, '#' ) === 0 || $entry === '' )
+ continue;
+ if( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
+ # Image matches a whitelist entry
+ $text = $sk->makeExternalImage( $url );
+ break;
+ }
+ }
+ }
return $text;
}
/**
* Process [[ ]] wikilinks
+ * @return processed text
*
* @private
*/
function replaceInternalLinks( $s ) {
+ $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
+ return $s;
+ }
+
+ /**
+ * Process [[ ]] wikilinks (RIL)
+ * @return LinkHolderArray
+ *
+ * @private
+ */
+ function replaceInternalLinks2( &$s ) {
global $wgContLang;
- static $fname = 'Parser::replaceInternalLinks' ;
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
- wfProfileIn( $fname.'-setup' );
- static $tc = FALSE;
+ wfProfileIn( __METHOD__.'-setup' );
+ static $tc = FALSE, $e1, $e1_img;
# the % is needed to support urlencoded titles as well
- if ( !$tc ) { $tc = Title::legalChars() . '#%'; }
+ if ( !$tc ) {
+ $tc = Title::legalChars() . '#%';
+ # Match a link having the form [[namespace:link|alternate]]trail
+ $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
+ # Match cases where there is no "]]", which might still be images
+ $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
+ }
$sk = $this->mOptions->getSkin();
+ $holders = new LinkHolderArray( $this );
#split the entire text string on occurences of [[
- $a = explode( '[[', ' ' . $s );
+ $a = StringUtils::explode( '[[', ' ' . $s );
#get the first element (all text up to first [[), and remove the space we added
- $s = array_shift( $a );
+ $s = $a->current();
+ $a->next();
+ $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
$s = substr( $s, 1 );
- # Match a link having the form [[namespace:link|alternate]]trail
- static $e1 = FALSE;
- if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; }
- # Match cases where there is no "]]", which might still be images
- static $e1_img = FALSE;
- if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; }
-
$useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
$e2 = null;
if ( $useLinkPrefixExtension ) {
@@ -1520,8 +1579,8 @@ class Parser
}
if( is_null( $this->mTitle ) ) {
- wfProfileOut( $fname );
- wfProfileOut( $fname.'-setup' );
+ wfProfileOut( __METHOD__.'-setup' );
+ wfProfileOut( __METHOD__ );
throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
}
$nottalk = !$this->mTitle->isTalkPage();
@@ -1543,13 +1602,20 @@ class Parser
$selflink = array($this->mTitle->getPrefixedText());
}
$useSubpages = $this->areSubpagesAllowed();
- wfProfileOut( $fname.'-setup' );
+ wfProfileOut( __METHOD__.'-setup' );
# Loop for each link
- for ($k = 0; isset( $a[$k] ); $k++) {
- $line = $a[$k];
+ for ( ; $line !== false && $line !== null ; $a->next(), $line = $a->current() ) {
+ # Check for excessive memory usage
+ if ( $holders->isBig() ) {
+ # Too big
+ # Do the existence check, replace the link holders and clear the array
+ $holders->replace( $s );
+ $holders->clear();
+ }
+
if ( $useLinkPrefixExtension ) {
- wfProfileIn( $fname.'-prefixhandling' );
+ wfProfileIn( __METHOD__.'-prefixhandling' );
if ( preg_match( $e2, $s, $m ) ) {
$prefix = $m[2];
$s = $m[1];
@@ -1561,12 +1627,12 @@ class Parser
$prefix = $first_prefix;
$first_prefix = false;
}
- wfProfileOut( $fname.'-prefixhandling' );
+ wfProfileOut( __METHOD__.'-prefixhandling' );
}
$might_be_img = false;
- wfProfileIn( "$fname-e1" );
+ wfProfileIn( __METHOD__."-e1" );
if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
$text = $m[2];
# If we get a ] at the beginning of $m[3] that means we have a link that's something like:
@@ -1600,18 +1666,18 @@ class Parser
$trail = "";
} else { # Invalid form; output directly
$s .= $prefix . '[[' . $line ;
- wfProfileOut( "$fname-e1" );
+ wfProfileOut( __METHOD__."-e1" );
continue;
}
- wfProfileOut( "$fname-e1" );
- wfProfileIn( "$fname-misc" );
+ wfProfileOut( __METHOD__."-e1" );
+ wfProfileIn( __METHOD__."-misc" );
# Don't allow internal links to pages containing
# PROTO: where PROTO is a valid URL protocol; these
# should be external links.
if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) {
$s .= $prefix . '[[' . $line ;
- wfProfileOut( "$fname-misc" );
+ wfProfileOut( __METHOD__."-misc" );
continue;
}
@@ -1622,33 +1688,36 @@ class Parser
$link = $m[1];
}
- $noforce = (substr($m[1], 0, 1) != ':');
+ $noforce = (substr($m[1], 0, 1) !== ':');
if (!$noforce) {
# Strip off leading ':'
$link = substr($link, 1);
}
- wfProfileOut( "$fname-misc" );
- wfProfileIn( "$fname-title" );
+ wfProfileOut( __METHOD__."-misc" );
+ wfProfileIn( __METHOD__."-title" );
$nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
if( !$nt ) {
$s .= $prefix . '[[' . $line;
- wfProfileOut( "$fname-title" );
+ wfProfileOut( __METHOD__."-title" );
continue;
}
$ns = $nt->getNamespace();
$iw = $nt->getInterWiki();
- wfProfileOut( "$fname-title" );
+ wfProfileOut( __METHOD__."-title" );
if ($might_be_img) { # if this is actually an invalid link
- wfProfileIn( "$fname-might_be_img" );
- if ($ns == NS_IMAGE && $noforce) { #but might be an image
+ wfProfileIn( __METHOD__."-might_be_img" );
+ if ($ns == NS_FILE && $noforce) { #but might be an image
$found = false;
- while (isset ($a[$k+1]) ) {
+ while ( true ) {
#look at the next 'line' to see if we can close it there
- $spliced = array_splice( $a, $k + 1, 1 );
- $next_line = array_shift( $spliced );
+ $a->next();
+ $next_line = $a->current();
+ if ( $next_line === false || $next_line === null ) {
+ break;
+ }
$m = explode( ']]', $next_line, 3 );
if ( count( $m ) == 3 ) {
# the first ]] closes the inner link, the second the image
@@ -1668,19 +1737,19 @@ class Parser
if ( !$found ) {
# we couldn't find the end of this imageLink, so output it raw
#but don't ignore what might be perfectly normal links in the text we've examined
- $text = $this->replaceInternalLinks($text);
+ $holders->merge( $this->replaceInternalLinks2( $text ) );
$s .= "{$prefix}[[$link|$text";
# note: no $trail, because without an end, there *is* no trail
- wfProfileOut( "$fname-might_be_img" );
+ wfProfileOut( __METHOD__."-might_be_img" );
continue;
}
} else { #it's not an image, so output it raw
$s .= "{$prefix}[[$link|$text";
# note: no $trail, because without an end, there *is* no trail
- wfProfileOut( "$fname-might_be_img" );
+ wfProfileOut( __METHOD__."-might_be_img" );
continue;
}
- wfProfileOut( "$fname-might_be_img" );
+ wfProfileOut( __METHOD__."-might_be_img" );
}
$wasblank = ( '' == $text );
@@ -1690,41 +1759,36 @@ class Parser
if( $noforce ) {
# Interwikis
- wfProfileIn( "$fname-interwiki" );
+ wfProfileIn( __METHOD__."-interwiki" );
if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
$this->mOutput->addLanguageLink( $nt->getFullText() );
$s = rtrim($s . $prefix);
$s .= trim($trail, "\n") == '' ? '': $prefix . $trail;
- wfProfileOut( "$fname-interwiki" );
+ wfProfileOut( __METHOD__."-interwiki" );
continue;
}
- wfProfileOut( "$fname-interwiki" );
+ wfProfileOut( __METHOD__."-interwiki" );
- if ( $ns == NS_IMAGE ) {
- wfProfileIn( "$fname-image" );
+ if ( $ns == NS_FILE ) {
+ wfProfileIn( __METHOD__."-image" );
if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
# recursively parse links inside the image caption
# actually, this will parse them in any other parameters, too,
# but it might be hard to fix that, and it doesn't matter ATM
$text = $this->replaceExternalLinks($text);
- $text = $this->replaceInternalLinks($text);
+ $holders->merge( $this->replaceInternalLinks2( $text ) );
# cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
- $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text ) ) . $trail;
- $this->mOutput->addImage( $nt->getDBkey() );
-
- wfProfileOut( "$fname-image" );
- continue;
- } else {
- # We still need to record the image's presence on the page
- $this->mOutput->addImage( $nt->getDBkey() );
+ $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text, $holders ) ) . $trail;
}
- wfProfileOut( "$fname-image" );
+ $this->mOutput->addImage( $nt->getDBkey() );
+ wfProfileOut( __METHOD__."-image" );
+ continue;
}
if ( $ns == NS_CATEGORY ) {
- wfProfileIn( "$fname-category" );
+ wfProfileIn( __METHOD__."-category" );
$s = rtrim($s . "\n"); # bug 87
if ( $wasblank ) {
@@ -1743,26 +1807,27 @@ class Parser
*/
$s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
- wfProfileOut( "$fname-category" );
+ wfProfileOut( __METHOD__."-category" );
continue;
}
}
# Self-link checking
- if( $nt->getFragment() === '' ) {
+ if( $nt->getFragment() === '' && $ns != NS_SPECIAL ) {
if( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
$s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
continue;
}
}
- # Special and Media are pseudo-namespaces; no pages actually exist in them
+ # NS_MEDIA is a pseudo-namespace for linking directly to a file
+ # FIXME: Should do batch file existence checks, see comment below
if( $ns == NS_MEDIA ) {
# Give extensions a chance to select the file revision for us
$skip = $time = false;
wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$nt, &$skip, &$time ) );
if ( $skip ) {
- $link = $sk->makeLinkObj( $nt );
+ $link = $sk->link( $nt );
} else {
$link = $sk->makeMediaLinkObj( $nt, $text, $time );
}
@@ -1770,28 +1835,23 @@ class Parser
$s .= $prefix . $this->armorLinks( $link ) . $trail;
$this->mOutput->addImage( $nt->getDBkey() );
continue;
- } elseif( $ns == NS_SPECIAL ) {
- 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 = 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.
- $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
- $this->mOutput->addLink( $nt );
- continue;
- }
}
- $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
+
+ # Some titles, such as valid special pages or files in foreign repos, should
+ # be shown as bluelinks even though they're not included in the page table
+ #
+ # FIXME: isAlwaysKnown() can be expensive for file links; we should really do
+ # batch file existence checks for NS_FILE and NS_MEDIA
+ if( $iw == '' && $nt->isAlwaysKnown() ) {
+ $this->mOutput->addLink( $nt );
+ $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
+ } else {
+ # Links will be added to the output link list after checking
+ $s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
+ }
}
- wfProfileOut( $fname );
- return $s;
+ wfProfileOut( __METHOD__ );
+ return $holders;
}
/**
@@ -1800,32 +1860,10 @@ class Parser
* parsing of interwiki links, and secondly to allow all existence checks and
* article length checks (for stub links) to be bundled into a single query.
*
+ * @deprecated
*/
function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
- wfProfileIn( __METHOD__ );
- if ( ! is_object($nt) ) {
- # Fail gracefully
- $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
- } else {
- # Separate the link trail from the rest of the link
- list( $inside, $trail ) = Linker::splitTrail( $trail );
-
- if ( $nt->isExternal() ) {
- $nr = array_push( $this->mInterwikiLinkHolders['texts'], $prefix.$text.$inside );
- $this->mInterwikiLinkHolders['titles'][] = $nt;
- $retVal = '<!--IWLINK '. ($nr-1) ."-->{$trail}";
- } else {
- $nr = array_push( $this->mLinkHolders['namespaces'], $nt->getNamespace() );
- $this->mLinkHolders['dbkeys'][] = $nt->getDBkey();
- $this->mLinkHolders['queries'][] = $query;
- $this->mLinkHolders['texts'][] = $prefix.$text.$inside;
- $this->mLinkHolders['titles'][] = $nt;
-
- $retVal = '<!--LINK '. ($nr-1) ."-->{$trail}";
- }
- }
- wfProfileOut( __METHOD__ );
- return $retVal;
+ return $this->mLinkHolders->makeHolder( $nt, $text, $query, $trail, $prefix );
}
/**
@@ -1853,10 +1891,8 @@ class Parser
* Insert a NOPARSE hacky thing into any inline links in a chunk that's
* going to go through further parsing steps before inline URL expansion.
*
- * In particular this is important when using action=render, which causes
- * full URLs to be included.
- *
- * Oh man I hate our multi-layer parser!
+ * Not needed quite as much as it used to be since free links are a bit
+ * more sensible these days. But bracketed links are still an issue.
*
* @param string more-or-less HTML
* @return string less-or-more HTML with NOPARSE bits
@@ -1891,8 +1927,7 @@ class Parser
# ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
# ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
- $fname = 'Parser::maybeDoSubpageLink';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$ret = $target; # default return value is no change
# Some namespaces don't allow subpages,
@@ -1908,7 +1943,7 @@ class Parser
# bug 7425
$target = trim( $target );
# Look at the first character
- if( $target != '' && $target{0} == '/' ) {
+ if( $target != '' && $target{0} === '/' ) {
# / at end means we don't want the slash to be shown
$m = array();
$trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
@@ -1935,7 +1970,7 @@ class Parser
if( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
$ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
# / at the end means don't show full path
- if( substr( $nodotdot, -1, 1 ) == '/' ) {
+ if( substr( $nodotdot, -1, 1 ) === '/' ) {
$nodotdot = substr( $nodotdot, 0, -1 );
if( '' === $text ) {
$text = $nodotdot . $suffix;
@@ -1951,7 +1986,7 @@ class Parser
}
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $ret;
}
@@ -1987,10 +2022,10 @@ class Parser
/* private */ function openList( $char ) {
$result = $this->closeParagraph();
- if ( '*' == $char ) { $result .= '<ul><li>'; }
- else if ( '#' == $char ) { $result .= '<ol><li>'; }
- else if ( ':' == $char ) { $result .= '<dl><dd>'; }
- else if ( ';' == $char ) {
+ if ( '*' === $char ) { $result .= '<ul><li>'; }
+ else if ( '#' === $char ) { $result .= '<ol><li>'; }
+ else if ( ':' === $char ) { $result .= '<dl><dd>'; }
+ else if ( ';' === $char ) {
$result .= '<dl><dt>';
$this->mDTopen = true;
}
@@ -2000,11 +2035,11 @@ class Parser
}
/* private */ function nextItem( $char ) {
- if ( '*' == $char || '#' == $char ) { return '</li><li>'; }
- else if ( ':' == $char || ';' == $char ) {
+ if ( '*' === $char || '#' === $char ) { return '</li><li>'; }
+ else if ( ':' === $char || ';' === $char ) {
$close = '</dd>';
if ( $this->mDTopen ) { $close = '</dt>'; }
- if ( ';' == $char ) {
+ if ( ';' === $char ) {
$this->mDTopen = true;
return $close . '<dt>';
} else {
@@ -2016,9 +2051,9 @@ class Parser
}
/* private */ function closeList( $char ) {
- if ( '*' == $char ) { $text = '</li></ul>'; }
- else if ( '#' == $char ) { $text = '</li></ol>'; }
- else if ( ':' == $char ) {
+ if ( '*' === $char ) { $text = '</li></ul>'; }
+ else if ( '#' === $char ) { $text = '</li></ol>'; }
+ else if ( ':' === $char ) {
if ( $this->mDTopen ) {
$this->mDTopen = false;
$text = '</dt></dl>';
@@ -2032,56 +2067,59 @@ class Parser
/**#@-*/
/**
- * Make lists from lines starting with ':', '*', '#', etc.
+ * Make lists from lines starting with ':', '*', '#', etc. (DBL)
*
* @private
* @return string the lists rendered as HTML
*/
function doBlockLevels( $text, $linestart ) {
- $fname = 'Parser::doBlockLevels';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
# Parsing through the text line by line. The main thing
# happening here is handling of block-level elements p, pre,
# and making lists from lines starting with * # : etc.
#
- $textLines = explode( "\n", $text );
+ $textLines = StringUtils::explode( "\n", $text );
$lastPrefix = $output = '';
$this->mDTopen = $inBlockElem = false;
$prefixLength = 0;
$paragraphStack = false;
- if ( !$linestart ) {
- $output .= array_shift( $textLines );
- }
foreach ( $textLines as $oLine ) {
+ # Fix up $linestart
+ if ( !$linestart ) {
+ $output .= $oLine;
+ $linestart = true;
+ continue;
+ }
+
$lastPrefixLength = strlen( $lastPrefix );
$preCloseMatch = preg_match('/<\\/pre/i', $oLine );
$preOpenMatch = preg_match('/<pre/i', $oLine );
if ( !$this->mInPre ) {
# Multiple prefixes may abut each other for nested lists.
$prefixLength = strspn( $oLine, '*#:;' );
- $pref = substr( $oLine, 0, $prefixLength );
+ $prefix = substr( $oLine, 0, $prefixLength );
# eh?
- $pref2 = str_replace( ';', ':', $pref );
+ $prefix2 = str_replace( ';', ':', $prefix );
$t = substr( $oLine, $prefixLength );
- $this->mInPre = !empty($preOpenMatch);
+ $this->mInPre = (bool)$preOpenMatch;
} else {
# Don't interpret any other prefixes in preformatted text
$prefixLength = 0;
- $pref = $pref2 = '';
+ $prefix = $prefix2 = '';
$t = $oLine;
}
# List generation
- if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) {
+ if( $prefixLength && $lastPrefix === $prefix2 ) {
# Same as the last item, so no need to deal with nesting or opening stuff
- $output .= $this->nextItem( substr( $pref, -1 ) );
+ $output .= $this->nextItem( substr( $prefix, -1 ) );
$paragraphStack = false;
- if ( substr( $pref, -1 ) == ';') {
+ if ( substr( $prefix, -1 ) === ';') {
# The one nasty exception: definition lists work like this:
# ; title : definition text
# So we check for : in the remainder text to split up the
@@ -2094,21 +2132,21 @@ class Parser
}
} elseif( $prefixLength || $lastPrefixLength ) {
# Either open or close a level...
- $commonPrefixLength = $this->getCommon( $pref, $lastPrefix );
+ $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
$paragraphStack = false;
while( $commonPrefixLength < $lastPrefixLength ) {
- $output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} );
+ $output .= $this->closeList( $lastPrefix[$lastPrefixLength-1] );
--$lastPrefixLength;
}
if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
- $output .= $this->nextItem( $pref{$commonPrefixLength-1} );
+ $output .= $this->nextItem( $prefix[$commonPrefixLength-1] );
}
while ( $prefixLength > $commonPrefixLength ) {
- $char = substr( $pref, $commonPrefixLength, 1 );
+ $char = substr( $prefix, $commonPrefixLength, 1 );
$output .= $this->openList( $char );
- if ( ';' == $char ) {
+ if ( ';' === $char ) {
# FIXME: This is dupe of code above
if ($this->findColonNoLinks($t, $term, $t2) !== false) {
$t = $t2;
@@ -2117,10 +2155,10 @@ class Parser
}
++$commonPrefixLength;
}
- $lastPrefix = $pref2;
+ $lastPrefix = $prefix2;
}
if( 0 == $prefixLength ) {
- wfProfileIn( "$fname-paragraph" );
+ wfProfileIn( __METHOD__."-paragraph" );
# No prefix (not in list)--go to paragraph mode
// XXX: use a stack for nestable elements like span, table and div
$openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
@@ -2140,9 +2178,9 @@ class Parser
$inBlockElem = true;
}
} else if ( !$inBlockElem && !$this->mInPre ) {
- if ( ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) {
+ if ( ' ' == $t{0} and ( $this->mLastSection === 'pre' or trim($t) != '' ) ) {
// pre
- if ($this->mLastSection != 'pre') {
+ if ($this->mLastSection !== 'pre') {
$paragraphStack = false;
$output .= $this->closeParagraph().'<pre>';
$this->mLastSection = 'pre';
@@ -2156,7 +2194,7 @@ class Parser
$paragraphStack = false;
$this->mLastSection = 'p';
} else {
- if ($this->mLastSection != 'p' ) {
+ if ($this->mLastSection !== 'p' ) {
$output .= $this->closeParagraph();
$this->mLastSection = '';
$paragraphStack = '<p>';
@@ -2169,14 +2207,14 @@ class Parser
$output .= $paragraphStack;
$paragraphStack = false;
$this->mLastSection = 'p';
- } else if ($this->mLastSection != 'p') {
+ } else if ($this->mLastSection !== 'p') {
$output .= $this->closeParagraph().'<p>';
$this->mLastSection = 'p';
}
}
}
}
- wfProfileOut( "$fname-paragraph" );
+ wfProfileOut( __METHOD__."-paragraph" );
}
// somewhere above we forget to get out of pre block (bug 785)
if($preCloseMatch && $this->mInPre) {
@@ -2187,7 +2225,7 @@ class Parser
}
}
while ( $prefixLength ) {
- $output .= $this->closeList( $pref2{$prefixLength-1} );
+ $output .= $this->closeList( $prefix2[$prefixLength-1] );
--$prefixLength;
}
if ( '' != $this->mLastSection ) {
@@ -2195,7 +2233,7 @@ class Parser
$this->mLastSection = '';
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $output;
}
@@ -2208,13 +2246,12 @@ class Parser
* return string the position of the ':', or false if none found
*/
function findColonNoLinks($str, &$before, &$after) {
- $fname = 'Parser::findColonNoLinks';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$pos = strpos( $str, ':' );
if( $pos === false ) {
// Nothing to find!
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return false;
}
@@ -2223,7 +2260,7 @@ class Parser
// Easy; no tag nesting to worry about
$before = substr( $str, 0, $pos );
$after = substr( $str, $pos+1 );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $pos;
}
@@ -2247,7 +2284,7 @@ class Parser
// We found it!
$before = substr( $str, 0, $i );
$after = substr( $str, $i + 1 );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $i;
}
// Embedded in a tag; don't break it.
@@ -2257,7 +2294,7 @@ class Parser
$colon = strpos( $str, ':', $i );
if( $colon === false ) {
// Nothing else interesting
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return false;
}
$lt = strpos( $str, '<', $i );
@@ -2266,7 +2303,7 @@ class Parser
// We found it!
$before = substr( $str, 0, $colon );
$after = substr( $str, $colon + 1 );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $i;
}
}
@@ -2313,18 +2350,18 @@ class Parser
break;
case 3: // self::COLON_STATE_CLOSETAG:
// In a </tag>
- if( $c == ">" ) {
+ if( $c === ">" ) {
$stack--;
if( $stack < 0 ) {
- wfDebug( "Invalid input in $fname; too many close tags\n" );
- wfProfileOut( $fname );
+ wfDebug( __METHOD__.": Invalid input; too many close tags\n" );
+ wfProfileOut( __METHOD__ );
return false;
}
$state = self::COLON_STATE_TEXT;
}
break;
case self::COLON_STATE_TAGSLASH:
- if( $c == ">" ) {
+ if( $c === ">" ) {
// Yes, a self-closed tag <blah/>
$state = self::COLON_STATE_TEXT;
} else {
@@ -2333,33 +2370,33 @@ class Parser
}
break;
case 5: // self::COLON_STATE_COMMENT:
- if( $c == "-" ) {
+ if( $c === "-" ) {
$state = self::COLON_STATE_COMMENTDASH;
}
break;
case self::COLON_STATE_COMMENTDASH:
- if( $c == "-" ) {
+ if( $c === "-" ) {
$state = self::COLON_STATE_COMMENTDASHDASH;
} else {
$state = self::COLON_STATE_COMMENT;
}
break;
case self::COLON_STATE_COMMENTDASHDASH:
- if( $c == ">" ) {
+ if( $c === ">" ) {
$state = self::COLON_STATE_TEXT;
} else {
$state = self::COLON_STATE_COMMENT;
}
break;
default:
- throw new MWException( "State machine error in $fname" );
+ throw new MWException( "State machine error in " . __METHOD__ );
}
}
if( $stack > 0 ) {
- wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" );
+ wfDebug( __METHOD__.": Invalid input; not enough close tags (stack $stack, state $state)\n" );
return false;
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return false;
}
@@ -2552,9 +2589,11 @@ class Parser
case 'numberofpages':
return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
case 'numberofadmins':
- return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::admins() );
+ return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::numberingroup('sysop') );
case 'numberofedits':
return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::edits() );
+ case 'numberofviews':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::views() );
case 'currenttimestamp':
return $this->mVarCache[$index] = wfTimestamp( TS_MW, $ts );
case 'localtimestamp':
@@ -2589,12 +2628,11 @@ class Parser
* @private
*/
function initialiseVariables() {
- $fname = 'Parser::initialiseVariables';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$variableIDs = MagicWord::getVariableIDs();
$this->mVariables = new MagicWordArray( $variableIDs );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
/**
@@ -2663,8 +2701,7 @@ class Parser
return $text;
}
- $fname = __METHOD__;
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
if ( $frame === false ) {
$frame = $this->getPreprocessor()->newFrame();
@@ -2677,7 +2714,7 @@ class Parser
$flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
$text = $frame->expand( $dom, $flags );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $text;
}
@@ -2718,7 +2755,7 @@ class Parser
function limitationWarn( $limitationType, $current=null, $max=null) {
$msgName = $limitationType . '-warning';
//does no harm if $current and $max are present but are unnecessary for the message
- $warning = wfMsg( $msgName, $current, $max);
+ $warning = wfMsgExt( $msgName, array( 'parsemag', 'escape' ), $current, $max );
$this->mOutput->addWarning( $warning );
$cat = Title::makeTitleSafe( NS_CATEGORY, wfMsgForContent( $limitationType . '-category' ) );
if ( $cat ) {
@@ -2739,9 +2776,8 @@ class Parser
* @private
*/
function braceSubstitution( $piece, $frame ) {
- global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces;
- $fname = __METHOD__;
- wfProfileIn( $fname );
+ global $wgContLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces;
+ wfProfileIn( __METHOD__ );
wfProfileIn( __METHOD__.'-setup' );
# Flags
@@ -2855,7 +2891,7 @@ class Parser
# Workaround for PHP bug 35229 and similar
if ( !is_callable( $callback ) ) {
- throw new MWException( "Tag hook for $name is not callable\n" );
+ throw new MWException( "Tag hook for $function is not callable\n" );
}
$result = call_user_func_array( $callback, $allArgs );
$found = true;
@@ -2898,19 +2934,19 @@ class Parser
$titleText = $title->getPrefixedText();
# Check for language variants if the template is not found
if($wgContLang->hasVariants() && $title->getArticleID() == 0){
- $wgContLang->findVariantLink($part1, $title);
+ $wgContLang->findVariantLink( $part1, $title, true );
}
# Do infinite loop check
if ( !$frame->loopCheck( $title ) ) {
$found = true;
- $text = "<span class=\"error\">Template loop detected: [[$titleText]]</span>";
+ $text = '<span class="error">' . wfMsgForContent( 'parser-template-loop-warning', $titleText ) . '</span>';
wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
}
# Do recursion depth check
$limit = $this->mOptions->getMaxTemplateDepth();
if ( $frame->depth >= $limit ) {
$found = true;
- $text = "<span class=\"error\">Template recursion depth limit exceeded ($limit)</span>";
+ $text = '<span class="error">' . wfMsgForContent( 'parser-template-recursion-depth-warning', $limit ) . '</span>';
}
}
}
@@ -2928,7 +2964,7 @@ class Parser
}
} else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) {
$found = false; //access denied
- wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() );
+ wfDebug( __METHOD__.": template inclusion denied for " . $title->getPrefixedDBkey() );
} else {
list( $text, $title ) = $this->getTemplateDom( $title );
if ( $text !== false ) {
@@ -2962,7 +2998,7 @@ class Parser
# Recover the source wikitext and return it
if ( !$found ) {
$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return array( 'object' => $text );
}
@@ -3021,7 +3057,7 @@ class Parser
$ret = array( 'text' => $text );
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $ret;
}
@@ -3121,8 +3157,8 @@ class Parser
if( $rev ) {
$text = $rev->getText();
} elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
- global $wgLang;
- $message = $wgLang->lcfirst( $title->getText() );
+ global $wgContLang;
+ $message = $wgContLang->lcfirst( $title->getText() );
$text = wfMsgForContentNoTrans( $message );
if( wfEmptyMsg( $message, $text ) ) {
$text = false;
@@ -3308,7 +3344,7 @@ class Parser
}
}
- if ( $name == 'html' || $name == 'nowiki' ) {
+ if ( $name === 'html' || $name === 'nowiki' ) {
$this->mStripState->nowiki->setPair( $marker, $output );
} else {
$this->mStripState->general->setPair( $marker, $output );
@@ -3384,6 +3420,16 @@ class Parser
wfDebug( __METHOD__.": [[MediaWiki:hidden-category-category]] is not a valid title!\n" );
}
}
+ # (bug 8068) Allow control over whether robots index a page.
+ #
+ # FIXME (bug 14899): __INDEX__ always overrides __NOINDEX__ here! This
+ # is not desirable, the last one on the page should win.
+ if( isset( $this->mDoubleUnderscores['noindex'] ) ) {
+ $this->mOutput->setIndexPolicy( 'noindex' );
+ } elseif( isset( $this->mDoubleUnderscores['index'] ) ) {
+ $this->mOutput->setIndexPolicy( 'index' );
+ }
+
return $text;
}
@@ -3402,13 +3448,14 @@ class Parser
* @private
*/
function formatHeadings( $text, $isMain=true ) {
- global $wgMaxTocLevel, $wgContLang;
+ global $wgMaxTocLevel, $wgContLang, $wgEnforceHtmlIds;
$doNumberHeadings = $this->mOptions->getNumberHeadings();
- if( !$this->mTitle->quickUserCan( 'edit' ) ) {
+ $showEditLink = $this->mOptions->getEditSection();
+
+ // Do not call quickUserCan unless necessary
+ if( $showEditLink && !$this->mTitle->quickUserCan( 'edit' ) ) {
$showEditLink = 0;
- } else {
- $showEditLink = $this->mOptions->getEditSection();
}
# Inhibit editsection links if requested in the page
@@ -3554,12 +3601,7 @@ class Parser
# <!--LINK number-->
# turns into
# link text with suffix
- $safeHeadline = preg_replace( '/<!--LINK ([0-9]*)-->/e',
- "\$this->mLinkHolders['texts'][\$1]",
- $safeHeadline );
- $safeHeadline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e',
- "\$this->mInterwikiLinkHolders['texts'][\$1]",
- $safeHeadline );
+ $safeHeadline = $this->replaceLinkHoldersText( $safeHeadline );
# Strip out HTML (other than plain <sup> and <sub>: bug 8393)
$tocline = preg_replace(
@@ -3575,13 +3617,60 @@ class Parser
# Save headline for section edit hint before it's escaped
$headlineHint = $safeHeadline;
- $safeHeadline = Sanitizer::escapeId( $safeHeadline );
- # HTML names must be case-insensitively unique (bug 10721)
+
+ if ( $wgEnforceHtmlIds ) {
+ $legacyHeadline = false;
+ $safeHeadline = Sanitizer::escapeId( $safeHeadline,
+ 'noninitial' );
+ } else {
+ # For reverse compatibility, provide an id that's
+ # HTML4-compatible, like we used to.
+ #
+ # It may be worth noting, academically, that it's possible for
+ # the legacy anchor to conflict with a non-legacy headline
+ # anchor on the page. In this case likely the "correct" thing
+ # would be to either drop the legacy anchors or make sure
+ # they're numbered first. However, this would require people
+ # to type in section names like "abc_.D7.93.D7.90.D7.A4"
+ # manually, so let's not bother worrying about it.
+ $legacyHeadline = Sanitizer::escapeId( $safeHeadline,
+ 'noninitial' );
+ $safeHeadline = Sanitizer::escapeId( $safeHeadline, 'xml' );
+
+ if ( $legacyHeadline == $safeHeadline ) {
+ # No reason to have both (in fact, we can't)
+ $legacyHeadline = false;
+ } elseif ( $legacyHeadline != Sanitizer::escapeId(
+ $legacyHeadline, 'xml' ) ) {
+ # The legacy id is invalid XML. We used to allow this, but
+ # there's no reason to do so anymore. Backward
+ # compatibility will fail slightly in this case, but it's
+ # no big deal.
+ $legacyHeadline = false;
+ }
+ }
+
+ # HTML names must be case-insensitively unique (bug 10721). FIXME:
+ # Does this apply to Unicode characters? Because we aren't
+ # handling those here.
$arrayKey = strtolower( $safeHeadline );
+ if ( $legacyHeadline === false ) {
+ $legacyArrayKey = false;
+ } else {
+ $legacyArrayKey = strtolower( $legacyHeadline );
+ }
# count how many in assoc. array so we can track dupes in anchors
- isset( $refers[$arrayKey] ) ? $refers[$arrayKey]++ : $refers[$arrayKey] = 1;
- $refcount[$headlineCount] = $refers[$arrayKey];
+ if ( isset( $refers[$arrayKey] ) ) {
+ $refers[$arrayKey]++;
+ } else {
+ $refers[$arrayKey] = 1;
+ }
+ if ( isset( $refers[$legacyArrayKey] ) ) {
+ $refers[$legacyArrayKey]++;
+ } else {
+ $refers[$legacyArrayKey] = 1;
+ }
# Don't number the heading if it is the only one (looks silly)
if( $doNumberHeadings && count( $matches[3] ) > 1) {
@@ -3591,8 +3680,12 @@ class Parser
# Create the anchor for linking from the TOC to the section
$anchor = $safeHeadline;
- if($refcount[$headlineCount] > 1 ) {
- $anchor .= '_' . $refcount[$headlineCount];
+ $legacyAnchor = $legacyHeadline;
+ if ( $refers[$arrayKey] > 1 ) {
+ $anchor .= '_' . $refers[$arrayKey];
+ }
+ if ( $legacyHeadline !== false && $refers[$legacyArrayKey] > 1 ) {
+ $legacyAnchor .= '_' . $refers[$legacyArrayKey];
}
if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
$toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
@@ -3603,14 +3696,16 @@ class Parser
if( $isTemplate ) {
# Put a T flag in the section identifier, to indicate to extractSections()
# that sections inside <includeonly> should be counted.
- $editlink = $sk->editSectionLinkForOther($titleText, "T-$sectionIndex");
+ $editlink = $sk->doEditSectionLink(Title::newFromText( $titleText ), "T-$sectionIndex");
} else {
- $editlink = $sk->editSectionLink($this->mTitle, $sectionIndex, $headlineHint);
+ $editlink = $sk->doEditSectionLink($this->mTitle, $sectionIndex, $headlineHint);
}
} else {
$editlink = '';
}
- $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink );
+ $head[$headlineCount] = $sk->makeHeadline( $level,
+ $matches['attrib'][$headlineCount], $anchor, $headline,
+ $editlink, $legacyAnchor );
$headlineCount++;
}
@@ -3635,7 +3730,7 @@ class Parser
$i = 0;
foreach( $blocks as $block ) {
- if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) {
+ if( $showEditLink && $headlineCount > 0 && $i == 0 && $block !== "\n" ) {
# This is the [edit] link that appears for the top block of text when
# section editing is enabled
@@ -3737,11 +3832,13 @@ class Parser
$nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
$p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/"; # [[ns:page (context)|]]
+ $p4 = "/\[\[(:?$nc+:|:|)($tc+?)(($tc+))\\|]]/"; # [[ns:page(context)|]]
$p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/"; # [[ns:page (context), context|]]
$p2 = "/\[\[\\|($tc+)]]/"; # [[|page]]
# try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
$text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
+ $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
$text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
$t = $this->mTitle->getText();
@@ -3787,7 +3884,7 @@ class Parser
} else {
# Failed to validate; fall back to the default
$nickname = $username;
- wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" );
+ wfDebug( __METHOD__.": $username has bad XML tags in signature.\n" );
}
}
@@ -3811,7 +3908,7 @@ class Parser
* @return mixed An expanded string, or false if invalid.
*/
function validateSig( $text ) {
- return( wfIsWellFormedXmlFragment( $text ) ? $text : false );
+ return( Xml::isWellFormedXmlFragment( $text ) ? $text : false );
}
/**
@@ -3833,6 +3930,11 @@ class Parser
$this->setOutputType = self::OT_PREPROCESS;
}
+ # Option to disable this feature
+ if ( !$this->mOptions->getCleanSignatures() ) {
+ return $text;
+ }
+
# FIXME: regex doesn't respect extension tags or nowiki
# => Move this logic to braceSubstitution()
$substWord = MagicWord::get( 'subst' );
@@ -3888,19 +3990,17 @@ class Parser
global $wgTitle;
static $executing = false;
- $fname = "Parser::transformMsg";
-
# Guard against infinite recursion
if ( $executing ) {
return $text;
}
$executing = true;
- wfProfileIn($fname);
+ wfProfileIn(__METHOD__);
$text = $this->preprocess( $text, $wgTitle, $options );
$executing = false;
- wfProfileOut($fname);
+ wfProfileOut(__METHOD__);
return $text;
}
@@ -3997,7 +4097,7 @@ class Parser
# Add to function cache
$mw = MagicWord::get( $id );
if( !$mw )
- throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' );
+ throw new MWException( __METHOD__.'() expecting a magic word identifier.' );
$synonyms = $mw->getSynonyms();
$sensitive = intval( $mw->isCaseSensitive() );
@@ -4012,7 +4112,7 @@ class Parser
$syn = '#' . $syn;
}
# Remove trailing colon
- if ( substr( $syn, -1, 1 ) == ':' ) {
+ if ( substr( $syn, -1, 1 ) === ':' ) {
$syn = substr( $syn, 0, -1 );
}
$this->mFunctionSynonyms[$sensitive][$syn] = $id;
@@ -4033,266 +4133,9 @@ class Parser
* Replace <!--LINK--> link placeholders with actual links, in the buffer
* Placeholders created in Skin::makeLinkObj()
* Returns an array of link CSS classes, indexed by PDBK.
- * $options is a bit field, RLH_FOR_UPDATE to select for update
*/
function replaceLinkHolders( &$text, $options = 0 ) {
- global $wgUser;
- global $wgContLang;
-
- $fname = 'Parser::replaceLinkHolders';
- wfProfileIn( $fname );
-
- $pdbks = array();
- $colours = array();
- $linkcolour_ids = array();
- $sk = $this->mOptions->getSkin();
- $linkCache = LinkCache::singleton();
-
- if ( !empty( $this->mLinkHolders['namespaces'] ) ) {
- wfProfileIn( $fname.'-check' );
- $dbr = wfGetDB( DB_SLAVE );
- $page = $dbr->tableName( 'page' );
- $threshold = $wgUser->getOption('stubthreshold');
-
- # Sort by namespace
- asort( $this->mLinkHolders['namespaces'] );
-
- # Generate query
- $query = false;
- $current = null;
- foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
- # Make title object
- $title = $this->mLinkHolders['titles'][$key];
-
- # Skip invalid entries.
- # Result will be ugly, but prevents crash.
- if ( is_null( $title ) ) {
- continue;
- }
- $pdbk = $pdbks[$key] = $title->getPrefixedDBkey();
-
- # Check if it's a static known link, e.g. interwiki
- if ( $title->isAlwaysKnown() ) {
- $colours[$pdbk] = '';
- } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
- $colours[$pdbk] = '';
- $this->mOutput->addLink( $title, $id );
- } elseif ( $linkCache->isBadLink( $pdbk ) ) {
- $colours[$pdbk] = 'new';
- } elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) {
- $colours[$pdbk] = 'new';
- } else {
- # Not in the link cache, add it to the query
- if ( !isset( $current ) ) {
- $current = $ns;
- $query = "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len";
- $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN(";
- } elseif ( $current != $ns ) {
- $current = $ns;
- $query .= ")) OR (page_namespace=$ns AND page_title IN(";
- } else {
- $query .= ', ';
- }
-
- $query .= $dbr->addQuotes( $this->mLinkHolders['dbkeys'][$key] );
- }
- }
- if ( $query ) {
- $query .= '))';
- if ( $options & RLH_FOR_UPDATE ) {
- $query .= ' FOR UPDATE';
- }
-
- $res = $dbr->query( $query, $fname );
-
- # Fetch data and form into an associative array
- # non-existent = broken
- while ( $s = $dbr->fetchObject($res) ) {
- $title = Title::makeTitle( $s->page_namespace, $s->page_title );
- $pdbk = $title->getPrefixedDBkey();
- $linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect );
- $this->mOutput->addLink( $title, $s->page_id );
- $colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
- //add id to the extension todolist
- $linkcolour_ids[$s->page_id] = $pdbk;
- }
- //pass an array of page_ids to an extension
- wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
- }
- wfProfileOut( $fname.'-check' );
-
- # Do a second query for different language variants of links and categories
- if($wgContLang->hasVariants()){
- $linkBatch = new LinkBatch();
- $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders)
- $categoryMap = array(); // maps $category_variant => $category (dbkeys)
- $varCategories = array(); // category replacements oldDBkey => newDBkey
-
- $categories = $this->mOutput->getCategoryLinks();
-
- // Add variants of links to link batch
- foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
- $title = $this->mLinkHolders['titles'][$key];
- if ( is_null( $title ) )
- continue;
-
- $pdbk = $title->getPrefixedDBkey();
- $titleText = $title->getText();
-
- // generate all variants of the link title text
- $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText);
-
- // if link was not found (in first query), add all variants to query
- if ( !isset($colours[$pdbk]) ){
- foreach($allTextVariants as $textVariant){
- if($textVariant != $titleText){
- $variantTitle = Title::makeTitle( $ns, $textVariant );
- if(is_null($variantTitle)) continue;
- $linkBatch->addObj( $variantTitle );
- $variantMap[$variantTitle->getPrefixedDBkey()][] = $key;
- }
- }
- }
- }
-
- // process categories, check if a category exists in some variant
- foreach( $categories as $category ){
- $variants = $wgContLang->convertLinkToAllVariants($category);
- foreach($variants as $variant){
- if($variant != $category){
- $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) );
- if(is_null($variantTitle)) continue;
- $linkBatch->addObj( $variantTitle );
- $categoryMap[$variant] = $category;
- }
- }
- }
-
-
- if(!$linkBatch->isEmpty()){
- // construct query
- $titleClause = $linkBatch->constructSet('page', $dbr);
-
- $variantQuery = "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len";
-
- $variantQuery .= " FROM $page WHERE $titleClause";
- if ( $options & RLH_FOR_UPDATE ) {
- $variantQuery .= ' FOR UPDATE';
- }
-
- $varRes = $dbr->query( $variantQuery, $fname );
-
- // for each found variants, figure out link holders and replace
- while ( $s = $dbr->fetchObject($varRes) ) {
-
- $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
- $varPdbk = $variantTitle->getPrefixedDBkey();
- $vardbk = $variantTitle->getDBkey();
-
- $holderKeys = array();
- if(isset($variantMap[$varPdbk])){
- $holderKeys = $variantMap[$varPdbk];
- $linkCache->addGoodLinkObj( $s->page_id, $variantTitle, $s->page_len, $s->page_is_redirect );
- $this->mOutput->addLink( $variantTitle, $s->page_id );
- }
-
- // loop over link holders
- foreach($holderKeys as $key){
- $title = $this->mLinkHolders['titles'][$key];
- if ( is_null( $title ) ) continue;
-
- $pdbk = $title->getPrefixedDBkey();
-
- if(!isset($colours[$pdbk])){
- // found link in some of the variants, replace the link holder data
- $this->mLinkHolders['titles'][$key] = $variantTitle;
- $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey();
-
- // set pdbk and colour
- $pdbks[$key] = $varPdbk;
- $colours[$varPdbk] = $sk->getLinkColour( $variantTitle, $threshold );
- $linkcolour_ids[$s->page_id] = $pdbk;
- }
- wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
- }
-
- // check if the object is a variant of a category
- if(isset($categoryMap[$vardbk])){
- $oldkey = $categoryMap[$vardbk];
- if($oldkey != $vardbk)
- $varCategories[$oldkey]=$vardbk;
- }
- }
-
- // rebuild the categories in original order (if there are replacements)
- if(count($varCategories)>0){
- $newCats = array();
- $originalCats = $this->mOutput->getCategories();
- foreach($originalCats as $cat => $sortkey){
- // make the replacement
- if( array_key_exists($cat,$varCategories) )
- $newCats[$varCategories[$cat]] = $sortkey;
- else $newCats[$cat] = $sortkey;
- }
- $this->mOutput->setCategoryLinks($newCats);
- }
- }
- }
-
- # Construct search and replace arrays
- wfProfileIn( $fname.'-construct' );
- $replacePairs = array();
- foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
- $pdbk = $pdbks[$key];
- $searchkey = "<!--LINK $key-->";
- $title = $this->mLinkHolders['titles'][$key];
- if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] == 'new' ) {
- $linkCache->addBadLinkObj( $title );
- $colours[$pdbk] = 'new';
- $this->mOutput->addLink( $title, 0 );
- $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title,
- $this->mLinkHolders['texts'][$key],
- $this->mLinkHolders['queries'][$key] );
- } else {
- $replacePairs[$searchkey] = $sk->makeColouredLinkObj( $title, $colours[$pdbk],
- $this->mLinkHolders['texts'][$key],
- $this->mLinkHolders['queries'][$key] );
- }
- }
- $replacer = new HashtableReplacer( $replacePairs, 1 );
- wfProfileOut( $fname.'-construct' );
-
- # Do the thing
- wfProfileIn( $fname.'-replace' );
- $text = preg_replace_callback(
- '/(<!--LINK .*?-->)/',
- $replacer->cb(),
- $text);
-
- wfProfileOut( $fname.'-replace' );
- }
-
- # Now process interwiki link holders
- # This is quite a bit simpler than internal links
- if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) {
- wfProfileIn( $fname.'-interwiki' );
- # Make interwiki link HTML
- $replacePairs = array();
- foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) {
- $title = $this->mInterwikiLinkHolders['titles'][$key];
- $replacePairs[$key] = $sk->makeLinkObj( $title, $link );
- }
- $replacer = new HashtableReplacer( $replacePairs, 1 );
-
- $text = preg_replace_callback(
- '/<!--IWLINK (.*?)-->/',
- $replacer->cb(),
- $text );
- wfProfileOut( $fname.'-interwiki' );
- }
-
- wfProfileOut( $fname );
- return $colours;
+ return $this->mLinkHolders->replace( $text );
}
/**
@@ -4302,36 +4145,7 @@ class Parser
* @return string
*/
function replaceLinkHoldersText( $text ) {
- $fname = 'Parser::replaceLinkHoldersText';
- wfProfileIn( $fname );
-
- $text = preg_replace_callback(
- '/<!--(LINK|IWLINK) (.*?)-->/',
- array( &$this, 'replaceLinkHoldersTextCallback' ),
- $text );
-
- wfProfileOut( $fname );
- return $text;
- }
-
- /**
- * @param array $matches
- * @return string
- * @private
- */
- function replaceLinkHoldersTextCallback( $matches ) {
- $type = $matches[1];
- $key = $matches[2];
- if( $type == 'LINK' ) {
- if( isset( $this->mLinkHolders['texts'][$key] ) ) {
- return $this->mLinkHolders['texts'][$key];
- }
- } elseif( $type == 'IWLINK' ) {
- if( isset( $this->mInterwikiLinkHolders['texts'][$key] ) ) {
- return $this->mInterwikiLinkHolders['texts'][$key];
- }
- }
- return $matches[0];
+ return $this->mLinkHolders->replaceText( $text );
}
/**
@@ -4342,7 +4156,7 @@ class Parser
$content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' );
$attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
- return wfOpenElement( 'pre', $attribs ) .
+ return Xml::openElement( 'pre', $attribs ) .
Xml::escapeTagsOnly( $content ) .
'</pre>';
}
@@ -4385,7 +4199,7 @@ class Parser
wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
- $lines = explode( "\n", $text );
+ $lines = StringUtils::explode( "\n", $text );
foreach ( $lines as $line ) {
# match lines like these:
# Image:someimage.jpg|This is some image
@@ -4398,7 +4212,7 @@ class Parser
if ( strpos( $matches[0], '%' ) !== false )
$matches[1] = urldecode( $matches[1] );
- $tp = Title::newFromText( $matches[1] );
+ $tp = Title::newFromText( $matches[1]/*, NS_FILE*/ );
$nt =& $tp;
if( is_null( $nt ) ) {
# Bogus title. Ignore these so we don't bomb out later.
@@ -4415,7 +4229,7 @@ class Parser
$ig->add( $nt, $html );
# Only add real images (bug #5586)
- if ( $nt->getNamespace() == NS_IMAGE ) {
+ if ( $nt->getNamespace() == NS_FILE ) {
$this->mOutput->addImage( $nt->getDBkey() );
}
}
@@ -4435,7 +4249,7 @@ class Parser
'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
'bottom', 'text-bottom' ),
'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
- 'upright', 'border' ),
+ 'upright', 'border', 'link', 'alt' ),
);
static $internalParamMap;
if ( !$internalParamMap ) {
@@ -4464,20 +4278,24 @@ class Parser
/**
* Parse image options text and use it to make an image
+ * @param Title $title
+ * @param string $options
+ * @param LinkHolderArray $holders
*/
- function makeImage( $title, $options ) {
+ function makeImage( $title, $options, $holders = false ) {
# Check if the options text is of the form "options|alt text"
# Options are:
- # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
- # * left no resizing, just left align. label is used for alt= only
- # * right same, but right aligned
- # * none same, but not aligned
- # * ___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
+ # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
+ # * left no resizing, just left align. label is used for alt= only
+ # * right same, but right aligned
+ # * none same, but not aligned
+ # * ___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
+ # * alt Text for HTML alt attribute (defaults to empty)
# vertical-align values (no % or length right now):
# * baseline
# * sub
@@ -4488,7 +4306,7 @@ class Parser
# * bottom
# * text-bottom
- $parts = array_map( 'trim', explode( '|', $options) );
+ $parts = StringUtils::explode( "|", $options );
$sk = $this->mOptions->getSkin();
# Give extensions a chance to select the file revision for us
@@ -4496,11 +4314,21 @@ class Parser
wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time, &$descQuery ) );
if ( $skip ) {
- return $sk->makeLinkObj( $title );
+ return $sk->link( $title );
}
+ # Get the file
+ $imagename = $title->getDBkey();
+ if ( isset( $this->mFileCache[$imagename][$time] ) ) {
+ $file = $this->mFileCache[$imagename][$time];
+ } else {
+ $file = wfFindFile( $title, $time );
+ if ( count( $this->mFileCache ) > 1000 ) {
+ $this->mFileCache = array();
+ }
+ $this->mFileCache[$imagename][$time] = $file;
+ }
# Get parameter map
- $file = wfFindFile( $title, $time );
$handler = $file ? $file->getHandler() : false;
list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
@@ -4510,13 +4338,14 @@ class Parser
$params = array( 'frame' => array(), 'handler' => array(),
'horizAlign' => array(), 'vertAlign' => array() );
foreach( $parts as $part ) {
+ $part = trim( $part );
list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
$validated = false;
if( isset( $paramMap[$magicName] ) ) {
list( $type, $paramName ) = $paramMap[$magicName];
// Special case; width and height come in one variable together
- if( $type == 'handler' && $paramName == 'width' ) {
+ if( $type === 'handler' && $paramName === 'width' ) {
$m = array();
# (bug 13500) In both cases (width/height and width only),
# permit trailing "px" for backward compatibility.
@@ -4539,16 +4368,42 @@ class Parser
}
} // else no validation -- bug 13436
} else {
- if ( $type == 'handler' ) {
+ if ( $type === 'handler' ) {
# Validate handler parameter
$validated = $handler->validateParam( $paramName, $value );
} else {
# Validate internal parameters
switch( $paramName ) {
- case "manualthumb":
- /// @fixme - possibly check validity here?
- /// downstream behavior seems odd with missing manual thumbs.
+ case 'manualthumb':
+ case 'alt':
+ // @fixme - possibly check validity here for
+ // manualthumb? downstream behavior seems odd with
+ // missing manual thumbs.
$validated = true;
+ $value = $this->stripAltText( $value, $holders );
+ break;
+ case 'link':
+ $chars = self::EXT_LINK_URL_CLASS;
+ $prots = $this->mUrlProtocols;
+ if ( $value === '' ) {
+ $paramName = 'no-link';
+ $value = true;
+ $validated = true;
+ } elseif ( preg_match( "/^$prots/", $value ) ) {
+ if ( preg_match( "/^($prots)$chars+$/", $value, $m ) ) {
+ $paramName = 'link-url';
+ $this->mOutput->addExternalLink( $value );
+ $validated = true;
+ }
+ } else {
+ $linkTitle = Title::newFromText( $value );
+ if ( $linkTitle ) {
+ $paramName = 'link-title';
+ $value = $linkTitle;
+ $this->mOutput->addLink( $linkTitle );
+ $validated = true;
+ }
+ }
break;
default:
// Most other things appear to be empty or numeric...
@@ -4574,17 +4429,32 @@ class Parser
$params['frame']['valign'] = key( $params['vertAlign'] );
}
- # Strip bad stuff out of the alt text
- $alt = $this->replaceLinkHoldersText( $caption );
+ $params['frame']['caption'] = $caption;
- # make sure there are no placeholders in thumbnail attributes
- # that are later expanded to html- so expand them now and
- # remove the tags
- $alt = $this->mStripState->unstripBoth( $alt );
- $alt = Sanitizer::stripAllTags( $alt );
+ $params['frame']['title'] = $this->stripAltText( $caption, $holders );
- $params['frame']['alt'] = $alt;
- $params['frame']['caption'] = $caption;
+ # In the old days, [[Image:Foo|text...]] would set alt text. Later it
+ # came to also set the caption, ordinary text after the image -- which
+ # makes no sense, because that just repeats the text multiple times in
+ # screen readers. It *also* came to set the title attribute.
+ #
+ # Now that we have an alt attribute, we should not set the alt text to
+ # equal the caption: that's worse than useless, it just repeats the
+ # text. This is the framed/thumbnail case. If there's no caption, we
+ # use the unnamed parameter for alt text as well, just for the time be-
+ # ing, if the unnamed param is set and the alt param is not.
+ #
+ # For the future, we need to figure out if we want to tweak this more,
+ # e.g., introducing a title= parameter for the title; ignoring the un-
+ # named parameter entirely for images without a caption; adding an ex-
+ # plicit caption= parameter and preserving the old magic unnamed para-
+ # meter for BC; ...
+ if( $caption !== '' && !isset( $params['frame']['alt'] )
+ && !isset( $params['frame']['framed'] )
+ && !isset( $params['frame']['thumbnail'] )
+ && !isset( $params['frame']['manualthumb'] ) ) {
+ $params['frame']['alt'] = $params['frame']['title'];
+ }
wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) );
@@ -4598,6 +4468,25 @@ class Parser
return $ret;
}
+
+ protected function stripAltText( $caption, $holders ) {
+ # Strip bad stuff out of the title (tooltip). We can't just use
+ # replaceLinkHoldersText() here, because if this function is called
+ # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
+ if ( $holders ) {
+ $tooltip = $holders->replaceText( $caption );
+ } else {
+ $tooltip = $this->replaceLinkHoldersText( $caption );
+ }
+
+ # make sure there are no placeholders in thumbnail attributes
+ # that are later expanded to html- so expand them now and
+ # remove the tags
+ $tooltip = $this->mStripState->unstripBoth( $tooltip );
+ $tooltip = Sanitizer::stripAllTags( $tooltip );
+
+ return $tooltip;
+ }
/**
* Set a flag in the output object indicating that the content is dynamic and
@@ -4678,7 +4567,7 @@ class Parser
$sectionParts = explode( '-', $section );
$sectionIndex = array_pop( $sectionParts );
foreach ( $sectionParts as $part ) {
- if ( $part == 'T' ) {
+ if ( $part === 'T' ) {
$flags |= self::PTD_FOR_INCLUSION;
}
}
@@ -4695,14 +4584,14 @@ class Parser
$targetLevel = 1000;
} else {
while ( $node ) {
- if ( $node->getName() == 'h' ) {
+ if ( $node->getName() === 'h' ) {
$bits = $node->splitHeading();
if ( $bits['i'] == $sectionIndex ) {
$targetLevel = $bits['level'];
break;
}
}
- if ( $mode == 'replace' ) {
+ if ( $mode === 'replace' ) {
$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
}
$node = $node->getNextSibling();
@@ -4711,7 +4600,7 @@ class Parser
if ( !$node ) {
// Not found
- if ( $mode == 'get' ) {
+ if ( $mode === 'get' ) {
return $newText;
} else {
return $text;
@@ -4720,21 +4609,21 @@ class Parser
// Find the end of the section, including nested sections
do {
- if ( $node->getName() == 'h' ) {
+ if ( $node->getName() === 'h' ) {
$bits = $node->splitHeading();
$curLevel = $bits['level'];
if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
break;
}
}
- if ( $mode == 'get' ) {
+ if ( $mode === 'get' ) {
$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
}
$node = $node->getNextSibling();
} while ( $node );
// Write out the remainder (in replace mode only)
- if ( $mode == 'replace' ) {
+ if ( $mode === 'replace' ) {
// Output the replacement text
// Add two newlines on -- trailing whitespace in $newText is conventionally
// stripped by the editor, so we need both newlines to restore the paragraph gap
@@ -4820,16 +4709,28 @@ class Parser
* @return string
*/
public function getDefaultSort() {
+ global $wgCategoryPrefixedDefaultSortkey;
if( $this->mDefaultSort !== false ) {
return $this->mDefaultSort;
+ } elseif ($this->mTitle->getNamespace() == NS_CATEGORY ||
+ !$wgCategoryPrefixedDefaultSortkey) {
+ return $this->mTitle->getText();
} else {
- return $this->mTitle->getNamespace() == NS_CATEGORY
- ? $this->mTitle->getText()
- : $this->mTitle->getPrefixedText();
+ return $this->mTitle->getPrefixedText();
}
}
/**
+ * Accessor for $mDefaultSort
+ * Unlike getDefaultSort(), will return false if none is set
+ *
+ * @return string or false
+ */
+ public function getCustomDefaultSort() {
+ return $this->mDefaultSort;
+ }
+
+ /**
* Try to guess the section anchor name based on a wikitext fragment
* presumably extracted from a heading, for example "Header" from
* "== Header ==".
@@ -4962,7 +4863,7 @@ class StripState {
do {
$oldText = $text;
$text = $this->general->replace( $text );
- } while ( $text != $oldText );
+ } while ( $text !== $oldText );
wfProfileOut( __METHOD__ );
return $text;
}
@@ -4972,7 +4873,7 @@ class StripState {
do {
$oldText = $text;
$text = $this->nowiki->replace( $text );
- } while ( $text != $oldText );
+ } while ( $text !== $oldText );
wfProfileOut( __METHOD__ );
return $text;
}
@@ -4983,7 +4884,7 @@ class StripState {
$oldText = $text;
$text = $this->general->replace( $text );
$text = $this->nowiki->replace( $text );
- } while ( $text != $oldText );
+ } while ( $text !== $oldText );
wfProfileOut( __METHOD__ );
return $text;
}
@@ -4997,7 +4898,7 @@ class OnlyIncludeReplacer {
var $output = '';
function replace( $matches ) {
- if ( substr( $matches[1], -1 ) == "\n" ) {
+ if ( substr( $matches[1], -1 ) === "\n" ) {
$this->output .= substr( $matches[1], 0, -1 );
} else {
$this->output .= $matches[1];
diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php
index bf11da2e..7e61157a 100644
--- a/includes/parser/ParserCache.php
+++ b/includes/parser/ParserCache.php
@@ -35,9 +35,9 @@ class ParserCache {
} else {
$edit = '';
}
- $pageid = intval( $article->getID() );
+ $pageid = $article->getID();
$renderkey = (int)($action == 'render');
- $key = wfMemcKey( 'pcache', 'idhash', "$pageid-$renderkey!$hash$edit" );
+ $key = wfMemcKey( 'pcache', 'idhash', "{$pageid}-{$renderkey}!{$hash}{$edit}" );
return $key;
}
diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php
index 330ec446..5b8cd3ee 100644
--- a/includes/parser/ParserOptions.php
+++ b/includes/parser/ParserOptions.php
@@ -13,6 +13,7 @@ class ParserOptions
var $mInterwikiMagic; # Interlanguage links are removed and returned in an array
var $mAllowExternalImages; # Allow external images inline
var $mAllowExternalImagesFrom; # If not, any exception?
+ var $mEnableImageWhitelist; # If not or it doesn't match, should we check an on-wiki whitelist?
var $mSkin; # Reference to the preferred skin
var $mDateFormat; # Date format index
var $mEditSection; # Create "edit section" links
@@ -29,6 +30,7 @@ class ParserOptions
var $mTemplateCallback; # Callback for template fetching
var $mEnableLimitReport; # Enable limit report in an HTML comment on output
var $mTimestamp; # Timestamp used for {{CURRENTDAY}} etc.
+ var $mExternalLinkTarget; # Target attribute for external links
var $mUser; # Stored user object, just used to initialise the skin
@@ -37,6 +39,7 @@ class ParserOptions
function getInterwikiMagic() { return $this->mInterwikiMagic; }
function getAllowExternalImages() { return $this->mAllowExternalImages; }
function getAllowExternalImagesFrom() { return $this->mAllowExternalImagesFrom; }
+ function getEnableImageWhitelist() { return $this->mEnableImageWhitelist; }
function getEditSection() { return $this->mEditSection; }
function getNumberHeadings() { return $this->mNumberHeadings; }
function getAllowSpecialInclusion() { return $this->mAllowSpecialInclusion; }
@@ -49,6 +52,8 @@ class ParserOptions
function getRemoveComments() { return $this->mRemoveComments; }
function getTemplateCallback() { return $this->mTemplateCallback; }
function getEnableLimitReport() { return $this->mEnableLimitReport; }
+ function getCleanSignatures() { return $this->mCleanSignatures; }
+ function getExternalLinkTarget() { return $this->mExternalLinkTarget; }
function getSkin() {
if ( !isset( $this->mSkin ) ) {
@@ -76,6 +81,7 @@ class ParserOptions
function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); }
function setAllowExternalImages( $x ) { return wfSetVar( $this->mAllowExternalImages, $x ); }
function setAllowExternalImagesFrom( $x ) { return wfSetVar( $this->mAllowExternalImagesFrom, $x ); }
+ function setEnableImageWhitelist( $x ) { return wfSetVar( $this->mEnableImageWhitelist, $x ); }
function setDateFormat( $x ) { return wfSetVar( $this->mDateFormat, $x ); }
function setEditSection( $x ) { return wfSetVar( $this->mEditSection, $x ); }
function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); }
@@ -91,6 +97,8 @@ class ParserOptions
function setTemplateCallback( $x ) { return wfSetVar( $this->mTemplateCallback, $x ); }
function enableLimitReport( $x = true ) { return wfSetVar( $this->mEnableLimitReport, $x ); }
function setTimestamp( $x ) { return wfSetVar( $this->mTimestamp, $x ); }
+ function setCleanSignatures( $x ) { return wfSetVar( $this->mCleanSignatures, $x ); }
+ function setExternalLinkTarget( $x ) { return wfSetVar( $this->mExternalLinkTarget, $x ); }
function __construct( $user = null ) {
$this->initialiseFromUser( $user );
@@ -107,8 +115,9 @@ class ParserOptions
/** Get user options */
function initialiseFromUser( $userInput ) {
global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages;
- global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion, $wgMaxArticleSize;
- global $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth;
+ global $wgAllowExternalImagesFrom, $wgEnableImageWhitelist, $wgAllowSpecialInclusion, $wgMaxArticleSize;
+ global $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth, $wgCleanSignatures;
+ global $wgExternalLinkTarget;
$fname = 'ParserOptions::initialiseFromUser';
wfProfileIn( $fname );
if ( !$userInput ) {
@@ -129,6 +138,7 @@ class ParserOptions
$this->mInterwikiMagic = $wgInterwikiMagic;
$this->mAllowExternalImages = $wgAllowExternalImages;
$this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom;
+ $this->mEnableImageWhitelist = $wgEnableImageWhitelist;
$this->mSkin = null; # Deferred
$this->mDateFormat = null; # Deferred
$this->mEditSection = true;
@@ -144,6 +154,8 @@ class ParserOptions
$this->mRemoveComments = true;
$this->mTemplateCallback = array( 'Parser', 'statelessFetchTemplate' );
$this->mEnableLimitReport = false;
+ $this->mCleanSignatures = $wgCleanSignatures;
+ $this->mExternalLinkTarget = $wgExternalLinkTarget;
wfProfileOut( $fname );
}
}
diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php
index f98d5641..35cb5c92 100644
--- a/includes/parser/ParserOutput.php
+++ b/includes/parser/ParserOutput.php
@@ -5,25 +5,26 @@
*/
class ParserOutput
{
- var $mText, # The output text
- $mLanguageLinks, # List of the full text of language links, in the order they appear
- $mCategories, # Map of category names to sort keys
- $mContainsOldMagic, # Boolean variable indicating if the input contained variables like {{CURRENTDAY}}
- $mCacheTime, # Time when this object was generated, or -1 for uncacheable. Used in ParserCache.
- $mVersion, # Compatibility check
- $mTitleText, # title text of the chosen language variant
- $mLinks, # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken.
- $mTemplates, # 2-D map of NS/DBK to ID for the template references. ID=zero for broken.
- $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
- $mNewSection, # Show a new section link?
- $mNoGallery, # No gallery on category page? (__NOGALLERY__)
- $mHeadItems, # Items to put in the <head> section
- $mOutputHooks, # Hook tags as per $wgParserOutputHooks
- $mWarnings, # Warning text to be returned to the user. Wikitext formatted, in the key only
- $mSections, # Table of contents
- $mProperties; # Name/value pairs to be cached in the DB
+ var $mText, # The output text
+ $mLanguageLinks, # List of the full text of language links, in the order they appear
+ $mCategories, # Map of category names to sort keys
+ $mContainsOldMagic, # Boolean variable indicating if the input contained variables like {{CURRENTDAY}}
+ $mTitleText, # title text of the chosen language variant
+ $mCacheTime = '', # Time when this object was generated, or -1 for uncacheable. Used in ParserCache.
+ $mVersion = Parser::VERSION, # Compatibility check
+ $mLinks = array(), # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken.
+ $mTemplates = array(), # 2-D map of NS/DBK to ID for the template references. ID=zero for broken.
+ $mTemplateIds = array(), # 2-D map of NS/DBK to rev ID for the template references. ID=zero for broken.
+ $mImages = array(), # DB keys of the images used, in the array key only
+ $mExternalLinks = array(), # External link URLs, in the key only
+ $mNewSection = false, # Show a new section link?
+ $mNoGallery = false, # No gallery on category page? (__NOGALLERY__)
+ $mHeadItems = array(), # Items to put in the <head> section
+ $mOutputHooks = array(), # Hook tags as per $wgParserOutputHooks
+ $mWarnings = array(), # Warning text to be returned to the user. Wikitext formatted, in the key only
+ $mSections = array(), # Table of contents
+ $mProperties = array(); # Name/value pairs to be cached in the DB
+ private $mIndexPolicy = ''; # 'index' or 'noindex'? Any other value will result in no change.
/**
* Overridden title for display
@@ -37,21 +38,7 @@ class ParserOutput
$this->mLanguageLinks = $languageLinks;
$this->mCategories = $categoryLinks;
$this->mContainsOldMagic = $containsOldMagic;
- $this->mCacheTime = '';
- $this->mVersion = Parser::VERSION;
$this->mTitleText = $titletext;
- $this->mSections = array();
- $this->mLinks = array();
- $this->mTemplates = array();
- $this->mImages = array();
- $this->mExternalLinks = array();
- $this->mNewSection = false;
- $this->mNoGallery = false;
- $this->mHeadItems = array();
- $this->mTemplateIds = array();
- $this->mOutputHooks = array();
- $this->mWarnings = array();
- $this->mProperties = array();
}
function getText() { return $this->mText; }
@@ -69,6 +56,7 @@ class ParserOutput
function getSubtitle() { return $this->mSubtitle; }
function getOutputHooks() { return (array)$this->mOutputHooks; }
function getWarnings() { return array_keys( $this->mWarnings ); }
+ function getIndexPolicy() { return $this->mIndexPolicy; }
function containsOldMagic() { return $this->mContainsOldMagic; }
function setText( $text ) { return wfSetVar( $this->mText, $text ); }
@@ -78,6 +66,7 @@ class ParserOutput
function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); }
function setTitleText( $t ) { return wfSetVar( $this->mTitleText, $t ); }
function setSections( $toc ) { return wfSetVar( $this->mSections, $toc ); }
+ function setIndexPolicy( $policy ) { return wfSetVar( $this->mIndexPolicy, $policy ); }
function addCategory( $c, $sort ) { $this->mCategories[$c] = $sort; }
function addLanguageLink( $t ) { $this->mLanguageLinks[] = $t; }
@@ -98,6 +87,14 @@ class ParserOutput
function addLink( $title, $id = null ) {
$ns = $title->getNamespace();
$dbk = $title->getDBkey();
+ if ( $ns == NS_MEDIA ) {
+ // Normalize this pseudo-alias if it makes it down here...
+ $ns = NS_FILE;
+ } elseif( $ns == NS_SPECIAL ) {
+ // We don't record Special: links currently
+ // It might actually be wise to, but we'd need to do some normalization.
+ return;
+ }
if ( !isset( $this->mLinks[$ns] ) ) {
$this->mLinks[$ns] = array();
}
diff --git a/includes/parser/Parser_DiffTest.php b/includes/parser/Parser_DiffTest.php
index be3702cf..608c883a 100644
--- a/includes/parser/Parser_DiffTest.php
+++ b/includes/parser/Parser_DiffTest.php
@@ -6,6 +6,7 @@
class Parser_DiffTest
{
var $parsers, $conf;
+ var $shortOutput = false;
var $dfUniqPrefix;
@@ -28,6 +29,9 @@ class Parser_DiffTest
$doneHook = true;
$wgHooks['ParserClearState'][] = array( $this, 'onClearState' );
}
+ if ( isset( $this->conf['shortOutput'] ) ) {
+ $this->shortOutput = $this->conf['shortOutput'];
+ }
foreach ( $this->conf['parsers'] as $i => $parserConf ) {
if ( !is_array( $parserConf ) ) {
@@ -65,13 +69,37 @@ class Parser_DiffTest
$lastResult = $currentResult;
}
if ( $mismatch ) {
- throw new MWException( "Parser_DiffTest: results mismatch on call to $name\n" .
- 'Arguments: ' . var_export( $args, true ) . "\n" .
- 'Results: ' . var_export( $results, true ) . "\n" );
+ if ( count( $results ) == 2 ) {
+ $resultsList = array();
+ foreach ( $this->parsers as $i => $parser ) {
+ $resultsList[] = var_export( $results[$i], true );
+ }
+ $diff = wfDiff( $resultsList[0], $resultsList[1] );
+ } else {
+ $diff = '[too many parsers]';
+ }
+ $msg = "Parser_DiffTest: results mismatch on call to $name\n";
+ if ( !$this->shortOutput ) {
+ $msg .= 'Arguments: ' . $this->formatArray( $args ) . "\n";
+ }
+ $msg .= 'Results: ' . $this->formatArray( $results ) . "\n" .
+ "Diff: $diff\n";
+ throw new MWException( $msg );
}
return $lastResult;
}
+ function formatArray( $array ) {
+ if ( $this->shortOutput ) {
+ foreach ( $array as $key => $value ) {
+ if ( $value instanceof ParserOutput ) {
+ $array[$key] = "ParserOutput: {$value->getText()}";
+ }
+ }
+ }
+ return var_export( $array, true );
+ }
+
function setFunctionHook( $id, $callback, $flags = 0 ) {
$this->init();
foreach ( $this->parsers as $i => $parser ) {
diff --git a/includes/parser/Parser_LinkHooks.php b/includes/parser/Parser_LinkHooks.php
new file mode 100644
index 00000000..2b306933
--- /dev/null
+++ b/includes/parser/Parser_LinkHooks.php
@@ -0,0 +1,315 @@
+<?php
+/**
+ * Parser with LinkHooks experiment
+ * @ingroup Parser
+ */
+class Parser_LinkHooks extends Parser
+{
+ /**
+ * Update this version number when the ParserOutput format
+ * changes in an incompatible way, so the parser cache
+ * can automatically discard old data.
+ */
+ const VERSION = '1.6.4';
+
+ # Flags for Parser::setLinkHook
+ # Also available as global constants from Defines.php
+ const SLH_PATTERN = 1;
+
+ # Constants needed for external link processing
+ # Everything except bracket, space, or control characters
+ const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
+ const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
+ \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
+
+ /**#@+
+ * @private
+ */
+ # Persistent:
+ var $mLinkHooks;
+
+ /**#@-*/
+
+ /**
+ * Constructor
+ *
+ * @public
+ */
+ function __construct( $conf = array() ) {
+ parent::__construct( $conf );
+ $this->mLinkHooks = array();
+ }
+
+ /**
+ * Do various kinds of initialisation on the first call of the parser
+ */
+ function firstCallInit() {
+ parent::__construct();
+ if ( !$this->mFirstCall ) {
+ return;
+ }
+ $this->mFirstCall = false;
+
+ wfProfileIn( __METHOD__ );
+
+ $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
+ CoreParserFunctions::register( $this );
+ CoreLinkFunctions::register( $this );
+ $this->initialiseVariables();
+
+ wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Create a link hook, e.g. [[Namepsace:...|display}}
+ * The callback function should have the form:
+ * function myLinkCallback( $parser, $holders, $markers,
+ * Title $title, $titleText, &$sortText = null, &$leadingColon = false ) { ... }
+ *
+ * Or with SLH_PATTERN:
+ * function myLinkCallback( $parser, $holders, $markers, )
+ * &$titleText, &$sortText = null, &$leadingColon = false ) { ... }
+ *
+ * The callback may either return a number of different possible values:
+ * String) Text result of the link
+ * True) (Treat as link) Parse the link according to normal link rules
+ * False) (Bad link) Just output the raw wikitext (You may modify the text first)
+ *
+ * @public
+ *
+ * @param integer|string $ns The Namespace ID or regex pattern if SLH_PATTERN is set
+ * @param mixed $callback The callback function (and object) to use
+ * @param integer $flags a combination of the following flags:
+ * SLH_PATTERN Use a regex link pattern rather than a namespace
+ *
+ * @return The old callback function for this name, if any
+ */
+ function setLinkHook( $ns, $callback, $flags = 0 ) {
+ if( $flags & SLH_PATTERN && !is_string($ns) )
+ throw new MWException( __METHOD__.'() expecting a regex string pattern.' );
+ elseif( $flags | ~SLH_PATTERN && !is_int($ns) )
+ throw new MWException( __METHOD__.'() expecting a namespace index.' );
+ $oldVal = isset( $this->mLinkHooks[$ns] ) ? $this->mLinkHooks[$ns][0] : null;
+ $this->mLinkHooks[$ns] = array( $callback, $flags );
+ return $oldVal;
+ }
+
+ /**
+ * Get all registered link hook identifiers
+ *
+ * @return array
+ */
+ function getLinkHooks() {
+ return array_keys( $this->mLinkHooks );
+ }
+
+ /**
+ * Process [[ ]] wikilinks
+ * @return LinkHolderArray
+ *
+ * @private
+ */
+ function replaceInternalLinks2( &$s ) {
+ global $wgContLang;
+
+ wfProfileIn( __METHOD__ );
+
+ wfProfileIn( __METHOD__.'-setup' );
+ static $tc = FALSE, $titleRegex;//$e1, $e1_img;
+ if( !$tc ) {
+ # the % is needed to support urlencoded titles as well
+ $tc = Title::legalChars() . '#%';
+ # Match a link having the form [[namespace:link|alternate]]trail
+ //$e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
+ # Match cases where there is no "]]", which might still be images
+ //$e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
+ # Match a valid plain title
+ $titleRegex = "/^([{$tc}]+)$/sD";
+ }
+
+ $sk = $this->mOptions->getSkin();
+ $holders = new LinkHolderArray( $this );
+
+ if( is_null( $this->mTitle ) ) {
+ wfProfileOut( __METHOD__ );
+ wfProfileOut( __METHOD__.'-setup' );
+ throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
+ }
+ $nottalk = !$this->mTitle->isTalkPage();
+
+ if($wgContLang->hasVariants()) {
+ $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText());
+ } else {
+ $selflink = array($this->mTitle->getPrefixedText());
+ }
+ wfProfileOut( __METHOD__.'-setup' );
+
+ $offset = 0;
+ $offsetStack = array();
+ $markers = new LinkMarkerReplacer( $this, $holders, array( &$this, 'replaceInternalLinksCallback' ) );
+ while( true ) {
+ $startBracketOffset = strpos( $s, '[[', $offset );
+ $endBracketOffset = strpos( $s, ']]', $offset );
+ # Finish when there are no more brackets
+ if( $startBracketOffset === false && $endBracketOffset === false ) break;
+ # Determine if the bracket is a starting or ending bracket
+ # When we find both, use the first one
+ elseif( $startBracketOffset !== false && $endBracketOffset !== false )
+ $isStart = $startBracketOffset <= $endBracketOffset;
+ # When we only found one, check which it is
+ else $isStart = $startBracketOffset !== false;
+ $bracketOffset = $isStart ? $startBracketOffset : $endBracketOffset;
+ if( $isStart ) {
+ /** Opening bracket **/
+ # Just push our current offset in the string onto the stack
+ $offsetStack[] = $startBracketOffset;
+ } else {
+ /** Closing bracket **/
+ # Pop the start pos for our current link zone off the stack
+ $startBracketOffset = array_pop($offsetStack);
+ # Just to clean up the code, lets place offsets on the outer ends
+ $endBracketOffset += 2;
+
+ # Only do logic if we actually have a opening bracket for this
+ if( isset($startBracketOffset) ) {
+ # Extract text inside the link
+ @list( $titleText, $paramText ) = explode('|',
+ substr($s, $startBracketOffset+2, $endBracketOffset-$startBracketOffset-4), 2);
+ # Create markers only for valid links
+ if( preg_match( $titleRegex, $titleText ) ) {
+ # Store the text for the marker
+ $marker = $markers->addMarker($titleText, $paramText);
+ # Replace the current link with the marker
+ $s = substr($s,0,$startBracketOffset).
+ $marker.
+ substr($s, $endBracketOffset);
+ # We have modified $s, because of this we need to set the
+ # offset manually since the end position is different now
+ $offset = $startBracketOffset+strlen($marker);
+ continue;
+ }
+ # ToDo: Some LinkHooks may allow recursive links inside of
+ # the link text, create a regex that also matches our
+ # <!-- LINKMARKER ### --> sequence in titles
+ # ToDO: Some LinkHooks use patterns rather than namespaces
+ # these need to be tested at this point here
+ }
+
+ }
+ # Bump our offset to after our current bracket
+ $offset = $bracketOffset+2;
+ }
+
+
+ # Now expand our tree
+ wfProfileIn( __METHOD__.'-expand' );
+ $s = $markers->expand( $s );
+ wfProfileOut( __METHOD__.'-expand' );
+
+ wfProfileOut( __METHOD__ );
+ return $holders;
+ }
+
+ function replaceInternalLinksCallback( $parser, $holders, $markers, $titleText, $paramText ) {
+ wfProfileIn( __METHOD__ );
+ $wt = isset($paramText) ? "[[$titleText|$paramText]]" : "[[$titleText]]";
+ wfProfileIn( __METHOD__."-misc" );
+ # Don't allow internal links to pages containing
+ # PROTO: where PROTO is a valid URL protocol; these
+ # should be external links.
+ if( preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $titleText) ) {
+ wfProfileOut( __METHOD__ );
+ return $wt;
+ }
+
+ # Make subpage if necessary
+ if( $this->areSubpagesAllowed() ) {
+ $titleText = $this->maybeDoSubpageLink( $titleText, $paramText );
+ }
+
+ # Check for a leading colon and strip it if it is there
+ $leadingColon = $titleText[0] == ':';
+ if( $leadingColon ) $titleText = substr( $titleText, 1 );
+
+ wfProfileOut( __METHOD__."-misc" );
+ # Make title object
+ wfProfileIn( __METHOD__."-title" );
+ $title = Title::newFromText( $this->mStripState->unstripNoWiki($titleText) );
+ if( !$title ) {
+ wfProfileOut( __METHOD__."-title" );
+ wfProfileOut( __METHOD__ );
+ return $wt;
+ }
+ $ns = $title->getNamespace();
+ wfProfileOut( __METHOD__."-title" );
+
+ # Default for Namespaces is a default link
+ # ToDo: Default for patterns is plain wikitext
+ $return = true;
+ if( isset($this->mLinkHooks[$ns]) ) {
+ list( $callback, $flags ) = $this->mLinkHooks[$ns];
+ if( $flags & SLH_PATTERN ) {
+ $args = array( $parser, $holders, $markers, $titleText, &$paramText, &$leadingColon );
+ } else {
+ $args = array( $parser, $holders, $markers, $title, $titleText, &$paramText, &$leadingColon );
+ }
+ # Workaround for PHP bug 35229 and similar
+ if ( !is_callable( $callback ) ) {
+ throw new MWException( "Tag hook for $name is not callable\n" );
+ }
+ $return = call_user_func_array( $callback, $args );
+ }
+ if( $return === true ) {
+ # True (treat as plain link) was returned, call the defaultLinkHook
+ $args = array( $parser, $holders, $markers, $title, $titleText, &$paramText, &$leadingColon );
+ $return = call_user_func_array( array( 'CoreLinkFunctions', 'defaultLinkHook' ), $args );
+ }
+ if( $return === false ) {
+ # False (no link) was returned, output plain wikitext
+ # Build it again as the hook is allowed to modify $paramText
+ return isset($paramText) ? "[[$titleText|$paramText]]" : "[[$titleText]]";
+ }
+ # Content was returned, return it
+ return $return;
+ }
+
+}
+
+class LinkMarkerReplacer {
+
+ protected $markers, $nextId, $parser, $holders, $callback;
+
+ function __construct( $parser, $holders, $callback ) {
+ $this->nextId = 0;
+ $this->markers = array();
+ $this->parser = $parser;
+ $this->holders = $holders;
+ $this->callback = $callback;
+ }
+
+ function addMarker($titleText, $paramText) {
+ $id = $this->nextId++;
+ $this->markers[$id] = array( $titleText, $paramText );
+ return "<!-- LINKMARKER $id -->";
+ }
+
+ function findMarker( $string ) {
+ return (bool) preg_match('/<!-- LINKMARKER [0-9]+ -->/', $string );
+ }
+
+ function expand( $string ) {
+ return StringUtils::delimiterReplaceCallback( "<!-- LINKMARKER ", " -->", array( &$this, 'callback' ), $string );
+ }
+
+ function callback( $m ) {
+ $id = intval($m[1]);
+ if( !array_key_exists($id, $this->markers) ) return $m[0];
+ $args = $this->markers[$id];
+ array_unshift( $args, $this );
+ array_unshift( $args, $this->holders );
+ array_unshift( $args, $this->parser );
+ return call_user_func_array( $this->callback, $args );
+ }
+
+}
diff --git a/includes/parser/Parser_OldPP.php b/includes/parser/Parser_OldPP.php
deleted file mode 100644
index 487d3ffd..00000000
--- a/includes/parser/Parser_OldPP.php
+++ /dev/null
@@ -1,4944 +0,0 @@
-<?php
-/**
- * Parser with old preprocessor
- * @ingroup Parser
- */
-class Parser_OldPP
-{
- /**
- * Update this version number when the ParserOutput format
- * changes in an incompatible way, so the parser cache
- * can automatically discard old data.
- */
- const VERSION = '1.6.4';
-
- # Flags for Parser::setFunctionHook
- # Also available as global constants from Defines.php
- const SFH_NO_HASH = 1;
-
- # Constants needed for external link processing
- # Everything except bracket, space, or control characters
- const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
- const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)\\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/S';
-
- // State constants for the definition list colon extraction
- const COLON_STATE_TEXT = 0;
- const COLON_STATE_TAG = 1;
- const COLON_STATE_TAGSTART = 2;
- const COLON_STATE_CLOSETAG = 3;
- const COLON_STATE_TAGSLASH = 4;
- const COLON_STATE_COMMENT = 5;
- const COLON_STATE_COMMENTDASH = 6;
- const COLON_STATE_COMMENTDASHDASH = 7;
-
- // Allowed values for $this->mOutputType
- // Parameter to startExternalParse().
- const OT_HTML = 1;
- const OT_WIKI = 2;
- const OT_PREPROCESS = 3;
- const OT_MSG = 4;
-
- /**#@+
- * @private
- */
- # Persistent:
- var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
- $mImageParams, $mImageParamsMagicArray, $mExtLinkBracketedRegex;
-
- # Cleared with clearState():
- var $mOutput, $mAutonumber, $mDTopen, $mStripState;
- var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
- var $mInterwikiLinkHolders, $mLinkHolders, $mUniqPrefix;
- var $mIncludeSizes, $mDefaultSort;
- var $mTemplates, // cache of already loaded templates, avoids
- // multiple SQL queries for the same string
- $mTemplatePath; // stores an unsorted hash of all the templates already loaded
- // in this path. Used for loop detection.
-
- # Temporary
- # These are variables reset at least once per parse regardless of $clearState
- var $mOptions, // ParserOptions object
- $mTitle, // Title context, used for self-link rendering and similar things
- $mOutputType, // Output type, one of the OT_xxx constants
- $ot, // Shortcut alias, see setOutputType()
- $mRevisionId, // ID to display in {{REVISIONID}} tags
- $mRevisionTimestamp, // The timestamp of the specified revision ID
- $mRevIdForTs; // The revision ID which was used to fetch the timestamp
-
- /**#@-*/
-
- /**
- * Constructor
- *
- * @public
- */
- function __construct( $conf = array() ) {
- $this->mTagHooks = array();
- $this->mTransparentTagHooks = array();
- $this->mFunctionHooks = array();
- $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
- $this->mFirstCall = true;
- $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
- '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
- }
-
- /**
- * Do various kinds of initialisation on the first call of the parser
- */
- function firstCallInit() {
- if ( !$this->mFirstCall ) {
- return;
- }
- $this->mFirstCall = false;
-
- wfProfileIn( __METHOD__ );
- global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions;
-
- $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
-
- # Syntax for arguments (see self::setFunctionHook):
- # "name for lookup in localized magic words array",
- # function callback,
- # optional SFH_NO_HASH to omit the hash from calls (e.g. {{int:...}
- # instead of {{#int:...}})
- $this->setFunctionHook( 'int', array( 'CoreParserFunctions', 'intFunction' ), SFH_NO_HASH );
- $this->setFunctionHook( 'ns', array( 'CoreParserFunctions', 'ns' ), SFH_NO_HASH );
- $this->setFunctionHook( 'urlencode', array( 'CoreParserFunctions', 'urlencode' ), SFH_NO_HASH );
- $this->setFunctionHook( 'lcfirst', array( 'CoreParserFunctions', 'lcfirst' ), SFH_NO_HASH );
- $this->setFunctionHook( 'ucfirst', array( 'CoreParserFunctions', 'ucfirst' ), SFH_NO_HASH );
- $this->setFunctionHook( 'lc', array( 'CoreParserFunctions', 'lc' ), SFH_NO_HASH );
- $this->setFunctionHook( 'uc', array( 'CoreParserFunctions', 'uc' ), SFH_NO_HASH );
- $this->setFunctionHook( 'localurl', array( 'CoreParserFunctions', 'localurl' ), SFH_NO_HASH );
- $this->setFunctionHook( 'localurle', array( 'CoreParserFunctions', 'localurle' ), SFH_NO_HASH );
- $this->setFunctionHook( 'fullurl', array( 'CoreParserFunctions', 'fullurl' ), SFH_NO_HASH );
- $this->setFunctionHook( 'fullurle', array( 'CoreParserFunctions', 'fullurle' ), SFH_NO_HASH );
- $this->setFunctionHook( 'formatnum', array( 'CoreParserFunctions', 'formatnum' ), SFH_NO_HASH );
- $this->setFunctionHook( 'grammar', array( 'CoreParserFunctions', 'grammar' ), SFH_NO_HASH );
- $this->setFunctionHook( 'plural', array( 'CoreParserFunctions', 'plural' ), SFH_NO_HASH );
- $this->setFunctionHook( 'numberofpages', array( 'CoreParserFunctions', 'numberofpages' ), SFH_NO_HASH );
- $this->setFunctionHook( 'numberofusers', array( 'CoreParserFunctions', 'numberofusers' ), SFH_NO_HASH );
- $this->setFunctionHook( 'numberofarticles', array( 'CoreParserFunctions', 'numberofarticles' ), SFH_NO_HASH );
- $this->setFunctionHook( 'numberoffiles', array( 'CoreParserFunctions', 'numberoffiles' ), SFH_NO_HASH );
- $this->setFunctionHook( 'numberofadmins', array( 'CoreParserFunctions', 'numberofadmins' ), SFH_NO_HASH );
- $this->setFunctionHook( 'numberofedits', array( 'CoreParserFunctions', 'numberofedits' ), SFH_NO_HASH );
- $this->setFunctionHook( 'language', array( 'CoreParserFunctions', 'language' ), SFH_NO_HASH );
- $this->setFunctionHook( 'padleft', array( 'CoreParserFunctions', 'padleft' ), SFH_NO_HASH );
- $this->setFunctionHook( 'padright', array( 'CoreParserFunctions', 'padright' ), SFH_NO_HASH );
- $this->setFunctionHook( 'anchorencode', array( 'CoreParserFunctions', 'anchorencode' ), SFH_NO_HASH );
- $this->setFunctionHook( 'special', array( 'CoreParserFunctions', 'special' ) );
- $this->setFunctionHook( 'defaultsort', array( 'CoreParserFunctions', 'defaultsort' ), SFH_NO_HASH );
- $this->setFunctionHook( 'filepath', array( 'CoreParserFunctions', 'filepath' ), SFH_NO_HASH );
-
- if ( $wgAllowDisplayTitle ) {
- $this->setFunctionHook( 'displaytitle', array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH );
- }
- if ( $wgAllowSlowParserFunctions ) {
- $this->setFunctionHook( 'pagesinnamespace', array( 'CoreParserFunctions', 'pagesinnamespace' ), SFH_NO_HASH );
- }
-
- $this->initialiseVariables();
-
- wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
- wfProfileOut( __METHOD__ );
- }
-
- /**
- * Clear Parser state
- *
- * @private
- */
- function clearState() {
- wfProfileIn( __METHOD__ );
- if ( $this->mFirstCall ) {
- $this->firstCallInit();
- }
- $this->mOutput = new ParserOutput;
- $this->mAutonumber = 0;
- $this->mLastSection = '';
- $this->mDTopen = false;
- $this->mIncludeCount = array();
- $this->mStripState = new StripState;
- $this->mArgStack = array();
- $this->mInPre = false;
- $this->mInterwikiLinkHolders = array(
- 'texts' => array(),
- 'titles' => array()
- );
- $this->mLinkHolders = array(
- 'namespaces' => array(),
- 'dbkeys' => array(),
- 'queries' => array(),
- 'texts' => array(),
- 'titles' => array()
- );
- $this->mRevisionTimestamp = $this->mRevisionId = null;
-
- /**
- * Prefix for temporary replacement strings for the multipass parser.
- * \x07 should never appear in input as it's disallowed in XML.
- * Using it at the front also gives us a little extra robustness
- * since it shouldn't match when butted up against identifier-like
- * string constructs.
- */
- $this->mUniqPrefix = "\x07UNIQ" . self::getRandomString();
-
- # Clear these on every parse, bug 4549
- $this->mTemplates = array();
- $this->mTemplatePath = array();
-
- $this->mShowToc = true;
- $this->mForceTocPosition = false;
- $this->mIncludeSizes = array(
- 'pre-expand' => 0,
- 'post-expand' => 0,
- 'arg' => 0
- );
- $this->mDefaultSort = false;
-
- wfRunHooks( 'ParserClearState', array( &$this ) );
- wfProfileOut( __METHOD__ );
- }
-
- function setOutputType( $ot ) {
- $this->mOutputType = $ot;
- // Shortcut alias
- $this->ot = array(
- 'html' => $ot == self::OT_HTML,
- 'wiki' => $ot == self::OT_WIKI,
- 'msg' => $ot == self::OT_MSG,
- 'pre' => $ot == self::OT_PREPROCESS,
- );
- }
-
- /**
- * Accessor for mUniqPrefix.
- *
- * @public
- */
- function uniqPrefix() {
- return $this->mUniqPrefix;
- }
-
- /**
- * Convert wikitext to HTML
- * Do not call this function recursively.
- *
- * @param string $text Text we want to parse
- * @param Title &$title A title object
- * @param array $options
- * @param boolean $linestart
- * @param boolean $clearState
- * @param int $revid number to pass in {{REVISIONID}}
- * @return ParserOutput a ParserOutput
- */
- public function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) {
- /**
- * First pass--just handle <nowiki> sections, pass the rest off
- * to internalParse() which does all the real work.
- */
-
- global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
- $fname = 'Parser::parse-' . wfGetCaller();
- wfProfileIn( __METHOD__ );
- wfProfileIn( $fname );
-
- if ( $clearState ) {
- $this->clearState();
- }
-
- $this->mOptions = $options;
- $this->mTitle =& $title;
- $oldRevisionId = $this->mRevisionId;
- $oldRevisionTimestamp = $this->mRevisionTimestamp;
- if( $revid !== null ) {
- $this->mRevisionId = $revid;
- $this->mRevisionTimestamp = null;
- }
- $this->setOutputType( self::OT_HTML );
- wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
- $text = $this->strip( $text, $this->mStripState );
- wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
- $text = $this->internalParse( $text );
- $text = $this->mStripState->unstripGeneral( $text );
-
- # Clean up special characters, only run once, next-to-last before doBlockLevels
- $fixtags = array(
- # french spaces, last one Guillemet-left
- # only if there is something before the space
- '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&nbsp;\\2',
- # french spaces, Guillemet-right
- '/(\\302\\253) /' => '\\1&nbsp;',
- );
- $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
-
- # only once and last
- $text = $this->doBlockLevels( $text, $linestart );
-
- $this->replaceLinkHolders( $text );
-
- # the position of the parserConvert() call should not be changed. it
- # assumes that the links are all replaced and the only thing left
- # is the <nowiki> mark.
- # Side-effects: this calls $this->mOutput->setTitleText()
- $text = $wgContLang->parserConvert( $text, $this );
-
- $text = $this->mStripState->unstripNoWiki( $text );
-
- wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
-
-//!JF Move to its own function
-
- $uniq_prefix = $this->mUniqPrefix;
- $matches = array();
- $elements = array_keys( $this->mTransparentTagHooks );
- $text = self::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) {
- $text = self::tidy($text);
- } else {
- # attempt to sanitize at least some nesting problems
- # (bug #2702 and quite a few others)
- $tidyregs = array(
- # ''Something [http://www.cool.com cool''] -->
- # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
- '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
- '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
- # fix up an anchor inside another anchor, only
- # at least for a single single nested link (bug 3695)
- '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
- '\\1\\2</a>\\3</a>\\1\\4</a>',
- # fix div inside inline elements- doBlockLevels won't wrap a line which
- # contains a div, so fix it up here; replace
- # div with escaped text
- '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
- '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
- # remove empty italic or bold tag pairs, some
- # introduced by rules above
- '/<([bi])><\/\\1>/' => '',
- );
-
- $text = preg_replace(
- array_keys( $tidyregs ),
- array_values( $tidyregs ),
- $text );
- }
-
- wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
-
- # Information on include size limits, for the benefit of users who try to skirt them
- if ( $this->mOptions->getEnableLimitReport() ) {
- $max = $this->mOptions->getMaxIncludeSize();
- $limitReport =
- "Pre-expand include size: {$this->mIncludeSizes['pre-expand']}/$max bytes\n" .
- "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
- "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n";
- wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
- $text .= "<!-- \n$limitReport-->\n";
- }
- $this->mOutput->setText( $text );
- $this->mRevisionId = $oldRevisionId;
- $this->mRevisionTimestamp = $oldRevisionTimestamp;
- wfProfileOut( $fname );
- wfProfileOut( __METHOD__ );
-
- return $this->mOutput;
- }
-
- /**
- * Recursive parser entry point that can be called from an extension tag
- * hook.
- */
- function recursiveTagParse( $text ) {
- wfProfileIn( __METHOD__ );
- wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
- $text = $this->strip( $text, $this->mStripState );
- wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
- $text = $this->internalParse( $text );
- wfProfileOut( __METHOD__ );
- return $text;
- }
-
- /**
- * Expand templates and variables in the text, producing valid, static wikitext.
- * Also removes comments.
- */
- function preprocess( $text, $title, $options, $revid = null ) {
- wfProfileIn( __METHOD__ );
- $this->clearState();
- $this->setOutputType( self::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 ) );
- if ( $this->mOptions->getRemoveComments() ) {
- $text = Sanitizer::removeHTMLcomments( $text );
- }
- $text = $this->replaceVariables( $text );
- $text = $this->mStripState->unstripBoth( $text );
- wfProfileOut( __METHOD__ );
- return $text;
- }
-
- /**
- * Get a random string
- *
- * @private
- * @static
- */
- function getRandomString() {
- return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
- }
-
- function &getTitle() { return $this->mTitle; }
- function getOptions() { return $this->mOptions; }
- function getRevisionId() { return $this->mRevisionId; }
-
- function getFunctionLang() {
- global $wgLang, $wgContLang;
- return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
- }
-
- /**
- * Replaces all occurrences of HTML-style comments and the given tags
- * in the text with a random marker and returns teh next text. The output
- * parameter $matches will be an associative array filled with data in
- * the form:
- * 'UNIQ-xxxxx' => array(
- * 'element',
- * 'tag content',
- * array( 'param' => 'x' ),
- * '<element param="x">tag content</element>' ) )
- *
- * @param $elements list of element names. Comments are always extracted.
- * @param $text Source text string.
- * @param $uniq_prefix
- *
- * @public
- * @static
- */
- function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
- static $n = 1;
- $stripped = '';
- $matches = array();
-
- $taglist = implode( '|', $elements );
- $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
-
- while ( '' != $text ) {
- $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
- $stripped .= $p[0];
- if( count( $p ) < 5 ) {
- break;
- }
- if( count( $p ) > 5 ) {
- // comment
- $element = $p[4];
- $attributes = '';
- $close = '';
- $inside = $p[5];
- } else {
- // tag
- $element = $p[1];
- $attributes = $p[2];
- $close = $p[3];
- $inside = $p[4];
- }
-
- $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . "-QINU\x07";
- $stripped .= $marker;
-
- if ( $close === '/>' ) {
- // Empty element tag, <tag />
- $content = null;
- $text = $inside;
- $tail = null;
- } else {
- if( $element == '!--' ) {
- $end = '/(-->)/';
- } else {
- $end = "/(<\\/$element\\s*>)/i";
- }
- $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
- $content = $q[0];
- if( count( $q ) < 3 ) {
- # No end tag -- let it run out to the end of the text.
- $tail = '';
- $text = '';
- } else {
- $tail = $q[1];
- $text = $q[2];
- }
- }
-
- $matches[$marker] = array( $element,
- $content,
- Sanitizer::decodeTagAttributes( $attributes ),
- "<$element$attributes$close$content$tail" );
- }
- return $stripped;
- }
-
- /**
- * Strips and renders nowiki, pre, math, hiero
- * If $render is set, performs necessary rendering operations on plugins
- * Returns the text, and fills an array with data needed in unstrip()
- *
- * @param StripState $state
- *
- * @param bool $stripcomments when set, HTML comments <!-- like this -->
- * will be stripped in addition to other tags. This is important
- * for section editing, where these comments cause confusion when
- * counting the sections in the wikisource
- *
- * @param array dontstrip contains tags which should not be stripped;
- * used to prevent stipping of <gallery> when saving (fixes bug 2700)
- *
- * @private
- */
- function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
- global $wgContLang;
- wfProfileIn( __METHOD__ );
- $render = ($this->mOutputType == self::OT_HTML);
-
- $uniq_prefix = $this->mUniqPrefix;
- $commentState = new ReplacementArray;
- $nowikiItems = array();
- $generalItems = array();
-
- $elements = array_merge(
- array( 'nowiki', 'gallery' ),
- array_keys( $this->mTagHooks ) );
- global $wgRawHtml;
- if( $wgRawHtml ) {
- $elements[] = 'html';
- }
- if( $this->mOptions->getUseTeX() ) {
- $elements[] = 'math';
- }
-
- # Removing $dontstrip tags from $elements list (currently only 'gallery', fixing bug 2700)
- foreach ( $elements AS $k => $v ) {
- if ( !in_array ( $v , $dontstrip ) ) continue;
- unset ( $elements[$k] );
- }
-
- $matches = array();
- $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
-
- foreach( $matches as $marker => $data ) {
- list( $element, $content, $params, $tag ) = $data;
- if( $render ) {
- $tagName = strtolower( $element );
- wfProfileIn( __METHOD__."-render-$tagName" );
- switch( $tagName ) {
- case '!--':
- // Comment
- if( substr( $tag, -3 ) == '-->' ) {
- $output = $tag;
- } else {
- // Unclosed comment in input.
- // Close it so later stripping can remove it
- $output = "$tag-->";
- }
- break;
- case 'html':
- if( $wgRawHtml ) {
- $output = $content;
- break;
- }
- // Shouldn't happen otherwise. :)
- case 'nowiki':
- $output = Xml::escapeTagsOnly( $content );
- break;
- case 'math':
- $output = $wgContLang->armourMath(
- MathRenderer::renderMath( $content, $params ) );
- break;
- case 'gallery':
- $output = $this->renderImageGallery( $content, $params );
- break;
- default:
- if( isset( $this->mTagHooks[$tagName] ) ) {
- $output = call_user_func_array( $this->mTagHooks[$tagName],
- array( $content, $params, $this ) );
- } else {
- throw new MWException( "Invalid call hook $element" );
- }
- }
- wfProfileOut( __METHOD__."-render-$tagName" );
- } else {
- // Just stripping tags; keep the source
- $output = $tag;
- }
-
- // Unstrip the output, to support recursive strip() calls
- $output = $state->unstripBoth( $output );
-
- if( !$stripcomments && $element == '!--' ) {
- $commentState->setPair( $marker, $output );
- } elseif ( $element == 'html' || $element == 'nowiki' ) {
- $nowikiItems[$marker] = $output;
- } else {
- $generalItems[$marker] = $output;
- }
- }
- # Add the new items to the state
- # We do this after the loop instead of during it to avoid slowing
- # down the recursive unstrip
- $state->nowiki->mergeArray( $nowikiItems );
- $state->general->mergeArray( $generalItems );
-
- # Unstrip comments unless explicitly told otherwise.
- # (The comments are always stripped prior to this point, so as to
- # not invoke any extension tags / parser hooks contained within
- # a comment.)
- if ( !$stripcomments ) {
- // Put them all back and forget them
- $text = $commentState->replace( $text );
- }
-
- wfProfileOut( __METHOD__ );
- return $text;
- }
-
- /**
- * Restores pre, math, and other extensions removed by strip()
- *
- * always call unstripNoWiki() after this one
- * @private
- * @deprecated use $this->mStripState->unstrip()
- */
- function unstrip( $text, $state ) {
- return $state->unstripGeneral( $text );
- }
-
- /**
- * Always call this after unstrip() to preserve the order
- *
- * @private
- * @deprecated use $this->mStripState->unstrip()
- */
- function unstripNoWiki( $text, $state ) {
- return $state->unstripNoWiki( $text );
- }
-
- /**
- * @deprecated use $this->mStripState->unstripBoth()
- */
- function unstripForHTML( $text ) {
- return $this->mStripState->unstripBoth( $text );
- }
-
- /**
- * Add an item to the strip state
- * Returns the unique tag which must be inserted into the stripped text
- * The tag will be replaced with the original text in unstrip()
- *
- * @private
- */
- function insertStripItem( $text, &$state ) {
- $rnd = $this->mUniqPrefix . '-item' . self::getRandomString();
- $state->general->setPair( $rnd, $text );
- return $rnd;
- }
-
- /**
- * Interface with html tidy, used if $wgUseTidy = true.
- * If tidy isn't able to correct the markup, the original will be
- * returned in all its glory with a warning comment appended.
- *
- * Either the external tidy program or the in-process tidy extension
- * will be used depending on availability. Override the default
- * $wgTidyInternal setting to disable the internal if it's not working.
- *
- * @param string $text Hideous HTML input
- * @return string Corrected HTML output
- * @public
- * @static
- */
- function tidy( $text ) {
- global $wgTidyInternal;
- $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'.
-' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'.
-'<head><title>test</title></head><body>'.$text.'</body></html>';
- if( $wgTidyInternal ) {
- $correctedtext = self::internalTidy( $wrappedtext );
- } else {
- $correctedtext = self::externalTidy( $wrappedtext );
- }
- if( is_null( $correctedtext ) ) {
- wfDebug( "Tidy error detected!\n" );
- return $text . "\n<!-- Tidy found serious XHTML errors -->\n";
- }
- return $correctedtext;
- }
-
- /**
- * Spawn an external HTML tidy process and get corrected markup back from it.
- *
- * @private
- * @static
- */
- function externalTidy( $text ) {
- global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
- $fname = 'Parser::externalTidy';
- wfProfileIn( $fname );
-
- $cleansource = '';
- $opts = ' -utf8';
-
- $descriptorspec = array(
- 0 => array('pipe', 'r'),
- 1 => array('pipe', 'w'),
- 2 => array('file', wfGetNull(), 'a')
- );
- $pipes = array();
- $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
- if (is_resource($process)) {
- // Theoretically, this style of communication could cause a deadlock
- // here. If the stdout buffer fills up, then writes to stdin could
- // block. This doesn't appear to happen with tidy, because tidy only
- // writes to stdout after it's finished reading from stdin. Search
- // for tidyParseStdin and tidySaveStdout in console/tidy.c
- fwrite($pipes[0], $text);
- fclose($pipes[0]);
- while (!feof($pipes[1])) {
- $cleansource .= fgets($pipes[1], 1024);
- }
- fclose($pipes[1]);
- proc_close($process);
- }
-
- wfProfileOut( $fname );
-
- if( $cleansource == '' && $text != '') {
- // Some kind of error happened, so we couldn't get the corrected text.
- // Just give up; we'll use the source text and append a warning.
- return null;
- } else {
- return $cleansource;
- }
- }
-
- /**
- * Use the HTML tidy PECL extension to use the tidy library in-process,
- * saving the overhead of spawning a new process.
- *
- * 'pear install tidy' should be able to compile the extension module.
- *
- * @private
- * @static
- */
- function internalTidy( $text ) {
- global $wgTidyConf, $IP;
- $fname = 'Parser::internalTidy';
- wfProfileIn( $fname );
-
- $tidy = new tidy;
- $tidy->parseString( $text, $wgTidyConf, 'utf8' );
- $tidy->cleanRepair();
- if( $tidy->getStatus() == 2 ) {
- // 2 is magic number for fatal error
- // http://www.php.net/manual/en/function.tidy-get-status.php
- $cleansource = null;
- } else {
- $cleansource = tidy_get_output( $tidy );
- }
- wfProfileOut( $fname );
- return $cleansource;
- }
-
- /**
- * parse the wiki syntax used to render tables
- *
- * @private
- */
- function doTableStuff ( $text ) {
- $fname = 'Parser::doTableStuff';
- wfProfileIn( $fname );
-
- $lines = explode ( "\n" , $text );
- $td_history = array (); // Is currently a td tag open?
- $last_tag_history = array (); // Save history of last lag activated (td, th or caption)
- $tr_history = array (); // Is currently a tr tag open?
- $tr_attributes = array (); // history of tr attributes
- $has_opened_tr = array(); // Did this table open a <tr> element?
- $indent_level = 0; // indent level of the table
- foreach ( $lines as $key => $line )
- {
- $line = trim ( $line );
-
- if( $line == '' ) { // empty line, go to next line
- continue;
- }
- $first_character = $line{0};
- $matches = array();
-
- if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) {
- // First check if we are starting a new table
- $indent_level = strlen( $matches[1] );
-
- $attributes = $this->mStripState->unstripBoth( $matches[2] );
- $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' );
-
- $lines[$key] = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
- array_push ( $td_history , false );
- array_push ( $last_tag_history , '' );
- array_push ( $tr_history , false );
- array_push ( $tr_attributes , '' );
- array_push ( $has_opened_tr , false );
- } else if ( count ( $td_history ) == 0 ) {
- // Don't do any of the following
- continue;
- } else if ( substr ( $line , 0 , 2 ) == '|}' ) {
- // We are ending a table
- $line = '</table>' . substr ( $line , 2 );
- $last_tag = array_pop ( $last_tag_history );
-
- if ( !array_pop ( $has_opened_tr ) ) {
- $line = "<tr><td></td></tr>{$line}";
- }
-
- if ( array_pop ( $tr_history ) ) {
- $line = "</tr>{$line}";
- }
-
- if ( array_pop ( $td_history ) ) {
- $line = "</{$last_tag}>{$line}";
- }
- array_pop ( $tr_attributes );
- $lines[$key] = $line . str_repeat( '</dd></dl>' , $indent_level );
- } else if ( substr ( $line , 0 , 2 ) == '|-' ) {
- // Now we have a table row
- $line = preg_replace( '#^\|-+#', '', $line );
-
- // Whats after the tag is now only attributes
- $attributes = $this->mStripState->unstripBoth( $line );
- $attributes = Sanitizer::fixTagAttributes ( $attributes , 'tr' );
- array_pop ( $tr_attributes );
- array_push ( $tr_attributes , $attributes );
-
- $line = '';
- $last_tag = array_pop ( $last_tag_history );
- array_pop ( $has_opened_tr );
- array_push ( $has_opened_tr , true );
-
- if ( array_pop ( $tr_history ) ) {
- $line = '</tr>';
- }
-
- if ( array_pop ( $td_history ) ) {
- $line = "</{$last_tag}>{$line}";
- }
-
- $lines[$key] = $line;
- array_push ( $tr_history , false );
- array_push ( $td_history , false );
- array_push ( $last_tag_history , '' );
- }
- else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 ) == '|+' ) {
- // This might be cell elements, td, th or captions
- if ( substr ( $line , 0 , 2 ) == '|+' ) {
- $first_character = '+';
- $line = substr ( $line , 1 );
- }
-
- $line = substr ( $line , 1 );
-
- if ( $first_character == '!' ) {
- $line = str_replace ( '!!' , '||' , $line );
- }
-
- // Split up multiple cells on the same line.
- // FIXME : This can result in improper nesting of tags processed
- // by earlier parser steps, but should avoid splitting up eg
- // attribute values containing literal "||".
- $cells = StringUtils::explodeMarkup( '||' , $line );
-
- $lines[$key] = '';
-
- // Loop through each table cell
- foreach ( $cells as $cell )
- {
- $previous = '';
- if ( $first_character != '+' )
- {
- $tr_after = array_pop ( $tr_attributes );
- if ( !array_pop ( $tr_history ) ) {
- $previous = "<tr{$tr_after}>\n";
- }
- array_push ( $tr_history , true );
- array_push ( $tr_attributes , '' );
- array_pop ( $has_opened_tr );
- array_push ( $has_opened_tr , true );
- }
-
- $last_tag = array_pop ( $last_tag_history );
-
- if ( array_pop ( $td_history ) ) {
- $previous = "</{$last_tag}>{$previous}";
- }
-
- if ( $first_character == '|' ) {
- $last_tag = 'td';
- } else if ( $first_character == '!' ) {
- $last_tag = 'th';
- } else if ( $first_character == '+' ) {
- $last_tag = 'caption';
- } else {
- $last_tag = '';
- }
-
- array_push ( $last_tag_history , $last_tag );
-
- // A cell could contain both parameters and data
- $cell_data = explode ( '|' , $cell , 2 );
-
- // Bug 553: Note that a '|' inside an invalid link should not
- // be mistaken as delimiting cell parameters
- if ( strpos( $cell_data[0], '[[' ) !== false ) {
- $cell = "{$previous}<{$last_tag}>{$cell}";
- } else if ( count ( $cell_data ) == 1 )
- $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
- else {
- $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
- $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag );
- $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
- }
-
- $lines[$key] .= $cell;
- array_push ( $td_history , true );
- }
- }
- }
-
- // Closing open td, tr && table
- while ( count ( $td_history ) > 0 )
- {
- if ( array_pop ( $td_history ) ) {
- $lines[] = '</td>' ;
- }
- if ( array_pop ( $tr_history ) ) {
- $lines[] = '</tr>' ;
- }
- if ( !array_pop ( $has_opened_tr ) ) {
- $lines[] = "<tr><td></td></tr>" ;
- }
-
- $lines[] = '</table>' ;
- }
-
- $output = implode ( "\n" , $lines ) ;
-
- // special case: don't return empty table
- if( $output == "<table>\n<tr><td></td></tr>\n</table>" ) {
- $output = '';
- }
-
- wfProfileOut( $fname );
-
- return $output;
- }
-
- /**
- * Helper function for parse() that transforms wiki markup into
- * HTML. Only called for $mOutputType == OT_HTML.
- *
- * @private
- */
- function internalParse( $text ) {
- $args = array();
- $isMain = true;
- $fname = 'Parser::internalParse';
- wfProfileIn( $fname );
-
- # Hook to suspend the parser in this state
- if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
- wfProfileOut( $fname );
- return $text ;
- }
-
- # Remove <noinclude> tags and <includeonly> sections
- $text = strtr( $text, array( '<onlyinclude>' => '' , '</onlyinclude>' => '' ) );
- $text = strtr( $text, array( '<noinclude>' => '', '</noinclude>' => '') );
- $text = StringUtils::delimiterReplace( '<includeonly>', '</includeonly>', '', $text );
-
- $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), array(), array_keys( $this->mTransparentTagHooks ) );
-
- $text = $this->replaceVariables( $text, $args );
- wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
-
- // Tables need to come after variable replacement for things to work
- // properly; putting them before other transformations should keep
- // exciting things like link expansions from showing up in surprising
- // places.
- $text = $this->doTableStuff( $text );
-
- $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
-
- $text = $this->stripToc( $text );
- $this->stripNoGallery( $text );
- $text = $this->doHeadings( $text );
- if($this->mOptions->getUseDynamicDates()) {
- $df =& DateFormatter::getInstance();
- $text = $df->reformat( $this->mOptions->getDateFormat(), $text );
- }
- $text = $this->doAllQuotes( $text );
- $text = $this->replaceInternalLinks( $text );
- $text = $this->replaceExternalLinks( $text );
-
- # replaceInternalLinks may sometimes leave behind
- # absolute URLs, which have to be masked to hide them from replaceExternalLinks
- $text = str_replace($this->mUniqPrefix."NOPARSE", "", $text);
-
- $text = $this->doMagicLinks( $text );
- $text = $this->formatHeadings( $text, $isMain );
-
- wfProfileOut( $fname );
- return $text;
- }
-
- /**
- * Replace special strings like "ISBN xxx" and "RFC xxx" with
- * magic external links.
- *
- * @private
- */
- function &doMagicLinks( &$text ) {
- wfProfileIn( __METHOD__ );
- $text = preg_replace_callback(
- '!(?: # Start cases
- <a.*?</a> | # Skip link text
- <.*?> | # Skip stuff inside HTML elements
- (?:RFC|PMID)\s+([0-9]+) | # RFC or PMID, capture number as m[1]
- ISBN\s+(\b # ISBN, capture number as m[2]
- (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix
- (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters
- [0-9Xx] # check digit
- \b)
- )!x', array( &$this, 'magicLinkCallback' ), $text );
- wfProfileOut( __METHOD__ );
- return $text;
- }
-
- function magicLinkCallback( $m ) {
- if ( substr( $m[0], 0, 1 ) == '<' ) {
- # Skip HTML element
- return $m[0];
- } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) {
- $isbn = $m[2];
- $num = strtr( $isbn, array(
- '-' => '',
- ' ' => '',
- 'x' => 'X',
- ));
- $titleObj = SpecialPage::getTitleFor( 'Booksources' );
- $text = '<a href="' .
- $titleObj->escapeLocalUrl( "isbn=$num" ) .
- "\" class=\"internal\">ISBN $isbn</a>";
- } else {
- if ( substr( $m[0], 0, 3 ) == 'RFC' ) {
- $keyword = 'RFC';
- $urlmsg = 'rfcurl';
- $id = $m[1];
- } elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) {
- $keyword = 'PMID';
- $urlmsg = 'pubmedurl';
- $id = $m[1];
- } else {
- throw new MWException( __METHOD__.': unrecognised match type "' .
- substr($m[0], 0, 20 ) . '"' );
- }
-
- $url = wfMsg( $urlmsg, $id);
- $sk = $this->mOptions->getSkin();
- $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
- $text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
- }
- return $text;
- }
-
- /**
- * Parse headers and return html
- *
- * @private
- */
- function doHeadings( $text ) {
- $fname = 'Parser::doHeadings';
- wfProfileIn( $fname );
- for ( $i = 6; $i >= 1; --$i ) {
- $h = str_repeat( '=', $i );
- $text = preg_replace( "/^{$h}(.+){$h}\\s*$/m",
- "<h{$i}>\\1</h{$i}>\\2", $text );
- }
- wfProfileOut( $fname );
- return $text;
- }
-
- /**
- * Replace single quotes with HTML markup
- * @private
- * @return string the altered text
- */
- function doAllQuotes( $text ) {
- $fname = 'Parser::doAllQuotes';
- wfProfileIn( $fname );
- $outtext = '';
- $lines = explode( "\n", $text );
- foreach ( $lines as $line ) {
- $outtext .= $this->doQuotes ( $line ) . "\n";
- }
- $outtext = substr($outtext, 0,-1);
- wfProfileOut( $fname );
- return $outtext;
- }
-
- /**
- * Helper function for doAllQuotes()
- */
- public function doQuotes( $text ) {
- $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
- if ( count( $arr ) == 1 )
- return $text;
- else
- {
- # First, do some preliminary work. This may shift some apostrophes from
- # being mark-up to being text. It also counts the number of occurrences
- # of bold and italics mark-ups.
- $i = 0;
- $numbold = 0;
- $numitalics = 0;
- foreach ( $arr as $r )
- {
- if ( ( $i % 2 ) == 1 )
- {
- # If there are ever four apostrophes, assume the first is supposed to
- # be text, and the remaining three constitute mark-up for bold text.
- if ( strlen( $arr[$i] ) == 4 )
- {
- $arr[$i-1] .= "'";
- $arr[$i] = "'''";
- }
- # If there are more than 5 apostrophes in a row, assume they're all
- # text except for the last 5.
- else if ( strlen( $arr[$i] ) > 5 )
- {
- $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 );
- $arr[$i] = "'''''";
- }
- # Count the number of occurrences of bold and italics mark-ups.
- # We are not counting sequences of five apostrophes.
- if ( strlen( $arr[$i] ) == 2 ) { $numitalics++; }
- else if ( strlen( $arr[$i] ) == 3 ) { $numbold++; }
- else if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; }
- }
- $i++;
- }
-
- # If there is an odd number of both bold and italics, it is likely
- # that one of the bold ones was meant to be an apostrophe followed
- # by italics. Which one we cannot know for certain, but it is more
- # likely to be one that has a single-letter word before it.
- if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) )
- {
- $i = 0;
- $firstsingleletterword = -1;
- $firstmultiletterword = -1;
- $firstspace = -1;
- foreach ( $arr as $r )
- {
- if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) )
- {
- $x1 = substr ($arr[$i-1], -1);
- $x2 = substr ($arr[$i-1], -2, 1);
- if ($x1 == ' ') {
- if ($firstspace == -1) $firstspace = $i;
- } else if ($x2 == ' ') {
- if ($firstsingleletterword == -1) $firstsingleletterword = $i;
- } else {
- if ($firstmultiletterword == -1) $firstmultiletterword = $i;
- }
- }
- $i++;
- }
-
- # If there is a single-letter word, use it!
- if ($firstsingleletterword > -1)
- {
- $arr [ $firstsingleletterword ] = "''";
- $arr [ $firstsingleletterword-1 ] .= "'";
- }
- # If not, but there's a multi-letter word, use that one.
- else if ($firstmultiletterword > -1)
- {
- $arr [ $firstmultiletterword ] = "''";
- $arr [ $firstmultiletterword-1 ] .= "'";
- }
- # ... otherwise use the first one that has neither.
- # (notice that it is possible for all three to be -1 if, for example,
- # there is only one pentuple-apostrophe in the line)
- else if ($firstspace > -1)
- {
- $arr [ $firstspace ] = "''";
- $arr [ $firstspace-1 ] .= "'";
- }
- }
-
- # Now let's actually convert our apostrophic mush to HTML!
- $output = '';
- $buffer = '';
- $state = '';
- $i = 0;
- foreach ($arr as $r)
- {
- if (($i % 2) == 0)
- {
- if ($state == 'both')
- $buffer .= $r;
- else
- $output .= $r;
- }
- else
- {
- if (strlen ($r) == 2)
- {
- if ($state == 'i')
- { $output .= '</i>'; $state = ''; }
- else if ($state == 'bi')
- { $output .= '</i>'; $state = 'b'; }
- else if ($state == 'ib')
- { $output .= '</b></i><b>'; $state = 'b'; }
- else if ($state == 'both')
- { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; }
- else # $state can be 'b' or ''
- { $output .= '<i>'; $state .= 'i'; }
- }
- else if (strlen ($r) == 3)
- {
- if ($state == 'b')
- { $output .= '</b>'; $state = ''; }
- else if ($state == 'bi')
- { $output .= '</i></b><i>'; $state = 'i'; }
- else if ($state == 'ib')
- { $output .= '</b>'; $state = 'i'; }
- else if ($state == 'both')
- { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; }
- else # $state can be 'i' or ''
- { $output .= '<b>'; $state .= 'b'; }
- }
- else if (strlen ($r) == 5)
- {
- if ($state == 'b')
- { $output .= '</b><i>'; $state = 'i'; }
- else if ($state == 'i')
- { $output .= '</i><b>'; $state = 'b'; }
- else if ($state == 'bi')
- { $output .= '</i></b>'; $state = ''; }
- else if ($state == 'ib')
- { $output .= '</b></i>'; $state = ''; }
- else if ($state == 'both')
- { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; }
- else # ($state == '')
- { $buffer = ''; $state = 'both'; }
- }
- }
- $i++;
- }
- # Now close all remaining tags. Notice that the order is important.
- if ($state == 'b' || $state == 'ib')
- $output .= '</b>';
- if ($state == 'i' || $state == 'bi' || $state == 'ib')
- $output .= '</i>';
- if ($state == 'bi')
- $output .= '</b>';
- # There might be lonely ''''', so make sure we have a buffer
- if ($state == 'both' && $buffer)
- $output .= '<b><i>'.$buffer.'</i></b>';
- return $output;
- }
- }
-
- /**
- * Replace external links
- *
- * Note: this is all very hackish and the order of execution matters a lot.
- * Make sure to run maintenance/parserTests.php if you change this code.
- *
- * @private
- */
- function replaceExternalLinks( $text ) {
- global $wgContLang;
- $fname = 'Parser::replaceExternalLinks';
- wfProfileIn( $fname );
-
- $sk = $this->mOptions->getSkin();
-
- $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
-
- $s = $this->replaceFreeExternalLinks( array_shift( $bits ) );
-
- $i = 0;
- while ( $i<count( $bits ) ) {
- $url = $bits[$i++];
- $protocol = $bits[$i++];
- $text = $bits[$i++];
- $trail = $bits[$i++];
-
- # The characters '<' and '>' (which were escaped by
- # removeHTMLtags()) should not be included in
- # URLs, per RFC 2396.
- $m2 = array();
- if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
- $text = substr($url, $m2[0][1]) . ' ' . $text;
- $url = substr($url, 0, $m2[0][1]);
- }
-
- # If the link text is an image URL, replace it with an <img> tag
- # This happened by accident in the original parser, but some people used it extensively
- $img = $this->maybeMakeExternalImage( $text );
- if ( $img !== false ) {
- $text = $img;
- }
-
- $dtrail = '';
-
- # Set linktype for CSS - if URL==text, link is essentially free
- $linktype = ($text == $url) ? 'free' : 'text';
-
- # No link text, e.g. [http://domain.tld/some.link]
- if ( $text == '' ) {
- # Autonumber if allowed. See bug #5918
- if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) {
- $text = '[' . ++$this->mAutonumber . ']';
- $linktype = 'autonumber';
- } else {
- # Otherwise just use the URL
- $text = htmlspecialchars( $url );
- $linktype = 'free';
- }
- } else {
- # Have link text, e.g. [http://domain.tld/some.link text]s
- # Check for trail
- list( $dtrail, $trail ) = Linker::splitTrail( $trail );
- }
-
- $text = $wgContLang->markNoConversion($text);
-
- $url = Sanitizer::cleanUrl( $url );
-
- # Process the trail (i.e. everything after this link up until start of the next link),
- # replacing any non-bracketed links
- $trail = $this->replaceFreeExternalLinks( $trail );
-
- # Use the encoded URL
- # This means that users can paste URLs directly into the text
- # Funny characters like &ouml; aren't valid in URLs anyway
- # This was changed in August 2004
- $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail;
-
- # Register link in the output object.
- # Replace unnecessary URL escape codes with the referenced character
- # This prevents spammers from hiding links from the filters
- $pasteurized = self::replaceUnusualEscapes( $url );
- $this->mOutput->addExternalLink( $pasteurized );
- }
-
- wfProfileOut( $fname );
- return $s;
- }
-
- /**
- * Replace anything that looks like a URL with a link
- * @private
- */
- function replaceFreeExternalLinks( $text ) {
- global $wgContLang;
- $fname = 'Parser::replaceFreeExternalLinks';
- wfProfileIn( $fname );
-
- $bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
- $s = array_shift( $bits );
- $i = 0;
-
- $sk = $this->mOptions->getSkin();
-
- while ( $i < count( $bits ) ){
- $protocol = $bits[$i++];
- $remainder = $bits[$i++];
-
- $m = array();
- if ( preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) {
- # Found some characters after the protocol that look promising
- $url = $protocol . $m[1];
- $trail = $m[2];
-
- # special case: handle urls as url args:
- # http://www.example.com/foo?=http://www.example.com/bar
- if(strlen($trail) == 0 &&
- isset($bits[$i]) &&
- preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) &&
- preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m ))
- {
- # add protocol, arg
- $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link
- $i += 2;
- $trail = $m[2];
- }
-
- # The characters '<' and '>' (which were escaped by
- # removeHTMLtags()) should not be included in
- # URLs, per RFC 2396.
- $m2 = array();
- if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
- $trail = substr($url, $m2[0][1]) . $trail;
- $url = substr($url, 0, $m2[0][1]);
- }
-
- # Move trailing punctuation to $trail
- $sep = ',;\.:!?';
- # If there is no left bracket, then consider right brackets fair game too
- if ( strpos( $url, '(' ) === false ) {
- $sep .= ')';
- }
-
- $numSepChars = strspn( strrev( $url ), $sep );
- if ( $numSepChars ) {
- $trail = substr( $url, -$numSepChars ) . $trail;
- $url = substr( $url, 0, -$numSepChars );
- }
-
- $url = Sanitizer::cleanUrl( $url );
-
- # Is this an external image?
- $text = $this->maybeMakeExternalImage( $url );
- if ( $text === false ) {
- # Not an image, make a link
- $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
- # Register it in the output object...
- # Replace unnecessary URL escape codes with their equivalent characters
- $pasteurized = self::replaceUnusualEscapes( $url );
- $this->mOutput->addExternalLink( $pasteurized );
- }
- $s .= $text . $trail;
- } else {
- $s .= $protocol . $remainder;
- }
- }
- wfProfileOut( $fname );
- return $s;
- }
-
- /**
- * Replace unusual URL escape codes with their equivalent characters
- * @param string
- * @return string
- * @static
- * @todo This can merge genuinely required bits in the path or query string,
- * breaking legit URLs. A proper fix would treat the various parts of
- * the URL differently; as a workaround, just use the output for
- * statistical records, not for actual linking/output.
- */
- static function replaceUnusualEscapes( $url ) {
- return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
- array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url );
- }
-
- /**
- * Callback function used in replaceUnusualEscapes().
- * Replaces unusual URL escape codes with their equivalent character
- * @static
- * @private
- */
- private static function replaceUnusualEscapesCallback( $matches ) {
- $char = urldecode( $matches[0] );
- $ord = ord( $char );
- // Is it an unsafe or HTTP reserved character according to RFC 1738?
- if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) {
- // No, shouldn't be escaped
- return $char;
- } else {
- // Yes, leave it escaped
- return $matches[0];
- }
- }
-
- /**
- * make an image if it's allowed, either through the global
- * option or through the exception
- * @private
- */
- function maybeMakeExternalImage( $url ) {
- $sk = $this->mOptions->getSkin();
- $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
- $imagesexception = !empty($imagesfrom);
- $text = false;
- if ( $this->mOptions->getAllowExternalImages()
- || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) {
- if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
- # Image found
- $text = $sk->makeExternalImage( $url );
- }
- }
- return $text;
- }
-
- /**
- * Process [[ ]] wikilinks
- *
- * @private
- */
- function replaceInternalLinks( $s ) {
- global $wgContLang;
- static $fname = 'Parser::replaceInternalLinks' ;
-
- wfProfileIn( $fname );
-
- wfProfileIn( $fname.'-setup' );
- static $tc = FALSE;
- # the % is needed to support urlencoded titles as well
- if ( !$tc ) { $tc = Title::legalChars() . '#%'; }
-
- $sk = $this->mOptions->getSkin();
-
- #split the entire text string on occurences of [[
- $a = explode( '[[', ' ' . $s );
- #get the first element (all text up to first [[), and remove the space we added
- $s = array_shift( $a );
- $s = substr( $s, 1 );
-
- # Match a link having the form [[namespace:link|alternate]]trail
- static $e1 = FALSE;
- if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; }
- # Match cases where there is no "]]", which might still be images
- static $e1_img = FALSE;
- if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; }
- # Match the end of a line for a word that's not followed by whitespace,
- # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
- $e2 = wfMsgForContent( 'linkprefix' );
-
- $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
- if( is_null( $this->mTitle ) ) {
- throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
- }
- $nottalk = !$this->mTitle->isTalkPage();
-
- if ( $useLinkPrefixExtension ) {
- $m = array();
- if ( preg_match( $e2, $s, $m ) ) {
- $first_prefix = $m[2];
- } else {
- $first_prefix = false;
- }
- } else {
- $prefix = '';
- }
-
- if($wgContLang->hasVariants()) {
- $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText());
- } else {
- $selflink = array($this->mTitle->getPrefixedText());
- }
- $useSubpages = $this->areSubpagesAllowed();
- wfProfileOut( $fname.'-setup' );
-
- # Loop for each link
- for ($k = 0; isset( $a[$k] ); $k++) {
- $line = $a[$k];
- if ( $useLinkPrefixExtension ) {
- wfProfileIn( $fname.'-prefixhandling' );
- if ( preg_match( $e2, $s, $m ) ) {
- $prefix = $m[2];
- $s = $m[1];
- } else {
- $prefix='';
- }
- # first link
- if($first_prefix) {
- $prefix = $first_prefix;
- $first_prefix = false;
- }
- wfProfileOut( $fname.'-prefixhandling' );
- }
-
- $might_be_img = false;
-
- wfProfileIn( "$fname-e1" );
- if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
- $text = $m[2];
- # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
- # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
- # the real problem is with the $e1 regex
- # See bug 1300.
- #
- # Still some problems for cases where the ] is meant to be outside punctuation,
- # and no image is in sight. See bug 2095.
- #
- if( $text !== '' &&
- substr( $m[3], 0, 1 ) === ']' &&
- strpos($text, '[') !== false
- )
- {
- $text .= ']'; # so that replaceExternalLinks($text) works later
- $m[3] = substr( $m[3], 1 );
- }
- # fix up urlencoded title texts
- if( strpos( $m[1], '%' ) !== false ) {
- # Should anchors '#' also be rejected?
- $m[1] = str_replace( array('<', '>'), array('&lt;', '&gt;'), urldecode($m[1]) );
- }
- $trail = $m[3];
- } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption
- $might_be_img = true;
- $text = $m[2];
- if ( strpos( $m[1], '%' ) !== false ) {
- $m[1] = urldecode($m[1]);
- }
- $trail = "";
- } else { # Invalid form; output directly
- $s .= $prefix . '[[' . $line ;
- wfProfileOut( "$fname-e1" );
- continue;
- }
- wfProfileOut( "$fname-e1" );
- wfProfileIn( "$fname-misc" );
-
- # Don't allow internal links to pages containing
- # PROTO: where PROTO is a valid URL protocol; these
- # should be external links.
- if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) {
- $s .= $prefix . '[[' . $line ;
- continue;
- }
-
- # Make subpage if necessary
- if( $useSubpages ) {
- $link = $this->maybeDoSubpageLink( $m[1], $text );
- } else {
- $link = $m[1];
- }
-
- $noforce = (substr($m[1], 0, 1) != ':');
- if (!$noforce) {
- # Strip off leading ':'
- $link = substr($link, 1);
- }
-
- wfProfileOut( "$fname-misc" );
- wfProfileIn( "$fname-title" );
- $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
- if( !$nt ) {
- $s .= $prefix . '[[' . $line;
- wfProfileOut( "$fname-title" );
- continue;
- }
-
- $ns = $nt->getNamespace();
- $iw = $nt->getInterWiki();
- wfProfileOut( "$fname-title" );
-
- if ($might_be_img) { # if this is actually an invalid link
- wfProfileIn( "$fname-might_be_img" );
- if ($ns == NS_IMAGE && $noforce) { #but might be an image
- $found = false;
- while (isset ($a[$k+1]) ) {
- #look at the next 'line' to see if we can close it there
- $spliced = array_splice( $a, $k + 1, 1 );
- $next_line = array_shift( $spliced );
- $m = explode( ']]', $next_line, 3 );
- if ( count( $m ) == 3 ) {
- # the first ]] closes the inner link, the second the image
- $found = true;
- $text .= "[[{$m[0]}]]{$m[1]}";
- $trail = $m[2];
- break;
- } elseif ( count( $m ) == 2 ) {
- #if there's exactly one ]] that's fine, we'll keep looking
- $text .= "[[{$m[0]}]]{$m[1]}";
- } else {
- #if $next_line is invalid too, we need look no further
- $text .= '[[' . $next_line;
- break;
- }
- }
- if ( !$found ) {
- # we couldn't find the end of this imageLink, so output it raw
- #but don't ignore what might be perfectly normal links in the text we've examined
- $text = $this->replaceInternalLinks($text);
- $s .= "{$prefix}[[$link|$text";
- # note: no $trail, because without an end, there *is* no trail
- wfProfileOut( "$fname-might_be_img" );
- continue;
- }
- } else { #it's not an image, so output it raw
- $s .= "{$prefix}[[$link|$text";
- # note: no $trail, because without an end, there *is* no trail
- wfProfileOut( "$fname-might_be_img" );
- continue;
- }
- wfProfileOut( "$fname-might_be_img" );
- }
-
- $wasblank = ( '' == $text );
- if( $wasblank ) $text = $link;
-
- # Link not escaped by : , create the various objects
- if( $noforce ) {
-
- # Interwikis
- wfProfileIn( "$fname-interwiki" );
- if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
- $this->mOutput->addLanguageLink( $nt->getFullText() );
- $s = rtrim($s . $prefix);
- $s .= trim($trail, "\n") == '' ? '': $prefix . $trail;
- wfProfileOut( "$fname-interwiki" );
- continue;
- }
- wfProfileOut( "$fname-interwiki" );
-
- if ( $ns == NS_IMAGE ) {
- wfProfileIn( "$fname-image" );
- if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
- # recursively parse links inside the image caption
- # actually, this will parse them in any other parameters, too,
- # but it might be hard to fix that, and it doesn't matter ATM
- $text = $this->replaceExternalLinks($text);
- $text = $this->replaceInternalLinks($text);
-
- # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
- $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text ) ) . $trail;
- $this->mOutput->addImage( $nt->getDBkey() );
-
- wfProfileOut( "$fname-image" );
- continue;
- } else {
- # We still need to record the image's presence on the page
- $this->mOutput->addImage( $nt->getDBkey() );
- }
- wfProfileOut( "$fname-image" );
-
- }
-
- if ( $ns == NS_CATEGORY ) {
- wfProfileIn( "$fname-category" );
- $s = rtrim($s . "\n"); # bug 87
-
- if ( $wasblank ) {
- $sortkey = $this->getDefaultSort();
- } else {
- $sortkey = $text;
- }
- $sortkey = Sanitizer::decodeCharReferences( $sortkey );
- $sortkey = str_replace( "\n", '', $sortkey );
- $sortkey = $wgContLang->convertCategoryKey( $sortkey );
- $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
-
- /**
- * Strip the whitespace Category links produce, see bug 87
- * @todo We might want to use trim($tmp, "\n") here.
- */
- $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
-
- wfProfileOut( "$fname-category" );
- continue;
- }
- }
-
- # Self-link checking
- if( $nt->getFragment() === '' ) {
- if( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
- $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
- continue;
- }
- }
-
- # Special and Media are pseudo-namespaces; no pages actually exist in them
- if( $ns == NS_MEDIA ) {
- # Give extensions a chance to select the file revision for us
- $skip = $time = false;
- wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$nt, &$skip, &$time ) );
- if ( $skip ) {
- $link = $sk->makeLinkObj( $nt );
- } else {
- $link = $sk->makeMediaLinkObj( $nt, $text, $time );
- }
- # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
- $s .= $prefix . $this->armorLinks( $link ) . $trail;
- $this->mOutput->addImage( $nt->getDBkey() );
- continue;
- } elseif( $ns == NS_SPECIAL ) {
- 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 = 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.
- $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
- $this->mOutput->addLink( $nt );
- continue;
- }
- }
- $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
- }
- wfProfileOut( $fname );
- return $s;
- }
-
- /**
- * Make a link placeholder. The text returned can be later resolved to a real link with
- * replaceLinkHolders(). This is done for two reasons: firstly to avoid further
- * parsing of interwiki links, and secondly to allow all existence checks and
- * article length checks (for stub links) to be bundled into a single query.
- *
- */
- function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
- wfProfileIn( __METHOD__ );
- if ( ! is_object($nt) ) {
- # Fail gracefully
- $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
- } else {
- # Separate the link trail from the rest of the link
- list( $inside, $trail ) = Linker::splitTrail( $trail );
-
- if ( $nt->isExternal() ) {
- $nr = array_push( $this->mInterwikiLinkHolders['texts'], $prefix.$text.$inside );
- $this->mInterwikiLinkHolders['titles'][] = $nt;
- $retVal = '<!--IWLINK '. ($nr-1) ."-->{$trail}";
- } else {
- $nr = array_push( $this->mLinkHolders['namespaces'], $nt->getNamespace() );
- $this->mLinkHolders['dbkeys'][] = $nt->getDBkey();
- $this->mLinkHolders['queries'][] = $query;
- $this->mLinkHolders['texts'][] = $prefix.$text.$inside;
- $this->mLinkHolders['titles'][] = $nt;
-
- $retVal = '<!--LINK '. ($nr-1) ."-->{$trail}";
- }
- }
- wfProfileOut( __METHOD__ );
- return $retVal;
- }
-
- /**
- * Render a forced-blue link inline; protect against double expansion of
- * URLs if we're in a mode that prepends full URL prefixes to internal links.
- * Since this little disaster has to split off the trail text to avoid
- * breaking URLs in the following text without breaking trails on the
- * wiki links, it's been made into a horrible function.
- *
- * @param Title $nt
- * @param string $text
- * @param string $query
- * @param string $trail
- * @param string $prefix
- * @return string HTML-wikitext mix oh yuck
- */
- function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
- list( $inside, $trail ) = Linker::splitTrail( $trail );
- $sk = $this->mOptions->getSkin();
- $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix );
- return $this->armorLinks( $link ) . $trail;
- }
-
- /**
- * Insert a NOPARSE hacky thing into any inline links in a chunk that's
- * going to go through further parsing steps before inline URL expansion.
- *
- * In particular this is important when using action=render, which causes
- * full URLs to be included.
- *
- * Oh man I hate our multi-layer parser!
- *
- * @param string more-or-less HTML
- * @return string less-or-more HTML with NOPARSE bits
- */
- function armorLinks( $text ) {
- return preg_replace( '/\b(' . wfUrlProtocols() . ')/',
- "{$this->mUniqPrefix}NOPARSE$1", $text );
- }
-
- /**
- * Return true if subpage links should be expanded on this page.
- * @return bool
- */
- function areSubpagesAllowed() {
- # Some namespaces don't allow subpages
- global $wgNamespacesWithSubpages;
- return !empty($wgNamespacesWithSubpages[$this->mTitle->getNamespace()]);
- }
-
- /**
- * Handle link to subpage if necessary
- * @param string $target the source of the link
- * @param string &$text the link text, modified as necessary
- * @return string the full name of the link
- * @private
- */
- function maybeDoSubpageLink($target, &$text) {
- # Valid link forms:
- # Foobar -- normal
- # :Foobar -- override special treatment of prefix (images, language links)
- # /Foobar -- convert to CurrentPage/Foobar
- # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
- # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
- # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
-
- $fname = 'Parser::maybeDoSubpageLink';
- wfProfileIn( $fname );
- $ret = $target; # default return value is no change
-
- # 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]) );
- } else {
- $noslash = substr( $target, 1 );
- }
-
- $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash) . $suffix;
- if( '' === $text ) {
- $text = $target . $suffix;
- } # this might be changed for ugliness reasons
- } else {
- # check for .. subpage backlinks
- $dotdotcount = 0;
- $nodotdot = $target;
- while( strncmp( $nodotdot, "../", 3 ) == 0 ) {
- ++$dotdotcount;
- $nodotdot = substr( $nodotdot, 3 );
- }
- if($dotdotcount > 0) {
- $exploded = explode( '/', $this->mTitle->GetPrefixedText() );
- if( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
- $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
- # / at the end means don't show full path
- if( substr( $nodotdot, -1, 1 ) == '/' ) {
- $nodotdot = substr( $nodotdot, 0, -1 );
- if( '' === $text ) {
- $text = $nodotdot . $suffix;
- }
- }
- $nodotdot = trim( $nodotdot );
- if( $nodotdot != '' ) {
- $ret .= '/' . $nodotdot;
- }
- $ret .= $suffix;
- }
- }
- }
- }
-
- wfProfileOut( $fname );
- return $ret;
- }
-
- /**#@+
- * Used by doBlockLevels()
- * @private
- */
- /* private */ function closeParagraph() {
- $result = '';
- if ( '' != $this->mLastSection ) {
- $result = '</' . $this->mLastSection . ">\n";
- }
- $this->mInPre = false;
- $this->mLastSection = '';
- return $result;
- }
- # getCommon() returns the length of the longest common substring
- # of both arguments, starting at the beginning of both.
- #
- /* private */ function getCommon( $st1, $st2 ) {
- $fl = strlen( $st1 );
- $shorter = strlen( $st2 );
- if ( $fl < $shorter ) { $shorter = $fl; }
-
- for ( $i = 0; $i < $shorter; ++$i ) {
- if ( $st1{$i} != $st2{$i} ) { break; }
- }
- return $i;
- }
- # These next three functions open, continue, and close the list
- # element appropriate to the prefix character passed into them.
- #
- /* private */ function openList( $char ) {
- $result = $this->closeParagraph();
-
- if ( '*' == $char ) { $result .= '<ul><li>'; }
- else if ( '#' == $char ) { $result .= '<ol><li>'; }
- else if ( ':' == $char ) { $result .= '<dl><dd>'; }
- else if ( ';' == $char ) {
- $result .= '<dl><dt>';
- $this->mDTopen = true;
- }
- else { $result = '<!-- ERR 1 -->'; }
-
- return $result;
- }
-
- /* private */ function nextItem( $char ) {
- if ( '*' == $char || '#' == $char ) { return '</li><li>'; }
- else if ( ':' == $char || ';' == $char ) {
- $close = '</dd>';
- if ( $this->mDTopen ) { $close = '</dt>'; }
- if ( ';' == $char ) {
- $this->mDTopen = true;
- return $close . '<dt>';
- } else {
- $this->mDTopen = false;
- return $close . '<dd>';
- }
- }
- return '<!-- ERR 2 -->';
- }
-
- /* private */ function closeList( $char ) {
- if ( '*' == $char ) { $text = '</li></ul>'; }
- else if ( '#' == $char ) { $text = '</li></ol>'; }
- else if ( ':' == $char ) {
- if ( $this->mDTopen ) {
- $this->mDTopen = false;
- $text = '</dt></dl>';
- } else {
- $text = '</dd></dl>';
- }
- }
- else { return '<!-- ERR 3 -->'; }
- return $text."\n";
- }
- /**#@-*/
-
- /**
- * Make lists from lines starting with ':', '*', '#', etc.
- *
- * @private
- * @return string the lists rendered as HTML
- */
- function doBlockLevels( $text, $linestart ) {
- $fname = 'Parser::doBlockLevels';
- wfProfileIn( $fname );
-
- # Parsing through the text line by line. The main thing
- # happening here is handling of block-level elements p, pre,
- # and making lists from lines starting with * # : etc.
- #
- $textLines = explode( "\n", $text );
-
- $lastPrefix = $output = '';
- $this->mDTopen = $inBlockElem = false;
- $prefixLength = 0;
- $paragraphStack = false;
-
- if ( !$linestart ) {
- $output .= array_shift( $textLines );
- }
- foreach ( $textLines as $oLine ) {
- $lastPrefixLength = strlen( $lastPrefix );
- $preCloseMatch = preg_match('/<\\/pre/i', $oLine );
- $preOpenMatch = preg_match('/<pre/i', $oLine );
- if ( !$this->mInPre ) {
- # Multiple prefixes may abut each other for nested lists.
- $prefixLength = strspn( $oLine, '*#:;' );
- $pref = substr( $oLine, 0, $prefixLength );
-
- # eh?
- $pref2 = str_replace( ';', ':', $pref );
- $t = substr( $oLine, $prefixLength );
- $this->mInPre = !empty($preOpenMatch);
- } else {
- # Don't interpret any other prefixes in preformatted text
- $prefixLength = 0;
- $pref = $pref2 = '';
- $t = $oLine;
- }
-
- # List generation
- if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) {
- # Same as the last item, so no need to deal with nesting or opening stuff
- $output .= $this->nextItem( substr( $pref, -1 ) );
- $paragraphStack = false;
-
- if ( substr( $pref, -1 ) == ';') {
- # The one nasty exception: definition lists work like this:
- # ; title : definition text
- # So we check for : in the remainder text to split up the
- # title and definition, without b0rking links.
- $term = $t2 = '';
- if ($this->findColonNoLinks($t, $term, $t2) !== false) {
- $t = $t2;
- $output .= $term . $this->nextItem( ':' );
- }
- }
- } elseif( $prefixLength || $lastPrefixLength ) {
- # Either open or close a level...
- $commonPrefixLength = $this->getCommon( $pref, $lastPrefix );
- $paragraphStack = false;
-
- while( $commonPrefixLength < $lastPrefixLength ) {
- $output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} );
- --$lastPrefixLength;
- }
- if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
- $output .= $this->nextItem( $pref{$commonPrefixLength-1} );
- }
- while ( $prefixLength > $commonPrefixLength ) {
- $char = substr( $pref, $commonPrefixLength, 1 );
- $output .= $this->openList( $char );
-
- if ( ';' == $char ) {
- # FIXME: This is dupe of code above
- if ($this->findColonNoLinks($t, $term, $t2) !== false) {
- $t = $t2;
- $output .= $term . $this->nextItem( ':' );
- }
- }
- ++$commonPrefixLength;
- }
- $lastPrefix = $pref2;
- }
- if( 0 == $prefixLength ) {
- wfProfileIn( "$fname-paragraph" );
- # No prefix (not in list)--go to paragraph mode
- // XXX: use a stack for nestable elements like span, table and div
- $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
- $closematch = preg_match(
- '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
- '<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t );
- if ( $openmatch or $closematch ) {
- $paragraphStack = false;
- # TODO bug 5718: paragraph closed
- $output .= $this->closeParagraph();
- if ( $preOpenMatch and !$preCloseMatch ) {
- $this->mInPre = true;
- }
- if ( $closematch ) {
- $inBlockElem = false;
- } else {
- $inBlockElem = true;
- }
- } else if ( !$inBlockElem && !$this->mInPre ) {
- if ( '' != $t and ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) {
- // pre
- if ($this->mLastSection != 'pre') {
- $paragraphStack = false;
- $output .= $this->closeParagraph().'<pre>';
- $this->mLastSection = 'pre';
- }
- $t = substr( $t, 1 );
- } else {
- // paragraph
- if ( '' == trim($t) ) {
- if ( $paragraphStack ) {
- $output .= $paragraphStack.'<br />';
- $paragraphStack = false;
- $this->mLastSection = 'p';
- } else {
- if ($this->mLastSection != 'p' ) {
- $output .= $this->closeParagraph();
- $this->mLastSection = '';
- $paragraphStack = '<p>';
- } else {
- $paragraphStack = '</p><p>';
- }
- }
- } else {
- if ( $paragraphStack ) {
- $output .= $paragraphStack;
- $paragraphStack = false;
- $this->mLastSection = 'p';
- } else if ($this->mLastSection != 'p') {
- $output .= $this->closeParagraph().'<p>';
- $this->mLastSection = 'p';
- }
- }
- }
- }
- wfProfileOut( "$fname-paragraph" );
- }
- // somewhere above we forget to get out of pre block (bug 785)
- if($preCloseMatch && $this->mInPre) {
- $this->mInPre = false;
- }
- if ($paragraphStack === false) {
- $output .= $t."\n";
- }
- }
- while ( $prefixLength ) {
- $output .= $this->closeList( $pref2{$prefixLength-1} );
- --$prefixLength;
- }
- if ( '' != $this->mLastSection ) {
- $output .= '</' . $this->mLastSection . '>';
- $this->mLastSection = '';
- }
-
- wfProfileOut( $fname );
- return $output;
- }
-
- /**
- * Split up a string on ':', ignoring any occurences inside tags
- * to prevent illegal overlapping.
- * @param string $str the string to split
- * @param string &$before set to everything before the ':'
- * @param string &$after set to everything after the ':'
- * return string the position of the ':', or false if none found
- */
- function findColonNoLinks($str, &$before, &$after) {
- $fname = 'Parser::findColonNoLinks';
- wfProfileIn( $fname );
-
- $pos = strpos( $str, ':' );
- if( $pos === false ) {
- // Nothing to find!
- wfProfileOut( $fname );
- return false;
- }
-
- $lt = strpos( $str, '<' );
- if( $lt === false || $lt > $pos ) {
- // Easy; no tag nesting to worry about
- $before = substr( $str, 0, $pos );
- $after = substr( $str, $pos+1 );
- wfProfileOut( $fname );
- return $pos;
- }
-
- // Ugly state machine to walk through avoiding tags.
- $state = self::COLON_STATE_TEXT;
- $stack = 0;
- $len = strlen( $str );
- for( $i = 0; $i < $len; $i++ ) {
- $c = $str{$i};
-
- switch( $state ) {
- // (Using the number is a performance hack for common cases)
- case 0: // self::COLON_STATE_TEXT:
- switch( $c ) {
- case "<":
- // Could be either a <start> tag or an </end> tag
- $state = self::COLON_STATE_TAGSTART;
- break;
- case ":":
- if( $stack == 0 ) {
- // We found it!
- $before = substr( $str, 0, $i );
- $after = substr( $str, $i + 1 );
- wfProfileOut( $fname );
- return $i;
- }
- // Embedded in a tag; don't break it.
- break;
- default:
- // Skip ahead looking for something interesting
- $colon = strpos( $str, ':', $i );
- if( $colon === false ) {
- // Nothing else interesting
- wfProfileOut( $fname );
- return false;
- }
- $lt = strpos( $str, '<', $i );
- if( $stack === 0 ) {
- if( $lt === false || $colon < $lt ) {
- // We found it!
- $before = substr( $str, 0, $colon );
- $after = substr( $str, $colon + 1 );
- wfProfileOut( $fname );
- return $i;
- }
- }
- if( $lt === false ) {
- // Nothing else interesting to find; abort!
- // We're nested, but there's no close tags left. Abort!
- break 2;
- }
- // Skip ahead to next tag start
- $i = $lt;
- $state = self::COLON_STATE_TAGSTART;
- }
- break;
- case 1: // self::COLON_STATE_TAG:
- // In a <tag>
- switch( $c ) {
- case ">":
- $stack++;
- $state = self::COLON_STATE_TEXT;
- break;
- case "/":
- // Slash may be followed by >?
- $state = self::COLON_STATE_TAGSLASH;
- break;
- default:
- // ignore
- }
- break;
- case 2: // self::COLON_STATE_TAGSTART:
- switch( $c ) {
- case "/":
- $state = self::COLON_STATE_CLOSETAG;
- break;
- case "!":
- $state = self::COLON_STATE_COMMENT;
- break;
- case ">":
- // Illegal early close? This shouldn't happen D:
- $state = self::COLON_STATE_TEXT;
- break;
- default:
- $state = self::COLON_STATE_TAG;
- }
- break;
- case 3: // self::COLON_STATE_CLOSETAG:
- // In a </tag>
- if( $c == ">" ) {
- $stack--;
- if( $stack < 0 ) {
- wfDebug( "Invalid input in $fname; too many close tags\n" );
- wfProfileOut( $fname );
- return false;
- }
- $state = self::COLON_STATE_TEXT;
- }
- break;
- case self::COLON_STATE_TAGSLASH:
- if( $c == ">" ) {
- // Yes, a self-closed tag <blah/>
- $state = self::COLON_STATE_TEXT;
- } else {
- // Probably we're jumping the gun, and this is an attribute
- $state = self::COLON_STATE_TAG;
- }
- break;
- case 5: // self::COLON_STATE_COMMENT:
- if( $c == "-" ) {
- $state = self::COLON_STATE_COMMENTDASH;
- }
- break;
- case self::COLON_STATE_COMMENTDASH:
- if( $c == "-" ) {
- $state = self::COLON_STATE_COMMENTDASHDASH;
- } else {
- $state = self::COLON_STATE_COMMENT;
- }
- break;
- case self::COLON_STATE_COMMENTDASHDASH:
- if( $c == ">" ) {
- $state = self::COLON_STATE_TEXT;
- } else {
- $state = self::COLON_STATE_COMMENT;
- }
- break;
- default:
- throw new MWException( "State machine error in $fname" );
- }
- }
- if( $stack > 0 ) {
- wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" );
- return false;
- }
- wfProfileOut( $fname );
- return false;
- }
-
- /**
- * Return value of a magic variable (like PAGENAME)
- *
- * @private
- */
- function getVariableValue( $index ) {
- global $wgContLang, $wgSitename, $wgServer, $wgServerName, $wgScriptPath;
-
- /**
- * Some of these require message or data lookups and can be
- * expensive to check many times.
- */
- static $varCache = array();
- if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$varCache ) ) ) {
- if ( isset( $varCache[$index] ) ) {
- return $varCache[$index];
- }
- }
-
- $ts = time();
- wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
-
- # Use the time zone
- global $wgLocaltimezone;
- if ( isset( $wgLocaltimezone ) ) {
- $oldtz = getenv( 'TZ' );
- putenv( 'TZ='.$wgLocaltimezone );
- }
-
- wfSuppressWarnings(); // E_STRICT system time bitching
- $localTimestamp = date( 'YmdHis', $ts );
- $localMonth = date( 'm', $ts );
- $localMonthName = date( 'n', $ts );
- $localDay = date( 'j', $ts );
- $localDay2 = date( 'd', $ts );
- $localDayOfWeek = date( 'w', $ts );
- $localWeek = date( 'W', $ts );
- $localYear = date( 'Y', $ts );
- $localHour = date( 'H', $ts );
- if ( isset( $wgLocaltimezone ) ) {
- putenv( 'TZ='.$oldtz );
- }
- wfRestoreWarnings();
-
- switch ( $index ) {
- case 'currentmonth':
- return $varCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) );
- case 'currentmonthname':
- return $varCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
- case 'currentmonthnamegen':
- return $varCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
- case 'currentmonthabbrev':
- return $varCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
- case 'currentday':
- return $varCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) );
- case 'currentday2':
- return $varCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) );
- case 'localmonth':
- return $varCache[$index] = $wgContLang->formatNum( $localMonth );
- case 'localmonthname':
- return $varCache[$index] = $wgContLang->getMonthName( $localMonthName );
- case 'localmonthnamegen':
- return $varCache[$index] = $wgContLang->getMonthNameGen( $localMonthName );
- case 'localmonthabbrev':
- return $varCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName );
- case 'localday':
- return $varCache[$index] = $wgContLang->formatNum( $localDay );
- case 'localday2':
- return $varCache[$index] = $wgContLang->formatNum( $localDay2 );
- case 'pagename':
- return wfEscapeWikiText( $this->mTitle->getText() );
- case 'pagenamee':
- return $this->mTitle->getPartialURL();
- case 'fullpagename':
- return wfEscapeWikiText( $this->mTitle->getPrefixedText() );
- case 'fullpagenamee':
- return $this->mTitle->getPrefixedURL();
- case 'subpagename':
- return wfEscapeWikiText( $this->mTitle->getSubpageText() );
- case 'subpagenamee':
- return $this->mTitle->getSubpageUrlForm();
- case 'basepagename':
- 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 wfEscapeWikiText( $talkPage->getPrefixedText() );
- } else {
- return '';
- }
- case 'talkpagenamee':
- if( $this->mTitle->canTalk() ) {
- $talkPage = $this->mTitle->getTalkPage();
- return $talkPage->getPrefixedUrl();
- } else {
- return '';
- }
- case 'subjectpagename':
- $subjPage = $this->mTitle->getSubjectPage();
- return wfEscapeWikiText( $subjPage->getPrefixedText() );
- case 'subjectpagenamee':
- $subjPage = $this->mTitle->getSubjectPage();
- return $subjPage->getPrefixedUrl();
- case 'revisionid':
- return $this->mRevisionId;
- case 'revisionday':
- return intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
- case 'revisionday2':
- return substr( $this->getRevisionTimestamp(), 6, 2 );
- case 'revisionmonth':
- return intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
- case 'revisionyear':
- return substr( $this->getRevisionTimestamp(), 0, 4 );
- case 'revisiontimestamp':
- return $this->getRevisionTimestamp();
- case 'namespace':
- return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
- case 'namespacee':
- return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
- case 'talkspace':
- return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : '';
- case 'talkspacee':
- return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
- case 'subjectspace':
- return $this->mTitle->getSubjectNsText();
- case 'subjectspacee':
- return( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
- case 'currentdayname':
- return $varCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
- case 'currentyear':
- 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( 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)gmdate( 'W', $ts ) );
- case 'currentdow':
- return $varCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) );
- case 'localdayname':
- return $varCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
- case 'localyear':
- return $varCache[$index] = $wgContLang->formatNum( $localYear, true );
- case 'localtime':
- return $varCache[$index] = $wgContLang->time( $localTimestamp, false, false );
- case 'localhour':
- return $varCache[$index] = $wgContLang->formatNum( $localHour, true );
- case 'localweek':
- // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
- // int to remove the padding
- return $varCache[$index] = $wgContLang->formatNum( (int)$localWeek );
- case 'localdow':
- return $varCache[$index] = $wgContLang->formatNum( $localDayOfWeek );
- case 'numberofarticles':
- return $varCache[$index] = $wgContLang->formatNum( SiteStats::articles() );
- case 'numberoffiles':
- return $varCache[$index] = $wgContLang->formatNum( SiteStats::images() );
- case 'numberofusers':
- return $varCache[$index] = $wgContLang->formatNum( SiteStats::users() );
- case 'numberofpages':
- return $varCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
- case 'numberofadmins':
- return $varCache[$index] = $wgContLang->formatNum( SiteStats::admins() );
- case 'numberofedits':
- return $varCache[$index] = $wgContLang->formatNum( SiteStats::edits() );
- case 'currenttimestamp':
- return $varCache[$index] = wfTimestampNow();
- case 'localtimestamp':
- return $varCache[$index] = $localTimestamp;
- case 'currentversion':
- return $varCache[$index] = SpecialVersion::getVersion();
- case 'sitename':
- return $wgSitename;
- case 'server':
- return $wgServer;
- case 'servername':
- return $wgServerName;
- case 'scriptpath':
- return $wgScriptPath;
- case 'directionmark':
- return $wgContLang->getDirMark();
- case 'contentlanguage':
- global $wgContLanguageCode;
- return $wgContLanguageCode;
- default:
- $ret = null;
- if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$varCache, &$index, &$ret ) ) )
- return $ret;
- else
- return null;
- }
- }
-
- /**
- * initialise the magic variables (like CURRENTMONTHNAME)
- *
- * @private
- */
- function initialiseVariables() {
- $fname = 'Parser::initialiseVariables';
- wfProfileIn( $fname );
- $variableIDs = MagicWord::getVariableIDs();
-
- $this->mVariables = array();
- foreach ( $variableIDs as $id ) {
- $mw =& MagicWord::get( $id );
- $mw->addToArray( $this->mVariables, $id );
- }
- wfProfileOut( $fname );
- }
-
- /**
- * parse any parentheses in format ((title|part|part))
- * and call callbacks to get a replacement text for any found piece
- *
- * @param string $text The text to parse
- * @param array $callbacks rules in form:
- * '{' => array( # opening parentheses
- * 'end' => '}', # closing parentheses
- * 'cb' => array(2 => callback, # replacement callback to call if {{..}} is found
- * 3 => callback # replacement callback to call if {{{..}}} is found
- * )
- * )
- * 'min' => 2, # Minimum parenthesis count in cb
- * 'max' => 3, # Maximum parenthesis count in cb
- * @private
- */
- function replace_callback ($text, $callbacks) {
- wfProfileIn( __METHOD__ );
- $openingBraceStack = array(); # this array will hold a stack of parentheses which are not closed yet
- $lastOpeningBrace = -1; # last not closed parentheses
-
- $validOpeningBraces = implode( '', array_keys( $callbacks ) );
-
- $i = 0;
- while ( $i < strlen( $text ) ) {
- # Find next opening brace, closing brace or pipe
- if ( $lastOpeningBrace == -1 ) {
- $currentClosing = '';
- $search = $validOpeningBraces;
- } else {
- $currentClosing = $openingBraceStack[$lastOpeningBrace]['braceEnd'];
- $search = $validOpeningBraces . '|' . $currentClosing;
- }
- $rule = null;
- $i += strcspn( $text, $search, $i );
- if ( $i < strlen( $text ) ) {
- if ( $text[$i] == '|' ) {
- $found = 'pipe';
- } elseif ( $text[$i] == $currentClosing ) {
- $found = 'close';
- } elseif ( isset( $callbacks[$text[$i]] ) ) {
- $found = 'open';
- $rule = $callbacks[$text[$i]];
- } else {
- # Some versions of PHP have a strcspn which stops on null characters
- # Ignore and continue
- ++$i;
- continue;
- }
- } else {
- # All done
- break;
- }
-
- if ( $found == 'open' ) {
- # found opening brace, let's add it to parentheses stack
- $piece = array('brace' => $text[$i],
- 'braceEnd' => $rule['end'],
- 'title' => '',
- 'parts' => null);
-
- # count opening brace characters
- $piece['count'] = strspn( $text, $piece['brace'], $i );
- $piece['startAt'] = $piece['partStart'] = $i + $piece['count'];
- $i += $piece['count'];
-
- # we need to add to stack only if opening brace count is enough for one of the rules
- if ( $piece['count'] >= $rule['min'] ) {
- $lastOpeningBrace ++;
- $openingBraceStack[$lastOpeningBrace] = $piece;
- }
- } elseif ( $found == 'close' ) {
- # lets check if it is enough characters for closing brace
- $maxCount = $openingBraceStack[$lastOpeningBrace]['count'];
- $count = strspn( $text, $text[$i], $i, $maxCount );
-
- # check for maximum matching characters (if there are 5 closing
- # characters, we will probably need only 3 - depending on the rules)
- $matchingCount = 0;
- $matchingCallback = null;
- $cbType = $callbacks[$openingBraceStack[$lastOpeningBrace]['brace']];
- if ( $count > $cbType['max'] ) {
- # The specified maximum exists in the callback array, unless the caller
- # has made an error
- $matchingCount = $cbType['max'];
- } else {
- # Count is less than the maximum
- # Skip any gaps in the callback array to find the true largest match
- # Need to use array_key_exists not isset because the callback can be null
- $matchingCount = $count;
- while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $cbType['cb'] ) ) {
- --$matchingCount;
- }
- }
-
- if ($matchingCount <= 0) {
- $i += $count;
- continue;
- }
- $matchingCallback = $cbType['cb'][$matchingCount];
-
- # let's set a title or last part (if '|' was found)
- if (null === $openingBraceStack[$lastOpeningBrace]['parts']) {
- $openingBraceStack[$lastOpeningBrace]['title'] =
- substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'],
- $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
- } else {
- $openingBraceStack[$lastOpeningBrace]['parts'][] =
- substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'],
- $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
- }
-
- $pieceStart = $openingBraceStack[$lastOpeningBrace]['startAt'] - $matchingCount;
- $pieceEnd = $i + $matchingCount;
-
- if( is_callable( $matchingCallback ) ) {
- $cbArgs = array (
- 'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart),
- 'title' => trim($openingBraceStack[$lastOpeningBrace]['title']),
- 'parts' => $openingBraceStack[$lastOpeningBrace]['parts'],
- 'lineStart' => (($pieceStart > 0) && ($text[$pieceStart-1] == "\n")),
- );
- # finally we can call a user callback and replace piece of text
- $replaceWith = call_user_func( $matchingCallback, $cbArgs );
- $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd);
- $i = $pieceStart + strlen($replaceWith);
- } else {
- # null value for callback means that parentheses should be parsed, but not replaced
- $i += $matchingCount;
- }
-
- # reset last opening parentheses, but keep it in case there are unused characters
- $piece = array('brace' => $openingBraceStack[$lastOpeningBrace]['brace'],
- 'braceEnd' => $openingBraceStack[$lastOpeningBrace]['braceEnd'],
- 'count' => $openingBraceStack[$lastOpeningBrace]['count'],
- 'title' => '',
- 'parts' => null,
- 'startAt' => $openingBraceStack[$lastOpeningBrace]['startAt']);
- $openingBraceStack[$lastOpeningBrace--] = null;
-
- if ($matchingCount < $piece['count']) {
- $piece['count'] -= $matchingCount;
- $piece['startAt'] -= $matchingCount;
- $piece['partStart'] = $piece['startAt'];
- # do we still qualify for any callback with remaining count?
- $currentCbList = $callbacks[$piece['brace']]['cb'];
- while ( $piece['count'] ) {
- if ( array_key_exists( $piece['count'], $currentCbList ) ) {
- $lastOpeningBrace++;
- $openingBraceStack[$lastOpeningBrace] = $piece;
- break;
- }
- --$piece['count'];
- }
- }
- } elseif ( $found == 'pipe' ) {
- # lets set a title if it is a first separator, or next part otherwise
- if (null === $openingBraceStack[$lastOpeningBrace]['parts']) {
- $openingBraceStack[$lastOpeningBrace]['title'] =
- substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'],
- $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
- $openingBraceStack[$lastOpeningBrace]['parts'] = array();
- } else {
- $openingBraceStack[$lastOpeningBrace]['parts'][] =
- substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'],
- $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
- }
- $openingBraceStack[$lastOpeningBrace]['partStart'] = ++$i;
- }
- }
-
- wfProfileOut( __METHOD__ );
- return $text;
- }
-
- /**
- * Replace magic variables, templates, and template arguments
- * with the appropriate text. Templates are substituted recursively,
- * taking care to avoid infinite loops.
- *
- * Note that the substitution depends on value of $mOutputType:
- * self::OT_WIKI: only {{subst:}} templates
- * self::OT_MSG: only magic variables
- * self::OT_HTML: all templates and magic variables
- *
- * @param string $tex The text to transform
- * @param array $args Key-value pairs representing template parameters to substitute
- * @param bool $argsOnly Only do argument (triple-brace) expansion, not double-brace expansion
- * @private
- */
- function replaceVariables( $text, $args = array(), $argsOnly = false ) {
- # Prevent too big inclusions
- if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
- return $text;
- }
-
- $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/;
- wfProfileIn( $fname );
-
- # This function is called recursively. To keep track of arguments we need a stack:
- array_push( $this->mArgStack, $args );
-
- $braceCallbacks = array();
- if ( !$argsOnly ) {
- $braceCallbacks[2] = array( &$this, 'braceSubstitution' );
- }
- if ( $this->mOutputType != self::OT_MSG ) {
- $braceCallbacks[3] = array( &$this, 'argSubstitution' );
- }
- if ( $braceCallbacks ) {
- $callbacks = array(
- '{' => array(
- 'end' => '}',
- 'cb' => $braceCallbacks,
- 'min' => $argsOnly ? 3 : 2,
- 'max' => isset( $braceCallbacks[3] ) ? 3 : 2,
- ),
- '[' => array(
- 'end' => ']',
- 'cb' => array(2=>null),
- 'min' => 2,
- 'max' => 2,
- )
- );
- $text = $this->replace_callback ($text, $callbacks);
-
- array_pop( $this->mArgStack );
- }
- wfProfileOut( $fname );
- return $text;
- }
-
- /**
- * Replace magic variables
- * @private
- */
- function variableSubstitution( $matches ) {
- global $wgContLang;
- $fname = 'Parser::variableSubstitution';
- $varname = $wgContLang->lc($matches[1]);
- wfProfileIn( $fname );
- $skip = false;
- if ( $this->mOutputType == self::OT_WIKI ) {
- # Do only magic variables prefixed by SUBST
- $mwSubst =& MagicWord::get( 'subst' );
- if (!$mwSubst->matchStartAndRemove( $varname ))
- $skip = true;
- # Note that if we don't substitute the variable below,
- # we don't remove the {{subst:}} magic word, in case
- # it is a template rather than a magic variable.
- }
- if ( !$skip && array_key_exists( $varname, $this->mVariables ) ) {
- $id = $this->mVariables[$varname];
- # Now check if we did really match, case sensitive or not
- $mw =& MagicWord::get( $id );
- if ($mw->match($matches[1])) {
- $text = $this->getVariableValue( $id );
- if (MagicWord::getCacheTTL($id)>-1)
- $this->mOutput->mContainsOldMagic = true;
- } else {
- $text = $matches[0];
- }
- } else {
- $text = $matches[0];
- }
- wfProfileOut( $fname );
- return $text;
- }
-
-
- /// Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
- static function createAssocArgs( $args ) {
- $assocArgs = array();
- $index = 1;
- foreach( $args as $arg ) {
- $eqpos = strpos( $arg, '=' );
- if ( $eqpos === false ) {
- $assocArgs[$index++] = $arg;
- } else {
- $name = trim( substr( $arg, 0, $eqpos ) );
- $value = trim( substr( $arg, $eqpos+1 ) );
- if ( $value === false ) {
- $value = '';
- }
- if ( $name !== false ) {
- $assocArgs[$name] = $value;
- }
- }
- }
-
- return $assocArgs;
- }
-
- /**
- * Return the text of a template, after recursively
- * replacing any variables or templates within the template.
- *
- * @param array $piece The parts of the template
- * $piece['text']: matched text
- * $piece['title']: the title, i.e. the part before the |
- * $piece['parts']: the parameter array
- * @return string the text of the template
- * @private
- */
- function braceSubstitution( $piece ) {
- global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces;
- $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/;
- wfProfileIn( $fname );
- wfProfileIn( __METHOD__.'-setup' );
-
- # Flags
- $found = false; # $text has been filled
- $nowiki = false; # wiki markup in $text should be escaped
- $noparse = false; # Unsafe HTML tags should not be stripped, etc.
- $noargs = false; # Don't replace triple-brace arguments in $text
- $replaceHeadings = false; # Make the edit section links go to the template not the article
- $headingOffset = 0; # Skip headings when number, to account for those that weren't transcluded.
- $isHTML = false; # $text is HTML, armour it against wikitext transformation
- $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered
-
- # Title object, where $text came from
- $title = NULL;
-
- $linestart = '';
-
-
- # $part1 is the bit before the first |, and must contain only title characters
- # $args is a list of arguments, starting from index 0, not including $part1
-
- $titleText = $part1 = $piece['title'];
- # If the third subpattern matched anything, it will start with |
-
- if (null == $piece['parts']) {
- $replaceWith = $this->variableSubstitution (array ($piece['text'], $piece['title']));
- if ($replaceWith != $piece['text']) {
- $text = $replaceWith;
- $found = true;
- $noparse = true;
- $noargs = true;
- }
- }
-
- $args = (null == $piece['parts']) ? array() : $piece['parts'];
- wfProfileOut( __METHOD__.'-setup' );
-
- # SUBST
- wfProfileIn( __METHOD__.'-modifiers' );
- if ( !$found ) {
- $mwSubst =& MagicWord::get( 'subst' );
- if ( $mwSubst->matchStartAndRemove( $part1 ) xor $this->ot['wiki'] ) {
- # One of two possibilities is true:
- # 1) Found SUBST but not in the PST phase
- # 2) Didn't find SUBST and in the PST phase
- # In either case, return without further processing
- $text = $piece['text'];
- $found = true;
- $noparse = true;
- $noargs = true;
- }
- }
-
- # MSG, MSGNW and RAW
- if ( !$found ) {
- # Check for MSGNW:
- $mwMsgnw =& MagicWord::get( 'msgnw' );
- if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
- $nowiki = true;
- } else {
- # Remove obsolete MSG:
- $mwMsg =& MagicWord::get( 'msg' );
- $mwMsg->matchStartAndRemove( $part1 );
- }
-
- # Check for RAW:
- $mwRaw =& MagicWord::get( 'raw' );
- if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
- $forceRawInterwiki = true;
- }
- }
- wfProfileOut( __METHOD__.'-modifiers' );
-
- //save path level before recursing into functions & templates.
- $lastPathLevel = $this->mTemplatePath;
-
- # Parser functions
- if ( !$found ) {
- wfProfileIn( __METHOD__ . '-pfunc' );
-
- $colonPos = strpos( $part1, ':' );
- if ( $colonPos !== false ) {
- # Case sensitive functions
- $function = substr( $part1, 0, $colonPos );
- if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
- $function = $this->mFunctionSynonyms[1][$function];
- } else {
- # Case insensitive functions
- $function = strtolower( $function );
- if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
- $function = $this->mFunctionSynonyms[0][$function];
- } else {
- $function = false;
- }
- }
- if ( $function ) {
- $funcArgs = array_map( 'trim', $args );
- $funcArgs = array_merge( array( &$this, trim( substr( $part1, $colonPos + 1 ) ) ), $funcArgs );
- $result = call_user_func_array( $this->mFunctionHooks[$function], $funcArgs );
- $found = true;
-
- // The text is usually already parsed, doesn't need triple-brace tags expanded, etc.
- //$noargs = true;
- //$noparse = true;
-
- if ( is_array( $result ) ) {
- if ( isset( $result[0] ) ) {
- $text = $linestart . $result[0];
- unset( $result[0] );
- }
-
- // Extract flags into the local scope
- // This allows callers to set flags such as nowiki, noparse, found, etc.
- extract( $result );
- } else {
- $text = $linestart . $result;
- }
- }
- }
- wfProfileOut( __METHOD__ . '-pfunc' );
- }
-
- # Template table test
-
- # Did we encounter this template already? If yes, it is in the cache
- # and we need to check for loops.
- if ( !$found && isset( $this->mTemplates[$piece['title']] ) ) {
- $found = true;
-
- # Infinite loop test
- if ( isset( $this->mTemplatePath[$part1] ) ) {
- $noparse = true;
- $noargs = true;
- $found = true;
- $text = $linestart .
- "[[$part1]]<!-- WARNING: template loop detected -->";
- wfDebug( __METHOD__.": template loop broken at '$part1'\n" );
- } else {
- # set $text to cached message.
- $text = $linestart . $this->mTemplates[$piece['title']];
- #treat title for cached page the same as others
- $ns = NS_TEMPLATE;
- $subpage = '';
- $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
- if ($subpage !== '') {
- $ns = $this->mTitle->getNamespace();
- }
- $title = Title::newFromText( $part1, $ns );
- //used by include size checking
- $titleText = $title->getPrefixedText();
- //used by edit section links
- $replaceHeadings = true;
-
- }
- }
-
- # Load from database
- if ( !$found ) {
- wfProfileIn( __METHOD__ . '-loadtpl' );
- $ns = NS_TEMPLATE;
- # declaring $subpage directly in the function call
- # does not work correctly with references and breaks
- # {{/subpage}}-style inclusions
- $subpage = '';
- $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
- if ($subpage !== '') {
- $ns = $this->mTitle->getNamespace();
- }
- $title = Title::newFromText( $part1, $ns );
-
-
- if ( !is_null( $title ) ) {
- $titleText = $title->getPrefixedText();
- # Check for language variants if the template is not found
- if($wgContLang->hasVariants() && $title->getArticleID() == 0){
- $wgContLang->findVariantLink($part1, $title);
- }
-
- if ( !$title->isExternal() ) {
- if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) {
- $text = SpecialPage::capturePath( $title );
- if ( is_string( $text ) ) {
- $found = true;
- $noparse = true;
- $noargs = true;
- $isHTML = true;
- $this->disableCache();
- }
- } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) {
- $found = false; //access denied
- wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() );
- } else {
- list($articleContent,$title) = $this->fetchTemplateAndtitle( $title );
- if ( $articleContent !== false ) {
- $found = true;
- $text = $articleContent;
- $replaceHeadings = true;
- }
- }
-
- # If the title is valid but undisplayable, make a link to it
- if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
- $text = "[[:$titleText]]";
- $found = true;
- }
- } elseif ( $title->isTrans() ) {
- // Interwiki transclusion
- if ( $this->ot['html'] && !$forceRawInterwiki ) {
- $text = $this->interwikiTransclude( $title, 'render' );
- $isHTML = true;
- $noparse = true;
- } else {
- $text = $this->interwikiTransclude( $title, 'raw' );
- $replaceHeadings = true;
- }
- $found = true;
- }
-
- # Template cache array insertion
- # Use the original $piece['title'] not the mangled $part1, so that
- # modifiers such as RAW: produce separate cache entries
- if( $found ) {
- if( $isHTML ) {
- // A special page; don't store it in the template cache.
- } else {
- $this->mTemplates[$piece['title']] = $text;
- }
- $text = $linestart . $text;
- }
- }
- wfProfileOut( __METHOD__ . '-loadtpl' );
- }
-
- if ( $found && !$this->incrementIncludeSize( 'pre-expand', strlen( $text ) ) ) {
- # Error, oversize inclusion
- $text = $linestart .
- "[[$titleText]]<!-- WARNING: template omitted, pre-expand include size too large -->";
- $noparse = true;
- $noargs = true;
- }
-
- # Recursive parsing, escaping and link table handling
- # Only for HTML output
- if ( $nowiki && $found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
- $text = wfEscapeWikiText( $text );
- } elseif ( !$this->ot['msg'] && $found ) {
- if ( $noargs ) {
- $assocArgs = array();
- } else {
- # Clean up argument array
- $assocArgs = self::createAssocArgs($args);
- # Add a new element to the templace recursion path
- $this->mTemplatePath[$part1] = 1;
- }
-
- if ( !$noparse ) {
- # If there are any <onlyinclude> tags, only include them
- if ( in_string( '<onlyinclude>', $text ) && in_string( '</onlyinclude>', $text ) ) {
- $replacer = new OnlyIncludeReplacer;
- StringUtils::delimiterReplaceCallback( '<onlyinclude>', '</onlyinclude>',
- array( &$replacer, 'replace' ), $text );
- $text = $replacer->output;
- }
- # Remove <noinclude> sections and <includeonly> tags
- $text = StringUtils::delimiterReplace( '<noinclude>', '</noinclude>', '', $text );
- $text = strtr( $text, array( '<includeonly>' => '' , '</includeonly>' => '' ) );
-
- if( $this->ot['html'] || $this->ot['pre'] ) {
- # Strip <nowiki>, <pre>, etc.
- $text = $this->strip( $text, $this->mStripState );
- if ( $this->ot['html'] ) {
- $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs );
- } elseif ( $this->ot['pre'] && $this->mOptions->getRemoveComments() ) {
- $text = Sanitizer::removeHTMLcomments( $text );
- }
- }
- $text = $this->replaceVariables( $text, $assocArgs );
-
- # If the template begins with a table or block-level
- # element, it should be treated as beginning a new line.
- if (!$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) /*}*/{
- $text = "\n" . $text;
- }
- } elseif ( !$noargs ) {
- # $noparse and !$noargs
- # Just replace the arguments, not any double-brace items
- # This is used for rendered interwiki transclusion
- $text = $this->replaceVariables( $text, $assocArgs, true );
- }
- }
- # Prune lower levels off the recursion check path
- $this->mTemplatePath = $lastPathLevel;
-
- if ( $found && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
- # Error, oversize inclusion
- $text = $linestart .
- "[[$titleText]]<!-- WARNING: template omitted, post-expand include size too large -->";
- $noparse = true;
- $noargs = true;
- }
-
- if ( !$found ) {
- wfProfileOut( $fname );
- return $piece['text'];
- } else {
- wfProfileIn( __METHOD__ . '-placeholders' );
- if ( $isHTML ) {
- # Replace raw HTML by a placeholder
- # Add a blank line preceding, to prevent it from mucking up
- # immediately preceding headings
- $text = "\n\n" . $this->insertStripItem( $text, $this->mStripState );
- } else {
- # replace ==section headers==
- # XXX this needs to go away once we have a better parser.
- if ( !$this->ot['wiki'] && !$this->ot['pre'] && $replaceHeadings ) {
- if( !is_null( $title ) )
- $encodedname = base64_encode($title->getPrefixedDBkey());
- else
- $encodedname = base64_encode("");
- $m = preg_split('/(^={1,6}.*?={1,6}\s*?$)/m', $text, -1,
- 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;
- $hl = $m[$i + 1];
- if( strstr($hl, "<!--MWTEMPLATESECTION") ) {
- $text .= $hl;
- continue;
- }
- $m2 = array();
- preg_match('/^(={1,6})(.*?)(={1,6}\s*?)$/m', $hl, $m2);
- $text .= $m2[1] . $m2[2] . "<!--MWTEMPLATESECTION="
- . $encodedname . "&" . base64_encode("$nsec") . "-->" . $m2[3];
-
- $nsec++;
- }
- }
- }
- wfProfileOut( __METHOD__ . '-placeholders' );
- }
-
- # Prune lower levels off the recursion check path
- $this->mTemplatePath = $lastPathLevel;
-
- if ( !$found ) {
- wfProfileOut( $fname );
- return $piece['text'];
- } else {
- wfProfileOut( $fname );
- return $text;
- }
- }
-
- /**
- * Fetch the unparsed text of a template and register a reference to it.
- */
- function fetchTemplateAndTitle( $title ) {
- $templateCb = $this->mOptions->getTemplateCallback();
- $stuff = call_user_func( $templateCb, $title, $this );
- $text = $stuff['text'];
- $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
- if ( isset( $stuff['deps'] ) ) {
- foreach ( $stuff['deps'] as $dep ) {
- $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
- }
- }
- return array($text,$finalTitle);
- }
-
- function fetchTemplate( $title ) {
- $rv = $this->fetchTemplateAndtitle($title);
- return $rv[0];
- }
-
- /**
- * Static function to get a template
- * Can be overridden via ParserOptions::setTemplateCallback().
- *
- * Returns an associative array:
- * text The unparsed template text
- * finalTitle (Optional) The title after following redirects
- * deps (Optional) An array of associative array dependencies:
- * title: The dependency title, to be registered in templatelinks
- * page_id: The page_id of the title
- * rev_id: The revision ID loaded
- */
- static function statelessFetchTemplate( $title, $parser=false ) {
- $text = $skip = false;
- $finalTitle = $title;
- $deps = array();
-
- // Loop to fetch the article, with up to 1 redirect
- for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
- # Give extensions a chance to select the revision instead
- $id = false; // Assume current
- wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( $parser, &$title, &$skip, &$id ) );
-
- if( $skip ) {
- $text = false;
- $deps[] = array(
- 'title' => $title,
- 'page_id' => $title->getArticleID(),
- 'rev_id' => null );
- break;
- }
- $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title );
- $rev_id = $rev ? $rev->getId() : 0;
-
- $deps[] = array(
- 'title' => $title,
- 'page_id' => $title->getArticleID(),
- 'rev_id' => $rev_id );
-
- if( $rev ) {
- $text = $rev->getText();
- } elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
- global $wgLang;
- $message = $wgLang->lcfirst( $title->getText() );
- $text = wfMsgForContentNoTrans( $message );
- if( wfEmptyMsg( $message, $text ) ) {
- $text = false;
- break;
- }
- } else {
- break;
- }
- if ( $text === false ) {
- break;
- }
- // Redirect?
- $finalTitle = $title;
- $title = Title::newFromRedirect( $text );
- }
- return array(
- 'text' => $text,
- 'finalTitle' => $finalTitle,
- 'deps' => $deps );
- }
-
- /**
- * Transclude an interwiki link.
- */
- function interwikiTransclude( $title, $action ) {
- global $wgEnableScaryTranscluding;
-
- if (!$wgEnableScaryTranscluding)
- return wfMsg('scarytranscludedisabled');
-
- $url = $title->getFullUrl( "action=$action" );
-
- if (strlen($url) > 255)
- return wfMsg('scarytranscludetoolong');
- return $this->fetchScaryTemplateMaybeFromCache($url);
- }
-
- function fetchScaryTemplateMaybeFromCache($url) {
- global $wgTranscludeCacheExpiry;
- $dbr = wfGetDB(DB_SLAVE);
- $obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'),
- array('tc_url' => $url));
- if ($obj) {
- $time = $obj->tc_time;
- $text = $obj->tc_contents;
- if ($time && time() < $time + $wgTranscludeCacheExpiry ) {
- return $text;
- }
- }
-
- $text = Http::get($url);
- if (!$text)
- return wfMsg('scarytranscludefailed', $url);
-
- $dbw = wfGetDB(DB_MASTER);
- $dbw->replace('transcache', array('tc_url'), array(
- 'tc_url' => $url,
- 'tc_time' => time(),
- 'tc_contents' => $text));
- return $text;
- }
-
-
- /**
- * Triple brace replacement -- used for template arguments
- * @private
- */
- function argSubstitution( $matches ) {
- $arg = trim( $matches['title'] );
- $text = $matches['text'];
- $inputArgs = end( $this->mArgStack );
-
- if ( array_key_exists( $arg, $inputArgs ) ) {
- $text = $inputArgs[$arg];
- } else if (($this->mOutputType == self::OT_HTML || $this->mOutputType == self::OT_PREPROCESS ) &&
- null != $matches['parts'] && count($matches['parts']) > 0) {
- $text = $matches['parts'][0];
- }
- if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
- $text = $matches['text'] .
- '<!-- WARNING: argument omitted, expansion size too large -->';
- }
-
- return $text;
- }
-
- /**
- * Increment an include size counter
- *
- * @param string $type The type of expansion
- * @param integer $size The size of the text
- * @return boolean False if this inclusion would take it over the maximum, true otherwise
- */
- function incrementIncludeSize( $type, $size ) {
- if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
- return false;
- } else {
- $this->mIncludeSizes[$type] += $size;
- return true;
- }
- }
-
- /**
- * Detect __NOGALLERY__ magic word and set a placeholder
- */
- function stripNoGallery( &$text ) {
- # if the string __NOGALLERY__ (not case-sensitive) occurs in the HTML,
- # do not add TOC
- $mw = MagicWord::get( 'nogallery' );
- $this->mOutput->mNoGallery = $mw->matchAndRemove( $text ) ;
- }
-
- /**
- * 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,
- # do not add TOC
- $mw = MagicWord::get( 'notoc' );
- if( $mw->matchAndRemove( $text ) ) {
- $this->mShowToc = false;
- }
-
- $mw = MagicWord::get( 'toc' );
- if( $mw->match( $text ) ) {
- $this->mShowToc = true;
- $this->mForceTocPosition = true;
-
- // Set a placeholder. At the end we'll fill it in with the TOC.
- $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
-
- // Only keep the first one.
- $text = $mw->replace( '', $text );
- }
- return $text;
- }
-
- /**
- * This function accomplishes several tasks:
- * 1) Auto-number headings if that option is enabled
- * 2) Add an [edit] link to sections for users who have enabled the option and can edit the page
- * 3) Add a Table of contents on the top for users who have enabled the option
- * 4) Auto-anchor headings
- *
- * It loops through all headlines, collects the necessary data, then splits up the
- * string and re-inserts the newly formatted headlines.
- *
- * @param string $text
- * @param boolean $isMain
- * @private
- */
- function formatHeadings( $text, $isMain=true ) {
- global $wgMaxTocLevel, $wgContLang;
-
- $doNumberHeadings = $this->mOptions->getNumberHeadings();
- if( !$this->mTitle->quickUserCan( 'edit' ) ) {
- $showEditLink = 0;
- } else {
- $showEditLink = $this->mOptions->getEditSection();
- }
-
- # Inhibit editsection links if requested in the page
- $esw =& MagicWord::get( 'noeditsection' );
- if( $esw->matchAndRemove( $text ) ) {
- $showEditLink = 0;
- }
-
- # Get all headlines for numbering them and adding funky stuff like [edit]
- # links - this is for later, but we need the number of headlines right now
- $matches = array();
- $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?'.'>)(?P<header>.*?)<\/H[1-6] *>/i', $text, $matches );
-
- # if there are fewer than 4 headlines in the article, do not show TOC
- # unless it's been explicitly enabled.
- $enoughToc = $this->mShowToc &&
- (($numMatches >= 4) || $this->mForceTocPosition);
-
- # Allow user to stipulate that a page should have a "new section"
- # link added via __NEWSECTIONLINK__
- $mw =& MagicWord::get( 'newsectionlink' );
- if( $mw->matchAndRemove( $text ) )
- $this->mOutput->setNewSection( true );
-
- # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
- # override above conditions and always show TOC above first header
- $mw =& MagicWord::get( 'forcetoc' );
- if ($mw->matchAndRemove( $text ) ) {
- $this->mShowToc = true;
- $enoughToc = true;
- }
-
- # 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
- $toc = '';
- $full = '';
- $head = array();
- $sublevelCount = array();
- $levelCount = array();
- $toclevel = 0;
- $level = 0;
- $prevlevel = 0;
- $toclevel = 0;
- $prevtoclevel = 0;
- $tocraw = array();
-
- foreach( $matches[3] as $headline ) {
- $istemplate = 0;
- $templatetitle = '';
- $templatesection = 0;
- $numbering = '';
- $mat = array();
- if (preg_match("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", $headline, $mat)) {
- $istemplate = 1;
- $templatetitle = base64_decode($mat[1]);
- $templatesection = 1 + (int)base64_decode($mat[2]);
- $headline = preg_replace("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", "", $headline);
- }
-
- if( $toclevel ) {
- $prevlevel = $level;
- $prevtoclevel = $toclevel;
- }
- $level = $matches[1][$headlineCount];
-
- if( $doNumberHeadings || $enoughToc ) {
-
- if ( $level > $prevlevel ) {
- # Increase TOC level
- $toclevel++;
- $sublevelCount[$toclevel] = 0;
- if( $toclevel<$wgMaxTocLevel ) {
- $prevtoclevel = $toclevel;
- $toc .= $sk->tocIndent();
- $numVisible++;
- }
- }
- elseif ( $level < $prevlevel && $toclevel > 1 ) {
- # Decrease TOC level, find level to jump to
-
- if ( $toclevel == 2 && $level <= $levelCount[1] ) {
- # Can only go down to level 1
- $toclevel = 1;
- } else {
- for ($i = $toclevel; $i > 0; $i--) {
- if ( $levelCount[$i] == $level ) {
- # Found last matching level
- $toclevel = $i;
- break;
- }
- elseif ( $levelCount[$i] < $level ) {
- # Found first matching level below current level
- $toclevel = $i + 1;
- break;
- }
- }
- }
- if( $toclevel<$wgMaxTocLevel ) {
- if($prevtoclevel < $wgMaxTocLevel) {
- # Unindent only if the previous toc level was shown :p
- $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
- } else {
- $toc .= $sk->tocLineEnd();
- }
- }
- }
- else {
- # No change in level, end TOC line
- if( $toclevel<$wgMaxTocLevel ) {
- $toc .= $sk->tocLineEnd();
- }
- }
-
- $levelCount[$toclevel] = $level;
-
- # count number of headlines for each level
- @$sublevelCount[$toclevel]++;
- $dot = 0;
- for( $i = 1; $i <= $toclevel; $i++ ) {
- if( !empty( $sublevelCount[$i] ) ) {
- if( $dot ) {
- $numbering .= '.';
- }
- $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
- $dot = 1;
- }
- }
- }
-
- # The canonized header is a version of the header text safe to use for links
- # Avoid insertion of weird stuff like <math> by expanding the relevant sections
- $canonized_headline = $this->mStripState->unstripBoth( $headline );
-
- # Remove link placeholders by the link text.
- # <!--LINK number-->
- # turns into
- # link text with suffix
- $canonized_headline = preg_replace( '/<!--LINK ([0-9]*)-->/e',
- "\$this->mLinkHolders['texts'][\$1]",
- $canonized_headline );
- $canonized_headline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e',
- "\$this->mInterwikiLinkHolders['texts'][\$1]",
- $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 = $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
- isset( $refers[$canonized_headline] ) ? $refers[$canonized_headline]++ : $refers[$canonized_headline] = 1;
- $refcount[$headlineCount]=$refers[$canonized_headline];
-
- # Don't number the heading if it is the only one (looks silly)
- if( $doNumberHeadings && count( $matches[3] ) > 1) {
- # the two are different if the line contains a link
- $headline=$numbering . ' ' . $headline;
- }
-
- # Create the anchor for linking from the TOC to the section
- $anchor = $canonized_headline;
- if($refcount[$headlineCount] > 1 ) {
- $anchor .= '_' . $refcount[$headlineCount];
- }
- if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
- $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
- $tocraw[] = array( 'toclevel' => $toclevel, 'level' => $level, 'line' => $tocline, 'number' => $numbering );
- }
- # give headline the correct <h#> tag
- if( $showEditLink && ( !$istemplate || $templatetitle !== "" ) ) {
- if( $istemplate )
- $editlink = $sk->editSectionLinkForOther($templatetitle, $templatesection);
- else
- $editlink = $sk->editSectionLink($this->mTitle, $sectionCount+1, $headline_hint);
- } else {
- $editlink = '';
- }
- $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink );
-
- $headlineCount++;
- if( !$istemplate )
- $sectionCount++;
- }
-
- $this->mOutput->setSections( $tocraw );
-
- # Never ever show TOC if no headers
- if( $numVisible < 1 ) {
- $enoughToc = false;
- }
-
- if( $enoughToc ) {
- if( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
- $toc .= $sk->tocUnindent( $prevtoclevel - 1 );
- }
- $toc = $sk->tocList( $toc );
- }
-
- # split up and insert constructed headlines
-
- $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
- $i = 0;
-
- foreach( $blocks as $block ) {
- if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) {
- # This is the [edit] link that appears for the top block of text when
- # section editing is enabled
-
- # Disabled because it broke block formatting
- # For example, a bullet point in the top line
- # $full .= $sk->editSectionLink(0);
- }
- $full .= $block;
- if( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) {
- # Top anchor now in skin
- $full = $full.$toc;
- }
-
- if( !empty( $head[$i] ) ) {
- $full .= $head[$i];
- }
- $i++;
- }
- if( $this->mForceTocPosition ) {
- return str_replace( '<!--MWTOC-->', $toc, $full );
- } else {
- return $full;
- }
- }
-
- /**
- * Transform wiki markup when saving a page by doing \r\n -> \n
- * conversion, substitting signatures, {{subst:}} templates, etc.
- *
- * @param string $text the text to transform
- * @param Title &$title the Title object for the current article
- * @param User &$user the User object describing the current user
- * @param ParserOptions $options parsing options
- * @param bool $clearState whether to clear the parser state first
- * @return string the altered wiki markup
- * @public
- */
- function preSaveTransform( $text, &$title, $user, $options, $clearState = true ) {
- $this->mOptions = $options;
- $this->mTitle =& $title;
- $this->setOutputType( self::OT_WIKI );
-
- if ( $clearState ) {
- $this->clearState();
- }
-
- $stripState = new StripState;
- $pairs = array(
- "\r\n" => "\n",
- );
- $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
- $text = $this->strip( $text, $stripState, true, array( 'gallery' ) );
- $text = $this->pstPass2( $text, $stripState, $user );
- $text = $stripState->unstripBoth( $text );
- return $text;
- }
-
- /**
- * Pre-save transform helper function
- * @private
- */
- function pstPass2( $text, &$stripState, $user ) {
- global $wgContLang, $wgLocaltimezone;
-
- /* Note: This is the timestamp saved as hardcoded wikitext to
- * the database, we use $wgContLang here in order to give
- * everyone the same signature and use the default one rather
- * than the one selected in each user's preferences.
- */
- if ( isset( $wgLocaltimezone ) ) {
- $oldtz = getenv( 'TZ' );
- putenv( 'TZ='.$wgLocaltimezone );
- }
- $d = $wgContLang->timeanddate( date( 'YmdHis' ), false, false) .
- ' (' . date( 'T' ) . ')';
- if ( isset( $wgLocaltimezone ) ) {
- putenv( 'TZ='.$oldtz );
- }
-
- # Variable replacement
- # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
- $text = $this->replaceVariables( $text );
-
- # Strip out <nowiki> etc. added via replaceVariables
- $text = $this->strip( $text, $stripState, false, array( 'gallery' ) );
-
- # Signatures
- $sigText = $this->getUserSig( $user );
- $text = strtr( $text, array(
- '~~~~~' => $d,
- '~~~~' => "$sigText $d",
- '~~~' => $sigText
- ) );
-
- # Context links: [[|name]] and [[name (context)|]]
- #
- global $wgLegalTitleChars;
- $tc = "[$wgLegalTitleChars]";
- $nc = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii!
-
- $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/"; # [[ns:page (context)|]]
- $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/"; # [[ns:page (context), context|]]
- $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]]
-
- # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
- $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
- $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
-
- $t = $this->mTitle->getText();
- $m = array();
- if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
- $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
- } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && '' != "$m[1]$m[2]" ) {
- $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
- } else {
- # if there's no context, don't bother duplicating the title
- $text = preg_replace( $p2, '[[\\1]]', $text );
- }
-
- # Trim trailing whitespace
- $text = rtrim( $text );
-
- return $text;
- }
-
- /**
- * Fetch the user's signature text, if any, and normalize to
- * validated, ready-to-insert wikitext.
- *
- * @param User $user
- * @return string
- * @private
- */
- function getUserSig( &$user ) {
- global $wgMaxSigChars;
-
- $username = $user->getName();
- $nickname = $user->getOption( 'nickname' );
- $nickname = $nickname === '' ? $username : $nickname;
-
- 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
- return $this->cleanSig( $nickname, true );
- } else {
- # Failed to validate; fall back to the default
- $nickname = $username;
- wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" );
- }
- }
-
- // Make sure nickname doesnt get a sig in a sig
- $nickname = $this->cleanSigInSig( $nickname );
-
- # If we're still here, make it a link to the user page
- $userText = wfEscapeWikiText( $username );
- $nickText = wfEscapeWikiText( $nickname );
- if ( $user->isAnon() ) {
- return wfMsgExt( 'signature-anon', array( 'content', 'parsemag' ), $userText, $nickText );
- } else {
- return wfMsgExt( 'signature', array( 'content', 'parsemag' ), $userText, $nickText );
- }
- }
-
- /**
- * Check that the user's signature contains no bad XML
- *
- * @param string $text
- * @return mixed An expanded string, or false if invalid.
- */
- function validateSig( $text ) {
- return( wfIsWellFormedXmlFragment( $text ) ? $text : false );
- }
-
- /**
- * Clean up signature text
- *
- * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig
- * 2) Substitute all transclusions
- *
- * @param string $text
- * @param $parsing Whether we're cleaning (preferences save) or parsing
- * @return string Signature text
- */
- function cleanSig( $text, $parsing = false ) {
- global $wgTitle;
- $this->startExternalParse( $this->mTitle, new ParserOptions(), $parsing ? self::OT_WIKI : self::OT_MSG );
-
- $substWord = MagicWord::get( 'subst' );
- $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
- $substText = '{{' . $substWord->getSynonym( 0 );
-
- $text = preg_replace( $substRegex, $substText, $text );
- $text = $this->cleanSigInSig( $text );
- $text = $this->replaceVariables( $text );
-
- $this->clearState();
- return $text;
- }
-
- /**
- * Strip ~~~, ~~~~ and ~~~~~ out of signatures
- * @param string $text
- * @return string Signature text with /~{3,5}/ removed
- */
- function cleanSigInSig( $text ) {
- $text = preg_replace( '/~{3,5}/', '', $text );
- return $text;
- }
-
- /**
- * Set up some variables which are usually set up in parse()
- * so that an external function can call some class members with confidence
- * @public
- */
- function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
- $this->mTitle =& $title;
- $this->mOptions = $options;
- $this->setOutputType( $outputType );
- if ( $clearState ) {
- $this->clearState();
- }
- }
-
- /**
- * Transform a MediaWiki message by replacing magic variables.
- *
- * @param string $text the text to transform
- * @param ParserOptions $options options
- * @return string the text with variables substituted
- * @public
- */
- function transformMsg( $text, $options ) {
- global $wgTitle;
- static $executing = false;
-
- $fname = "Parser::transformMsg";
-
- # Guard against infinite recursion
- if ( $executing ) {
- return $text;
- }
- $executing = true;
-
- wfProfileIn($fname);
-
- if ( $wgTitle && !( $wgTitle instanceof FakeTitle ) ) {
- $this->mTitle = $wgTitle;
- } else {
- $this->mTitle = Title::newFromText('msg');
- }
- $this->mOptions = $options;
- $this->setOutputType( self::OT_MSG );
- $this->clearState();
- $text = $this->replaceVariables( $text );
-
- $executing = false;
- wfProfileOut($fname);
- return $text;
- }
-
- /**
- * Create an HTML-style tag, e.g. <yourtag>special text</yourtag>
- * The callback should have the following form:
- * function myParserHook( $text, $params, &$parser ) { ... }
- *
- * Transform and return $text. Use $parser for any required context, e.g. use
- * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions
- *
- * @public
- *
- * @param mixed $tag The tag to use, e.g. 'hook' for <hook>
- * @param mixed $callback The callback function (and object) to use for the tag
- *
- * @return The old value of the mTagHooks array associated with the hook
- */
- function setHook( $tag, $callback ) {
- $tag = strtolower( $tag );
- $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
- $this->mTagHooks[$tag] = $callback;
-
- 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:
- * function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
- *
- * The callback may either return the text result of the function, or an array with the text
- * in element 0, and a number of flags in the other elements. The names of the flags are
- * specified in the keys. Valid flags are:
- * found The text returned is valid, stop processing the template. This
- * is on by default.
- * nowiki Wiki markup in the return value should be escaped
- * noparse Unsafe HTML tags should not be stripped, etc.
- * noargs Don't replace triple-brace arguments in the return value
- * isHTML The returned text is HTML, armour it against wikitext transformation
- *
- * @public
- *
- * @param string $id The magic word ID
- * @param mixed $callback The callback function (and object) to use
- * @param integer $flags a combination of the following flags:
- * SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
- *
- * @return The old callback function for this name, if any
- */
- function setFunctionHook( $id, $callback, $flags = 0 ) {
- $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id] : null;
- $this->mFunctionHooks[$id] = $callback;
-
- # Add to function cache
- $mw = MagicWord::get( $id );
- if( !$mw )
- throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' );
-
- $synonyms = $mw->getSynonyms();
- $sensitive = intval( $mw->isCaseSensitive() );
-
- foreach ( $synonyms as $syn ) {
- # Case
- if ( !$sensitive ) {
- $syn = strtolower( $syn );
- }
- # Add leading hash
- if ( !( $flags & SFH_NO_HASH ) ) {
- $syn = '#' . $syn;
- }
- # Remove trailing colon
- if ( substr( $syn, -1, 1 ) == ':' ) {
- $syn = substr( $syn, 0, -1 );
- }
- $this->mFunctionSynonyms[$sensitive][$syn] = $id;
- }
- return $oldVal;
- }
-
- /**
- * Get all registered function hook identifiers
- *
- * @return array
- */
- function getFunctionHooks() {
- return array_keys( $this->mFunctionHooks );
- }
-
- /**
- * Replace <!--LINK--> link placeholders with actual links, in the buffer
- * Placeholders created in Skin::makeLinkObj()
- * Returns an array of links found, indexed by PDBK:
- * 0 - broken
- * 1 - normal link
- * 2 - stub
- * $options is a bit field, RLH_FOR_UPDATE to select for update
- */
- function replaceLinkHolders( &$text, $options = 0 ) {
- global $wgUser;
- global $wgContLang;
-
- $fname = 'Parser::replaceLinkHolders';
- wfProfileIn( $fname );
-
- $pdbks = array();
- $colours = array();
- $sk = $this->mOptions->getSkin();
- $linkCache = LinkCache::singleton();
-
- if ( !empty( $this->mLinkHolders['namespaces'] ) ) {
- wfProfileIn( $fname.'-check' );
- $dbr = wfGetDB( DB_SLAVE );
- $page = $dbr->tableName( 'page' );
- $threshold = $wgUser->getOption('stubthreshold');
-
- # Sort by namespace
- asort( $this->mLinkHolders['namespaces'] );
-
- # Generate query
- $query = false;
- $current = null;
- foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
- # Make title object
- $title = $this->mLinkHolders['titles'][$key];
-
- # Skip invalid entries.
- # Result will be ugly, but prevents crash.
- if ( is_null( $title ) ) {
- continue;
- }
- $pdbk = $pdbks[$key] = $title->getPrefixedDBkey();
-
- # Check if it's a static known link, e.g. interwiki
- if ( $title->isAlwaysKnown() ) {
- $colours[$pdbk] = 1;
- } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
- $colours[$pdbk] = 1;
- $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 ) ) {
- $current = $ns;
- $query = "SELECT page_id, page_namespace, page_title, page_len, page_is_redirect";
- $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN(";
- } elseif ( $current != $ns ) {
- $current = $ns;
- $query .= ")) OR (page_namespace=$ns AND page_title IN(";
- } else {
- $query .= ', ';
- }
-
- $query .= $dbr->addQuotes( $this->mLinkHolders['dbkeys'][$key] );
- }
- }
- if ( $query ) {
- $query .= '))';
- if ( $options & RLH_FOR_UPDATE ) {
- $query .= ' FOR UPDATE';
- }
-
- $res = $dbr->query( $query, $fname );
-
- # Fetch data and form into an associative array
- # non-existent = broken
- # 1 = known
- # 2 = stub
- while ( $s = $dbr->fetchObject($res) ) {
- $title = Title::makeTitle( $s->page_namespace, $s->page_title );
- $pdbk = $title->getPrefixedDBkey();
- $linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect );
- $this->mOutput->addLink( $title, $s->page_id );
-
- $colours[$pdbk] = ( $threshold == 0 || (
- $s->page_len >= $threshold || # always true if $threshold <= 0
- $s->page_is_redirect ||
- !MWNamespace::isContent( $s->page_namespace ) )
- ? 1 : 2 );
- }
- }
- wfProfileOut( $fname.'-check' );
-
- # Do a second query for different language variants of links and categories
- if( $wgContLang->hasVariants() ) {
- $linkBatch = new LinkBatch();
- $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders)
- $categoryMap = array(); // maps $category_variant => $category (dbkeys)
- $varCategories = array(); // category replacements oldDBkey => newDBkey
-
- $categories = $this->mOutput->getCategoryLinks();
-
- // Add variants of links to link batch
- foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
- $title = $this->mLinkHolders['titles'][$key];
- if ( is_null( $title ) )
- continue;
-
- $pdbk = $title->getPrefixedDBkey();
- $titleText = $title->getText();
-
- // generate all variants of the link title text
- $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText);
-
- // if link was not found (in first query), add all variants to query
- if ( !isset($colours[$pdbk]) ){
- foreach($allTextVariants as $textVariant){
- if($textVariant != $titleText){
- $variantTitle = Title::makeTitle( $ns, $textVariant );
- if(is_null($variantTitle)) continue;
- $linkBatch->addObj( $variantTitle );
- $variantMap[$variantTitle->getPrefixedDBkey()][] = $key;
- }
- }
- }
- }
-
- // process categories, check if a category exists in some variant
- foreach( $categories as $category ){
- $variants = $wgContLang->convertLinkToAllVariants($category);
- foreach($variants as $variant){
- if($variant != $category){
- $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) );
- if(is_null($variantTitle)) continue;
- $linkBatch->addObj( $variantTitle );
- $categoryMap[$variant] = $category;
- }
- }
- }
-
-
- if ( !$linkBatch->isEmpty() ){
- // construct query
- $titleClause = $linkBatch->constructSet('page', $dbr);
-
- $variantQuery = "SELECT page_id, page_namespace, page_title, page_len, page_is_redirect";
-
- $variantQuery .= " FROM $page WHERE $titleClause";
- if ( $options & RLH_FOR_UPDATE ) {
- $variantQuery .= ' FOR UPDATE';
- }
-
- $varRes = $dbr->query( $variantQuery, $fname );
-
- // for each found variants, figure out link holders and replace
- while ( $s = $dbr->fetchObject($varRes) ) {
-
- $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
- $varPdbk = $variantTitle->getPrefixedDBkey();
- $vardbk = $variantTitle->getDBkey();
-
- $holderKeys = array();
- if(isset($variantMap[$varPdbk])){
- $holderKeys = $variantMap[$varPdbk];
- $linkCache->addGoodLinkObj( $s->page_id, $variantTitle, $s->page_len, $s->page_is_redirect );
- $this->mOutput->addLink( $variantTitle, $s->page_id );
- }
-
- // loop over link holders
- foreach($holderKeys as $key){
- $title = $this->mLinkHolders['titles'][$key];
- if ( is_null( $title ) ) continue;
-
- $pdbk = $title->getPrefixedDBkey();
-
- if(!isset($colours[$pdbk])){
- // found link in some of the variants, replace the link holder data
- $this->mLinkHolders['titles'][$key] = $variantTitle;
- $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey();
-
- // set pdbk and colour
- $pdbks[$key] = $varPdbk;
- if ( $threshold > 0 ) {
- $size = $s->page_len;
- if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) {
- $colours[$varPdbk] = 1;
- } else {
- $colours[$varPdbk] = 2;
- }
- }
- else {
- $colours[$varPdbk] = 1;
- }
- }
- }
-
- // check if the object is a variant of a category
- if(isset($categoryMap[$vardbk])){
- $oldkey = $categoryMap[$vardbk];
- if($oldkey != $vardbk)
- $varCategories[$oldkey]=$vardbk;
- }
- }
-
- // rebuild the categories in original order (if there are replacements)
- if(count($varCategories)>0){
- $newCats = array();
- $originalCats = $this->mOutput->getCategories();
- foreach($originalCats as $cat => $sortkey){
- // make the replacement
- if( array_key_exists($cat,$varCategories) )
- $newCats[$varCategories[$cat]] = $sortkey;
- else $newCats[$cat] = $sortkey;
- }
- $this->mOutput->setCategoryLinks($newCats);
- }
- }
- }
-
- # Construct search and replace arrays
- wfProfileIn( $fname.'-construct' );
- $replacePairs = array();
- foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
- $pdbk = $pdbks[$key];
- $searchkey = "<!--LINK $key-->";
- $title = $this->mLinkHolders['titles'][$key];
- if ( empty( $colours[$pdbk] ) ) {
- $linkCache->addBadLinkObj( $title );
- $colours[$pdbk] = 0;
- $this->mOutput->addLink( $title, 0 );
- $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title,
- $this->mLinkHolders['texts'][$key],
- $this->mLinkHolders['queries'][$key] );
- } elseif ( $colours[$pdbk] == 1 ) {
- $replacePairs[$searchkey] = $sk->makeKnownLinkObj( $title,
- $this->mLinkHolders['texts'][$key],
- $this->mLinkHolders['queries'][$key] );
- } elseif ( $colours[$pdbk] == 2 ) {
- $replacePairs[$searchkey] = $sk->makeStubLinkObj( $title,
- $this->mLinkHolders['texts'][$key],
- $this->mLinkHolders['queries'][$key] );
- }
- }
- $replacer = new HashtableReplacer( $replacePairs, 1 );
- wfProfileOut( $fname.'-construct' );
-
- # Do the thing
- wfProfileIn( $fname.'-replace' );
- $text = preg_replace_callback(
- '/(<!--LINK .*?-->)/',
- $replacer->cb(),
- $text);
-
- wfProfileOut( $fname.'-replace' );
- }
-
- # Now process interwiki link holders
- # This is quite a bit simpler than internal links
- if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) {
- wfProfileIn( $fname.'-interwiki' );
- # Make interwiki link HTML
- $replacePairs = array();
- foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) {
- $title = $this->mInterwikiLinkHolders['titles'][$key];
- $replacePairs[$key] = $sk->makeLinkObj( $title, $link );
- }
- $replacer = new HashtableReplacer( $replacePairs, 1 );
-
- $text = preg_replace_callback(
- '/<!--IWLINK (.*?)-->/',
- $replacer->cb(),
- $text );
- wfProfileOut( $fname.'-interwiki' );
- }
-
- wfProfileOut( $fname );
- return $colours;
- }
-
- /**
- * Replace <!--LINK--> link placeholders with plain text of links
- * (not HTML-formatted).
- * @param string $text
- * @return string
- */
- function replaceLinkHoldersText( $text ) {
- $fname = 'Parser::replaceLinkHoldersText';
- wfProfileIn( $fname );
-
- $text = preg_replace_callback(
- '/<!--(LINK|IWLINK) (.*?)-->/',
- array( &$this, 'replaceLinkHoldersTextCallback' ),
- $text );
-
- wfProfileOut( $fname );
- return $text;
- }
-
- /**
- * @param array $matches
- * @return string
- * @private
- */
- function replaceLinkHoldersTextCallback( $matches ) {
- $type = $matches[1];
- $key = $matches[2];
- if( $type == 'LINK' ) {
- if( isset( $this->mLinkHolders['texts'][$key] ) ) {
- return $this->mLinkHolders['texts'][$key];
- }
- } elseif( $type == 'IWLINK' ) {
- if( isset( $this->mInterwikiLinkHolders['texts'][$key] ) ) {
- return $this->mInterwikiLinkHolders['texts'][$key];
- }
- }
- return $matches[0];
- }
-
- /**
- * Tag hook handler for 'pre'.
- */
- function renderPreTag( $text, $attribs ) {
- // Backwards-compatibility hack
- $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' );
-
- $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
- return wfOpenElement( 'pre', $attribs ) .
- Xml::escapeTagsOnly( $content ) .
- '</pre>';
- }
-
- /**
- * Renders an image gallery from a text with one line per image.
- * text labels may be given by using |-style alternative text. E.g.
- * Image:one.jpg|The number "1"
- * Image:tree.jpg|A tree
- * given as text will return the HTML of a gallery with two images,
- * labeled 'The number "1"' and
- * 'A tree'.
- */
- function renderImageGallery( $text, $params ) {
- $ig = new ImageGallery();
- $ig->setContextTitle( $this->mTitle );
- $ig->setShowBytes( false );
- $ig->setShowFilename( false );
- $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'];
- $caption = htmlspecialchars( $caption );
- $caption = $this->replaceInternalLinks( $caption );
- $ig->setCaptionHtml( $caption );
- }
- if( isset( $params['perrow'] ) ) {
- $ig->setPerRow( $params['perrow'] );
- }
- if( isset( $params['widths'] ) ) {
- $ig->setWidths( $params['widths'] );
- }
- if( isset( $params['heights'] ) ) {
- $ig->setHeights( $params['heights'] );
- }
-
- wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
-
- $lines = explode( "\n", $text );
- foreach ( $lines as $line ) {
- # match lines like these:
- # Image:someimage.jpg|This is some image
- $matches = array();
- preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
- # Skip empty lines
- if ( count( $matches ) == 0 ) {
- continue;
- }
- $tp = Title::newFromText( $matches[1] );
- $nt =& $tp;
- if( is_null( $nt ) ) {
- # Bogus title. Ignore these so we don't bomb out later.
- continue;
- }
- if ( isset( $matches[3] ) ) {
- $label = $matches[3];
- } else {
- $label = '';
- }
-
- $pout = $this->parse( $label,
- $this->mTitle,
- $this->mOptions,
- false, // Strip whitespace...?
- false // Don't clear state!
- );
- $html = $pout->getText();
-
- $ig->add( $nt, $html );
-
- # Only add real images (bug #5586)
- if ( $nt->getNamespace() == NS_IMAGE ) {
- $this->mOutput->addImage( $nt->getDBkey() );
- }
- }
- 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( $title, $options ) {
- # @TODO: let the MediaHandler specify its transform parameters
- #
- # Check if the options text is of the form "options|alt text"
- # Options are:
- # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
- # * left no resizing, just left align. label is used for alt= only
- # * right same, but right aligned
- # * none same, but not aligned
- # * ___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
- # * super
- # * top
- # * text-top
- # * 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 ) );
-
- if ( $skip ) {
- return $sk->makeLinkObj( $title );
- }
-
- # Get parameter map
- $file = wfFindFile( $title, $time );
- $handler = $file ? $file->getHandler() : false;
-
- 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]*)$/', $value, $m ) ) {
- $params[$type]['width'] = intval( $m[1] );
- $params[$type]['height'] = intval( $m[2] );
- } else {
- $params[$type]['width'] = intval( $value );
- }
- }
- } 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 );
-
- # make sure there are no placeholders in thumbnail attributes
- # that are later expanded to html- so expand them now and
- # remove the tags
- $alt = $this->mStripState->unstripBoth( $alt );
- $alt = Sanitizer::stripAllTags( $alt );
-
- $params['frame']['alt'] = $alt;
- $params['frame']['caption'] = $caption;
-
- # Linker does the rest
- $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;
- }
-
- /**
- * Set a flag in the output object indicating that the content is dynamic and
- * shouldn't be cached.
- */
- function disableCache() {
- wfDebug( "Parser output marked as uncacheable.\n" );
- $this->mOutput->mCacheTime = -1;
- }
-
- /**#@+
- * Callback from the Sanitizer for expanding items found in HTML attribute
- * values, so they can be safely tested and escaped.
- * @param string $text
- * @param array $args
- * @return string
- * @private
- */
- function attributeStripCallback( &$text, $args ) {
- $text = $this->replaceVariables( $text, $args );
- $text = $this->mStripState->unstripBoth( $text );
- return $text;
- }
-
- /**#@-*/
-
- /**#@+
- * Accessor/mutator
- */
- function Title( $x = NULL ) { return wfSetVar( $this->mTitle, $x ); }
- function Options( $x = NULL ) { return wfSetVar( $this->mOptions, $x ); }
- function OutputType( $x = NULL ) { return wfSetVar( $this->mOutputType, $x ); }
- /**#@-*/
-
- /**#@+
- * Accessor
- */
- function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); }
- /**#@-*/
-
-
- /**
- * Break wikitext input into sections, and either pull or replace
- * some particular section's text.
- *
- * External callers should use the getSection and replaceSection methods.
- *
- * @param $text Page wikitext
- * @param $section Numbered section. 0 pulls the text before the first
- * heading; other numbers will pull the given section
- * along with its lower-level subsections.
- * @param $mode One of "get" or "replace"
- * @param $newtext Replacement text for section data.
- * @return string for "get", the extracted section text.
- * 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;
-
- $oldOutputType = $this->mOutputType;
- $oldOptions = $this->mOptions;
- $this->mOptions = new ParserOptions();
- $this->setOutputType( self::OT_WIKI );
-
- $striptext = $this->strip( $text, $stripState, true );
-
- $this->setOutputType( $oldOutputType );
- $this->mOptions = $oldOptions;
-
- # 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\x07)";
- $secs = preg_split(
- "/
- (
- ^
- (?:$comment|<\/?noinclude>)* # Initial comments will be stripped
- (=+) # Should this be limited to 6?
- .+? # Section title...
- \\2 # Ending = count must match start
- (?:$comment|<\/?noinclude>|[ \\t]+)* # Trailing whitespace ok
- $
- |
- <h([1-6])\b.*?>
- .*?
- <\/h\\3\s*>
- )
- /mix",
- $striptext, -1,
- PREG_SPLIT_DELIM_CAPTURE);
-
- if( $mode == "get" ) {
- if( $section == 0 ) {
- // "Section 0" returns the content before any other section.
- $rv = $secs[0];
- } else {
- //track missing section, will replace if found.
- $rv = $newtext;
- }
- } elseif( $mode == "replace" ) {
- if( $section == 0 ) {
- $rv = $newtext . "\n\n";
- $remainder = true;
- } else {
- $rv = $secs[0];
- $remainder = false;
- }
- }
- $count = 0;
- $sectionLevel = 0;
- for( $index = 1; $index < count( $secs ); ) {
- $headerLine = $secs[$index++];
- if( $secs[$index] ) {
- // A wiki header
- $headerLevel = strlen( $secs[$index++] );
- } else {
- // An HTML header
- $index++;
- $headerLevel = intval( $secs[$index++] );
- }
- $content = $secs[$index++];
-
- $count++;
- if( $mode == "get" ) {
- if( $count == $section ) {
- $rv = $headerLine . $content;
- $sectionLevel = $headerLevel;
- } elseif( $count > $section ) {
- if( $sectionLevel && $headerLevel > $sectionLevel ) {
- $rv .= $headerLine . $content;
- } else {
- // Broke out to a higher-level section
- break;
- }
- }
- } elseif( $mode == "replace" ) {
- if( $count < $section ) {
- $rv .= $headerLine . $content;
- } elseif( $count == $section ) {
- $rv .= $newtext . "\n\n";
- $sectionLevel = $headerLevel;
- } elseif( $count > $section ) {
- if( $headerLevel <= $sectionLevel ) {
- // Passed the section's sub-parts.
- $remainder = true;
- }
- if( $remainder ) {
- $rv .= $headerLine . $content;
- }
- }
- }
- }
- if (is_string($rv))
- # reinsert stripped tags
- $rv = trim( $stripState->unstripBoth( $rv ) );
-
- return $rv;
- }
-
- /**
- * This function returns the text of a section, specified by a number ($section).
- * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
- * the first section before any such heading (section 0).
- *
- * If a section contains subsections, these are also returned.
- *
- * @param $text String: text to look in
- * @param $section Integer: section number
- * @param $deftext: default to return if section is not found
- * @return string text of the requested section
- */
- public function getSection( $text, $section, $deftext='' ) {
- return $this->extractSections( $text, $section, "get", $deftext );
- }
-
- public function replaceSection( $oldtext, $section, $text ) {
- return $this->extractSections( $oldtext, $section, "replace", $text );
- }
-
- /**
- * Get the timestamp associated with the current revision, adjusted for
- * the default server-local timestamp
- */
- function getRevisionTimestamp() {
- if ( is_null( $this->mRevisionTimestamp ) ) {
- wfProfileIn( __METHOD__ );
- global $wgContLang;
- $dbr = wfGetDB( DB_SLAVE );
- $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
- array( 'rev_id' => $this->mRevisionId ), __METHOD__ );
-
- // Normalize timestamp to internal MW format for timezone processing.
- // This has the added side-effect of replacing a null value with
- // the current time, which gives us more sensible behavior for
- // previews.
- $timestamp = wfTimestamp( TS_MW, $timestamp );
-
- // The cryptic '' timezone parameter tells to use the site-default
- // timezone offset instead of the user settings.
- //
- // Since this value will be saved into the parser cache, served
- // to other users, and potentially even used inside links and such,
- // it needs to be consistent for all visitors.
- $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
-
- wfProfileOut( __METHOD__ );
- }
- return $this->mRevisionTimestamp;
- }
-
- /**
- * Mutator for $mDefaultSort
- *
- * @param $sort New value
- */
- public function setDefaultSort( $sort ) {
- $this->mDefaultSort = $sort;
- }
-
- /**
- * Accessor for $mDefaultSort
- * Will use the title/prefixed title if none is set
- *
- * @return string
- */
- public function getDefaultSort() {
- if( $this->mDefaultSort !== false ) {
- return $this->mDefaultSort;
- } else {
- return $this->mTitle->getNamespace() == NS_CATEGORY
- ? $this->mTitle->getText()
- : $this->mTitle->getPrefixedText();
- }
- }
-
- /**
- * Try to guess the section anchor name based on a wikitext fragment
- * presumably extracted from a heading, for example "Header" from
- * "== Header ==".
- */
- public function guessSectionNameFromWikiText( $text ) {
- # Strip out wikitext links(they break the anchor)
- $text = $this->stripSectionName( $text );
- $headline = Sanitizer::decodeCharReferences( $text );
- # strip out HTML
- $headline = StringUtils::delimiterReplace( '<', '>', '', $headline );
- $headline = trim( $headline );
- $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) );
- $replacearray = array(
- '%3A' => ':',
- '%' => '.'
- );
- return str_replace(
- array_keys( $replacearray ),
- array_values( $replacearray ),
- $sectionanchor );
- }
-
- /**
- * Strips a text string of wikitext for use in a section anchor
- *
- * Accepts a text string and then removes all wikitext from the
- * string and leaves only the resultant text (i.e. the result of
- * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of
- * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended
- * to create valid section anchors by mimicing the output of the
- * parser when headings are parsed.
- *
- * @param $text string Text string to be stripped of wikitext
- * for use in a Section anchor
- * @return Filtered text string
- */
- public function stripSectionName( $text ) {
- # Strip internal link markup
- $text = preg_replace('/\[\[:?([^[|]+)\|([^[]+)\]\]/','$2',$text);
- $text = preg_replace('/\[\[:?([^[]+)\|?\]\]/','$1',$text);
-
- # Strip external link markup (FIXME: Not Tolerant to blank link text
- # I.E. [http://www.mediawiki.org] will render as [1] or something depending
- # on how many empty links there are on the page - need to figure that out.
- $text = preg_replace('/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/','$2',$text);
-
- # Parse wikitext quotes (italics & bold)
- $text = $this->doQuotes($text);
-
- # Strip HTML tags
- $text = StringUtils::delimiterReplace( '<', '>', '', $text );
- return $text;
- }
-
- /**
- * strip/replaceVariables/unstrip for preprocessor regression testing
- */
- function srvus( $text ) {
- $text = $this->strip( $text, $this->mStripState );
- $text = Sanitizer::removeHTMLtags( $text );
- $text = $this->replaceVariables( $text );
- $text = preg_replace( '/<!--MWTEMPLATESECTION.*?-->/', '', $text );
- $text = $this->mStripState->unstripBoth( $text );
- return $text;
- }
-}
diff --git a/includes/parser/Preprocessor_DOM.php b/includes/parser/Preprocessor_DOM.php
index 34d58967..af591b67 100644
--- a/includes/parser/Preprocessor_DOM.php
+++ b/includes/parser/Preprocessor_DOM.php
@@ -770,6 +770,7 @@ class PPFrame_DOM implements PPFrame {
/**
* Recursion depth of this frame, top = 0
+ * Note that this is NOT the same as expansion depth in expand()
*/
var $depth;
@@ -826,20 +827,21 @@ class PPFrame_DOM implements PPFrame {
}
function expand( $root, $flags = 0 ) {
- static $depth = 0;
+ static $expansionDepth = 0;
if ( is_string( $root ) ) {
return $root;
}
+ wfProfileIn( __METHOD__ );
if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount )
{
return '<span class="error">Node-count limit exceeded</span>';
}
- if ( $depth > $this->parser->mOptions->mMaxPPExpandDepth ) {
+ if ( $expansionDepth > $this->parser->mOptions->mMaxPPExpandDepth ) {
return '<span class="error">Expansion depth limit exceeded</span>';
}
- ++$depth;
+ ++$expansionDepth;
if ( $root instanceof PPNode_DOM ) {
$root = $root->node;
@@ -1005,6 +1007,7 @@ class PPFrame_DOM implements PPFrame {
$newIterator = $contextNode->childNodes;
}
} else {
+ wfProfileOut( __METHOD__ );
throw new MWException( __METHOD__.': Invalid parameter type' );
}
@@ -1027,7 +1030,8 @@ class PPFrame_DOM implements PPFrame {
}
}
}
- --$depth;
+ --$expansionDepth;
+ wfProfileOut( __METHOD__ );
return $outStack[0];
}
@@ -1218,6 +1222,32 @@ class PPTemplateFrame_DOM extends PPFrame_DOM {
return !count( $this->numberedArgs ) && !count( $this->namedArgs );
}
+ function getArguments() {
+ $arguments = array();
+ foreach ( array_merge(
+ array_keys($this->numberedArgs),
+ array_keys($this->namedArgs)) as $key ) {
+ $arguments[$key] = $this->getArgument($key);
+ }
+ return $arguments;
+ }
+
+ function getNumberedArguments() {
+ $arguments = array();
+ foreach ( array_keys($this->numberedArgs) as $key ) {
+ $arguments[$key] = $this->getArgument($key);
+ }
+ return $arguments;
+ }
+
+ function getNamedArguments() {
+ $arguments = array();
+ foreach ( array_keys($this->namedArgs) as $key ) {
+ $arguments[$key] = $this->getArgument($key);
+ }
+ return $arguments;
+ }
+
function getNumberedArgument( $index ) {
if ( !isset( $this->numberedArgs[$index] ) ) {
return false;
@@ -1291,6 +1321,9 @@ class PPCustomFrame_DOM extends PPFrame_DOM {
}
function getArgument( $index ) {
+ if ( !isset( $this->args[$index] ) ) {
+ return false;
+ }
return $this->args[$index];
}
}
diff --git a/includes/parser/Preprocessor_Hash.php b/includes/parser/Preprocessor_Hash.php
index b5775243..62028291 100644
--- a/includes/parser/Preprocessor_Hash.php
+++ b/includes/parser/Preprocessor_Hash.php
@@ -758,6 +758,7 @@ class PPFrame_Hash implements PPFrame {
/**
* Recursion depth of this frame, top = 0
+ * Note that this is NOT the same as expansion depth in expand()
*/
var $depth;
@@ -810,6 +811,7 @@ class PPFrame_Hash implements PPFrame {
}
function expand( $root, $flags = 0 ) {
+ static $expansionDepth = 0;
if ( is_string( $root ) ) {
return $root;
}
@@ -818,10 +820,10 @@ class PPFrame_Hash implements PPFrame {
{
return '<span class="error">Node-count limit exceeded</span>';
}
- if ( $this->depth > $this->parser->mOptions->mMaxPPExpandDepth ) {
+ if ( $expansionDepth > $this->parser->mOptions->mMaxPPExpandDepth ) {
return '<span class="error">Expansion depth limit exceeded</span>';
}
- ++$this->depth;
+ ++$expansionDepth;
$outStack = array( '', '' );
$iteratorStack = array( false, $root );
@@ -974,7 +976,7 @@ class PPFrame_Hash implements PPFrame {
}
}
}
- --$this->depth;
+ --$expansionDepth;
return $outStack[0];
}
@@ -1173,6 +1175,32 @@ class PPTemplateFrame_Hash extends PPFrame_Hash {
return !count( $this->numberedArgs ) && !count( $this->namedArgs );
}
+ function getArguments() {
+ $arguments = array();
+ foreach ( array_merge(
+ array_keys($this->numberedArgs),
+ array_keys($this->namedArgs)) as $key ) {
+ $arguments[$key] = $this->getArgument($key);
+ }
+ return $arguments;
+ }
+
+ function getNumberedArguments() {
+ $arguments = array();
+ foreach ( array_keys($this->numberedArgs) as $key ) {
+ $arguments[$key] = $this->getArgument($key);
+ }
+ return $arguments;
+ }
+
+ function getNamedArguments() {
+ $arguments = array();
+ foreach ( array_keys($this->namedArgs) as $key ) {
+ $arguments[$key] = $this->getArgument($key);
+ }
+ return $arguments;
+ }
+
function getNumberedArgument( $index ) {
if ( !isset( $this->numberedArgs[$index] ) ) {
return false;
@@ -1246,6 +1274,9 @@ class PPCustomFrame_Hash extends PPFrame_Hash {
}
function getArgument( $index ) {
+ if ( !isset( $this->args[$index] ) ) {
+ return false;
+ }
return $this->args[$index];
}
}
diff --git a/includes/specials/SpecialAllmessages.php b/includes/specials/SpecialAllmessages.php
index c2a8de4e..0ff94b49 100644
--- a/includes/specials/SpecialAllmessages.php
+++ b/includes/specials/SpecialAllmessages.php
@@ -29,15 +29,19 @@ function wfSpecialAllmessages() {
$wgMessageCache->loadAllMessages();
- $sortedArray = array_merge( Language::getMessagesFor( 'en' ), $wgMessageCache->getExtensionMessagesFor( 'en' ) );
+ $sortedArray = array_merge( Language::getMessagesFor( 'en' ),
+ $wgMessageCache->getExtensionMessagesFor( 'en' ) );
ksort( $sortedArray );
- $messages = array();
- foreach ( $sortedArray as $key => $value ) {
+ $messages = array();
+ foreach( $sortedArray as $key => $value ) {
$messages[$key]['enmsg'] = $value;
- $messages[$key]['statmsg'] = wfMsgReal( $key, array(), false, false, false ); // wfMsgNoDbNoTrans doesn't exist
+ $messages[$key]['statmsg'] = wfMsgReal( $key, array(), false, false, false );
$messages[$key]['msg'] = wfMsgNoTrans( $key );
+ $sortedArray[$key] = NULL; // trade bytes from $sortedArray to this
+
}
+ unset($sortedArray); // trade bytes from $sortedArray to this
wfProfileOut( __METHOD__ . '-setup' );
@@ -63,13 +67,14 @@ function wfSpecialAllmessages() {
wfProfileOut( __METHOD__ );
}
-function wfAllMessagesMakeXml( $messages ) {
+function wfAllMessagesMakeXml( &$messages ) {
global $wgLang;
$lang = $wgLang->getCode();
$txt = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
$txt .= "<messages lang=\"$lang\">\n";
foreach( $messages as $key => $m ) {
$txt .= "\t" . Xml::element( 'message', array( 'name' => $key ), $m['msg'] ) . "\n";
+ $messages[$key] = NULL; // trade bytes
}
$txt .= "</messages>";
return $txt;
@@ -81,7 +86,7 @@ function wfAllMessagesMakeXml( $messages ) {
* @return The PHP messages array.
* @todo Make suitable for language files.
*/
-function wfAllMessagesMakePhp( $messages ) {
+function wfAllMessagesMakePhp( &$messages ) {
global $wgLang;
$txt = "\n\n\$messages = array(\n";
foreach( $messages as $key => $m ) {
@@ -94,6 +99,7 @@ function wfAllMessagesMakePhp( $messages ) {
$comment = '';
}
$txt .= "'$key' => '" . preg_replace( '/(?<!\\\\)\'/', "\'", $m['msg']) . "',$comment\n";
+ $messages[$key] = NULL; // trade bytes
}
$txt .= ');';
return $txt;
@@ -104,7 +110,7 @@ function wfAllMessagesMakePhp( $messages ) {
* @param $messages Messages array.
* @return The HTML list of messages.
*/
-function wfAllMessagesMakeHTMLText( $messages ) {
+function wfAllMessagesMakeHTMLText( &$messages ) {
global $wgLang, $wgContLang, $wgUser;
wfProfileIn( __METHOD__ );
@@ -123,7 +129,8 @@ function wfAllMessagesMakeHTMLText( $messages ) {
'onclick' => 'allmessagesmodified()'
), '' );
- $txt = '<span id="allmessagesfilter" style="display: none;">' . wfMsgHtml( 'allmessagesfilter' ) . " {$input}{$checkbox} " . '</span>';
+ $txt = '<span id="allmessagesfilter" style="display: none;">' . wfMsgHtml( 'allmessagesfilter' ) .
+ " {$input}{$checkbox} " . '</span>';
$txt .= '
<table border="1" cellspacing="0" width="100%" id="allmessagestable">
@@ -144,11 +151,14 @@ function wfAllMessagesMakeHTMLText( $messages ) {
NS_MEDIAWIKI_TALK => array()
);
$dbr = wfGetDB( DB_SLAVE );
- $page = $dbr->tableName( 'page' );
- $sql = "SELECT page_namespace,page_title FROM $page WHERE page_namespace IN (" . NS_MEDIAWIKI . ", " . NS_MEDIAWIKI_TALK . ")";
- $res = $dbr->query( $sql );
+ $res = $dbr->select( 'page',
+ array( 'page_namespace', 'page_title' ),
+ array( 'page_namespace' => array(NS_MEDIAWIKI,NS_MEDIAWIKI_TALK) ),
+ __METHOD__,
+ array( 'USE INDEX' => 'name_title' )
+ );
while( $s = $dbr->fetchObject( $res ) ) {
- $pageExists[$s->page_namespace][$s->page_title] = true;
+ $pageExists[$s->page_namespace][$s->page_title] = 1;
}
$dbr->freeResult( $res );
wfProfileOut( __METHOD__ . "-check" );
@@ -163,19 +173,21 @@ function wfAllMessagesMakeHTMLText( $messages ) {
$title .= '/' . $wgLang->getCode();
}
- $titleObj =& Title::makeTitle( NS_MEDIAWIKI, $title );
- $talkPage =& Title::makeTitle( NS_MEDIAWIKI_TALK, $title );
+ $titleObj = Title::makeTitle( NS_MEDIAWIKI, $title );
+ $talkPage = Title::makeTitle( NS_MEDIAWIKI_TALK, $title );
$changed = ( $m['statmsg'] != $m['msg'] );
$message = htmlspecialchars( $m['statmsg'] );
$mw = htmlspecialchars( $m['msg'] );
- if( isset( $pageExists[NS_MEDIAWIKI][$title] ) ) {
- $pageLink = $sk->makeKnownLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . htmlspecialchars( $key ) . '</span>' );
+ if( array_key_exists( $title, $pageExists[NS_MEDIAWIKI] ) ) {
+ $pageLink = $sk->makeKnownLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" .
+ htmlspecialchars( $key ) . '</span>' );
} else {
- $pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . htmlspecialchars( $key ) . '</span>' );
+ $pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" .
+ htmlspecialchars( $key ) . '</span>' );
}
- if( isset( $pageExists[NS_MEDIAWIKI_TALK][$title] ) ) {
+ if( array_key_exists( $title, $pageExists[NS_MEDIAWIKI_TALK] ) ) {
$talkLink = $sk->makeKnownLinkObj( $talkPage, htmlspecialchars( $talk ) );
} else {
$talkLink = $sk->makeBrokenLinkObj( $talkPage, htmlspecialchars( $talk ) );
@@ -186,27 +198,28 @@ function wfAllMessagesMakeHTMLText( $messages ) {
if( $changed ) {
$txt .= "
- <tr class=\"orig\" id=\"sp-allmessages-r1-$i\">
- <td rowspan=\"2\">
- $anchor$pageLink<br />$talkLink
- </td><td>
-$message
- </td>
- </tr><tr class=\"new\" id=\"sp-allmessages-r2-$i\">
- <td>
-$mw
- </td>
- </tr>";
+ <tr class=\"orig\" id=\"sp-allmessages-r1-$i\">
+ <td rowspan=\"2\">
+ $anchor$pageLink<br />$talkLink
+ </td><td>
+ $message
+ </td>
+ </tr><tr class=\"new\" id=\"sp-allmessages-r2-$i\">
+ <td>
+ $mw
+ </td>
+ </tr>";
} else {
$txt .= "
- <tr class=\"def\" id=\"sp-allmessages-r1-$i\">
- <td>
- $anchor$pageLink<br />$talkLink
- </td><td>
-$mw
- </td>
- </tr>";
+ <tr class=\"def\" id=\"sp-allmessages-r1-$i\">
+ <td>
+ $anchor$pageLink<br />$talkLink
+ </td><td>
+ $mw
+ </td>
+ </tr>";
}
+ $messages[$key] = NULL; // trade bytes
$i++;
}
$txt .= '</table>';
diff --git a/includes/specials/SpecialAllpages.php b/includes/specials/SpecialAllpages.php
index 7223e317..bf68dfa6 100644
--- a/includes/specials/SpecialAllpages.php
+++ b/includes/specials/SpecialAllpages.php
@@ -1,404 +1,445 @@
<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Entry point : initialise variables and call subfunctions.
- * @param $par String: becomes "FOO" when called like Special:Allpages/FOO (default NULL)
- * @param $specialPage See the SpecialPage object.
- */
-function wfSpecialAllpages( $par=NULL, $specialPage ) {
- global $wgRequest, $wgOut, $wgContLang;
-
- # GET values
- $from = $wgRequest->getVal( 'from' );
- $namespace = $wgRequest->getInt( 'namespace' );
-
- $namespaces = $wgContLang->getNamespaces();
-
- $indexPage = new SpecialAllpages();
-
- $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() );
- } elseif ( isset($from) ) {
- $indexPage->showChunk( $namespace, $from, $specialPage->including() );
- } else {
- $indexPage->showToplevel ( $namespace, $specialPage->including() );
- }
-}
/**
* Implements Special:Allpages
* @ingroup SpecialPage
*/
-class SpecialAllpages {
+class SpecialAllpages extends IncludableSpecialPage {
+
/**
* Maximum number of pages to show on single subpage.
*/
- protected $maxPerPage = 960;
+ protected $maxPerPage = 345;
/**
- * Name of this special page. Used to make title objects that reference back
- * to this page.
+ * Maximum number of pages to show on single index subpage.
*/
- protected $name = 'Allpages';
+ protected $maxLineCount = 200;
+
+ /**
+ * Maximum number of chars to show for an entry.
+ */
+ protected $maxPageLength = 70;
/**
* Determines, which message describes the input field 'nsfrom'.
*/
protected $nsfromMsg = 'allpagesfrom';
-/**
- * HTML for the top form
- * @param integer $namespace A namespace constant (default NS_MAIN).
- * @param string $from Article name we are starting listing at.
- */
-function namespaceForm ( $namespace = NS_MAIN, $from = '' ) {
- global $wgScript;
- $t = SpecialPage::getTitleFor( $this->name );
-
- $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( 'fieldset' );
- $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) );
- $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) );
- $out .= "<tr>
- <td class='mw-label'>" .
- Xml::label( wfMsg( $this->nsfromMsg ), 'nsfrom' ) .
- "</td>
- <td class='mw-input'>" .
- Xml::input( 'from', 20, $from, array( 'id' => 'nsfrom' ) ) .
- "</td>
- </tr>
- <tr>
- <td class='mw-label'>" .
- Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
- "</td>
- <td class='mw-input'>" .
- Xml::namespaceSelector( $namespace, null ) . ' ' .
- Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
- "</td>
- </tr>";
- $out .= Xml::closeElement( 'table' );
- $out .= Xml::closeElement( 'fieldset' );
- $out .= Xml::closeElement( 'form' );
- $out .= Xml::closeElement( 'div' );
- return $out;
-}
+ function __construct( $name = 'Allpages' ){
+ parent::__construct( $name );
+ }
-/**
- * @param integer $namespace (default NS_MAIN)
- */
-function showToplevel ( $namespace = NS_MAIN, $including = false ) {
- global $wgOut, $wgContLang;
- $align = $wgContLang->isRtl() ? 'left' : 'right';
+ /**
+ * Entry point : initialise variables and call subfunctions.
+ * @param $par String: becomes "FOO" when called like Special:Allpages/FOO (default NULL)
+ * @param $specialPage See the SpecialPage object.
+ */
+ function execute( $par ) {
+ global $wgRequest, $wgOut, $wgContLang;
- # TODO: Either make this *much* faster or cache the title index points
- # in the querycache table.
+ $this->setHeaders();
+ $this->outputHeader();
- $dbr = wfGetDB( DB_SLAVE );
- $out = "";
- $where = array( 'page_namespace' => $namespace );
+ # GET values
+ $from = $wgRequest->getVal( 'from', null );
+ $to = $wgRequest->getVal( 'to', null );
+ $namespace = $wgRequest->getInt( 'namespace' );
- global $wgMemc;
- $key = wfMemcKey( 'allpages', 'ns', $namespace );
- $lines = $wgMemc->get( $key );
+ $namespaces = $wgContLang->getNamespaces();
- if( !is_array( $lines ) ) {
- $options = array( 'LIMIT' => 1 );
- if ( ! $dbr->implicitOrderby() ) {
- $options['ORDER BY'] = 'page_title';
+ $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) ) ?
+ wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) :
+ wfMsg( 'allarticles' )
+ );
+
+ if( isset($par) ) {
+ $this->showChunk( $namespace, $par, $to );
+ } elseif( isset($from) && !isset($to) ) {
+ $this->showChunk( $namespace, $from, $to );
+ } else {
+ $this->showToplevel( $namespace, $from, $to );
}
- $firstTitle = $dbr->selectField( 'page', 'page_title', $where, __METHOD__, $options );
- $lastTitle = $firstTitle;
-
- # This array is going to hold the page_titles in order.
- $lines = array( $firstTitle );
-
- # If we are going to show n rows, we need n+1 queries to find the relevant titles.
- $done = false;
- for( $i = 0; !$done; ++$i ) {
- // Fetch the last title of this chunk and the first of the next
- $chunk = is_null( $lastTitle )
- ? ''
- : 'page_title >= ' . $dbr->addQuotes( $lastTitle );
- $res = $dbr->select(
- 'page', /* FROM */
- 'page_title', /* WHAT */
- $where + array($chunk),
- __METHOD__,
- array ('LIMIT' => 2, 'OFFSET' => $this->maxPerPage - 1, 'ORDER BY' => 'page_title') );
+ }
- if ( $s = $dbr->fetchObject( $res ) ) {
- array_push( $lines, $s->page_title );
- } else {
- // Final chunk, but ended prematurely. Go back and find the end.
- $endTitle = $dbr->selectField( 'page', 'MAX(page_title)',
- array(
- 'page_namespace' => $namespace,
- $chunk
- ), __METHOD__ );
- array_push( $lines, $endTitle );
- $done = true;
+ /**
+ * HTML for the top form
+ * @param integer $namespace A namespace constant (default NS_MAIN).
+ * @param string $from dbKey we are starting listing at.
+ * @param string $to dbKey we are ending listing at.
+ */
+ function namespaceForm( $namespace = NS_MAIN, $from = '', $to = '' ) {
+ global $wgScript;
+ $t = $this->getTitle();
+
+ $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( 'fieldset' );
+ $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) );
+ $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) );
+ $out .= "<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'allpagesfrom' ), 'nsfrom' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'allpagesto' ), 'nsto' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'to', 30, str_replace('_',' ',$to), array( 'id' => 'nsto' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::namespaceSelector( $namespace, null ) . ' ' .
+ Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
+ "</td>
+ </tr>";
+ $out .= Xml::closeElement( 'table' );
+ $out .= Xml::closeElement( 'fieldset' );
+ $out .= Xml::closeElement( 'form' );
+ $out .= Xml::closeElement( 'div' );
+ return $out;
+ }
+
+ /**
+ * @param integer $namespace (default NS_MAIN)
+ */
+ function showToplevel( $namespace = NS_MAIN, $from = '', $to = '' ) {
+ global $wgOut, $wgContLang;
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+ # TODO: Either make this *much* faster or cache the title index points
+ # in the querycache table.
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $out = "";
+ $where = array( 'page_namespace' => $namespace );
+
+ $from = Title::makeTitleSafe( $namespace, $from );
+ $to = Title::makeTitleSafe( $namespace, $to );
+ $from = ( $from && $from->isLocal() ) ? $from->getDBKey() : null;
+ $to = ( $to && $to->isLocal() ) ? $to->getDBKey() : null;
+
+ if( isset($from) )
+ $where[] = 'page_title >= '.$dbr->addQuotes( $from );
+ if( isset($to) )
+ $where[] = 'page_title <= '.$dbr->addQuotes( $to );
+
+ global $wgMemc;
+ $key = wfMemcKey( 'allpages', 'ns', $namespace, $from, $to );
+ $lines = $wgMemc->get( $key );
+
+ $count = $dbr->estimateRowCount( 'page', '*', $where, __METHOD__ );
+ $maxPerSubpage = intval($count/$this->maxLineCount);
+ $maxPerSubpage = max($maxPerSubpage,$this->maxPerPage);
+
+ if( !is_array( $lines ) ) {
+ $options = array( 'LIMIT' => 1 );
+ $options['ORDER BY'] = 'page_title ASC';
+ $firstTitle = $dbr->selectField( 'page', 'page_title', $where, __METHOD__, $options );
+ $lastTitle = $firstTitle;
+ # This array is going to hold the page_titles in order.
+ $lines = array( $firstTitle );
+ # If we are going to show n rows, we need n+1 queries to find the relevant titles.
+ $done = false;
+ while( !$done ) {
+ // Fetch the last title of this chunk and the first of the next
+ $chunk = ( $lastTitle === false )
+ ? array()
+ : array( 'page_title >= ' . $dbr->addQuotes( $lastTitle ) );
+ $res = $dbr->select( 'page', /* FROM */
+ 'page_title', /* WHAT */
+ array_merge($where,$chunk),
+ __METHOD__,
+ array ('LIMIT' => 2, 'OFFSET' => $maxPerSubpage - 1, 'ORDER BY' => 'page_title ASC')
+ );
+
+ if( $s = $dbr->fetchObject( $res ) ) {
+ array_push( $lines, $s->page_title );
+ } else {
+ // Final chunk, but ended prematurely. Go back and find the end.
+ $endTitle = $dbr->selectField( 'page', 'MAX(page_title)',
+ array_merge($where,$chunk),
+ __METHOD__ );
+ array_push( $lines, $endTitle );
+ $done = true;
+ }
+ if( $s = $res->fetchObject() ) {
+ array_push( $lines, $s->page_title );
+ $lastTitle = $s->page_title;
+ } else {
+ // This was a final chunk and ended exactly at the limit.
+ // Rare but convenient!
+ $done = true;
+ }
+ $res->free();
}
- if( $s = $dbr->fetchObject( $res ) ) {
- array_push( $lines, $s->page_title );
- $lastTitle = $s->page_title;
+ $wgMemc->add( $key, $lines, 3600 );
+ }
+
+ // If there are only two or less sections, don't even display them.
+ // Instead, display the first section directly.
+ if( count( $lines ) <= 2 ) {
+ if( !empty($lines) ) {
+ $this->showChunk( $namespace, $lines[0], $lines[count($lines)-1] );
} else {
- // This was a final chunk and ended exactly at the limit.
- // Rare but convenient!
- $done = true;
+ $wgOut->addHTML( $this->namespaceForm( $namespace, $from, $to ) );
}
- $dbr->freeResult( $res );
+ return;
}
- $wgMemc->add( $key, $lines, 3600 );
- }
- // If there are only two or less sections, don't even display them.
- // Instead, display the first section directly.
- if( count( $lines ) <= 2 ) {
- $this->showChunk( $namespace, '', $including );
- return;
- }
+ # At this point, $lines should contain an even number of elements.
+ $out .= "<table class='allpageslist' style='background: inherit;'>";
+ while( count ( $lines ) > 0 ) {
+ $inpoint = array_shift( $lines );
+ $outpoint = array_shift( $lines );
+ $out .= $this->showline( $inpoint, $outpoint, $namespace );
+ }
+ $out .= '</table>';
+ $nsForm = $this->namespaceForm( $namespace, $from, $to );
- # At this point, $lines should contain an even number of elements.
- $out .= "<table class='allpageslist' style='background: inherit;'>";
- while ( count ( $lines ) > 0 ) {
- $inpoint = array_shift ( $lines );
- $outpoint = array_shift ( $lines );
- $out .= $this->showline ( $inpoint, $outpoint, $namespace, false );
- }
- $out .= '</table>';
- $nsForm = $this->namespaceForm( $namespace, '', false );
-
- # Is there more?
- if ( $including ) {
- $out2 = '';
- } else {
- $morelinks = '';
- if ( $morelinks != '' ) {
- $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
- $out2 .= '<tr valign="top"><td>' . $nsForm;
- $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">';
- $out2 .= $morelinks . '</td></tr></table><hr />';
+ # Is there more?
+ if( $this->including() ) {
+ $out2 = '';
} else {
- $out2 = $nsForm . '<hr />';
+ if( isset($from) || isset($to) ) {
+ global $wgUser;
+ $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
+ $out2 .= '<tr valign="top"><td>' . $nsForm;
+ $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' .
+ $wgUser->getSkin()->makeKnownLinkObj( $this->getTitle(), wfMsgHtml ( 'allpages' ) );
+ $out2 .= "</td></tr></table><hr />";
+ } else {
+ $out2 = $nsForm . '<hr />';
+ }
}
+ $wgOut->addHTML( $out2 . $out );
}
- $wgOut->addHtml( $out2 . $out );
-}
-
-/**
- * @todo Document
- * @param string $from
- * @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" : '');
- $special = SpecialPage::getTitleFor( $this->name, $inpoint );
- $link = $special->escapeLocalUrl( $queryparams );
-
- $out = wfMsgHtml(
- 'alphaindexline',
- "<a href=\"$link\">$inpointf</a></td><td><a href=\"$link\">",
- "</a></td><td><a href=\"$link\">$outpointf</a>"
- );
- return '<tr><td align="' . $align . '">'.$out.'</td></tr>';
-}
+ /**
+ * Show a line of "ABC to DEF" ranges of articles
+ * @param string $inpoint Lower limit of pagenames
+ * @param string $outpout Upper limit of pagenames
+ * @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 ) );
+ // Don't let the length runaway
+ $inpointf = $wgContLang->truncate( $inpointf, $this->maxPageLength, '...' );
+ $outpointf = $wgContLang->truncate( $outpointf, $this->maxPageLength, '...' );
+
+ $queryparams = $namespace ? "namespace=$namespace&" : '';
+ $special = $this->getTitle();
+ $link = $special->escapeLocalUrl( $queryparams . 'from=' . urlencode($inpoint) . '&to=' . urlencode($outpoint) );
+
+ $out = wfMsgHtml( 'alphaindexline',
+ "<a href=\"$link\">$inpointf</a></td><td>",
+ "</td><td><a href=\"$link\">$outpointf</a>"
+ );
+ return '<tr><td align="' . $align . '">'.$out.'</td></tr>';
+ }
-/**
- * @param integer $namespace (Default NS_MAIN)
- * @param string $from list all pages from this name (default FALSE)
- */
-function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
- global $wgOut, $wgUser, $wgContLang;
+ /**
+ * @param integer $namespace (Default NS_MAIN)
+ * @param string $from list all pages from this name (default FALSE)
+ * @param string $to list all pages to this name (default FALSE)
+ */
+ function showChunk( $namespace = NS_MAIN, $from = false, $to = false ) {
+ global $wgOut, $wgUser, $wgContLang;
- $sk = $wgUser->getSkin();
+ $sk = $wgUser->getSkin();
- $fromList = $this->getNamespaceKeyAndText($namespace, $from);
- $namespaces = $wgContLang->getNamespaces();
- $align = $wgContLang->isRtl() ? 'left' : 'right';
+ $fromList = $this->getNamespaceKeyAndText($namespace, $from);
+ $toList = $this->getNamespaceKeyAndText( $namespace, $to );
+ $namespaces = $wgContLang->getNamespaces();
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
- $n = 0;
+ $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;
+ if ( !$fromList || !$toList ) {
+ $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;
+ list( $namespace2, $toKey, $to ) = $toList;
- $dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( 'page',
- array( 'page_namespace', 'page_title', 'page_is_redirect' ),
- array(
+ $dbr = wfGetDB( DB_SLAVE );
+ $conds = array(
'page_namespace' => $namespace,
'page_title >= ' . $dbr->addQuotes( $fromKey )
- ),
- __METHOD__,
- array(
- 'ORDER BY' => 'page_title',
- 'LIMIT' => $this->maxPerPage + 1,
- 'USE INDEX' => 'name_title',
- )
- );
+ );
+ if( $toKey !== "" ) {
+ $conds[] = 'page_title <= ' . $dbr->addQuotes( $toKey );
+ }
- if( $res->numRows() > 0 ) {
- $out = '<table style="background: inherit;" border="0" width="100%">';
-
- while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
- $t = Title::makeTitle( $s->page_namespace, $s->page_title );
- if( $t ) {
- $link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
- $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) .
- ($s->page_is_redirect ? '</div>' : '' );
- } else {
- $link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
- }
- if( $n % 3 == 0 ) {
- $out .= '<tr>';
+ $res = $dbr->select( 'page',
+ array( 'page_namespace', 'page_title', 'page_is_redirect' ),
+ $conds,
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'page_title',
+ 'LIMIT' => $this->maxPerPage + 1,
+ 'USE INDEX' => 'name_title',
+ )
+ );
+
+ if( $res->numRows() > 0 ) {
+ $out = '<table style="background: inherit;" border="0" width="100%">';
+
+ while( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) {
+ $t = Title::makeTitle( $s->page_namespace, $s->page_title );
+ if( $t ) {
+ $link = ( $s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
+ $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) .
+ ($s->page_is_redirect ? '</div>' : '' );
+ } else {
+ $link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
+ }
+ if( $n % 3 == 0 ) {
+ $out .= '<tr>';
+ }
+ $out .= "<td width=\"33%\">$link</td>";
+ $n++;
+ if( $n % 3 == 0 ) {
+ $out .= '</tr>';
+ }
}
- $out .= "<td width=\"33%\">$link</td>";
- $n++;
- if( $n % 3 == 0 ) {
+ if( ($n % 3) != 0 ) {
$out .= '</tr>';
}
+ $out .= '</table>';
+ } else {
+ $out = '';
}
- if( ($n % 3) != 0 ) {
- $out .= '</tr>';
- }
- $out .= '</table>';
- } else {
- $out = '';
}
- }
- if ( $including ) {
- $out2 = '';
- } else {
- if( $from == '' ) {
- // First chunk; no previous link.
- $prevTitle = null;
+ if ( $this->including() ) {
+ $out2 = '';
} else {
- # Get the last title from previous chunk
- $dbr = wfGetDB( DB_SLAVE );
- $res_prev = $dbr->select(
- 'page',
- 'page_title',
- array( 'page_namespace' => $namespace, 'page_title < '.$dbr->addQuotes($from) ),
- __METHOD__,
- array( 'ORDER BY' => 'page_title DESC', 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 ) )
- );
-
- # Get first title of previous complete chunk
- if( $dbr->numrows( $res_prev ) >= $this->maxPerPage ) {
- $pt = $dbr->fetchObject( $res_prev );
- $prevTitle = Title::makeTitle( $namespace, $pt->page_title );
+ if( $from == '' ) {
+ // First chunk; no previous link.
+ $prevTitle = null;
} else {
- # The previous chunk is not complete, need to link to the very first title
- # available in the database
- $options = array( 'LIMIT' => 1 );
- if ( ! $dbr->implicitOrderby() ) {
- $options['ORDER BY'] = 'page_title';
- }
- $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), __METHOD__, $options );
- # Show the previous link if it s not the current requested chunk
- if( $from != $reallyFirstPage_title ) {
- $prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title );
+ # Get the last title from previous chunk
+ $dbr = wfGetDB( DB_SLAVE );
+ $res_prev = $dbr->select(
+ 'page',
+ 'page_title',
+ array( 'page_namespace' => $namespace, 'page_title < '.$dbr->addQuotes($from) ),
+ __METHOD__,
+ array( 'ORDER BY' => 'page_title DESC', 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 ) )
+ );
+
+ # Get first title of previous complete chunk
+ if( $dbr->numrows( $res_prev ) >= $this->maxPerPage ) {
+ $pt = $dbr->fetchObject( $res_prev );
+ $prevTitle = Title::makeTitle( $namespace, $pt->page_title );
} else {
- $prevTitle = null;
+ # The previous chunk is not complete, need to link to the very first title
+ # available in the database
+ $options = array( 'LIMIT' => 1 );
+ if ( ! $dbr->implicitOrderby() ) {
+ $options['ORDER BY'] = 'page_title';
+ }
+ $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title',
+ array( 'page_namespace' => $namespace ), __METHOD__, $options );
+ # Show the previous link if it s not the current requested chunk
+ if( $from != $reallyFirstPage_title ) {
+ $prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title );
+ } else {
+ $prevTitle = null;
+ }
}
}
- }
- $nsForm = $this->namespaceForm( $namespace, $from );
- $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
- $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' ) );
-
- $self = SpecialPage::getTitleFor( 'Allpages' );
-
- # Do we put a previous link ?
- if( isset( $prevTitle ) && $pt = $prevTitle->getText() ) {
- $q = 'from=' . $prevTitle->getPartialUrl()
- . ( $namespace ? '&namespace=' . $namespace : '' );
- $prevLink = $sk->makeKnownLinkObj( $self,
- wfMsgHTML( 'prevpage', htmlspecialchars( $pt ) ), $q );
- $out2 .= ' | ' . $prevLink;
- }
+ $self = $this->getTitle();
- if( $n == $this->maxPerPage && $s = $dbr->fetchObject($res) ) {
- # $s is the first link of the next chunk
- $t = Title::MakeTitle($namespace, $s->page_title);
- $q = 'from=' . $t->getPartialUrl()
- . ( $namespace ? '&namespace=' . $namespace : '' );
- $nextLink = $sk->makeKnownLinkObj( $self,
- wfMsgHtml( 'nextpage', htmlspecialchars( $t->getText() ) ), $q );
- $out2 .= ' | ' . $nextLink;
- }
- $out2 .= "</td></tr></table><hr />";
- }
+ $nsForm = $this->namespaceForm( $namespace, $from, $to );
+ $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
+ $out2 .= '<tr valign="top"><td>' . $nsForm;
+ $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' .
+ $sk->makeKnownLinkObj( $self,
+ wfMsgHtml ( 'allpages' ) );
+
+ # Do we put a previous link ?
+ if( isset( $prevTitle ) && $pt = $prevTitle->getText() ) {
+ $q = 'from=' . $prevTitle->getPartialUrl()
+ . ( $namespace ? '&namespace=' . $namespace : '' );
+ $prevLink = $sk->makeKnownLinkObj( $self,
+ wfMsgHTML( 'prevpage', htmlspecialchars( $pt ) ), $q );
+ $out2 .= ' | ' . $prevLink;
+ }
- $wgOut->addHtml( $out2 . $out );
- if( isset($prevLink) or 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 );
+ if( $n == $this->maxPerPage && $s = $res->fetchObject() ) {
+ # $s is the first link of the next chunk
+ $t = Title::MakeTitle($namespace, $s->page_title);
+ $q = 'from=' . $t->getPartialUrl()
+ . ( $namespace ? '&namespace=' . $namespace : '' );
+ $nextLink = $sk->makeKnownLinkObj( $self,
+ wfMsgHtml( 'nextpage', htmlspecialchars( $t->getText() ) ), $q );
+ $out2 .= ' | ' . $nextLink;
+ }
+ $out2 .= "</td></tr></table><hr />";
}
- $wgOut->addHTML( '</p>' );
- }
+ $wgOut->addHTML( $out2 . $out );
+ if( isset($prevLink) or 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>' );
-}
+ }
-/**
- * @param int $ns the namespace of the article
- * @param string $text the name of the article
- * @return array( int namespace, string dbkey, string pagename ) or NULL on error
- * @static (sort of)
- * @access private
- */
-function getNamespaceKeyAndText ($ns, $text) {
- if ( $text == '' )
- return array( $ns, '', '' ); # shortcut for common case
-
- $t = Title::makeTitleSafe($ns, $text);
- if ( $t && $t->isLocal() ) {
- return array( $t->getNamespace(), $t->getDBkey(), $t->getText() );
- } else if ( $t ) {
- return NULL;
}
- # try again, in case the problem was an empty pagename
- $text = preg_replace('/(#|$)/', 'X$1', $text);
- $t = Title::makeTitleSafe($ns, $text);
- if ( $t && $t->isLocal() ) {
- return array( $t->getNamespace(), '', '' );
- } else {
- return NULL;
+ /**
+ * @param int $ns the namespace of the article
+ * @param string $text the name of the article
+ * @return array( int namespace, string dbkey, string pagename ) or NULL on error
+ * @static (sort of)
+ * @access private
+ */
+ function getNamespaceKeyAndText($ns, $text) {
+ if ( $text == '' )
+ return array( $ns, '', '' ); # shortcut for common case
+
+ $t = Title::makeTitleSafe($ns, $text);
+ if ( $t && $t->isLocal() ) {
+ return array( $t->getNamespace(), $t->getDBkey(), $t->getText() );
+ } else if ( $t ) {
+ return NULL;
+ }
+
+ # try again, in case the problem was an empty pagename
+ $text = preg_replace('/(#|$)/', 'X$1', $text);
+ $t = Title::makeTitleSafe($ns, $text);
+ if ( $t && $t->isLocal() ) {
+ return array( $t->getNamespace(), '', '' );
+ } else {
+ return NULL;
+ }
}
}
-}
diff --git a/includes/specials/SpecialBlockip.php b/includes/specials/SpecialBlockip.php
index 52829d92..4d82997f 100644
--- a/includes/specials/SpecialBlockip.php
+++ b/includes/specials/SpecialBlockip.php
@@ -47,7 +47,7 @@ class IPBlockForm {
# var $BlockEmail;
function IPBlockForm( $par ) {
- global $wgRequest, $wgUser;
+ global $wgRequest, $wgUser, $wgBlockAllowsUTEdit;
$this->BlockAddress = $wgRequest->getVal( 'wpBlockAddress', $wgRequest->getVal( 'ip', $par ) );
$this->BlockAddress = strtr( $this->BlockAddress, '_', ' ' );
@@ -66,6 +66,8 @@ class IPBlockForm {
$this->BlockWatchUser = $wgRequest->getBool( 'wpWatchUser', false );
# Re-check user's rights to hide names, very serious, defaults to 0
$this->BlockHideName = ( $wgRequest->getBool( 'wpHideName', 0 ) && $wgUser->isAllowed( 'hideuser' ) ) ? 1 : 0;
+ $this->BlockAllowUsertalk = ( $wgRequest->getBool( 'wpAllowUsertalk', $byDefault ) && $wgBlockAllowsUTEdit );
+ $this->BlockReblock = $wgRequest->getBool( 'wpChangeBlock', false );
}
function showForm( $err ) {
@@ -85,10 +87,26 @@ class IPBlockForm {
$mIpbreason = Xml::label( wfMsg( 'ipbotherreason' ), 'mw-bi-reason' );
$titleObj = SpecialPage::getTitleFor( 'Blockip' );
-
- if ( "" != $err ) {
+ $user = User::newFromName( $this->BlockAddress );
+
+ $alreadyBlocked = false;
+ if ( $err && $err[0] != 'ipb_already_blocked' ) {
+ $key = array_shift($err);
+ $msg = wfMsgReal($key, $err);
$wgOut->setSubtitle( wfMsgHtml( 'formerror' ) );
- $wgOut->addHTML( Xml::tags( 'p', array( 'class' => 'error' ), $err ) );
+ $wgOut->addHTML( Xml::tags( 'p', array( 'class' => 'error' ), $msg ) );
+ } elseif ( $this->BlockAddress ) {
+ $userId = 0;
+ if ( is_object( $user ) )
+ $userId = $user->getId();
+ $currentBlock = Block::newFromDB( $this->BlockAddress, $userId );
+ if ( !is_null($currentBlock) && !$currentBlock->mAuto && # The block exists and isn't an autoblock
+ ( $currentBlock->mRangeStart == $currentBlock->mRangeEnd || # The block isn't a rangeblock
+ # or if it is, the range is what we're about to block
+ ( $currentBlock->mAddress == $this->BlockAddress ) ) ) {
+ $wgOut->addWikiMsg( 'ipb-needreblock', $this->BlockAddress );
+ $alreadyBlocked = true;
+ }
}
$scBlockExpiryOptions = wfMsgForContent( 'ipboptions' );
@@ -108,7 +126,7 @@ class IPBlockForm {
$reasonDropDown = Xml::listDropDown( 'wpBlockReasonList',
wfMsgForContent( 'ipbreason-dropdown' ),
- wfMsgForContent( 'ipbreasonotherlist' ), '', 'wpBlockDropDown', 4 );
+ wfMsgForContent( 'ipbreasonotherlist' ), $this->BlockReasonList, 'wpBlockDropDown', 4 );
global $wgStylePath, $wgStyleVersion;
$wgOut->addHTML(
@@ -201,7 +219,7 @@ class IPBlockForm {
</tr>"
);
- global $wgSysopEmailBans;
+ global $wgSysopEmailBans, $wgBlockAllowsUTEdit;
if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) {
$wgOut->addHTML("
<tr id='wpEnableEmailBan'>
@@ -240,25 +258,37 @@ class IPBlockForm {
</td>
</tr>"
);
+ if( $wgBlockAllowsUTEdit ){
+ $wgOut->addHTML("
+ <tr id='wpAllowUsertalkRow'>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'ipballowusertalk' ),
+ 'wpAllowUsertalk', 'wpAllowUsertalk', $this->BlockAllowUsertalk,
+ array( 'tabindex' => '12' ) ) . "
+ </td>
+ </tr>"
+ );
+ }
$wgOut->addHTML("
<tr>
<td style='padding-top: 1em'>&nbsp;</td>
<td class='mw-submit' style='padding-top: 1em'>" .
- Xml::submitButton( wfMsg( 'ipbsubmit' ),
- array( 'name' => 'wpBlock', 'tabindex' => '12' ) ) . "
+ Xml::submitButton( wfMsg( $alreadyBlocked ? 'ipb-change-block' : 'ipbsubmit' ),
+ array( 'name' => 'wpBlock', 'tabindex' => '13', 'accesskey' => 's' ) ) . "
</td>
</tr>" .
Xml::closeElement( 'table' ) .
Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
+ ( $alreadyBlocked ? Xml::hidden( 'wpChangeBlock', 1 ) : "" ) .
Xml::closeElement( 'fieldset' ) .
Xml::closeElement( 'form' ) .
Xml::tags( 'script', array( 'type' => 'text/javascript' ), 'updateBlockOptions()' ) . "\n"
);
- $wgOut->addHtml( $this->getConvenienceLinks() );
+ $wgOut->addHTML( $this->getConvenienceLinks() );
- $user = User::newFromName( $this->BlockAddress );
if( is_object( $user ) ) {
$this->showLogFragment( $wgOut, $user->getUserPage() );
} elseif( preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', $this->BlockAddress ) ) {
@@ -273,9 +303,8 @@ class IPBlockForm {
* $userID and $expiry will be filled accordingly
* @return array(message key, arguments) on failure, empty array on success
*/
- function doBlock(&$userId = null, &$expiry = null)
- {
- global $wgUser, $wgSysopUserBans, $wgSysopRangeBans;
+ function doBlock( &$userId = null, &$expiry = null ) {
+ global $wgUser, $wgSysopUserBans, $wgSysopRangeBans, $wgBlockAllowsUTEdit;
$userId = 0;
# Expand valid IPv6 addresses, usernames are left as is
@@ -327,8 +356,12 @@ class IPBlockForm {
}
}
+ if ( $wgUser->isBlocked() && ( $wgUser->getId() !== $userId ) ) {
+ return array( 'cant-block-while-blocked' );
+ }
+
$reasonstr = $this->BlockReasonList;
- if ( $reasonstr != 'other' && $this->BlockReason != '') {
+ if ( $reasonstr != 'other' && $this->BlockReason != '' ) {
// Entry from drop down menu + additional comment
$reasonstr .= ': ' . $this->BlockReason;
} elseif ( $reasonstr == 'other' ) {
@@ -339,7 +372,7 @@ class IPBlockForm {
if( $expirestr == 'other' )
$expirestr = $this->BlockOther;
- if ((strlen($expirestr) == 0) || (strlen($expirestr) > 50)) {
+ if ( ( strlen( $expirestr ) == 0) || ( strlen( $expirestr ) > 50) ) {
return array('ipb_expiry_invalid');
}
@@ -358,14 +391,27 @@ class IPBlockForm {
$block = new Block( $this->BlockAddress, $userId, $wgUser->getId(),
$reasonstr, wfTimestampNow(), 0, $expiry, $this->BlockAnonOnly,
$this->BlockCreateAccount, $this->BlockEnableAutoblock, $this->BlockHideName,
- $this->BlockEmail );
+ $this->BlockEmail, isset( $this->BlockAllowUsertalk ) ? $this->BlockAllowUsertalk : $wgBlockAllowsUTEdit );
if ( wfRunHooks('BlockIp', array(&$block, &$wgUser)) ) {
if ( !$block->insert() ) {
- return array('ipb_already_blocked', htmlspecialchars($this->BlockAddress));
+ if ( !$this->BlockReblock ) {
+ return array( 'ipb_already_blocked' );
+ } else {
+ # This returns direct blocks before autoblocks/rangeblocks, since we should
+ # be sure the user is blocked by now it should work for our purposes
+ $currentBlock = Block::newFromDB( $this->BlockAddress, $userId );
+ if( $block->equals( $currentBlock ) ) {
+ return array( 'ipb_already_blocked' );
+ }
+ $currentBlock->delete();
+ $block->insert();
+ $log_action = 'reblock';
+ }
+ } else {
+ $log_action = 'block';
}
-
wfRunHooks('BlockIpComplete', array($block, $wgUser));
if ( $this->BlockWatchUser ) {
@@ -380,7 +426,7 @@ class IPBlockForm {
# Make log entry, if the name is hidden, put it in the oversight log
$log_type = ($this->BlockHideName) ? 'suppress' : 'block';
$log = new LogPage( $log_type );
- $log->addEntry( 'block', Title::makeTitle( NS_USER, $this->BlockAddress ),
+ $log->addEntry( $log_action, Title::makeTitle( NS_USER, $this->BlockAddress ),
$reasonstr, $logParams );
# Report to the user
@@ -404,8 +450,7 @@ class IPBlockForm {
urlencode( $this->BlockAddress ) ) );
return;
}
- $key = array_shift($retval);
- $this->showForm(wfMsgReal($key, $retval));
+ $this->showForm( $retval );
}
function showSuccess() {
@@ -414,12 +459,22 @@ class IPBlockForm {
$wgOut->setPagetitle( wfMsg( 'blockip' ) );
$wgOut->setSubtitle( wfMsg( 'blockipsuccesssub' ) );
$text = wfMsgExt( 'blockipsuccesstext', array( 'parse' ), $this->BlockAddress );
- $wgOut->addHtml( $text );
+ $wgOut->addHTML( $text );
}
function showLogFragment( $out, $title ) {
- $out->addHtml( Xml::element( 'h2', NULL, LogPage::logName( 'block' ) ) );
- LogEventsList::showLogExtract( $out, 'block', $title->getPrefixedText() );
+ global $wgUser;
+ $out->addHTML( Xml::element( 'h2', NULL, LogPage::logName( 'block' ) ) );
+ $count = LogEventsList::showLogExtract( $out, 'block', $title->getPrefixedText(), '', 10 );
+ if($count > 10){
+ $out->addHTML( $wgUser->getSkin()->link(
+ SpecialPage::getTitleFor( 'Log' ),
+ wfMsgHtml( 'blocklog-fulllog' ),
+ array(),
+ array(
+ 'type' => 'block',
+ 'page' => $title->getPrefixedText() ) ) );
+ }
}
/**
@@ -429,6 +484,7 @@ class IPBlockForm {
* @return array
*/
private function blockLogFlags() {
+ global $wgBlockAllowsUTEdit;
$flags = array();
if( $this->BlockAnonOnly && IP::isIPAddress( $this->BlockAddress ) )
// when blocking a user the option 'anononly' is not available/has no effect -> do not write this into log
@@ -439,6 +495,8 @@ class IPBlockForm {
$flags[] = 'noautoblock';
if ( $this->BlockEmail )
$flags[] = 'noemail';
+ if ( !$this->BlockAllowUsertalk && $wgBlockAllowsUTEdit )
+ $flags[] = 'nousertalk';
return implode( ',', $flags );
}
@@ -450,11 +508,25 @@ class IPBlockForm {
private function getConvenienceLinks() {
global $wgUser;
$skin = $wgUser->getSkin();
- $links[] = $skin->makeLink ( 'MediaWiki:Ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) );
+ if( $this->BlockAddress )
+ $links[] = $this->getContribsLink( $skin );
$links[] = $this->getUnblockLink( $skin );
$links[] = $this->getBlockListLink( $skin );
+ $links[] = $skin->makeLink ( 'MediaWiki:Ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) );
return '<p class="mw-ipb-conveniencelinks">' . implode( ' | ', $links ) . '</p>';
}
+
+ /**
+ * Build a convenient link to a user or IP's contribs
+ * form
+ *
+ * @param $skin Skin to use
+ * @return string
+ */
+ private function getContribsLink( $skin ) {
+ $contribsPage = SpecialPage::getTitleFor( 'Contributions', $this->BlockAddress );
+ return $skin->link( $contribsPage, wfMsgHtml( 'ipb-blocklist-contribs', $this->BlockAddress ) );
+ }
/**
* Build a convenient link to unblock the given username or IP
@@ -491,4 +563,77 @@ class IPBlockForm {
return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist' ) );
}
}
+
+ /**
+ * Block a list of selected users
+ * @param array $users
+ * @param string $reason
+ * @param string $tag replaces user pages
+ * @param string $talkTag replaces user talk pages
+ * @returns array, list of html-safe usernames
+ */
+ public static function doMassUserBlock( $users, $reason = '', $tag = '', $talkTag = '' ) {
+ global $wgUser;
+ $counter = $blockSize = 0;
+ $safeUsers = array();
+ $log = new LogPage( 'block' );
+ foreach( $users as $name ) {
+ # Enforce limits
+ $counter++;
+ $blockSize++;
+ # Lets not go *too* fast
+ if( $blockSize >= 20 ) {
+ $blockSize = 0;
+ wfWaitForSlaves( 5 );
+ }
+ $u = User::newFromName( $name, false );
+ // If user doesn't exist, it ought to be an IP then
+ if( is_null($u) || (!$u->getId() && !IP::isIPAddress( $u->getName() )) ) {
+ continue;
+ }
+ $userTitle = $u->getUserPage();
+ $userTalkTitle = $u->getTalkPage();
+ $userpage = new Article( $userTitle );
+ $usertalk = new Article( $userTalkTitle );
+ $safeUsers[] = '[[' . $userTitle->getPrefixedText() . '|' . $userTitle->getText() . ']]';
+ $expirestr = $u->getId() ? 'indefinite' : '1 week';
+ $expiry = Block::parseExpiryInput( $expirestr );
+ $anonOnly = IP::isIPAddress( $u->getName() ) ? 1 : 0;
+ // Create the block
+ $block = new Block( $u->getName(), // victim
+ $u->getId(), // uid
+ $wgUser->getId(), // blocker
+ $reason, // comment
+ wfTimestampNow(), // block time
+ 0, // auto ?
+ $expiry, // duration
+ $anonOnly, // anononly?
+ 1, // block account creation?
+ 1, // autoblocking?
+ 0, // suppress name?
+ 0 // block from sending email?
+ );
+ $oldblock = Block::newFromDB( $u->getName(), $u->getId() );
+ if( !$oldblock ) {
+ $block->insert();
+ # Prepare log parameters
+ $logParams = array();
+ $logParams[] = $expirestr;
+ if( $anonOnly ) {
+ $logParams[] = 'anononly';
+ }
+ $logParams[] = 'nocreate';
+ # Add log entry
+ $log->addEntry( 'block', $userTitle, $reason, $logParams );
+ }
+ # Tag userpage! (check length to avoid mistakes)
+ if( strlen($tag) > 2 ) {
+ $userpage->doEdit( $tag, $reason, EDIT_MINOR );
+ }
+ if( strlen($talkTag) > 2 ) {
+ $usertalk->doEdit( $talkTag, $reason, EDIT_MINOR );
+ }
+ }
+ return $safeUsers;
+ }
}
diff --git a/includes/specials/SpecialBooksources.php b/includes/specials/SpecialBooksources.php
index 0690c5c0..12b119d8 100644
--- a/includes/specials/SpecialBooksources.php
+++ b/includes/specials/SpecialBooksources.php
@@ -30,20 +30,62 @@ class SpecialBookSources extends SpecialPage {
public function execute( $isbn ) {
global $wgOut, $wgRequest;
$this->setHeaders();
- $this->isbn = $this->cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) );
+ $this->isbn = self::cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) );
$wgOut->addWikiMsg( 'booksources-summary' );
- $wgOut->addHtml( $this->makeForm() );
- if( strlen( $this->isbn ) > 0 )
+ $wgOut->addHTML( $this->makeForm() );
+ if( strlen( $this->isbn ) > 0 ) {
+ if( !$this->isValidIsbn( $this->isbn ) ) {
+ $wgOut->wrapWikiMsg( '<div class="error">$1</div>', 'booksources-invalid-isbn' );
+ }
$this->showList();
+ }
}
/**
+ * Returns whether a given ISBN (10 or 13) is valid. True indicates validity.
+ * @param isbn ISBN passed for check
+ */
+ public static function isValidISBN( $isbn ) {
+ $isbn = self::cleanIsbn( $isbn );
+ $sum = 0;
+ $check = -1;
+ if( strlen( $isbn ) == 13 ) {
+ for( $i = 0; $i < 12; $i++ ) {
+ if($i % 2 == 0) {
+ $sum += $isbn{$i};
+ } else {
+ $sum += 3 * $isbn{$i};
+ }
+ }
+
+ $check = (10 - ($sum % 10)) % 10;
+ if ($check == $isbn{12}) {
+ return true;
+ }
+ } elseif( strlen( $isbn ) == 10 ) {
+ for($i = 0; $i < 9; $i++) {
+ $sum += $isbn{$i} * ($i + 1);
+ }
+
+ $check = $sum % 11;
+ if($check == 10) {
+ $check = "X";
+ }
+ if($check == $isbn{9}) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
* Trim ISBN and remove characters which aren't required
*
* @param $isbn Unclean ISBN
* @return string
*/
- private function cleanIsbn( $isbn ) {
+ private static function cleanIsbn( $isbn ) {
return trim( preg_replace( '![^0-9X]!', '', $isbn ) );
}
@@ -88,11 +130,11 @@ class SpecialBookSources extends SpecialPage {
# Fall back to the defaults given in the language file
$wgOut->addWikiMsg( 'booksources-text' );
- $wgOut->addHtml( '<ul>' );
+ $wgOut->addHTML( '<ul>' );
$items = $wgContLang->getBookstoreList();
foreach( $items as $label => $url )
- $wgOut->addHtml( $this->makeListItem( $label, $url ) );
- $wgOut->addHtml( '</ul>' );
+ $wgOut->addHTML( $this->makeListItem( $label, $url ) );
+ $wgOut->addHTML( '</ul>' );
return true;
}
diff --git a/includes/specials/SpecialCategories.php b/includes/specials/SpecialCategories.php
index 951c2228..8c2ae2b6 100644
--- a/includes/specials/SpecialCategories.php
+++ b/includes/specials/SpecialCategories.php
@@ -14,11 +14,13 @@ function wfSpecialCategories( $par=null ) {
}
$cap = new CategoryPager( $from );
$wgOut->addHTML(
+ XML::openElement( 'div', array('class' => 'mw-spcontent') ) .
wfMsgExt( 'categoriespagetext', array( 'parse' ) ) .
$cap->getStartForm( $from ) .
$cap->getNavigationBar() .
'<ul>' . $cap->getBody() . '</ul>' .
- $cap->getNavigationBar()
+ $cap->getNavigationBar() .
+ XML::closeElement( 'div' )
);
}
diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php
index 9075fb95..e19aa99b 100644
--- a/includes/specials/SpecialConfirmemail.php
+++ b/includes/specials/SpecialConfirmemail.php
@@ -36,8 +36,9 @@ class EmailConfirmation extends UnlistedSpecialPage {
$title = SpecialPage::getTitleFor( 'Userlogin' );
$self = SpecialPage::getTitleFor( 'Confirmemail' );
$skin = $wgUser->getSkin();
- $llink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $self->getPrefixedUrl() );
- $wgOut->addHtml( wfMsgWikiHtml( 'confirmemail_needlogin', $llink ) );
+ $llink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'loginreqlink' ),
+ 'returnto=' . $self->getPrefixedUrl() );
+ $wgOut->addHTML( wfMsgWikiHtml( 'confirmemail_needlogin', $llink ) );
}
} else {
$this->attemptConfirm( $code );
@@ -58,19 +59,24 @@ class EmailConfirmation extends UnlistedSpecialPage {
}
} else {
if( $wgUser->isEmailConfirmed() ) {
+ // date and time are separate parameters to facilitate localisation.
+ // $time is kept for backward compat reasons.
+ // 'emailauthenticated' is also used in SpecialPreferences.php
$time = $wgLang->timeAndDate( $wgUser->mEmailAuthenticated, true );
- $wgOut->addWikiMsg( 'emailauthenticated', $time );
+ $d = $wgLang->date( $wgUser->mEmailAuthenticated, true );
+ $t = $wgLang->time( $wgUser->mEmailAuthenticated, true );
+ $wgOut->addWikiMsg( 'emailauthenticated', $time, $d, $t );
}
if( $wgUser->isEmailConfirmationPending() ) {
$wgOut->addWikiMsg( 'confirmemail_pending' );
}
$wgOut->addWikiMsg( 'confirmemail_text' );
$self = SpecialPage::getTitleFor( 'Confirmemail' );
- $form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) );
- $form .= wfHidden( 'token', $wgUser->editToken() );
- $form .= wfSubmitButton( wfMsgHtml( 'confirmemail_send' ) );
- $form .= wfCloseElement( 'form' );
- $wgOut->addHtml( $form );
+ $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) );
+ $form .= Xml::hidden( 'token', $wgUser->editToken() );
+ $form .= Xml::submitButton( wfMsgHtml( 'confirmemail_send' ) );
+ $form .= Xml::closeElement( 'form' );
+ $wgOut->addHTML( $form );
}
}
diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php
index 4a131f15..3d8c18dd 100644
--- a/includes/specials/SpecialContributions.php
+++ b/includes/specials/SpecialContributions.php
@@ -4,6 +4,363 @@
* @file
* @ingroup SpecialPage
*/
+
+class SpecialContributions extends SpecialPage {
+
+ public function __construct() {
+ parent::__construct( 'Contributions' );
+ }
+
+ public function execute( $par ) {
+ global $wgUser, $wgOut, $wgLang, $wgRequest;
+
+ $this->setHeaders();
+ $this->outputHeader();
+
+ $this->opts = array();
+
+ if( $par == 'newbies' ) {
+ $target = 'newbies';
+ $this->opts['contribs'] = 'newbie';
+ } elseif( isset( $par ) ) {
+ $target = $par;
+ } else {
+ $target = $wgRequest->getVal( 'target' );
+ }
+
+ // check for radiobox
+ if( $wgRequest->getVal( 'contribs' ) == 'newbie' ) {
+ $target = 'newbies';
+ $this->opts['contribs'] = 'newbie';
+ }
+
+ if( !strlen( $target ) ) {
+ $wgOut->addHTML( $this->getForm( '' ) );
+ return;
+ }
+
+ $this->opts['limit'] = $wgRequest->getInt( 'limit', 50 );
+ $this->opts['target'] = $target;
+
+ $nt = Title::makeTitleSafe( NS_USER, $target );
+ if( !$nt ) {
+ $wgOut->addHTML( $this->getForm( '' ) );
+ return;
+ }
+ $id = User::idFromName( $nt->getText() );
+
+ if( $target != 'newbies' ) {
+ $target = $nt->getText();
+ $wgOut->setSubtitle( $this->contributionsSub( $nt, $id ) );
+ $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'contributions-title', $target ) ) );
+ } else {
+ $wgOut->setSubtitle( wfMsgHtml( 'sp-contributions-newbies-sub') );
+ $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'sp-contributions-newbies-title' ) ) );
+ }
+
+ if( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) {
+ $this->opts['namespace'] = intval( $ns );
+ } else {
+ $this->opts['namespace'] = '';
+ }
+
+ // Allows reverts to have the bot flag in recent changes. It is just here to
+ // be passed in the form at the top of the page
+ if( $wgUser->isAllowed( 'markbotedits' ) && $wgRequest->getBool( 'bot' ) ) {
+ $this->opts['bot'] = '1';
+ }
+
+ $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev';
+ # Offset overrides year/month selection
+ if( ( $month = $wgRequest->getIntOrNull( 'month' ) ) !== null && $month !== -1 ) {
+ $this->opts['month'] = intval( $month );
+ } else {
+ $this->opts['month'] = '';
+ }
+ if( ( $year = $wgRequest->getIntOrNull( 'year' ) ) !== null ) {
+ $this->opts['year'] = intval( $year );
+ } else if( $this->opts['month'] ) {
+ $thisMonth = intval( gmdate( 'n' ) );
+ $thisYear = intval( gmdate( 'Y' ) );
+ if( intval( $this->opts['month'] ) > $thisMonth ) {
+ $thisYear--;
+ }
+ $this->opts['year'] = $thisYear;
+ } else {
+ $this->opts['year'] = '';
+ }
+
+ if( $skip ) {
+ $this->opts['year'] = '';
+ $this->opts['month'] = '';
+ }
+
+ // Add RSS/atom links
+ $this->setSyndicated();
+ $feedType = $wgRequest->getVal( 'feed' );
+ if( $feedType ) {
+ return $this->feed( $feedType );
+ }
+
+ wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id );
+
+ $wgOut->addHTML( $this->getForm( $this->opts ) );
+
+ $pager = new ContribsPager( $target, $this->opts['namespace'], $this->opts['year'], $this->opts['month'] );
+ if( !$pager->getNumRows() ) {
+ $wgOut->addWikiMsg( '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() .
+ '<p>' . $pager->getNavigationBar() . '</p>'
+ );
+
+ # If there were contributions, and it was a valid user or IP, show
+ # the appropriate "footer" message - WHOIS tools, etc.
+ if( $target != 'newbies' ) {
+ $message = IP::isIPAddress( $target ) ?
+ 'sp-contributions-footer-anon' : 'sp-contributions-footer';
+
+ $text = wfMsgNoTrans( $message, $target );
+ if( !wfEmptyMsg( $message, $text ) && $text != '-' ) {
+ $wgOut->addHTML( '<div class="mw-contributions-footer">' );
+ $wgOut->addWikiText( $text );
+ $wgOut->addHTML( '</div>' );
+ }
+ }
+ }
+
+ protected function setSyndicated() {
+ global $wgOut;
+ $queryParams = array(
+ 'namespace' => $this->opts['namespace'],
+ 'target' => $this->opts['target']
+ );
+ $wgOut->setSyndicated( true );
+ $wgOut->setFeedAppendQuery( wfArrayToCGI( $queryParams ) );
+ }
+
+ /**
+ * Generates the subheading with links
+ * @param Title $nt Title object for the target
+ * @param integer $id User ID for the target
+ * @return String: appropriately-escaped HTML to be output literally
+ */
+ protected function contributionsSub( $nt, $id ) {
+ global $wgSysopUserBans, $wgLang, $wgUser;
+
+ $sk = $wgUser->getSkin();
+
+ if( 0 == $id ) {
+ $user = $nt->getText();
+ } else {
+ $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) );
+ }
+ $talk = $nt->getTalkPage();
+ if( $talk ) {
+ # Talk page link
+ $tools[] = $sk->makeLinkObj( $talk, wfMsgHtml( 'talkpagelinktext' ) );
+ if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && IP::isIPAddress( $nt->getText() ) ) ) {
+ # Block link
+ if( $wgUser->isAllowed( 'block' ) )
+ $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip',
+ $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) );
+ # Block log link
+ $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ),
+ wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() );
+ }
+ # Other logs link
+ $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'log' ),
+ 'user=' . $nt->getPartialUrl() );
+
+ # Add link to deleted user contributions for priviledged users
+ if( $wgUser->isAllowed( 'deletedhistory' ) ) {
+ $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'DeletedContributions',
+ $nt->getDBkey() ), wfMsgHtml( 'deletedcontributions' ) );
+ }
+
+ wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) );
+
+ $links = implode( ' | ', $tools );
+ }
+
+ // Old message 'contribsub' had one parameter, but that doesn't work for
+ // languages that want to put the "for" bit right after $user but before
+ // $links. If 'contribsub' is around, use it for reverse compatibility,
+ // otherwise use 'contribsub2'.
+ if( wfEmptyMsg( 'contribsub', wfMsg( 'contribsub' ) ) ) {
+ return wfMsgHtml( 'contribsub2', $user, $links );
+ } else {
+ return wfMsgHtml( 'contribsub', "$user ($links)" );
+ }
+ }
+
+ /**
+ * Generates the namespace selector form with hidden attributes.
+ * @param $this->opts Array: the options to be included.
+ */
+ protected function getForm() {
+ global $wgScript, $wgTitle;
+
+ $this->opts['title'] = $wgTitle->getPrefixedText();
+ if( !isset( $this->opts['target'] ) ) {
+ $this->opts['target'] = '';
+ } else {
+ $this->opts['target'] = str_replace( '_' , ' ' , $this->opts['target'] );
+ }
+
+ if( !isset( $this->opts['namespace'] ) ) {
+ $this->opts['namespace'] = '';
+ }
+
+ if( !isset( $this->opts['contribs'] ) ) {
+ $this->opts['contribs'] = 'user';
+ }
+
+ if( !isset( $this->opts['year'] ) ) {
+ $this->opts['year'] = '';
+ }
+
+ if( !isset( $this->opts['month'] ) ) {
+ $this->opts['month'] = '';
+ }
+
+ if( $this->opts['contribs'] == 'newbie' ) {
+ $this->opts['target'] = '';
+ }
+
+ $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
+
+ foreach ( $this->opts as $name => $value ) {
+ if( in_array( $name, array( 'namespace', 'target', 'contribs', 'year', 'month' ) ) ) {
+ continue;
+ }
+ $f .= "\t" . Xml::hidden( $name, $value ) . "\n";
+ }
+
+ $f .= '<fieldset>' .
+ Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) .
+ Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parseinline' ) ),
+ 'contribs', 'newbie' , 'newbie', $this->opts['contribs'] == 'newbie' ? true : false ) . '<br />' .
+ Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parseinline' ) ),
+ 'contribs' , 'user', 'user', $this->opts['contribs'] == 'user' ? true : false ) . ' ' .
+ Xml::input( 'target', 20, $this->opts['target']) . ' '.
+ '<span style="white-space: nowrap">' .
+ Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' .
+ Xml::namespaceSelector( $this->opts['namespace'], '' ) .
+ '</span>' .
+ Xml::openElement( 'p' ) .
+ '<span style="white-space: nowrap">' .
+ Xml::label( wfMsg( 'year' ), 'year' ) . ' '.
+ Xml::input( 'year', 4, $this->opts['year'], array('id' => 'year', 'maxlength' => 4) ) .
+ '</span>' .
+ ' '.
+ '<span style="white-space: nowrap">' .
+ Xml::label( wfMsg( 'month' ), 'month' ) . ' '.
+ Xml::monthSelector( $this->opts['month'], -1 ) . ' '.
+ '</span>' .
+ Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) .
+ 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;
+ }
+
+ /**
+ * Output a subscription feed listing recent edits to this page.
+ * @param string $type
+ */
+ protected function feed( $type ) {
+ global $wgRequest, $wgFeed, $wgFeedClasses, $wgFeedLimit;
+
+ if( !$wgFeed ) {
+ global $wgOut;
+ $wgOut->addWikiMsg( 'feed-unavailable' );
+ return;
+ }
+
+ if( !isset( $wgFeedClasses[$type] ) ) {
+ global $wgOut;
+ $wgOut->addWikiMsg( 'feed-invalid' );
+ return;
+ }
+
+ $feed = new $wgFeedClasses[$type](
+ $this->feedTitle(),
+ wfMsgExt( 'tagline', 'parsemag' ),
+ $this->getTitle()->getFullUrl() );
+
+ // Already valid title
+ $nt = Title::makeTitleSafe( NS_USER, $this->opts['target'] );
+ $target = $this->opts['target'] == 'newbies' ? 'newbies' : $nt->getText();
+
+ $pager = new ContribsPager( $target, $this->opts['namespace'],
+ $this->opts['year'], $this->opts['month'] );
+
+ $pager->mLimit = min( $this->opts['limit'], $wgFeedLimit );
+
+ $feed->outHeader();
+ if( $pager->getNumRows() > 0 ) {
+ while( $row = $pager->mResult->fetchObject() ) {
+ $feed->outItem( $this->feedItem( $row ) );
+ }
+ }
+ $feed->outFooter();
+ }
+
+ protected function feedTitle() {
+ global $wgContLanguageCode, $wgSitename;
+ $page = SpecialPage::getPage( 'Contributions' );
+ $desc = $page->getDescription();
+ return "$wgSitename - $desc [$wgContLanguageCode]";
+ }
+
+ protected function feedItem( $row ) {
+ $title = Title::MakeTitle( intval( $row->page_namespace ), $row->page_title );
+ if( $title ) {
+ $date = $row->rev_timestamp;
+ $comments = $title->getTalkPage()->getFullURL();
+ $revision = Revision::newFromTitle( $title, $row->rev_id );
+
+ return new FeedItem(
+ $title->getPrefixedText(),
+ $this->feedItemDesc( $revision ),
+ $title->getFullURL(),
+ $date,
+ $this->feedItemAuthor( $revision ),
+ $comments
+ );
+ } else {
+ return NULL;
+ }
+ }
+
+ protected function feedItemAuthor( $revision ) {
+ return $revision->getUserText();
+ }
+
+ protected function feedItemDesc( $revision ) {
+ if( $revision ) {
+ return '<p>' . htmlspecialchars( $revision->getUserText() ) . ': ' .
+ htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) .
+ "</p>\n<hr />\n<div>" .
+ nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
+ }
+ return '';
+ }
+}
/**
* Pager for Special:Contributions
@@ -12,7 +369,7 @@
class ContribsPager extends ReverseChronologicalPager {
public $mDefaultDirection = true;
var $messages, $target;
- var $namespace = '', $year = '', $month = '', $mDb;
+ var $namespace = '', $mDb;
function __construct( $target, $namespace = false, $year = false, $month = false ) {
parent::__construct();
@@ -22,12 +379,7 @@ class ContribsPager extends ReverseChronologicalPager {
$this->target = $target;
$this->namespace = $namespace;
- $year = intval($year);
- $month = intval($month);
-
- $this->year = $year > 0 ? $year : false;
- $this->month = ($month > 0 && $month < 13) ? $month : false;
- $this->getDateCond();
+ $this->getDateCond( $year, $month );
$this->mDb = wfGetDB( DB_SLAVE, 'contributions' );
}
@@ -35,23 +387,22 @@ class ContribsPager extends ReverseChronologicalPager {
function getDefaultQuery() {
$query = parent::getDefaultQuery();
$query['target'] = $this->target;
- $query['month'] = $this->month;
- $query['year'] = $this->year;
return $query;
}
function getQueryInfo() {
- list( $index, $userCond ) = $this->getUserCond();
+ list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond();
$conds = array_merge( array('page_id=rev_page'), $userCond, $this->getNamespaceCond() );
$queryInfo = array(
- 'tables' => array( 'page', 'revision' ),
+ 'tables' => $tables,
'fields' => array(
'page_namespace', 'page_title', 'page_is_new', 'page_latest', 'rev_id', 'rev_page',
'rev_text_id', 'rev_timestamp', 'rev_comment', 'rev_minor_edit', 'rev_user',
'rev_user_text', 'rev_parent_id', 'rev_deleted'
),
'conds' => $conds,
- 'options' => array( 'USE INDEX' => array('revision' => $index) )
+ 'options' => array( 'USE INDEX' => array('revision' => $index) ),
+ 'join_conds' => $join_cond
);
wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) );
return $queryInfo;
@@ -59,73 +410,31 @@ class ContribsPager extends ReverseChronologicalPager {
function getUserCond() {
$condition = array();
-
- if ( $this->target == 'newbies' ) {
+ $join_conds = array();
+ if( $this->target == 'newbies' ) {
+ $tables = array( 'user_groups', 'page', 'revision' );
$max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ );
$condition[] = 'rev_user >' . (int)($max - $max / 100);
+ $condition[] = 'ug_group IS NULL';
$index = 'user_timestamp';
+ # FIXME: other groups may have 'bot' rights
+ $join_conds['user_groups'] = array( 'LEFT JOIN', "ug_user = rev_user AND ug_group = 'bot'" );
} else {
+ $tables = array( 'page', 'revision' );
$condition['rev_user_text'] = $this->target;
$index = 'usertext_timestamp';
}
- return array( $index, $condition );
+ return array( $tables, $index, $condition, $join_conds );
}
function getNamespaceCond() {
- if ( $this->namespace !== '' ) {
+ if( $this->namespace !== '' ) {
return array( 'page_namespace' => (int)$this->namespace );
} else {
return array();
}
}
- function getDateCond() {
- // Given an optional year and month, we need to generate a timestamp
- // to use as "WHERE rev_timestamp <= result"
- // Examples: year = 2006 equals < 20070101 (+000000)
- // year=2005, month=1 equals < 20050201
- // year=2005, month=12 equals < 20060101
-
- if (!$this->year && !$this->month)
- return;
-
- if ( $this->year ) {
- $year = $this->year;
- }
- else {
- // If no year given, assume the current one
- $year = gmdate( 'Y' );
- // If this month hasn't happened yet this year, go back to last year's month
- if( $this->month > gmdate( 'n' ) ) {
- $year--;
- }
- }
-
- if ( $this->month ) {
- $month = $this->month + 1;
- // For December, we want January 1 of the next year
- if ($month > 12) {
- $month = 1;
- $year++;
- }
- }
- else {
- // No month implies we want up to the end of the year in question
- $month = 1;
- $year++;
- }
-
- if ($year > 2032)
- $year = 2032;
- $ymd = (int)sprintf( "%04d%02d01", $year, $month );
-
- // Y2K38 bug
- if ($ymd > 20320101)
- $ymd = 20320101;
-
- $this->mOffset = $this->mDb->timestamp( "${ymd}000000" );
- }
-
function getIndexField() {
return 'rev_timestamp';
}
@@ -167,8 +476,7 @@ class ContribsPager extends ReverseChronologicalPager {
$difftext .= $this->messages['newarticle'];
}
- if( !$page->getUserPermissionsErrors( 'rollback', $wgUser )
- && !$page->getUserPermissionsErrors( 'edit', $wgUser ) ) {
+ if( $page->userCan( 'rollback') && $page->userCan( 'edit' ) ) {
$topmarktext .= ' '.$sk->generateRollback( $rev );
}
@@ -182,7 +490,8 @@ class ContribsPager extends ReverseChronologicalPager {
$histlink='('.$sk->makeKnownLinkObj( $page, $this->messages['hist'], 'action=history' ) . ')';
$comment = $wgContLang->getDirMark() . $sk->revComment( $rev, false, true );
- $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true );
+ $date = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true );
+ $d = $sk->makeKnownLinkObj( $page, $date, 'oldid='.intval($row->rev_id) );
if( $this->target == 'newbies' ) {
$userlink = ' . . ' . $sk->userLink( $row->rev_user, $row->rev_user_text );
@@ -207,7 +516,7 @@ class ContribsPager extends ReverseChronologicalPager {
$mflag = '';
}
- $ret = "{$d} {$histlink} {$difftext} {$nflag}{$mflag} {$link}{$userlink}{$comment} {$topmarktext}";
+ $ret = "{$d} {$histlink} {$difftext} {$nflag}{$mflag} {$link}{$userlink} {$comment} {$topmarktext}";
if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
$ret .= ' ' . wfMsgHtml( 'deletedrev' );
}
@@ -229,242 +538,3 @@ class ContribsPager extends ReverseChronologicalPager {
}
}
-
-/**
- * Special page "user contributions".
- * Shows a list of the contributions of a user.
- *
- * @return none
- * @param $par String: (optional) user name of the user for which to show the contributions
- */
-function wfSpecialContributions( $par = null ) {
- global $wgUser, $wgOut, $wgLang, $wgRequest;
-
- $options = array();
-
- if ( isset( $par ) && $par == 'newbies' ) {
- $target = 'newbies';
- $options['contribs'] = 'newbie';
- } elseif ( isset( $par ) ) {
- $target = $par;
- } else {
- $target = $wgRequest->getVal( 'target' );
- }
-
- // check for radiobox
- if ( $wgRequest->getVal( 'contribs' ) == 'newbie' ) {
- $target = 'newbies';
- $options['contribs'] = 'newbie';
- }
-
- if ( !strlen( $target ) ) {
- $wgOut->addHTML( contributionsForm( '' ) );
- return;
- }
-
- $options['limit'] = $wgRequest->getInt( 'limit', 50 );
- $options['target'] = $target;
-
- $nt = Title::makeTitleSafe( NS_USER, $target );
- if ( !$nt ) {
- $wgOut->addHTML( contributionsForm( '' ) );
- return;
- }
- $id = User::idFromName( $nt->getText() );
-
- if ( $target != 'newbies' ) {
- $target = $nt->getText();
- $wgOut->setSubtitle( contributionsSub( $nt, $id ) );
- } else {
- $wgOut->setSubtitle( wfMsgHtml( 'sp-contributions-newbies-sub') );
- }
-
- if ( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) {
- $options['namespace'] = intval( $ns );
- } else {
- $options['namespace'] = '';
- }
- if ( $wgUser->isAllowed( 'markbotedit' ) && $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 );
-
- if( $skip ) {
- $options['year'] = '';
- $options['month'] = '';
- }
-
- $wgOut->addHTML( contributionsForm( $options ) );
-
- $pager = new ContribsPager( $target, $options['namespace'], $options['year'], $options['month'] );
- if ( !$pager->getNumRows() ) {
- $wgOut->addWikiMsg( '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() .
- '<p>' . $pager->getNavigationBar() . '</p>' );
-
- # If there were contributions, and it was a valid user or IP, show
- # the appropriate "footer" message - WHOIS tools, etc.
- if( $target != 'newbies' ) {
- $message = IP::isIPAddress( $target )
- ? 'sp-contributions-footer-anon'
- : 'sp-contributions-footer';
-
-
- $text = wfMsgNoTrans( $message, $target );
- if( !wfEmptyMsg( $message, $text ) && $text != '-' ) {
- $wgOut->addHtml( '<div class="mw-contributions-footer">' );
- $wgOut->addWikiText( $text );
- $wgOut->addHtml( '</div>' );
- }
- }
-}
-
-/**
- * Generates the subheading with links
- * @param Title $nt Title object for the target
- * @param integer $id User ID for the target
- * @return String: appropriately-escaped HTML to be output literally
- */
-function contributionsSub( $nt, $id ) {
- global $wgSysopUserBans, $wgLang, $wgUser;
-
- $sk = $wgUser->getSkin();
-
- if ( 0 == $id ) {
- $user = $nt->getText();
- } else {
- $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) );
- }
- $talk = $nt->getTalkPage();
- if( $talk ) {
- # Talk page link
- $tools[] = $sk->makeLinkObj( $talk, wfMsgHtml( 'talkpagelinktext' ) );
- if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && User::isIP( $nt->getText() ) ) ) {
- # Block link
- if( $wgUser->isAllowed( 'block' ) )
- $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) );
- # Block log link
- $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() );
- }
- # Other logs link
- $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() );
-
- wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) );
-
- $links = implode( ' | ', $tools );
- }
-
- // Old message 'contribsub' had one parameter, but that doesn't work for
- // languages that want to put the "for" bit right after $user but before
- // $links. If 'contribsub' is around, use it for reverse compatibility,
- // otherwise use 'contribsub2'.
- if( wfEmptyMsg( 'contribsub', wfMsg( 'contribsub' ) ) ) {
- return wfMsgHtml( 'contribsub2', $user, $links );
- } else {
- return wfMsgHtml( 'contribsub', "$user ($links)" );
- }
-}
-
-/**
- * Generates the namespace selector form with hidden attributes.
- * @param $options Array: the options to be included.
- */
-function contributionsForm( $options ) {
- global $wgScript, $wgTitle, $wgRequest;
-
- $options['title'] = $wgTitle->getPrefixedText();
- if ( !isset( $options['target'] ) ) {
- $options['target'] = '';
- } else {
- $options['target'] = str_replace( '_' , ' ' , $options['target'] );
- }
-
- if ( !isset( $options['namespace'] ) ) {
- $options['namespace'] = '';
- }
-
- 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'] = '';
- }
-
- $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
-
- foreach ( $options as $name => $value ) {
- if ( in_array( $name, array( 'namespace', 'target', 'contribs', 'year', 'month' ) ) ) {
- continue;
- }
- $f .= "\t" . Xml::hidden( $name, $value ) . "\n";
- }
-
- $f .= '<fieldset>' .
- Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) .
- Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parseinline' ) ), 'contribs' , 'newbie' , 'newbie', $options['contribs'] == 'newbie' ? true : false ) . '<br />' .
- Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parseinline' ) ), 'contribs' , 'user', 'user', $options['contribs'] == 'user' ? true : false ) . ' ' .
- Xml::input( 'target', 20, $options['target']) . ' '.
- '<span style="white-space: nowrap">' .
- Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' .
- Xml::namespaceSelector( $options['namespace'], '' ) .
- '</span>' .
- Xml::openElement( 'p' ) .
- '<span style="white-space: nowrap">' .
- Xml::label( wfMsg( 'year' ), 'year' ) . ' '.
- Xml::input( 'year', 4, $options['year'], array('id' => 'year', 'maxlength' => 4) ) .
- '</span>' .
- ' '.
- '<span style="white-space: nowrap">' .
- Xml::label( wfMsg( 'month' ), 'month' ) . ' '.
- Xml::monthSelector( $options['month'], -1 ) . ' '.
- '</span>' .
- Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) .
- 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/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php
new file mode 100644
index 00000000..513d25e2
--- /dev/null
+++ b/includes/specials/SpecialDeletedContributions.php
@@ -0,0 +1,369 @@
+<?php
+/**
+ * Implements Special:DeletedContributions to display archived revisions
+ * @ingroup SpecialPage
+ */
+
+class DeletedContribsPager extends IndexPager {
+ public $mDefaultDirection = true;
+ var $messages, $target;
+ var $namespace = '', $mDb;
+
+ function __construct( $target, $namespace = false ) {
+ parent::__construct();
+ foreach( explode( ' ', 'deletionlog undeletebtn minoreditletter diff' ) as $msg ) {
+ $this->messages[$msg] = wfMsgExt( $msg, array( 'escape') );
+ }
+ $this->target = $target;
+ $this->namespace = $namespace;
+ $this->mDb = wfGetDB( DB_SLAVE, 'contributions' );
+ }
+
+ function getDefaultQuery() {
+ $query = parent::getDefaultQuery();
+ $query['target'] = $this->target;
+ return $query;
+ }
+
+ function getQueryInfo() {
+ list( $index, $userCond ) = $this->getUserCond();
+ $conds = array_merge( $userCond, $this->getNamespaceCond() );
+
+ return array(
+ 'tables' => array( 'archive' ),
+ 'fields' => array(
+ 'ar_rev_id', 'ar_namespace', 'ar_title', 'ar_timestamp', 'ar_comment', 'ar_minor_edit',
+ 'ar_user', 'ar_user_text', 'ar_deleted'
+ ),
+ 'conds' => $conds,
+ 'options' => array( 'FORCE INDEX' => $index )
+ );
+ }
+
+ function getUserCond() {
+ $condition = array();
+
+ $condition['ar_user_text'] = $this->target;
+ $index = 'usertext_timestamp';
+
+ return array( $index, $condition );
+ }
+
+ function getIndexField() {
+ return 'ar_timestamp';
+ }
+
+ function getStartBody() {
+ return "<ul>\n";
+ }
+
+ function getEndBody() {
+ return "</ul>\n";
+ }
+
+ function getNavigationBar() {
+ if ( isset( $this->mNavigationBar ) ) {
+ return $this->mNavigationBar;
+ }
+ $linkTexts = array(
+ 'prev' => wfMsgHtml( 'pager-newer-n', $this->mLimit ),
+ 'next' => wfMsgHtml( 'pager-older-n', $this->mLimit ),
+ 'first' => wfMsgHtml( 'histlast' ),
+ 'last' => wfMsgHtml( 'histfirst' )
+ );
+
+ $pagingLinks = $this->getPagingLinks( $linkTexts );
+ $limitLinks = $this->getLimitLinks();
+ $limits = implode( ' | ', $limitLinks );
+
+ $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " .
+ wfMsgExt( 'viewprevnext', array( 'parsemag' ), $pagingLinks['prev'], $pagingLinks['next'], $limits );
+ return $this->mNavigationBar;
+ }
+
+ function getNamespaceCond() {
+ if ( $this->namespace !== '' ) {
+ return array( 'ar_namespace' => (int)$this->namespace );
+ } else {
+ return array();
+ }
+ }
+
+ /**
+ * Generates each row in the contributions list.
+ *
+ * Contributions which are marked "top" are currently on top of the history.
+ * For these contributions, a [rollback] link is shown for users with sysop
+ * privileges. The rollback link restores the most recent version that was not
+ * written by the target user.
+ *
+ * @todo This would probably look a lot nicer in a table.
+ */
+ function formatRow( $row ) {
+ wfProfileIn( __METHOD__ );
+
+ global $wgLang, $wgUser;
+
+ $sk = $this->getSkin();
+
+ $rev = new Revision( array(
+ 'id' => $row->ar_rev_id,
+ 'comment' => $row->ar_comment,
+ 'user' => $row->ar_user,
+ 'user_text' => $row->ar_user_text,
+ 'timestamp' => $row->ar_timestamp,
+ 'minor_edit' => $row->ar_minor_edit,
+ 'rev_deleted' => $row->ar_deleted,
+ ) );
+
+ $page = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+
+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
+
+ $logs = SpecialPage::getTitleFor( 'Log' );
+ $dellog = $sk->makeKnownLinkObj( $logs,
+ $this->messages['deletionlog'],
+ 'type=delete&page=' . $page->getPrefixedUrl() );
+
+ $reviewlink = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete', $page->getPrefixedDBkey() ),
+ $this->messages['undeletebtn'] );
+
+ $link = $sk->makeKnownLinkObj( $undelete,
+ htmlspecialchars( $page->getPrefixedText() ),
+ 'target=' . $page->getPrefixedUrl() .
+ '&timestamp=' . $rev->getTimestamp() );
+
+ $last = $sk->makeKnownLinkObj( $undelete,
+ $this->messages['diff'],
+ "target=" . $page->getPrefixedUrl() .
+ "&timestamp=" . $rev->getTimestamp() .
+ "&diff=prev" );
+
+ $comment = $sk->revComment( $rev );
+ $d = $wgLang->timeanddate( $rev->getTimestamp(), true );
+
+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $d = '<span class="history-deleted">' . $d . '</span>';
+ } else {
+ $link = $sk->makeKnownLinkObj( $undelete, $d,
+ 'target=' . $page->getPrefixedUrl() .
+ '&timestamp=' . $rev->getTimestamp() );
+ }
+
+ $pagelink = $sk->makeLinkObj( $page );
+
+ if( $rev->isMinor() ) {
+ $mflag = '<span class="minor">' . $this->messages['minoreditletter'] . '</span> ';
+ } else {
+ $mflag = '';
+ }
+
+
+ $ret = "{$link} ($last) ({$dellog}) ({$reviewlink}) . . {$mflag} {$pagelink} {$comment}";
+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $ret .= ' ' . wfMsgHtml( 'deletedrev' );
+ }
+
+ $ret = "<li>$ret</li>\n";
+
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
+ /**
+ * Get the Database object in use
+ *
+ * @return Database
+ */
+ public function getDatabase() {
+ return $this->mDb;
+ }
+}
+
+class DeletedContributionsPage extends SpecialPage {
+ function __construct() {
+ parent::__construct( 'DeletedContributions', 'deletedhistory',
+ /*listed*/ true, /*function*/ false, /*file*/ false );
+ }
+
+ /**
+ * Special page "deleted user contributions".
+ * Shows a list of the deleted contributions of a user.
+ *
+ * @return none
+ * @param $par String: (optional) user name of the user for which to show the contributions
+ */
+ function execute( $par ) {
+ global $wgUser;
+ $this->setHeaders();
+
+ if ( !$this->userCanExecute( $wgUser ) ) {
+ $this->displayRestrictionError();
+ return;
+ }
+
+ global $wgUser, $wgOut, $wgLang, $wgRequest;
+
+ $options = array();
+
+ if ( isset( $par ) ) {
+ $target = $par;
+ } else {
+ $target = $wgRequest->getVal( 'target' );
+ }
+
+ if ( !strlen( $target ) ) {
+ $wgOut->addHTML( $this->getForm( '' ) );
+ return;
+ }
+
+ $options['limit'] = $wgRequest->getInt( 'limit', 50 );
+ $options['target'] = $target;
+
+ $nt = Title::makeTitleSafe( NS_USER, $target );
+ if ( !$nt ) {
+ $wgOut->addHTML( $this->getForm( '' ) );
+ return;
+ }
+ $id = User::idFromName( $nt->getText() );
+
+ $target = $nt->getText();
+ $wgOut->setSubtitle( $this->getSubTitle( $nt, $id ) );
+
+ if ( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) {
+ $options['namespace'] = intval( $ns );
+ } else {
+ $options['namespace'] = '';
+ }
+
+ $wgOut->addHTML( $this->getForm( $options ) );
+
+ $pager = new DeletedContribsPager( $target, $options['namespace'] );
+ 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() .
+ '<p>' . $pager->getNavigationBar() . '</p>' );
+
+ # If there were contributions, and it was a valid user or IP, show
+ # the appropriate "footer" message - WHOIS tools, etc.
+ if( $target != 'newbies' ) {
+ $message = IP::isIPAddress( $target )
+ ? 'sp-contributions-footer-anon'
+ : 'sp-contributions-footer';
+
+
+ $text = wfMsgNoTrans( $message, $target );
+ if( !wfEmptyMsg( $message, $text ) && $text != '-' ) {
+ $wgOut->addHTML( '<div class="mw-contributions-footer">' );
+ $wgOut->addWikiText( $text );
+ $wgOut->addHTML( '</div>' );
+ }
+ }
+ }
+
+ /**
+ * Generates the subheading with links
+ * @param $nt @see Title object for the target
+ */
+ function getSubTitle( $nt, $id ) {
+ global $wgSysopUserBans, $wgLang, $wgUser;
+
+ $sk = $wgUser->getSkin();
+
+ if ( 0 == $id ) {
+ $user = $nt->getText();
+ } else {
+ $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) );
+ }
+ $talk = $nt->getTalkPage();
+ if( $talk ) {
+ # Talk page link
+ $tools[] = $sk->makeLinkObj( $talk, wfMsgHtml( 'talkpagelinktext' ) );
+ if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && User::isIP( $nt->getText() ) ) ) {
+ # Block link
+ if( $wgUser->isAllowed( 'block' ) )
+ $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ),
+ wfMsgHtml( 'blocklink' ) );
+ # Block log link
+ $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ),
+ wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() );
+ }
+ # Other logs link
+ $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ),
+ wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() );
+ # Link to undeleted contributions
+ $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $nt->getDBkey() ),
+ wfMsgHtml( 'contributions' ) );
+
+ wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) );
+
+ $links = implode( ' | ', $tools );
+ }
+
+ // Old message 'contribsub' had one parameter, but that doesn't work for
+ // languages that want to put the "for" bit right after $user but before
+ // $links. If 'contribsub' is around, use it for reverse compatibility,
+ // otherwise use 'contribsub2'.
+ if( wfEmptyMsg( 'contribsub', wfMsg( 'contribsub' ) ) ) {
+ return wfMsgHtml( 'contribsub2', $user, $links );
+ } else {
+ return wfMsgHtml( 'contribsub', "$user ($links)" );
+ }
+ }
+
+ /**
+ * Generates the namespace selector form with hidden attributes.
+ * @param $options Array: the options to be included.
+ */
+ function getForm( $options ) {
+ global $wgScript, $wgTitle, $wgRequest;
+
+ $options['title'] = $wgTitle->getPrefixedText();
+ if ( !isset( $options['target'] ) ) {
+ $options['target'] = '';
+ } else {
+ $options['target'] = str_replace( '_' , ' ' , $options['target'] );
+ }
+
+ if ( !isset( $options['namespace'] ) ) {
+ $options['namespace'] = '';
+ }
+
+ if ( !isset( $options['contribs'] ) ) {
+ $options['contribs'] = 'user';
+ }
+
+ if ( $options['contribs'] == 'newbie' ) {
+ $options['target'] = '';
+ }
+
+ $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
+
+ foreach ( $options as $name => $value ) {
+ if ( in_array( $name, array( 'namespace', 'target', 'contribs' ) ) ) {
+ continue;
+ }
+ $f .= "\t" . Xml::hidden( $name, $value ) . "\n";
+ }
+
+ $f .= Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) .
+ Xml::tags( 'label', array( 'for' => 'target' ), wfMsgExt( 'sp-contributions-username', 'parseinline' ) ) . ' ' .
+ Xml::input( 'target', 20, $options['target']) . ' '.
+ Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' .
+ Xml::namespaceSelector( $options['namespace'], '' ) . ' ' .
+ Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' );
+ return $f;
+ }
+}
diff --git a/includes/specials/SpecialDisambiguations.php b/includes/specials/SpecialDisambiguations.php
index 34045660..0a728b68 100644
--- a/includes/specials/SpecialDisambiguations.php
+++ b/includes/specials/SpecialDisambiguations.php
@@ -84,13 +84,13 @@ class DisambiguationsPage extends PageQueryPage {
function formatResult( $skin, $result ) {
global $wgContLang;
- $title = Title::newFromId( $result->value );
+ $title = Title::newFromID( $result->value );
$dp = Title::makeTitle( $result->namespace, $result->title );
- $from = $skin->makeKnownLinkObj( $title, '' );
- $edit = $skin->makeKnownLinkObj( $title, "(".wfMsgHtml("qbedit").")" , 'redirect=no&action=edit' );
+ $from = $skin->link( $title );
+ $edit = $skin->link( $title, "(".wfMsgHtml("qbedit").")", array(), array( 'redirect' => 'no', 'action' => 'edit' ) );
$arr = $wgContLang->getArrow();
- $to = $skin->makeKnownLinkObj( $dp, '' );
+ $to = $skin->link( $dp );
return "$from $edit $arr $to";
}
diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php
index 3874c6a1..cf90f94d 100644
--- a/includes/specials/SpecialEmailuser.php
+++ b/includes/specials/SpecialEmailuser.php
@@ -5,17 +5,22 @@
*/
/**
- * @todo document
+ * Constructor for Special:Emailuser.
*/
function wfSpecialEmailuser( $par ) {
global $wgRequest, $wgUser, $wgOut;
+ if ( !EmailUserForm::userEmailEnabled() ) {
+ $wgOut->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' );
+ return;
+ }
+
$action = $wgRequest->getVal( 'action' );
$target = isset($par) ? $par : $wgRequest->getVal( 'target' );
$targetUser = EmailUserForm::validateEmailTarget( $target );
if ( !( $targetUser instanceof User ) ) {
- $wgOut->showErrorPage( $targetUser[0], $targetUser[1] );
+ $wgOut->showErrorPage( $targetUser.'title', $targetUser.'text' );
return;
}
@@ -30,7 +35,7 @@ function wfSpecialEmailuser( $par ) {
$error = EmailUserForm::getPermissionsError( $wgUser, $wgRequest->getVal( 'wpEditToken' ) );
if ( $error ) {
- switch ( $error[0] ) {
+ switch ( $error ) {
case 'blockedemailuser':
$wgOut->blockedPage();
return;
@@ -40,12 +45,11 @@ function wfSpecialEmailuser( $par ) {
case 'sessionfailure':
$form->showForm();
return;
- default:
- $wgOut->showErrorPage( $error[0], $error[1] );
+ case 'mailnologin':
+ $wgOut->showErrorPage( 'mailnologin', 'mailnologintext' );
return;
}
}
-
if ( "submit" == $action && $wgRequest->wasPosted() ) {
$result = $form->doSubmit();
@@ -94,46 +98,64 @@ class EmailUserForm {
$this->subject = wfMsgExt( 'defemailsubject', array( 'content', 'parsemag' ) );
}
- $emf = wfMsg( "emailfrom" );
- $senderLink = $skin->makeLinkObj(
- $wgUser->getUserPage(), htmlspecialchars( $wgUser->getName() ) );
- $emt = wfMsg( "emailto" );
- $recipientLink = $skin->makeLinkObj(
- $this->target->getUserPage(), htmlspecialchars( $this->target->getName() ) );
- $emr = wfMsg( "emailsubject" );
- $emm = wfMsg( "emailmessage" );
- $ems = wfMsg( "emailsend" );
- $emc = wfMsg( "emailccme" );
- $encSubject = htmlspecialchars( $this->subject );
-
$titleObj = SpecialPage::getTitleFor( "Emailuser" );
- $action = $titleObj->escapeLocalURL( "target=" .
+ $action = $titleObj->getLocalURL( "target=" .
urlencode( $this->target->getName() ) . "&action=submit" );
- $token = htmlspecialchars( $wgUser->editToken() );
-
- $wgOut->addHTML( "
-<form id=\"emailuser\" method=\"post\" action=\"{$action}\">
-<table border='0' id='mailheader'><tr>
-<td align='right'>{$emf}:</td>
-<td align='left'><strong>{$senderLink}</strong></td>
-</tr><tr>
-<td align='right'>{$emt}:</td>
-<td align='left'><strong>{$recipientLink}</strong></td>
-</tr><tr>
-<td align='right'>{$emr}:</td>
-<td align='left'>
-<input type='text' size='60' maxlength='200' name=\"wpSubject\" value=\"{$encSubject}\" />
-</td>
-</tr>
-</table>
-<span id='wpTextLabel'><label for=\"wpText\">{$emm}:</label><br /></span>
-<textarea id=\"wpText\" name=\"wpText\" rows='20' cols='80' style=\"width: 100%;\">" . htmlspecialchars( $this->text ) .
-"</textarea>
-" . wfCheckLabel( $emc, 'wpCCMe', 'wpCCMe', $wgUser->getBoolOption( 'ccmeonemails' ) ) . "<br />
-<input type='submit' name=\"wpSend\" value=\"{$ems}\" />
-<input type='hidden' name='wpEditToken' value=\"$token\" />
-</form>\n" );
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'emailuser' ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsgExt( 'email-legend', 'parsemag' ) ) .
+ Xml::openElement( 'table', array( 'class' => 'mw-emailuser-table' ) ) .
+ "<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'emailfrom' ), 'emailfrom' ) .
+ "</td>
+ <td class='mw-input' id='mw-emailuser-sender'>" .
+ $skin->link( $wgUser->getUserPage(), htmlspecialchars( $wgUser->getName() ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'emailto' ), 'emailto' ) .
+ "</td>
+ <td class='mw-input' id='mw-emailuser-recipient'>" .
+ $skin->link( $this->target->getUserPage(), htmlspecialchars( $this->target->getName() ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'emailsubject' ), 'wpSubject' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpSubject', 60, $this->subject, array( 'type' => 'text', 'maxlength' => 200 ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'emailmessage' ), 'wpText' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::textarea( 'wpText', $this->text, 80, 20, array( 'id' => 'wpText' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'emailccme' ), 'wpCCMe', 'wpCCMe', $wgUser->getBoolOption( 'ccmeonemails' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( wfMsg( 'emailsend' ), array( 'name' => 'wpSend', 'accesskey' => 's' ) ) .
+ "</td>
+ </tr>" .
+ Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
+ );
}
/*
@@ -149,7 +171,7 @@ class EmailUserForm {
$subject = $this->subject;
// Add a standard footer and trim up trailing newlines
- $this->text = rtrim($this->text) . "\n\n---\n" . wfMsgExt( 'emailuserfooter',
+ $this->text = rtrim($this->text) . "\n\n-- \n" . wfMsgExt( 'emailuserfooter',
array( 'content', 'parsemag' ), array( $from->name, $to->name ) );
if( wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$this->text ) ) ) {
@@ -228,27 +250,33 @@ class EmailUserForm {
return $this->target;
}
- static function validateEmailTarget ( $target ) {
+ static function userEmailEnabled() {
global $wgEnableEmail, $wgEnableUserEmail;
-
- if( !( $wgEnableEmail && $wgEnableUserEmail ) )
- return array( "nosuchspecialpage", "nospecialpagetext" );
+ return $wgEnableEmail && $wgEnableUserEmail;
+ }
+ static function validateEmailTarget ( $target ) {
if ( "" == $target ) {
wfDebug( "Target is empty.\n" );
- return array( "notargettitle", "notargettext" );
+ return "notarget";
}
$nt = Title::newFromURL( $target );
if ( is_null( $nt ) ) {
wfDebug( "Target is invalid title.\n" );
- return array( "notargettitle", "notargettext" );
+ return "notarget";
}
$nu = User::newFromName( $nt->getText() );
- if( is_null( $nu ) || !$nu->canReceiveEmail() ) {
- wfDebug( "Target is invalid user or can't receive.\n" );
- return array( "noemailtitle", "noemailtext" );
+ if( is_null( $nu ) || !$nu->getId() ) {
+ wfDebug( "Target is invalid user.\n" );
+ return "notarget";
+ } else if ( !$nu->isEmailConfirmed() ) {
+ wfDebug( "User has no valid email.\n" );
+ return "noemail";
+ } else if ( !$nu->canReceiveEmail() ) {
+ wfDebug( "User does not allow user emails.\n" );
+ return "nowikiemail";
}
return $nu;
@@ -256,22 +284,22 @@ class EmailUserForm {
static function getPermissionsError ( $user, $editToken ) {
if( !$user->canSendEmail() ) {
wfDebug( "User can't send.\n" );
- return array( "mailnologin", "mailnologintext" );
+ return "mailnologin";
}
if( $user->isBlockedFromEmailuser() ) {
wfDebug( "User is blocked from sending e-mail.\n" );
- return array( "blockedemailuser", "" );
+ return "blockedemailuser";
}
if( $user->pingLimiter( 'emailuser' ) ) {
wfDebug( "Ping limiter triggered.\n" );
- return array( 'actionthrottledtext', '' );
+ return 'actionthrottledtext';
}
if( !$user->matchEditToken( $editToken ) ) {
wfDebug( "Matching edit token failed.\n" );
- return array( 'sessionfailure', '' );
+ return 'sessionfailure';
}
return;
diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php
index 38bfc83e..898b5a78 100644
--- a/includes/specials/SpecialExport.php
+++ b/includes/specials/SpecialExport.php
@@ -1,5 +1,5 @@
<?php
-# Copyright (C) 2003 Brion Vibber <brion@pobox.com>
+# Copyright (C) 2003-2008 Brion Vibber <brion@pobox.com>
# http://www.mediawiki.org/
#
# This program is free software; you can redistribute it and/or modify
@@ -71,7 +71,7 @@ function wfExportGetTemplates( $inputPages, $pageSet ) {
function wfExportGetImages( $inputPages, $pageSet ) {
return wfExportGetLinks( $inputPages, $pageSet,
'imagelinks',
- array( NS_IMAGE . ' AS namespace', 'il_to AS title' ),
+ array( NS_FILE . ' AS namespace', 'il_to AS title' ),
array( 'page_id=il_from' ) );
}
@@ -93,7 +93,7 @@ function wfExportGetLinks( $inputPages, $pageSet, $table, $fields, $join ) {
array_merge( $join,
array(
'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDbKey() ) ),
+ 'page_title' => $title->getDBKey() ) ),
__METHOD__ );
foreach( $result as $row ) {
$template = Title::makeTitle( $row->namespace, $row->title );
@@ -126,7 +126,7 @@ function wfSpecialExport( $page = '' ) {
$catname = $wgRequest->getText( 'catname' );
if ( $catname !== '' && $catname !== NULL && $catname !== false ) {
- $t = Title::makeTitleSafe( NS_CATEGORY, $catname );
+ $t = Title::makeTitleSafe( NS_MAIN, $catname );
if ( $t ) {
/**
* @fixme This can lead to hitting memory limit for very large
@@ -223,8 +223,23 @@ function wfSpecialExport( $page = '' ) {
/* Ok, let's get to it... */
- $db = wfGetDB( DB_SLAVE );
- $exporter = new WikiExporter( $db, $history );
+ if( $history == WikiExporter::CURRENT ) {
+ $lb = false;
+ $db = wfGetDB( DB_SLAVE );
+ $buffer = WikiExporter::BUFFER;
+ } else {
+ // Use an unbuffered query; histories may be very long!
+ $lb = wfGetLBFactory()->newMainLB();
+ $db = $lb->getConnection( DB_SLAVE );
+ $buffer = WikiExporter::STREAM;
+
+ // This might take a while... :D
+ wfSuppressWarnings();
+ set_time_limit(0);
+ wfRestoreWarnings();
+ }
+
+ $exporter = new WikiExporter( $db, $history, $buffer );
$exporter->list_authors = $list_authors ;
$exporter->openStream();
@@ -251,11 +266,14 @@ function wfSpecialExport( $page = '' ) {
}
$exporter->closeStream();
+ if( $lb ) {
+ $lb->closeAll();
+ }
return;
}
$self = SpecialPage::getTitleFor( 'Export' );
- $wgOut->addHtml( wfMsgExt( 'exporttext', 'parse' ) );
+ $wgOut->addHTML( wfMsgExt( 'exporttext', 'parse' ) );
$form = Xml::openElement( 'form', array( 'method' => 'post',
'action' => $self->getLocalUrl( 'action=submit' ) ) );
@@ -271,14 +289,14 @@ function wfSpecialExport( $page = '' ) {
if( $wgExportAllowHistory ) {
$form .= Xml::checkLabel( wfMsg( 'exportcuronly' ), 'curonly', 'curonly', true ) . '<br />';
} else {
- $wgOut->addHtml( wfMsgExt( 'exportnohistory', 'parse' ) );
+ $wgOut->addHTML( wfMsgExt( 'exportnohistory', 'parse' ) );
}
$form .= Xml::checkLabel( wfMsg( 'export-templates' ), 'templates', 'wpExportTemplates', false ) . '<br />';
// Enable this when we can do something useful exporting/importing image information. :)
//$form .= Xml::checkLabel( wfMsg( 'export-images' ), 'images', 'wpExportImages', false ) . '<br />';
$form .= Xml::checkLabel( wfMsg( 'export-download' ), 'wpDownload', 'wpDownload', true ) . '<br />';
- $form .= Xml::submitButton( wfMsg( 'export-submit' ) );
+ $form .= Xml::submitButton( wfMsg( 'export-submit' ), array( 'accesskey' => 's' ) );
$form .= Xml::closeElement( 'form' );
- $wgOut->addHtml( $form );
+ $wgOut->addHTML( $form );
}
diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php
index 5236ca25..49a218c8 100644
--- a/includes/specials/SpecialFileDuplicateSearch.php
+++ b/includes/specials/SpecialFileDuplicateSearch.php
@@ -49,7 +49,7 @@ class FileDuplicateSearchPage extends QueryPage {
function formatResult( $skin, $result ) {
global $wgContLang, $wgLang;
- $nt = Title::makeTitle( NS_IMAGE, $result->title );
+ $nt = Title::makeTitle( NS_FILE, $result->title );
$text = $wgContLang->convert( $nt->getText() );
$plink = $skin->makeLink( $nt->getPrefixedText(), $text );
@@ -73,7 +73,7 @@ function wfSpecialFileDuplicateSearch( $par = null ) {
if( $title && $title->getText() != '' ) {
$dbr = wfGetDB( DB_SLAVE );
$image = $dbr->tableName( 'image' );
- $encFilename = $dbr->addQuotes( htmlspecialchars( $title->getDbKey() ) );
+ $encFilename = $dbr->addQuotes( htmlspecialchars( $title->getDBKey() ) );
$sql = "SELECT img_sha1 from $image where img_name = $encFilename";
$res = $dbr->query( $sql );
$row = $dbr->fetchRow( $res );
@@ -100,7 +100,7 @@ function wfSpecialFileDuplicateSearch( $par = null ) {
# Show a thumbnail of the file
$img = wfFindFile( $title );
if ( $img ) {
- $thumb = $img->getThumbnail( 120, 120 );
+ $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
if( $thumb ) {
$wgOut->addHTML( '<div style="float:' . $align . '" id="mw-fileduplicatesearch-icon">' .
$thumb->toHtml( array( 'desc-link' => false ) ) . '<br />' .
diff --git a/includes/specials/SpecialFilepath.php b/includes/specials/SpecialFilepath.php
index a2ba3e57..4a724b1f 100644
--- a/includes/specials/SpecialFilepath.php
+++ b/includes/specials/SpecialFilepath.php
@@ -9,9 +9,9 @@ function wfSpecialFilepath( $par ) {
$file = isset( $par ) ? $par : $wgRequest->getText( 'file' );
- $title = Title::newFromText( $file, NS_IMAGE );
+ $title = Title::makeTitleSafe( NS_FILE, $file );
- if ( ! $title instanceof Title || $title->getNamespace() != NS_IMAGE ) {
+ if ( ! $title instanceof Title || $title->getNamespace() != NS_FILE ) {
$cform = new FilepathForm( $title );
$cform->execute();
} else {
diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php
index 1623245d..5e1a6533 100644
--- a/includes/specials/SpecialImport.php
+++ b/includes/specials/SpecialImport.php
@@ -23,28 +23,55 @@
* @ingroup SpecialPage
*/
-/**
- * Constructor
- */
-function wfSpecialImport( $page = '' ) {
- global $wgUser, $wgOut, $wgRequest, $wgTitle, $wgImportSources;
- global $wgImportTargetNamespace;
-
- $interwiki = false;
- $namespace = $wgImportTargetNamespace;
- $frompage = '';
- $history = true;
-
- if ( wfReadOnly() ) {
- $wgOut->readOnlyPage();
- return;
+class SpecialImport extends SpecialPage {
+
+ private $interwiki = false;
+ private $namespace;
+ private $frompage = '';
+ private $logcomment= false;
+ private $history = true;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ parent::__construct( 'Import', 'import' );
+ global $wgImportTargetNamespace;
+ $this->namespace = $wgImportTargetNamespace;
}
-
- if( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit') {
+
+ /**
+ * Execute
+ */
+ function execute( $par ) {
+ global $wgRequest;
+
+ $this->setHeaders();
+ $this->outputHeader();
+
+ if ( wfReadOnly() ) {
+ global $wgOut;
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ if ( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit' ) {
+ $this->doImport();
+ }
+ $this->showForm();
+ }
+
+ /**
+ * Do the actual import
+ */
+ private function doImport() {
+ global $wgOut, $wgRequest, $wgUser, $wgImportSources;
$isUpload = false;
- $namespace = $wgRequest->getIntOrNull( 'namespace' );
+ $this->namespace = $wgRequest->getIntOrNull( 'namespace' );
$sourceName = $wgRequest->getVal( "source" );
+ $this->logcomment = $wgRequest->getText( 'log-comment' );
+
if ( !$wgUser->matchEditToken( $wgRequest->getVal( 'editToken' ) ) ) {
$source = new WikiErrorMsg( 'import-token-mismatch' );
} elseif ( $sourceName == 'upload' ) {
@@ -55,16 +82,16 @@ function wfSpecialImport( $page = '' ) {
return $wgOut->permissionRequired( 'importupload' );
}
} elseif ( $sourceName == "interwiki" ) {
- $interwiki = $wgRequest->getVal( 'interwiki' );
- if ( !in_array( $interwiki, $wgImportSources ) ) {
+ $this->interwiki = $wgRequest->getVal( 'interwiki' );
+ if ( !in_array( $this->interwiki, $wgImportSources ) ) {
$source = new WikiErrorMsg( "import-invalid-interwiki" );
} else {
- $history = $wgRequest->getCheck( 'interwikiHistory' );
- $frompage = $wgRequest->getText( "frompage" );
+ $this->history = $wgRequest->getCheck( 'interwikiHistory' );
+ $this->frompage = $wgRequest->getText( "frompage" );
$source = ImportStreamSource::newFromInterwiki(
- $interwiki,
- $frompage,
- $history );
+ $this->interwiki,
+ $this->frompage,
+ $this->history );
}
} else {
$source = new WikiErrorMsg( "importunknownsource" );
@@ -76,10 +103,10 @@ function wfSpecialImport( $page = '' ) {
$wgOut->addWikiMsg( "importstart" );
$importer = new WikiImporter( $source );
- if( !is_null( $namespace ) ) {
- $importer->setTargetNamespace( $namespace );
+ if( !is_null( $this->namespace ) ) {
+ $importer->setTargetNamespace( $this->namespace );
}
- $reporter = new ImportReporter( $importer, $isUpload, $interwiki );
+ $reporter = new ImportReporter( $importer, $isUpload, $this->interwiki , $this->logcomment);
$reporter->open();
$result = $importer->doImport();
@@ -99,79 +126,121 @@ function wfSpecialImport( $page = '' ) {
}
}
- $action = $wgTitle->getLocalUrl( 'action=submit' );
-
- if( $wgUser->isAllowed( 'importupload' ) ) {
- $wgOut->addWikiMsg( "importtext" );
- $wgOut->addHTML(
- Xml::openElement( 'fieldset' ).
- Xml::element( 'legend', null, wfMsg( 'import-upload' ) ) .
- Xml::openElement( 'form', array( 'enctype' => 'multipart/form-data', 'method' => 'post', 'action' => $action ) ) .
- Xml::hidden( 'action', 'submit' ) .
- Xml::hidden( 'source', 'upload' ) .
- Xml::input( 'xmlimport', 50, '', array( 'type' => 'file' ) ) . ' ' .
- Xml::hidden( 'editToken', $wgUser->editToken() ) .
- Xml::submitButton( wfMsg( 'uploadbtn' ) ) .
- Xml::closeElement( 'form' ) .
- Xml::closeElement( 'fieldset' )
- );
- } else {
- if( empty( $wgImportSources ) ) {
- $wgOut->addWikiMsg( 'importnosources' );
+ private function showForm() {
+ global $wgUser, $wgOut, $wgRequest, $wgTitle, $wgImportSources;
+ # FIXME: Quick hack to disable import for non privileged users /Raymond
+ # Regression from 43963
+ if( !$wgUser->isAllowed( 'import' ) && !$wgUser->isAllowed( 'importupload' ) )
+ return $wgOut->permissionRequired( 'import' );
+
+ $action = $wgTitle->getLocalUrl( 'action=submit' );
+
+ if( $wgUser->isAllowed( 'importupload' ) ) {
+ $wgOut->addWikiMsg( "importtext" );
+ $wgOut->addHTML(
+ Xml::openElement( 'fieldset' ).
+ Xml::element( 'legend', null, wfMsg( 'import-upload' ) ) .
+ Xml::openElement( 'form', array( 'enctype' => 'multipart/form-data', 'method' => 'post', 'action' => $action ) ) .
+ Xml::hidden( 'action', 'submit' ) .
+ Xml::hidden( 'source', 'upload' ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-import-table' ) ) .
+
+ "<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'import-upload-filename' ), 'xmlimport' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'xmlimport', 50, '', array( 'type' => 'file' ) ) . ' ' .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'import-comment' ), 'mw-import-comment' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'log-comment', 50, '',
+ array( 'id' => 'mw-import-comment', 'type' => 'text' ) ) . ' ' .
+ "</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::submitButton( wfMsg( 'uploadbtn' ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ).
+ Xml::hidden( 'editToken', $wgUser->editToken() ) .
+ Xml::closeElement( 'form' ) .
+ Xml::closeElement( 'fieldset' )
+ );
+ } else {
+ if( empty( $wgImportSources ) ) {
+ $wgOut->addWikiMsg( 'importnosources' );
+ }
}
- }
- if( !empty( $wgImportSources ) ) {
- $wgOut->addHTML(
- Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, wfMsg( 'importinterwiki' ) ) .
- Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action ) ) .
- wfMsgExt( 'import-interwiki-text', array( 'parse' ) ) .
- Xml::hidden( 'action', 'submit' ) .
- Xml::hidden( 'source', 'interwiki' ) .
- Xml::hidden( 'editToken', $wgUser->editToken() ) .
- Xml::openElement( 'table', array( 'id' => 'mw-import-table' ) ) .
- "<tr>
- <td>" .
- Xml::openElement( 'select', array( 'name' => 'interwiki' ) )
- );
- foreach( $wgImportSources as $prefix ) {
- $selected = ( $interwiki === $prefix ) ? ' selected="selected"' : '';
- $wgOut->addHTML( Xml::option( $prefix, $prefix, $selected ) );
+ if( $wgUser->isAllowed( 'import' ) && !empty( $wgImportSources ) ) {
+ $wgOut->addHTML(
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'importinterwiki' ) ) .
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action ) ) .
+ wfMsgExt( 'import-interwiki-text', array( 'parse' ) ) .
+ Xml::hidden( 'action', 'submit' ) .
+ Xml::hidden( 'source', 'interwiki' ) .
+ Xml::hidden( 'editToken', $wgUser->editToken() ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-import-table' ) ) .
+ "<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'import-interwiki-source' ), 'interwiki' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::openElement( 'select', array( 'name' => 'interwiki' ) )
+ );
+ foreach( $wgImportSources as $prefix ) {
+ $selected = ( $this->interwiki === $prefix ) ? ' selected="selected"' : '';
+ $wgOut->addHTML( Xml::option( $prefix, $prefix, $selected ) );
+ }
+ $wgOut->addHTML(
+ Xml::closeElement( 'select' ) .
+ Xml::input( 'frompage', 50, $this->frompage ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>
+ </td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'import-interwiki-history' ), 'interwikiHistory', 'interwikiHistory', $this->history ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>" .
+ Xml::label( wfMsg( 'import-interwiki-namespace' ), 'namespace' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::namespaceSelector( $this->namespace, '' ) .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'import-comment' ), 'mw-interwiki-comment' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'log-comment', 50, '',
+ array( 'id' => 'mw-interwiki-comment', 'type' => 'text' ) ) . ' ' .
+ "</td>
+ </tr>
+ <tr>
+ <td>
+ </td>
+ <td class='mw-input'>" .
+ Xml::submitButton( wfMsg( 'import-interwiki-submit' ), array( 'accesskey' => 's' ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ).
+ Xml::closeElement( 'form' ) .
+ Xml::closeElement( 'fieldset' )
+ );
}
- $wgOut->addHTML(
- Xml::closeElement( 'select' ) .
- "</td>
- <td>" .
- Xml::input( 'frompage', 50, $frompage ) .
- "</td>
- </tr>
- <tr>
- <td>
- </td>
- <td>" .
- Xml::checkLabel( wfMsg( 'import-interwiki-history' ), 'interwikiHistory', 'interwikiHistory', $history ) .
- "</td>
- </tr>
- <tr>
- <td>
- </td>
- <td>" .
- Xml::label( wfMsg( 'import-interwiki-namespace' ), 'namespace' ) .
- Xml::namespaceSelector( $namespace, '' ) .
- "</td>
- </tr>
- <tr>
- <td>
- </td>
- <td>" .
- Xml::submitButton( wfMsg( 'import-interwiki-submit' ) ) .
- "</td>
- </tr>" .
- Xml::closeElement( 'table' ).
- Xml::closeElement( 'form' ) .
- Xml::closeElement( 'fieldset' )
- );
}
}
@@ -180,16 +249,19 @@ function wfSpecialImport( $page = '' ) {
* @ingroup SpecialPage
*/
class ImportReporter {
- function __construct( $importer, $upload, $interwiki ) {
+ private $reason=false;
+
+ function __construct( $importer, $upload, $interwiki , $reason=false ) {
$importer->setPageOutCallback( array( $this, 'reportPage' ) );
$this->mPageCount = 0;
$this->mIsUpload = $upload;
$this->mInterwiki = $interwiki;
+ $this->reason = $reason;
}
function open() {
global $wgOut;
- $wgOut->addHtml( "<ul>\n" );
+ $wgOut->addHTML( "<ul>\n" );
}
function reportPage( $title, $origTitle, $revisionCount, $successCount ) {
@@ -203,7 +275,7 @@ class ImportReporter {
$contentCount = $wgContLang->formatNum( $successCount );
if( $successCount > 0 ) {
- $wgOut->addHtml( "<li>" . $skin->makeKnownLinkObj( $title ) . " " .
+ $wgOut->addHTML( "<li>" . $skin->makeKnownLinkObj( $title ) . " " .
wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) .
"</li>\n"
);
@@ -212,949 +284,43 @@ class ImportReporter {
if( $this->mIsUpload ) {
$detail = wfMsgExt( 'import-logentry-upload-detail', array( 'content', 'parsemag' ),
$contentCount );
+ if ( $this->reason ) {
+ $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason;
+ }
$log->addEntry( 'upload', $title, $detail );
} else {
$interwiki = '[[:' . $this->mInterwiki . ':' .
$origTitle->getPrefixedText() . ']]';
$detail = wfMsgExt( 'import-logentry-interwiki-detail', array( 'content', 'parsemag' ),
$contentCount, $interwiki );
+ if ( $this->reason ) {
+ $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason;
+ }
$log->addEntry( 'interwiki', $title, $detail );
}
$comment = $detail; // quick
$dbw = wfGetDB( DB_MASTER );
+ $latest = $title->getLatestRevID();
$nullRevision = Revision::newNullRevision( $dbw, $title->getArticleId(), $comment, true );
$nullRevision->insertOn( $dbw );
$article = new Article( $title );
# Update page record
$article->updateRevisionOn( $dbw, $nullRevision );
- wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $wgUser) );
} else {
- $wgOut->addHtml( '<li>' . wfMsgHtml( 'import-nonewrevisions' ) . '</li>' );
+ $wgOut->addHTML( '<li>' . wfMsgHtml( 'import-nonewrevisions' ) . '</li>' );
}
}
function close() {
global $wgOut;
if( $this->mPageCount == 0 ) {
- $wgOut->addHtml( "</ul>\n" );
+ $wgOut->addHTML( "</ul>\n" );
return new WikiErrorMsg( "importnopages" );
}
- $wgOut->addHtml( "</ul>\n" );
+ $wgOut->addHTML( "</ul>\n" );
return $this->mPageCount;
}
}
-
-/**
- *
- * @ingroup SpecialPage
- */
-class WikiRevision {
- var $title = null;
- var $id = 0;
- var $timestamp = "20010115000000";
- var $user = 0;
- var $user_text = "";
- var $text = "";
- var $comment = "";
- var $minor = false;
-
- function setTitle( $title ) {
- if( is_object( $title ) ) {
- $this->title = $title;
- } elseif( is_null( $title ) ) {
- throw new MWException( "WikiRevision given a null title in import. You may need to adjust \$wgLegalTitleChars." );
- } else {
- throw new MWException( "WikiRevision given non-object title in import." );
- }
- }
-
- function setID( $id ) {
- $this->id = $id;
- }
-
- function setTimestamp( $ts ) {
- # 2003-08-05T18:30:02Z
- $this->timestamp = wfTimestamp( TS_MW, $ts );
- }
-
- function setUsername( $user ) {
- $this->user_text = $user;
- }
-
- function setUserIP( $ip ) {
- $this->user_text = $ip;
- }
-
- function setText( $text ) {
- $this->text = $text;
- }
-
- function setComment( $text ) {
- $this->comment = $text;
- }
-
- function setMinor( $minor ) {
- $this->minor = (bool)$minor;
- }
-
- function setSrc( $src ) {
- $this->src = $src;
- }
-
- function setFilename( $filename ) {
- $this->filename = $filename;
- }
-
- function setSize( $size ) {
- $this->size = intval( $size );
- }
-
- function getTitle() {
- return $this->title;
- }
-
- function getID() {
- return $this->id;
- }
-
- function getTimestamp() {
- return $this->timestamp;
- }
-
- function getUser() {
- return $this->user_text;
- }
-
- function getText() {
- return $this->text;
- }
-
- function getComment() {
- return $this->comment;
- }
-
- function getMinor() {
- return $this->minor;
- }
-
- function getSrc() {
- return $this->src;
- }
-
- function getFilename() {
- return $this->filename;
- }
-
- function getSize() {
- return $this->size;
- }
-
- function importOldRevision() {
- $dbw = wfGetDB( DB_MASTER );
-
- # Sneak a single revision into place
- $user = User::newFromName( $this->getUser() );
- if( $user ) {
- $userId = intval( $user->getId() );
- $userText = $user->getName();
- } else {
- $userId = 0;
- $userText = $this->getUser();
- }
-
- // avoid memory leak...?
- $linkCache = LinkCache::singleton();
- $linkCache->clear();
-
- $article = new Article( $this->title );
- $pageId = $article->getId();
- if( $pageId == 0 ) {
- # must create the page...
- $pageId = $article->insertOn( $dbw );
- $created = true;
- } else {
- $created = false;
-
- $prior = Revision::loadFromTimestamp( $dbw, $this->title, $this->timestamp );
- if( !is_null( $prior ) ) {
- // FIXME: this could fail slightly for multiple matches :P
- wfDebug( __METHOD__ . ": skipping existing revision for [[" .
- $this->title->getPrefixedText() . "]], timestamp " .
- $this->timestamp . "\n" );
- return false;
- }
- }
-
- # FIXME: Use original rev_id optionally
- # FIXME: blah blah blah
-
- #if( $numrows > 0 ) {
- # return wfMsg( "importhistoryconflict" );
- #}
-
- # Insert the row
- $revision = new Revision( array(
- 'page' => $pageId,
- 'text' => $this->getText(),
- 'comment' => $this->getComment(),
- 'user' => $userId,
- 'user_text' => $userText,
- 'timestamp' => $this->timestamp,
- 'minor_edit' => $this->minor,
- ) );
- $revId = $revision->insertOn( $dbw );
- $changed = $article->updateIfNewerOn( $dbw, $revision );
-
- if( $created ) {
- wfDebug( __METHOD__ . ": running onArticleCreate\n" );
- Article::onArticleCreate( $this->title );
-
- wfDebug( __METHOD__ . ": running create updates\n" );
- $article->createUpdates( $revision );
-
- } elseif( $changed ) {
- wfDebug( __METHOD__ . ": running onArticleEdit\n" );
- Article::onArticleEdit( $this->title );
-
- wfDebug( __METHOD__ . ": running edit updates\n" );
- $article->editUpdates(
- $this->getText(),
- $this->getComment(),
- $this->minor,
- $this->timestamp,
- $revId );
- }
-
- return true;
- }
-
- function importUpload() {
- wfDebug( __METHOD__ . ": STUB\n" );
-
- /**
- // from file revert...
- $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() ) {
- */
-
- /**
- // from file upload...
- $this->mLocalFile = wfLocalFile( $nt );
- $this->mDestName = $this->mLocalFile->getName();
- //....
- $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
- File::DELETE_SOURCE, $this->mFileProps );
- if ( !$status->isGood() ) {
- $resultDetails = array( 'internal' => $status->getWikiText() );
- */
-
- // @fixme upload() uses $wgUser, which is wrong here
- // it may also create a page without our desire, also wrong potentially.
- // and, it will record a *current* upload, but we might want an archive version here
-
- $file = wfLocalFile( $this->getTitle() );
- if( !$file ) {
- var_dump( $file );
- wfDebug( "IMPORT: Bad file. :(\n" );
- return false;
- }
-
- $source = $this->downloadSource();
- if( !$source ) {
- wfDebug( "IMPORT: Could not fetch remote file. :(\n" );
- return false;
- }
-
- $status = $file->upload( $source,
- $this->getComment(),
- $this->getComment(), // Initial page, if none present...
- File::DELETE_SOURCE,
- false, // props...
- $this->getTimestamp() );
-
- if( $status->isGood() ) {
- // yay?
- wfDebug( "IMPORT: is ok?\n" );
- return true;
- }
-
- wfDebug( "IMPORT: is bad? " . $status->getXml() . "\n" );
- return false;
-
- }
-
- function downloadSource() {
- global $wgEnableUploads;
- if( !$wgEnableUploads ) {
- return false;
- }
-
- $tempo = tempnam( wfTempDir(), 'download' );
- $f = fopen( $tempo, 'wb' );
- if( !$f ) {
- wfDebug( "IMPORT: couldn't write to temp file $tempo\n" );
- return false;
- }
-
- // @fixme!
- $src = $this->getSrc();
- $data = Http::get( $src );
- if( !$data ) {
- wfDebug( "IMPORT: couldn't fetch source $src\n" );
- fclose( $f );
- unlink( $tempo );
- return false;
- }
-
- fwrite( $f, $data );
- fclose( $f );
-
- return $tempo;
- }
-
-}
-
-/**
- * implements Special:Import
- * @ingroup SpecialPage
- */
-class WikiImporter {
- var $mDebug = false;
- var $mSource = null;
- var $mPageCallback = null;
- var $mPageOutCallback = null;
- var $mRevisionCallback = null;
- var $mUploadCallback = null;
- var $mTargetNamespace = null;
- var $lastfield;
- var $tagStack = array();
-
- function __construct( $source ) {
- $this->setRevisionCallback( array( $this, "importRevision" ) );
- $this->setUploadCallback( array( $this, "importUpload" ) );
- $this->mSource = $source;
- }
-
- function throwXmlError( $err ) {
- $this->debug( "FAILURE: $err" );
- wfDebug( "WikiImporter XML error: $err\n" );
- }
-
- # --------------
-
- function doImport() {
- if( empty( $this->mSource ) ) {
- return new WikiErrorMsg( "importnotext" );
- }
-
- $parser = xml_parser_create( "UTF-8" );
-
- # case folding violates XML standard, turn it off
- xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
-
- xml_set_object( $parser, $this );
- xml_set_element_handler( $parser, "in_start", "" );
-
- $offset = 0; // for context extraction on error reporting
- do {
- $chunk = $this->mSource->readChunk();
- if( !xml_parse( $parser, $chunk, $this->mSource->atEnd() ) ) {
- wfDebug( "WikiImporter::doImport encountered XML parsing error\n" );
- return new WikiXmlError( $parser, wfMsgHtml( 'import-parse-failure' ), $chunk, $offset );
- }
- $offset += strlen( $chunk );
- } while( $chunk !== false && !$this->mSource->atEnd() );
- xml_parser_free( $parser );
-
- return true;
- }
-
- function debug( $data ) {
- if( $this->mDebug ) {
- wfDebug( "IMPORT: $data\n" );
- }
- }
-
- function notice( $data ) {
- global $wgCommandLineMode;
- if( $wgCommandLineMode ) {
- print "$data\n";
- } else {
- global $wgOut;
- $wgOut->addHTML( "<li>" . htmlspecialchars( $data ) . "</li>\n" );
- }
- }
-
- /**
- * Set debug mode...
- */
- function setDebug( $debug ) {
- $this->mDebug = $debug;
- }
-
- /**
- * Sets the action to perform as each new page in the stream is reached.
- * @param $callback callback
- * @return callback
- */
- function setPageCallback( $callback ) {
- $previous = $this->mPageCallback;
- $this->mPageCallback = $callback;
- return $previous;
- }
-
- /**
- * Sets the action to perform as each page in the stream is completed.
- * Callback accepts the page title (as a Title object), a second object
- * with the original title form (in case it's been overridden into a
- * local namespace), and a count of revisions.
- *
- * @param $callback callback
- * @return callback
- */
- function setPageOutCallback( $callback ) {
- $previous = $this->mPageOutCallback;
- $this->mPageOutCallback = $callback;
- return $previous;
- }
-
- /**
- * Sets the action to perform as each page revision is reached.
- * @param $callback callback
- * @return callback
- */
- function setRevisionCallback( $callback ) {
- $previous = $this->mRevisionCallback;
- $this->mRevisionCallback = $callback;
- return $previous;
- }
-
- /**
- * Sets the action to perform as each file upload version is reached.
- * @param $callback callback
- * @return callback
- */
- function setUploadCallback( $callback ) {
- $previous = $this->mUploadCallback;
- $this->mUploadCallback = $callback;
- return $previous;
- }
-
- /**
- * Set a target namespace to override the defaults
- */
- function setTargetNamespace( $namespace ) {
- if( is_null( $namespace ) ) {
- // Don't override namespaces
- $this->mTargetNamespace = null;
- } elseif( $namespace >= 0 ) {
- // FIXME: Check for validity
- $this->mTargetNamespace = intval( $namespace );
- } else {
- return false;
- }
- }
-
- /**
- * Default per-revision callback, performs the import.
- * @param $revision WikiRevision
- * @private
- */
- function importRevision( $revision ) {
- $dbw = wfGetDB( DB_MASTER );
- return $dbw->deadlockLoop( array( $revision, 'importOldRevision' ) );
- }
-
- /**
- * Dummy for now...
- */
- function importUpload( $revision ) {
- //$dbw = wfGetDB( DB_MASTER );
- //return $dbw->deadlockLoop( array( $revision, 'importUpload' ) );
- return false;
- }
-
- /**
- * Alternate per-revision callback, for debugging.
- * @param $revision WikiRevision
- * @private
- */
- function debugRevisionHandler( &$revision ) {
- $this->debug( "Got revision:" );
- if( is_object( $revision->title ) ) {
- $this->debug( "-- Title: " . $revision->title->getPrefixedText() );
- } else {
- $this->debug( "-- Title: <invalid>" );
- }
- $this->debug( "-- User: " . $revision->user_text );
- $this->debug( "-- Timestamp: " . $revision->timestamp );
- $this->debug( "-- Comment: " . $revision->comment );
- $this->debug( "-- Text: " . $revision->text );
- }
-
- /**
- * Notify the callback function when a new <page> is reached.
- * @param $title Title
- * @private
- */
- function pageCallback( $title ) {
- if( is_callable( $this->mPageCallback ) ) {
- call_user_func( $this->mPageCallback, $title );
- }
- }
-
- /**
- * Notify the callback function when a </page> is closed.
- * @param $title Title
- * @param $origTitle Title
- * @param $revisionCount int
- * @param $successCount Int: number of revisions for which callback returned true
- * @private
- */
- function pageOutCallback( $title, $origTitle, $revisionCount, $successCount ) {
- if( is_callable( $this->mPageOutCallback ) ) {
- call_user_func( $this->mPageOutCallback, $title, $origTitle,
- $revisionCount, $successCount );
- }
- }
-
-
- # XML parser callbacks from here out -- beware!
- function donothing( $parser, $x, $y="" ) {
- #$this->debug( "donothing" );
- }
-
- function in_start( $parser, $name, $attribs ) {
- $this->debug( "in_start $name" );
- if( $name != "mediawiki" ) {
- return $this->throwXMLerror( "Expected <mediawiki>, got <$name>" );
- }
- xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
- }
-
- function in_mediawiki( $parser, $name, $attribs ) {
- $this->debug( "in_mediawiki $name" );
- if( $name == 'siteinfo' ) {
- xml_set_element_handler( $parser, "in_siteinfo", "out_siteinfo" );
- } elseif( $name == 'page' ) {
- $this->push( $name );
- $this->workRevisionCount = 0;
- $this->workSuccessCount = 0;
- $this->uploadCount = 0;
- $this->uploadSuccessCount = 0;
- xml_set_element_handler( $parser, "in_page", "out_page" );
- } else {
- return $this->throwXMLerror( "Expected <page>, got <$name>" );
- }
- }
- function out_mediawiki( $parser, $name ) {
- $this->debug( "out_mediawiki $name" );
- if( $name != "mediawiki" ) {
- return $this->throwXMLerror( "Expected </mediawiki>, got </$name>" );
- }
- xml_set_element_handler( $parser, "donothing", "donothing" );
- }
-
-
- function in_siteinfo( $parser, $name, $attribs ) {
- // no-ops for now
- $this->debug( "in_siteinfo $name" );
- switch( $name ) {
- case "sitename":
- case "base":
- case "generator":
- case "case":
- case "namespaces":
- case "namespace":
- break;
- default:
- return $this->throwXMLerror( "Element <$name> not allowed in <siteinfo>." );
- }
- }
-
- function out_siteinfo( $parser, $name ) {
- if( $name == "siteinfo" ) {
- xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
- }
- }
-
-
- function in_page( $parser, $name, $attribs ) {
- $this->debug( "in_page $name" );
- switch( $name ) {
- case "id":
- case "title":
- case "restrictions":
- $this->appendfield = $name;
- $this->appenddata = "";
- xml_set_element_handler( $parser, "in_nothing", "out_append" );
- xml_set_character_data_handler( $parser, "char_append" );
- break;
- case "revision":
- $this->push( "revision" );
- if( is_object( $this->pageTitle ) ) {
- $this->workRevision = new WikiRevision;
- $this->workRevision->setTitle( $this->pageTitle );
- $this->workRevisionCount++;
- } else {
- // Skipping items due to invalid page title
- $this->workRevision = null;
- }
- xml_set_element_handler( $parser, "in_revision", "out_revision" );
- break;
- case "upload":
- $this->push( "upload" );
- if( is_object( $this->pageTitle ) ) {
- $this->workRevision = new WikiRevision;
- $this->workRevision->setTitle( $this->pageTitle );
- $this->uploadCount++;
- } else {
- // Skipping items due to invalid page title
- $this->workRevision = null;
- }
- xml_set_element_handler( $parser, "in_upload", "out_upload" );
- break;
- default:
- return $this->throwXMLerror( "Element <$name> not allowed in a <page>." );
- }
- }
-
- function out_page( $parser, $name ) {
- $this->debug( "out_page $name" );
- $this->pop();
- if( $name != "page" ) {
- return $this->throwXMLerror( "Expected </page>, got </$name>" );
- }
- xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
-
- $this->pageOutCallback( $this->pageTitle, $this->origTitle,
- $this->workRevisionCount, $this->workSuccessCount );
-
- $this->workTitle = null;
- $this->workRevision = null;
- $this->workRevisionCount = 0;
- $this->workSuccessCount = 0;
- $this->pageTitle = null;
- $this->origTitle = null;
- }
-
- function in_nothing( $parser, $name, $attribs ) {
- $this->debug( "in_nothing $name" );
- return $this->throwXMLerror( "No child elements allowed here; got <$name>" );
- }
- function char_append( $parser, $data ) {
- $this->debug( "char_append '$data'" );
- $this->appenddata .= $data;
- }
- function out_append( $parser, $name ) {
- $this->debug( "out_append $name" );
- if( $name != $this->appendfield ) {
- return $this->throwXMLerror( "Expected </{$this->appendfield}>, got </$name>" );
- }
-
- switch( $this->appendfield ) {
- case "title":
- $this->workTitle = $this->appenddata;
- $this->origTitle = Title::newFromText( $this->workTitle );
- if( !is_null( $this->mTargetNamespace ) && !is_null( $this->origTitle ) ) {
- $this->pageTitle = Title::makeTitle( $this->mTargetNamespace,
- $this->origTitle->getDBkey() );
- } else {
- $this->pageTitle = Title::newFromText( $this->workTitle );
- }
- if( is_null( $this->pageTitle ) ) {
- // Invalid page title? Ignore the page
- $this->notice( "Skipping invalid page title '$this->workTitle'" );
- } else {
- $this->pageCallback( $this->workTitle );
- }
- break;
- case "id":
- if ( $this->parentTag() == 'revision' ) {
- if( $this->workRevision )
- $this->workRevision->setID( $this->appenddata );
- }
- break;
- case "text":
- if( $this->workRevision )
- $this->workRevision->setText( $this->appenddata );
- break;
- case "username":
- if( $this->workRevision )
- $this->workRevision->setUsername( $this->appenddata );
- break;
- case "ip":
- if( $this->workRevision )
- $this->workRevision->setUserIP( $this->appenddata );
- break;
- case "timestamp":
- if( $this->workRevision )
- $this->workRevision->setTimestamp( $this->appenddata );
- break;
- case "comment":
- if( $this->workRevision )
- $this->workRevision->setComment( $this->appenddata );
- break;
- case "minor":
- if( $this->workRevision )
- $this->workRevision->setMinor( true );
- break;
- case "filename":
- if( $this->workRevision )
- $this->workRevision->setFilename( $this->appenddata );
- break;
- case "src":
- if( $this->workRevision )
- $this->workRevision->setSrc( $this->appenddata );
- break;
- case "size":
- if( $this->workRevision )
- $this->workRevision->setSize( intval( $this->appenddata ) );
- break;
- default:
- $this->debug( "Bad append: {$this->appendfield}" );
- }
- $this->appendfield = "";
- $this->appenddata = "";
-
- $parent = $this->parentTag();
- xml_set_element_handler( $parser, "in_$parent", "out_$parent" );
- xml_set_character_data_handler( $parser, "donothing" );
- }
-
- function in_revision( $parser, $name, $attribs ) {
- $this->debug( "in_revision $name" );
- switch( $name ) {
- case "id":
- case "timestamp":
- case "comment":
- case "minor":
- case "text":
- $this->appendfield = $name;
- xml_set_element_handler( $parser, "in_nothing", "out_append" );
- xml_set_character_data_handler( $parser, "char_append" );
- break;
- case "contributor":
- $this->push( "contributor" );
- xml_set_element_handler( $parser, "in_contributor", "out_contributor" );
- break;
- default:
- return $this->throwXMLerror( "Element <$name> not allowed in a <revision>." );
- }
- }
-
- function out_revision( $parser, $name ) {
- $this->debug( "out_revision $name" );
- $this->pop();
- if( $name != "revision" ) {
- return $this->throwXMLerror( "Expected </revision>, got </$name>" );
- }
- xml_set_element_handler( $parser, "in_page", "out_page" );
-
- if( $this->workRevision ) {
- $ok = call_user_func_array( $this->mRevisionCallback,
- array( $this->workRevision, $this ) );
- if( $ok ) {
- $this->workSuccessCount++;
- }
- }
- }
-
- function in_upload( $parser, $name, $attribs ) {
- $this->debug( "in_upload $name" );
- switch( $name ) {
- case "timestamp":
- case "comment":
- case "text":
- case "filename":
- case "src":
- case "size":
- $this->appendfield = $name;
- xml_set_element_handler( $parser, "in_nothing", "out_append" );
- xml_set_character_data_handler( $parser, "char_append" );
- break;
- case "contributor":
- $this->push( "contributor" );
- xml_set_element_handler( $parser, "in_contributor", "out_contributor" );
- break;
- default:
- return $this->throwXMLerror( "Element <$name> not allowed in an <upload>." );
- }
- }
-
- function out_upload( $parser, $name ) {
- $this->debug( "out_revision $name" );
- $this->pop();
- if( $name != "upload" ) {
- return $this->throwXMLerror( "Expected </upload>, got </$name>" );
- }
- xml_set_element_handler( $parser, "in_page", "out_page" );
-
- if( $this->workRevision ) {
- $ok = call_user_func_array( $this->mUploadCallback,
- array( $this->workRevision, $this ) );
- if( $ok ) {
- $this->workUploadSuccessCount++;
- }
- }
- }
-
- function in_contributor( $parser, $name, $attribs ) {
- $this->debug( "in_contributor $name" );
- switch( $name ) {
- case "username":
- case "ip":
- case "id":
- $this->appendfield = $name;
- xml_set_element_handler( $parser, "in_nothing", "out_append" );
- xml_set_character_data_handler( $parser, "char_append" );
- break;
- default:
- $this->throwXMLerror( "Invalid tag <$name> in <contributor>" );
- }
- }
-
- function out_contributor( $parser, $name ) {
- $this->debug( "out_contributor $name" );
- $this->pop();
- if( $name != "contributor" ) {
- return $this->throwXMLerror( "Expected </contributor>, got </$name>" );
- }
- $parent = $this->parentTag();
- xml_set_element_handler( $parser, "in_$parent", "out_$parent" );
- }
-
- private function push( $name ) {
- array_push( $this->tagStack, $name );
- $this->debug( "PUSH $name" );
- }
-
- private function pop() {
- $name = array_pop( $this->tagStack );
- $this->debug( "POP $name" );
- return $name;
- }
-
- private function parentTag() {
- $name = $this->tagStack[count( $this->tagStack ) - 1];
- $this->debug( "PARENT $name" );
- return $name;
- }
-
-}
-
-/**
- * @todo document (e.g. one-sentence class description).
- * @ingroup SpecialPage
- */
-class ImportStringSource {
- function __construct( $string ) {
- $this->mString = $string;
- $this->mRead = false;
- }
-
- function atEnd() {
- return $this->mRead;
- }
-
- function readChunk() {
- if( $this->atEnd() ) {
- return false;
- } else {
- $this->mRead = true;
- return $this->mString;
- }
- }
-}
-
-/**
- * @todo document (e.g. one-sentence class description).
- * @ingroup SpecialPage
- */
-class ImportStreamSource {
- function __construct( $handle ) {
- $this->mHandle = $handle;
- }
-
- function atEnd() {
- return feof( $this->mHandle );
- }
-
- function readChunk() {
- return fread( $this->mHandle, 32768 );
- }
-
- static function newFromFile( $filename ) {
- $file = @fopen( $filename, 'rt' );
- if( !$file ) {
- return new WikiErrorMsg( "importcantopen" );
- }
- return new ImportStreamSource( $file );
- }
-
- static function newFromUpload( $fieldname = "xmlimport" ) {
- $upload =& $_FILES[$fieldname];
-
- if( !isset( $upload ) || !$upload['name'] ) {
- return new WikiErrorMsg( 'importnofile' );
- }
- if( !empty( $upload['error'] ) ) {
- switch($upload['error']){
- case 1: # The uploaded file exceeds the upload_max_filesize directive in php.ini.
- return new WikiErrorMsg( 'importuploaderrorsize' );
- case 2: # The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.
- return new WikiErrorMsg( 'importuploaderrorsize' );
- case 3: # The uploaded file was only partially uploaded
- return new WikiErrorMsg( 'importuploaderrorpartial' );
- case 6: #Missing a temporary folder. Introduced in PHP 4.3.10 and PHP 5.0.3.
- return new WikiErrorMsg( 'importuploaderrortemp' );
- # case else: # Currently impossible
- }
-
- }
- $fname = $upload['tmp_name'];
- if( is_uploaded_file( $fname ) ) {
- return ImportStreamSource::newFromFile( $fname );
- } else {
- return new WikiErrorMsg( 'importnofile' );
- }
- }
-
- static function newFromURL( $url, $method = 'GET' ) {
- wfDebug( __METHOD__ . ": opening $url\n" );
- # 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 ) {
- if( $page == '' ) {
- return new WikiErrorMsg( 'import-noarticle' );
- }
- $link = Title::newFromText( "$interwiki:Special:Export/$page" );
- if( is_null( $link ) || $link->getInterwiki() == '' ) {
- return new WikiErrorMsg( 'importbadinterwiki' );
- } else {
- $params = $history ? 'history=1' : '';
- $url = $link->getFullUrl( $params );
- # For interwikis, use POST to avoid redirects.
- return ImportStreamSource::newFromURL( $url, "POST" );
- }
- }
-}
diff --git a/includes/specials/SpecialIpblocklist.php b/includes/specials/SpecialIpblocklist.php
index 696c7efe..8d573547 100644
--- a/includes/specials/SpecialIpblocklist.php
+++ b/includes/specials/SpecialIpblocklist.php
@@ -10,7 +10,7 @@
function wfSpecialIpblocklist() {
global $wgUser, $wgOut, $wgRequest;
- $ip = $wgRequest->getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) );
+ $ip = trim( $wgRequest->getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) ) );
$id = $wgRequest->getVal( 'id' );
$reason = $wgRequest->getText( 'wpUnblockReason' );
$action = $wgRequest->getText( 'action' );
@@ -71,9 +71,13 @@ class IPUnblockForm {
var $ip, $reason, $id;
function IPUnblockForm( $ip, $id, $reason ) {
+ global $wgRequest;
$this->ip = strtr( $ip, '_', ' ' );
$this->id = $id;
$this->reason = $reason;
+ $this->hideuserblocks = $wgRequest->getBool( 'hideuserblocks' );
+ $this->hidetempblocks = $wgRequest->getBool( 'hidetempblocks' );
+ $this->hideaddressblocks = $wgRequest->getBool( 'hideaddressblocks' );
}
/**
@@ -158,8 +162,7 @@ class IPUnblockForm {
* @return array array(message key, parameters) on failure, empty array on success
*/
- static function doUnblock(&$id, &$ip, &$reason, &$range = null)
- {
+ static function doUnblock(&$id, &$ip, &$reason, &$range = null) {
if ( $id ) {
$block = Block::newFromID( $id );
if ( !$block ) {
@@ -241,10 +244,27 @@ class IPUnblockForm {
// No extra conditions
} elseif ( substr( $this->ip, 0, 1 ) == '#' ) {
$conds['ipb_id'] = substr( $this->ip, 1 );
- } elseif ( IP::toUnsigned( $this->ip ) !== false ) {
- $conds['ipb_address'] = $this->ip;
+ // Single IPs
+ } elseif ( IP::isIPAddress($this->ip) && strpos($this->ip,'/') === false ) {
+ if( $iaddr = IP::toHex($this->ip) ) {
+ # Only scan ranges which start in this /16, this improves search speed
+ # Blocks should not cross a /16 boundary.
+ $range = substr( $iaddr, 0, 4 );
+ // Fixme -- encapsulate this sort of query-building.
+ $dbr = wfGetDB( DB_SLAVE );
+ $encIp = $dbr->addQuotes( IP::sanitizeIP($this->ip) );
+ $encRange = $dbr->addQuotes( "$range%" );
+ $encAddr = $dbr->addQuotes( $iaddr );
+ $conds[] = "(ipb_address = $encIp) OR
+ (ipb_range_start LIKE $encRange AND
+ ipb_range_start <= $encAddr
+ AND ipb_range_end >= $encAddr)";
+ } else {
+ $conds['ipb_address'] = IP::sanitizeIP($this->ip);
+ }
$conds['ipb_auto'] = 0;
- } elseif( preg_match( '/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\\/(\\d{1,2})$/', $this->ip, $matches ) ) {
+ // IP range
+ } elseif ( IP::isIPAddress($this->ip) ) {
$conds['ipb_address'] = Block::normaliseRange( $this->ip );
$conds['ipb_auto'] = 0;
} else {
@@ -257,6 +277,16 @@ class IPUnblockForm {
$conds['ipb_auto'] = 0;
}
}
+ // Apply filters
+ if( $this->hideuserblocks ) {
+ $conds['ipb_user'] = 0;
+ }
+ if( $this->hidetempblocks ) {
+ $conds['ipb_expiry'] = 'infinity';
+ }
+ if( $this->hideaddressblocks ) {
+ $conds[] = "ipb_user != 0 OR ipb_range_end > ipb_range_start";
+ }
$pager = new IPBlocklistPager( $this, $conds );
if ( $pager->getNumRows() ) {
@@ -270,12 +300,38 @@ class IPUnblockForm {
$wgOut->addHTML( $this->searchForm() );
$wgOut->addWikiMsg( 'ipblocklist-no-results' );
} else {
+ $wgOut->addHTML( $this->searchForm() );
$wgOut->addWikiMsg( 'ipblocklist-empty' );
}
}
function searchForm() {
global $wgTitle, $wgScript, $wgRequest;
+
+ $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' ) );
+ $nondefaults = array();
+ if( $this->hideuserblocks ) {
+ $nondefaults['hideuserblocks'] = $this->hideuserblocks;
+ }
+ if( $this->hidetempblocks ) {
+ $nondefaults['hidetempblocks'] = $this->hidetempblocks;
+ }
+ if( $this->hideaddressblocks ) {
+ $nondefaults['hideaddressblocks'] = $this->hideaddressblocks;
+ }
+ $ubLink = $this->makeOptionsLink( $showhide[1-$this->hideuserblocks],
+ array( 'hideuserblocks' => 1-$this->hideuserblocks ), $nondefaults);
+ $tbLink = $this->makeOptionsLink( $showhide[1-$this->hidetempblocks],
+ array( 'hidetempblocks' => 1-$this->hidetempblocks ), $nondefaults);
+ $sipbLink = $this->makeOptionsLink( $showhide[1-$this->hideaddressblocks],
+ array( 'hideaddressblocks' => 1-$this->hideaddressblocks ), $nondefaults);
+
+ $links = array();
+ $links[] = wfMsgHtml( 'ipblocklist-sh-userblocks', $ubLink );
+ $links[] = wfMsgHtml( 'ipblocklist-sh-tempblocks', $tbLink );
+ $links[] = wfMsgHtml( 'ipblocklist-sh-addressblocks', $sipbLink );
+ $hl = implode( ' ' . wfMsg( 'pipe-separator' ) . ' ', $links );
+
return
Xml::tags( 'form', array( 'action' => $wgScript ),
Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) .
@@ -283,16 +339,32 @@ class IPUnblockForm {
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::submitButton( wfMsg( 'ipblocklist-submit' ) ) . '<br />' .
+ $hl .
Xml::closeElement( 'fieldset' )
);
}
/**
+ * Makes change an option link which carries all the other options
+ * @param $title see Title
+ * @param $override
+ * @param $options
+ */
+ function makeOptionsLink( $title, $override, $options, $active = false ) {
+ global $wgUser;
+ $sk = $wgUser->getSkin();
+ $params = $override + $options;
+ $ipblocklist = SpecialPage::getTitleFor( 'IPBlockList' );
+ return $sk->link( $ipblocklist, htmlspecialchars( $title ),
+ ( $active ? array( 'style'=>'font-weight: bold;' ) : array() ), $params, array( 'known' ) );
+ }
+
+ /**
* Callback function to output a block
*/
function formatRow( $block ) {
- global $wgUser, $wgLang;
+ global $wgUser, $wgLang, $wgBlockAllowsUTEdit;
wfProfileIn( __METHOD__ );
@@ -302,8 +374,8 @@ class IPUnblockForm {
$sk = $wgUser->getSkin();
if( is_null( $msg ) ) {
$msg = array();
- $keys = array( 'infiniteblock', 'expiringblock', 'unblocklink',
- 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock' );
+ $keys = array( 'infiniteblock', 'expiringblock', 'unblocklink', 'change-blocklink',
+ 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock', 'blocklist-nousertalk' );
foreach( $keys as $key ) {
$msg[$key] = wfMsgHtml( $key );
}
@@ -341,15 +413,33 @@ class IPUnblockForm {
if ( $block->mBlockEmail && $block->mUser ) {
$properties[] = $msg['emailblock'];
}
+
+ if ( !$block->mAllowUsertalk && $wgBlockAllowsUTEdit ) {
+ $properties[] = $msg['blocklist-nousertalk'];
+ }
$properties = implode( ', ', $properties );
$line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $properties ) );
$unblocklink = '';
- if ( $wgUser->isAllowed('block') ) {
- $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
- $unblocklink = ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&id=' . urlencode( $block->mId ) ) . ')';
+ $changeblocklink = '';
+ $toolLinks = '';
+ if ( $wgUser->isAllowed( 'block' ) ) {
+ $unblocklink = $sk->link( SpecialPage::getTitleFor( 'Ipblocklist' ),
+ $msg['unblocklink'],
+ array(),
+ array( 'action' => 'unblock', 'id' => $block->mId ),
+ 'known' );
+
+ # Create changeblocklink for all blocks with exception of autoblocks
+ if( !$block->mAuto ) {
+ $changeblocklink = ' ' . wfMsg( 'pipe-separator' ) . ' ' .
+ $sk->link( SpecialPage::getTitleFor( 'Blockip', $block->mAddress ),
+ $msg['change-blocklink'],
+ array(), array(), 'known' );
+ }
+ $toolLinks = "($unblocklink$changeblocklink)";
}
$comment = $sk->commentBlock( $block->mReason );
@@ -359,7 +449,7 @@ class IPUnblockForm {
$s = '<span class="history-deleted">' . $s . '</span>';
wfProfileOut( __METHOD__ );
- return "<li>$s $unblocklink</li>\n";
+ return "<li>$s $toolLinks</li>\n";
}
}
diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php
new file mode 100644
index 00000000..6b9df58f
--- /dev/null
+++ b/includes/specials/SpecialLinkSearch.php
@@ -0,0 +1,185 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Brion Vibber
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * Special:LinkSearch to search the external-links table.
+ * @ingroup SpecialPage
+ */
+
+function wfSpecialLinkSearch( $par ) {
+
+ list( $limit, $offset ) = wfCheckLimits();
+ global $wgOut, $wgRequest, $wgUrlProtocols, $wgMiserMode;
+ $target = $GLOBALS['wgRequest']->getVal( 'target', $par );
+ $namespace = $GLOBALS['wgRequest']->getIntorNull( 'namespace', null );
+
+ $protocols_list[] = '';
+ foreach( $wgUrlProtocols as $prot ) {
+ $protocols_list[] = $prot;
+ }
+
+ $target2 = $target;
+ $protocol = '';
+ $pr_sl = strpos($target2, '//' );
+ $pr_cl = strpos($target2, ':' );
+ if ( $pr_sl ) {
+ // For protocols with '//'
+ $protocol = substr( $target2, 0 , $pr_sl+2 );
+ $target2 = substr( $target2, $pr_sl+2 );
+ } elseif ( !$pr_sl && $pr_cl ) {
+ // For protocols without '//' like 'mailto:'
+ $protocol = substr( $target2, 0 , $pr_cl+1 );
+ $target2 = substr( $target2, $pr_cl+1 );
+ } elseif ( $protocol == '' && $target2 != '' ) {
+ // default
+ $protocol = 'http://';
+ }
+ if ( !in_array( $protocol, $protocols_list ) ) {
+ // unsupported protocol, show original search request
+ $target2 = $target;
+ $protocol = '';
+ }
+
+ $self = Title::makeTitle( NS_SPECIAL, 'Linksearch' );
+
+ $wgOut->addWikiText( wfMsg( 'linksearch-text', '<nowiki>' . implode( ', ', $wgUrlProtocols) . '</nowiki>' ) );
+ $s = Xml::openElement( 'form', array( 'id' => 'mw-linksearch-form', 'method' => 'get', 'action' => $GLOBALS['wgScript'] ) ) .
+ Xml::hidden( 'title', $self->getPrefixedDbKey() ) .
+ '<fieldset>' .
+ Xml::element( 'legend', array(), wfMsg( 'linksearch' ) ) .
+ Xml::inputLabel( wfMsg( 'linksearch-pat' ), 'target', 'target', 50, $target ) . ' ';
+ if ( !$wgMiserMode ) {
+ $s .= Xml::label( wfMsg( 'linksearch-ns' ), 'namespace' ) . ' ' .
+ XML::namespaceSelector( $namespace, '' );
+ }
+ $s .= Xml::submitButton( wfMsg( 'linksearch-ok' ) ) .
+ '</fieldset>' .
+ Xml::closeElement( 'form' );
+ $wgOut->addHTML( $s );
+
+ if( $target != '' ) {
+ $searcher = new LinkSearchPage;
+ $searcher->setParams( array(
+ 'query' => $target2,
+ 'namespace' => $namespace,
+ 'protocol' => $protocol ) );
+ $searcher->doQuery( $offset, $limit );
+ }
+}
+
+class LinkSearchPage extends QueryPage {
+ function setParams( $params ) {
+ $this->mQuery = $params['query'];
+ $this->mNs = $params['namespace'];
+ $this->mProt = $params['protocol'];
+ }
+
+ function getName() {
+ return 'LinkSearch';
+ }
+
+ /**
+ * Disable RSS/Atom feeds
+ */
+ function isSyndicated() {
+ return false;
+ }
+
+ /**
+ * Return an appropriately formatted LIKE query and the clause
+ */
+ static function mungeQuery( $query , $prot ) {
+ $field = 'el_index';
+ $rv = LinkFilter::makeLike( $query , $prot );
+ if ($rv === false) {
+ //makeLike doesn't handle wildcard in IP, so we'll have to munge here.
+ if (preg_match('/^(:?[0-9]{1,3}\.)+\*\s*$|^(:?[0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]*\*\s*$/', $query)) {
+ $rv = $prot . rtrim($query, " \t*") . '%';
+ $field = 'el_to';
+ }
+ }
+ return array( $rv, $field );
+ }
+
+ function linkParameters() {
+ global $wgMiserMode;
+ $params = array();
+ $params['target'] = $this->mProt . $this->mQuery;
+ if( isset( $this->mNs ) && !$wgMiserMode ) {
+ $params['namespace'] = $this->mNs;
+ }
+ return $params;
+ }
+
+ function getSQL() {
+ global $wgMiserMode;
+ $dbr = wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $externallinks = $dbr->tableName( 'externallinks' );
+
+ /* strip everything past first wildcard, so that index-based-only lookup would be done */
+ list( $munged, $clause ) = self::mungeQuery( $this->mQuery, $this->mProt );
+ $stripped = substr($munged,0,strpos($munged,'%')+1);
+ $encSearch = $dbr->addQuotes( $stripped );
+
+ $encSQL = '';
+ if ( isset ($this->mNs) && !$wgMiserMode )
+ $encSQL = 'AND page_namespace=' . $dbr->addQuotes( $this->mNs );
+
+ $use_index = $dbr->useIndexClause( $clause );
+ return
+ "SELECT
+ page_namespace AS namespace,
+ page_title AS title,
+ el_index AS value,
+ el_to AS url
+ FROM
+ $page,
+ $externallinks $use_index
+ WHERE
+ page_id=el_from
+ AND $clause LIKE $encSearch
+ $encSQL";
+ }
+
+ function formatResult( $skin, $result ) {
+ $title = Title::makeTitle( $result->namespace, $result->title );
+ $url = $result->url;
+ $pageLink = $skin->makeKnownLinkObj( $title );
+ $urlLink = $skin->makeExternalLink( $url, $url );
+
+ return wfMsgHtml( 'linksearch-line', $urlLink, $pageLink );
+ }
+
+ /**
+ * Override to check query validity.
+ */
+ function doQuery( $offset, $limit, $shownavigation=true ) {
+ global $wgOut;
+ list( $this->mMungedQuery, $clause ) = LinkSearchPage::mungeQuery( $this->mQuery, $this->mProt );
+ if( $this->mMungedQuery === false ) {
+ $wgOut->addWikiText( wfMsg( 'linksearch-error' ) );
+ } else {
+ // For debugging
+ // Generates invalid xhtml with patterns that contain --
+ //$wgOut->addHTML( "\n<!-- " . htmlspecialchars( $this->mMungedQuery ) . " -->\n" );
+ parent::doQuery( $offset, $limit, $shownavigation );
+ }
+ }
+
+ /**
+ * Override to squash the ORDER BY.
+ * We do a truncated index search, so the optimizer won't trust
+ * it as good enough for optimizing sort. The implicit ordering
+ * from the scan will usually do well enough for our needs.
+ */
+ function getOrder() {
+ return '';
+ }
+}
diff --git a/includes/specials/SpecialListUserRestrictions.php b/includes/specials/SpecialListUserRestrictions.php
new file mode 100644
index 00000000..27b24298
--- /dev/null
+++ b/includes/specials/SpecialListUserRestrictions.php
@@ -0,0 +1,161 @@
+<?php
+
+function wfSpecialListUserRestrictions() {
+ global $wgOut, $wgRequest;
+
+ $wgOut->addWikiMsg( 'listuserrestrictions-intro' );
+ $f = new SpecialListUserRestrictionsForm();
+ $wgOut->addHTML( $f->getHTML() );
+
+ if( !mt_rand( 0, 10 ) )
+ UserRestriction::purgeExpired();
+ $pager = new UserRestrictionsPager( $f->getConds() );
+ if( $pager->getNumRows() )
+ $wgOut->addHTML( $pager->getNavigationBar() .
+ Xml::tags( 'ul', null, $pager->getBody() ) .
+ $pager->getNavigationBar()
+ );
+ elseif( $f->getConds() )
+ $wgOut->addWikiMsg( 'listuserrestrictions-notfound' );
+ else
+ $wgOut->addWikiMsg( 'listuserrestrictions-empty' );
+}
+
+class SpecialListUserRestrictionsForm {
+ public function getHTML() {
+ global $wgRequest, $wgScript, $wgTitle;
+ $s = '';
+ $s .= Xml::fieldset( wfMsg( 'listuserrestrictions-legend' ) );
+ $s .= "<form action=\"{$wgScript}\">";
+ $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() );
+ $s .= Xml::label( wfMsgHtml( 'listuserrestrictions-type' ), 'type' ) . '&nbsp;' .
+ self::typeSelector( 'type', $wgRequest->getVal( 'type' ), 'type' );
+ $s .= '&nbsp;';
+ $s .= Xml::inputLabel( wfMsgHtml( 'listuserrestrictions-user' ), 'user', 'user',
+ false, $wgRequest->getVal( 'user' ) );
+ $s .= '<p>';
+ $s .= Xml::label( wfMsgHtml( 'listuserrestrictions-namespace' ), 'namespace' ) . '&nbsp;' .
+ Xml::namespaceSelector( $wgRequest->getVal( 'namespace' ), '', 'namespace' );
+ $s .= '&nbsp;';
+ $s .= Xml::inputLabel( wfMsgHtml( 'listuserrestrictions-page' ), 'page', 'page',
+ false, $wgRequest->getVal( 'page' ) );
+ $s .= Xml::submitButton( wfMsg( 'listuserrestrictions-submit' ) );
+ $s .= "</p></form></fieldset>";
+ return $s;
+ }
+
+ public static function typeSelector( $name = 'type', $value = '', $id = false ) {
+ $s = new XmlSelect( $name, $id, $value );
+ $s->addOption( wfMsg( 'userrestrictiontype-none' ), '' );
+ $s->addOption( wfMsg( 'userrestrictiontype-page' ), UserRestriction::PAGE );
+ $s->addOption( wfMsg( 'userrestrictiontype-namespace' ), UserRestriction::NAMESPACE );
+ return $s->getHTML();
+ }
+
+ public function getConds() {
+ global $wgRequest;
+ $conds = array();
+
+ $type = $wgRequest->getVal( 'type' );
+ if( in_array( $type, array( UserRestriction::PAGE, UserRestriction::NAMESPACE ) ) )
+ $conds['ur_type'] = $type;
+
+ $user = $wgRequest->getVal( 'user' );
+ if( $user )
+ $conds['ur_user_text'] = $user;
+
+ $namespace = $wgRequest->getVal( 'namespace' );
+ if( $namespace || $namespace === '0' )
+ $conds['ur_namespace'] = $namespace;
+
+ $page = $wgRequest->getVal( 'page' );
+ $title = Title::newFromText( $page );
+ if( $title ) {
+ $conds['ur_page_namespace'] = $title->getNamespace();
+ $conds['ur_page_title'] = $title->getDBKey();
+ }
+
+ return $conds;
+ }
+}
+
+class UserRestrictionsPager extends ReverseChronologicalPager {
+ public $mConds;
+
+ public function __construct( $conds = array() ) {
+ $this->mConds = $conds;
+ parent::__construct();
+ }
+
+ public function getStartBody() {
+ # Copied from Special:Ipblocklist
+ wfProfileIn( __METHOD__ );
+ # Do a link batch query
+ $this->mResult->seek( 0 );
+ $lb = new LinkBatch;
+
+ # Faster way
+ # Usernames and titles are in fact related by a simple substitution of space -> underscore
+ # The last few lines of Title::secureAndSplit() tell the story.
+ foreach( $this->mResult as $row ) {
+ $name = str_replace( ' ', '_', $row->ur_by_text );
+ $lb->add( NS_USER, $name );
+ $lb->add( NS_USER_TALK, $name );
+ $name = str_replace( ' ', '_', $row->ur_user_text );
+ $lb->add( NS_USER, $name );
+ $lb->add( NS_USER_TALK, $name );
+ if( $row->ur_type == UserRestriction::PAGE )
+ $lb->add( $row->ur_page_namespace, $row->ur_page_title );
+ }
+ $lb->execute();
+ wfProfileOut( __METHOD__ );
+ return '';
+ }
+
+ public function getQueryInfo() {
+ return array(
+ 'tables' => 'user_restrictions',
+ 'fields' => '*',
+ 'conds' => $this->mConds,
+ );
+ }
+
+ public function formatRow( $row ) {
+ return self::formatRestriction( UserRestriction::newFromRow( $row ) );
+ }
+
+ // Split off for use on Special:RestrictUser
+ public static function formatRestriction( $r ) {
+ global $wgUser, $wgLang;
+ $sk = $wgUser->getSkin();
+ $timestamp = $wgLang->timeanddate( $r->getTimestamp(), true );
+ $blockerlink = $sk->userLink( $r->getBlockerId(), $r->getBlockerText() ) .
+ $sk->userToolLinks( $r->getBlockerId(), $r->getBlockerText() );
+ $subjlink = $sk->userLink( $r->getSubjectId(), $r->getSubjectText() ) .
+ $sk->userToolLinks( $r->getSubjectId(), $r->getSubjectText() );
+ $expiry = is_numeric( $r->getExpiry() ) ?
+ wfMsg( 'listuserrestrictions-row-expiry', $wgLang->timeanddate( $r->getExpiry() ) ) :
+ wfMsg( 'ipbinfinite' );
+ $msg = '';
+ if( $r->isNamespace() ) {
+ $msg = wfMsgHtml( 'listuserrestrictions-row-ns', $subjlink,
+ $wgLang->getDisplayNsText( $r->getNamespace() ), $expiry );
+ }
+ if( $r->isPage() ) {
+ $pagelink = $sk->link( $r->getPage() );
+ $msg = wfMsgHtml( 'listuserrestrictions-row-page', $subjlink,
+ $pagelink, $expiry );
+ }
+ $reason = $sk->commentBlock( $r->getReason() );
+ $removelink = '';
+ if( $wgUser->isAllowed( 'restrict' ) ) {
+ $removelink = '(' . $sk->link( SpecialPage::getTitleFor( 'RemoveRestrictions' ),
+ wfMsgHtml( 'listuserrestrictions-remove' ), array(), array( 'id' => $r->getId() ) ) . ')';
+ }
+ return "<li>{$timestamp}, {$blockerlink} {$msg} {$reason} {$removelink}</li>\n";
+ }
+
+ public function getIndexField() {
+ return 'ur_timestamp';
+ }
+}
diff --git a/includes/specials/SpecialImagelist.php b/includes/specials/SpecialListfiles.php
index 3d449b54..d2178ee0 100644
--- a/includes/specials/SpecialImagelist.php
+++ b/includes/specials/SpecialListfiles.php
@@ -7,7 +7,7 @@
/**
*
*/
-function wfSpecialImagelist() {
+function wfSpecialListfiles() {
global $wgOut;
$pager = new ImageListPager;
@@ -49,13 +49,17 @@ class ImageListPager extends TablePager {
function getFieldNames() {
if ( !$this->mFieldNames ) {
+ global $wgMiserMode;
$this->mFieldNames = array(
- 'img_timestamp' => wfMsg( 'imagelist_date' ),
- 'img_name' => wfMsg( 'imagelist_name' ),
- 'img_user_text' => wfMsg( 'imagelist_user' ),
- 'img_size' => wfMsg( 'imagelist_size' ),
- 'img_description' => wfMsg( 'imagelist_description' ),
+ 'img_timestamp' => wfMsg( 'listfiles_date' ),
+ 'img_name' => wfMsg( 'listfiles_name' ),
+ 'img_user_text' => wfMsg( 'listfiles_user' ),
+ 'img_size' => wfMsg( 'listfiles_size' ),
+ 'img_description' => wfMsg( 'listfiles_description' ),
);
+ if( !$wgMiserMode ) {
+ $this->mFieldNames['COUNT(oi_archive_name)'] = wfMsg( 'listfiles_count' );
+ }
}
return $this->mFieldNames;
}
@@ -66,13 +70,22 @@ class ImageListPager extends TablePager {
}
function getQueryInfo() {
- $fields = $this->getFieldNames();
- $fields = array_keys( $fields );
+ $tables = array( 'image' );
+ $fields = array_keys( $this->getFieldNames() );
$fields[] = 'img_user';
+ $options = $join_conds = array();
+ # Depends on $wgMiserMode
+ if( isset($this->mFieldNames['COUNT(oi_archive_name)']) ) {
+ $tables[] = 'oldimage';
+ $options = array('GROUP BY' => 'img_name');
+ $join_conds = array('oldimage' => array('LEFT JOIN','oi_name = img_name') );
+ }
return array(
- 'tables' => 'image',
- 'fields' => $fields,
- 'conds' => $this->mQueryConds
+ 'tables' => $tables,
+ 'fields' => $fields,
+ 'conds' => $this->mQueryConds,
+ 'options' => $options,
+ 'join_conds' => $join_conds
);
}
@@ -106,7 +119,7 @@ class ImageListPager extends TablePager {
if ( $imgfile === null ) $imgfile = wfMsg( 'imgfile' );
$name = $this->mCurrentRow->img_name;
- $link = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ), $value );
+ $link = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_FILE, $name ), $value );
$image = wfLocalFile( $value );
$url = $image->getURL();
$download = Xml::element('a', array( 'href' => $url ), $imgfile );
@@ -123,6 +136,8 @@ class ImageListPager extends TablePager {
return $this->getSkin()->formatSize( $value );
case 'img_description':
return $this->getSkin()->commentBlock( $value );
+ case 'COUNT(oi_archive_name)':
+ return intval($value)+1;
}
}
@@ -130,14 +145,14 @@ class ImageListPager extends TablePager {
global $wgRequest, $wgMiserMode;
$search = $wgRequest->getText( 'ilsearch' );
- $s = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getTitle()->getLocalURL(), 'id' => 'mw-imagelist-form' ) ) .
+ $s = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getTitle()->getLocalURL(), 'id' => 'mw-listfiles-form' ) ) .
Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, wfMsg( 'imagelist' ) ) .
+ Xml::element( 'legend', null, wfMsg( 'listfiles' ) ) .
Xml::tags( 'label', null, wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) );
if ( !$wgMiserMode ) {
$s .= "<br />\n" .
- Xml::inputLabel( wfMsg( 'imagelist_search_for' ), 'ilsearch', 'mw-ilsearch', 20, $search );
+ Xml::inputLabel( wfMsg( 'listfiles_search_for' ), 'ilsearch', 'mw-ilsearch', 20, $search );
}
$s .= ' ' .
Xml::submitButton( wfMsg( 'table_pager_limit_submit' ) ) ."\n" .
@@ -148,14 +163,14 @@ class ImageListPager extends TablePager {
}
function getTableClass() {
- return 'imagelist ' . parent::getTableClass();
+ return 'listfiles ' . parent::getTableClass();
}
function getNavClass() {
- return 'imagelist_nav ' . parent::getNavClass();
+ return 'listfiles_nav ' . parent::getNavClass();
}
function getSortHeaderClass() {
- return 'imagelist_sort ' . parent::getSortHeaderClass();
+ return 'listfiles_sort ' . parent::getSortHeaderClass();
}
}
diff --git a/includes/specials/SpecialListgrouprights.php b/includes/specials/SpecialListgrouprights.php
index 131c0606..5c76df8c 100644
--- a/includes/specials/SpecialListgrouprights.php
+++ b/includes/specials/SpecialListgrouprights.php
@@ -24,7 +24,8 @@ class SpecialListGroupRights extends SpecialPage {
* Show the special page
*/
public function execute( $par ) {
- global $wgOut, $wgGroupPermissions, $wgImplicitGroups, $wgMessageCache;
+ global $wgOut, $wgImplicitGroups, $wgMessageCache;
+ global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups;
$wgMessageCache->loadAllMessages();
$this->setHeaders();
@@ -69,13 +70,16 @@ class SpecialListGroupRights extends SpecialPage {
$grouplink = '';
}
+ $addgroups = isset( $wgAddGroups[$group] ) ? $wgAddGroups[$group] : array();
+ $removegroups = isset( $wgRemoveGroups[$group] ) ? $wgRemoveGroups[$group] : array();
+
$wgOut->addHTML(
'<tr>
<td>' .
$grouppage . $grouplink .
'</td>
<td>' .
- self::formatPermissions( $permissions ) .
+ self::formatPermissions( $permissions, $addgroups, $removegroups ) .
'</td>
</tr>'
);
@@ -91,18 +95,29 @@ class SpecialListGroupRights extends SpecialPage {
* @param $permissions Array of permission => bool (from $wgGroupPermissions items)
* @return string List of all granted permissions, separated by comma separator
*/
- private static function formatPermissions( $permissions ) {
+ private static function formatPermissions( $permissions, $add, $remove ) {
+ global $wgLang;
$r = array();
foreach( $permissions as $permission => $granted ) {
if ( $granted ) {
- $description = wfMsgHTML( 'listgrouprights-right-display',
- User::getRightDescription($permission),
+ $description = wfMsgExt( 'listgrouprights-right-display', array( 'parseinline' ),
+ User::getRightDescription( $permission ),
$permission
);
$r[] = $description;
}
}
sort( $r );
+ if( $add === true ){
+ $r[] = wfMsgExt( 'listgrouprights-addgroup-all', array( 'escape' ) );
+ } else if( is_array( $add ) && count( $add ) ) {
+ $r[] = wfMsgExt( 'listgrouprights-addgroup', array( 'parseinline' ), $wgLang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $add ) ), count( $add ) );
+ }
+ if( $remove === true ){
+ $r[] = wfMsgExt( 'listgrouprights-removegroup-all', array( 'escape' ) );
+ } else if( is_array( $remove ) && count( $remove ) ) {
+ $r[] = wfMsgExt( 'listgrouprights-removegroup', array( 'parseinline' ), $wgLang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $remove ) ), count( $remove ) );
+ }
if( empty( $r ) ) {
return '';
} else {
diff --git a/includes/specials/SpecialListredirects.php b/includes/specials/SpecialListredirects.php
index 808aab14..9555bd16 100644
--- a/includes/specials/SpecialListredirects.php
+++ b/includes/specials/SpecialListredirects.php
@@ -22,7 +22,8 @@ class ListredirectsPage extends QueryPage {
function getSQL() {
$dbr = wfGetDB( DB_SLAVE );
$page = $dbr->tableName( 'page' );
- $sql = "SELECT 'Listredirects' AS type, page_title AS title, page_namespace AS namespace, 0 AS value FROM $page WHERE page_is_redirect = 1";
+ $sql = "SELECT 'Listredirects' AS type, page_title AS title, page_namespace AS namespace,
+ 0 AS value FROM $page WHERE page_is_redirect = 1";
return( $sql );
}
diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php
index 7dba44e2..17bec70e 100644
--- a/includes/specials/SpecialListusers.php
+++ b/includes/specials/SpecialListusers.php
@@ -35,10 +35,25 @@
*/
class UsersPager extends AlphabeticPager {
- function __construct($group=null) {
+ function __construct( $par=null ) {
global $wgRequest;
- $this->requestedGroup = $group != "" ? $group : $wgRequest->getVal( 'group' );
- $un = $wgRequest->getText( 'username' );
+ $parms = explode( '/', ($par = ( $par !== null ) ? $par : '' ) );
+ $symsForAll = array( '*', 'user' );
+ if ( $parms[0] != '' && ( in_array( $par, User::getAllGroups() ) || in_array( $par, $symsForAll ) ) ) {
+ $this->requestedGroup = $par;
+ $un = $wgRequest->getText( 'username' );
+ } else if ( count( $parms ) == 2 ) {
+ $this->requestedGroup = $parms[0];
+ $un = $parms[1];
+ } else {
+ $this->requestedGroup = $wgRequest->getVal( 'group' );
+ $un = ( $par != '' ) ? $par : $wgRequest->getText( 'username' );
+ }
+ if ( in_array( $this->requestedGroup, $symsForAll ) ) {
+ $this->requestedGroup = '';
+ }
+ $this->editsOnly = $wgRequest->getBool( 'editsOnly' );
+
$this->requestedUser = '';
if ( $un != '' ) {
$username = Title::makeTitleSafe( NS_USER, $un );
@@ -56,9 +71,9 @@ class UsersPager extends AlphabeticPager {
function getQueryInfo() {
$dbr = wfGetDB( DB_SLAVE );
- $conds=array();
- // don't show hidden names
- $conds[]='ipb_deleted IS NULL OR ipb_deleted = 0';
+ $conds = array();
+ // Don't show hidden names
+ $conds[] = 'ipb_deleted IS NULL OR ipb_deleted = 0';
if ($this->requestedGroup != "") {
$conds['ug_group'] = $this->requestedGroup;
$useIndex = '';
@@ -68,6 +83,9 @@ class UsersPager extends AlphabeticPager {
if ($this->requestedUser != "") {
$conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser );
}
+ if( $this->editsOnly ) {
+ $conds[] = 'user_editcount > 0';
+ }
list ($user,$user_groups,$ipblocks) = $dbr->tableNamesN('user','user_groups','ipblocks');
@@ -76,6 +94,7 @@ class UsersPager extends AlphabeticPager {
LEFT JOIN $ipblocks ON user_id=ipb_user AND ipb_auto=0 ",
'fields' => array('user_name',
'MAX(user_id) AS user_id',
+ 'MAX(user_editcount) AS edits',
'COUNT(ug_group) AS numgroups',
'MAX(ug_group) AS singlegroup'),
'options' => array('GROUP BY' => 'user_name'),
@@ -87,6 +106,8 @@ class UsersPager extends AlphabeticPager {
}
function formatRow( $row ) {
+ global $wgLang;
+
$userPage = Title::makeTitle( NS_USER, $row->user_name );
$name = $this->getSkin()->makeLinkObj( $userPage, htmlspecialchars( $userPage->getText() ) );
@@ -102,18 +123,24 @@ class UsersPager extends AlphabeticPager {
}
$item = wfSpecialList( $name, $groups );
+
+ global $wgEdititis;
+ if ( $wgEdititis ) {
+ $editCount = $wgLang->formatNum( $row->edits );
+ $edits = ' [' . wfMsgExt( 'usereditcount', 'parsemag', $editCount ) . ']';
+ } else {
+ $edits = '';
+ }
wfRunHooks( 'SpecialListusersFormatRow', array( &$item, $row ) );
- return "<li>{$item}</li>";
+ return "<li>{$item}{$edits}</li>";
}
function getBody() {
- if (!$this->mQueryDone) {
+ if( !$this->mQueryDone ) {
$this->doQuery();
}
- $batch = new LinkBatch;
-
$this->mResult->rewind();
-
+ $batch = new LinkBatch;
while ( $row = $this->mResult->fetchObject() ) {
$batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
}
@@ -142,7 +169,9 @@ class UsersPager extends AlphabeticPager {
Xml::option( wfMsg( 'group-all' ), '' );
foreach( $this->getAllGroups() as $group => $groupText )
$out .= Xml::option( $groupText, $group, $group == $this->requestedGroup );
- $out .= Xml::closeElement( 'select' ) . ' ';
+ $out .= Xml::closeElement( 'select' ) . '<br/>';
+ $out .= Xml::checkLabel( wfMsg('listusers-editsonly'), 'editsOnly', 'editsOnly', $this->editsOnly );
+ $out .= '&nbsp;';
wfRunHooks( 'SpecialListusersHeaderForm', array( $this, &$out ) );
@@ -186,14 +215,8 @@ class UsersPager extends AlphabeticPager {
* @return array
*/
protected static function getGroups( $uid ) {
- $dbr = wfGetDB( DB_SLAVE );
- $groups = array();
- $res = $dbr->select( 'user_groups', 'ug_group', array( 'ug_user' => $uid ), __METHOD__ );
- if( $res && $dbr->numRows( $res ) > 0 ) {
- while( $row = $dbr->fetchObject( $res ) )
- $groups[] = $row->ug_group;
- $dbr->freeResult( $res );
- }
+ $user = User::newFromId( $uid );
+ $groups = array_diff( $user->getEffectiveGroups(), $user->getImplicitGroups() );
return $groups;
}
@@ -222,7 +245,8 @@ function wfSpecialListusers( $par = null ) {
# getBody() first to check, if empty
$usersbody = $up->getBody();
- $s = $up->getPageHeader();
+ $s = XML::openElement( 'div', array('class' => 'mw-spcontent') );
+ $s .= $up->getPageHeader();
if( $usersbody ) {
$s .= $up->getNavigationBar();
$s .= '<ul>' . $usersbody . '</ul>';
@@ -230,6 +254,6 @@ function wfSpecialListusers( $par = null ) {
} else {
$s .= '<p>' . wfMsgHTML('listusers-noresult') . '</p>';
};
-
+ $s .= XML::closeElement( 'div' );
$wgOut->addHTML( $s );
}
diff --git a/includes/specials/SpecialLockdb.php b/includes/specials/SpecialLockdb.php
index 04019223..5859d5b2 100644
--- a/includes/specials/SpecialLockdb.php
+++ b/includes/specials/SpecialLockdb.php
@@ -109,7 +109,7 @@ END
}
fwrite( $fp, $this->reason );
fwrite( $fp, "\n<p>(by " . $wgUser->getName() . " at " .
- $wgLang->timeanddate( wfTimestampNow() ) . ")\n" );
+ $wgLang->timeanddate( wfTimestampNow() ) . ")</p>\n" );
fclose( $fp );
$titleObj = SpecialPage::getTitleFor( 'Lockdb' );
diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php
index 3154ed13..492c2608 100644
--- a/includes/specials/SpecialLog.php
+++ b/includes/specials/SpecialLog.php
@@ -26,10 +26,21 @@
* constructor
*/
function wfSpecialLog( $par = '' ) {
- global $wgRequest, $wgOut, $wgUser;
+ global $wgRequest, $wgOut, $wgUser, $wgLogTypes;
+
# Get parameters
- $type = $wgRequest->getVal( 'type', $par );
- $user = $wgRequest->getText( 'user' );
+ $parms = explode( '/', ($par = ( $par !== null ) ? $par : '' ) );
+ $symsForAll = array( '*', 'all' );
+ if ( $parms[0] != '' && ( in_array( $par, $wgLogTypes ) || in_array( $par, $symsForAll ) ) ) {
+ $type = $par;
+ $user = $wgRequest->getText( 'user' );
+ } else if ( count( $parms ) == 2 ) {
+ $type = $parms[0];
+ $user = $parms[1];
+ } else {
+ $type = $wgRequest->getVal( 'type' );
+ $user = ( $par != '' ) ? $par : $wgRequest->getText( 'user' );
+ }
$title = $wgRequest->getText( 'page' );
$pattern = $wgRequest->getBool( 'pattern' );
$y = $wgRequest->getIntOrNull( 'year' );
@@ -40,15 +51,14 @@ function wfSpecialLog( $par = '' ) {
$y = '';
$m = '';
}
- # Create a LogPager item to get the results and a LogEventsList
- # item to format them...
+ # Create a LogPager item to get the results and a LogEventsList item to format them...
$loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 );
$pager = new LogPager( $loglist, $type, $user, $title, $pattern, array(), $y, $m );
# Set title and add header
$loglist->showHeader( $pager->getType() );
# Show form options
$loglist->showOptions( $pager->getType(), $pager->getUser(), $pager->getPage(), $pager->getPattern(),
- $pager->getYear(), $pager->getMonth() );
+ $pager->getYear(), $pager->getMonth(), $pager->getFilterParams() );
# Insert list
$logBody = $pager->getBody();
if( $logBody ) {
diff --git a/includes/specials/SpecialLonelypages.php b/includes/specials/SpecialLonelypages.php
index 5aafac7d..90da25fd 100644
--- a/includes/specials/SpecialLonelypages.php
+++ b/includes/specials/SpecialLonelypages.php
@@ -29,7 +29,7 @@ class LonelyPagesPage extends PageQueryPage {
function getSQL() {
$dbr = wfGetDB( DB_SLAVE );
- list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' );
+ list( $page, $pagelinks, $templatelinks ) = $dbr->tableNamesN( 'page', 'pagelinks', 'templatelinks' );
return
"SELECT 'Lonelypages' AS type,
@@ -39,9 +39,12 @@ class LonelyPagesPage extends PageQueryPage {
FROM $page
LEFT JOIN $pagelinks
ON page_namespace=pl_namespace AND page_title=pl_title
+ LEFT JOIN $templatelinks
+ ON page_namespace=tl_namespace AND page_title=tl_title
WHERE pl_namespace IS NULL
AND page_namespace=".NS_MAIN."
- AND page_is_redirect=0";
+ AND page_is_redirect=0
+ AND tl_namespace IS NULL";
}
}
diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php
index 82ee4be6..cdfde24e 100644
--- a/includes/specials/SpecialMIMEsearch.php
+++ b/includes/specials/SpecialMIMEsearch.php
@@ -46,7 +46,7 @@ class MIMEsearchPage extends QueryPage {
return
"SELECT 'MIMEsearch' AS type,
- " . NS_IMAGE . " AS namespace,
+ " . NS_FILE . " AS namespace,
img_name AS title,
img_major_mime AS value,
diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php
index 0460c207..f870406c 100644
--- a/includes/specials/SpecialMergeHistory.php
+++ b/includes/specials/SpecialMergeHistory.php
@@ -96,8 +96,10 @@ class MergehistoryForm {
wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
);
}
-
- // TODO: warn about target = dest?
+
+ if ( $this->mTargetObj->equals( $this->mDestObj ) ) {
+ $errors[] = wfMsgExt( 'mergehistory-same-destination', array( 'parse' ) );
+ }
if ( count( $errors ) ) {
$this->showMergeForm();
@@ -113,7 +115,7 @@ class MergehistoryForm {
$wgOut->addWikiMsg( 'mergehistory-header' );
- $wgOut->addHtml(
+ $wgOut->addHTML(
Xml::openElement( 'form', array(
'method' => 'get',
'action' => $wgScript ) ) .
@@ -156,7 +158,7 @@ class MergehistoryForm {
$action = $titleObj->getLocalURL( "action=submit" );
# Start the form here
$top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'merge' ) );
- $wgOut->addHtml( $top );
+ $wgOut->addHTML( $top );
if( $haveRevisions ) {
# Format the user-visible controls (comment field, submission button)
@@ -188,7 +190,7 @@ class MergehistoryForm {
Xml::closeElement( 'table' ) .
Xml::closeElement( 'fieldset' );
- $wgOut->addHtml( $table );
+ $wgOut->addHTML( $table );
}
$wgOut->addHTML( "<h2 id=\"mw-mergehistory\">" . wfMsgHtml( "mergehistory-list" ) . "</h2>\n" );
@@ -215,7 +217,7 @@ class MergehistoryForm {
$misc .= Xml::hidden( 'dest', $this->mDest );
$misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
$misc .= Xml::closeElement( 'form' );
- $wgOut->addHtml( $misc );
+ $wgOut->addHTML( $misc );
return true;
}
@@ -229,7 +231,7 @@ class MergehistoryForm {
$last = $this->message['last'];
$ts = wfTimestamp( TS_MW, $row->rev_timestamp );
- $checkBox = wfRadio( "mergepoint", $ts, false );
+ $checkBox = Xml::radio( "mergepoint", $ts, false );
$pageLink = $this->sk->makeKnownLinkObj( $rev->getTitle(),
htmlspecialchars( $wgLang->timeanddate( $ts ) ), 'oldid=' . $rev->getId() );
@@ -370,7 +372,7 @@ class MergehistoryForm {
$log->addEntry( 'merge', $targetTitle, $this->mComment,
array($destTitle->getPrefixedText(),$TimestampLimit) );
- $wgOut->addHtml( wfMsgExt( 'mergehistory-success', array('parseinline'),
+ $wgOut->addHTML( wfMsgExt( 'mergehistory-success', array('parseinline'),
$targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count ) );
wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) );
@@ -432,10 +434,10 @@ class MergeHistoryPager extends ReverseChronologicalPager {
function getQueryInfo() {
$conds = $this->mConds;
$conds['rev_page'] = $this->articleID;
+ $conds[] = 'page_id = rev_page';
$conds[] = "rev_timestamp < {$this->maxTimestamp}";
-
return array(
- 'tables' => array('revision'),
+ 'tables' => array('revision','page'),
'fields' => array( 'rev_minor_edit', 'rev_timestamp', 'rev_user', 'rev_user_text', 'rev_comment',
'rev_id', 'rev_page', 'rev_text_id', 'rev_len', 'rev_deleted' ),
'conds' => $conds
diff --git a/includes/specials/SpecialMostcategories.php b/includes/specials/SpecialMostcategories.php
index e6810999..1ba05626 100644
--- a/includes/specials/SpecialMostcategories.php
+++ b/includes/specials/SpecialMostcategories.php
@@ -39,9 +39,9 @@ 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() );
+ $link = $skin->link( $title );
return wfSpecialList( $link, $count );
}
}
diff --git a/includes/specials/SpecialMostimages.php b/includes/specials/SpecialMostimages.php
index 6cfeb7ad..5cc100ba 100644
--- a/includes/specials/SpecialMostimages.php
+++ b/includes/specials/SpecialMostimages.php
@@ -25,7 +25,7 @@ class MostimagesPage extends ImageQueryPage {
"
SELECT
'Mostimages' as type,
- " . NS_IMAGE . " as namespace,
+ " . NS_FILE . " as namespace,
il_to as title,
COUNT(*) as value
FROM $imagelinks
diff --git a/includes/specials/SpecialMostlinkedtemplates.php b/includes/specials/SpecialMostlinkedtemplates.php
index d597a4e0..2d398a38 100644
--- a/includes/specials/SpecialMostlinkedtemplates.php
+++ b/includes/specials/SpecialMostlinkedtemplates.php
@@ -92,15 +92,12 @@ class SpecialMostlinkedtemplates extends QueryPage {
*/
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}";
- }
+
+ $skin->link( $title );
+ return wfSpecialList(
+ $skin->makeLinkObj( $title ),
+ $this->makeWlhLink( $title, $skin, $result )
+ );
}
/**
@@ -115,8 +112,8 @@ class SpecialMostlinkedtemplates extends QueryPage {
global $wgLang;
$wlh = SpecialPage::getTitleFor( 'Whatlinkshere' );
$label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
- $wgLang->formatNum( $result->value ) );
- return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() );
+ $wgLang->formatNum( $result->value ) );
+ return $skin->link( $wlh, $label, array(), array( 'target' => $title->getPrefixedText() ) );
}
}
diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php
index efd2dcfd..acc27625 100644
--- a/includes/specials/SpecialMovepage.php
+++ b/includes/specials/SpecialMovepage.php
@@ -54,12 +54,13 @@ function wfSpecialMovepage( $par = null ) {
* @ingroup SpecialPage
*/
class MovePageForm {
- var $oldTitle, $newTitle, $reason; # Text input
- var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects;
+ var $oldTitle, $newTitle; # Objects
+ var $reason; # Text input
+ var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects, $leaveRedirect; # Checks
private $watch = false;
- function MovePageForm( $oldTitle, $newTitle ) {
+ function __construct( $oldTitle, $newTitle ) {
global $wgRequest;
$target = isset($par) ? $par : $wgRequest->getVal( 'target' );
$this->oldTitle = $oldTitle;
@@ -68,48 +69,54 @@ class MovePageForm {
if ( $wgRequest->wasPosted() ) {
$this->moveTalk = $wgRequest->getBool( 'wpMovetalk', false );
$this->fixRedirects = $wgRequest->getBool( 'wpFixRedirects', false );
+ $this->leaveRedirect = $wgRequest->getBool( 'wpLeaveRedirect', false );
} else {
$this->moveTalk = $wgRequest->getBool( 'wpMovetalk', true );
$this->fixRedirects = $wgRequest->getBool( 'wpFixRedirects', true );
+ $this->leaveRedirect = $wgRequest->getBool( 'wpLeaveRedirect', true );
}
$this->moveSubpages = $wgRequest->getBool( 'wpMovesubpages', false );
$this->deleteAndMove = $wgRequest->getBool( 'wpDeleteAndMove' ) && $wgRequest->getBool( 'wpConfirm' );
$this->watch = $wgRequest->getCheck( 'wpWatch' );
}
- function showForm( $err, $hookErr = '' ) {
- global $wgOut, $wgUser;
+ /**
+ * Show the form
+ * @param mixed $err Error message. May either be a string message name or
+ * array message name and parameters, like the second argument to
+ * OutputPage::wrapWikiMsg().
+ */
+ function showForm( $err ) {
+ global $wgOut, $wgUser, $wgFixDoubleRedirects;
$skin = $wgUser->getSkin();
$oldTitleLink = $skin->makeLinkObj( $this->oldTitle );
- $oldTitle = $this->oldTitle->getPrefixedText();
- $wgOut->setPagetitle( wfMsg( 'move-page', $oldTitle ) );
+ $wgOut->setPagetitle( wfMsg( 'move-page', $this->oldTitle->getPrefixedText() ) );
$wgOut->setSubtitle( wfMsg( 'move-page-backlink', $oldTitleLink ) );
- if( $this->newTitle == '' ) {
+ $newTitle = $this->newTitle;
+
+ if( !$newTitle ) {
# Show the current title as a default
# when the form is first opened.
- $newTitle = $oldTitle;
- } else {
- if( $err == '' ) {
- $nt = Title::newFromURL( $this->newTitle );
- if( $nt ) {
- # If a title was supplied, probably from the move log revert
- # link, check for validity. We can then show some diagnostic
- # information and save a click.
- $newerr = $this->oldTitle->isValidMoveOperation( $nt );
- if( is_string( $newerr ) ) {
- $err = $newerr;
- }
+ $newTitle = $this->oldTitle;
+ }
+ else {
+ if( empty($err) ) {
+ # If a title was supplied, probably from the move log revert
+ # link, check for validity. We can then show some diagnostic
+ # information and save a click.
+ $newerr = $this->oldTitle->isValidMoveOperation( $newTitle );
+ if( $newerr ) {
+ $err = $newerr[0];
}
}
- $newTitle = $this->newTitle;
}
- if ( $err == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) {
- $wgOut->addWikiMsg( 'delete_and_move_text', $newTitle );
+ if ( !empty($err) && $err[0] == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) {
+ $wgOut->addWikiMsg( 'delete_and_move_text', $newTitle->getPrefixedText() );
$movepagebtn = wfMsg( 'delete_and_move' );
$submitVar = 'wpDeleteAndMove';
$confirm = "
@@ -131,12 +138,16 @@ class MovePageForm {
$considerTalk = ( !$this->oldTitle->isTalkPage() && $oldTalk->exists() );
$dbr = wfGetDB( DB_SLAVE );
- $hasRedirects = $dbr->selectField( 'redirect', '1',
- array(
- 'rd_namespace' => $this->oldTitle->getNamespace(),
- 'rd_title' => $this->oldTitle->getDBkey(),
- ) , __METHOD__ );
-
+ if ( $wgFixDoubleRedirects ) {
+ $hasRedirects = $dbr->selectField( 'redirect', '1',
+ array(
+ 'rd_namespace' => $this->oldTitle->getNamespace(),
+ 'rd_title' => $this->oldTitle->getDBkey(),
+ ) , __METHOD__ );
+ } else {
+ $hasRedirects = false;
+ }
+
if ( $considerTalk ) {
$wgOut->addWikiMsg( 'movepagetalktext' );
}
@@ -144,9 +155,10 @@ class MovePageForm {
$titleObj = SpecialPage::getTitleFor( 'Movepage' );
$token = htmlspecialchars( $wgUser->editToken() );
- if ( $err != '' ) {
+ if ( !empty($err) ) {
$wgOut->setSubtitle( wfMsg( 'formerror' ) );
- if( $err == 'hookaborted' ) {
+ if( $err[0] == 'hookaborted' ) {
+ $hookErr = $err[1];
$errMsg = "<p><strong class=\"error\">$hookErr</strong></p>\n";
$wgOut->addHTML( $errMsg );
} else {
@@ -172,8 +184,8 @@ class MovePageForm {
Xml::label( wfMsg( 'newtitle' ), 'wpNewTitle' ) .
"</td>
<td class='mw-input'>" .
- Xml::input( 'wpNewTitle', 40, $newTitle, array( 'type' => 'text', 'id' => 'wpNewTitle' ) ) .
- Xml::hidden( 'wpOldTitle', $oldTitle ) .
+ Xml::input( 'wpNewTitle', 40, $newTitle->getPrefixedText(), array( 'type' => 'text', 'id' => 'wpNewTitle' ) ) .
+ Xml::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) .
"</td>
</tr>
<tr>
@@ -197,6 +209,18 @@ class MovePageForm {
);
}
+ if ( $wgUser->isAllowed( 'suppressredirect' ) ) {
+ $wgOut->addHTML( "
+ <tr>
+ <td></td>
+ <td class='mw-input' >" .
+ Xml::checkLabel( wfMsg( 'move-leave-redirect' ), 'wpLeaveRedirect',
+ 'wpLeaveRedirect', $this->leaveRedirect ) .
+ "</td>
+ </tr>"
+ );
+ }
+
if ( $hasRedirects ) {
$wgOut->addHTML( "
<tr>
@@ -205,7 +229,7 @@ class MovePageForm {
Xml::checkLabel( wfMsg( 'fix-double-redirects' ), 'wpFixRedirects',
'wpFixRedirects', $this->fixRedirects ) .
"</td>
- </td>"
+ </tr>"
);
}
@@ -215,7 +239,7 @@ class MovePageForm {
<tr>
<td></td>
<td class=\"mw-input\">" .
- Xml::checkLabel( wfMsgHtml(
+ Xml::checkLabel( wfMsg(
$this->oldTitle->hasSubpages()
? 'move-subpages'
: 'move-talk-subpages'
@@ -259,6 +283,7 @@ class MovePageForm {
function doSubmit() {
global $wgOut, $wgUser, $wgRequest, $wgMaximumMovedPages, $wgLang;
+ global $wgFixDoubleRedirects;
if ( $wgUser->pingLimiter( 'move' ) ) {
$wgOut->rateLimited();
@@ -280,6 +305,12 @@ class MovePageForm {
return;
}
+ // Delete an associated image if there is
+ $file = wfLocalFile( $nt );
+ if( $file->exists() ) {
+ $file->delete( wfMsgForContent( 'delete_and_move_reason' ), false );
+ }
+
// This may output an error message and exit
$article->doDelete( wfMsgForContent( 'delete_and_move_reason' ) );
}
@@ -290,14 +321,20 @@ class MovePageForm {
return;
}
- $error = $ot->moveTo( $nt, true, $this->reason );
+ if ( $wgUser->isAllowed( 'suppressredirect' ) ) {
+ $createRedirect = $this->leaveRedirect;
+ } else {
+ $createRedirect = true;
+ }
+
+ $error = $ot->moveTo( $nt, true, $this->reason, $createRedirect );
if ( $error !== true ) {
- # FIXME: showForm() should handle multiple errors
- call_user_func_array(array($this, 'showForm'), $error[0]);
+ # FIXME: show all the errors in a list, not just the first one
+ $this->showForm( reset( $error ) );
return;
}
- if ( $this->fixRedirects ) {
+ if ( $wgFixDoubleRedirects && $this->fixRedirects ) {
DoubleRedirectJob::fixRedirects( 'move', $ot, $nt );
}
@@ -312,7 +349,9 @@ class MovePageForm {
$oldLink = "<span class='plainlinks'>[$oldUrl $oldText]</span>";
$newLink = "<span class='plainlinks'>[$newUrl $newText]</span>";
+ $msgName = $createRedirect ? 'movepage-moved-redirect' : 'movepage-moved-noredirect';
$wgOut->addWikiMsg( 'movepage-moved', $oldLink, $newLink, $oldText, $newText );
+ $wgOut->addWikiMsg( $msgName );
# Now we move extra pages we've been asked to move: subpages and talk
# pages. First, if the old page or the new page is a talk page, we
@@ -364,25 +403,26 @@ class MovePageForm {
$conds = null;
}
- $extrapages = array();
+ $extraPages = array();
if( !is_null( $conds ) ) {
- $extrapages = $dbr->select( 'page',
- array( 'page_id', 'page_namespace', 'page_title' ),
- $conds,
- __METHOD__
+ $extraPages = TitleArray::newFromResult(
+ $dbr->select( 'page',
+ array( 'page_id', 'page_namespace', 'page_title' ),
+ $conds,
+ __METHOD__
+ )
);
}
$extraOutput = array();
$skin = $wgUser->getSkin();
$count = 1;
- foreach( $extrapages as $row ) {
- if( $row->page_id == $ot->getArticleId() ) {
+ foreach( $extraPages as $oldSubpage ) {
+ if( $oldSubpage->getArticleId() == $ot->getArticleId() ) {
# Already did this one.
continue;
}
- $oldSubpage = Title::newFromRow( $row );
$newPageName = preg_replace(
'#^'.preg_quote( $ot->getDBKey(), '#' ).'#',
$nt->getDBKey(),
@@ -408,7 +448,7 @@ class MovePageForm {
$link = $skin->makeKnownLinkObj( $newSubpage );
$extraOutput []= wfMsgHtml( 'movepage-page-exists', $link );
} else {
- $success = $oldSubpage->moveTo( $newSubpage, true, $this->reason );
+ $success = $oldSubpage->moveTo( $newSubpage, true, $this->reason, $createRedirect );
if( $success === true ) {
if ( $this->fixRedirects ) {
DoubleRedirectJob::fixRedirects( 'move', $oldSubpage, $newSubpage );
diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php
index e57f6fc1..575e37a7 100644
--- a/includes/specials/SpecialNewimages.php
+++ b/includes/specials/SpecialNewimages.php
@@ -5,66 +5,56 @@
* FIXME: this code is crap, should use Pager and Database::select().
*/
-/**
- *
- */
function wfSpecialNewimages( $par, $specialPage ) {
- global $wgUser, $wgOut, $wgLang, $wgRequest, $wgGroupPermissions, $wgMiserMode;
+ global $wgUser, $wgOut, $wgLang, $wgRequest, $wgMiserMode;
$wpIlMatch = $wgRequest->getText( 'wpIlMatch' );
$dbr = wfGetDB( DB_SLAVE );
$sk = $wgUser->getSkin();
$shownav = !$specialPage->including();
- $hidebots = $wgRequest->getBool('hidebots',1);
+ $hidebots = $wgRequest->getBool( 'hidebots' , 1 );
$hidebotsql = '';
- if ($hidebots) {
-
- /** Make a list of group names which have the 'bot' flag
- set.
- */
- $botconds=array();
- foreach ($wgGroupPermissions as $groupname=>$perms) {
- if(array_key_exists('bot',$perms) && $perms['bot']) {
- $botconds[]="ug_group='$groupname'";
- }
+ if ( $hidebots ) {
+ # Make a list of group names which have the 'bot' flag set.
+ $botconds = array();
+ foreach ( User::getGroupsWithPermission('bot') as $groupname ) {
+ $botconds[] = 'ug_group = ' . $dbr->addQuotes( $groupname );
}
- /* If not bot groups, do not set $hidebotsql */
- if ($botconds) {
- $isbotmember=$dbr->makeList($botconds, LIST_OR);
-
- /** This join, in conjunction with WHERE ug_group
- IS NULL, returns only those rows from IMAGE
- where the uploading user is not a member of
- a group which has the 'bot' permission set.
- */
- $ug = $dbr->tableName('user_groups');
- $hidebotsql = " LEFT OUTER JOIN $ug ON img_user=ug_user AND ($isbotmember)";
+ # If not bot groups, do not set $hidebotsql
+ if ( $botconds ) {
+ $isbotmember = $dbr->makeList( $botconds, LIST_OR );
+
+ # This join, in conjunction with WHERE ug_group IS NULL, returns
+ # only those rows from IMAGE where the uploading user is not a mem-
+ # ber of a group which has the 'bot' permission set.
+ $ug = $dbr->tableName( 'user_groups' );
+ $hidebotsql = " LEFT JOIN $ug ON img_user=ug_user AND ($isbotmember)";
}
}
- $image = $dbr->tableName('image');
+ $image = $dbr->tableName( 'image' );
- $sql="SELECT img_timestamp from $image";
+ $sql = "SELECT img_timestamp from $image";
if ($hidebotsql) {
$sql .= "$hidebotsql WHERE ug_group IS NULL";
}
- $sql.=' ORDER BY img_timestamp DESC LIMIT 1';
- $res = $dbr->query($sql, 'wfSpecialNewImages');
- $row = $dbr->fetchRow($res);
- if($row!==false) {
- $ts=$row[0];
+ $sql .= ' ORDER BY img_timestamp DESC LIMIT 1';
+ $res = $dbr->query( $sql, __FUNCTION__ );
+ $row = $dbr->fetchRow( $res );
+ if( $row !== false ) {
+ $ts = $row[0];
} else {
- $ts=false;
+ $ts = false;
}
- $dbr->freeResult($res);
- $sql='';
+ $dbr->freeResult( $res );
+ $sql = '';
- /** If we were clever, we'd use this to cache. */
- $latestTimestamp = wfTimestamp( TS_MW, $ts);
+ # If we were clever, we'd use this to cache.
+ $latestTimestamp = wfTimestamp( TS_MW, $ts );
- /** Hardcode this for now. */
+ # Hardcode this for now.
$limit = 48;
if ( $parval = intval( $par ) ) {
@@ -77,10 +67,8 @@ function wfSpecialNewimages( $par, $specialPage ) {
$searchpar = '';
if ( $wpIlMatch != '' && !$wgMiserMode) {
$nt = Title::newFromUrl( $wpIlMatch );
- if($nt ) {
- $m = $dbr->strencode( strtolower( $nt->getDBkey() ) );
- $m = str_replace( '%', "\\%", $m );
- $m = str_replace( '_', "\\_", $m );
+ if( $nt ) {
+ $m = $dbr->escapeLike( strtolower( $nt->getDBkey() ) );
$where[] = "LOWER(img_name) LIKE '%{$m}%'";
$searchpar = '&wpIlMatch=' . urlencode( $wpIlMatch );
}
@@ -97,16 +85,16 @@ function wfSpecialNewimages( $par, $specialPage ) {
$sql='SELECT img_size, img_name, img_user, img_user_text,'.
"img_description,img_timestamp FROM $image";
- if($hidebotsql) {
+ if( $hidebotsql ) {
$sql .= $hidebotsql;
- $where[]='ug_group IS NULL';
+ $where[] = 'ug_group IS NULL';
}
- if(count($where)) {
- $sql.=' WHERE '.$dbr->makeList($where, LIST_AND);
+ if( count( $where ) ) {
+ $sql .= ' WHERE ' . $dbr->makeList( $where, LIST_AND );
}
$sql.=' ORDER BY img_timestamp '. ( $invertSort ? '' : ' DESC' );
- $sql.=' LIMIT '.($limit+1);
- $res = $dbr->query($sql, 'wfSpecialNewImages');
+ $sql.=' LIMIT ' . ( $limit + 1 );
+ $res = $dbr->query( $sql, __FUNCTION__ );
/**
* We have to flip things around to get the last N after a certain date
@@ -126,7 +114,8 @@ function wfSpecialNewimages( $par, $specialPage ) {
$lastTimestamp = null;
$shownImages = 0;
foreach( $images as $s ) {
- if( ++$shownImages > $limit ) {
+ $shownImages++;
+ if( $shownImages > $limit ) {
# One extra just to test for whether to show a page link;
# don't actually show it.
break;
@@ -135,7 +124,7 @@ function wfSpecialNewimages( $par, $specialPage ) {
$name = $s->img_name;
$ut = $s->img_user_text;
- $nt = Title::newFromText( $name, NS_IMAGE );
+ $nt = Title::newFromText( $name, NS_FILE );
$ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut );
$gallery->add( $nt, "$ul<br />\n<i>".$wgLang->timeanddate( $s->img_timestamp, true )."</i><br />\n" );
@@ -147,33 +136,35 @@ function wfSpecialNewimages( $par, $specialPage ) {
$lastTimestamp = $timestamp;
}
+ $titleObj = SpecialPage::getTitleFor( 'Newimages' );
+ $action = $titleObj->getLocalURL( $hidebots ? '' : 'hidebots=0' );
+ if ( $shownav && !$wgMiserMode ) {
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'action' => $action, 'method' => 'post', 'id' => 'imagesearch' ) ) .
+ Xml::fieldset( wfMsg( 'newimages-legend' ) ) .
+ Xml::inputLabel( wfMsg( 'newimages-label' ), 'wpIlMatch', 'wpIlMatch', 20, $wpIlMatch ) . ' ' .
+ Xml::submitButton( wfMsg( 'ilsubmit' ), array( 'name' => 'wpIlSubmit' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
+ );
+ }
+
$bydate = wfMsg( 'bydate' );
$lt = $wgLang->formatNum( min( $shownImages, $limit ) );
- if ($shownav) {
+ if ( $shownav ) {
$text = wfMsgExt( 'imagelisttext', array('parse'), $lt, $bydate );
$wgOut->addHTML( $text . "\n" );
}
- $sub = wfMsg( 'ilsubmit' );
- $titleObj = SpecialPage::getTitleFor( 'Newimages' );
- $action = $titleObj->escapeLocalURL( $hidebots ? '' : 'hidebots=0' );
- if ($shownav && !$wgMiserMode) {
- $wgOut->addHTML( "<form id=\"imagesearch\" method=\"post\" action=\"" .
- "{$action}\">" .
- Xml::input( 'wpIlMatch', 20, $wpIlMatch ) . ' ' .
- Xml::submitButton( $sub, array( 'name' => 'wpIlSubmit' ) ) .
- "</form>" );
- }
-
/**
* Paging controls...
*/
# If we change bot visibility, this needs to be carried along.
- if(!$hidebots) {
- $botpar='&hidebots=0';
+ if( !$hidebots ) {
+ $botpar = '&hidebots=0';
} else {
- $botpar='';
+ $botpar = '';
}
$now = wfTimestampNow();
$d = $wgLang->date( $now, true );
@@ -186,12 +177,12 @@ function wfSpecialNewimages( $par, $specialPage ) {
$opts = array( 'parsemag', 'escapenoentities' );
- $prevLink = wfMsgExt( 'prevn', $opts, $wgLang->formatNum( $limit ) );
+ $prevLink = wfMsgExt( 'pager-newer-n', $opts, $wgLang->formatNum( $limit ) );
if( $firstTimestamp && $firstTimestamp != $latestTimestamp ) {
$prevLink = $sk->makeKnownLinkObj( $titleObj, $prevLink, 'from=' . $firstTimestamp . $botpar . $searchpar );
}
- $nextLink = wfMsgExt( 'nextn', $opts, $wgLang->formatNum( $limit ) );
+ $nextLink = wfMsgExt( 'pager-older-n', $opts, $wgLang->formatNum( $limit ) );
if( $shownImages > $limit && $lastTimestamp ) {
$nextLink = $sk->makeKnownLinkObj( $titleObj, $nextLink, 'until=' . $lastTimestamp.$botpar.$searchpar );
}
diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php
index 1a410ae0..08e776d8 100644
--- a/includes/specials/SpecialNewpages.php
+++ b/includes/specials/SpecialNewpages.php
@@ -12,7 +12,7 @@ class SpecialNewpages extends SpecialPage {
// Some internal settings
protected $showNavigation = false;
- public function __construct(){
+ public function __construct() {
parent::__construct( 'Newpages' );
$this->includable( true );
}
@@ -26,7 +26,8 @@ class SpecialNewpages extends SpecialPage {
$opts->add( 'hideliu', false );
$opts->add( 'hidepatrolled', false );
$opts->add( 'hidebots', false );
- $opts->add( 'limit', 50 );
+ $opts->add( 'hideredirs', true );
+ $opts->add( 'limit', (int)$wgUser->getOption( 'rclimit' ) );
$opts->add( 'offset', '' );
$opts->add( 'namespace', '0' );
$opts->add( 'username', '' );
@@ -58,6 +59,8 @@ class SpecialNewpages extends SpecialPage {
$this->opts->setValue( 'hidepatrolled', true );
if ( 'hidebots' == $bit )
$this->opts->setValue( 'hidebots', true );
+ if ( 'showredirs' == $bit )
+ $this->opts->setValue( 'hideredirs', false );
if ( is_numeric( $bit ) )
$this->opts->setValue( 'limit', intval( $bit ) );
@@ -67,6 +70,8 @@ class SpecialNewpages extends SpecialPage {
// PG offsets not just digits!
if ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) )
$this->opts->setValue( 'offset', intval($m[1]) );
+ if ( preg_match( '/^username=(.*)$/', $bit, $m ) )
+ $this->opts->setValue( 'username', $m[1] );
if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
$ns = $wgLang->getNsIndex( $m[1] );
if( $ns !== false ) {
@@ -83,7 +88,7 @@ class SpecialNewpages extends SpecialPage {
* @return string
*/
public function execute( $par ) {
- global $wgLang, $wgGroupPermissions, $wgUser, $wgOut;
+ global $wgLang, $wgUser, $wgOut;
$this->setHeaders();
$this->outputHeader();
@@ -125,7 +130,8 @@ class SpecialNewpages extends SpecialPage {
$filters = array(
'hideliu' => 'rcshowhideliu',
'hidepatrolled' => 'rcshowhidepatr',
- 'hidebots' => 'rcshowhidebots'
+ 'hidebots' => 'rcshowhidebots',
+ 'hideredirs' => 'whatlinkshere-hideredirs'
);
// Disable some if needed
@@ -142,8 +148,8 @@ class SpecialNewpages extends SpecialPage {
$self = $this->getTitle();
foreach ( $filters as $key => $msg ) {
$onoff = 1 - $this->opts->getValue($key);
- $link = $this->skin->makeKnownLinkObj( $self, $showhide[$onoff],
- wfArrayToCGI( array( $key => $onoff ), $changed )
+ $link = $this->skin->link( $self, $showhide[$onoff], array(),
+ array( $key => $onoff ) + $changed
);
$links[$key] = wfMsgHtml( $msg, $link );
}
@@ -231,7 +237,7 @@ class SpecialNewpages extends SpecialPage {
global $wgLang, $wgContLang, $wgUser;
$dm = $wgContLang->getDirMark();
- $title = Title::makeTitleSafe( $result->page_namespace, $result->page_title );
+ $title = Title::makeTitleSafe( $result->rc_namespace, $result->rc_title );
$time = $wgLang->timeAndDate( $result->rc_timestamp, true );
$plink = $this->skin->makeKnownLinkObj( $title, '', $this->patrollable( $result ) ? 'rcid=' . $result->rc_id : '' );
$hist = $this->skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' );
@@ -261,7 +267,7 @@ class SpecialNewpages extends SpecialPage {
* @param string $type
*/
protected function feed( $type ) {
- global $wgFeed, $wgFeedClasses;
+ global $wgFeed, $wgFeedClasses, $wgFeedLimit;
if ( !$wgFeed ) {
global $wgOut;
@@ -277,16 +283,12 @@ class SpecialNewpages extends SpecialPage {
$feed = new $wgFeedClasses[$type](
$this->feedTitle(),
- wfMsg( 'tagline' ),
+ wfMsgExt( 'tagline', 'parsemag' ),
$this->getTitle()->getFullUrl() );
$pager = new NewPagesPager( $this, $this->opts );
$limit = $this->opts->getValue( 'limit' );
- global $wgFeedLimit;
- if( $limit > $wgFeedLimit ) {
- $limit = $wgFeedLimit;
- }
- $pager->mLimit = $limit;
+ $pager->mLimit = min( $limit, $wgFeedLimit );
$feed->outHeader();
if( $pager->getNumRows() > 0 ) {
@@ -305,7 +307,7 @@ class SpecialNewpages extends SpecialPage {
}
protected function feedItem( $row ) {
- $title = Title::MakeTitle( intval( $row->page_namespace ), $row->page_title );
+ $title = Title::MakeTitle( intval( $row->rc_namespace ), $row->rc_title );
if( $title ) {
$date = $row->rc_timestamp;
$comments = $title->getTalkPage()->getFullURL();
@@ -322,13 +324,6 @@ class SpecialNewpages extends SpecialPage {
}
}
- /**
- * Quickie hack... strip out wikilinks to more legible form from the comment.
- */
- protected function stripComment( $text ) {
- return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text );
- }
-
protected function feedItemAuthor( $row ) {
return isset( $row->rc_user_text ) ? $row->rc_user_text : '';
}
@@ -337,7 +332,7 @@ class SpecialNewpages extends SpecialPage {
$revision = Revision::newFromId( $row->rev_id );
if( $revision ) {
return '<p>' . htmlspecialchars( $revision->getUserText() ) . ': ' .
- htmlspecialchars( $revision->getComment() ) .
+ htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) .
"</p>\n<hr />\n<div>" .
nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
}
@@ -352,15 +347,13 @@ class NewPagesPager extends ReverseChronologicalPager {
// Stored opts
protected $opts, $mForm;
- private $hideliu, $hidepatrolled, $hidebots, $namespace, $user, $spTitle;
-
function __construct( $form, FormOptions $opts ) {
parent::__construct();
$this->mForm = $form;
$this->opts = $opts;
}
- function getTitle(){
+ function getTitle() {
static $title = null;
if ( $title === null )
$title = $this->mForm->getTitle();
@@ -379,13 +372,13 @@ class NewPagesPager extends ReverseChronologicalPager {
$user = Title::makeTitleSafe( NS_USER, $username );
if( $namespace !== false ) {
- $conds['page_namespace'] = $namespace;
+ $conds['rc_namespace'] = $namespace;
$rcIndexes = array( 'new_name_timestamp' );
} else {
$rcIndexes = array( 'rc_timestamp' );
}
$conds[] = 'page_id = rc_cur_id';
- $conds['page_is_redirect'] = 0;
+
# $wgEnableNewpagesUserFilter - temp WMF hack
if( $wgEnableNewpagesUserFilter && $user ) {
$conds['rc_user_text'] = $user->getText();
@@ -402,9 +395,13 @@ class NewPagesPager extends ReverseChronologicalPager {
$conds['rc_bot'] = 0;
}
+ if ( $this->opts->getValue( 'hideredirs' ) ) {
+ $conds['page_is_redirect'] = 0;
+ }
+
return array(
'tables' => array( 'recentchanges', 'page' ),
- 'fields' => 'page_namespace,page_title, rc_cur_id, rc_user,rc_user_text,rc_comment,
+ 'fields' => 'rc_namespace,rc_title, rc_cur_id, rc_user,rc_user_text,rc_comment,
rc_timestamp,rc_patrolled,rc_id,page_len as length, page_latest as rev_id',
'conds' => $conds,
'options' => array( 'USE INDEX' => array('recentchanges' => $rcIndexes) )
@@ -425,7 +422,7 @@ class NewPagesPager extends ReverseChronologicalPager {
while( $row = $this->mResult->fetchObject() ) {
$linkBatch->add( NS_USER, $row->rc_user_text );
$linkBatch->add( NS_USER_TALK, $row->rc_user_text );
- $linkBatch->add( $row->page_namespace, $row->page_title );
+ $linkBatch->add( $row->rc_namespace, $row->rc_title );
}
$linkBatch->execute();
return "<ul>";
diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php
index b3468a3c..ca2236ee 100644
--- a/includes/specials/SpecialPreferences.php
+++ b/includes/specials/SpecialPreferences.php
@@ -21,11 +21,11 @@ function wfSpecialPreferences() {
* @ingroup SpecialPage
*/
class PreferencesForm {
- var $mQuickbar, $mOldpass, $mNewpass, $mRetypePass, $mStubs;
+ var $mQuickbar, $mStubs;
var $mRows, $mCols, $mSkin, $mMath, $mDate, $mUserEmail, $mEmailFlag, $mNick;
var $mUserLanguage, $mUserVariant;
- var $mSearch, $mRecent, $mRecentDays, $mHourDiff, $mSearchLines, $mSearchChars, $mAction;
- var $mReset, $mPosted, $mToggles, $mUseAjaxSearch, $mSearchNs, $mRealName, $mImageSize;
+ var $mSearch, $mRecent, $mRecentDays, $mTimeZone, $mHourDiff, $mSearchLines, $mSearchChars, $mAction;
+ var $mReset, $mPosted, $mToggles, $mSearchNs, $mRealName, $mImageSize;
var $mUnderline, $mWatchlistEdits;
/**
@@ -36,13 +36,10 @@ class PreferencesForm {
global $wgContLang, $wgUser, $wgAllowRealName;
$this->mQuickbar = $request->getVal( 'wpQuickbar' );
- $this->mOldpass = $request->getVal( 'wpOldpass' );
- $this->mNewpass = $request->getVal( 'wpNewpass' );
- $this->mRetypePass =$request->getVal( 'wpRetypePass' );
$this->mStubs = $request->getVal( 'wpStubs' );
$this->mRows = $request->getVal( 'wpRows' );
$this->mCols = $request->getVal( 'wpCols' );
- $this->mSkin = $request->getVal( 'wpSkin' );
+ $this->mSkin = Skin::normalizeKey( $request->getVal( 'wpSkin' ) );
$this->mMath = $request->getVal( 'wpMath' );
$this->mDate = $request->getVal( 'wpDate' );
$this->mUserEmail = $request->getVal( 'wpUserEmail' );
@@ -54,6 +51,7 @@ class PreferencesForm {
$this->mSearch = $request->getVal( 'wpSearch' );
$this->mRecent = $request->getVal( 'wpRecent' );
$this->mRecentDays = $request->getVal( 'wpRecentDays' );
+ $this->mTimeZone = $request->getVal( 'wpTimeZone' );
$this->mHourDiff = $request->getVal( 'wpHourDiff' );
$this->mSearchLines = $request->getVal( 'wpSearchLines' );
$this->mSearchChars = $request->getVal( 'wpSearchChars' );
@@ -66,7 +64,6 @@ class PreferencesForm {
$this->mSuccess = $request->getCheck( 'success' );
$this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' );
$this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' );
- $this->mUseAjaxSearch = $request->getCheck( 'wpUseAjaxSearch' );
$this->mDisableMWSuggest = $request->getCheck( 'wpDisableMWSuggest' );
$this->mSaveprefs = $request->getCheck( 'wpSaveprefs' ) &&
@@ -105,10 +102,10 @@ class PreferencesForm {
}
function execute() {
- global $wgUser, $wgOut;
+ global $wgUser, $wgOut, $wgTitle;
if ( $wgUser->isAnon() ) {
- $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext' );
+ $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext', array($wgTitle->getPrefixedDBkey()) );
return;
}
if ( wfReadOnly() ) {
@@ -174,34 +171,37 @@ class PreferencesForm {
/**
* Used to validate the user inputed timezone before saving it as
- * 'timecorrection', will return '00:00' if fed bogus data.
- * Note: It's not a 100% correct implementation timezone-wise, it will
- * accept stuff like '14:30',
+ * 'timecorrection', will return 'System' if fed bogus data.
* @access private
- * @param string $s the user input
+ * @param string $tz the user input Zoneinfo timezone
+ * @param string $s the user input offset string
* @return string
*/
- function validateTimeZone( $s ) {
- if ( $s !== '' ) {
- if ( strpos( $s, ':' ) ) {
- # HH:MM
- $array = explode( ':' , $s );
- $hour = intval( $array[0] );
- $minute = intval( $array[1] );
- } else {
- $minute = intval( $s * 60 );
- $hour = intval( $minute / 60 );
- $minute = abs( $minute ) % 60;
- }
- # Max is +14:00 and min is -12:00, see:
- # http://en.wikipedia.org/wiki/Timezone
- $hour = min( $hour, 14 );
- $hour = max( $hour, -12 );
- $minute = min( $minute, 59 );
- $minute = max( $minute, 0 );
- $s = sprintf( "%02d:%02d", $hour, $minute );
+ function validateTimeZone( $tz, $s ) {
+ $data = explode( '|', $tz, 3 );
+ switch ( $data[0] ) {
+ case 'ZoneInfo':
+ case 'System':
+ return $tz;
+ case 'Offset':
+ default:
+ $data = explode( ':', $s, 2 );
+ $minDiff = 0;
+ if( count( $data ) == 2 ) {
+ $data[0] = intval( $data[0] );
+ $data[1] = intval( $data[1] );
+ $minDiff = abs( $data[0] ) * 60 + $data[1];
+ if ( $data[0] < 0 ) $minDiff = -$minDiff;
+ } else {
+ $minDiff = intval( $data[0] ) * 60;
+ }
+
+ # Max is +14:00 and min is -12:00, see:
+ # http://en.wikipedia.org/wiki/Timezone
+ $minDiff = min( $minDiff, 840 ); # 14:00
+ $minDiff = max( $minDiff, -720 ); # -12:00
+ return 'Offset|'.$minDiff;
}
- return $s;
}
/**
@@ -213,30 +213,6 @@ class PreferencesForm {
global $wgEmailAuthentication, $wgRCMaxAge;
global $wgAuth, $wgEmailConfirmToEdit;
-
- 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;
- }
- }
$wgUser->setRealName( $this->mRealName );
$oldOptions = $wgUser->mOptions;
@@ -269,7 +245,10 @@ class PreferencesForm {
$wgUser->setOption( 'variant', $this->mUserVariant );
$wgUser->setOption( 'nickname', $this->mNick );
$wgUser->setOption( 'quickbar', $this->mQuickbar );
- $wgUser->setOption( 'skin', $this->mSkin );
+ global $wgAllowUserSkin;
+ if( $wgAllowUserSkin ) {
+ $wgUser->setOption( 'skin', $this->mSkin );
+ }
global $wgUseTeX;
if( $wgUseTeX ) {
$wgUser->setOption( 'math', $this->mMath );
@@ -284,12 +263,11 @@ class PreferencesForm {
$wgUser->setOption( 'rows', $this->validateInt( $this->mRows, 4, 1000 ) );
$wgUser->setOption( 'cols', $this->validateInt( $this->mCols, 4, 1000 ) );
$wgUser->setOption( 'stubthreshold', $this->validateIntOrNull( $this->mStubs ) );
- $wgUser->setOption( 'timecorrection', $this->validateTimeZone( $this->mHourDiff, -12, 14 ) );
+ $wgUser->setOption( 'timecorrection', $this->validateTimeZone( $this->mTimeZone, $this->mHourDiff ) );
$wgUser->setOption( 'imagesize', $this->mImageSize );
$wgUser->setOption( 'thumbsize', $this->mThumbSize );
$wgUser->setOption( 'underline', $this->validateInt($this->mUnderline, 0, 2) );
$wgUser->setOption( 'watchlistdays', $this->validateFloat( $this->mWatchlistDays, 0, 7 ) );
- $wgUser->setOption( 'ajaxsearch', $this->mUseAjaxSearch );
$wgUser->setOption( 'disablesuggest', $this->mDisableMWSuggest );
# Set search namespace options
@@ -370,9 +348,8 @@ class PreferencesForm {
* @access private
*/
function resetPrefs() {
- global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName;
+ global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName, $wgLocalTZoffset;
- $this->mOldpass = $this->mNewpass = $this->mRetypePass = '';
$this->mUserEmail = $wgUser->getEmail();
$this->mUserEmailAuthenticationtimestamp = $wgUser->getEmailAuthenticationtimestamp();
$this->mRealName = ($wgAllowRealName) ? $wgUser->getRealName() : '';
@@ -391,7 +368,47 @@ class PreferencesForm {
$this->mRows = $wgUser->getOption( 'rows' );
$this->mCols = $wgUser->getOption( 'cols' );
$this->mStubs = $wgUser->getOption( 'stubthreshold' );
- $this->mHourDiff = $wgUser->getOption( 'timecorrection' );
+
+ $tz = $wgUser->getOption( 'timecorrection' );
+ $data = explode( '|', $tz, 3 );
+ $minDiff = null;
+ switch ( $data[0] ) {
+ case 'ZoneInfo':
+ $this->mTimeZone = $tz;
+ # Check if the specified TZ exists, and change to 'Offset' if
+ # not.
+ if ( !function_exists('timezone_open') || @timezone_open( $data[2] ) === false ) {
+ $this->mTimeZone = 'Offset';
+ $minDiff = intval( $data[1] );
+ }
+ break;
+ case '':
+ case 'System':
+ $this->mTimeZone = 'System|'.$wgLocalTZoffset;
+ break;
+ case 'Offset':
+ $this->mTimeZone = 'Offset';
+ $minDiff = intval( $data[1] );
+ break;
+ default:
+ $this->mTimeZone = 'Offset';
+ $data = explode( ':', $tz, 2 );
+ if( count( $data ) == 2 ) {
+ $data[0] = intval( $data[0] );
+ $data[1] = intval( $data[1] );
+ $minDiff = abs( $data[0] ) * 60 + $data[1];
+ if ( $data[0] < 0 ) $minDiff = -$minDiff;
+ } else {
+ $minDiff = intval( $data[0] ) * 60;
+ }
+ break;
+ }
+ if ( is_null( $minDiff ) ) {
+ $this->mHourDiff = '';
+ } else {
+ $this->mHourDiff = sprintf( '%+03d:%02d', floor($minDiff/60), abs($minDiff)%60 );
+ }
+
$this->mSearch = $wgUser->getOption( 'searchlimit' );
$this->mSearchLines = $wgUser->getOption( 'contextlines' );
$this->mSearchChars = $wgUser->getOption( 'contextchars' );
@@ -402,7 +419,6 @@ class PreferencesForm {
$this->mWatchlistEdits = $wgUser->getOption( 'wllimit' );
$this->mUnderline = $wgUser->getOption( 'underline' );
$this->mWatchlistDays = $wgUser->getOption( 'watchlistdays' );
- $this->mUseAjaxSearch = $wgUser->getBoolOption( 'ajaxsearch' );
$this->mDisableMWSuggest = $wgUser->getBoolOption( 'disablesuggest' );
$togs = User::getToggles();
@@ -511,18 +527,18 @@ class PreferencesForm {
* @access private
*/
function mainPrefsForm( $status , $message = '' ) {
- global $wgUser, $wgOut, $wgLang, $wgContLang;
+ global $wgUser, $wgOut, $wgLang, $wgContLang, $wgAuth;
global $wgAllowRealName, $wgImageLimits, $wgThumbLimits;
- global $wgDisableLangConversion;
+ global $wgDisableLangConversion, $wgDisableTitleConversion;
global $wgEnotifWatchlist, $wgEnotifUserTalk,$wgEnotifMinorEdits;
global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress;
global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication;
- global $wgContLanguageCode, $wgDefaultSkin, $wgSkipSkins, $wgAuth;
- global $wgEmailConfirmToEdit, $wgAjaxSearch, $wgEnableMWSuggest;
+ global $wgContLanguageCode, $wgDefaultSkin, $wgCookieExpiration;
+ global $wgEmailConfirmToEdit, $wgEnableMWSuggest, $wgLocalTZoffset;
$wgOut->setPageTitle( wfMsg( 'preferences' ) );
$wgOut->setArticleRelated( false );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->addScriptFile( 'prefs.js' );
$wgOut->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc.
@@ -536,7 +552,6 @@ class PreferencesForm {
}
$qbs = $wgLang->getQuickbarSettings();
- $skinNames = $wgLang->getSkinNames();
$mathopts = $wgLang->getMathNames();
$dateopts = $wgLang->getDatePreferences();
$togs = User::getToggles();
@@ -552,6 +567,7 @@ class PreferencesForm {
$this->mUsedToggles[ 'enotifrevealaddr' ] = true;
$this->mUsedToggles[ 'ccmeonemails' ] = true;
$this->mUsedToggles[ 'uselivepreview' ] = true;
+ $this->mUsedToggles[ 'noconvertlink' ] = true;
if ( !$this->mEmailFlag ) { $emfc = 'checked="checked"'; }
@@ -560,7 +576,13 @@ class PreferencesForm {
if ($wgEmailAuthentication && ($this->mUserEmail != '') ) {
if( $wgUser->getEmailAuthenticationTimestamp() ) {
- $emailauthenticated = wfMsg('emailauthenticated',$wgLang->timeanddate($wgUser->getEmailAuthenticationTimestamp(), true ) ).'<br />';
+ // date and time are separate parameters to facilitate localisation.
+ // $time is kept for backward compat reasons.
+ // 'emailauthenticated' is also used in SpecialConfirmemail.php
+ $time = $wgLang->timeAndDate( $wgUser->getEmailAuthenticationTimestamp(), true );
+ $d = $wgLang->date( $wgUser->getEmailAuthenticationTimestamp(), true );
+ $t = $wgLang->time( $wgUser->getEmailAuthenticationTimestamp(), true );
+ $emailauthenticated = wfMsg('emailauthenticated', $time, $d, $t ).'<br />';
$disableEmailPrefs = false;
} else {
$disableEmailPrefs = true;
@@ -620,26 +642,26 @@ class PreferencesForm {
$toolLinks = array();
$toolLinks[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'ListGroupRights' ), wfMsg( 'listgrouprights' ) );
# At the moment one tool link only but be prepared for the future...
- # FIXME: Add a link to Special:Userrights for users who are allowed to use it.
+ # FIXME: Add a link to Special:Userrights for users who are allowed to use it.
# $wgUser->isAllowed( 'userrights' ) seems to strict in some cases
$userInformationHtml =
$this->tableRow( wfMsgHtml( 'username' ), htmlspecialchars( $wgUser->getName() ) ) .
- $this->tableRow( wfMsgHtml( 'uid' ), htmlspecialchars( $wgUser->getId() ) ) .
+ $this->tableRow( wfMsgHtml( 'uid' ), $wgLang->formatNum( htmlspecialchars( $wgUser->getId() ) ) ).
$this->tableRow(
wfMsgExt( 'prefs-memberingroups', array( 'parseinline' ), count( $userEffectiveGroupsArray ) ),
- implode( wfMsg( 'comma-separator' ), $userEffectiveGroupsArray ) .
+ $wgLang->commaList( $userEffectiveGroupsArray ) .
'<br />(' . implode( ' | ', $toolLinks ) . ')'
) .
$this->tableRow(
wfMsgHtml( 'prefs-edits' ),
- $wgLang->formatNum( User::edits( $wgUser->getId() ) )
+ $wgLang->formatNum( $wgUser->getEditCount() )
);
if( wfRunHooks( 'PreferencesUserInformationPanel', array( $this, &$userInformationHtml ) ) ) {
- $wgOut->addHtml( $userInformationHtml );
+ $wgOut->addHTML( $userInformationHtml );
}
if ( $wgAllowRealName ) {
@@ -724,7 +746,7 @@ class PreferencesForm {
}
if(count($variantArray) > 1) {
- $wgOut->addHtml(
+ $wgOut->addHTML(
$this->tableRow(
Xml::label( wfMsg( 'yourvariant' ), 'wpUserVariant' ),
Xml::tags( 'select',
@@ -734,30 +756,25 @@ class PreferencesForm {
)
);
}
+
+ if(count($variantArray) > 1 && !$wgDisableLangConversion && !$wgDisableTitleConversion) {
+ $wgOut->addHTML(
+ Xml::tags( 'tr', null,
+ Xml::tags( 'td', array( 'colspan' => '2' ),
+ $this->getToggle( "noconvertlink" )
+ )
+ )
+ );
+ }
}
# Password
if( $wgAuth->allowPasswordChange() ) {
+ $link = $wgUser->getSkin()->link( SpecialPage::getTitleFor( 'ResetPass' ), wfMsgHtml( 'prefs-resetpass' ),
+ array() , array('returnto' => SpecialPage::getTitleFor( 'Preferences') ) );
$wgOut->addHTML(
$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->tableRow(
- Xml::label( wfMsg( 'newpassword' ), 'wpNewpass' ),
- Xml::password( 'wpNewpass', 25, $this->mNewpass, array( 'id' => 'wpNewpass' ) )
- ) .
- $this->tableRow(
- Xml::label( wfMsg( 'retypenew' ), 'wpRetypePass' ),
- Xml::password( 'wpRetypePass', 25, $this->mRetypePass, array( 'id' => 'wpRetypePass' ) )
- ) .
- Xml::tags( 'tr', null,
- Xml::tags( 'td', array( 'colspan' => '2' ),
- $this->getToggle( "rememberpassword" )
- )
- )
- );
+ $this->tableRow( '<ul><li>' . $link . '</li></ul>' ) );
}
# <FIXME>
@@ -799,48 +816,49 @@ class PreferencesForm {
# Quickbar
#
if ($this->mSkin == 'cologneblue' || $this->mSkin == 'standard') {
- $wgOut->addHtml( "<fieldset>\n<legend>" . wfMsg( 'qbsettings' ) . "</legend>\n" );
+ $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg( 'qbsettings' ) . "</legend>\n" );
for ( $i = 0; $i < count( $qbs ); ++$i ) {
if ( $i == $this->mQuickbar ) { $checked = ' checked="checked"'; }
else { $checked = ""; }
$wgOut->addHTML( "<div><label><input type='radio' name='wpQuickbar' value=\"$i\"$checked />{$qbs[$i]}</label></div>\n" );
}
- $wgOut->addHtml( "</fieldset>\n\n" );
+ $wgOut->addHTML( "</fieldset>\n\n" );
} else {
# Need to output a hidden option even if the relevant skin is not in use,
# otherwise the preference will get reset to 0 on submit
- $wgOut->addHtml( wfHidden( 'wpQuickbar', $this->mQuickbar ) );
+ $wgOut->addHTML( Xml::hidden( 'wpQuickbar', $this->mQuickbar ) );
}
# Skin
#
- $wgOut->addHTML( "<fieldset>\n<legend>\n" . wfMsg('skin') . "</legend>\n" );
- $mptitle = Title::newMainPage();
- $previewtext = wfMsg('skinpreview');
- # Only show members of Skin::getSkinNames() rather than
- # $skinNames (skins is all skin names from Language.php)
- $validSkinNames = Skin::getSkinNames();
- # Sort by UI skin name. First though need to update validSkinNames as sometimes
- # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI).
- foreach ($validSkinNames as $skinkey => & $skinname ) {
- if ( isset( $skinNames[$skinkey] ) ) {
- $skinname = $skinNames[$skinkey];
+ global $wgAllowUserSkin;
+ if( $wgAllowUserSkin ) {
+ $wgOut->addHTML( "<fieldset>\n<legend>\n" . wfMsg( 'skin' ) . "</legend>\n" );
+ $mptitle = Title::newMainPage();
+ $previewtext = wfMsg( 'skin-preview' );
+ # Only show members of Skin::getSkinNames() rather than
+ # $skinNames (skins is all skin names from Language.php)
+ $validSkinNames = Skin::getUsableSkins();
+ # Sort by UI skin name. First though need to update validSkinNames as sometimes
+ # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI).
+ foreach ( $validSkinNames as $skinkey => &$skinname ) {
+ $msgName = "skinname-{$skinkey}";
+ $localisedSkinName = wfMsg( $msgName );
+ if ( !wfEmptyMsg( $msgName, $localisedSkinName ) ) {
+ $skinname = $localisedSkinName;
+ }
}
- }
- asort($validSkinNames);
- foreach ($validSkinNames as $skinkey => $sn ) {
- if ( in_array( $skinkey, $wgSkipSkins ) ) {
- continue;
+ asort($validSkinNames);
+ foreach ($validSkinNames as $skinkey => $sn ) {
+ $checked = $skinkey == $this->mSkin ? ' checked="checked"' : '';
+ $mplink = htmlspecialchars( $mptitle->getLocalURL( "useskin=$skinkey" ) );
+ $previewlink = "(<a target='_blank' href=\"$mplink\">$previewtext</a>)";
+ if( $skinkey == $wgDefaultSkin )
+ $sn .= ' (' . wfMsg( 'default' ) . ')';
+ $wgOut->addHTML( "<input type='radio' name='wpSkin' id=\"wpSkin$skinkey\" value=\"$skinkey\"$checked /> <label for=\"wpSkin$skinkey\">{$sn}</label> $previewlink<br />\n" );
}
- $checked = $skinkey == $this->mSkin ? ' checked="checked"' : '';
-
- $mplink = htmlspecialchars($mptitle->getLocalURL("useskin=$skinkey"));
- $previewlink = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
- if( $skinkey == $wgDefaultSkin )
- $sn .= ' (' . wfMsg( 'default' ) . ')';
- $wgOut->addHTML( "<input type='radio' name='wpSkin' id=\"wpSkin$skinkey\" value=\"$skinkey\"$checked /> <label for=\"wpSkin$skinkey\">{$sn}</label> $previewlink<br />\n" );
+ $wgOut->addHTML( "</fieldset>\n\n" );
}
- $wgOut->addHTML( "</fieldset>\n\n" );
# Math
#
@@ -860,10 +878,6 @@ class PreferencesForm {
# Files
#
- $wgOut->addHTML(
- "<fieldset>\n" . Xml::element( 'legend', null, wfMsg( 'files' ) ) . "\n"
- );
-
$imageLimitOptions = null;
foreach ( $wgImageLimits as $index => $limits ) {
$selected = ($index == $this->mImageSize);
@@ -871,14 +885,6 @@ class PreferencesForm {
wfMsg('unit-pixel'), $index, $selected );
}
- $imageSizeId = 'wpImageSize';
- $wgOut->addHTML(
- "<div>" . Xml::label( wfMsg('imagemaxsize'), $imageSizeId ) . " " .
- Xml::openElement( 'select', array( 'name' => $imageSizeId, 'id' => $imageSizeId ) ) .
- $imageLimitOptions .
- Xml::closeElement( 'select' ) . "</div>\n"
- );
-
$imageThumbOptions = null;
foreach ( $wgThumbLimits as $index => $size ) {
$selected = ($index == $this->mThumbSize);
@@ -886,16 +892,34 @@ class PreferencesForm {
$selected);
}
+ $imageSizeId = 'wpImageSize';
$thumbSizeId = 'wpThumbSize';
$wgOut->addHTML(
- "<div>" . Xml::label( wfMsg('thumbsize'), $thumbSizeId ) . " " .
- Xml::openElement( 'select', array( 'name' => $thumbSizeId, 'id' => $thumbSizeId ) ) .
- $imageThumbOptions .
- Xml::closeElement( 'select' ) . "</div>\n"
+ Xml::fieldset( wfMsg( 'files' ) ) . "\n" .
+ Xml::openElement( 'table' ) .
+ '<tr>
+ <td class="mw-label">' .
+ Xml::label( wfMsg( 'imagemaxsize' ), $imageSizeId ) .
+ '</td>
+ <td class="mw-input">' .
+ Xml::openElement( 'select', array( 'name' => $imageSizeId, 'id' => $imageSizeId ) ) .
+ $imageLimitOptions .
+ Xml::closeElement( 'select' ) .
+ '</td>
+ </tr><tr>
+ <td class="mw-label">' .
+ Xml::label( wfMsg( 'thumbsize' ), $thumbSizeId ) .
+ '</td>
+ <td class="mw-input">' .
+ Xml::openElement( 'select', array( 'name' => $thumbSizeId, 'id' => $thumbSizeId ) ) .
+ $imageThumbOptions .
+ Xml::closeElement( 'select' ) .
+ '</td>
+ </tr>' .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' )
);
- $wgOut->addHTML( "</fieldset>\n\n" );
-
# Date format
#
# Date/Time
@@ -929,18 +953,61 @@ class PreferencesForm {
$wgOut->addHTML( Xml::closeElement( 'fieldset' ) . "\n" );
}
- $nowlocal = $wgLang->time( $now = wfTimestampNow(), true );
- $nowserver = $wgLang->time( $now, false );
+ $nowlocal = Xml::openElement( 'span', array( 'id' => 'wpLocalTime' ) ) .
+ $wgLang->time( $now = wfTimestampNow(), true ) .
+ Xml::closeElement( 'span' );
+ $nowserver = $wgLang->time( $now, false ) .
+ Xml::hidden( 'wpServerTime', substr( $now, 8, 2 ) * 60 + substr( $now, 10, 2 ) );
$wgOut->addHTML(
Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', null, wfMsg( 'timezonelegend' ) ) .
Xml::openElement( 'table' ) .
$this->addRow( wfMsg( 'servertime' ), $nowserver ) .
- $this->addRow( wfMsg( 'localtime' ), $nowlocal ) .
+ $this->addRow( wfMsg( 'localtime' ), $nowlocal )
+ );
+ $opt = Xml::openElement( 'select', array(
+ 'name' => 'wpTimeZone',
+ 'id' => 'wpTimeZone',
+ 'onchange' => 'javascript:updateTimezoneSelection(false)' ) );
+ $opt .= Xml::option( wfMsg( 'timezoneuseserverdefault' ), "System|$wgLocalTZoffset", $this->mTimeZone === "System|$wgLocalTZoffset" );
+ $opt .= Xml::option( wfMsg( 'timezoneuseoffset' ), 'Offset', $this->mTimeZone === 'Offset' );
+ if ( function_exists( 'timezone_identifiers_list' ) ) {
+ $optgroup = '';
+ $tzs = timezone_identifiers_list();
+ sort( $tzs );
+ $selZone = explode( '|', $this->mTimeZone, 3);
+ $selZone = ( $selZone[0] == 'ZoneInfo' ) ? $selZone[2] : null;
+ $now = date_create( 'now' );
+ foreach ( $tzs as $tz ) {
+ $z = explode( '/', $tz, 2 );
+ # timezone_identifiers_list() returns a number of
+ # backwards-compatibility entries. This filters them out of the
+ # list presented to the user.
+ if ( count( $z ) != 2 || !in_array( $z[0], array( 'Africa', 'America', 'Antarctica', 'Arctic', 'Asia', 'Atlantic', 'Australia', 'Europe', 'Indian', 'Pacific' ) ) ) continue;
+ if ( $optgroup != $z[0] ) {
+ if ( $optgroup !== '' ) $opt .= Xml::closeElement( 'optgroup' );
+ $optgroup = $z[0];
+ $opt .= Xml::openElement( 'optgroup', array( 'label' => $z[0] ) );
+ }
+ $minDiff = floor( timezone_offset_get( timezone_open( $tz ), $now ) / 60 );
+ $opt .= Xml::option( str_replace( '_', ' ', $tz ), "ZoneInfo|$minDiff|$tz", $selZone === $tz, array( 'label' => $z[1] ) );
+ }
+ if ( $optgroup !== '' ) $opt .= Xml::closeElement( 'optgroup' );
+ }
+ $opt .= Xml::closeElement( 'select' );
+ $wgOut->addHTML(
+ $this->addRow(
+ Xml::label( wfMsg( 'timezoneselect' ), 'wpTimeZone' ),
+ $opt )
+ );
+ $wgOut->addHTML(
$this->addRow(
Xml::label( wfMsg( 'timezoneoffset' ), 'wpHourDiff' ),
- Xml::input( 'wpHourDiff', 6, $this->mHourDiff, array( 'id' => 'wpHourDiff' ) ) ) .
+ Xml::input( 'wpHourDiff', 6, $this->mHourDiff, array(
+ 'id' => 'wpHourDiff',
+ 'onfocus' => 'javascript:updateTimezoneSelection(true)',
+ 'onblur' => 'javascript:updateTimezoneSelection(false)' ) ) ) .
"<tr>
<td></td>
<td class='mw-submit'>" .
@@ -961,12 +1028,11 @@ class PreferencesForm {
# Editing
#
global $wgLivePreview;
- $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'textboxsize' ) . '</legend>
- <div>' .
- wfInputLabel( wfMsg( 'rows' ), 'wpRows', 'wpRows', 3, $this->mRows ) .
- ' ' .
- wfInputLabel( wfMsg( 'columns' ), 'wpCols', 'wpCols', 3, $this->mCols ) .
- "</div>" .
+ $wgOut->addHTML(
+ Xml::fieldset( wfMsg( 'textboxsize' ) ) .
+ wfMsgHTML( 'prefs-edit-boxsize' ) . ' ' .
+ Xml::inputLabel( wfMsg( 'rows' ), 'wpRows', 'wpRows', 3, $this->mRows ) . ' ' .
+ Xml::inputLabel( wfMsg( 'columns' ), 'wpCols', 'wpCols', 3, $this->mCols ) .
$this->getToggles( array(
'editsection',
'editsectiononrightclick',
@@ -980,62 +1046,76 @@ class PreferencesForm {
'externaldiff',
$wgLivePreview ? 'uselivepreview' : false,
'forceeditsummary',
- ) ) . '</fieldset>'
+ ) )
);
- # Recent changes
- $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'prefs-rc' ) . '</legend>' );
-
- $rc = '<table><tr>';
- $rc .= '<td>' . Xml::label( wfMsg( 'recentchangesdays' ), 'wpRecentDays' ) . '</td>';
- $rc .= '<td>' . Xml::input( 'wpRecentDays', 3, $this->mRecentDays, array( 'id' => 'wpRecentDays' ) ) . '</td>';
- $rc .= '</tr><tr>';
- $rc .= '<td>' . Xml::label( wfMsg( 'recentchangescount' ), 'wpRecent' ) . '</td>';
- $rc .= '<td>' . Xml::input( 'wpRecent', 3, $this->mRecent, array( 'id' => 'wpRecent' ) ) . '</td>';
- $rc .= '</tr></table>';
- $wgOut->addHtml( $rc );
+ $wgOut->addHTML( Xml::closeElement( 'fieldset' ) );
- $wgOut->addHtml( '<br />' );
+ # Recent changes
+ global $wgRCMaxAge;
+ $wgOut->addHTML(
+ Xml::fieldset( wfMsg( 'prefs-rc' ) ) .
+ Xml::openElement( 'table' ) .
+ '<tr>
+ <td class="mw-label">' .
+ Xml::label( wfMsg( 'recentchangesdays' ), 'wpRecentDays' ) .
+ '</td>
+ <td class="mw-input">' .
+ Xml::input( 'wpRecentDays', 3, $this->mRecentDays, array( 'id' => 'wpRecentDays' ) ) . ' ' .
+ wfMsgExt( 'recentchangesdays-max', 'parsemag',
+ $wgLang->formatNum( ceil( $wgRCMaxAge / ( 3600 * 24 ) ) ) ) .
+ '</td>
+ </tr><tr>
+ <td class="mw-label">' .
+ Xml::label( wfMsg( 'recentchangescount' ), 'wpRecent' ) .
+ '</td>
+ <td class="mw-input">' .
+ Xml::input( 'wpRecent', 3, $this->mRecent, array( 'id' => 'wpRecent' ) ) .
+ '</td>
+ </tr>' .
+ Xml::closeElement( 'table' ) .
+ '<br />'
+ );
$toggles[] = 'hideminor';
if( $wgRCShowWatchingUsers )
$toggles[] = 'shownumberswatching';
$toggles[] = 'usenewrc';
- $wgOut->addHtml( $this->getToggles( $toggles ) );
- $wgOut->addHtml( '</fieldset>' );
+ $wgOut->addHTML(
+ $this->getToggles( $toggles ) .
+ Xml::closeElement( 'fieldset' )
+ );
# Watchlist
- $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'prefs-watchlist' ) . '</legend>' );
-
- $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-days' ), 'wpWatchlistDays', 'wpWatchlistDays', 3, $this->mWatchlistDays ) );
- $wgOut->addHtml( '<br /><br />' );
-
- $wgOut->addHtml( $this->getToggle( 'extendwatchlist' ) );
- $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-edits' ), 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) );
- $wgOut->addHtml( '<br /><br />' );
+ $wgOut->addHTML(
+ Xml::fieldset( wfMsg( 'prefs-watchlist' ) ) .
+ Xml::inputLabel( wfMsg( 'prefs-watchlist-days' ), 'wpWatchlistDays', 'wpWatchlistDays', 3, $this->mWatchlistDays ) . ' ' .
+ wfMsgHTML( 'prefs-watchlist-days-max' ) .
+ '<br /><br />' .
+ $this->getToggle( 'extendwatchlist' ) .
+ Xml::inputLabel( wfMsg( 'prefs-watchlist-edits' ), 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) . ' ' .
+ wfMsgHTML( 'prefs-watchlist-edits-max' ) .
+ '<br /><br />' .
+ $this->getToggles( array( 'watchlisthideminor', 'watchlisthidebots', 'watchlisthideown', 'watchlisthideanons', 'watchlisthideliu' ) )
+ );
- $wgOut->addHtml( $this->getToggles( array( 'watchlisthideown', 'watchlisthidebots', 'watchlisthideminor' ) ) );
+ if( $wgUser->isAllowed( 'createpage' ) || $wgUser->isAllowed( 'createtalk' ) ) {
+ $wgOut->addHTML( $this->getToggle( 'watchcreations' ) );
+ }
- if( $wgUser->isAllowed( 'createpage' ) || $wgUser->isAllowed( 'createtalk' ) )
- $wgOut->addHtml( $this->getToggle( 'watchcreations' ) );
foreach( array( 'edit' => 'watchdefault', 'move' => 'watchmoves', 'delete' => 'watchdeletion' ) as $action => $toggle ) {
if( $wgUser->isAllowed( $action ) )
- $wgOut->addHtml( $this->getToggle( $toggle ) );
+ $wgOut->addHTML( $this->getToggle( $toggle ) );
}
$this->mUsedToggles['watchcreations'] = true;
$this->mUsedToggles['watchdefault'] = true;
$this->mUsedToggles['watchmoves'] = true;
$this->mUsedToggles['watchdeletion'] = true;
- $wgOut->addHtml( '</fieldset>' );
+ $wgOut->addHTML( Xml::closeElement( 'fieldset' ) );
# Search
- $ajaxsearch = $wgAjaxSearch ?
- $this->addRow(
- Xml::label( wfMsg( 'useajaxsearch' ), 'wpUseAjaxSearch' ),
- Xml::check( 'wpUseAjaxSearch', $this->mUseAjaxSearch, array( 'id' => 'wpUseAjaxSearch' ) )
- ) : '';
$mwsuggest = $wgEnableMWSuggest ?
$this->addRow(
Xml::label( wfMsg( 'mwsuggest-disable' ), 'wpDisableMWSuggest' ),
@@ -1049,7 +1129,6 @@ class PreferencesForm {
Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', null, wfMsg( 'prefs-searchoptions' ) ) .
Xml::openElement( 'table' ) .
- $ajaxsearch .
$this->addRow(
Xml::label( wfMsg( 'resultsperpage' ), 'wpSearch' ),
Xml::input( 'wpSearch', 4, $this->mSearch, array( 'id' => 'wpSearch' ) )
@@ -1078,8 +1157,8 @@ class PreferencesForm {
# Misc
#
$wgOut->addHTML('<fieldset><legend>' . wfMsg('prefs-misc') . '</legend>');
- $wgOut->addHtml( '<label for="wpStubs">' . wfMsg( 'stub-threshold' ) . '</label>&nbsp;' );
- $wgOut->addHtml( Xml::input( 'wpStubs', 6, $this->mStubs, array( 'id' => 'wpStubs' ) ) );
+ $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' ) );
@@ -1098,9 +1177,13 @@ class PreferencesForm {
foreach ( $togs as $tname ) {
if( !array_key_exists( $tname, $this->mUsedToggles ) ) {
- $wgOut->addHTML( $this->getToggle( $tname ) );
+ if( $tname == 'norollbackdiff' && $wgUser->isAllowed( 'rollback' ) )
+ $wgOut->addHTML( $this->getToggle( $tname ) );
+ else
+ $wgOut->addHTML( $this->getToggle( $tname ) );
}
}
+
$wgOut->addHTML( '</fieldset>' );
wfRunHooks( 'RenderPreferencesForm', array( $this, $wgOut ) );
@@ -1119,7 +1202,7 @@ class PreferencesForm {
<input type='hidden' name='wpEditToken' value=\"{$token}\" />
</div></form>\n" );
- $wgOut->addHtml( Xml::tags( 'div', array( 'class' => "prefcache" ),
+ $wgOut->addHTML( Xml::tags( 'div', array( 'class' => "prefcache" ),
wfMsgExt( 'clearyourcache', 'parseinline' ) )
);
}
diff --git a/includes/specials/SpecialPrefixindex.php b/includes/specials/SpecialPrefixindex.php
index 9c880349..ea0c1135 100644
--- a/includes/specials/SpecialPrefixindex.php
+++ b/includes/specials/SpecialPrefixindex.php
@@ -1,40 +1,4 @@
<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Entry point : initialise variables and call subfunctions.
- * @param $par String: becomes "FOO" when called like Special:Prefixindex/FOO (default NULL)
- * @param $specialPage SpecialPage object.
- */
-function wfSpecialPrefixIndex( $par=NULL, $specialPage ) {
- global $wgRequest, $wgOut, $wgContLang;
-
- # GET values
- $from = $wgRequest->getVal( 'from' );
- $prefix = $wgRequest->getVal( 'prefix' );
- $namespace = $wgRequest->getInt( 'namespace' );
- $namespaces = $wgContLang->getNamespaces();
-
- $indexPage = new SpecialPrefixIndex();
-
- $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 );
- } elseif ( isset($prefix) ) {
- $indexPage->showChunk( $namespace, $prefix, $specialPage->including(), $from );
- } elseif ( isset($from) ) {
- $indexPage->showChunk( $namespace, $from, $specialPage->including(), $from );
- } else {
- $wgOut->addHtml($indexPage->namespaceForm ( $namespace, null ));
- }
-}
/**
* implements Special:Prefixindex
@@ -44,18 +8,90 @@ class SpecialPrefixindex extends SpecialAllpages {
// Inherit $maxPerPage
// Define other properties
- protected $name = 'Prefixindex';
protected $nsfromMsg = 'allpagesprefix';
+
+ function __construct(){
+ parent::__construct( 'Prefixindex' );
+ }
+
+ /**
+ * Entry point : initialise variables and call subfunctions.
+ * @param $par String: becomes "FOO" when called like Special:Prefixindex/FOO (default null)
+ */
+ function execute( $par ) {
+ global $wgRequest, $wgOut, $wgContLang;
+
+ $this->setHeaders();
+ $this->outputHeader();
+
+ # GET values
+ $from = $wgRequest->getVal( 'from' );
+ $prefix = $wgRequest->getVal( 'prefix' );
+ $namespace = $wgRequest->getInt( 'namespace' );
+ $namespaces = $wgContLang->getNamespaces();
+
+ $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces ) ) )
+ ? wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) )
+ : wfMsg( 'prefixindex' )
+ );
+
+ if( isset( $par ) ){
+ $this->showPrefixChunk( $namespace, $par, $from );
+ } elseif( isset( $prefix ) ){
+ $this->showPrefixChunk( $namespace, $prefix, $from );
+ } elseif( isset( $from ) ){
+ $this->showPrefixChunk( $namespace, $from, $from );
+ } else {
+ $wgOut->addHTML( $this->namespacePrefixForm( $namespace, null ) );
+ }
+ }
+
+ /**
+ * HTML for the top form
+ * @param integer $namespace A namespace constant (default NS_MAIN).
+ * @param string $from dbKey we are starting listing at.
+ */
+ function namespacePrefixForm( $namespace = NS_MAIN, $from = '' ) {
+ global $wgScript;
+ $t = $this->getTitle();
+
+ $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( 'fieldset' );
+ $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) );
+ $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) );
+ $out .= "<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'allpagesfrom' ), 'nsfrom' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::namespaceSelector( $namespace, null ) . ' ' .
+ Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
+ "</td>
+ </tr>";
+ $out .= Xml::closeElement( 'table' );
+ $out .= Xml::closeElement( 'fieldset' );
+ $out .= Xml::closeElement( 'form' );
+ $out .= Xml::closeElement( 'div' );
+ return $out;
+ }
/**
* @param integer $namespace (Default NS_MAIN)
* @param string $from list all pages from this name (default FALSE)
*/
- function showChunk( $namespace = NS_MAIN, $prefix, $including = false, $from = null ) {
+ function showPrefixChunk( $namespace = NS_MAIN, $prefix, $from = null ) {
global $wgOut, $wgUser, $wgContLang;
- $fname = 'indexShowChunk';
-
$sk = $wgUser->getSkin();
if (!isset($from)) $from = $prefix;
@@ -86,7 +122,7 @@ class SpecialPrefixindex extends SpecialAllpages {
'page_title LIKE \'' . $dbr->escapeLike( $prefixKey ) .'%\'',
'page_title >= ' . $dbr->addQuotes( $fromKey ),
),
- $fname,
+ __METHOD__,
array(
'ORDER BY' => 'page_title',
'LIMIT' => $this->maxPerPage + 1,
@@ -100,7 +136,7 @@ class SpecialPrefixindex extends SpecialAllpages {
if( $res->numRows() > 0 ) {
$out = '<table style="background: inherit;" border="0" width="100%">';
- while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
+ while( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) {
$t = Title::makeTitle( $s->page_namespace, $s->page_title );
if( $t ) {
$link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
@@ -127,26 +163,27 @@ class SpecialPrefixindex extends SpecialAllpages {
}
}
- if ( $including ) {
+ if ( $this->including() ) {
$out2 = '';
} else {
- $nsForm = $this->namespaceForm ( $namespace, $prefix );
+ $nsForm = $this->namespacePrefixForm( $namespace, $prefix );
+ $self = $this->getTitle();
$out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
$out2 .= '<tr valign="top"><td>' . $nsForm;
$out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' .
- $sk->makeKnownLink( $wgContLang->specialPage( $this->name ),
+ $sk->makeKnownLinkObj( $self,
wfMsg ( 'allpages' ) );
- if ( isset($dbr) && $dbr && ($n == $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
+ if( isset( $res ) && $res && ( $n == $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) {
$namespaceparam = $namespace ? "&namespace=$namespace" : "";
- $out2 .= " | " . $sk->makeKnownLink(
- $wgContLang->specialPage( $this->name ),
+ $out2 .= " | " . $sk->makeKnownLinkObj(
+ $self,
wfMsgHtml( 'nextpage', htmlspecialchars( $s->page_title ) ),
- "from=" . wfUrlEncode ( $s->page_title ) .
- "&prefix=" . wfUrlEncode ( $prefix ) . $namespaceparam );
+ "from=" . wfUrlEncode( $s->page_title ) .
+ "&prefix=" . wfUrlEncode( $prefix ) . $namespaceparam );
}
$out2 .= "</td></tr></table><hr />";
}
- $wgOut->addHtml( $out2 . $out );
+ $wgOut->addHTML( $out2 . $out );
}
}
diff --git a/includes/specials/SpecialProtectedpages.php b/includes/specials/SpecialProtectedpages.php
index 3025c055..4e56ca42 100644
--- a/includes/specials/SpecialProtectedpages.php
+++ b/includes/specials/SpecialProtectedpages.php
@@ -16,7 +16,6 @@ class ProtectedPagesForm {
public function showList( $msg = '' ) {
global $wgOut, $wgRequest;
- $wgOut->setPagetitle( wfMsg( "protectedpages" ) );
if ( "" != $msg ) {
$wgOut->setSubtitle( $msg );
}
@@ -32,10 +31,11 @@ class ProtectedPagesForm {
$size = $wgRequest->getIntOrNull( 'size' );
$NS = $wgRequest->getIntOrNull( 'namespace' );
$indefOnly = $wgRequest->getBool( 'indefonly' ) ? 1 : 0;
+ $cascadeOnly = $wgRequest->getBool('cascadeonly') ? 1 : 0;
- $pager = new ProtectedPagesPager( $this, array(), $type, $level, $NS, $sizetype, $size, $indefOnly );
+ $pager = new ProtectedPagesPager( $this, array(), $type, $level, $NS, $sizetype, $size, $indefOnly, $cascadeOnly );
- $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size, $indefOnly ) );
+ $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size, $indefOnly, $cascadeOnly ) );
if ( $pager->getNumRows() ) {
$s = $pager->getNavigationBar();
@@ -83,7 +83,7 @@ class ProtectedPagesForm {
if ( $row->pr_expiry != 'infinity' && strlen($row->pr_expiry) ) {
$expiry = Block::decodeExpiry( $row->pr_expiry );
- $expiry_description = wfMsgForContent( 'protect-expiring', $wgLang->timeanddate( $expiry ) );
+ $expiry_description = wfMsg( 'protect-expiring' , $wgLang->timeanddate( $expiry ) , $wgLang->date( $expiry ) , $wgLang->time( $expiry ) );
$description_items[] = $expiry_description;
}
@@ -111,21 +111,24 @@ class ProtectedPagesForm {
* @param $level string
* @param $minsize int
* @param $indefOnly bool
+ * @param $cascadeOnly bool
* @return string Input form
* @private
*/
- protected function showOptions( $namespace, $type='edit', $level, $sizetype, $size, $indefOnly ) {
+ protected function showOptions( $namespace, $type='edit', $level, $sizetype, $size, $indefOnly, $cascadeOnly ) {
global $wgScript;
$title = SpecialPage::getTitleFor( 'ProtectedPages' );
return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', array(), wfMsg( 'protectedpages' ) ) .
- Xml::hidden( 'title', $title->getPrefixedDBkey() ) . "&nbsp;\n" .
+ Xml::hidden( 'title', $title->getPrefixedDBkey() ) . "\n" .
$this->getNamespaceMenu( $namespace ) . "&nbsp;\n" .
$this->getTypeMenu( $type ) . "&nbsp;\n" .
$this->getLevelMenu( $level ) . "&nbsp;\n" .
- "<br /><span style='white-space: nowrap'>&nbsp;&nbsp;" .
+ "<br/><span style='white-space: nowrap'>" .
$this->getExpiryCheck( $indefOnly ) . "&nbsp;\n" .
+ $this->getCascadeCheck( $cascadeOnly ) . "&nbsp;\n" .
+ "</span><br/><span style='white-space: nowrap'>" .
$this->getSizeLimit( $sizetype, $size ) . "&nbsp;\n" .
"</span>" .
"&nbsp;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
@@ -153,6 +156,14 @@ class ProtectedPagesForm {
return
Xml::checkLabel( wfMsg('protectedpages-indef'), 'indefonly', 'indefonly', $indefOnly ) . "\n";
}
+
+ /**
+ * @return string Formatted HTML
+ */
+ protected function getCascadeCheck( $cascadeOnly ) {
+ return
+ Xml::checkLabel( wfMsg('protectedpages-cascade'), 'cascadeonly', 'cascadeonly', $cascadeOnly ) . "\n";
+ }
/**
* @return string Formatted HTML
@@ -237,7 +248,8 @@ class ProtectedPagesPager extends AlphabeticPager {
public $mForm, $mConds;
private $type, $level, $namespace, $sizetype, $size, $indefonly;
- function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0, $indefonly=false ) {
+ function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='',
+ $size=0, $indefonly = false, $cascadeonly = false ) {
$this->mForm = $form;
$this->mConds = $conds;
$this->type = ( $type ) ? $type : 'edit';
@@ -246,6 +258,7 @@ class ProtectedPagesPager extends AlphabeticPager {
$this->sizetype = $sizetype;
$this->size = intval($size);
$this->indefonly = (bool)$indefonly;
+ $this->cascadeonly = (bool)$cascadeonly;
parent::__construct();
}
@@ -281,6 +294,9 @@ class ProtectedPagesPager extends AlphabeticPager {
if( $this->indefonly ) {
$conds[] = "pr_expiry = 'infinity' OR pr_expiry IS NULL";
}
+ if ( $this->cascadeonly ) {
+ $conds[] = "pr_cascade = '1'";
+ }
if( $this->level )
$conds[] = 'pr_level=' . $this->mDb->addQuotes( $this->level );
diff --git a/includes/specials/SpecialProtectedtitles.php b/includes/specials/SpecialProtectedtitles.php
index 2ec68a66..7e8126d9 100644
--- a/includes/specials/SpecialProtectedtitles.php
+++ b/includes/specials/SpecialProtectedtitles.php
@@ -16,7 +16,6 @@ class ProtectedTitlesForm {
function showList( $msg = '' ) {
global $wgOut, $wgRequest;
- $wgOut->setPagetitle( wfMsg( "protectedtitles" ) );
if ( "" != $msg ) {
$wgOut->setSubtitle( $msg );
}
@@ -75,7 +74,7 @@ class ProtectedTitlesForm {
if ( $row->pt_expiry != 'infinity' && strlen($row->pt_expiry) ) {
$expiry = Block::decodeExpiry( $row->pt_expiry );
- $expiry_description = wfMsgForContent( 'protect-expiring', $wgLang->timeanddate( $expiry ) );
+ $expiry_description = wfMsg( 'protect-expiring', $wgLang->timeanddate( $expiry ) , $wgLang->date( $expiry ) , $wgLang->time( $expiry ) );
$description_items[] = $expiry_description;
}
@@ -102,7 +101,7 @@ class ProtectedTitlesForm {
Xml::element( 'legend', array(), wfMsg( 'protectedtitles' ) ) .
Xml::hidden( 'title', $special ) . "&nbsp;\n" .
$this->getNamespaceMenu( $namespace ) . "&nbsp;\n" .
- // $this->getLevelMenu( $level ) . "<br/>\n" .
+ $this->getLevelMenu( $level ) . "&nbsp;\n" .
"&nbsp;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
"</fieldset></form>";
}
@@ -137,7 +136,10 @@ class ProtectedTitlesForm {
$m[$text] = $type;
}
}
-
+ // Is there only one level (aside from "all")?
+ if( count($m) <= 2 ) {
+ return '';
+ }
// Third pass generates sorted XHTML content
foreach( $m as $text => $type ) {
$selected = ($type == $pr_level );
@@ -190,7 +192,8 @@ class ProtectedtitlesPager extends AlphabeticPager {
function getQueryInfo() {
$conds = $this->mConds;
$conds[] = 'pt_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
-
+ if( $this->level )
+ $conds['pt_create_perm'] = $this->level;
if( !is_null($this->namespace) )
$conds[] = 'pt_namespace=' . $this->mDb->addQuotes( $this->namespace );
return array(
diff --git a/includes/specials/SpecialRandompage.php b/includes/specials/SpecialRandompage.php
index 0e7ada1d..f4bff30b 100644
--- a/includes/specials/SpecialRandompage.php
+++ b/includes/specials/SpecialRandompage.php
@@ -8,19 +8,23 @@
* @license GNU General Public Licence 2.0 or later
*/
class RandomPage extends SpecialPage {
- private $namespace = NS_MAIN; // namespace to select pages from
+ private $namespaces; // namespaces to select pages from
function __construct( $name = 'Randompage' ){
+ global $wgContentNamespaces;
+
+ $this->namespaces = $wgContentNamespaces;
+
parent::__construct( $name );
}
- public function getNamespace() {
- return $this->namespace;
+ public function getNamespaces() {
+ return $this->namespaces;
}
public function setNamespace ( $ns ) {
if( $ns < NS_MAIN ) $ns = NS_MAIN;
- $this->namespace = $ns;
+ $this->namespaces = array( $ns );
}
// select redirects instead of normal pages?
@@ -39,7 +43,7 @@ class RandomPage extends SpecialPage {
if( is_null( $title ) ) {
$this->setHeaders();
- $wgOut->addWikiMsg( strtolower( $this->mName ) . '-nopages' );
+ $wgOut->addWikiMsg( strtolower( $this->mName ) . '-nopages', $wgContLang->getNsText( $this->namespace ) );
return;
}
@@ -67,7 +71,7 @@ class RandomPage extends SpecialPage {
$row = $this->selectRandomPageFromDB( "0" );
if( $row )
- return Title::makeTitleSafe( $this->namespace, $row->page_title );
+ return Title::makeTitleSafe( $row->page_namespace, $row->page_title );
else
return null;
}
@@ -81,13 +85,13 @@ class RandomPage extends SpecialPage {
$use_index = $dbr->useIndexClause( 'page_random' );
$page = $dbr->tableName( 'page' );
- $ns = (int) $this->namespace;
+ $ns = implode( ",", $this->namespaces );
$redirect = $this->isRedirect() ? 1 : 0;
$extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : "";
- $sql = "SELECT page_title
+ $sql = "SELECT page_title, page_namespace
FROM $page $use_index
- WHERE page_namespace = $ns
+ WHERE page_namespace IN ( $ns )
AND page_is_redirect = $redirect
AND page_random >= $randstr
$extra
diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php
index cb718bdc..8c14e1fc 100644
--- a/includes/specials/SpecialRecentchanges.php
+++ b/includes/specials/SpecialRecentchanges.php
@@ -6,7 +6,7 @@
*/
class SpecialRecentChanges extends SpecialPage {
public function __construct() {
- SpecialPage::SpecialPage( 'Recentchanges' );
+ parent::__construct( 'Recentchanges' );
$this->includable( true );
}
@@ -16,13 +16,14 @@ class SpecialRecentChanges extends SpecialPage {
* @return FormOptions
*/
public function getDefaultOptions() {
+ global $wgUser;
$opts = new FormOptions();
- $opts->add( 'days', (int)User::getDefaultOption( 'rcdays' ) );
- $opts->add( 'limit', (int)User::getDefaultOption( 'rclimit' ) );
+ $opts->add( 'days', (int)$wgUser->getOption( 'rcdays' ) );
+ $opts->add( 'limit', (int)$wgUser->getOption( 'rclimit' ) );
$opts->add( 'from', '' );
- $opts->add( 'hideminor', false );
+ $opts->add( 'hideminor', (bool)$wgUser->getOption( 'hideminor' ) );
$opts->add( 'hidebots', true );
$opts->add( 'hideanons', false );
$opts->add( 'hideliu', false );
@@ -34,7 +35,6 @@ class SpecialRecentChanges extends SpecialPage {
$opts->add( 'categories', '' );
$opts->add( 'categories_any', false );
-
return $opts;
}
@@ -44,16 +44,13 @@ class SpecialRecentChanges extends SpecialPage {
* @return FormOptions
*/
public function setup( $parameters ) {
- global $wgUser, $wgRequest;
+ global $wgRequest;
$opts = $this->getDefaultOptions();
- $opts['days'] = (int)$wgUser->getOption( 'rcdays', $opts['days'] );
- $opts['limit'] = (int)$wgUser->getOption( 'rclimit', $opts['limit'] );
- $opts['hideminor'] = $wgUser->getOption( 'hideminor', $opts['hideminor'] );
$opts->fetchValuesFromRequest( $wgRequest );
// Give precedence to subpage syntax
- if ( $parameters !== null ) {
+ if( $parameters !== null ) {
$this->parseParameters( $parameters, $opts );
}
@@ -85,9 +82,9 @@ class SpecialRecentChanges extends SpecialPage {
# 10 seconds server-side caching max
$wgOut->setSquidMaxage( 10 );
-
+ # Check if the client has a cached version
$lastmod = $this->checkLastModified( $feedFormat );
- if( $lastmod === false ){
+ if( $lastmod === false ) {
return;
}
@@ -97,33 +94,32 @@ class SpecialRecentChanges extends SpecialPage {
// Fetch results, prepare a batch link existence check query
$rows = array();
- $batch = new LinkBatch;
$conds = $this->buildMainQueryConds( $opts );
- $res = $this->doMainQuery( $conds, $opts );
- if( $res === false ){
- $this->doHeader( $opts );
+ $rows = $this->doMainQuery( $conds, $opts );
+ if( $rows === false ){
+ if( !$this->including() ) {
+ $this->doHeader( $opts );
+ }
return;
}
- $dbr = wfGetDB( DB_SLAVE );
- while( $row = $dbr->fetchObject( $res ) ){
- $rows[] = $row;
- if ( !$feedFormat ) {
- // User page and talk links
+
+ if( !$feedFormat ) {
+ $batch = new LinkBatch;
+ foreach( $rows as $row ) {
$batch->add( NS_USER, $row->rc_user_text );
$batch->add( NS_USER_TALK, $row->rc_user_text );
}
-
+ $batch->execute();
}
- $dbr->freeResult( $res );
- if ( $feedFormat ) {
+ if( $feedFormat ) {
list( $feed, $feedObj ) = $this->getFeedObject( $feedFormat );
$feed->execute( $feedObj, $rows, $opts['limit'], $opts['hideminor'], $lastmod );
} else {
- $batch->execute();
$this->webOutput( $rows, $opts );
}
-
+
+ $rows->free();
}
/**
@@ -149,21 +145,21 @@ class SpecialRecentChanges extends SpecialPage {
*/
public function parseParameters( $par, FormOptions $opts ) {
$bits = preg_split( '/\s*,\s*/', trim( $par ) );
- foreach ( $bits as $bit ) {
- if ( 'hidebots' === $bit ) $opts['hidebots'] = true;
- if ( 'bots' === $bit ) $opts['hidebots'] = false;
- if ( 'hideminor' === $bit ) $opts['hideminor'] = true;
- if ( 'minor' === $bit ) $opts['hideminor'] = false;
- if ( 'hideliu' === $bit ) $opts['hideliu'] = true;
- if ( 'hidepatrolled' === $bit ) $opts['hidepatrolled'] = true;
- if ( 'hideanons' === $bit ) $opts['hideanons'] = true;
- if ( 'hidemyself' === $bit ) $opts['hidemyself'] = true;
-
- if ( is_numeric( $bit ) ) $opts['limit'] = $bit;
+ foreach( $bits as $bit ) {
+ if( 'hidebots' === $bit ) $opts['hidebots'] = true;
+ if( 'bots' === $bit ) $opts['hidebots'] = false;
+ if( 'hideminor' === $bit ) $opts['hideminor'] = true;
+ if( 'minor' === $bit ) $opts['hideminor'] = false;
+ if( 'hideliu' === $bit ) $opts['hideliu'] = true;
+ if( 'hidepatrolled' === $bit ) $opts['hidepatrolled'] = true;
+ if( 'hideanons' === $bit ) $opts['hideanons'] = true;
+ if( 'hidemyself' === $bit ) $opts['hidemyself'] = true;
+
+ if( is_numeric( $bit ) ) $opts['limit'] = $bit;
$m = array();
- if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) $opts['limit'] = $m[1];
- if ( preg_match( '/^days=(\d+)$/', $bit, $m ) ) $opts['days'] = $m[1];
+ if( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) $opts['limit'] = $m[1];
+ if( preg_match( '/^days=(\d+)$/', $bit, $m ) ) $opts['days'] = $m[1];
}
}
@@ -173,14 +169,14 @@ class SpecialRecentChanges extends SpecialPage {
* update the timestamp
*
* @param $feedFormat String
- * @return int or false
+ * @return string or false
*/
public function checkLastModified( $feedFormat ) {
global $wgUseRCPatrol, $wgOut;
$dbr = wfGetDB( DB_SLAVE );
$lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __FUNCTION__ );
- if ( $feedFormat || !$wgUseRCPatrol ) {
- if( $lastmod && $wgOut->checkLastModified( $lastmod ) ){
+ if( $feedFormat || !$wgUseRCPatrol ) {
+ if( $lastmod && $wgOut->checkLastModified( $lastmod ) ) {
# Client cache fresh and headers sent, nothing more to do.
return false;
}
@@ -232,12 +228,12 @@ class SpecialRecentChanges extends SpecialPage {
$hideLoggedInUsers = $opts['hideliu'] && !$forcebot;
$hideAnonymousUsers = $opts['hideanons'] && !$forcebot;
- if ( $opts['hideminor'] ) $conds['rc_minor'] = 0;
- if ( $opts['hidebots'] ) $conds['rc_bot'] = 0;
- if ( $hidePatrol ) $conds['rc_patrolled'] = 0;
- if ( $forcebot ) $conds['rc_bot'] = 1;
- if ( $hideLoggedInUsers ) $conds[] = 'rc_user = 0';
- if ( $hideAnonymousUsers ) $conds[] = 'rc_user != 0';
+ if( $opts['hideminor'] ) $conds['rc_minor'] = 0;
+ if( $opts['hidebots'] ) $conds['rc_bot'] = 0;
+ if( $hidePatrol ) $conds['rc_patrolled'] = 0;
+ if( $forcebot ) $conds['rc_bot'] = 1;
+ if( $hideLoggedInUsers ) $conds[] = 'rc_user = 0';
+ if( $hideAnonymousUsers ) $conds[] = 'rc_user != 0';
if( $opts['hidemyself'] ) {
if( $wgUser->getId() ) {
@@ -248,8 +244,8 @@ class SpecialRecentChanges extends SpecialPage {
}
# Namespace filtering
- if ( $opts['namespace'] !== '' ) {
- if ( !$opts['invert'] ) {
+ if( $opts['namespace'] !== '' ) {
+ if( !$opts['invert'] ) {
$conds[] = 'rc_namespace = ' . $dbr->addQuotes( $opts['namespace'] );
} else {
$conds[] = 'rc_namespace != ' . $dbr->addQuotes( $opts['namespace'] );
@@ -281,7 +277,8 @@ class SpecialRecentChanges extends SpecialPage {
// JOIN on watchlist for users
if( $uid ) {
$tables[] = 'watchlist';
- $join_conds = array( 'watchlist' => array('LEFT JOIN',"wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace") );
+ $join_conds = array( 'watchlist' => array('LEFT JOIN',
+ "wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace") );
}
wfRunHooks('SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts ) );
@@ -329,7 +326,7 @@ class SpecialRecentChanges extends SpecialPage {
$limit = $opts['limit'];
- if ( !$this->including() ) {
+ if( !$this->including() ) {
// Output options box
$this->doHeader( $opts );
}
@@ -337,55 +334,46 @@ class SpecialRecentChanges extends SpecialPage {
// And now for the content
$wgOut->setSyndicated( true );
- $list = ChangesList::newFromUser( $wgUser );
-
- if ( $wgAllowCategorizedRecentChanges ) {
+ if( $wgAllowCategorizedRecentChanges ) {
$this->filterByCategories( $rows, $opts );
}
- $s = $list->beginRecentChangesList();
- $counter = 1;
-
$showWatcherCount = $wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' );
$watcherCache = array();
$dbr = wfGetDB( DB_SLAVE );
- foreach( $rows as $obj ){
- if( $limit == 0) {
- break;
- }
-
- if ( ! ( $opts['hideminor'] && $obj->rc_minor ) &&
- ! ( $opts['hidepatrolled'] && $obj->rc_patrolled ) ) {
- $rc = RecentChange::newFromRow( $obj );
- $rc->counter = $counter++;
-
- if ($wgShowUpdatedMarker
- && !empty( $obj->wl_notificationtimestamp )
- && ($obj->rc_timestamp >= $obj->wl_notificationtimestamp)) {
- $rc->notificationtimestamp = true;
- } else {
- $rc->notificationtimestamp = false;
- }
+ $counter = 1;
+ $list = ChangesList::newFromUser( $wgUser );
- $rc->numberofWatchingusers = 0; // Default
- if ($showWatcherCount && $obj->rc_namespace >= 0) {
- if (!isset($watcherCache[$obj->rc_namespace][$obj->rc_title])) {
- $watcherCache[$obj->rc_namespace][$obj->rc_title] =
- $dbr->selectField( 'watchlist',
- 'COUNT(*)',
- array(
- 'wl_namespace' => $obj->rc_namespace,
- 'wl_title' => $obj->rc_title,
- ),
- __METHOD__ . '-watchers' );
- }
- $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
+ $s = $list->beginRecentChangesList();
+ foreach( $rows as $obj ) {
+ if( $limit == 0 ) break;
+ $rc = RecentChange::newFromRow( $obj );
+ $rc->counter = $counter++;
+ # Check if the page has been updated since the last visit
+ if( $wgShowUpdatedMarker && !empty($obj->wl_notificationtimestamp) ) {
+ $rc->notificationtimestamp = ($obj->rc_timestamp >= $obj->wl_notificationtimestamp);
+ } else {
+ $rc->notificationtimestamp = false; // Default
+ }
+ # Check the number of users watching the page
+ $rc->numberofWatchingusers = 0; // Default
+ if( $showWatcherCount && $obj->rc_namespace >= 0 ) {
+ if( !isset($watcherCache[$obj->rc_namespace][$obj->rc_title]) ) {
+ $watcherCache[$obj->rc_namespace][$obj->rc_title] =
+ $dbr->selectField( 'watchlist',
+ 'COUNT(*)',
+ array(
+ 'wl_namespace' => $obj->rc_namespace,
+ 'wl_title' => $obj->rc_title,
+ ),
+ __METHOD__ . '-watchers' );
}
- $s .= $list->recentChangesLine( $rc, !empty( $obj->wl_user ) );
- --$limit;
+ $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
}
+ $s .= $list->recentChangesLine( $rc, !empty( $obj->wl_user ) );
+ --$limit;
}
$s .= $list->endRecentChangesList();
$wgOut->addHTML( $s );
@@ -411,22 +399,29 @@ class SpecialRecentChanges extends SpecialPage {
$panel[] = '<hr />';
$extraOpts = $this->getExtraOptions( $opts );
+ $extraOptsCount = count( $extraOpts );
+ $count = 0;
+ $submit = ' ' . Xml::submitbutton( wfMsg( 'allpagessubmit' ) );
+
+ $out = Xml::openElement( 'table', array( 'class' => 'mw-recentchanges-table' ) );
+ foreach( $extraOpts as $optionRow ) {
+ # Add submit button to the last row only
+ ++$count;
+ $addSubmit = $count === $extraOptsCount ? $submit : '';
- $out = Xml::openElement( 'table' );
- foreach ( $extraOpts as $optionRow ) {
$out .= Xml::openElement( 'tr' );
- if ( is_array($optionRow) ) {
- $out .= Xml::tags( 'td', null, $optionRow[0] );
- $out .= Xml::tags( 'td', null, $optionRow[1] );
+ if( is_array( $optionRow ) ) {
+ $out .= Xml::tags( 'td', array( 'class' => 'mw-label' ), $optionRow[0] );
+ $out .= Xml::tags( 'td', array( 'class' => 'mw-input' ), $optionRow[1] . $addSubmit );
} else {
- $out .= Xml::tags( 'td', array( 'colspan' => 2 ), $optionRow );
+ $out .= Xml::tags( 'td', array( 'class' => 'mw-input', 'colspan' => 2 ), $optionRow . $addSubmit );
}
$out .= Xml::closeElement( 'tr' );
}
$out .= Xml::closeElement( 'table' );
$unconsumed = $opts->getUnconsumedValues();
- foreach ( $unconsumed as $key => $value ) {
+ foreach( $unconsumed as $key => $value ) {
$out .= Xml::hidden( $key, $value );
}
@@ -437,7 +432,7 @@ class SpecialRecentChanges extends SpecialPage {
$panelString = implode( "\n", $panel );
$wgOut->addHTML(
- Xml::fieldset( wfMsg( strtolower( $this->mName ) ), $panelString, array( 'class' => 'rcoptions' ) )
+ Xml::fieldset( wfMsg( 'recentchanges-legend' ), $panelString, array( 'class' => 'rcoptions' ) )
);
$this->setBottomText( $wgOut, $opts );
@@ -454,12 +449,11 @@ class SpecialRecentChanges extends SpecialPage {
$extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
global $wgAllowCategorizedRecentChanges;
- if ( $wgAllowCategorizedRecentChanges ) {
+ if( $wgAllowCategorizedRecentChanges ) {
$extraOpts['category'] = $this->categoryFilterForm( $opts );
}
wfRunHooks( 'SpecialRecentChangesPanel', array( &$extraOpts, $opts ) );
- $extraOpts['submit'] = Xml::submitbutton( wfMsg('allpagessubmit') );
return $extraOpts;
}
@@ -469,7 +463,7 @@ class SpecialRecentChanges extends SpecialPage {
* @param $out OutputPage
* @param $opts FormOptions
*/
- function setTopText( &$out, $opts ){
+ function setTopText( OutputPage $out, FormOptions $opts ){
$out->addWikiText( wfMsgForContentNoTrans( 'recentchangestext' ) );
}
@@ -480,7 +474,7 @@ class SpecialRecentChanges extends SpecialPage {
* @param $out OutputPage
* @param $opts FormOptions
*/
- function setBottomText( &$out, $opts ){}
+ function setBottomText( OutputPage $out, FormOptions $opts ){}
/**
* Creates the choose namespace selection
@@ -489,7 +483,7 @@ class SpecialRecentChanges extends SpecialPage {
* @return string
*/
protected function namespaceFilterForm( FormOptions $opts ) {
- $nsSelect = HTMLnamespaceselector( $opts['namespace'], '' );
+ $nsSelect = Xml::namespaceSelector( $opts['namespace'], '' );
$nsLabel = Xml::label( wfMsg('namespace'), 'namespace' );
$invert = Xml::checkLabel( wfMsg('invert'), 'invert', 'nsinvert', $opts['invert'] );
return array( $nsLabel, "$nsSelect $invert" );
@@ -526,30 +520,30 @@ class SpecialRecentChanges extends SpecialPage {
# Filter categories
$cats = array();
- foreach ( $categories as $cat ) {
+ foreach( $categories as $cat ) {
$cat = trim( $cat );
- if ( $cat == "" ) continue;
+ if( $cat == "" ) continue;
$cats[] = $cat;
}
# Filter articles
$articles = array();
$a2r = array();
- foreach ( $rows AS $k => $r ) {
+ foreach( $rows AS $k => $r ) {
$nt = Title::makeTitle( $r->rc_namespace, $r->rc_title );
$id = $nt->getArticleID();
- if ( $id == 0 ) continue; # Page might have been deleted...
- if ( !in_array($id, $articles) ) {
+ if( $id == 0 ) continue; # Page might have been deleted...
+ if( !in_array($id, $articles) ) {
$articles[] = $id;
}
- if ( !isset($a2r[$id]) ) {
+ if( !isset($a2r[$id]) ) {
$a2r[$id] = array();
}
$a2r[$id][] = $k;
}
# Shortcut?
- if ( !count($articles) || !count($cats) )
+ if( !count($articles) || !count($cats) )
return ;
# Look up
@@ -559,8 +553,8 @@ class SpecialRecentChanges extends SpecialPage {
# Filter
$newrows = array();
- foreach ( $match AS $id ) {
- foreach ( $a2r[$id] AS $rev ) {
+ foreach( $match AS $id ) {
+ foreach( $a2r[$id] AS $rev ) {
$k = $rev;
$newrows[$k] = $rows[$k];
}
@@ -577,8 +571,9 @@ class SpecialRecentChanges extends SpecialPage {
function makeOptionsLink( $title, $override, $options, $active = false ) {
global $wgUser;
$sk = $wgUser->getSkin();
- return $sk->makeKnownLinkObj( $this->getTitle(), htmlspecialchars( $title ),
- wfArrayToCGI( $override, $options ), '', '', $active ? 'style="font-weight: bold;"' : '' );
+ $params = $override + $options;
+ return $sk->link( $this->getTitle(), htmlspecialchars( $title ),
+ ( $active ? array( 'style'=>'font-weight: bold;' ) : array() ), $params, array( 'known' ) );
}
/**
@@ -591,43 +586,41 @@ class SpecialRecentChanges extends SpecialPage {
$options = $nondefaults + $defaults;
- if( $options['from'] )
- $note = wfMsgExt( 'rcnotefrom', array( 'parseinline' ),
+ $note = '';
+ if( $options['from'] ) {
+ $note .= wfMsgExt( 'rcnotefrom', array( 'parseinline' ),
$wgLang->formatNum( $options['limit'] ),
- $wgLang->timeanddate( $options['from'], true ) );
- else
- $note = wfMsgExt( 'rcnote', array( 'parseinline' ),
- $wgLang->formatNum( $options['limit'] ),
- $wgLang->formatNum( $options['days'] ),
- $wgLang->timeAndDate( wfTimestampNow(), true ),
- $wgLang->date( wfTimestampNow(), true ),
- $wgLang->time( wfTimestampNow(), true ) );
+ $wgLang->timeanddate( $options['from'], true ) ) . '<br />';
+ }
+ if( !wfEmptyMsg( 'rclegend', wfMsg('rclegend') ) ) {
+ $note .= wfMsgExt( 'rclegend', array('parseinline') ) . '<br />';
+ }
# Sort data for display and make sure it's unique after we've added user data.
$wgRCLinkLimits[] = $options['limit'];
$wgRCLinkDays[] = $options['days'];
- sort($wgRCLinkLimits);
- sort($wgRCLinkDays);
- $wgRCLinkLimits = array_unique($wgRCLinkLimits);
- $wgRCLinkDays = array_unique($wgRCLinkDays);
+ sort( $wgRCLinkLimits );
+ sort( $wgRCLinkDays );
+ $wgRCLinkLimits = array_unique( $wgRCLinkLimits );
+ $wgRCLinkDays = array_unique( $wgRCLinkDays );
// limit links
foreach( $wgRCLinkLimits as $value ) {
$cl[] = $this->makeOptionsLink( $wgLang->formatNum( $value ),
array( 'limit' => $value ), $nondefaults, $value == $options['limit'] ) ;
}
- $cl = implode( ' | ', $cl);
+ $cl = implode( ' | ', $cl );
// day links, reset 'from' to none
foreach( $wgRCLinkDays as $value ) {
$dl[] = $this->makeOptionsLink( $wgLang->formatNum( $value ),
array( 'days' => $value, 'from' => '' ), $nondefaults, $value == $options['days'] ) ;
}
- $dl = implode( ' | ', $dl);
+ $dl = implode( ' | ', $dl );
// show/hide links
- $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' ));
+ $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' ) );
$minorLink = $this->makeOptionsLink( $showhide[1-$options['hideminor']],
array( 'hideminor' => 1-$options['hideminor'] ), $nondefaults);
$botLink = $this->makeOptionsLink( $showhide[1-$options['hidebots']],
@@ -652,11 +645,11 @@ class SpecialRecentChanges extends SpecialPage {
// show from this onward link
$now = $wgLang->timeanddate( wfTimestampNow(), true );
- $tl = $this->makeOptionsLink( $now, array( 'from' => wfTimestampNow()), $nondefaults );
+ $tl = $this->makeOptionsLink( $now, array( 'from' => wfTimestampNow() ), $nondefaults );
- $rclinks = wfMsgExt( 'rclinks', array( 'parseinline', 'replaceafter'),
+ $rclinks = wfMsgExt( 'rclinks', array( 'parseinline', 'replaceafter' ),
$cl, $dl, $hl );
- $rclistfrom = wfMsgExt( 'rclistfrom', array( 'parseinline', 'replaceafter'), $tl );
- return "$note<br />$rclinks<br />$rclistfrom";
+ $rclistfrom = wfMsgExt( 'rclistfrom', array( 'parseinline', 'replaceafter' ), $tl );
+ return "{$note}$rclinks<br />$rclistfrom";
}
}
diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php
index d773fb77..c0734354 100644
--- a/includes/specials/SpecialRecentchangeslinked.php
+++ b/includes/specials/SpecialRecentchangeslinked.php
@@ -7,7 +7,8 @@
class SpecialRecentchangeslinked extends SpecialRecentchanges {
function __construct(){
- SpecialPage::SpecialPage( 'Recentchangeslinked' );
+ SpecialPage::SpecialPage( 'Recentchangeslinked' );
+ $this->includable( true );
}
public function getDefaultOptions() {
@@ -92,10 +93,13 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges {
} else {
// for now, always join on these tables; really should be configurable as in whatlinkshere
$link_tables = array( 'pagelinks', 'templatelinks' );
- // imagelinks only contains links to pages in NS_IMAGE
- if( $ns == NS_IMAGE || !$showlinkedto ) $link_tables[] = 'imagelinks';
+ // imagelinks only contains links to pages in NS_FILE
+ if( $ns == NS_FILE || !$showlinkedto ) $link_tables[] = 'imagelinks';
}
+ if( $id == 0 && !$showlinkedto )
+ return false; // nonexistent pages can't link to any pages
+
// field name prefixes for all the various tables we might want to join with
$prefix = array( 'pagelinks' => 'pl', 'templatelinks' => 'tl', 'categorylinks' => 'cl', 'imagelinks' => 'il' );
@@ -105,7 +109,7 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges {
$pfx = $prefix[$link_table];
// imagelinks and categorylinks tables have no xx_namespace field, and have xx_to instead of xx_title
- if( $link_table == 'imagelinks' ) $link_ns = NS_IMAGE;
+ if( $link_table == 'imagelinks' ) $link_ns = NS_FILE;
else if( $link_table == 'categorylinks' ) $link_ns = NS_CATEGORY;
else $link_ns = 0;
@@ -145,7 +149,7 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges {
$res = $dbr->query( $sql, __METHOD__ );
- if( $dbr->numRows( $res ) == 0 )
+ if( $res->numRows() == 0 )
$this->mResultEmpty = true;
return $res;
@@ -159,17 +163,21 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges {
Xml::input( 'target', 40, str_replace('_',' ',$opts['target']) ) .
Xml::check( 'showlinkedto', $opts['showlinkedto'], array('id' => 'showlinkedto') ) . ' ' .
Xml::label( wfMsg("recentchangeslinked-to"), 'showlinkedto' ) );
- $extraOpts['submit'] = Xml::submitbutton( wfMsg('allpagessubmit') );
return $extraOpts;
}
-
- function setTopText( &$out, $opts ){}
-
- function setBottomText( &$out, $opts ){
+
+ function setTopText( OutputPage $out, FormOptions $opts ) {
+ global $wgUser;
+ $skin = $wgUser->getSkin();
+ if( isset( $this->mTargetTitle ) && is_object( $this->mTargetTitle ) )
+ $out->setSubtitle( wfMsg( 'recentchangeslinked-backlink', $skin->link( $this->mTargetTitle,
+ $this->mTargetTitle->getPrefixedText(), array(), array( 'redirect' => 'no' ) ) ) );
+ }
+
+ function setBottomText( OutputPage $out, FormOptions $opts ){
if( isset( $this->mTargetTitle ) && is_object( $this->mTargetTitle ) ){
global $wgUser;
$out->setFeedAppendQuery( "target=" . urlencode( $this->mTargetTitle->getPrefixedDBkey() ) );
- $out->addHTML("&lt; ".$wgUser->getSkin()->makeLinkObj( $this->mTargetTitle, "", "redirect=no" )."<hr />\n");
}
if( isset( $this->mResultEmpty ) && $this->mResultEmpty ){
$out->addWikiMsg( 'recentchangeslinked-noresult' );
diff --git a/includes/specials/SpecialRemoveRestrictions.php b/includes/specials/SpecialRemoveRestrictions.php
new file mode 100644
index 00000000..ded6cbe3
--- /dev/null
+++ b/includes/specials/SpecialRemoveRestrictions.php
@@ -0,0 +1,60 @@
+<?php
+
+function wfSpecialRemoveRestrictions() {
+ global $wgOut, $wgRequest, $wgUser, $wgLang, $wgTitle;
+ $sk = $wgUser->getSkin();
+
+ $id = $wgRequest->getVal( 'id' );
+ if( !is_numeric( $id ) ) {
+ $wgOut->addWikiMsg( 'removerestrictions-noid' );
+ return;
+ }
+
+ UserRestriction::purgeExpired();
+ $r = UserRestriction::newFromId( $id, true );
+ if( !$r ) {
+ $wgOut->addWikiMsg( 'removerestrictions-wrongid' );
+ return;
+ }
+
+ $form = array();
+ $form['removerestrictions-user'] = $sk->userLink( $r->getSubjectId(), $r->getSubjectText() ) .
+ $sk->userToolLinks( $r->getSubjectId(), $r->getSubjectText() );
+ $form['removerestrictions-type'] = UserRestriction::formatType( $r->getType() );
+ if( $r->isPage() )
+ $form['removerestrictions-page'] = $sk->link( $r->getPage() );
+ if( $r->isNamespace() )
+ $form['removerestrictions-namespace'] = $wgLang->getDisplayNsText( $r->getNamespace() );
+ $form['removerestrictions-reason'] = Xml::input( 'reason' );
+
+ $result = null;
+ if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'edittoken' ) ) )
+ $result = wfSpecialRemoveRestrictionsProcess( $r );
+
+ $wgOut->addWikiMsg( 'removerestrictions-intro' );
+ $wgOut->addHTML( Xml::fieldset( wfMsgHtml( 'removerestrictions-legend' ) ) );
+ if( $result )
+ $wgOut->addHTML( '<strong class="success">' . wfMsgExt( 'removerestrictions-success',
+ 'parseinline', $r->getSubjectText() ) . '</strong>' );
+ $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl( array( 'id' => $id ) ),
+ 'method' => 'post' ) ) );
+ $wgOut->addHTML( Xml::buildForm( $form, 'removerestrictions-submit' ) );
+ $wgOut->addHTML( Xml::hidden( 'id', $r->getId() ) );
+ $wgOut->addHTML( Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) );
+ $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) );
+ $wgOut->addHTML( "</form></fieldset>" );
+}
+
+function wfSpecialRemoveRestrictionsProcess( $r ) {
+ global $wgUser, $wgRequest;
+ $reason = $wgRequest->getVal( 'reason' );
+ $result = $r->delete();
+ $log = new LogPage( 'restrict' );
+ $params = array( $r->getType() );
+ if( $r->isPage() )
+ $params[] = $r->getPage()->getPrefixedDbKey();
+ if( $r->isNamespace() )
+ $params[] = $r->getNamespace();
+ $log->addEntry( 'remove', Title::makeTitle( NS_USER, $r->getSubjectText() ), $reason, $params );
+ return $result;
+}
diff --git a/includes/specials/SpecialResetpass.php b/includes/specials/SpecialResetpass.php
index 707b941d..059f8dbd 100644
--- a/includes/specials/SpecialResetpass.php
+++ b/includes/specials/SpecialResetpass.php
@@ -4,26 +4,13 @@
* @ingroup SpecialPage
*/
-/** Constructor */
-function wfSpecialResetpass( $par ) {
- $form = new PasswordResetForm();
- $form->execute( $par );
-}
-
/**
* Let users recover their password.
* @ingroup SpecialPage
*/
-class PasswordResetForm extends SpecialPage {
- function __construct( $name=null, $reset=null ) {
- if( $name !== null ) {
- $this->mName = $name;
- $this->mTemporaryPassword = $reset;
- } else {
- global $wgRequest;
- $this->mName = $wgRequest->getVal( 'wpName' );
- $this->mTemporaryPassword = $wgRequest->getVal( 'wpPassword' );
- }
+class SpecialResetpass extends SpecialPage {
+ public function __construct() {
+ parent::__construct( 'Resetpass' );
}
/**
@@ -32,36 +19,46 @@ class PasswordResetForm extends SpecialPage {
function execute( $par ) {
global $wgUser, $wgAuth, $wgOut, $wgRequest;
+ $this->mUserName = $wgRequest->getVal( 'wpName' );
+ $this->mOldpass = $wgRequest->getVal( 'wpPassword' );
+ $this->mNewpass = $wgRequest->getVal( 'wpNewPassword' );
+ $this->mRetype = $wgRequest->getVal( 'wpRetype' );
+
+ $this->setHeaders();
+ $this->outputHeader();
+
if( !$wgAuth->allowPasswordChange() ) {
$this->error( wfMsg( 'resetpass_forbidden' ) );
return;
}
- if( $this->mName === null && !$wgRequest->wasPosted() ) {
- $this->error( wfMsg( 'resetpass_missing' ) );
+ if( !$wgRequest->wasPosted() && !$wgUser->isLoggedIn() ) {
+ $this->error( wfMsg( 'resetpass-no-info' ) );
return;
}
- if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'token' ) ) ) {
- $newpass = $wgRequest->getVal( 'wpNewPassword' );
- $retype = $wgRequest->getVal( 'wpRetype' );
+ if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal('token') ) ) {
try {
- $this->attemptReset( $newpass, $retype );
+ $this->attemptReset( $this->mNewpass, $this->mRetype );
$wgOut->addWikiMsg( 'resetpass_success' );
-
- $data = array(
- 'action' => 'submitlogin',
- 'wpName' => $this->mName,
- 'wpPassword' => $newpass,
- 'returnto' => $wgRequest->getVal( 'returnto' ),
- );
- if( $wgRequest->getCheck( 'wpRemember' ) ) {
- $data['wpRemember'] = 1;
+ if( !$wgUser->isLoggedIn() ) {
+ $data = array(
+ 'action' => 'submitlogin',
+ 'wpName' => $this->mUserName,
+ 'wpPassword' => $this->mNewpass,
+ 'returnto' => $wgRequest->getVal( 'returnto' ),
+ );
+ if( $wgRequest->getCheck( 'wpRemember' ) ) {
+ $data['wpRemember'] = 1;
+ }
+ $login = new LoginForm( new FauxRequest( $data, true ) );
+ $login->execute();
}
- $login = new LoginForm( new FauxRequest( $data, true ) );
- $login->execute();
-
- return;
+ $titleObj = Title::newFromText( $wgRequest->getVal( 'returnto' ) );
+ if ( !$titleObj instanceof Title ) {
+ $titleObj = Title::newMainPage();
+ }
+ $wgOut->redirect( $titleObj->getFullURL() );
} catch( PasswordError $e ) {
$this->error( $e->getMessage() );
}
@@ -71,9 +68,7 @@ class PasswordResetForm extends SpecialPage {
function error( $msg ) {
global $wgOut;
- $wgOut->addHtml( '<div class="errorbox">' .
- htmlspecialchars( $msg ) .
- '</div>' );
+ $wgOut->addHTML( Xml::element('p', array( 'class' => 'error' ), $msg ) );
}
function showForm() {
@@ -82,44 +77,54 @@ class PasswordResetForm extends SpecialPage {
$wgOut->disallowUserJs();
$self = SpecialPage::getTitleFor( 'Resetpass' );
- $form =
- '<div id="userloginForm">' .
- wfOpenElement( 'form',
+ if ( !$this->mUserName ) {
+ $this->mUserName = $wgUser->getName();
+ }
+ $rememberMe = '';
+ if ( !$wgUser->isLoggedIn() ) {
+ $rememberMe = '<tr>' .
+ '<td></td>' .
+ '<td class="mw-input">' .
+ Xml::checkLabel( wfMsg( 'remembermypassword' ),
+ 'wpRemember', 'wpRemember',
+ $wgRequest->getCheck( 'wpRemember' ) ) .
+ '</td>' .
+ '</tr>';
+ $submitMsg = 'resetpass_submit';
+ $oldpassMsg = 'resetpass-temp-password';
+ } else {
+ $oldpassMsg = 'oldpassword';
+ $submitMsg = 'resetpass-submit-loggedin';
+ }
+ $wgOut->addHTML(
+ Xml::fieldset( wfMsg( 'resetpass_header' ) ) .
+ Xml::openElement( 'form',
array(
'method' => 'post',
- 'action' => $self->getLocalUrl() ) ) .
- '<h2>' . wfMsgHtml( 'resetpass_header' ) . '</h2>' .
- '<div id="userloginprompt">' .
+ 'action' => $self->getLocalUrl(),
+ 'id' => 'mw-resetpass-form' ) ) .
+ Xml::hidden( 'token', $wgUser->editToken() ) .
+ Xml::hidden( 'wpName', $this->mUserName ) .
+ Xml::hidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) .
wfMsgExt( 'resetpass_text', array( 'parse' ) ) .
- '</div>' .
- '<table>' .
- wfHidden( 'token', $wgUser->editToken() ) .
- wfHidden( 'wpName', $this->mName ) .
- wfHidden( 'wpPassword', $this->mTemporaryPassword ) .
- wfHidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-resetpass-table' ) ) .
$this->pretty( array(
- array( 'wpName', 'username', 'text', $this->mName ),
+ array( 'wpName', 'username', 'text', $this->mUserName ),
+ array( 'wpPassword', $oldpassMsg, 'password', $this->mOldpass ),
array( 'wpNewPassword', 'newpassword', 'password', '' ),
- array( 'wpRetype', 'yourpasswordagain', 'password', '' ),
+ array( 'wpRetype', 'retypenew', 'password', '' ),
) ) .
+ $rememberMe .
'<tr>' .
'<td></td>' .
- '<td>' .
- Xml::checkLabel( wfMsg( 'remembermypassword' ),
- 'wpRemember', 'wpRemember',
- $wgRequest->getCheck( 'wpRemember' ) ) .
- '</td>' .
- '</tr>' .
- '<tr>' .
- '<td></td>' .
- '<td>' .
- wfSubmitButton( wfMsgHtml( 'resetpass_submit' ) ) .
+ '<td class="mw-input">' .
+ Xml::submitButton( wfMsg( $submitMsg ) ) .
'</td>' .
'</tr>' .
- '</table>' .
- wfCloseElement( 'form' ) .
- '</div>';
- $wgOut->addHtml( $form );
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'form' ) .
+ Xml::closeElement( 'fieldset' )
+ );
}
function pretty( $fields ) {
@@ -127,16 +132,19 @@ class PasswordResetForm extends SpecialPage {
foreach( $fields as $list ) {
list( $name, $label, $type, $value ) = $list;
if( $type == 'text' ) {
- $field = '<tt>' . htmlspecialchars( $value ) . '</tt>';
+ $field = htmlspecialchars( $value );
} else {
$field = Xml::input( $name, 20, $value,
array( 'id' => $name, 'type' => $type ) );
}
$out .= '<tr>';
- $out .= '<td align="right">';
- $out .= Xml::label( wfMsg( $label ), $name );
+ $out .= "<td class='mw-label'>";
+ if ( $type != 'text' )
+ $out .= Xml::label( wfMsg( $label ), $name );
+ else
+ $out .= wfMsg( $label );
$out .= '</td>';
- $out .= '<td>';
+ $out .= "<td class='mw-input'>";
$out .= $field;
$out .= '</td>';
$out .= '</tr>';
@@ -147,21 +155,33 @@ class PasswordResetForm extends SpecialPage {
/**
* @throws PasswordError when cannot set the new password because requirements not met.
*/
- function attemptReset( $newpass, $retype ) {
- $user = User::newFromName( $this->mName );
- if( $user->isAnon() ) {
+ protected function attemptReset( $newpass, $retype ) {
+ $user = User::newFromName( $this->mUserName );
+ if( !$user || $user->isAnon() ) {
throw new PasswordError( 'no such user' );
}
-
- if( !$user->checkTemporaryPassword( $this->mTemporaryPassword ) ) {
- throw new PasswordError( wfMsg( 'resetpass_bad_temporary' ) );
- }
-
+
if( $newpass !== $retype ) {
+ wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'badretype' ) );
throw new PasswordError( wfMsg( 'badretype' ) );
}
- $user->setPassword( $newpass );
+ if( !$user->checkTemporaryPassword($this->mOldpass) && !$user->checkPassword($this->mOldpass) ) {
+ wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'wrongpassword' ) );
+ throw new PasswordError( wfMsg( 'resetpass-wrong-oldpass' ) );
+ }
+
+ try {
+ $user->setPassword( $this->mNewpass );
+ wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'success' ) );
+ $this->mNewpass = $this->mOldpass = $this->mRetypePass = '';
+ } catch( PasswordError $e ) {
+ wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'error' ) );
+ throw new PasswordError( $e->getMessage() );
+ return;
+ }
+
+ $user->setCookies();
$user->saveSettings();
}
}
diff --git a/includes/specials/SpecialRestrictUser.php b/includes/specials/SpecialRestrictUser.php
new file mode 100644
index 00000000..761e0cd6
--- /dev/null
+++ b/includes/specials/SpecialRestrictUser.php
@@ -0,0 +1,189 @@
+<?php
+
+function wfSpecialRestrictUser( $par = null ) {
+ global $wgOut, $wgRequest;
+ $user = $userOrig = null;
+ if( $par ) {
+ $userOrig = $par;
+ } elseif( $wgRequest->getVal( 'user' ) ) {
+ $userOrig = $wgRequest->getVal( 'user' );
+ } else {
+ $wgOut->addHTML( RestrictUserForm::selectUserForm() );
+ return;
+ }
+ $isIP = User::isIP( $userOrig );
+ $user = $isIP ? $userOrig : User::getCanonicalName( $userOrig );
+ $uid = User::idFromName( $user );
+ if( !$uid && !$isIP ) {
+ $err = '<strong class="error">' . wfMsgHtml( 'restrictuser-notfound' ) . '</strong>';
+ $wgOut->addHTML( RestrictUserForm::selectUserForm( $userOrig, $err ) );
+ return;
+ }
+ $wgOut->addHTML( RestrictUserForm::selectUserForm( $user ) );
+
+ UserRestriction::purgeExpired();
+ $old = UserRestriction::fetchForUser( $user, true );
+
+ RestrictUserForm::pageRestrictionForm( $uid, $user, $old );
+ RestrictUserForm::namespaceRestrictionForm( $uid, $user, $old );
+
+ // Renew it after possible changes in previous two functions
+ $old = UserRestriction::fetchForUser( $user, true );
+ if( $old ) {
+ $wgOut->addHTML( RestrictUserForm::existingRestrictions( $old ) );
+ }
+}
+
+class RestrictUserForm {
+ public static function selectUserForm( $val = null, $error = null ) {
+ global $wgScript, $wgTitle;
+ $s = Xml::fieldset( wfMsg( 'restrictuser-userselect' ) ) . "<form action=\"{$wgScript}\">";
+ if( $error )
+ $s .= '<p>' . $error . '</p>';
+ $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() );
+ $form = array( 'restrictuser-user' => Xml::input( 'user', false, $val ) );
+ $s .= Xml::buildForm( $form, 'restrictuser-go' );
+ $s .= "</form></fieldset>";
+ return $s;
+ }
+
+ public static function existingRestrictions( $restrictions ) {
+ //TODO: autoload?
+ require_once( dirname( __FILE__ ) . '/SpecialListUserRestrictions.php' );
+ $s = Xml::fieldset( wfMsg( 'restrictuser-existing' ) ) . '<ul>';
+ foreach( $restrictions as $r )
+ $s .= UserRestrictionsPager::formatRestriction( $r );
+ $s .= "</ul></fieldset>";
+ return $s;
+ }
+
+ public static function pageRestrictionForm( $uid, $user, $oldRestrictions ) {
+ global $wgOut, $wgTitle, $wgRequest, $wgUser;
+ $error = '';
+ $success = false;
+ if( $wgRequest->wasPosted() && $wgRequest->getVal( 'type' ) == UserRestriction::PAGE &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'edittoken' ) ) ) {
+
+ $title = Title::newFromText( $wgRequest->getVal( 'page' ) );
+ if( !$title ) {
+ $error = array( 'restrictuser-badtitle', $wgRequest->getVal( 'page' ) );
+ } elseif( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) === false ) {
+ $error = array( 'restrictuser-badexpiry', $wgRequest->getVal( 'expiry' ) );
+ } else {
+ foreach( $oldRestrictions as $r ) {
+ if( $r->isPage() && $r->getPage()->equals( $title ) )
+ $error = array( 'restrictuser-duptitle' );
+ }
+ }
+ if( !$error ) {
+ self::doPageRestriction( $uid, $user );
+ $success = array('restrictuser-success', $user);
+ }
+ }
+ $useRequestValues = $wgRequest->getVal( 'type' ) == UserRestriction::PAGE;
+ $wgOut->addHTML( Xml::fieldset( wfMsg( 'restrictuser-legend-page' ) ) );
+
+ self::printSuccessError( $success, $error );
+
+ $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl(),
+ 'method' => 'post' ) ) );
+ $wgOut->addHTML( Xml::hidden( 'type', UserRestriction::PAGE ) );
+ $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) );
+ $wgOut->addHTML( Xml::hidden( 'user', $user ) );
+ $form = array();
+ $form['restrictuser-title'] = Xml::input( 'page', false,
+ $useRequestValues ? $wgRequest->getVal( 'page' ) : false );
+ $form['restrictuser-expiry'] = Xml::input( 'expiry', false,
+ $useRequestValues ? $wgRequest->getVal( 'expiry' ) : false );
+ $form['restrictuser-reason'] = Xml::input( 'reason', false,
+ $useRequestValues ? $wgRequest->getVal( 'reason' ) : false );
+ $wgOut->addHTML( Xml::buildForm( $form, 'restrictuser-submit' ) );
+ $wgOut->addHTML( "</form></fieldset>" );
+ }
+
+ public static function printSuccessError( $success, $error ) {
+ global $wgOut;
+ if ( $error )
+ $wgOut->wrapWikiMsg( '<strong class="error">$1</strong>', $error );
+ if ( $success )
+ $wgOut->wrapWikiMsg( '<strong class="success">$1</strong>', $success );
+ }
+
+ public static function doPageRestriction( $uid, $user ) {
+ global $wgUser, $wgRequest;
+ $r = new UserRestriction();
+ $r->setType( UserRestriction::PAGE );
+ $r->setPage( Title::newFromText( $wgRequest->getVal( 'page' ) ) );
+ $r->setSubjectId( $uid );
+ $r->setSubjectText( $user );
+ $r->setBlockerId( $wgUser->getId() );
+ $r->setBlockerText( $wgUser->getName() );
+ $r->setReason( $wgRequest->getVal( 'reason' ) );
+ $r->setExpiry( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) );
+ $r->setTimestamp( wfTimestampNow( TS_MW ) );
+ $r->commit();
+ $logExpiry = $wgRequest->getVal( 'expiry' ) ? $wgRequest->getVal( 'expiry' ) : Block::infinity();
+ $l = new LogPage( 'restrict' );
+ $l->addEntry( 'restrict', Title::makeTitle( NS_USER, $user ), $r->getReason(),
+ array( $r->getType(), $r->getPage()->getFullText(), $logExpiry) );
+ }
+
+ public static function namespaceRestrictionForm( $uid, $user, $oldRestrictions ) {
+ global $wgOut, $wgTitle, $wgRequest, $wgUser, $wgContLang;
+ $error = '';
+ $success = false;
+ if( $wgRequest->wasPosted() && $wgRequest->getVal( 'type' ) == UserRestriction::NAMESPACE &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'edittoken' ) ) ) {
+ $ns = $wgRequest->getVal( 'namespace' );
+ if( $wgContLang->getNsText( $ns ) === false )
+ $error = wfMsgExt( 'restrictuser-badnamespace', 'parseinline' );
+ elseif( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) === false )
+ $error = wfMsgExt( 'restrictuser-badexpiry', 'parseinline', $wgRequest->getVal( 'expiry' ) );
+ else
+ foreach( $oldRestrictions as $r )
+ if( $r->isNamespace() && $r->getNamespace() == $ns )
+ $error = wfMsgExt( 'restrictuser-dupnamespace', 'parse' );
+ if( !$error ) {
+ self::doNamespaceRestriction( $uid, $user );
+ $success = array('restrictuser-success', $user);
+ }
+ }
+ $useRequestValues = $wgRequest->getVal( 'type' ) == UserRestriction::NAMESPACE;
+ $wgOut->addHTML( Xml::fieldset( wfMsg( 'restrictuser-legend-namespace' ) ) );
+
+ self::printSuccessError( $success, $error );
+
+ $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl(),
+ 'method' => 'post' ) ) );
+ $wgOut->addHTML( Xml::hidden( 'type', UserRestriction::NAMESPACE ) );
+ $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) );
+ $wgOut->addHTML( Xml::hidden( 'user', $user ) );
+ $form = array();
+ $form['restrictuser-namespace'] = Xml::namespaceSelector( $wgRequest->getVal( 'namespace' ) );
+ $form['restrictuser-expiry'] = Xml::input( 'expiry', false,
+ $useRequestValues ? $wgRequest->getVal( 'expiry' ) : false );
+ $form['restrictuser-reason'] = Xml::input( 'reason', false,
+ $useRequestValues ? $wgRequest->getVal( 'reason' ) : false );
+ $wgOut->addHTML( Xml::buildForm( $form, 'restrictuser-submit' ) );
+ $wgOut->addHTML( "</form></fieldset>" );
+ }
+
+ public static function doNamespaceRestriction( $uid, $user ) {
+ global $wgUser, $wgRequest;
+ $r = new UserRestriction();
+ $r->setType( UserRestriction::NAMESPACE );
+ $r->setNamespace( $wgRequest->getVal( 'namespace' ) );
+ $r->setSubjectId( $uid );
+ $r->setSubjectText( $user );
+ $r->setBlockerId( $wgUser->getId() );
+ $r->setBlockerText( $wgUser->getName() );
+ $r->setReason( $wgRequest->getVal( 'reason' ) );
+ $r->setExpiry( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) );
+ $r->setTimestamp( wfTimestampNow( TS_MW ) );
+ $r->commit();
+ $logExpiry = $wgRequest->getVal( 'expiry' ) ? $wgRequest->getVal( 'expiry' ) : Block::infinity();
+ $l = new LogPage( 'restrict' );
+ $l->addEntry( 'restrict', Title::makeTitle( NS_USER, $user ), $r->getReason(),
+ array( $r->getType(), $r->getNamespace(), $logExpiry ) );
+ }
+}
diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php
index e94fc222..74b118e2 100644
--- a/includes/specials/SpecialRevisiondelete.php
+++ b/includes/specials/SpecialRevisiondelete.php
@@ -171,7 +171,7 @@ class RevisionDeleteForm {
$wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(), $count );
$bitfields = 0;
- $wgOut->addHtml( "<ul>" );
+ $wgOut->addHTML( "<ul>" );
$where = $revObjs = array();
$dbr = wfGetDB( DB_SLAVE );
@@ -204,7 +204,7 @@ class RevisionDeleteForm {
$UserAllowed = false;
}
$revisions++;
- $wgOut->addHtml( $this->historyLine( $revObjs[$revid] ) );
+ $wgOut->addHTML( $this->historyLine( $revObjs[$revid] ) );
$bitfields |= $revObjs[$revid]->mDeleted;
}
// The archives...
@@ -245,7 +245,7 @@ class RevisionDeleteForm {
$UserAllowed = false;
}
$revisions++;
- $wgOut->addHtml( $this->historyLine( $revObjs[$timestamp] ) );
+ $wgOut->addHTML( $this->historyLine( $revObjs[$timestamp] ) );
$bitfields |= $revObjs[$timestamp]->mDeleted;
}
}
@@ -254,7 +254,7 @@ class RevisionDeleteForm {
return;
}
- $wgOut->addHtml( "</ul>" );
+ $wgOut->addHTML( "</ul>" );
$wgOut->addWikiMsg( 'revdelete-text' );
@@ -278,7 +278,7 @@ class RevisionDeleteForm {
$hidden[] = Xml::hidden( 'artimestamp[]', $rev->getTimestamp() );
}
$special = SpecialPage::getTitleFor( 'Revisiondelete' );
- $wgOut->addHtml(
+ $wgOut->addHTML(
Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ),
'id' => 'mw-revdel-form-revisions' ) ) .
Xml::openElement( 'fieldset' ) .
@@ -287,15 +287,15 @@ class RevisionDeleteForm {
// FIXME: all items checked for just one rev are checked, even if not set for the others
foreach( $this->checks as $item ) {
list( $message, $name, $field ) = $item;
- $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
+ $wgOut->addHTML( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
}
foreach( $items as $item ) {
- $wgOut->addHtml( Xml::tags( 'p', null, $item ) );
+ $wgOut->addHTML( Xml::tags( 'p', null, $item ) );
}
foreach( $hidden as $item ) {
- $wgOut->addHtml( $item );
+ $wgOut->addHTML( $item );
}
- $wgOut->addHtml(
+ $wgOut->addHTML(
Xml::closeElement( 'fieldset' ) .
Xml::closeElement( 'form' ) . "\n"
);
@@ -317,7 +317,7 @@ class RevisionDeleteForm {
$wgLang->formatNum($count) );
$bitfields = 0;
- $wgOut->addHtml( "<ul>" );
+ $wgOut->addHTML( "<ul>" );
$where = $filesObjs = array();
$dbr = wfGetDB( DB_SLAVE );
@@ -326,11 +326,11 @@ class RevisionDeleteForm {
if( $this->deleteKey=='oldimage' ) {
// Run through and pull all our data in one query
foreach( $this->ofiles as $timestamp ) {
- $where[] = $dbr->addQuotes( $timestamp.'!'.$this->page->getDbKey() );
+ $where[] = $dbr->addQuotes( $timestamp.'!'.$this->page->getDBKey() );
}
$whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
$result = $dbr->select( 'oldimage', '*',
- array( 'oi_name' => $this->page->getDbKey(),
+ array( 'oi_name' => $this->page->getDBKey(),
$whereClause ),
__METHOD__ );
while( $row = $dbr->fetchObject( $result ) ) {
@@ -340,7 +340,7 @@ class RevisionDeleteForm {
}
// Check through our images
foreach( $this->ofiles as $timestamp ) {
- $archivename = $timestamp.'!'.$this->page->getDbKey();
+ $archivename = $timestamp.'!'.$this->page->getDBKey();
if( !isset($filesObjs[$archivename]) ) {
continue;
} else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) {
@@ -353,7 +353,7 @@ class RevisionDeleteForm {
}
$revisions++;
// Inject history info
- $wgOut->addHtml( $this->fileLine( $filesObjs[$archivename] ) );
+ $wgOut->addHTML( $this->fileLine( $filesObjs[$archivename] ) );
$bitfields |= $filesObjs[$archivename]->deleted;
}
// Archived files...
@@ -364,7 +364,7 @@ class RevisionDeleteForm {
}
$whereClause = 'fa_id IN(' . implode(',',$where) . ')';
$result = $dbr->select( 'filearchive', '*',
- array( 'fa_name' => $this->page->getDbKey(),
+ array( 'fa_name' => $this->page->getDBKey(),
$whereClause ),
__METHOD__ );
while( $row = $dbr->fetchObject( $result ) ) {
@@ -384,7 +384,7 @@ class RevisionDeleteForm {
}
$revisions++;
// Inject history info
- $wgOut->addHtml( $this->archivedfileLine( $filesObjs[$fileid] ) );
+ $wgOut->addHTML( $this->archivedfileLine( $filesObjs[$fileid] ) );
$bitfields |= $filesObjs[$fileid]->deleted;
}
}
@@ -393,7 +393,7 @@ class RevisionDeleteForm {
return;
}
- $wgOut->addHtml( "</ul>" );
+ $wgOut->addHTML( "</ul>" );
$wgOut->addWikiMsg('revdelete-text' );
//Normal sysops can always see what they did, but can't always change it
@@ -416,7 +416,7 @@ class RevisionDeleteForm {
$hidden[] = Xml::hidden( 'fileid[]', $fileid );
}
$special = SpecialPage::getTitleFor( 'Revisiondelete' );
- $wgOut->addHtml(
+ $wgOut->addHTML(
Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ),
'id' => 'mw-revdel-form-filerevisions' ) ) .
Xml::fieldset( wfMsg( 'revdelete-legend' ) )
@@ -424,16 +424,16 @@ class RevisionDeleteForm {
// FIXME: all items checked for just one file are checked, even if not set for the others
foreach( $this->checks as $item ) {
list( $message, $name, $field ) = $item;
- $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
+ $wgOut->addHTML( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
}
foreach( $items as $item ) {
- $wgOut->addHtml( "<p>$item</p>" );
+ $wgOut->addHTML( "<p>$item</p>" );
}
foreach( $hidden as $item ) {
- $wgOut->addHtml( $item );
+ $wgOut->addHTML( $item );
}
- $wgOut->addHtml(
+ $wgOut->addHTML(
Xml::closeElement( 'fieldset' ) .
Xml::closeElement( 'form' ) . "\n"
);
@@ -449,7 +449,7 @@ class RevisionDeleteForm {
$wgOut->addWikiMsg( 'logdelete-selected', $wgLang->formatNum( count($this->events) ) );
$bitfields = 0;
- $wgOut->addHtml( "<ul>" );
+ $wgOut->addHTML( "<ul>" );
$where = $logRows = array();
$dbr = wfGetDB( DB_SLAVE );
@@ -480,7 +480,7 @@ class RevisionDeleteForm {
$UserAllowed = false;
}
$logItems++;
- $wgOut->addHtml( $this->logLine( $logRows[$logid] ) );
+ $wgOut->addHTML( $this->logLine( $logRows[$logid] ) );
$bitfields |= $logRows[$logid]->log_deleted;
}
if( !$logItems ) {
@@ -488,7 +488,7 @@ class RevisionDeleteForm {
return;
}
- $wgOut->addHtml( "</ul>" );
+ $wgOut->addHTML( "</ul>" );
$wgOut->addWikiMsg( 'revdelete-text' );
// Normal sysops can always see what they did, but can't always change it
@@ -506,7 +506,7 @@ class RevisionDeleteForm {
}
$special = SpecialPage::getTitleFor( 'Revisiondelete' );
- $wgOut->addHtml(
+ $wgOut->addHTML(
Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ),
'id' => 'mw-revdel-form-logs' ) ) .
Xml::fieldset( wfMsg( 'revdelete-legend' ) )
@@ -514,16 +514,16 @@ class RevisionDeleteForm {
// FIXME: all items checked for just on event are checked, even if not set for the others
foreach( $this->checks as $item ) {
list( $message, $name, $field ) = $item;
- $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
+ $wgOut->addHTML( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
}
foreach( $items as $item ) {
- $wgOut->addHtml( "<p>$item</p>" );
+ $wgOut->addHTML( "<p>$item</p>" );
}
foreach( $hidden as $item ) {
- $wgOut->addHtml( $item );
+ $wgOut->addHTML( $item );
}
- $wgOut->addHtml(
+ $wgOut->addHTML(
Xml::closeElement( 'fieldset' ) .
Xml::closeElement( 'form' ) . "\n"
);
@@ -606,7 +606,7 @@ class RevisionDeleteForm {
* @returns string
*/
private function archivedfileLine( $file ) {
- global $wgLang, $wgTitle;
+ global $wgLang;
$target = $this->page->getPrefixedText();
$date = $wgLang->timeanddate( $file->getTimestamp(), true );
@@ -939,11 +939,11 @@ class RevisionDeleter {
$set = array();
// Run through and pull all our data in one query
foreach( $items as $timestamp ) {
- $where[] = $this->dbw->addQuotes( $timestamp.'!'.$title->getDbKey() );
+ $where[] = $this->dbw->addQuotes( $timestamp.'!'.$title->getDBKey() );
}
$whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
$result = $this->dbw->select( 'oldimage', '*',
- array( 'oi_name' => $title->getDbKey(),
+ array( 'oi_name' => $title->getDBKey(),
$whereClause ),
__METHOD__ );
while( $row = $this->dbw->fetchObject( $result ) ) {
@@ -953,7 +953,7 @@ class RevisionDeleter {
}
// To work!
foreach( $items as $timestamp ) {
- $archivename = $timestamp.'!'.$title->getDbKey();
+ $archivename = $timestamp.'!'.$title->getDBKey();
if( !isset($filesObjs[$archivename]) ) {
$success = false;
continue; // Must exist
@@ -1036,7 +1036,7 @@ class RevisionDeleter {
}
$whereClause = 'fa_id IN(' . implode(',',$where) . ')';
$result = $this->dbw->select( 'filearchive', '*',
- array( 'fa_name' => $title->getDbKey(),
+ array( 'fa_name' => $title->getDBKey(),
$whereClause ),
__METHOD__ );
while( $row = $this->dbw->fetchObject( $result ) ) {
@@ -1344,7 +1344,7 @@ class RevisionDeleter {
function updatePage( $title ) {
$title->invalidateCache();
$title->purgeSquid();
-
+ $title->touchLinks();
// Extensions that require referencing previous revisions may need this
wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) );
}
diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php
index f13c1676..f3117242 100644
--- a/includes/specials/SpecialSearch.php
+++ b/includes/specials/SpecialSearch.php
@@ -29,13 +29,18 @@
* @param $par String: (default '')
*/
function wfSpecialSearch( $par = '' ) {
- global $wgRequest, $wgUser;
-
- $search = str_replace( "\n", " ", $wgRequest->getText( 'search', $par ) );
- $searchPage = new SpecialSearch( $wgRequest, $wgUser );
+ global $wgRequest, $wgUser, $wgUseOldSearchUI;
+ // Strip underscores from title parameter; most of the time we'll want
+ // text form here. But don't strip underscores from actual text params!
+ $titleParam = str_replace( '_', ' ', $par );
+ // Fetch the search term
+ $search = str_replace( "\n", " ", $wgRequest->getText( 'search', $titleParam ) );
+ $class = $wgUseOldSearchUI ? 'SpecialSearchOld' : 'SpecialSearch';
+ $searchPage = new $class( $wgRequest, $wgUser );
if( $wgRequest->getVal( 'fulltext' )
|| !is_null( $wgRequest->getVal( 'offset' ))
- || !is_null( $wgRequest->getVal( 'searchx' ))) {
+ || !is_null( $wgRequest->getVal( 'searchx' )) )
+ {
$searchPage->showResults( $search, 'search' );
} else {
$searchPage->goResult( $search );
@@ -56,9 +61,806 @@ class SpecialSearch {
* @param User $user
* @public
*/
- function SpecialSearch( &$request, &$user ) {
+ function __construct( &$request, &$user ) {
list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' );
+ $this->mPrefix = $request->getVal('prefix', '');
+ # Extract requested namespaces
+ $this->namespaces = $this->powerSearch( $request );
+ if( empty( $this->namespaces ) ) {
+ $this->namespaces = SearchEngine::userNamespaces( $user );
+ }
+ $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false;
+ $this->searchAdvanced = $request->getVal( 'advanced' );
+ $this->active = 'advanced';
+ $this->sk = $user->getSkin();
+ $this->didYouMeanHtml = ''; # html of did you mean... link
+ }
+
+ /**
+ * If an exact title match can be found, jump straight ahead to it.
+ * @param string $term
+ */
+ public function goResult( $term ) {
+ global $wgOut;
+ $this->setupPage( $term );
+ # Try to go to page as entered.
+ $t = Title::newFromText( $term );
+ # If the string cannot be used to create a title
+ if( is_null( $t ) ) {
+ return $this->showResults( $term );
+ }
+ # If there's an exact or very near match, jump right there.
+ $t = SearchEngine::getNearMatch( $term );
+ if( !is_null( $t ) ) {
+ $wgOut->redirect( $t->getFullURL() );
+ return;
+ }
+ # No match, generate an edit URL
+ $t = Title::newFromText( $term );
+ if( !is_null( $t ) ) {
+ global $wgGoToEdit;
+ wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) );
+ # If the feature is enabled, go straight to the edit page
+ if( $wgGoToEdit ) {
+ $wgOut->redirect( $t->getFullURL( 'action=edit' ) );
+ return;
+ }
+ }
+ return $this->showResults( $term );
+ }
+
+ /**
+ * @param string $term
+ */
+ public function showResults( $term ) {
+ global $wgOut, $wgUser, $wgDisableTextSearch, $wgContLang;
+ wfProfileIn( __METHOD__ );
+
+ $sk = $wgUser->getSkin();
+
+ $this->searchEngine = SearchEngine::create();
+ $search =& $this->searchEngine;
+ $search->setLimitOffset( $this->limit, $this->offset );
+ $search->setNamespaces( $this->namespaces );
+ $search->showRedirects = $this->searchRedirects;
+ $search->prefix = $this->mPrefix;
+ $term = $search->transformSearchTerm($term);
+
+ $this->setupPage( $term );
+
+ if( $wgDisableTextSearch ) {
+ global $wgSearchForwardUrl;
+ if( $wgSearchForwardUrl ) {
+ $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl );
+ $wgOut->redirect( $url );
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+ global $wgInputEncoding;
+ $wgOut->addHTML(
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'search-external' ) ) .
+ Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), wfMsg( 'searchdisabled' ) ) .
+ wfMsg( 'googlesearch',
+ htmlspecialchars( $term ),
+ htmlspecialchars( $wgInputEncoding ),
+ htmlspecialchars( wfMsg( 'searchbutton' ) )
+ ) .
+ Xml::closeElement( 'fieldset' )
+ );
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ $t = Title::newFromText( $term );
+
+ // fetch search results
+ $rewritten = $search->replacePrefixes($term);
+
+ $titleMatches = $search->searchTitle( $rewritten );
+ if( !($titleMatches instanceof SearchResultTooMany))
+ $textMatches = $search->searchText( $rewritten );
+
+ // did you mean... suggestions
+ if( $textMatches && $textMatches->hasSuggestion() ) {
+ $st = SpecialPage::getTitleFor( 'Search' );
+ $stParams = wfArrayToCGI(
+ array( 'search' => $textMatches->getSuggestionQuery(), 'fulltext' => wfMsg('search') ),
+ $this->powerSearchOptions()
+ );
+ $suggestLink = $sk->makeKnownLinkObj( $st,
+ $textMatches->getSuggestionSnippet(),
+ $stParams );
+
+ $this->didYouMeanHtml = '<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>';
+ }
+
+ // start rendering the page
+ $wgOut->addHtml(
+ Xml::openElement( 'table', array( 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) .
+ Xml::openElement( 'tr' ) .
+ Xml::openElement( 'td' ) . "\n" .
+ ( $this->searchAdvanced ? $this->powerSearchBox( $term ) : $this->shortDialog( $term ) ) .
+ Xml::closeElement('td') .
+ Xml::closeElement('tr') .
+ Xml::closeElement('table')
+ );
+
+ // Sometimes the search engine knows there are too many hits
+ if( $titleMatches instanceof SearchResultTooMany ) {
+ $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" );
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ $filePrefix = $wgContLang->getFormattedNsText(NS_FILE).':';
+ if( '' === trim( $term ) || $filePrefix === trim( $term ) ) {
+ $wgOut->addHTML( $this->searchAdvanced ? $this->powerSearchFocus() : $this->searchFocus() );
+ // Empty query -- straight view of search form
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ // show direct page/create link
+ if( !is_null($t) ) {
+ if( !$t->exists() ) {
+ $wgOut->addWikiMsg( 'searchmenu-new', wfEscapeWikiText( $t->getPrefixedText() ) );
+ } else {
+ $wgOut->addWikiMsg( 'searchmenu-exists', wfEscapeWikiText( $t->getPrefixedText() ) );
+ }
+ }
+
+ // Get number of results
+ $titleMatchesSQL = $titleMatches ? $titleMatches->numRows() : 0;
+ $textMatchesSQL = $textMatches ? $textMatches->numRows() : 0;
+ // Total initial query matches (possible false positives)
+ $numSQL = $titleMatchesSQL + $textMatchesSQL;
+ // Get total actual results (after second filtering, if any)
+ $numTitleMatches = $titleMatches && !is_null( $titleMatches->getTotalHits() ) ?
+ $titleMatches->getTotalHits() : $titleMatchesSQL;
+ $numTextMatches = $textMatches && !is_null( $textMatches->getTotalHits() ) ?
+ $textMatches->getTotalHits() : $textMatchesSQL;
+ $totalRes = $numTitleMatches + $numTextMatches;
+
+ // show number of results and current offset
+ if( $numSQL > 0 ) {
+ if( $numSQL > 0 ) {
+ $top = wfMsgExt('showingresultstotal', array( 'parseinline' ),
+ $this->offset+1, $this->offset+$numSQL, $totalRes, $numSQL );
+ } elseif( $numSQL >= $this->limit ) {
+ $top = wfShowingResults( $this->offset, $this->limit );
+ } else {
+ $top = wfShowingResultsNum( $this->offset, $this->limit, $numSQL );
+ }
+ $wgOut->addHTML( "<p class='mw-search-numberresults'>{$top}</p>\n" );
+ }
+
+ // prev/next links
+ if( $numSQL || $this->offset ) {
+ $prevnext = wfViewPrevNext( $this->offset, $this->limit,
+ SpecialPage::getTitleFor( 'Search' ),
+ wfArrayToCGI( $this->powerSearchOptions(), array( 'search' => $term ) ),
+ max( $titleMatchesSQL, $textMatchesSQL ) < $this->limit
+ );
+ $wgOut->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" );
+ wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) );
+ } else {
+ wfRunHooks( 'SpecialSearchNoResults', array( $term ) );
+ }
+
+ $wgOut->addHtml( "<div class='searchresults'>" );
+ if( $titleMatches ) {
+ if( $numTitleMatches > 0 ) {
+ $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' );
+ $wgOut->addHTML( $this->showMatches( $titleMatches ) );
+ }
+ $titleMatches->free();
+ }
+ if( $textMatches ) {
+ // output appropriate heading
+ if( $numTextMatches > 0 && $numTitleMatches > 0 ) {
+ // if no title matches the heading is redundant
+ $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' );
+ } elseif( $totalRes == 0 ) {
+ # Don't show the 'no text matches' if we received title matches
+ $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' );
+ }
+ // show interwiki results if any
+ if( $textMatches->hasInterwikiResults() ) {
+ $wgOut->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ) );
+ }
+ // show results
+ if( $numTextMatches > 0 ) {
+ $wgOut->addHTML( $this->showMatches( $textMatches ) );
+ }
+
+ $textMatches->free();
+ }
+ if( $totalRes === 0 ) {
+ $wgOut->addWikiMsg( 'search-nonefound' );
+ }
+ $wgOut->addHtml( "</div>" );
+ if( $totalRes === 0 ) {
+ $wgOut->addHTML( $this->searchAdvanced ? $this->powerSearchFocus() : $this->searchFocus() );
+ }
+
+ if( $numSQL || $this->offset ) {
+ $wgOut->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
+ }
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ *
+ */
+ protected function setupPage( $term ) {
+ global $wgOut;
+ // Figure out the active search profile header
+ $nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
+ if( $this->searchAdvanced )
+ $this->active = 'advanced';
+ else if( $this->namespaces === NS_FILE || $this->startsWithImage( $term ) )
+ $this->active = 'images';
+ elseif( $this->namespaces === $nsAllSet )
+ $this->active = 'all';
+ elseif( $this->namespaces === SearchEngine::defaultNamespaces() )
+ $this->active = 'default';
+ elseif( $this->namespaces === SearchEngine::projectNamespaces() )
+ $this->active = 'project';
+ else
+ $this->active = 'advanced';
+ # Should advanced UI be used?
+ $this->searchAdvanced = ($this->active === 'advanced');
+ if( !empty( $term ) ) {
+ $wgOut->setPageTitle( wfMsg( 'searchresults') );
+ $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term ) ) );
+ }
+ $wgOut->setArticleRelated( false );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ }
+
+ /**
+ * Extract "power search" namespace settings from the request object,
+ * returning a list of index numbers to search.
+ *
+ * @param WebRequest $request
+ * @return array
+ */
+ protected function powerSearch( &$request ) {
+ $arr = array();
+ foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+ if( $request->getCheck( 'ns' . $ns ) ) {
+ $arr[] = $ns;
+ }
+ }
+ return $arr;
+ }
+ /**
+ * Reconstruct the 'power search' options for links
+ * @return array
+ */
+ protected function powerSearchOptions() {
+ $opt = array();
+ foreach( $this->namespaces as $n ) {
+ $opt['ns' . $n] = 1;
+ }
+ $opt['redirs'] = $this->searchRedirects ? 1 : 0;
+ if( $this->searchAdvanced ) {
+ $opt['advanced'] = $this->searchAdvanced;
+ }
+ return $opt;
+ }
+
+ /**
+ * Show whole set of results
+ *
+ * @param SearchResultSet $matches
+ */
+ protected function showMatches( &$matches ) {
+ global $wgContLang;
+ wfProfileIn( __METHOD__ );
+
+ $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
+
+ $out = "";
+ $infoLine = $matches->getInfo();
+ if( !is_null($infoLine) ) {
+ $out .= "\n<!-- {$infoLine} -->\n";
+ }
+ $off = $this->offset + 1;
+ $out .= "<ul class='mw-search-results'>\n";
+ while( $result = $matches->next() ) {
+ $out .= $this->showHit( $result, $terms );
+ }
+ $out .= "</ul>\n";
+
+ // convert the whole thing to desired language variant
+ $out = $wgContLang->convert( $out );
+ wfProfileOut( __METHOD__ );
+ return $out;
+ }
+
+ /**
+ * Format a single hit result
+ * @param SearchResult $result
+ * @param array $terms terms to highlight
+ */
+ protected function showHit( $result, $terms ) {
+ global $wgContLang, $wgLang, $wgUser;
+ wfProfileIn( __METHOD__ );
+
+ if( $result->isBrokenTitle() ) {
+ wfProfileOut( __METHOD__ );
+ return "<!-- Broken link in search result -->\n";
+ }
+
+ $sk = $wgUser->getSkin();
+ $t = $result->getTitle();
+
+ $link = $this->sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
+
+ //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() ) {
+ wfProfileOut( __METHOD__ );
+ return "<li>{$link}</li>\n";
+ }
+
+ // If the page doesn't *exist*... our search index is out of date.
+ // The least confusing at this point is to drop the result.
+ // You may get less results, but... oh well. :P
+ if( $result->isMissingRevision() ) {
+ wfProfileOut( __METHOD__ );
+ return "<!-- missing page " . htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
+ }
+
+ // format redirects / relevant sections
+ $redirectTitle = $result->getRedirectTitle();
+ $redirectText = $result->getRedirectSnippet($terms);
+ $sectionTitle = $result->getSectionTitle();
+ $sectionText = $result->getSectionSnippet($terms);
+ $redirect = '';
+ if( !is_null($redirectTitle) )
+ $redirect = "<span class='searchalttitle'>"
+ .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText))
+ ."</span>";
+ $section = '';
+ if( !is_null($sectionTitle) )
+ $section = "<span class='searchalttitle'>"
+ .wfMsg('search-section', $this->sk->makeKnownLinkObj( $sectionTitle, $sectionText))
+ ."</span>";
+
+ // format text extract
+ $extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>";
+
+ // format score
+ if( is_null( $result->getScore() ) ) {
+ // Search engine doesn't report scoring info
+ $score = '';
+ } else {
+ $percent = sprintf( '%2.1f', $result->getScore() * 100 );
+ $score = wfMsg( 'search-result-score', $wgLang->formatNum( $percent ) )
+ . ' - ';
+ }
+
+ // format description
+ $byteSize = $result->getByteSize();
+ $wordCount = $result->getWordCount();
+ $timestamp = $result->getTimestamp();
+ $size = wfMsgExt( 'search-result-size', array( 'parsemag', 'escape' ),
+ $this->sk->formatSize( $byteSize ), $wordCount );
+ $date = $wgLang->timeanddate( $timestamp );
+
+ // link to related articles if supported
+ $related = '';
+ if( $result->hasRelated() ) {
+ $st = SpecialPage::getTitleFor( 'Search' );
+ $stParams = wfArrayToCGI( $this->powerSearchOptions(),
+ array('search' => wfMsgForContent('searchrelated').':'.$t->getPrefixedText(),
+ 'fulltext' => wfMsg('search') ));
+
+ $related = ' -- ' . $sk->makeKnownLinkObj( $st,
+ wfMsg('search-relatedarticle'), $stParams );
+ }
+
+ // Include a thumbnail for media files...
+ if( $t->getNamespace() == NS_FILE ) {
+ $img = wfFindFile( $t );
+ if( $img ) {
+ $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
+ if( $thumb ) {
+ $desc = $img->getShortDesc();
+ wfProfileOut( __METHOD__ );
+ // Float doesn't seem to interact well with the bullets.
+ // Table messes up vertical alignment of the bullets.
+ // Bullets are therefore disabled (didn't look great anyway).
+ return "<li>" .
+ '<table class="searchResultImage">' .
+ '<tr>' .
+ '<td width="120" align="center" valign="top">' .
+ $thumb->toHtml( array( 'desc-link' => true ) ) .
+ '</td>' .
+ '<td valign="top">' .
+ $link .
+ $extract .
+ "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" .
+ '</td>' .
+ '</tr>' .
+ '</table>' .
+ "</li>\n";
+ }
+ }
+ }
+
+ wfProfileOut( __METHOD__ );
+ return "<li>{$link} {$redirect} {$section} {$extract}\n" .
+ "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
+ "</li>\n";
+
+ }
+
+ /**
+ * Show results from other wikis
+ *
+ * @param SearchResultSet $matches
+ */
+ protected function showInterwiki( &$matches, $query ) {
+ global $wgContLang;
+ wfProfileIn( __METHOD__ );
+ $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
+
+ $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".
+ wfMsg('search-interwiki-caption')."</div>\n";
+ $off = $this->offset + 1;
+ $out .= "<ul class='mw-search-iwresults'>\n";
+
+ // work out custom project captions
+ $customCaptions = array();
+ $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption>
+ foreach($customLines as $line) {
+ $parts = explode(":",$line,2);
+ if(count($parts) == 2) // validate line
+ $customCaptions[$parts[0]] = $parts[1];
+ }
+
+ $prev = null;
+ while( $result = $matches->next() ) {
+ $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
+ $prev = $result->getInterwikiPrefix();
+ }
+ // FIXME: should support paging in a non-confusing way (not sure how though, maybe via ajax)..
+ $out .= "</ul></div>\n";
+
+ // convert the whole thing to desired language variant
+ $out = $wgContLang->convert( $out );
+ wfProfileOut( __METHOD__ );
+ return $out;
+ }
+
+ /**
+ * Show single interwiki link
+ *
+ * @param SearchResult $result
+ * @param string $lastInterwiki
+ * @param array $terms
+ * @param string $query
+ * @param array $customCaptions iw prefix -> caption
+ */
+ protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) {
+ wfProfileIn( __METHOD__ );
+ global $wgContLang, $wgLang;
+
+ if( $result->isBrokenTitle() ) {
+ wfProfileOut( __METHOD__ );
+ return "<!-- Broken link in search result -->\n";
+ }
+
+ $t = $result->getTitle();
+
+ $link = $this->sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
+
+ // format redirect if any
+ $redirectTitle = $result->getRedirectTitle();
+ $redirectText = $result->getRedirectSnippet($terms);
+ $redirect = '';
+ if( !is_null($redirectTitle) )
+ $redirect = "<span class='searchalttitle'>"
+ .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText))
+ ."</span>";
+
+ $out = "";
+ // display project name
+ if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()) {
+ if( key_exists($t->getInterwiki(),$customCaptions) )
+ // captions from 'search-interwiki-custom'
+ $caption = $customCaptions[$t->getInterwiki()];
+ else{
+ // default is to show the hostname of the other wiki which might suck
+ // if there are many wikis on one hostname
+ $parsed = parse_url($t->getFullURL());
+ $caption = wfMsg('search-interwiki-default', $parsed['host']);
+ }
+ // "more results" link (special page stuff could be localized, but we might not know target lang)
+ $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");
+ $searchLink = $this->sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'),
+ wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search')));
+ $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>
+ {$searchLink}</span>{$caption}</div>\n<ul>";
+ }
+
+ $out .= "<li>{$link} {$redirect}</li>\n";
+ wfProfileOut( __METHOD__ );
+ return $out;
+ }
+
+
+ /**
+ * Generates the power search box at bottom of [[Special:Search]]
+ * @param $term string: search term
+ * @return $out string: HTML form
+ */
+ protected function powerSearchBox( $term ) {
+ global $wgScript;
+
+ $namespaces = SearchEngine::searchableNamespaces();
+
+ $tables = $this->namespaceTables( $namespaces );
+
+ $redirect = Xml::check( 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) );
+ $redirectLabel = Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' );
+ $searchField = Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'powerSearchText' ) );
+ $searchButton = Xml::submitButton( wfMsg( 'powersearch' ), array( 'name' => 'fulltext' )) . "\n";
+ $searchTitle = SpecialPage::getTitleFor( 'Search' );
+
+ $redirectText = '';
+ // show redirects check only if backend supports it
+ if( $this->searchEngine->acceptListRedirects() ) {
+ $redirectText = "<p>". $redirect . " " . $redirectLabel ."</p>";
+ }
+
+ $out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) .
+ Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n" .
+ "<p>" .
+ wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) .
+ "</p>\n" .
+ '<input type="hidden" name="advanced" value="'.$this->searchAdvanced."\"/>\n".
+ $tables .
+ "<hr style=\"clear: both;\" />\n".
+ $redirectText ."\n".
+ "<div style=\"padding-top:2px;padding-bottom:2px;\">".
+ wfMsgExt( 'powersearch-field', array( 'parseinline' ) ) .
+ "&nbsp;" .
+ $searchField .
+ "&nbsp;" .
+ $searchButton .
+ "</div>".
+ "</form>";
+ $t = Title::newFromText( $term );
+ /* if( $t != null && count($this->namespaces) === 1 ) {
+ $out .= wfMsgExt( 'searchmenu-prefix', array('parseinline'), $term );
+ } */
+ return Xml::openElement( 'fieldset', array('id' => 'mw-searchoptions','style' => 'margin:0em;') ) .
+ Xml::element( 'legend', null, wfMsg('powersearch-legend') ) .
+ $this->formHeader($term) . $out . $this->didYouMeanHtml .
+ Xml::closeElement( 'fieldset' );
+ }
+
+ protected function searchFocus() {
+ global $wgJsMimeType;
+ return "<script type=\"$wgJsMimeType\">" .
+ "hookEvent(\"load\", function() {" .
+ "document.getElementById('searchText').focus();" .
+ "});" .
+ "</script>";
+ }
+
+ protected function powerSearchFocus() {
+ global $wgJsMimeType;
+ return "<script type=\"$wgJsMimeType\">" .
+ "hookEvent(\"load\", function() {" .
+ "document.getElementById('powerSearchText').focus();" .
+ "});" .
+ "</script>";
+ }
+
+ protected function formHeader( $term ) {
+ global $wgContLang, $wgCanonicalNamespaceNames;
+
+ $sep = '&nbsp;&nbsp;&nbsp;';
+ $out = Xml::openElement('div', array( 'style' => 'padding-bottom:0.5em;' ) );
+
+ $bareterm = $term;
+ if( $this->startsWithImage( $term ) )
+ $bareterm = substr( $term, strpos( $term, ':' ) + 1 ); // delete all/image prefix
+
+ $nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
+
+ // search profiles headers
+ $m = wfMsg( 'searchprofile-articles' );
+ $tt = wfMsg( 'searchprofile-articles-tooltip',
+ implode( ', ', SearchEngine::namespacesAsText( SearchEngine::defaultNamespaces() ) ) );
+ if( $this->active == 'default' ) {
+ $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
+ } else {
+ $out .= $this->makeSearchLink( $bareterm, SearchEngine::defaultNamespaces(), $m, $tt );
+ }
+ $out .= $sep;
+
+ $m = wfMsg( 'searchprofile-images' );
+ $tt = wfMsg( 'searchprofile-images-tooltip' );
+ if( $this->active == 'images' ) {
+ $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
+ } else {
+ $imageTextForm = $wgContLang->getFormattedNsText(NS_FILE).':'.$bareterm;
+ $out .= $this->makeSearchLink( $imageTextForm, array( NS_FILE ) , $m, $tt );
+ }
+ $out .= $sep;
+
+ /*
+ $m = wfMsg( 'searchprofile-articles-and-proj' );
+ $tt = wfMsg( 'searchprofile-project-tooltip',
+ implode( ', ', SearchEngine::namespacesAsText( SearchEngine::defaultAndProjectNamespaces() ) ) );
+ if( $this->active == 'withproject' ) {
+ $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
+ } else {
+ $out .= $this->makeSearchLink( $bareterm, SearchEngine::defaultAndProjectNamespaces(), $m, $tt );
+ }
+ $out .= $sep;
+ */
+
+ $m = wfMsg( 'searchprofile-project' );
+ $tt = wfMsg( 'searchprofile-project-tooltip',
+ implode( ', ', SearchEngine::namespacesAsText( SearchEngine::projectNamespaces() ) ) );
+ if( $this->active == 'project' ) {
+ $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
+ } else {
+ $out .= $this->makeSearchLink( $bareterm, SearchEngine::projectNamespaces(), $m, $tt );
+ }
+ $out .= $sep;
+
+ $m = wfMsg( 'searchprofile-everything' );
+ $tt = wfMsg( 'searchprofile-everything-tooltip' );
+ if( $this->active == 'all' ) {
+ $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
+ } else {
+ $out .= $this->makeSearchLink( $bareterm, $nsAllSet, $m, $tt );
+ }
+ $out .= $sep;
+
+ $m = wfMsg( 'searchprofile-advanced' );
+ $tt = wfMsg( 'searchprofile-advanced-tooltip' );
+ if( $this->active == 'advanced' ) {
+ $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
+ } else {
+ $out .= $this->makeSearchLink( $bareterm, $this->namespaces, $m, $tt, array( 'advanced' => '1' ) );
+ }
+ $out .= Xml::closeElement('div') ;
+
+ return $out;
+ }
+
+ protected function shortDialog( $term ) {
+ global $wgScript;
+ $searchTitle = SpecialPage::getTitleFor( 'Search' );
+ $searchable = SearchEngine::searchableNamespaces();
+ $out = Xml::openElement( 'form', array( 'id' => 'search', 'method' => 'get', 'action' => $wgScript ) );
+ $out .= Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n";
+ // show namespaces only for advanced search
+ if( $this->active == 'advanced' ) {
+ $active = array();
+ foreach( $this->namespaces as $ns ) {
+ $active[$ns] = $searchable[$ns];
+ }
+ $out .= wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) . "<br/>\n";
+ $out .= $this->namespaceTables( $active, 1 )."<br/>\n";
+ // Still keep namespace settings otherwise, but don't show them
+ } else {
+ foreach( $this->namespaces as $ns ) {
+ $out .= Xml::hidden( "ns{$ns}", '1' );
+ }
+ }
+ // Keep redirect setting
+ $out .= Xml::hidden( "redirs", (int)$this->searchRedirects );
+ // Term box
+ $out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . "\n";
+ $out .= Xml::submitButton( wfMsg( 'searchbutton' ), array( 'name' => 'fulltext' ) );
+ $out .= ' (' . wfMsgExt('searchmenu-help',array('parseinline') ) . ')';
+ $out .= Xml::closeElement( 'form' );
+ // Add prefix link for single-namespace searches
+ $t = Title::newFromText( $term );
+ /*if( $t != null && count($this->namespaces) === 1 ) {
+ $out .= wfMsgExt( 'searchmenu-prefix', array('parseinline'), $term );
+ }*/
+ return Xml::openElement( 'fieldset', array('id' => 'mw-searchoptions','style' => 'margin:0em;') ) .
+ Xml::element( 'legend', null, wfMsg('searchmenu-legend') ) .
+ $this->formHeader($term) . $out . $this->didYouMeanHtml .
+ Xml::closeElement( 'fieldset' );
+ }
+
+ /** Make a search link with some target namespaces */
+ protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params=array() ) {
+ $opt = $params;
+ foreach( $namespaces as $n ) {
+ $opt['ns' . $n] = 1;
+ }
+ $opt['redirs'] = $this->searchRedirects ? 1 : 0;
+
+ $st = SpecialPage::getTitleFor( 'Search' );
+ $stParams = wfArrayToCGI( array( 'search' => $term, 'fulltext' => wfMsg( 'search' ) ), $opt );
+
+ return Xml::element( 'a',
+ array( 'href'=> $st->getLocalURL( $stParams ), 'title' => $tooltip ),
+ $label );
+ }
+
+ /** Check if query starts with image: prefix */
+ protected function startsWithImage( $term ) {
+ global $wgContLang;
+
+ $p = explode( ':', $term );
+ if( count( $p ) > 1 ) {
+ return $wgContLang->getNsIndex( $p[0] ) == NS_FILE;
+ }
+ return false;
+ }
+
+ protected function namespaceTables( $namespaces, $rowsPerTable = 3 ) {
+ global $wgContLang;
+ // Group namespaces into rows according to subject.
+ // Try not to make too many assumptions about namespace numbering.
+ $rows = array();
+ $tables = "";
+ foreach( $namespaces as $ns => $name ) {
+ $subj = MWNamespace::getSubject( $ns );
+ if( !array_key_exists( $subj, $rows ) ) {
+ $rows[$subj] = "";
+ }
+ $name = str_replace( '_', ' ', $name );
+ if( '' == $name ) {
+ $name = wfMsg( 'blanknamespace' );
+ }
+ $rows[$subj] .= Xml::openElement( 'td', array( 'style' => 'white-space: nowrap' ) ) .
+ Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) .
+ Xml::closeElement( 'td' ) . "\n";
+ }
+ $rows = array_values( $rows );
+ $numRows = count( $rows );
+ // Lay out namespaces in multiple floating two-column tables so they'll
+ // be arranged nicely while still accommodating different screen widths
+ // Float to the right on RTL wikis
+ $tableStyle = $wgContLang->isRTL() ?
+ 'float: right; margin: 0 0 0em 1em' : 'float: left; margin: 0 1em 0em 0';
+ // Build the final HTML table...
+ for( $i = 0; $i < $numRows; $i += $rowsPerTable ) {
+ $tables .= Xml::openElement( 'table', array( 'style' => $tableStyle ) );
+ for( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) {
+ $tables .= "<tr>\n" . $rows[$j] . "</tr>";
+ }
+ $tables .= Xml::closeElement( 'table' ) . "\n";
+ }
+ return $tables;
+ }
+}
+
+/**
+ * implements Special:Search - Run text & title search and display the output
+ * @ingroup SpecialPage
+ */
+class SpecialSearchOld {
+
+ /**
+ * Set up basic search parameters from the request and user settings.
+ * Typically you'll pass $wgRequest and $wgUser.
+ *
+ * @param WebRequest $request
+ * @param User $user
+ * @public
+ */
+ function __construct( &$request, &$user ) {
+ list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' );
+ $this->mPrefix = $request->getVal('prefix', '');
$this->namespaces = $this->powerSearch( $request );
if( empty( $this->namespaces ) ) {
$this->namespaces = SearchEngine::userNamespaces( $user );
@@ -119,13 +921,38 @@ class SpecialSearch {
* @public
*/
function showResults( $term ) {
- $fname = 'SpecialSearch::showResults';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
global $wgOut, $wgUser;
$sk = $wgUser->getSkin();
+ $search = SearchEngine::create();
+ $search->setLimitOffset( $this->limit, $this->offset );
+ $search->setNamespaces( $this->namespaces );
+ $search->showRedirects = $this->searchRedirects;
+ $search->prefix = $this->mPrefix;
+ $term = $search->transformSearchTerm($term);
+
$this->setupPage( $term );
+ $rewritten = $search->replacePrefixes($term);
+ $titleMatches = $search->searchTitle( $rewritten );
+ $textMatches = $search->searchText( $rewritten );
+
+ // did you mean... suggestions
+ if($textMatches && $textMatches->hasSuggestion()){
+ $st = SpecialPage::getTitleFor( 'Search' );
+ $stParams = wfArrayToCGI( array(
+ 'search' => $textMatches->getSuggestionQuery(),
+ 'fulltext' => wfMsg('search')),
+ $this->powerSearchOptions());
+
+ $suggestLink = $sk->makeKnownLinkObj( $st,
+ $textMatches->getSuggestionSnippet(),
+ $stParams );
+
+ $wgOut->addHTML('<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>');
+ }
+
$wgOut->addWikiMsg( 'searchresulttext' );
if( '' === trim( $term ) ) {
@@ -133,7 +960,7 @@ class SpecialSearch {
$wgOut->setSubtitle( '' );
$wgOut->addHTML( $this->powerSearchBox( $term ) );
$wgOut->addHTML( $this->powerSearchFocus() );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return;
}
@@ -143,6 +970,7 @@ class SpecialSearch {
if( $wgSearchForwardUrl ) {
$url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl );
$wgOut->redirect( $url );
+ wfProfileOut( __METHOD__ );
return;
}
global $wgInputEncoding;
@@ -157,45 +985,21 @@ class SpecialSearch {
) .
Xml::closeElement( 'fieldset' )
);
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return;
}
- $wgOut->addHTML( $this->shortDialog( $term ) );
-
- $search = SearchEngine::create();
- $search->setLimitOffset( $this->limit, $this->offset );
- $search->setNamespaces( $this->namespaces );
- $search->showRedirects = $this->searchRedirects;
- $rewritten = $search->replacePrefixes($term);
-
- $titleMatches = $search->searchTitle( $rewritten );
+ $wgOut->addHTML( $this->shortDialog( $term ) );
// Sometimes the search engine knows there are too many hits
if ($titleMatches instanceof SearchResultTooMany) {
$wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" );
$wgOut->addHTML( $this->powerSearchBox( $term ) );
$wgOut->addHTML( $this->powerSearchFocus() );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return;
}
- $textMatches = $search->searchText( $rewritten );
-
- // did you mean... suggestions
- if($textMatches && $textMatches->hasSuggestion()){
- $st = SpecialPage::getTitleFor( 'Search' );
- $stParams = wfArrayToCGI( array(
- 'search' => $textMatches->getSuggestionQuery(),
- 'fulltext' => wfMsg('search')),
- $this->powerSearchOptions());
-
- $suggestLink = '<a href="'.$st->escapeLocalURL($stParams).'">'.
- $textMatches->getSuggestionSnippet().'</a>';
-
- $wgOut->addHTML('<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>');
- }
-
// show number of results
$num = ( $titleMatches ? $titleMatches->numRows() : 0 )
+ ( $textMatches ? $textMatches->numRows() : 0);
@@ -207,7 +1011,7 @@ class SpecialSearch {
if ( $num > 0 ) {
if ( $totalNum > 0 ){
$top = wfMsgExt('showingresultstotal', array( 'parseinline' ),
- $this->offset+1, $this->offset+$num, $totalNum );
+ $this->offset+1, $this->offset+$num, $totalNum, $num );
} elseif ( $num >= $this->limit ) {
$top = wfShowingResults( $this->offset, $this->limit );
} else {
@@ -251,7 +1055,7 @@ class SpecialSearch {
}
// show interwiki results if any
if( $textMatches->hasInterwikiResults() )
- $wgOut->addHtml( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ));
+ $wgOut->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ));
// show results
if( $textMatches->numRows() )
$wgOut->addHTML( $this->showMatches( $textMatches ) );
@@ -266,7 +1070,7 @@ class SpecialSearch {
$wgOut->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
}
$wgOut->addHTML( $this->powerSearchBox( $term ) );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
#------------------------------------------------------------------
@@ -277,12 +1081,14 @@ class SpecialSearch {
*/
function setupPage( $term ) {
global $wgOut;
- if( !empty( $term ) )
- $wgOut->setPageTitle( wfMsg( 'searchresults' ) );
+ if( !empty( $term ) ){
+ $wgOut->setPageTitle( wfMsg( 'searchresults') );
+ $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term) ) );
+ }
$subtitlemsg = ( Title::newFromText( $term ) ? 'searchsubtitle' : 'searchsubtitleinvalid' );
$wgOut->setSubtitle( $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ) );
$wgOut->setArticleRelated( false );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
}
/**
@@ -323,8 +1129,7 @@ class SpecialSearch {
* @param SearchResultSet $matches
*/
function showMatches( &$matches ) {
- $fname = 'SpecialSearch::showMatches';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
global $wgContLang;
$terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
@@ -347,7 +1152,7 @@ class SpecialSearch {
// convert the whole thing to desired language variant
global $wgContLang;
$out = $wgContLang->convert( $out );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $out;
}
@@ -357,12 +1162,11 @@ class SpecialSearch {
* @param array $terms terms to highlight
*/
function showHit( $result, $terms ) {
- $fname = 'SpecialSearch::showHit';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
global $wgUser, $wgContLang, $wgLang;
if( $result->isBrokenTitle() ) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return "<!-- Broken link in search result -->\n";
}
@@ -375,7 +1179,7 @@ class SpecialSearch {
//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()) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return "<li>{$link}</li>\n";
}
@@ -383,7 +1187,7 @@ class SpecialSearch {
// The least confusing at this point is to drop the result.
// You may get less results, but... oh well. :P
if( $result->isMissingRevision() ) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return "<!-- missing page " .
htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
}
@@ -434,18 +1238,18 @@ class SpecialSearch {
array('search' => wfMsgForContent('searchrelated').':'.$t->getPrefixedText(),
'fulltext' => wfMsg('search') ));
- $related = ' -- <a href="'.$st->escapeLocalURL($stParams).'">'.
- wfMsg('search-relatedarticle').'</a>';
+ $related = ' -- ' . $sk->makeKnownLinkObj( $st,
+ wfMsg('search-relatedarticle'), $stParams );
}
// Include a thumbnail for media files...
- if( $t->getNamespace() == NS_IMAGE ) {
+ if( $t->getNamespace() == NS_FILE ) {
$img = wfFindFile( $t );
if( $img ) {
- $thumb = $img->getThumbnail( 120, 120 );
+ $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
if( $thumb ) {
$desc = $img->getShortDesc();
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
// Ugly table. :D
// Float doesn't seem to interact well with the bullets.
// Table messes up vertical alignment of the bullet, but I'm
@@ -468,7 +1272,7 @@ class SpecialSearch {
}
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return "<li>{$link} {$redirect} {$section} {$extract}\n" .
"<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
"</li>\n";
@@ -481,8 +1285,7 @@ class SpecialSearch {
* @param SearchResultSet $matches
*/
function showInterwiki( &$matches, $query ) {
- $fname = 'SpecialSearch::showInterwiki';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
global $wgContLang;
$terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
@@ -512,7 +1315,7 @@ class SpecialSearch {
// convert the whole thing to desired language variant
global $wgContLang;
$out = $wgContLang->convert( $out );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $out;
}
@@ -525,13 +1328,12 @@ class SpecialSearch {
* @param string $query
* @param array $customCaptions iw prefix -> caption
*/
- function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions){
- $fname = 'SpecialSearch::showInterwikiHit';
- wfProfileIn( $fname );
+ function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) {
+ wfProfileIn( __METHOD__ );
global $wgUser, $wgContLang, $wgLang;
if( $result->isBrokenTitle() ) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return "<!-- Broken link in search result -->\n";
}
@@ -569,7 +1371,7 @@ class SpecialSearch {
}
$out .= "<li>{$link} {$redirect}</li>\n";
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $out;
}
@@ -580,35 +1382,64 @@ class SpecialSearch {
* @return $out string: HTML form
*/
function powerSearchBox( $term ) {
- global $wgScript;
+ global $wgScript, $wgContLang;
- $namespaces = '';
- foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+ $namespaces = SearchEngine::searchableNamespaces();
+
+ // group namespaces into rows according to subject; try not to make too
+ // many assumptions about namespace numbering
+ $rows = array();
+ foreach( $namespaces as $ns => $name ) {
+ $subj = MWNamespace::getSubject( $ns );
+ if( !array_key_exists( $subj, $rows ) ) {
+ $rows[$subj] = "";
+ }
$name = str_replace( '_', ' ', $name );
if( '' == $name ) {
$name = wfMsg( 'blanknamespace' );
}
- $namespaces .= Xml::openElement( 'span', array( 'style' => 'white-space: nowrap' ) ) .
+ $rows[$subj] .= Xml::openElement( 'td', array( 'style' => 'white-space: nowrap' ) ) .
Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) .
- Xml::closeElement( 'span' ) . "\n";
+ Xml::closeElement( 'td' ) . "\n";
+ }
+ $rows = array_values( $rows );
+ $numRows = count( $rows );
+
+ // lay out namespaces in multiple floating two-column tables so they'll
+ // be arranged nicely while still accommodating different screen widths
+ $rowsPerTable = 3; // seems to look nice
+
+ // float to the right on RTL wikis
+ $tableStyle = ( $wgContLang->isRTL() ?
+ 'float: right; margin: 0 0 1em 1em' :
+ 'float: left; margin: 0 1em 1em 0' );
+
+ $tables = "";
+ for( $i = 0; $i < $numRows; $i += $rowsPerTable ) {
+ $tables .= Xml::openElement( 'table', array( 'style' => $tableStyle ) );
+ for( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) {
+ $tables .= "<tr>\n" . $rows[$j] . "</tr>";
+ }
+ $tables .= Xml::closeElement( 'table' ) . "\n";
}
$redirect = Xml::check( 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) );
$redirectLabel = Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' );
$searchField = Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'powerSearchText' ) );
$searchButton = Xml::submitButton( wfMsg( 'powersearch' ), array( 'name' => 'fulltext' ) ) . "\n";
-
+ $searchTitle = SpecialPage::getTitleFor( 'Search' );
+
$out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) .
Xml::fieldset( wfMsg( 'powersearch-legend' ),
- Xml::hidden( 'title', 'Special:Search' ) .
+ Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n" .
"<p>" .
wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) .
- "<br />" .
- $namespaces .
- "</p>" .
+ "</p>\n" .
+ $tables .
+ "<hr style=\"clear: both\" />\n" .
"<p>" .
$redirect . " " . $redirectLabel .
- "</p>" .
+ "</p>\n" .
wfMsgExt( 'powersearch-field', array( 'parseinline' ) ) .
"&nbsp;" .
$searchField .
@@ -636,7 +1467,8 @@ class SpecialSearch {
'method' => 'get',
'action' => $wgScript
));
- $out .= Xml::hidden( 'title', 'Special:Search' );
+ $searchTitle = SpecialPage::getTitleFor( 'Search' );
+ $out .= Xml::hidden( 'title', $searchTitle->getPrefixedText() );
$out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . ' ';
foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
if( in_array( $ns, $this->namespaces ) ) {
diff --git a/includes/specials/SpecialSpecialpages.php b/includes/specials/SpecialSpecialpages.php
index ca91ad51..560ba445 100644
--- a/includes/specials/SpecialSpecialpages.php
+++ b/includes/specials/SpecialSpecialpages.php
@@ -12,7 +12,7 @@ function wfSpecialSpecialpages() {
$wgMessageCache->loadAllMessages();
- $wgOut->setRobotpolicy( 'noindex,nofollow' ); # Is this really needed?
+ $wgOut->setRobotPolicy( 'noindex,nofollow' ); # Is this really needed?
$sk = $wgUser->getSkin();
$pages = SpecialPage::getUsablePages();
diff --git a/includes/specials/SpecialStatistics.php b/includes/specials/SpecialStatistics.php
index 570a21c6..109c5c30 100644
--- a/includes/specials/SpecialStatistics.php
+++ b/includes/specials/SpecialStatistics.php
@@ -13,50 +13,214 @@
*
* @param mixed $par (not used)
*/
-function wfSpecialStatistics( $par = '' ) {
- global $wgOut, $wgLang, $wgRequest;
- $dbr = wfGetDB( DB_SLAVE );
+class SpecialStatistics extends SpecialPage {
+
+ private $views, $edits, $good, $images, $total, $users,
+ $activeUsers, $admins, $numJobs = 0;
+
+ public function __construct() {
+ parent::__construct( 'Statistics' );
+ }
+
+ public function execute( $par ) {
+ global $wgOut, $wgRequest, $wgMessageCache;
+ global $wgDisableCounters, $wgMiserMode;
+ $wgMessageCache->loadAllMessages();
+
+ $this->setHeaders();
+
+ $this->views = SiteStats::views();
+ $this->edits = SiteStats::edits();
+ $this->good = SiteStats::articles();
+ $this->images = SiteStats::images();
+ $this->total = SiteStats::pages();
+ $this->users = SiteStats::users();
+ $this->activeUsers = SiteStats::activeUsers();
+ $this->admins = SiteStats::numberingroup('sysop');
+ $this->numJobs = SiteStats::jobs();
+
+ # Staticic - views
+ $viewsStats = '';
+ if( !$wgDisableCounters ) {
+ $viewsStats = $this->getViewsStats();
+ }
+
+ # Set active user count
+ if( !$wgMiserMode ) {
+ $dbw = wfGetDB( DB_MASTER );
+ SiteStatsUpdate::cacheUpdate( $dbw );
+ }
+
+ # Do raw output
+ if( $wgRequest->getVal( 'action' ) == 'raw' ) {
+ $this->doRawOutput();
+ }
- $views = SiteStats::views();
- $edits = SiteStats::edits();
- $good = SiteStats::articles();
- $images = SiteStats::images();
- $total = SiteStats::pages();
- $users = SiteStats::users();
- $admins = SiteStats::admins();
- $numJobs = SiteStats::jobs();
+ $text = Xml::openElement( 'table', array( 'class' => 'mw-statistics-table' ) );
- 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 = "__NOTOC__\n";
- $text .= '==' . wfMsgNoTrans( 'sitestats' ) . "==\n";
- $text .= wfMsgExt( 'sitestatstext', array( 'parsemag' ),
- $wgLang->formatNum( $total ),
- $wgLang->formatNum( $good ),
- $wgLang->formatNum( $views ),
- $wgLang->formatNum( $edits ),
- $wgLang->formatNum( sprintf( '%.2f', $total ? $edits / $total : 0 ) ),
- $wgLang->formatNum( sprintf( '%.2f', $edits ? $views / $edits : 0 ) ),
- $wgLang->formatNum( $numJobs ),
- $wgLang->formatNum( $images )
- )."\n";
+ # Statistic - pages
+ $text .= $this->getPageStats();
+
+ # Statistic - edits
+ $text .= $this->getEditStats();
- $text .= "==" . wfMsgNoTrans( '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' )
- )."\n";
+ # Statistic - users
+ $text .= $this->getUserStats();
- global $wgDisableCounters, $wgMiserMode, $wgUser, $wgLang, $wgContLang;
+ # Statistic - usergroups
+ $text .= $this->getGroupStats();
+ $text .= $viewsStats;
+
+ # Statistic - popular pages
if( !$wgDisableCounters && !$wgMiserMode ) {
- $res = $dbr->select(
+ $text .= $this->getMostViewedPages();
+ }
+
+ $text .= Xml::closeElement( 'table' );
+
+ # Customizable footer
+ $footer = wfMsgExt( 'statistics-footer', array('parseinline') );
+ if( !wfEmptyMsg( 'statistics-footer', $footer ) && $footer != '' ) {
+ $text .= "\n" . $footer;
+ }
+
+ $wgOut->addHTML( $text );
+ }
+
+ /**
+ * Format a row
+ * @param string $text description of the row
+ * @param float $number a number
+ * @param array $trExtraParams
+ * @param string $descMsg
+ * @param string $descMsgParam
+ * @return string table row in HTML format
+ */
+ private function formatRow( $text, $number, $trExtraParams = array(), $descMsg = '', $descMsgParam = '' ) {
+ global $wgStylePath;
+ if( $descMsg ) {
+ $descriptionText = wfMsgExt( $descMsg, array( 'parseinline' ), $descMsgParam );
+ if ( !wfEmptyMsg( $descMsg, $descriptionText ) ) {
+ $descriptionText = " ($descriptionText)";
+ $text .= "<br />" . Xml::element( 'small', array( 'class' => 'mw-statistic-desc'),
+ $descriptionText );
+ }
+ }
+ return Xml::openElement( 'tr', $trExtraParams ) .
+ Xml::openElement( 'td' ) . $text . Xml::closeElement( 'td' ) .
+ Xml::openElement( 'td', array( 'class' => 'mw-statistics-numbers' ) ) . $number . Xml::closeElement( 'td' ) .
+ Xml::closeElement( 'tr' );
+ }
+
+ /**
+ * Each of these methods is pretty self-explanatory, get a particular
+ * row for the table of statistics
+ * @return string
+ */
+ private function getPageStats() {
+ global $wgLang;
+ return Xml::openElement( 'tr' ) .
+ Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-pages', array( 'parseinline' ) ) ) .
+ Xml::closeElement( 'tr' ) .
+ $this->formatRow( wfMsgExt( 'statistics-articles', array( 'parseinline' ) ),
+ $wgLang->formatNum( $this->good ),
+ array( 'class' => 'mw-statistics-articles' ) ) .
+ $this->formatRow( wfMsgExt( 'statistics-pages', array( 'parseinline' ) ),
+ $wgLang->formatNum( $this->total ),
+ array( 'class' => 'mw-statistics-pages' ),
+ 'statistics-pages-desc' ) .
+ $this->formatRow( wfMsgExt( 'statistics-files', array( 'parseinline' ) ),
+ $wgLang->formatNum( $this->images ),
+ array( 'class' => 'mw-statistics-files' ) );
+ }
+ private function getEditStats() {
+ global $wgLang;
+ return Xml::openElement( 'tr' ) .
+ Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-edits', array( 'parseinline' ) ) ) .
+ Xml::closeElement( 'tr' ) .
+ $this->formatRow( wfMsgExt( 'statistics-edits', array( 'parseinline' ) ),
+ $wgLang->formatNum( $this->edits ),
+ array( 'class' => 'mw-statistics-edits' ) ) .
+ $this->formatRow( wfMsgExt( 'statistics-edits-average', array( 'parseinline' ) ),
+ $wgLang->formatNum( sprintf( '%.2f', $this->total ? $this->edits / $this->total : 0 ) ),
+ array( 'class' => 'mw-statistics-edits-average' ) ) .
+ $this->formatRow( wfMsgExt( 'statistics-jobqueue', array( 'parseinline' ) ),
+ $wgLang->formatNum( $this->numJobs ),
+ array( 'class' => 'mw-statistics-jobqueue' ) );
+ }
+ private function getUserStats() {
+ global $wgLang, $wgRCMaxAge;
+ return Xml::openElement( 'tr' ) .
+ Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-users', array( 'parseinline' ) ) ) .
+ Xml::closeElement( 'tr' ) .
+ $this->formatRow( wfMsgExt( 'statistics-users', array( 'parseinline' ) ),
+ $wgLang->formatNum( $this->users ),
+ array( 'class' => 'mw-statistics-users' ) ) .
+ $this->formatRow( wfMsgExt( 'statistics-users-active', array( 'parseinline' ) ),
+ $wgLang->formatNum( $this->activeUsers ),
+ array( 'class' => 'mw-statistics-users-active' ),
+ 'statistics-users-active-desc',
+ $wgLang->formatNum( ceil( $wgRCMaxAge / ( 3600 * 24 ) ) ) );
+ }
+ private function getGroupStats() {
+ global $wgGroupPermissions, $wgImplicitGroups, $wgLang, $wgUser;
+ $sk = $wgUser->getSkin();
+ $text = '';
+ foreach( $wgGroupPermissions as $group => $permissions ) {
+ # Skip generic * and implicit groups
+ if ( in_array( $group, $wgImplicitGroups ) || $group == '*' ) {
+ continue;
+ }
+ $groupname = htmlspecialchars( $group );
+ $msg = wfMsg( 'group-' . $groupname );
+ if ( wfEmptyMsg( 'group-' . $groupname, $msg ) || $msg == '' ) {
+ $groupnameLocalized = $groupname;
+ } else {
+ $groupnameLocalized = $msg;
+ }
+ $msg = wfMsgForContent( 'grouppage-' . $groupname );
+ if ( wfEmptyMsg( 'grouppage-' . $groupname, $msg ) || $msg == '' ) {
+ $grouppageLocalized = MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname;
+ } else {
+ $grouppageLocalized = $msg;
+ }
+ $grouppage = $sk->makeLink( $grouppageLocalized, htmlspecialchars( $groupnameLocalized ) );
+ $grouplink = $sk->link( SpecialPage::getTitleFor( 'Listusers' ),
+ wfMsgHtml( 'listgrouprights-members' ),
+ array(),
+ array( 'group' => $group ),
+ 'known' );
+ # Add a class when a usergroup contains no members to allow hiding these rows
+ $classZero = '';
+ $countUsers = SiteStats::numberingroup( $groupname );
+ if( $countUsers == 0 ) {
+ $classZero = ' statistics-group-zero';
+ }
+ $text .= $this->formatRow( $grouppage . ' ' . $grouplink,
+ $wgLang->formatNum( $countUsers ),
+ array( 'class' => 'statistics-group-' . Sanitizer::escapeClass( $group ) . $classZero ) );
+ }
+ return $text;
+ }
+ private function getViewsStats() {
+ global $wgLang;
+ return Xml::openElement( 'tr' ) .
+ Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-views', array( 'parseinline' ) ) ) .
+ Xml::closeElement( 'tr' ) .
+ $this->formatRow( wfMsgExt( 'statistics-views-total', array( 'parseinline' ) ),
+ $wgLang->formatNum( $this->views ),
+ array ( 'class' => 'mw-statistics-views-total' ) ) .
+ $this->formatRow( wfMsgExt( 'statistics-views-peredit', array( 'parseinline' ) ),
+ $wgLang->formatNum( sprintf( '%.2f', $this->edits ?
+ $this->views / $this->edits : 0 ) ),
+ array ( 'class' => 'mw-statistics-views-peredit' ) );
+ }
+ private function getMostViewedPages() {
+ global $wgLang, $wgUser;
+ $text = '';
+ $dbr = wfGetDB( DB_SLAVE );
+ $sk = $wgUser->getSkin();
+ $res = $dbr->select(
'page',
array(
'page_namespace',
@@ -74,20 +238,33 @@ function wfSpecialStatistics( $par = '' ) {
)
);
if( $res->numRows() > 0 ) {
- $text .= "==" . wfMsgNoTrans( 'statistics-mostpopular' ) . "==\n";
+ $text .= Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-mostpopular', array( 'parseinline' ) ) );
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";
+ if( $title instanceof Title ) {
+ $text .= $this->formatRow( $sk->link( $title ),
+ $wgLang->formatNum( $row->page_counter ) );
+
+ }
}
$res->free();
}
- }
-
- $footer = wfMsgNoTrans( 'statistics-footer' );
- if( !wfEmptyMsg( 'statistics-footer', $footer ) && $footer != '' )
- $text .= "\n" . $footer;
-
- $wgOut->addWikiText( $text );
+ return $text;
+ }
+
+ /**
+ * Do the action=raw output for this page. Legacy, but we support
+ * it for backwards compatibility
+ * http://lists.wikimedia.org/pipermail/wikitech-l/2008-August/039202.html
+ */
+ private function doRawOutput() {
+ global $wgOut;
+ $wgOut->disable();
+ header( 'Pragma: nocache' );
+ echo "total=" . $this->total . ";good=" . $this->good . ";views=" .
+ $this->views . ";edits=" . $this->edits . ";users=" . $this->users . ";";
+ echo "activeusers=" . $this->activeUsers . ";admins=" . $this->admins .
+ ";images=" . $this->images . ";jobs=" . $this->numJobs . "\n";
+ return;
}
-}
+} \ No newline at end of file
diff --git a/includes/specials/SpecialUncategorizedimages.php b/includes/specials/SpecialUncategorizedimages.php
index 986ec967..25310081 100644
--- a/includes/specials/SpecialUncategorizedimages.php
+++ b/includes/specials/SpecialUncategorizedimages.php
@@ -31,7 +31,7 @@ class UncategorizedImagesPage extends ImageQueryPage {
function getSQL() {
$dbr = wfGetDB( DB_SLAVE );
list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' );
- $ns = NS_IMAGE;
+ $ns = NS_FILE;
return "SELECT 'Uncategorizedimages' AS type, page_namespace AS namespace,
page_title AS title, page_title AS value
diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php
index d862ebb3..a9fb4ef1 100644
--- a/includes/specials/SpecialUndelete.php
+++ b/includes/specials/SpecialUndelete.php
@@ -119,7 +119,7 @@ class PageArchive {
* @todo Does this belong in Image for fuller encapsulation?
*/
function listFiles() {
- if( $this->title->getNamespace() == NS_IMAGE ) {
+ if( $this->title->getNamespace() == NS_FILE ) {
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'filearchive',
array(
@@ -336,7 +336,7 @@ class PageArchive {
$restoreText = $restoreAll || !empty( $timestamps );
$restoreFiles = $restoreAll || !empty( $fileVersions );
- if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) {
+ if( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
$img = wfLocalFile( $this->title );
$this->fileStatus = $img->restore( $fileVersions, $unsuppress );
$filesRestored = $this->fileStatus->successCount;
@@ -412,7 +412,7 @@ class PageArchive {
# we'll update the latest revision field in the record.
$newid = 0;
$pageId = $page->page_id;
- $previousRevId = $page->page_latest;
+ $previousRevId = $page->page_latest;
# Get the time span of this page
$previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
array( 'rev_id' => $previousRevId ),
@@ -461,25 +461,10 @@ class PageArchive {
'ar_title' => $this->title->getDBkey(),
$oldones ),
__METHOD__,
- /* options */ array(
- 'ORDER BY' => 'ar_timestamp' )
+ /* options */ array( 'ORDER BY' => 'ar_timestamp' )
);
$ret = $dbw->resultObject( $result );
-
$rev_count = $dbw->numRows( $result );
- if( $rev_count ) {
- # We need to seek around as just using DESC in the ORDER BY
- # would leave the revisions inserted in the wrong order
- $first = $ret->fetchObject();
- $ret->seek( $rev_count - 1 );
- $last = $ret->fetchObject();
- // We don't handle well changing the top revision's settings
- if( !$unsuppress && $last->ar_deleted && $last->ar_timestamp > $previousTimestamp ) {
- wfDebug( __METHOD__.": restoration would result in a deleted top revision\n" );
- return false;
- }
- $ret->seek( 0 );
- }
if( $makepage ) {
$newid = $article->insertOn( $dbw );
@@ -502,6 +487,12 @@ class PageArchive {
// a new text table entry will be created for it.
$revText = Revision::getRevisionText( $row, 'ar_' );
}
+ // Check for key dupes due to shitty archive integrity.
+ if( $row->ar_rev_id ) {
+ $exists = $dbw->selectField( 'revision', '1', array('rev_id' => $row->ar_rev_id), __METHOD__ );
+ if( $exists ) continue; // don't throw DB errors
+ }
+
$revision = new Revision( array(
'page' => $pageId,
'id' => $row->ar_rev_id,
@@ -520,17 +511,32 @@ class PageArchive {
wfRunHooks( 'ArticleRevisionUndeleted', array( &$this->title, $revision, $row->ar_page_id ) );
}
+ # Now that it's safely stored, take it out of the archive
+ $dbw->delete( 'archive',
+ /* WHERE */ array(
+ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ $oldones ),
+ __METHOD__ );
+
// Was anything restored at all?
- if($restored == 0)
+ if( $restored == 0 )
return 0;
if( $revision ) {
// 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 );
+ // We don't handle well with top revision deleted
+ if( $revision->getVisibility() ) {
+ $dbw->update( 'revision',
+ array( 'rev_deleted' => 0 ),
+ array( 'rev_id' => $revision->getId() ),
+ __METHOD__
+ );
+ }
}
if( $newid ) {
@@ -541,7 +547,7 @@ class PageArchive {
Article::onArticleEdit( $this->title );
}
- if( $this->title->getNamespace() == NS_IMAGE ) {
+ if( $this->title->getNamespace() == NS_FILE ) {
$update = new HTMLCacheUpdate( $this->title, 'imagelinks' );
$update->doUpdate();
}
@@ -550,14 +556,6 @@ class PageArchive {
return self::UNDELETE_UNKNOWNERR;
}
- # Now that it's safely stored, take it out of the archive
- $dbw->delete( 'archive',
- /* WHERE */ array(
- 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey(),
- $oldones ),
- __METHOD__ );
-
return $restored;
}
@@ -570,7 +568,7 @@ class PageArchive {
* @ingroup SpecialPage
*/
class UndeleteForm {
- var $mAction, $mTarget, $mTimestamp, $mRestore, $mTargetObj;
+ var $mAction, $mTarget, $mTimestamp, $mRestore, $mInvert, $mTargetObj;
var $mTargetTimestamp, $mAllowed, $mComment, $mToken;
function UndeleteForm( $request, $par = "" ) {
@@ -585,6 +583,7 @@ class UndeleteForm {
$posted = $request->wasPosted() &&
$wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
$this->mRestore = $request->getCheck( 'restore' ) && $posted;
+ $this->mInvert = $request->getCheck( 'invert' ) && $posted;
$this->mPreview = $request->getCheck( 'preview' ) && $posted;
$this->mDiff = $request->getCheck( 'diff' );
$this->mComment = $request->getText( 'wpComment' );
@@ -606,7 +605,7 @@ class UndeleteForm {
} else {
$this->mTargetObj = NULL;
}
- if( $this->mRestore ) {
+ if( $this->mRestore || $this->mInvert ) {
$timestamps = array();
$this->mFileVersions = array();
foreach( $_REQUEST as $key => $val ) {
@@ -666,6 +665,9 @@ class UndeleteForm {
if( $this->mRestore && $this->mAction == "submit" ) {
return $this->undelete();
}
+ if( $this->mInvert && $this->mAction == "submit" ) {
+ return $this->showHistory( );
+ }
return $this->showHistory();
}
@@ -673,21 +675,20 @@ class UndeleteForm {
global $wgOut, $wgScript;
$wgOut->addWikiMsg( 'undelete-header' );
- $wgOut->addHtml(
+ $wgOut->addHTML(
Xml::openElement( 'form', array(
'method' => 'get',
'action' => $wgScript ) ) .
- '<fieldset>' .
- Xml::element( 'legend', array(),
- wfMsg( 'undelete-search-box' ) ) .
+ Xml::fieldset( wfMsg( 'undelete-search-box' ) ) .
Xml::hidden( 'title',
SpecialPage::getTitleFor( 'Undelete' )->getPrefixedDbKey() ) .
Xml::inputLabel( wfMsg( 'undelete-search-prefix' ),
'prefix', 'prefix', 20,
- $this->mSearchPrefix ) .
+ $this->mSearchPrefix ) . ' ' .
Xml::submitButton( wfMsg( 'undelete-search-submit' ) ) .
- '</fieldset>' .
- '</form>' );
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
+ );
}
// Generic list of deleted pages
@@ -699,7 +700,7 @@ class UndeleteForm {
return;
}
- $wgOut->addWikiMsg( "undeletepagetext" );
+ $wgOut->addWikiMsg( 'undeletepagetext', $wgLang->formatNum( $result->numRows() ) );
$sk = $wgUser->getSkin();
$undelete = SpecialPage::getTitleFor( 'Undelete' );
@@ -708,11 +709,10 @@ class UndeleteForm {
$title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
$link = $sk->makeKnownLinkObj( $undelete, htmlspecialchars( $title->getPrefixedText() ),
'target=' . $title->getPrefixedUrl() );
- #$revs = wfMsgHtml( 'undeleterevisions', $wgLang->formatNum( $row->count ) );
$revs = wfMsgExt( 'undeleterevisions',
array( 'parseinline' ),
$wgLang->formatNum( $row->count ) );
- $wgOut->addHtml( "<li>{$link} ({$revs})</li>\n" );
+ $wgOut->addHTML( "<li>{$link} ({$revs})</li>\n" );
}
$result->free();
$wgOut->addHTML( "</ul>\n" );
@@ -752,8 +752,6 @@ class UndeleteForm {
SpecialPage::getTitleFor( 'Undelete', $this->mTargetObj->getPrefixedDBkey() ),
htmlspecialchars( $this->mTargetObj->getPrefixedText() )
);
- $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp, true ) );
- $user = $skin->revUserTools( $rev );
if( $this->mDiff ) {
$previousRev = $archive->getPreviousRevision( $timestamp );
@@ -762,59 +760,66 @@ class UndeleteForm {
if( $wgUser->getOption( 'diffonly' ) ) {
return;
} else {
- $wgOut->addHtml( '<hr />' );
+ $wgOut->addHTML( '<hr />' );
}
} else {
- $wgOut->addHtml( wfMsgHtml( 'undelete-nodiff' ) );
+ $wgOut->addHTML( wfMsgHtml( 'undelete-nodiff' ) );
}
}
- $wgOut->addHtml( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user ) . '</p>' );
+ // date and time are separate parameters to facilitate localisation.
+ // $time is kept for backward compat reasons.
+ $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp, true ) );
+ $d = htmlspecialchars( $wgLang->date( $timestamp, true ) );
+ $t = htmlspecialchars( $wgLang->time( $timestamp, true ) );
+ $user = $skin->revUserTools( $rev );
+
+ $wgOut->addHTML( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user, $d, $t ) . '</p>' );
wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
if( $this->mPreview ) {
- $wgOut->addHtml( "<hr />\n" );
+ $wgOut->addHTML( "<hr />\n" );
//Hide [edit]s
$popts = $wgOut->parserOptions();
$popts->setEditSection( false );
$wgOut->parserOptions( $popts );
- $wgOut->addWikiTextTitleTidy( $rev->revText(), $this->mTargetObj, true );
+ $wgOut->addWikiTextTitleTidy( $rev->getText( Revision::FOR_THIS_USER ), $this->mTargetObj, true );
}
- $wgOut->addHtml(
- wfElement( 'textarea', array(
+ $wgOut->addHTML(
+ Xml::element( 'textarea', array(
'readonly' => 'readonly',
'cols' => intval( $wgUser->getOption( 'cols' ) ),
'rows' => intval( $wgUser->getOption( 'rows' ) ) ),
- $rev->revText() . "\n" ) .
- wfOpenElement( 'div' ) .
- wfOpenElement( 'form', array(
+ $rev->getText( Revision::FOR_THIS_USER ) . "\n" ) .
+ Xml::openElement( 'div' ) .
+ Xml::openElement( 'form', array(
'method' => 'post',
'action' => $self->getLocalURL( "action=submit" ) ) ) .
- wfElement( 'input', array(
+ Xml::element( 'input', array(
'type' => 'hidden',
'name' => 'target',
'value' => $this->mTargetObj->getPrefixedDbKey() ) ) .
- wfElement( 'input', array(
+ Xml::element( 'input', array(
'type' => 'hidden',
'name' => 'timestamp',
'value' => $timestamp ) ) .
- wfElement( 'input', array(
+ Xml::element( 'input', array(
'type' => 'hidden',
'name' => 'wpEditToken',
'value' => $wgUser->editToken() ) ) .
- wfElement( 'input', array(
+ Xml::element( 'input', array(
'type' => 'submit',
'name' => 'preview',
'value' => wfMsg( 'showpreview' ) ) ) .
- wfElement( 'input', array(
+ Xml::element( 'input', array(
'name' => 'diff',
'type' => 'submit',
'value' => wfMsg( 'showdiff' ) ) ) .
- wfCloseElement( 'form' ) .
- wfCloseElement( 'div' ) );
+ Xml::closeElement( 'form' ) .
+ Xml::closeElement( 'div' ) );
}
/**
@@ -829,7 +834,7 @@ class UndeleteForm {
$diffEngine = new DifferenceEngine();
$diffEngine->showDiffStyle();
- $wgOut->addHtml(
+ $wgOut->addHTML(
"<div>" .
"<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
"<col class='diff-marker' />" .
@@ -838,11 +843,11 @@ class UndeleteForm {
"<col class='diff-content' />" .
"<tr>" .
"<td colspan='2' width='50%' align='center' class='diff-otitle'>" .
- $this->diffHeader( $previousRev ) .
- "</td>" .
+ $this->diffHeader( $previousRev, 'o' ) .
+ "</td>\n" .
"<td colspan='2' width='50%' align='center' class='diff-ntitle'>" .
- $this->diffHeader( $currentRev ) .
- "</td>" .
+ $this->diffHeader( $currentRev, 'n' ) .
+ "</td>\n" .
"</tr>" .
$diffEngine->generateDiffBody(
$previousRev->getText(), $currentRev->getText() ) .
@@ -851,7 +856,7 @@ class UndeleteForm {
}
- private function diffHeader( $rev ) {
+ private function diffHeader( $rev, $prefix ) {
global $wgUser, $wgLang, $wgLang;
$sk = $wgUser->getSkin();
$isDeleted = !( $rev->getId() && $rev->getTitle() );
@@ -868,17 +873,17 @@ class UndeleteForm {
$targetQuery = 'oldid=' . $rev->getId();
}
return
- '<div id="mw-diff-otitle1"><strong>' .
+ '<div id="mw-diff-'.$prefix.'title1"><strong>' .
$sk->makeLinkObj( $targetPage,
wfMsgHtml( 'revisionasof',
$wgLang->timeanddate( $rev->getTimestamp(), true ) ),
$targetQuery ) .
( $isDeleted ? ' ' . wfMsgHtml( 'deletedrev' ) : '' ) .
'</strong></div>' .
- '<div id="mw-diff-otitle2">' .
+ '<div id="mw-diff-'.$prefix.'title2">' .
$sk->revUserTools( $rev ) . '<br/>' .
'</div>' .
- '<div id="mw-diff-otitle3">' .
+ '<div id="mw-diff-'.$prefix.'title3">' .
$sk->revComment( $rev ) . '<br/>' .
'</div>';
}
@@ -891,7 +896,8 @@ class UndeleteForm {
$file = new ArchivedFile( $this->mTargetObj, '', $this->mFile );
$wgOut->addWikiMsg( 'undelete-show-file-confirm',
$this->mTargetObj->getText(),
- $wgLang->timeanddate( $file->getTimestamp() ) );
+ $wgLang->date( $file->getTimestamp() ),
+ $wgLang->time( $file->getTimestamp() ) );
$wgOut->addHTML(
Xml::openElement( 'form', array(
'method' => 'POST',
@@ -925,7 +931,7 @@ class UndeleteForm {
$store->stream( $key );
}
- private function showHistory() {
+ private function showHistory( ) {
global $wgLang, $wgUser, $wgOut;
$sk = $wgUser->getSkin();
@@ -984,7 +990,7 @@ class UndeleteForm {
$action = $titleObj->getLocalURL( "action=submit" );
# Start the form here
$top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) );
- $wgOut->addHtml( $top );
+ $wgOut->addHTML( $top );
}
# Show relevant lines from the deletion log:
@@ -1007,8 +1013,7 @@ class UndeleteForm {
$unsuppressBox = "";
}
$table =
- Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, wfMsg( 'undelete-fieldset-title' ) ).
+ Xml::fieldset( wfMsg( 'undelete-fieldset-title' ) ) .
Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) .
"<tr>
<td colspan='2'>" .
@@ -1026,15 +1031,16 @@ class UndeleteForm {
<tr>
<td>&nbsp;</td>
<td class='mw-submit'>" .
- Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) .
- Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) .
+ Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) . ' ' .
+ Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) . ' ' .
+ Xml::submitButton( wfMsg( 'undeleteinvert' ), array( 'name' => 'invert', 'id' => 'mw-undelete-invert' ) ) .
"</td>
</tr>" .
$unsuppressBox .
Xml::closeElement( 'table' ) .
Xml::closeElement( 'fieldset' );
- $wgOut->addHtml( $table );
+ $wgOut->addHTML( $table );
}
$wgOut->addHTML( Xml::element( 'h2', null, wfMsg( 'history' ) ) . "\n" );
@@ -1044,7 +1050,7 @@ class UndeleteForm {
$wgOut->addHTML("<ul>");
$target = urlencode( $this->mTarget );
$remaining = $revisions->numRows();
- $earliestLiveTime = $this->getEarliestTime( $this->mTargetObj );
+ $earliestLiveTime = $this->mTargetObj->getEarliestRevTime();
while( $row = $revisions->fetchObject() ) {
$remaining--;
@@ -1057,8 +1063,8 @@ class UndeleteForm {
}
if( $haveFiles ) {
- $wgOut->addHtml( Xml::element( 'h2', null, wfMsg( 'filehist' ) ) . "\n" );
- $wgOut->addHtml( "<ul>" );
+ $wgOut->addHTML( Xml::element( 'h2', null, wfMsg( 'filehist' ) ) . "\n" );
+ $wgOut->addHTML( "<ul>" );
while( $row = $files->fetchObject() ) {
$wgOut->addHTML( $this->formatFileRow( $row, $sk ) );
}
@@ -1071,7 +1077,7 @@ class UndeleteForm {
$misc = Xml::hidden( 'target', $this->mTarget );
$misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
$misc .= Xml::closeElement( 'form' );
- $wgOut->addHtml( $misc );
+ $wgOut->addHTML( $misc );
}
return true;
@@ -1093,7 +1099,15 @@ class UndeleteForm {
$stxt = '';
$ts = wfTimestamp( TS_MW, $row->ar_timestamp );
if( $this->mAllowed ) {
- $checkBox = Xml::check( "ts$ts" );
+ if( $this->mInvert){
+ if( in_array( $ts, $this->mTargetTimestamp ) ) {
+ $checkBox = Xml::check( "ts$ts");
+ } else {
+ $checkBox = Xml::check( "ts$ts", true );
+ }
+ } else {
+ $checkBox = Xml::check( "ts$ts" );
+ }
$titleObj = SpecialPage::getTitleFor( "Undelete" );
$pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk );
# Last link
@@ -1123,7 +1137,6 @@ class UndeleteForm {
// If revision was hidden from sysops
$del = wfMsgHtml('rev-delundel');
} else {
- $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
$del = $sk->makeKnownLinkObj( $revdel,
wfMsgHtml('rev-delundel'),
'target=' . $this->mTargetObj->getPrefixedUrl() . "&artimestamp=$ts" );
@@ -1183,18 +1196,6 @@ class UndeleteForm {
return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
}
- private function getEarliestTime( $title ) {
- $dbr = wfGetDB( DB_SLAVE );
- if( $title->exists() ) {
- $min = $dbr->selectField( 'revision',
- 'MIN(rev_timestamp)',
- array( 'rev_page' => $title->getArticleId() ),
- __METHOD__ );
- return wfTimestampOrNull( TS_MW, $min );
- }
- return null;
- }
-
/**
* Fetch revision text link if it's available to all users
* @return string
@@ -1286,10 +1287,10 @@ class UndeleteForm {
$skin = $wgUser->getSkin();
$link = $skin->makeKnownLinkObj( $this->mTargetObj );
- $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
+ $wgOut->addHTML( wfMsgWikiHtml( 'undeletedpage', $link ) );
} else {
$wgOut->showFatalError( wfMsg( "cannotundelete" ) );
- $wgOut->addHtml( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
+ $wgOut->addHTML( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
}
// Show file deletion warnings and errors
diff --git a/includes/specials/SpecialUnusedimages.php b/includes/specials/SpecialUnusedimages.php
index d71b638f..4adf405d 100644
--- a/includes/specials/SpecialUnusedimages.php
+++ b/includes/specials/SpecialUnusedimages.php
@@ -33,7 +33,7 @@ class UnusedimagesPage extends ImageQueryPage {
FROM ((($page AS I LEFT JOIN $categorylinks AS L ON I.page_id = L.cl_from)
LEFT JOIN $imagelinks AS P ON I.page_title = P.il_to)
INNER JOIN $image AS G ON I.page_title = G.img_name)
- WHERE I.page_namespace = ".NS_IMAGE." AND L.cl_from IS NULL AND P.il_to IS NULL";
+ WHERE I.page_namespace = ".NS_FILE." AND L.cl_from IS NULL AND P.il_to IS NULL";
} else {
list( $image, $imagelinks ) = $dbr->tableNamesN( 'image','imagelinks' );
diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php
index 3a79e052..450c8728 100644
--- a/includes/specials/SpecialUpload.php
+++ b/includes/specials/SpecialUpload.php
@@ -23,7 +23,7 @@ class UploadForm {
const BEFORE_PROCESSING = 1;
const LARGE_FILE_SERVER = 2;
const EMPTY_FILE = 3;
- const MIN_LENGHT_PARTNAME = 4;
+ const MIN_LENGTH_PARTNAME = 4;
const ILLEGAL_FILENAME = 5;
const PROTECTED_PAGE = 6;
const OVERWRITE_EXISTING_FILE = 7;
@@ -300,7 +300,7 @@ class UploadForm {
$this->mainUploadForm( wfMsgHtml( 'emptyfile' ) );
break;
- case self::MIN_LENGHT_PARTNAME:
+ case self::MIN_LENGTH_PARTNAME:
$this->mainUploadForm( wfMsgHtml( 'minlength1' ) );
break;
@@ -328,10 +328,7 @@ class UploadForm {
wfMsgExt( 'filetype-banned-type',
array( 'parseinline' ),
htmlspecialchars( $finalExt ),
- implode(
- wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ),
- $wgFileExtensions
- ),
+ $wgLang->commaList( $wgFileExtensions ),
$wgLang->formatNum( count($wgFileExtensions) )
)
);
@@ -402,7 +399,15 @@ class UploadForm {
$basename = $this->mSrcName;
}
$filtered = wfStripIllegalFilenameChars( $basename );
-
+
+ /* Normalize to title form before we do any further processing */
+ $nt = Title::makeTitleSafe( NS_FILE, $filtered );
+ if( is_null( $nt ) ) {
+ $resultDetails = array( 'filtered' => $filtered );
+ return self::ILLEGAL_FILENAME;
+ }
+ $filtered = $nt->getDBkey();
+
/**
* We'll want to blacklist against *any* 'extension', and use
* only the final one for the whitelist.
@@ -423,14 +428,9 @@ class UploadForm {
}
if( strlen( $partname ) < 1 ) {
- return self::MIN_LENGHT_PARTNAME;
+ return self::MIN_LENGTH_PARTNAME;
}
- $nt = Title::makeTitleSafe( NS_IMAGE, $filtered );
- if( is_null( $nt ) ) {
- $resultDetails = array( 'filtered' => $filtered );
- return self::ILLEGAL_FILENAME;
- }
$this->mLocalFile = wfLocalFile( $nt );
$this->mDestName = $this->mLocalFile->getName();
@@ -520,10 +520,7 @@ class UploadForm {
wfMsgExt( 'filetype-unwanted-type',
array( 'parseinline' ),
htmlspecialchars( $finalExt ),
- implode(
- wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ),
- $wgFileExtensions
- ),
+ $wgLang->commaList( $wgFileExtensions ),
$wgLang->formatNum( count($wgFileExtensions) )
) . '</li>';
}
@@ -544,7 +541,7 @@ class UploadForm {
$warning .= self::getExistsWarning( $this->mLocalFile );
}
- $warning .= $this->getDupeWarning( $this->mTempPath );
+ $warning .= $this->getDupeWarning( $this->mTempPath, $finalExt );
if( $warning != '' ) {
/**
@@ -610,7 +607,7 @@ class UploadForm {
// extensions (eg 'jpg' rather than 'JPEG').
//
// Check for another file using the normalized form...
- $nt_lc = Title::makeTitle( NS_IMAGE, $partname . '.' . $file->getExtension() );
+ $nt_lc = Title::makeTitle( NS_FILE, $partname . '.' . $file->getExtension() );
$file_lc = wfLocalFile( $nt_lc );
} else {
$file_lc = false;
@@ -737,7 +734,7 @@ class UploadForm {
public static function ajaxGetLicensePreview( $license ) {
global $wgParser, $wgUser;
$text = '{{' . $license . '}}';
- $title = Title::makeTitle( NS_IMAGE, 'Sample.jpg' );
+ $title = Title::makeTitle( NS_FILE, 'Sample.jpg' );
$options = ParserOptions::newFromUser( $wgUser );
// Expand subst: first, then live templates...
@@ -751,9 +748,10 @@ class UploadForm {
* Check for duplicate files and throw up a warning before the upload
* completes.
*/
- function getDupeWarning( $tempfile ) {
+ function getDupeWarning( $tempfile, $extension ) {
$hash = File::sha1Base36( $tempfile );
$dupes = RepoGroup::singleton()->findBySha1( $hash );
+ $archivedImage = new ArchivedFile( null, 0, $hash.".$extension" );
if( $dupes ) {
global $wgOut;
$msg = "<gallery>";
@@ -767,6 +765,10 @@ class UploadForm {
wfMsgExt( "file-exists-duplicate", array( "parse" ), count( $dupes ) ) .
$wgOut->parse( $msg ) .
"</li>\n";
+ } elseif ( $archivedImage->getID() > 0 ) {
+ global $wgOut;
+ $name = Title::makeTitle( NS_FILE, $archivedImage->getName() )->getPrefixedText();
+ return Xml::tags( 'li', null, wfMsgExt( 'file-deleted-duplicate', array( 'parseinline' ), array( $name ) ) );
} else {
return '';
}
@@ -961,7 +963,7 @@ wgUploadAutoFill = {$autofill};
}
if( $this->mDesiredDestName ) {
- $title = Title::makeTitleSafe( NS_IMAGE, $this->mDesiredDestName );
+ $title = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
// Show a subtitle link to deleted revisions (to sysops et al only)
if( $title instanceof Title && ( $count = $title->isDeleted() ) > 0 && $wgUser->isAllowed( 'deletedhistory' ) ) {
$link = wfMsgExt(
@@ -972,7 +974,7 @@ wgUploadAutoFill = {$autofill};
wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $count )
)
);
- $wgOut->addHtml( "<div id=\"contentSub2\">{$link}</div>" );
+ $wgOut->addHTML( "<div id=\"contentSub2\">{$link}</div>" );
}
// Show the relevant lines from deletion log (for still deleted files only)
@@ -1005,21 +1007,20 @@ wgUploadAutoFill = {$autofill};
$allowedExtensions = '';
if( $wgCheckFileExtensions ) {
- $delim = wfMsgExt( 'comma-separator', array( 'escapenoentities' ) );
if( $wgStrictFileExtensions ) {
# Everything not permitted is banned
$extensionsList =
'<div id="mw-upload-permitted">' .
- wfMsgWikiHtml( 'upload-permitted', implode( $wgFileExtensions, $delim ) ) .
+ wfMsgWikiHtml( 'upload-permitted', $wgLang->commaList( $wgFileExtensions ) ) .
"</div>\n";
} else {
# We have to list both preferred and prohibited
$extensionsList =
'<div id="mw-upload-preferred">' .
- wfMsgWikiHtml( 'upload-preferred', implode( $wgFileExtensions, $delim ) ) .
+ wfMsgWikiHtml( 'upload-preferred', $wgLang->commaList( $wgFileExtensions ) ) .
"</div>\n" .
'<div id="mw-upload-prohibited">' .
- wfMsgWikiHtml( 'upload-prohibited', implode( $wgFileBlacklist, $delim ) ) .
+ wfMsgWikiHtml( 'upload-prohibited', $wgLang->commaList( $wgFileBlacklist ) ) .
"</div>\n";
}
} else {
@@ -1169,7 +1170,7 @@ wgUploadAutoFill = {$autofill};
<tr>"
);
if( $useAjaxLicensePreview ) {
- $wgOut->addHtml( "
+ $wgOut->addHTML( "
<td></td>
<td id=\"mw-license-preview\"></td>
</tr>
@@ -1205,7 +1206,7 @@ wgUploadAutoFill = {$autofill};
);
}
- $wgOut->addHtml( "
+ $wgOut->addHTML( "
<td></td>
<td>
<input tabindex='7' type='checkbox' name='wpWatchthis' id='wpWatchthis' $watchChecked value='true' />
@@ -1279,7 +1280,7 @@ wgUploadAutoFill = {$autofill};
*
* @return array
*/
- function splitExtensions( $filename ) {
+ public function splitExtensions( $filename ) {
$bits = explode( '.', $filename );
$basename = array_shift( $bits );
return array( $basename, $bits );
@@ -1305,7 +1306,7 @@ wgUploadAutoFill = {$autofill};
* @param array $list
* @return bool
*/
- function checkFileExtensionList( $ext, $list ) {
+ public function checkFileExtensionList( $ext, $list ) {
foreach( $ext as $e ) {
if( in_array( strtolower( $e ), $list ) ) {
return true;
@@ -1754,7 +1755,7 @@ wgUploadAutoFill = {$autofill};
function showError( $description ) {
global $wgOut;
$wgOut->setPageTitle( wfMsg( "internalerror" ) );
- $wgOut->setRobotpolicy( "noindex,nofollow" );
+ $wgOut->setRobotPolicy( "noindex,nofollow" );
$wgOut->setArticleRelated( false );
$wgOut->enableClientCache( false );
$wgOut->addWikiText( $description );
@@ -1797,14 +1798,14 @@ wgUploadAutoFill = {$autofill};
$loglist = new LogEventsList( $wgUser->getSkin(), $out );
$pager = new LogPager( $loglist, 'delete', false, $filename );
if( $pager->getNumRows() > 0 ) {
- $out->addHtml( '<div id="mw-upload-deleted-warn">' );
+ $out->addHTML( '<div class="mw-warning-with-logexcerpt">' );
$out->addWikiMsg( 'upload-wasdeleted' );
$out->addHTML(
$loglist->beginLogEventsList() .
$pager->getBody() .
$loglist->endLogEventsList()
);
- $out->addHtml( '</div>' );
+ $out->addHTML( '</div>' );
}
}
}
diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php
index 27009eed..6a4da7a4 100644
--- a/includes/specials/SpecialUserlogin.php
+++ b/includes/specials/SpecialUserlogin.php
@@ -33,6 +33,7 @@ class LoginForm {
const RESET_PASS = 7;
const ABORTED = 8;
const CREATE_BLOCKED = 9;
+ const THROTTLED = 10;
var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted;
var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword;
@@ -128,9 +129,10 @@ class LoginForm {
$result = $this->mailPasswordInternal( $u, false, 'createaccount-title', 'createaccount-text' );
wfRunHooks( 'AddNewAccount', array( $u, true ) );
+ $u->addNewUserLogEntry();
$wgOut->setPageTitle( wfMsg( 'accmailtitle' ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setArticleRelated( false );
if( WikiError::isError( $result ) ) {
@@ -174,14 +176,16 @@ class LoginForm {
# Save settings (including confirmation token)
$u->saveSettings();
- # If not logged in, assume the new account as the current one and set session cookies
- # then show a "welcome" message or a "need cookies" message as needed
+ # If not logged in, assume the new account as the current one and set
+ # session cookies then show a "welcome" message or a "need cookies"
+ # message as needed
if( $wgUser->isAnon() ) {
$wgUser = $u;
$wgUser->setCookies();
wfRunHooks( 'AddNewAccount', array( $wgUser ) );
+ $wgUser->addNewUserLogEntry();
if( $this->hasSessionCookie() ) {
- return $this->successfulLogin( 'welcomecreation', $wgUser->getName(), false );
+ return $this->successfulCreation();
} else {
return $this->cookieRedirectCheck( 'new' );
}
@@ -192,9 +196,10 @@ class LoginForm {
$wgOut->setPageTitle( wfMsgHtml( 'accountcreated' ) );
$wgOut->setArticleRelated( false );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
- $wgOut->addHtml( wfMsgWikiHtml( 'accountcreatedtext', $u->getName() ) );
+ $wgOut->addHTML( wfMsgWikiHtml( 'accountcreatedtext', $u->getName() ) );
$wgOut->returnToMain( false, $self );
wfRunHooks( 'AddNewAccount', array( $u ) );
+ $u->addNewUserLogEntry();
return true;
}
}
@@ -215,12 +220,11 @@ class LoginForm {
return false;
}
- // If we are not allowing users to login locally, we should
- // be checking to see if the user is actually able to
- // authenticate to the authentication server before they
- // create an account (otherwise, they can create a local account
- // and login as any domain user). We only need to check this for
- // domains that aren't local.
+ // If we are not allowing users to login locally, we should be checking
+ // to see if the user is actually able to authenticate to the authenti-
+ // cation server before they create an account (otherwise, they can
+ // create a local account and login as any domain user). We only need
+ // to check this for domains that aren't local.
if( 'local' != $this->mDomain && '' != $this->mDomain ) {
if( !$wgAuth->canCreateAccounts() && ( !$wgAuth->userExists( $this->mName ) || !$wgAuth->authenticate( $this->mName, $this->mPassword ) ) ) {
$this->mainLoginForm( wfMsg( 'wrongpassword' ) );
@@ -280,7 +284,8 @@ class LoginForm {
}
}
- # if you need a confirmed email address to edit, then obviously you need an email address.
+ # if you need a confirmed email address to edit, then obviously you
+ # need an email address.
if ( $wgEmailConfirmToEdit && empty( $this->mEmail ) ) {
$this->mainLoginForm( wfMsg( 'noemailtitle' ) );
return false;
@@ -291,8 +296,8 @@ class LoginForm {
return false;
}
- # Set some additional data so the AbortNewAccount hook can be
- # used for more than just username validation
+ # 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 );
@@ -306,14 +311,15 @@ class LoginForm {
if ( $wgAccountCreationThrottle && $wgUser->isPingLimitable() ) {
$key = wfMemcKey( 'acctcreate', 'ip', $ip );
- $value = $wgMemc->incr( $key );
+ $value = $wgMemc->get( $key );
if ( !$value ) {
- $wgMemc->set( $key, 1, 86400 );
+ $wgMemc->set( $key, 0, 86400 );
}
- if ( $value > $wgAccountCreationThrottle ) {
+ if ( $value >= $wgAccountCreationThrottle ) {
$this->throttleHit( $wgAccountCreationThrottle );
return false;
}
+ $wgMemc->incr( $key );
}
if( !$wgAuth->addUser( $u, $this->mPassword, $this->mEmail, $this->mRealName ) ) {
@@ -372,12 +378,32 @@ class LoginForm {
if ( '' == $this->mName ) {
return self::NO_NAME;
}
+
+ global $wgPasswordAttemptThrottle;
+
+ $throttleCount=0;
+ if ( is_array($wgPasswordAttemptThrottle) ) {
+ $throttleKey = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) );
+ $count = $wgPasswordAttemptThrottle['count'];
+ $period = $wgPasswordAttemptThrottle['seconds'];
+
+ global $wgMemc;
+ $throttleCount = $wgMemc->get($throttleKey);
+ if ( !$throttleCount ) {
+ $wgMemc->add( $throttleKey, 1, $period ); // start counter
+ } else if ( $throttleCount < $count ) {
+ $wgMemc->incr($throttleKey);
+ } else if ( $throttleCount >= $count ) {
+ return self::THROTTLED;
+ }
+ }
- // Load $wgUser now, and check to see if we're logging in as the same name.
- // This is necessary because loading $wgUser (say by calling getName()) calls
- // the UserLoadFromSession hook, which potentially creates the user in the
- // database. Until we load $wgUser, checking for user existence using
- // User::newFromName($name)->getId() below will effectively be using stale data.
+ // Load $wgUser now, and check to see if we're logging in as the same
+ // name. This is necessary because loading $wgUser (say by calling
+ // getName()) calls the UserLoadFromSession hook, which potentially
+ // creates the user in the database. Until we load $wgUser, checking
+ // for user existence using User::newFromName($name)->getId() below
+ // will effectively be using stale data.
if ( $wgUser->getName() === $this->mName ) {
wfDebug( __METHOD__.": already logged in as {$this->mName}\n" );
return self::SUCCESS;
@@ -407,34 +433,30 @@ class LoginForm {
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,
- // and insecure if an attacker has a few seconds to
- // click "search" on someone's open mail reader.
+ // The e-mailed temporary password should not be used for actu-
+ // al logins; that's a very sloppy habit, and insecure if an
+ // attacker has a few seconds to click "search" on someone's o-
+ // pen mail reader.
//
- // Allow it to be used only to reset the password
- // a single time to a new value, which won't be in
- // the user's e-mail archives.
+ // Allow it to be used only to reset the password a single time
+ // to a new value, which won't be in the user's e-mail ar-
+ // chives.
//
- // For backwards compatibility, we'll still recognize
- // it at the login form to minimize surprises for
- // people who have been logging in with a temporary
- // password for some time.
- //
- // As a side-effect, we can authenticate the user's
- // e-mail address if it's not already done, since
- // the temporary password was sent via e-mail.
+ // For backwards compatibility, we'll still recognize it at the
+ // login form to minimize surprises for people who have been
+ // logging in with a temporary password for some time.
//
+ // As a side-effect, we can authenticate the user's e-mail ad-
+ // dress if it's not already done, since the temporary password
+ // was sent via e-mail.
if( !$u->isEmailConfirmed() ) {
$u->confirmEmail();
$u->saveSettings();
}
- // At this point we just return an appropriate code
- // indicating that the UI should show a password
- // reset form; bot interfaces etc will probably just
- // fail cleanly here.
- //
+ // At this point we just return an appropriate code/ indicating
+ // that the UI should show a password reset form; bot inter-
+ // faces etc will probably just fail cleanly here.
$retval = self::RESET_PASS;
} else {
$retval = '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS;
@@ -443,6 +465,11 @@ class LoginForm {
$wgAuth->updateUser( $u );
$wgUser = $u;
+ // Please reset throttle for successful logins, thanks!
+ if($throttleCount) {
+ $wgMemc->delete($throttleKey);
+ }
+
if ( $isAutoCreated ) {
// Must be run after $wgUser is set, for correct new user log
wfRunHooks( 'AuthPluginAutoCreate', array( $wgUser ) );
@@ -455,16 +482,16 @@ class LoginForm {
}
/**
- * Attempt to automatically create a user on login.
- * Only succeeds if there is an external authentication method which allows it.
+ * Attempt to automatically create a user on login. Only succeeds if there
+ * is an external authentication method which allows it.
* @return integer Status code
*/
function attemptAutoCreate( $user ) {
global $wgAuth, $wgUser;
/**
- * If the external authentication plugin allows it,
- * automatically create a new account for users that
- * are externally defined but have not yet logged in.
+ * If the external authentication plugin allows it, automatically cre-
+ * ate a new account for users that are externally defined but have not
+ * yet logged in.
*/
if ( !$wgAuth->autoCreate() ) {
return self::NOT_EXISTS;
@@ -502,14 +529,19 @@ class LoginForm {
}
$wgUser->setCookies();
+ // Reset the throttle
+ $key = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) );
+ global $wgMemc;
+ $wgMemc->delete( $key );
+
if( $this->hasSessionCookie() || $this->mSkipCookieCheck ) {
- /* Replace the language object to provide user interface in correct
- * language immediately on this first page load.
+ /* Replace the language object to provide user interface in
+ * correct language immediately on this first page load.
*/
global $wgLang, $wgRequest;
$code = $wgRequest->getVal( 'uselang', $wgUser->getOption( 'language' ) );
$wgLang = Language::factory( $code );
- return $this->successfulLogin( 'loginsuccess', $wgUser->getName() );
+ return $this->successfulLogin();
} else {
return $this->cookieRedirectCheck( 'login' );
}
@@ -524,7 +556,7 @@ class LoginForm {
break;
case self::NOT_EXISTS:
if( $wgUser->isAllowed( 'createaccount' ) ){
- $this->mainLoginForm( wfMsg( 'nosuchuser', htmlspecialchars( $this->mName ) ) );
+ $this->mainLoginForm( wfMsgWikiHtml( 'nosuchuser', htmlspecialchars( $this->mName ) ) );
} else {
$this->mainLoginForm( wfMsg( 'nosuchusershort', htmlspecialchars( $this->mName ) ) );
}
@@ -541,6 +573,9 @@ class LoginForm {
case self::CREATE_BLOCKED:
$this->userBlockedMessage();
break;
+ case self::THROTTLED:
+ $this->mainLoginForm( wfMsg( 'login-throttled' ) );
+ break;
default:
throw new MWException( "Unhandled case value" );
}
@@ -548,8 +583,8 @@ class LoginForm {
function resetLoginForm( $error ) {
global $wgOut;
- $wgOut->addWikiText( "<div class=\"errorbox\">$error</div>" );
- $reset = new PasswordResetForm( $this->mName, $this->mPassword );
+ $wgOut->addHTML( Xml::element('p', array( 'class' => 'error' ), $error ) );
+ $reset = new SpecialResetpass();
$reset->execute( null );
}
@@ -587,14 +622,15 @@ class LoginForm {
return;
}
if ( 0 == $u->getID() ) {
- $this->mainLoginForm( wfMsg( 'nosuchuser', $u->getName() ) );
+ $this->mainLoginForm( wfMsgWikiHtml( 'nosuchuser', htmlspecialchars( $u->getName() ) ) );
return;
}
# Check against password throttle
if ( $u->isPasswordReminderThrottled() ) {
global $wgPasswordReminderResendTime;
- # Round the time in hours to 3 d.p., in case someone is specifying minutes or seconds.
+ # Round the time in hours to 3 d.p., in case someone is specifying
+ # minutes or seconds.
$this->mainLoginForm( wfMsgExt( 'throttled-mailpassword', array( 'parsemag' ),
round( $wgPasswordReminderResendTime, 3 ) ) );
return;
@@ -618,20 +654,22 @@ class LoginForm {
* @private
*/
function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle', $emailText = 'passwordremindertext' ) {
- global $wgCookiePath, $wgCookieDomain, $wgCookiePrefix, $wgCookieSecure;
- global $wgServer, $wgScript;
+ global $wgServer, $wgScript, $wgUser;
if ( '' == $u->getEmail() ) {
return new WikiError( wfMsg( 'noemail', $u->getName() ) );
}
+ $ip = wfGetIP();
+ if( !$ip ) {
+ return new WikiError( wfMsg( 'badipaddress' ) );
+ }
+
+ wfRunHooks( 'User::mailPasswordInternal', array(&$wgUser, &$ip, &$u) );
$np = $u->randomPassword();
$u->setNewpassword( $np, $throttle );
$u->saveSettings();
- $ip = wfGetIP();
- if ( '' == $ip ) { $ip = '(Unknown)'; }
-
$m = wfMsg( $emailText, $ip, $u->getName(), $np, $wgServer . $wgScript );
$result = $u->sendMail( wfMsg( $emailTitle ), $m );
@@ -640,29 +678,66 @@ class LoginForm {
/**
- * @param string $msg Message key that will be shown on success
- * @param $params String: parameters for the above message
- * @param bool $auto Toggle auto-redirect to main page; default true
+ * Run any hooks registered for logins, then HTTP redirect to
+ * $this->mReturnTo (or Main Page if that's undefined). Formerly we had a
+ * nice message here, but that's really not as useful as just being sent to
+ * wherever you logged in from. It should be clear that the action was
+ * successful, given the lack of error messages plus the appearance of your
+ * name in the upper right.
+ *
* @private
*/
- function successfulLogin( $msg, $params, $auto = true ) {
- global $wgUser;
- global $wgOut;
+ function successfulLogin() {
+ global $wgUser, $wgOut;
- # Run any hooks; ignore results
+ # Run any hooks; display injected HTML if any, else redirect
+ $injected_html = '';
+ wfRunHooks('UserLoginComplete', array(&$wgUser, &$injected_html));
+ if( $injected_html !== '' ) {
+ $this->displaySuccessfulLogin( 'loginsuccess', $injected_html );
+ } else {
+ $titleObj = Title::newFromText( $this->mReturnTo );
+ if ( !$titleObj instanceof Title ) {
+ $titleObj = Title::newMainPage();
+ }
+
+ $wgOut->redirect( $titleObj->getFullURL() );
+ }
+ }
+
+ /**
+ * Run any hooks registered for logins, then display a message welcoming
+ * the user.
+ *
+ * @private
+ */
+ function successfulCreation() {
+ global $wgUser, $wgOut;
+
+ # Run any hooks; display injected HTML
$injected_html = '';
wfRunHooks('UserLoginComplete', array(&$wgUser, &$injected_html));
+ $this->displaySuccessfulLogin( 'welcomecreation', $injected_html );
+ }
+
+ /**
+ * Display a "login successful" page.
+ */
+ private function displaySuccessfulLogin( $msgname, $injected_html ) {
+ global $wgOut, $wgUser;
+
$wgOut->setPageTitle( wfMsg( 'loginsuccesstitle' ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setArticleRelated( false );
- $wgOut->addWikiMsgArray( $msg, $params );
- $wgOut->addHtml( $injected_html );
+ $wgOut->addWikiMsg( $msgname, $wgUser->getName() );
+ $wgOut->addHTML( $injected_html );
+
if ( !empty( $this->mReturnTo ) ) {
- $wgOut->returnToMain( $auto, $this->mReturnTo );
+ $wgOut->returnToMain( null, $this->mReturnTo );
} else {
- $wgOut->returnToMain( $auto );
+ $wgOut->returnToMain( null );
}
}
@@ -671,11 +746,12 @@ class LoginForm {
global $wgOut;
$wgOut->setPageTitle( wfMsg( 'permissionserrors' ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setArticleRelated( false );
$wgOut->addWikitext( $wgOut->formatPermissionsErrorMessage( $errors, 'createaccount' ) );
- // Stuff that might want to be added at the end. For example, instructions if blocked.
+ // Stuff that might want to be added at the end. For example, instruc-
+ // tions if blocked.
$wgOut->addWikiMsg( 'cantcreateaccount-nonblock-text' );
$wgOut->returnToMain( false );
@@ -694,7 +770,7 @@ class LoginForm {
# out.
$wgOut->setPageTitle( wfMsg( 'cantcreateaccounttitle' ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setArticleRelated( false );
$ip = wfGetIP();
@@ -713,8 +789,8 @@ class LoginForm {
*/
function mainLoginForm( $msg, $msgtype = 'error' ) {
global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail;
- global $wgCookiePrefix, $wgAuth, $wgLoginLanguageSelector;
- global $wgAuth, $wgEmailConfirmToEdit;
+ global $wgCookiePrefix, $wgLoginLanguageSelector;
+ global $wgAuth, $wgEmailConfirmToEdit, $wgCookieExpiration;
$titleObj = SpecialPage::getTitleFor( 'Userlogin' );
@@ -792,6 +868,7 @@ class LoginForm {
$template->set( 'useemail', $wgEnableEmail );
$template->set( 'emailrequired', $wgEmailConfirmToEdit );
$template->set( 'canreset', $wgAuth->allowPasswordChange() );
+ $template->set( 'canremember', ( $wgCookieExpiration > 0 ) );
$template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) or $this->mRemember );
# Prepare language selection links as needed
@@ -810,7 +887,7 @@ class LoginForm {
}
$wgOut->setPageTitle( wfMsg( 'userlogin' ) );
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setArticleRelated( false );
$wgOut->disallowUserJs(); // just in case...
$wgOut->addTemplate( $template );
@@ -832,9 +909,9 @@ class LoginForm {
/**
* Check if a session cookie is present.
*
- * This will not pick up a cookie set during _this_ request, but is
- * meant to ensure that the client is returning the cookie which was
- * set on a previous pass through the system.
+ * This will not pick up a cookie set during _this_ request, but is meant
+ * to ensure that the client is returning the cookie which was set on a
+ * previous pass through the system.
*
* @private
*/
@@ -850,7 +927,9 @@ class LoginForm {
global $wgOut;
$titleObj = SpecialPage::getTitleFor( 'Userlogin' );
- $check = $titleObj->getFullURL( 'wpCookieCheck='.$type );
+ $query = array( 'wpCookieCheck' => $type );
+ if ( $this->mReturnTo ) $query['returnto'] = $this->mReturnTo;
+ $check = $titleObj->getFullURL( $query );
return $wgOut->redirect( $check );
}
@@ -871,7 +950,7 @@ class LoginForm {
return $this->mainLoginForm( wfMsg( 'error' ) );
}
} else {
- return $this->successfulLogin( 'loginsuccess', $wgUser->getName() );
+ return $this->successfulLogin();
}
}
@@ -879,9 +958,7 @@ class LoginForm {
* @private
*/
function throttleHit( $limit ) {
- global $wgOut;
-
- $wgOut->addWikiMsg( 'acct_creation_throttle_hit', $limit );
+ $this->mainLoginForm( wfMsgExt( 'acct_creation_throttle_hit', array( 'parseinline' ), $limit ) );
}
/**
diff --git a/includes/specials/SpecialUserlogout.php b/includes/specials/SpecialUserlogout.php
index 137eadb4..3d497bd7 100644
--- a/includes/specials/SpecialUserlogout.php
+++ b/includes/specials/SpecialUserlogout.php
@@ -12,7 +12,7 @@ function wfSpecialUserlogout() {
$oldName = $wgUser->getName();
$wgUser->logout();
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
// Hook.
$injected_html = '';
diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php
index fd3c690b..ce0097b2 100644
--- a/includes/specials/SpecialUserrights.php
+++ b/includes/specials/SpecialUserrights.php
@@ -26,10 +26,14 @@ class UserrightsPage extends SpecialPage {
}
public function userCanExecute( $user ) {
+ return $this->userCanChangeRights( $user, false );
+ }
+
+ public function userCanChangeRights( $user, $checkIfSelf = true ) {
$available = $this->changeableGroups();
return !empty( $available['add'] )
or !empty( $available['remove'] )
- or ($this->isself and
+ or ( ( $this->isself || !$checkIfSelf ) and
(!empty( $available['add-self'] )
or !empty( $available['remove-self'] )));
}
@@ -65,7 +69,7 @@ class UserrightsPage extends SpecialPage {
if ($this->mTarget == $wgUser->getName())
$this->isself = true;
- if( !$this->userCanExecute( $wgUser ) ) {
+ if( !$this->userCanChangeRights( $wgUser, true ) ) {
// fixme... there may be intermediate groups we can mention.
global $wgOut;
$wgOut->showPermissionsErrorPage( array(
@@ -141,13 +145,8 @@ class UserrightsPage extends SpecialPage {
// Validate input set...
$changeable = $this->changeableGroups();
- if ($wgUser->getId() != 0 && $wgUser->getId() == $user->getId()) {
- $addable = array_merge($changeable['add'], $wgGroupsAddToSelf);
- $removable = array_merge($changeable['remove'], $wgGroupsRemoveFromSelf);
- } else {
- $addable = $changeable['add'];
- $removable = $changeable['remove'];
- }
+ $addable = array_merge( $changeable['add'], $this->isself ? $changeable['add-self'] : array() );
+ $removable = array_merge( $changeable['remove'], $this->isself ? $changeable['remove-self'] : array() );
$removegroup = array_unique(
array_intersect( (array)$removegroup, $removable ) );
@@ -289,7 +288,7 @@ class UserrightsPage extends SpecialPage {
function makeGroupNameList( $ids ) {
if( empty( $ids ) ) {
- return wfMsg( 'rightsnone' );
+ return wfMsgForContent( 'rightsnone' );
} else {
return implode( ', ', $ids );
}
@@ -329,14 +328,13 @@ class UserrightsPage extends SpecialPage {
* @return Array: Tuple of addable, then removable groups
*/
protected function splitGroups( $groups ) {
- global $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
- list($addable, $removable) = array_values( $this->changeableGroups() );
+ list($addable, $removable, $addself, $removeself) = array_values( $this->changeableGroups() );
$removable = array_intersect(
- array_merge($this->isself ? $wgGroupsRemoveFromSelf : array(), $removable),
+ array_merge( $this->isself ? $removeself : array(), $removable ),
$groups ); // Can't remove groups the user doesn't have
$addable = array_diff(
- array_merge($this->isself ? $wgGroupsAddToSelf : array(), $addable),
+ array_merge( $this->isself ? $addself : array(), $addable ),
$groups ); // Can't add groups the user does have
return array( $addable, $removable );
@@ -351,10 +349,8 @@ class UserrightsPage extends SpecialPage {
protected function showEditUserGroupsForm( $user, $groups ) {
global $wgOut, $wgUser, $wgLang;
- list( $addable, $removable ) = $this->splitGroups( $groups );
-
$list = array();
- foreach( $user->getGroups() as $group )
+ foreach( $groups as $group )
$list[] = self::buildGroupLink( $group );
$grouplist = '';
@@ -384,7 +380,7 @@ class UserrightsPage extends SpecialPage {
<tr>
<td></td>
<td class='mw-submit'>" .
- Xml::submitButton( wfMsg( 'saveusergroups' ), array( 'name' => 'saveusergroups' ) ) .
+ Xml::submitButton( wfMsg( 'saveusergroups' ), array( 'name' => 'saveusergroups', 'accesskey' => 's' ) ) .
"</td>
</tr>" .
Xml::closeElement( 'table' ) . "\n" .
@@ -510,10 +506,10 @@ class UserrightsPage extends SpecialPage {
/**
* Returns an array of the groups that the user can add/remove.
*
- * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
+ * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) )
*/
function changeableGroups() {
- global $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
+ global $wgUser;
if( $wgUser->isAllowed( 'userrights' ) ) {
// This group gives the right to modify everything (reverse-
@@ -533,8 +529,8 @@ class UserrightsPage extends SpecialPage {
$groups = array(
'add' => array(),
'remove' => array(),
- 'add-self' => $wgGroupsAddToSelf,
- 'remove-self' => $wgGroupsRemoveFromSelf);
+ 'add-self' => array(),
+ 'remove-self' => array() );
$addergroups = $wgUser->getEffectiveGroups();
foreach ($addergroups as $addergroup) {
@@ -543,7 +539,13 @@ class UserrightsPage extends SpecialPage {
);
$groups['add'] = array_unique( $groups['add'] );
$groups['remove'] = array_unique( $groups['remove'] );
+ $groups['add-self'] = array_unique( $groups['add-self'] );
+ $groups['remove-self'] = array_unique( $groups['remove-self'] );
}
+
+ // Run a hook because we can
+ wfRunHooks( 'UserrightsChangeableGroups', array( $this, $wgUser, $addergroups, &$groups ) );
+
return $groups;
}
@@ -551,12 +553,12 @@ class UserrightsPage extends SpecialPage {
* Returns an array of the groups that a particular group can add/remove.
*
* @param $group String: the group to check for whether it can add/remove
- * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
+ * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) )
*/
private function changeableByGroup( $group ) {
- global $wgAddGroups, $wgRemoveGroups;
+ global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
- $groups = array( 'add' => array(), 'remove' => array() );
+ $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
if( empty($wgAddGroups[$group]) ) {
// Don't add anything to $groups
} elseif( $wgAddGroups[$group] === true ) {
@@ -573,6 +575,40 @@ class UserrightsPage extends SpecialPage {
} elseif( is_array($wgRemoveGroups[$group]) ) {
$groups['remove'] = $wgRemoveGroups[$group];
}
+
+ // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
+ if( empty($wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) {
+ foreach($wgGroupsAddToSelf as $key => $value) {
+ if( is_int($key) ) {
+ $wgGroupsAddToSelf['user'][] = $value;
+ }
+ }
+ }
+
+ if( empty($wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) {
+ foreach($wgGroupsRemoveFromSelf as $key => $value) {
+ if( is_int($key) ) {
+ $wgGroupsRemoveFromSelf['user'][] = $value;
+ }
+ }
+ }
+
+ // Now figure out what groups the user can add to him/herself
+ if( empty($wgGroupsAddToSelf[$group]) ) {
+ } elseif( $wgGroupsAddToSelf[$group] === true ) {
+ // No idea WHY this would be used, but it's there
+ $groups['add-self'] = User::getAllGroups();
+ } elseif( is_array($wgGroupsAddToSelf[$group]) ) {
+ $groups['add-self'] = $wgGroupsAddToSelf[$group];
+ }
+
+ if( empty($wgGroupsRemoveFromSelf[$group]) ) {
+ } elseif( $wgGroupsRemoveFromSelf[$group] === true ) {
+ $groups['remove-self'] = User::getAllGroups();
+ } elseif( is_array($wgGroupsRemoveFromSelf[$group]) ) {
+ $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
+ }
+
return $groups;
}
@@ -583,7 +619,7 @@ class UserrightsPage extends SpecialPage {
* @param $output OutputPage to use
*/
protected function showLogFragment( $user, $output ) {
- $output->addHtml( Xml::element( 'h2', null, LogPage::logName( 'rights' ) . "\n" ) );
+ $output->addHTML( Xml::element( 'h2', null, LogPage::logName( 'rights' ) . "\n" ) );
LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage()->getPrefixedText() );
}
}
diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php
index 8c8e386d..29f527f2 100644
--- a/includes/specials/SpecialVersion.php
+++ b/includes/specials/SpecialVersion.php
@@ -1,42 +1,37 @@
<?php
-/**#@+
+
+/**
* Give information about the version of MediaWiki, PHP, the DB and extensions
*
- * @file
* @ingroup SpecialPage
*
* @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
* @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
*/
-
-/**
- * constructor
- */
-function wfSpecialVersion() {
- $version = new SpecialVersion;
- $version->execute();
-}
-
-/**
- * @ingroup SpecialPage
- */
-class SpecialVersion {
+class SpecialVersion extends SpecialPage {
private $firstExtOpened = true;
+ function __construct(){
+ parent::__construct( 'Version' );
+ }
+
/**
* main()
*/
- function execute() {
+ function execute( $par ) {
global $wgOut, $wgMessageCache, $wgSpecialVersionShowHooks;
$wgMessageCache->loadAllMessages();
+ $this->setHeaders();
+ $this->outputHeader();
+
$wgOut->addHTML( '<div dir="ltr">' );
$text =
$this->MediaWikiCredits() .
$this->softwareInformation() .
$this->extensionCredits();
- if ( $wgSpecialVersionShowHooks ) {
+ if ( $wgSpecialVersionShowHooks ) {
$text .= $this->wgHooks();
}
$wgOut->addWikiText( $text );
@@ -162,15 +157,21 @@ class SpecialVersion {
usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
foreach ( $wgExtensionCredits[$type] as $extension ) {
+ $version = null;
+ $subVersion = '';
if ( isset( $extension['version'] ) ) {
$version = $extension['version'];
- } elseif ( isset( $extension['svn-revision'] ) &&
+ }
+ if ( isset( $extension['svn-revision'] ) &&
preg_match( '/\$(?:Rev|LastChangedRevision|Revision): *(\d+)/',
- $extension['svn-revision'], $m ) )
- {
- $version = 'r' . $m[1];
- } else {
- $version = null;
+ $extension['svn-revision'], $m ) ) {
+ $subVersion = 'r' . $m[1];
+ }
+
+ if( $version && $subVersion ) {
+ $version = $version . ' [' . $subVersion . ']';
+ } elseif ( !$version && $subVersion ) {
+ $version = $subVersion;
}
$out .= $this->formatCredits(
@@ -287,8 +288,6 @@ class SpecialVersion {
}
/**
- * @static
- *
* @return string
*/
function IPInfo() {
@@ -306,35 +305,34 @@ class SpecialVersion {
if ( $cnt == 1 ) {
// Enforce always returning a string
- return (string)$this->arrayToString( $list[0] );
+ return (string)self::arrayToString( $list[0] );
} elseif ( $cnt == 0 ) {
return '';
} else {
+ global $wgLang;
sort( $list );
- $t = array_slice( $list, 0, $cnt - 1 );
- $one = array_map( array( &$this, 'arrayToString' ), $t );
- $two = $this->arrayToString( $list[$cnt - 1] );
- $and = wfMsg( 'and' );
-
- return implode( ', ', $one ) . " $and $two";
+ return $wgLang->listToText( array_map( array( __CLASS__, 'arrayToString' ), $list ) );
}
}
/**
- * @static
- *
* @param mixed $list Will convert an array to string if given and return
* the paramater unaltered otherwise
* @return mixed
*/
- function arrayToString( $list ) {
+ static function arrayToString( $list ) {
+ if( is_array( $list ) && count( $list ) == 1 )
+ $list = $list[0];
if( is_object( $list ) ) {
$class = get_class( $list );
return "($class)";
- } elseif ( ! is_array( $list ) ) {
+ } elseif ( !is_array( $list ) ) {
return $list;
} else {
- $class = get_class( $list[0] );
+ if( is_object( $list[0] ) )
+ $class = get_class( $list[0] );
+ else
+ $class = $list[0];
return "($class, {$list[1]})";
}
}
@@ -387,5 +385,3 @@ class SpecialVersion {
/**#@-*/
}
-
-/**#@-*/
diff --git a/includes/specials/SpecialWantedfiles.php b/includes/specials/SpecialWantedfiles.php
new file mode 100644
index 00000000..c2731fa9
--- /dev/null
+++ b/includes/specials/SpecialWantedfiles.php
@@ -0,0 +1,90 @@
+<?php
+/*
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Querypage that lists the most wanted files - implements Special:Wantedfiles
+ *
+ * @ingroup SpecialPage
+ *
+ * @author Soxred93 <soxred93@gmail.com>
+ * @copyright Copyright © 2008, Soxred93
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+class WantedFilesPage extends QueryPage {
+
+ function getName() {
+ return 'Wantedfiles';
+ }
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $imagelinks, $page ) = $dbr->tableNamesN( 'imagelinks', 'page' );
+ $name = $dbr->addQuotes( $this->getName() );
+ return
+ "
+ SELECT
+ $name as type,
+ " . NS_FILE . " as namespace,
+ il_to as title,
+ COUNT(*) as value
+ FROM $imagelinks
+ LEFT JOIN $page ON il_to = page_title AND page_namespace = ". NS_FILE ."
+ WHERE page_title IS NULL
+ GROUP BY il_to
+ ";
+ }
+
+ function sortDescending() { return true; }
+
+ /**
+ * Fetch user page links and cache their existence
+ */
+ function preprocessResults( $db, $res ) {
+ $batch = new LinkBatch;
+ while ( $row = $db->fetchObject( $res ) )
+ $batch->add( $row->namespace, $row->title );
+ $batch->execute();
+
+ // Back to start for display
+ if ( $db->numRows( $res ) > 0 )
+ // If there are no rows we get an error seeking.
+ $db->dataSeek( $res, 0 );
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getText() );
+
+ $plink = $this->isCached() ?
+ $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) :
+ $skin->makeBrokenLinkObj( $nt, htmlspecialchars( $text ) );
+
+ $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ return wfSpecialList($plink, $nlinks);
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialWantedFiles() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new WantedFilesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialWantedtemplates.php b/includes/specials/SpecialWantedtemplates.php
new file mode 100644
index 00000000..43b5cf8f
--- /dev/null
+++ b/includes/specials/SpecialWantedtemplates.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A querypage to list the most wanted templates - implements Special:Wantedtemplates
+ * based on SpecialWantedcategories.php by Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * makeWlhLink() taken from SpecialMostlinkedtemplates by Rob Church <robchur@gmail.com>
+ *
+ * @ingroup SpecialPage
+ *
+ * @author Danny B.
+ * @copyright Copyright © 2008, Danny B.
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+class WantedTemplatesPage extends QueryPage {
+
+ function getName() {
+ return 'Wantedtemplates';
+ }
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $templatelinks, $page ) = $dbr->tableNamesN( 'templatelinks', 'page' );
+ $name = $dbr->addQuotes( $this->getName() );
+ return
+ "
+ SELECT $name as type,
+ tl_namespace as namespace,
+ tl_title as title,
+ COUNT(*) as value
+ FROM $templatelinks LEFT JOIN
+ $page ON tl_title = page_title AND tl_namespace = page_namespace
+ WHERE page_title IS NULL AND tl_namespace = ". NS_TEMPLATE ."
+ GROUP BY tl_title
+ ";
+ }
+
+ function sortDescending() { return true; }
+
+ /**
+ * Fetch user page links and cache their existence
+ */
+ function preprocessResults( $db, $res ) {
+ $batch = new LinkBatch;
+ while ( $row = $db->fetchObject( $res ) )
+ $batch->add( $row->namespace, $row->title );
+ $batch->execute();
+
+ // Back to start for display
+ if ( $db->numRows( $res ) > 0 )
+ // If there are no rows we get an error seeking.
+ $db->dataSeek( $res, 0 );
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getText() );
+
+ $plink = $this->isCached() ?
+ $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) :
+ $skin->makeBrokenLinkObj( $nt, htmlspecialchars( $text ) );
+
+ $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ return wfSpecialList(
+ $plink,
+ $this->makeWlhLink( $nt, $skin, $result )
+ );
+ }
+
+ /**
+ * 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->link( $wlh, $label, array(), array( 'target' => $title->getPrefixedText() ) );
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialWantedTemplates() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new WantedTemplatesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php
index db7cd423..61dd6b3e 100644
--- a/includes/specials/SpecialWatchlist.php
+++ b/includes/specials/SpecialWatchlist.php
@@ -13,7 +13,6 @@ function wfSpecialWatchlist( $par ) {
global $wgUser, $wgOut, $wgLang, $wgRequest;
global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker;
global $wgEnotifWatchlist;
- $fname = 'wfSpecialWatchlist';
$skin = $wgUser->getSkin();
$specialTitle = SpecialPage::getTitleFor( 'Watchlist' );
@@ -22,8 +21,9 @@ function wfSpecialWatchlist( $par ) {
# Anons don't get a watchlist
if( $wgUser->isAnon() ) {
$wgOut->setPageTitle( wfMsg( 'watchnologin' ) );
- $llink = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogin' ), wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() );
- $wgOut->addHtml( wfMsgWikiHtml( 'watchlistanontext', $llink ) );
+ $llink = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogin' ),
+ wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() );
+ $wgOut->addHTML( wfMsgWikiHtml( 'watchlistanontext', $llink ) );
return;
}
@@ -40,40 +40,56 @@ function wfSpecialWatchlist( $par ) {
}
$uid = $wgUser->getId();
- if( ($wgEnotifWatchlist || $wgShowUpdatedMarker) && $wgRequest->getVal( 'reset' ) && $wgRequest->wasPosted() ) {
+ if( ($wgEnotifWatchlist || $wgShowUpdatedMarker) && $wgRequest->getVal( 'reset' ) &&
+ $wgRequest->wasPosted() )
+ {
$wgUser->clearAllNotifications( $uid );
$wgOut->redirect( $specialTitle->getFullUrl() );
return;
}
$defaults = array(
- /* float */ 'days' => floatval( $wgUser->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */
- /* bool */ 'hideOwn' => (int)$wgUser->getBoolOption( 'watchlisthideown' ),
- /* bool */ 'hideBots' => (int)$wgUser->getBoolOption( 'watchlisthidebots' ),
- /* bool */ 'hideMinor' => (int)$wgUser->getBoolOption( 'watchlisthideminor' ),
+ /* float */ 'days' => floatval( $wgUser->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */
+ /* bool */ 'hideMinor' => (int)$wgUser->getBoolOption( 'watchlisthideminor' ),
+ /* bool */ 'hideBots' => (int)$wgUser->getBoolOption( 'watchlisthidebots' ),
+ /* bool */ 'hideAnons' => (int)$wgUser->getBoolOption( 'watchlisthideanons' ),
+ /* bool */ 'hideLiu' => (int)$wgUser->getBoolOption( 'watchlisthideliu' ),
+ /* bool */ 'hidePatrolled' => (int)$wgUser->getBoolOption( 'watchlisthidepatrolled' ), // TODO
+ /* bool */ 'hideOwn' => (int)$wgUser->getBoolOption( 'watchlisthideown' ),
/* ? */ 'namespace' => 'all',
+ /* ? */ 'invert' => false,
);
extract($defaults);
# Extract variables from the request, falling back to user preferences or
# other default values if these don't exist
- $prefs['days' ] = floatval( $wgUser->getOption( 'watchlistdays' ) );
- $prefs['hideown' ] = $wgUser->getBoolOption( 'watchlisthideown' );
- $prefs['hidebots'] = $wgUser->getBoolOption( 'watchlisthidebots' );
+ $prefs['days'] = floatval( $wgUser->getOption( 'watchlistdays' ) );
$prefs['hideminor'] = $wgUser->getBoolOption( 'watchlisthideminor' );
+ $prefs['hidebots'] = $wgUser->getBoolOption( 'watchlisthidebots' );
+ $prefs['hideanons'] = $wgUser->getBoolOption( 'watchlisthideanon' );
+ $prefs['hideliu'] = $wgUser->getBoolOption( 'watchlisthideliu' );
+ $prefs['hideown' ] = $wgUser->getBoolOption( 'watchlisthideown' );
+ $prefs['hidepatrolled' ] = $wgUser->getBoolOption( 'watchlisthidepatrolled' );
# Get query variables
- $days = $wgRequest->getVal( 'days', $prefs['days'] );
- $hideOwn = $wgRequest->getBool( 'hideOwn', $prefs['hideown'] );
- $hideBots = $wgRequest->getBool( 'hideBots', $prefs['hidebots'] );
+ $days = $wgRequest->getVal( 'days' , $prefs['days'] );
$hideMinor = $wgRequest->getBool( 'hideMinor', $prefs['hideminor'] );
+ $hideBots = $wgRequest->getBool( 'hideBots' , $prefs['hidebots'] );
+ $hideAnons = $wgRequest->getBool( 'hideAnons', $prefs['hideanons'] );
+ $hideLiu = $wgRequest->getBool( 'hideLiu' , $prefs['hideliu'] );
+ $hideOwn = $wgRequest->getBool( 'hideOwn' , $prefs['hideown'] );
+ $hidePatrolled = $wgRequest->getBool( 'hidePatrolled' , $prefs['hidepatrolled'] );
# Get namespace value, if supplied, and prepare a WHERE fragment
$nameSpace = $wgRequest->getIntOrNull( 'namespace' );
+ $invert = $wgRequest->getIntOrNull( 'invert' );
if( !is_null( $nameSpace ) ) {
$nameSpace = intval( $nameSpace );
- $nameSpaceClause = " AND rc_namespace = $nameSpace";
+ if( $invert && $nameSpace !== 'all' )
+ $nameSpaceClause = "rc_namespace != $nameSpace";
+ else
+ $nameSpaceClause = "rc_namespace = $nameSpace";
} else {
$nameSpace = '';
$nameSpaceClause = '';
@@ -103,32 +119,24 @@ function wfSpecialWatchlist( $par ) {
// Dump everything here
$nondefaults = array();
- wfAppendToArrayIfNotDefault('days' , $days , $defaults, $nondefaults);
- wfAppendToArrayIfNotDefault('hideOwn' , (int)$hideOwn , $defaults, $nondefaults);
- wfAppendToArrayIfNotDefault('hideBots' , (int)$hideBots, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'days' , $days , $defaults, $nondefaults);
wfAppendToArrayIfNotDefault( 'hideMinor', (int)$hideMinor, $defaults, $nondefaults );
- wfAppendToArrayIfNotDefault('namespace', $nameSpace , $defaults, $nondefaults);
-
- $hookSql = "";
- if( ! wfRunHooks('BeforeWatchlist', array($nondefaults, $wgUser, &$hookSql)) ) {
- return;
- }
-
- if($nitems == 0) {
+ wfAppendToArrayIfNotDefault( 'hideBots' , (int)$hideBots , $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'hideAnons', (int)$hideAnons, $defaults, $nondefaults );
+ wfAppendToArrayIfNotDefault( 'hideLiu' , (int)$hideLiu , $defaults, $nondefaults );
+ wfAppendToArrayIfNotDefault( 'hideOwn' , (int)$hideOwn , $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'namespace', $nameSpace , $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'hidePatrolled', (int)$hidePatrolled, $defaults, $nondefaults );
+
+ if( $nitems == 0 ) {
$wgOut->addWikiMsg( 'nowatchlist' );
return;
}
- if ( $days <= 0 ) {
+ if( $days <= 0 ) {
$andcutoff = '';
} else {
- $andcutoff = "AND rc_timestamp > '".$dbr->timestamp( time() - intval( $days * 86400 ) )."'";
- /*
- $sql = "SELECT COUNT(*) AS n FROM $page, $revision WHERE rev_timestamp>'$cutoff' AND page_id=rev_page";
- $res = $dbr->query( $sql, $fname );
- $s = $dbr->fetchObject( $res );
- $npages = $s->n;
- */
+ $andcutoff = "rc_timestamp > '".$dbr->timestamp( time() - intval( $days * 86400 ) )."'";
}
# If the watchlist is relatively short, it's simplest to zip
@@ -140,128 +148,158 @@ function wfSpecialWatchlist( $par ) {
# Up estimate of watched items by 15% to compensate for talk pages...
# Toggles
- $andHideOwn = $hideOwn ? "AND (rc_user <> $uid)" : '';
- $andHideBots = $hideBots ? "AND (rc_bot = 0)" : '';
- $andHideMinor = $hideMinor ? 'AND rc_minor = 0' : '';
-
- # Show watchlist header
- $header = '';
- if( $wgUser->getOption( 'enotifwatchlistpages' ) && $wgEnotifWatchlist) {
- $header .= wfMsg( 'wlheader-enotif' ) . "\n";
- }
- if ( $wgShowUpdatedMarker ) {
- $header .= wfMsg( 'wlheader-showupdated' ) . "\n";
- }
-
- # Toggle watchlist content (all recent edits or just the latest)
+ $andHideOwn = $hideOwn ? "rc_user != $uid" : '';
+ $andHideBots = $hideBots ? "rc_bot = 0" : '';
+ $andHideMinor = $hideMinor ? "rc_minor = 0" : '';
+ $andHideLiu = $hideLiu ? "rc_user = 0" : '';
+ $andHideAnons = $hideAnons ? "rc_user != 0" : '';
+ $andHidePatrolled = $wgUser->useRCPatrol() && $hidePatrolled ? "rc_patrolled != 1" : '';
+
+ # Toggle watchlist content (all recent edits or just the latest)
if( $wgUser->getOption( 'extendwatchlist' )) {
$andLatest='';
- $limitWatchlist = 'LIMIT ' . intval( $wgUser->getOption( 'wllimit' ) );
+ $limitWatchlist = 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 . ') ';
- $limitWatchlist = '';
+ $andLatest = 'rc_this_oldid=page_latest OR rc_type=' . RC_LOG;
+ $limitWatchlist = 0;
}
- $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 ( $wgShowUpdatedMarker ) {
- $wgOut->addHTML( '<form action="' .
- $specialTitle->escapeLocalUrl() .
- '" method="post"><input type="submit" name="dummy" value="' .
- htmlspecialchars( wfMsg( 'enotif_reset' ) ) .
- '" /><input type="hidden" name="reset" value="all" /></form>' .
- "\n\n" );
+ # Create output form
+ $form = Xml::fieldset( wfMsg( 'watchlist-options' ), false, array( 'id' => 'mw-watchlist-options' ) );
+
+ # Show watchlist header
+ $form .= wfMsgExt( 'watchlist-details', array( 'parseinline' ), $wgLang->formatNum( $nitems ) );
+
+ if( $wgUser->getOption( 'enotifwatchlistpages' ) && $wgEnotifWatchlist) {
+ $form .= wfMsgExt( 'wlheader-enotif', 'parse' ) . "\n";
}
- if ( $wgShowUpdatedMarker ) {
- $wltsfield = ", ${watchlist}.wl_notificationtimestamp ";
- } else {
- $wltsfield = '';
+ if( $wgShowUpdatedMarker ) {
+ $form .= Xml::openElement( 'form', array( 'method' => 'post',
+ 'action' => $specialTitle->getLocalUrl(),
+ 'id' => 'mw-watchlist-resetbutton' ) ) .
+ wfMsgExt( 'wlheader-showupdated', array( 'parseinline' ) ) . ' ' .
+ Xml::submitButton( wfMsg( 'enotif_reset' ), array( 'name' => 'dummy' ) ) .
+ Xml::hidden( 'reset', 'all' ) .
+ Xml::closeElement( 'form' );
+ }
+ $form .= '<hr />';
+
+ $tables = array( 'recentchanges', 'watchlist', 'page' );
+ $fields = array( "{$recentchanges}.*" );
+ $conds = array();
+ $join_conds = array(
+ 'watchlist' => array('INNER JOIN',"wl_user='{$uid}' AND wl_namespace=rc_namespace AND wl_title=rc_title"),
+ 'page' => array('LEFT JOIN','rc_cur_id=page_id')
+ );
+ $options = array( 'ORDER BY' => 'rc_timestamp DESC' );
+ if( $wgShowUpdatedMarker ) {
+ $fields[] = 'wl_notificationtimestamp';
+ }
+ if( $limitWatchlist ) {
+ $options['LIMIT'] = $limitWatchlist;
}
- $sql = "SELECT ${recentchanges}.* ${wltsfield}
- FROM $watchlist,$recentchanges
- LEFT JOIN $page ON rc_cur_id=page_id
- WHERE wl_user=$uid
- AND wl_namespace=rc_namespace
- AND wl_title=rc_title
- $andcutoff
- $andLatest
- $andHideOwn
- $andHideBots
- $andHideMinor
- $nameSpaceClause
- $hookSql
- ORDER BY rc_timestamp DESC
- $limitWatchlist";
-
- $res = $dbr->query( $sql, $fname );
+ if( $andcutoff ) $conds[] = $andcutoff;
+ if( $andLatest ) $conds[] = $andLatest;
+ if( $andHideOwn ) $conds[] = $andHideOwn;
+ if( $andHideBots ) $conds[] = $andHideBots;
+ if( $andHideMinor ) $conds[] = $andHideMinor;
+ if( $andHideLiu ) $conds[] = $andHideLiu;
+ if( $andHideAnons ) $conds[] = $andHideAnons;
+ if( $andHidePatrolled ) $conds[] = $andHidePatrolled;
+ if( $nameSpaceClause ) $conds[] = $nameSpaceClause;
+
+ wfRunHooks('SpecialWatchlistQuery', array(&$conds,&$tables,&$join_conds,&$fields) );
+
+ $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $join_conds );
$numRows = $dbr->numRows( $res );
/* Start bottom header */
- $wgOut->addHTML( "<hr />\n" );
- if($days >= 1) {
- $wgOut->addHTML(
- wfMsgExt( 'rcnote', 'parseinline',
+ $wlInfo = '';
+ if( $days >= 1 ) {
+ $wlInfo = wfMsgExt( 'rcnote', 'parseinline',
$wgLang->formatNum( $numRows ),
$wgLang->formatNum( $days ),
$wgLang->timeAndDate( wfTimestampNow(), true ),
$wgLang->date( wfTimestampNow(), true ),
$wgLang->time( wfTimestampNow(), true )
- ) . '<br />'
- );
- } elseif($days > 0) {
- $wgOut->addHtml(
- wfMsgExt( 'wlnote', 'parseinline',
+ ) . '<br />';
+ } elseif( $days > 0 ) {
+ $wlInfo = wfMsgExt( 'wlnote', 'parseinline',
$wgLang->formatNum( $numRows ),
$wgLang->formatNum( round($days*24) )
- ) . '<br />'
- );
+ ) . '<br />';
}
- $wgOut->addHTML( "\n" . wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n" );
+ $cutofflinks = "\n" . wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n";
# Spit out some control panel links
$thisTitle = SpecialPage::getTitleFor( 'Watchlist' );
$skin = $wgUser->getSkin();
+ $showLinktext = wfMsgHtml( 'show' );
+ $hideLinktext = wfMsgHtml( 'hide' );
+ # Hide/show minor edits
+ $label = $hideMinor ? $showLinktext : $hideLinktext;
+ $linkBits = wfArrayToCGI( array( 'hideMinor' => 1 - (int)$hideMinor ), $nondefaults );
+ $links[] = wfMsgHtml( 'rcshowhideminor', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) );
+
# Hide/show bot edits
- $label = $hideBots ? wfMsgHtml( 'watchlist-show-bots' ) : wfMsgHtml( 'watchlist-hide-bots' );
+ $label = $hideBots ? $showLinktext : $hideLinktext;
$linkBits = wfArrayToCGI( array( 'hideBots' => 1 - (int)$hideBots ), $nondefaults );
- $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
+ $links[] = wfMsgHtml( 'rcshowhidebots', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) );
+
+ # Hide/show anonymous edits
+ $label = $hideAnons ? $showLinktext : $hideLinktext;
+ $linkBits = wfArrayToCGI( array( 'hideAnons' => 1 - (int)$hideAnons ), $nondefaults );
+ $links[] = wfMsgHtml( 'rcshowhideanons', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) );
+
+ # Hide/show logged in edits
+ $label = $hideLiu ? $showLinktext : $hideLinktext;
+ $linkBits = wfArrayToCGI( array( 'hideLiu' => 1 - (int)$hideLiu ), $nondefaults );
+ $links[] = wfMsgHtml( 'rcshowhideliu', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) );
# Hide/show own edits
- $label = $hideOwn ? wfMsgHtml( 'watchlist-show-own' ) : wfMsgHtml( 'watchlist-hide-own' );
+ $label = $hideOwn ? $showLinktext : $hideLinktext;
$linkBits = wfArrayToCGI( array( 'hideOwn' => 1 - (int)$hideOwn ), $nondefaults );
- $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
+ $links[] = wfMsgHtml( 'rcshowhidemine', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) );
- # Hide/show minor edits
- $label = $hideMinor ? wfMsgHtml( 'watchlist-show-minor' ) : wfMsgHtml( 'watchlist-hide-minor' );
- $linkBits = wfArrayToCGI( array( 'hideMinor' => 1 - (int)$hideMinor ), $nondefaults );
- $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
-
- $wgOut->addHTML( implode( ' | ', $links ) );
+ # Hide/show patrolled edits
+ if( $wgUser->useRCPatrol() ) {
+ $label = $hidePatrolled ? $showLinktext : $hideLinktext;
+ $linkBits = wfArrayToCGI( array( 'hidePatrolled' => 1 - (int)$hidePatrolled ), $nondefaults );
+ $links[] = wfMsgHtml( 'rcshowhidepatr', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) );
+ }
- # Form for namespace filtering
- $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $thisTitle->getLocalUrl() ) );
- $form .= '<p>';
+ # Namespace filter and put the whole form together.
+ $form .= $wlInfo;
+ $form .= $cutofflinks;
+ $form .= implode( ' | ', $links );
+ $form .= Xml::openElement( 'form', array( 'method' => 'post', 'action' => $thisTitle->getLocalUrl() ) );
+ $form .= '<hr /><p>';
$form .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;';
$form .= Xml::namespaceSelector( $nameSpace, '' ) . '&nbsp;';
+ $form .= Xml::checkLabel( wfMsg('invert'), 'invert', 'nsinvert', $invert ) . '&nbsp;';
$form .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . '</p>';
$form .= Xml::hidden( 'days', $days );
- if( $hideOwn )
- $form .= Xml::hidden( 'hideOwn', 1 );
- if( $hideBots )
- $form .= Xml::hidden( 'hideBots', 1 );
if( $hideMinor )
$form .= Xml::hidden( 'hideMinor', 1 );
+ if( $hideBots )
+ $form .= Xml::hidden( 'hideBots', 1 );
+ if( $hideAnons )
+ $form .= Xml::hidden( 'hideAnons', 1 );
+ if( $hideLiu )
+ $form .= Xml::hidden( 'hideLiu', 1 );
+ if( $hideOwn )
+ $form .= Xml::hidden( 'hideOwn', 1 );
$form .= Xml::closeElement( 'form' );
- $wgOut->addHtml( $form );
+ $form .= Xml::closeElement( 'fieldset' );
+ $wgOut->addHTML( $form );
# If there's nothing to show, stop here
if( $numRows == 0 ) {
@@ -316,7 +354,6 @@ function wfSpecialWatchlist( $par ) {
$dbr->freeResult( $res );
$wgOut->addHTML( $s );
-
}
function wlHoursLink( $h, $page, $options = array() ) {
@@ -370,7 +407,8 @@ function wlCountItems( &$user, $talk = true ) {
$dbr = wfGetDB( DB_SLAVE, 'watchlist' );
# Fetch the raw count
- $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->mId ), 'wlCountItems' );
+ $res = $dbr->select( 'watchlist', 'COUNT(*) AS count',
+ array( 'wl_user' => $user->mId ), 'wlCountItems' );
$row = $dbr->fetchObject( $res );
$count = $row->count;
$dbr->freeResult( $res );
diff --git a/includes/specials/SpecialWhatlinkshere.php b/includes/specials/SpecialWhatlinkshere.php
index 3502e33c..d91b4960 100644
--- a/includes/specials/SpecialWhatlinkshere.php
+++ b/includes/specials/SpecialWhatlinkshere.php
@@ -74,9 +74,7 @@ class WhatLinksHerePage {
$this->selfTitle = SpecialPage::getTitleFor( 'Whatlinkshere', $this->target->getPrefixedDBkey() );
$wgOut->setPageTitle( wfMsg( 'whatlinkshere-title', $this->target->getPrefixedText() ) );
- $wgOut->setSubtitle( wfMsgHtml( 'linklistsub' ) );
-
- $wgOut->addHTML( wfMsgExt( 'whatlinkshere-barrow', array( 'escapenoentities') ) . ' ' .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n");
+ $wgOut->setSubtitle( wfMsg( 'whatlinkshere-backlink', $this->skin->link( $this->target, $this->target->getPrefixedText(), array(), array( 'redirect' => 'no' ) ) ) );
$this->showIndirectLinks( 0, $this->target, $opts->getValue( 'limit' ),
$opts->getValue( 'from' ), $opts->getValue( 'back' ) );
@@ -98,7 +96,7 @@ class WhatLinksHerePage {
$hidelinks = $this->opts->getValue( 'hidelinks' );
$hideredirs = $this->opts->getValue( 'hideredirs' );
$hidetrans = $this->opts->getValue( 'hidetrans' );
- $hideimages = $target->getNamespace() != NS_IMAGE || $this->opts->getValue( 'hideimages' );
+ $hideimages = $target->getNamespace() != NS_FILE || $this->opts->getValue( 'hideimages' );
$fetchlinks = (!$hidelinks || !$hideredirs);
@@ -169,11 +167,13 @@ class WhatLinksHerePage {
if( ( !$fetchlinks || !$dbr->numRows($plRes) ) && ( $hidetrans || !$dbr->numRows($tlRes) ) && ( $hideimages || !$dbr->numRows($ilRes) ) ) {
if ( 0 == $level ) {
$wgOut->addHTML( $this->whatlinkshereForm() );
- $errMsg = is_int($namespace) ? 'nolinkshere-ns' : 'nolinkshere';
- $wgOut->addWikiMsg( $errMsg, $this->target->getPrefixedText() );
+
// Show filters only if there are links
if( $hidelinks || $hidetrans || $hideredirs || $hideimages )
$wgOut->addHTML( $this->getFilterPanel() );
+
+ $errMsg = is_int($namespace) ? 'nolinkshere-ns' : 'nolinkshere';
+ $wgOut->addWikiMsg( $errMsg, $this->target->getPrefixedText() );
}
return;
}
@@ -256,7 +256,7 @@ class WhatLinksHerePage {
}
protected function listStart() {
- return Xml::openElement( 'ul' );
+ return Xml::openElement( 'ul', array ( 'id' => 'mw-whatlinkshere-list' ) );
}
protected function listItem( $row, $nt, $notClose = false ) {
@@ -267,7 +267,7 @@ class WhatLinksHerePage {
'whatlinkshere-links', 'isimage' );
$msgcache = array();
foreach ( $msgs as $msg ) {
- $msgcache[$msg] = wfMsgHtml( $msg );
+ $msgcache[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) );
}
}
@@ -377,6 +377,8 @@ class WhatLinksHerePage {
$f .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;' .
Xml::namespaceSelector( $namespace, '' );
+ $f .= ' ';
+
# Submit
$f .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
@@ -395,7 +397,7 @@ class WhatLinksHerePage {
$links = array();
$types = array( 'hidetrans', 'hidelinks', 'hideredirs' );
- if( $this->target->getNamespace() == NS_IMAGE )
+ if( $this->target->getNamespace() == NS_FILE )
$types[] = 'hideimages';
foreach( $types as $type ) {
$chosen = $this->opts->getValue( $type );
diff --git a/includes/templates/NoLocalSettings.php b/includes/templates/NoLocalSettings.php
index 75a7e95a..5f7e93c7 100644
--- a/includes/templates/NoLocalSettings.php
+++ b/includes/templates/NoLocalSettings.php
@@ -10,12 +10,31 @@ if ( isset( $wgVersion ) ) {
} else {
$wgVersion = 'VERSION';
}
-# Set the path in case we hit a page such as /index.php/Main_Page
-# Could use <base href> but then we have to worry about http[s]/port #/etc.
-$ext = strpos( $_SERVER['SCRIPT_NAME'], 'index.php5' ) === false ? 'php' : 'php5';
+
+$scriptName = $_SERVER['SCRIPT_NAME'];
+$ext = substr( $scriptName, strrpos( $scriptName, "." ) + 1 );
$path = '';
-if( isset( $_SERVER['SCRIPT_NAME'] )) {
- $path = htmlspecialchars( preg_replace('/index.php5?/', '', $_SERVER['SCRIPT_NAME']) );
+# Add any directories in the main folder that could contain an entrypoint (even possibly).
+# We cannot just do a dir listing here, as we do not know where it is yet
+# These must not also be the names of subfolders that may contain an entrypoint
+$topdirs = array( 'extensions', 'includes' );
+foreach( $topdirs as $dir ){
+ # Check whether a directory by this name is in the path
+ if( strrpos( $scriptName, "/" . $dir . "/" ) ){
+ # If so, check whether it is the right folder
+ # First, get the number of directories up it is (to generate path)
+ $numToGoUp = substr_count( substr( $scriptName, strrpos( $scriptName, "/" . $dir . "/" ) + 1 ), "/" );
+ # And generate the path using ..'s
+ for( $i = 0; $i < $numToGoUp; $i++ ){
+ $realPath = "../" . $realPath;
+ }
+ # Checking existance (using the image here as it is something not likely to change, and to always be here)
+ if( file_exists( $realPath . "skins/common/images/mediawiki.png" ) ) {
+ # If so, get the path that we can use in this file, and stop looking
+ $path = substr( $scriptName, 0, strrpos( $scriptName, "/" . $dir . "/" ) + 1 );
+ break;
+ }
+ }
}
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
diff --git a/includes/templates/PHP4.php b/includes/templates/PHP4.php
new file mode 100644
index 00000000..058351a0
--- /dev/null
+++ b/includes/templates/PHP4.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * @file
+ * @ingroup Templates
+ */
+
+if( !defined( 'MW_PHP4' ) ) {
+ die( "Not an entry point.");
+}
+
+if( isset( $_SERVER['SCRIPT_NAME'] ) ) {
+ // Probably IIS; doesn't set REQUEST_URI
+ $scriptUrl = $_SERVER['SCRIPT_NAME'];
+} elseif( isset( $_SERVER['REQUEST_URI'] ) ) {
+ // We're trying SCRIPT_NAME first because it won't include PATH_INFO... hopefully
+ $scriptUrl = $_SERVER['REQUEST_URI'];
+} else {
+ $scriptUrl = '';
+}
+if ( preg_match( '!^(.*)/config/[^/]*.php$!', $scriptUrl, $m ) ) {
+ $baseUrl = $m[1];
+} elseif ( preg_match( '!^(.*)/[^/]*.php$!', $scriptUrl, $m ) ) {
+ $baseUrl = $m[1];
+} else {
+ $baseUrl = dirname( $scriptUrl );
+}
+
+?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>
+ <head>
+ <title>MediaWiki <?php echo htmlspecialchars( $wgVersion ); ?></title>
+ <meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
+ <style type='text/css' media='screen, projection'>
+ html, body {
+ color: #000;
+ background-color: #fff;
+ font-family: sans-serif;
+ text-align: center;
+ }
+
+ p {
+ text-align: left;
+ margin-left: 2em;
+ margin-right: 2em;
+ }
+
+ h1 {
+ font-size: 150%;
+ }
+ </style>
+ </head>
+ <body>
+ <img src="<?php echo htmlspecialchars( $baseUrl ) ?>/skins/common/images/mediawiki.png" alt='The MediaWiki logo' />
+
+ <h1>MediaWiki <?php echo htmlspecialchars( $wgVersion ); ?></h1>
+ <div class='error'>
+<p>
+ MediaWiki requires PHP 5.0.0 or higher. You are running PHP
+ <?php echo htmlspecialchars( phpversion() ); ?>.
+</p>
+<?php
+flush();
+/**
+ * Test the *.php5 extension
+ */
+$downloadOther = true;
+if ( $baseUrl ) {
+ $testUrl = "$wgServer$baseUrl/php5.php5";
+ if( function_exists( 'file_get_contents' ) ) {
+ $errorLevel = error_reporting();
+ error_reporting( $errorLevel & !E_WARNING );
+
+ ini_set( 'allow_url_fopen', '1' );
+ $s = file_get_contents( $testUrl );
+
+ error_reporting( $errorLevel );
+ }
+
+ if ( strpos( $s, 'yes' ) !== false ) {
+ $encUrl = htmlspecialchars( str_replace( '.php', '.php5', $scriptUrl ) );
+ echo "<p>You may be able to use MediaWiki using a <a href=\"$encUrl\">.php5</a> file extension.</p>";
+ $downloadOther = false;
+ }
+}
+if ( $downloadOther ) {
+?>
+<p>Please consider
+<a href="http://www.php.net/downloads.php">upgrading your copy of PHP</a>.
+PHP 4 is at the end of its lifecycle and will not receive further security updates.</p>
+<p>If for some reason you really really need to run MediaWiki on PHP 4, you will need to
+<a href="http://www.mediawiki.org/wiki/Download">download version 1.6.x</a>
+from our website. </p>
+<?php
+}
+?>
+
+ </div>
+ </body>
+</html>
diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php
index deeeb274..c4a60b6c 100644
--- a/includes/templates/Userlogin.php
+++ b/includes/templates/Userlogin.php
@@ -16,7 +16,7 @@ class UserloginTemplate extends QuickTemplate {
?>
<div class="<?php $this->text('messagetype') ?>box">
<?php if ( $this->data['messagetype'] == 'error' ) { ?>
- <h2><?php $this->msg('loginerror') ?>:</h2>
+ <h2><?php $this->msg('loginerror') ?></h2>
<?php } ?>
<?php $this->html('message') ?>
</div>
@@ -54,7 +54,7 @@ class UserloginTemplate extends QuickTemplate {
$doms .= "<option>" . htmlspecialchars( $dom ) . "</option>";
}
?>
- <tr>
+ <tr id="mw-user-domain-section">
<td class="mw-label"><?php $this->msg( 'yourdomainname' ) ?></td>
<td class="mw-input">
<select name="wpDomain" value="<?php $this->text( 'domain' ) ?>"
@@ -63,7 +63,8 @@ class UserloginTemplate extends QuickTemplate {
</select>
</td>
</tr>
- <?php } ?>
+ <?php }
+ if( $this->data['canremember'] ) { ?>
<tr>
<td></td>
<td class="mw-input">
@@ -74,6 +75,7 @@ class UserloginTemplate extends QuickTemplate {
/> <label for="wpRemember"><?php $this->msg('remembermypassword') ?></label>
</td>
</tr>
+ <?php } ?>
<tr>
<td></td>
<td class="mw-submit">
@@ -111,7 +113,7 @@ class UsercreateTemplate extends QuickTemplate {
?>
<div class="<?php $this->text('messagetype') ?>box">
<?php if ( $this->data['messagetype'] == 'error' ) { ?>
- <h2><?php $this->msg('loginerror') ?>:</h2>
+ <h2><?php $this->msg('loginerror') ?></h2>
<?php } ?>
<?php $this->html('message') ?>
</div>
@@ -196,6 +198,7 @@ class UsercreateTemplate extends QuickTemplate {
</td>
<?php } ?>
</tr>
+ <?php if( $this->data['canremember'] ) { ?>
<tr>
<td></td>
<td class="mw-input">
@@ -206,7 +209,8 @@ class UsercreateTemplate extends QuickTemplate {
/> <label for="wpRemember"><?php $this->msg('remembermypassword') ?></label>
</td>
</tr>
-<?php
+<?php }
+
$tabIndex = 8;
if ( isset( $this->data['extraInput'] ) && is_array( $this->data['extraInput'] ) ) {
foreach ( $this->data['extraInput'] as $inputItem ) { ?>
diff --git a/includes/zhtable/simpphrases.manual b/includes/zhtable/simpphrases.manual
index 8e754b7f..60d0861c 100644
--- a/includes/zhtable/simpphrases.manual
+++ b/includes/zhtable/simpphrases.manual
@@ -16,13 +16,13 @@
乾红
乾乾
乾清宫
-乾象;
-乾宅;
-乾造;
-乾曜;
-乾元;
-乾卦;
-李乾德;
+乾象
+乾宅
+乾造
+乾曜
+乾元
+乾卦
+李乾德
挨着
爱着
暗着
@@ -374,6 +374,10 @@
写著作
写著名
遇着
+杀着
+杀著名
+杀著作
+杀著者
於乎
於戏
魏徵
diff --git a/includes/zhtable/toCN.manual b/includes/zhtable/toCN.manual
index bc2222f4..feeca9dc 100644
--- a/includes/zhtable/toCN.manual
+++ b/includes/zhtable/toCN.manual
@@ -5,6 +5,7 @@
記憶體 内存
預設 默认
串列 串行
+串列加速器 串列加速器
乙太網 以太网
點陣圖 位图
常式 例程
@@ -109,150 +110,92 @@
簡訊 短信
烏茲別克 乌兹别克斯坦
查德 乍得
-乍得 乍得
-也門
葉門 也门
伯利茲 伯利兹
貝里斯 伯利兹
維德角 佛得角
-佛得角 佛得角
-克羅地亞 克罗地亚
克羅埃西亞 克罗地亚
-岡比亞 冈比亚
甘比亞 冈比亚
-幾內亞比紹 几内亚比绍
幾內亞比索 几内亚比绍
列支敦斯登 列支敦士登
-列支敦士登 列支敦士登
-利比里亞 利比里亚
賴比瑞亞 利比里亚
-加納 加纳
迦納 加纳
加彭 加蓬
-加蓬 加蓬
-博茨瓦納 博茨瓦纳
波札那 博茨瓦纳
-卡塔爾 卡塔尔
卡達 卡塔尔
-盧旺達 卢旺达
盧安達 卢旺达
-危地馬拉 危地马拉
瓜地馬拉 危地马拉
厄瓜多爾 厄瓜多尔
+厄瓜多尔 厄瓜多尔
厄瓜多 厄瓜多尔
-厄立特里亞 厄立特里亚
厄利垂亞 厄立特里亚
-吉布堤 吉布提
吉布地 吉布提
哈薩克 哈萨克斯坦
-哥斯達黎加 哥斯达黎加
哥斯大黎加 哥斯达黎加
-圖瓦盧 图瓦卢
吐瓦魯 图瓦卢
土庫曼 土库曼斯坦
-聖盧西亞 圣卢西亚
聖露西亞 圣卢西亚
聖吉斯納域斯 圣基茨和尼维斯
聖克里斯多福及尼維斯 圣基茨和尼维斯
-聖文森特和格林納丁斯 圣文森特和格林纳丁斯
聖文森及格瑞那丁 圣文森特和格林纳丁斯
-聖馬力諾 圣马力诺
聖馬利諾 圣马力诺
-圭亞那 圭亚那
蓋亞那 圭亚那
-坦桑尼亞 坦桑尼亚
坦尚尼亞 坦桑尼亚
-埃塞俄比亞 埃塞俄比亚
衣索匹亞 埃塞俄比亚
衣索比亞 埃塞俄比亚
吉里巴斯 基里巴斯
-基里巴斯 基里巴斯
塔吉克 塔吉克斯坦
塞拉利昂 塞拉利昂
塞普勒斯 塞浦路斯
-塞浦路斯 塞浦路斯
-塞舌爾 塞舌尔
塞席爾 塞舌尔
-多明尼加共和國 多米尼加
-多明尼加 多米尼加
-多明尼加聯邦 多米尼加联邦
-多米尼克 多米尼加联邦
-安提瓜和巴布達 安提瓜和巴布达
+多米尼克 多米尼加国
安地卡及巴布達 安提瓜和巴布达
尼日利亞 尼日利亚
+尼日利亚 尼日利亚
奈及利亞 尼日利亚
尼日爾 尼日尔
+尼日尔 尼日尔
尼日 尼日尔
巴貝多 巴巴多斯
-巴巴多斯 巴巴多斯
-巴布亞新畿內亞 巴布亚新几内亚
巴布亞紐幾內亞 巴布亚新几内亚
布基納法索 布基纳法索
布吉納法索 布基纳法索
蒲隆地 布隆迪
-布隆迪 布隆迪
-希臘 希腊
帛琉 帕劳
義大利 意大利
-意大利 意大利
-所羅門群島 所罗门群岛
索羅門群島 所罗门群岛
汶萊 文莱
-斯威士蘭 斯威士兰
史瓦濟蘭 斯威士兰
-斯洛文尼亞 斯洛文尼亚
斯洛維尼亞 斯洛文尼亚
-新西蘭 新西兰
紐西蘭 新西兰
-格林納達 格林纳达
格瑞那達 格林纳达
-格魯吉亞 乔治亚
-喬治亞 乔治亚
-梵蒂岡 梵蒂冈
-毛里塔尼亞 毛里塔尼亚
茅利塔尼亞 毛里塔尼亚
毛里裘斯 毛里求斯
模里西斯 毛里求斯
沙地阿拉伯 沙特阿拉伯
沙烏地阿拉伯 沙特阿拉伯
-波斯尼亞黑塞哥維那 波斯尼亚和黑塞哥维那
波士尼亞赫塞哥維納 波斯尼亚和黑塞哥维那
-津巴布韋 津巴布韦
辛巴威 津巴布韦
宏都拉斯 洪都拉斯
-洪都拉斯 洪都拉斯
-特立尼達和多巴哥 特立尼达和托巴哥
千里達托貝哥 特立尼达和托巴哥
-瑙魯 瑙鲁
諾魯 瑙鲁
-瓦努阿圖 瓦努阿图
萬那杜 瓦努阿图
溫納圖 瓦努阿图
-科摩羅 科摩罗
葛摩 科摩罗
象牙海岸 科特迪瓦
突尼西亞 突尼斯
-索馬里 索马里
索馬利亞 索马里
-老撾 老挝
寮國 老挝
肯雅 肯尼亚
肯亞 肯尼亚
蘇利南 苏里南
莫三比克 莫桑比克
-莫桑比克 莫桑比克
-萊索托 莱索托
賴索托 莱索托
-貝寧 贝宁
貝南 贝宁
-贊比亞 赞比亚
尚比亞 赞比亚
亞塞拜然 阿塞拜疆
-阿塞拜疆 阿塞拜疆
-阿拉伯聯合酋長國 阿拉伯联合酋长国
阿拉伯聯合大公國 阿拉伯联合酋长国
南韓 韩国
-馬爾代夫 马尔代夫
馬爾地夫 马尔代夫
馬爾他 马耳他
馬利共和國 马里共和国
@@ -262,7 +205,7 @@
泡麵 方便面
笨豬跳 蹦极跳
绑紧跳 蹦极跳
-冷盤   凉菜
+冷盤 凉菜
冷菜 凉菜
散钱 零钱
谐星 笑星
@@ -290,16 +233,12 @@
積架 捷豹
福斯 大众
福士 大众
-雪鐵龍 雪铁龙
萬事得 马自达
-馬自達 马自达
寶獅 标志
拿破崙 拿破仑
布殊 布什
布希 布什
柯林頓 克林顿
-克林頓 克林顿
-薩達姆 萨达姆
海珊 萨达姆
梵谷 凡高
大衛碧咸 大卫·贝克汉姆
diff --git a/includes/zhtable/toHK.manual b/includes/zhtable/toHK.manual
index 9afddb77..916b4020 100644
--- a/includes/zhtable/toHK.manual
+++ b/includes/zhtable/toHK.manual
@@ -6,25 +6,17 @@
凶殘 兇殘
緝凶 緝兇
買凶 買兇
-打印机 打印機
印表機 打印機
字节 位元組
字節 位元組
-打印 打印
列印 打印
硬件 硬件
硬體 硬件
-二极管 二極管
二極體 二極管
-三极管 三極管
三極體 三極管
-数码 數碼
數位 數碼
-软件 軟件
軟體 軟件
-网络 網絡
網路 網絡
-人工智能 人工智能
人工智慧 人工智能
航天飞机 穿梭機
太空梭 穿梭機
@@ -34,141 +26,85 @@
機器人 機械人
移动电话 流動電話
行動電話 流動電話
-调制解调器 調制解調器
數據機 調制解調器
短信 短訊
簡訊 短訊
-乍得 乍得
查德 乍得
-也门 也門
葉門 也門
-伯利兹 伯利茲
貝里斯 伯利茲
-佛得角 佛得角
維德角 佛得角
-克罗地亚 克羅地亞
克羅埃西亞 克羅地亞
-冈比亚 岡比亞
甘比亞 岡比亞
-几内亚比绍 幾內亞比紹
幾內亞比索 幾內亞比紹
-列支敦士登 列支敦士登
列支敦斯登 列支敦士登
-利比里亚 利比里亞
賴比瑞亞 利比里亞
-加纳 加納
迦納 加納
-加蓬 加蓬
加彭 加蓬
-博茨瓦纳 博茨瓦納
波札那 博茨瓦納
-卡塔尔 卡塔爾
卡達 卡塔爾
-卢旺达 盧旺達
盧安達 盧旺達
-危地马拉 危地馬拉
瓜地馬拉 危地馬拉
厄瓜多尔 厄瓜多爾
+厄瓜多爾 厄瓜多爾
厄瓜多 厄瓜多爾
-厄立特里亚 厄立特里亞
厄利垂亞 厄立特里亞
-吉布提 吉布堤
吉布地 吉布堤
-哥斯达黎加 哥斯達黎加
哥斯大黎加 哥斯達黎加
-图瓦卢 圖瓦盧
吐瓦魯 圖瓦盧
-圣卢西亚 聖盧西亞
聖露西亞 聖盧西亞
圣基茨和尼维斯 聖吉斯納域斯
聖克里斯多福及尼維斯 聖吉斯納域斯
-圣文森特和格林纳丁斯 聖文森特和格林納丁斯
聖文森及格瑞那丁 聖文森特和格林納丁斯
-圣马力诺 聖馬力諾
聖馬利諾 聖馬力諾
-圭亚那 圭亞那
蓋亞那 圭亞那
-坦桑尼亚 坦桑尼亞
坦尚尼亞 坦桑尼亞
-埃塞俄比亚 埃塞俄比亞
衣索匹亞 埃塞俄比亞
衣索比亞 埃塞俄比亞
-基里巴斯 基里巴斯
吉里巴斯 基里巴斯
-狮子山 獅子山
塞普勒斯 塞浦路斯
-塞舌尔 塞舌爾
塞席爾 塞舌爾
-多米尼加 多明尼加共和國
-多明尼加 多明尼加共和國
-多米尼加联邦 多明尼加聯邦
-多米尼克 多明尼加聯邦
-安提瓜和巴布达 安提瓜和巴布達
+多米尼克 多明尼加國
安地卡及巴布達 安提瓜和巴布達
尼日利亚 尼日利亞
+尼日利亞 尼日利亞
奈及利亞 尼日利亞
尼日尔 尼日爾
+尼日爾 尼日爾
尼日 尼日爾
-巴巴多斯 巴巴多斯
巴貝多 巴巴多斯
-巴布亚新几内亚 巴布亞新畿內亞
巴布亞紐幾內亞 巴布亞新畿內亞
-布基纳法索 布基納法索
布吉納法索 布基納法索
-布隆迪 布隆迪
蒲隆地 布隆迪
+帕劳 帛琉
義大利 意大利
-所罗门群岛 所羅門群島
索羅門群島 所羅門群島
-斯威士兰 斯威士蘭
+文莱 汶萊
史瓦濟蘭 斯威士蘭
-斯洛文尼亚 斯洛文尼亞
斯洛維尼亞 斯洛文尼亞
-新西兰 新西蘭
紐西蘭 新西蘭
-格林纳达 格林納達
格瑞那達 格林納達
-格鲁吉亚 喬治亞
-格魯吉亞 喬治亞
-梵蒂冈 梵蒂岡
-毛里塔尼亚 毛里塔尼亞
茅利塔尼亞 毛里塔尼亞
毛里求斯 毛里裘斯
模里西斯 毛里裘斯
+沙地阿拉伯 沙特阿拉伯
沙烏地阿拉伯 沙特阿拉伯
-波斯尼亚和黑塞哥维那 波斯尼亞黑塞哥維那
波士尼亞赫塞哥維納 波斯尼亞黑塞哥維那
-津巴布韦 津巴布韋
辛巴威 津巴布韋
-洪都拉斯 洪都拉斯
宏都拉斯 洪都拉斯
-特立尼达和托巴哥 特立尼達和多巴哥
千里達托貝哥 特立尼達和多巴哥
-瑙鲁 瑙魯
諾魯 瑙魯
-瓦努阿图 瓦努阿圖
萬那杜 瓦努阿圖
-科摩罗 科摩羅
葛摩 科摩羅
-索马里 索馬里
索馬利亞 索馬里
-老挝 老撾
寮國 老撾
肯尼亚 肯雅
肯亞 肯雅
-莫桑比克 莫桑比克
莫三比克 莫桑比克
-莱索托 萊索托
賴索托 萊索托
-贝宁 貝寧
貝南 貝寧
-赞比亚 贊比亞
尚比亞 贊比亞
-阿塞拜疆 阿塞拜疆
亞塞拜然 阿塞拜疆
-阿拉伯联合酋长国 阿拉伯聯合酋長國
阿拉伯聯合大公國 阿拉伯聯合酋長國
-马尔代夫 馬爾代夫
馬爾地夫 馬爾代夫
馬利共和國 馬里共和國
方便面 即食麵
@@ -200,11 +136,9 @@
拿破崙 拿破侖
布什 布殊
布希 布殊
-克林顿 克林頓
柯林頓 克林頓
萨达姆 薩達姆
海珊 侯賽因
-侯赛因 侯賽因
大卫·贝克汉姆 大衛碧咸
迈克尔·欧文 米高奧雲
珍妮弗·卡普里亚蒂 卡佩雅蒂
diff --git a/includes/zhtable/toSG.manual b/includes/zhtable/toSG.manual
index 5f7cb0ca..3c0cbc1d 100644
--- a/includes/zhtable/toSG.manual
+++ b/includes/zhtable/toSG.manual
@@ -5,6 +5,7 @@
方便面 快速面
速食麵 快速面
即食麵 快速面
+泡麵 快速面
蹦极跳 绑紧跳
笨豬跳 绑紧跳
凉菜 冷菜
@@ -16,4 +17,3 @@
民乐 华乐
住房 住屋
房价 屋价
-泡麵 快速面
diff --git a/includes/zhtable/toTW.manual b/includes/zhtable/toTW.manual
index 13a81a69..b0041ccf 100644
--- a/includes/zhtable/toTW.manual
+++ b/includes/zhtable/toTW.manual
@@ -21,6 +21,7 @@
復蘇 復甦
缺省 預設
串行 串列
+串列加速器 串列加速器
以太网 乙太網
位图 點陣圖
例程 常式
@@ -85,7 +86,6 @@
服务器 伺服器
等于 等於
局域网 區域網
-计算机 電腦
扫瞄仪 掃瞄器
宽带 寬頻
数据库 資料庫
@@ -143,7 +143,6 @@
伯利兹 貝里斯
伯利茲 貝里斯
佛得角 維德角
-佛得角 維德角
克罗地亚 克羅埃西亞
克羅地亞 克羅埃西亞
冈比亚 甘比亞
@@ -201,10 +200,11 @@
塞浦路斯 塞普勒斯
塞舌尔 塞席爾
塞舌爾 塞席爾
-多米尼加 多明尼加
+多米尼加共和国 多明尼加
+多米尼加共和國 多明尼加
多明尼加共和國 多明尼加
-多米尼加联邦 多米尼克
-多明尼加聯邦 多米尼克
+多米尼加国 多米尼克
+多明尼加國 多米尼克
安提瓜和巴布达 安地卡及巴布達
安提瓜和巴布達 安地卡及巴布達
尼日利亚 奈及利亞
@@ -212,17 +212,14 @@
尼日尔 尼日
尼日爾 尼日
巴巴多斯 巴貝多
-巴巴多斯 巴貝多
巴布亚新几内亚 巴布亞紐幾內亞
巴布亞新畿內亞 巴布亞紐幾內亞
布基纳法索 布吉納法索
布基納法索 布吉納法索
布隆迪 蒲隆地
布隆迪 蒲隆地
-希腊 希臘
帕劳 帛琉
意大利 義大利
-意大利 義大利
所罗门群岛 索羅門群島
所羅門群島 索羅門群島
文莱 汶萊
@@ -276,7 +273,6 @@
赞比亚 尚比亞
贊比亞 尚比亞
阿塞拜疆 亞塞拜然
-阿塞拜疆 亞塞拜然
阿拉伯联合酋长国 阿拉伯聯合大公國
阿拉伯聯合酋長國 阿拉伯聯合大公國
马尔代夫 馬爾地夫
@@ -307,7 +303,6 @@
積架 捷豹
福士 福斯
雪铁龙 雪鐵龍
-马自达 馬自達
萬事得 馬自達
拿破仑 拿破崙
拿破侖 拿破崙
diff --git a/includes/zhtable/tradphrases.manual b/includes/zhtable/tradphrases.manual
index 9caa3cc5..02d07d20 100644
--- a/includes/zhtable/tradphrases.manual
+++ b/includes/zhtable/tradphrases.manual
@@ -14,6 +14,7 @@
千隻
萬隻
億隻
+多只是
多隻
0多隻
零多隻
@@ -308,7 +309,6 @@
豐濱
豐濱鄉
象徵著
-這么著
這麼著
那麼著
配合著
@@ -327,7 +327,6 @@
披頭散髮
髮禁
鬥著
-鬧著玩儿
鬧著玩兒
鯰魚
世界盃
@@ -996,6 +995,10 @@
藥籤
萬籤插架
雲笈七籤
+上簽名
+上簽字
+上簽收
+上簽寫
犖确
磽确
确瘠
diff --git a/includes/zhtable/tradphrases_exclude.manual b/includes/zhtable/tradphrases_exclude.manual
index 40dc5c09..0db69513 100644
--- a/includes/zhtable/tradphrases_exclude.manual
+++ b/includes/zhtable/tradphrases_exclude.manual
@@ -72,6 +72,7 @@
白麵
切麵
和麵
+過水麵
復甦
複蘇
甦醒
@@ -131,6 +132,7 @@
採邑
嚮日
佔城
+水錶
名錶
錶面
彆腳